类和对象(二)
一、默认成员函数
什么是默认成员函数:用户不自己显示写,编译器默认生成的成员函数就是默认成员函数。
下面是 8 个默认成员函数
- 默认构造函数:用于创建对象时初始化对象。如果没有为类定义构造函数,编译器会自动生成一个默认构造函数。
- 析构函数:用于在对象生命周期结束时进行清理工作。如果没有定义析构函数,编译器也会提供一个默认的析构函数。
- 拷贝构造函数:用于创建一个新对象,该对象是其参数(另一个同类型对象)的副本。如果没有显式定义,编译器会生成一个默认的拷贝构造函数。
- 拷贝赋值运算符:用于将一个对象的值赋给另一个同类型的对象。如果没有显式定义,编译器会生成一个默认的拷贝赋值运算符。
- 移动构造函数:C++11 引入,用于通过移动而非复制的方式转移资源。如果没有定义,编译器会生成一个默认的移动构造函数(仅当类中没有定义任何构造函数时)。
- 移动赋值运算符:C++11 引入,用于通过移动而非复制的方式转移资源给另一个对象。如果没有定义,编译器会生成一个默认的移动赋值运算符(仅当类中没有定义任何赋值运算符时)。
- 类型转换运算符:允许类的对象被用作其他类型。例如,可以将类的一个对象用作内置类型(如int或double)。
- 虚析构函数:当类包含虚函数时,析构函数通常被声明为虚函数,以支持多态删除。虽然这不是一个默认成员函数,但它是与虚拟函数相关的一个重要概念。
其中 5,6 为 C++ 11 加入,7,8 不是很重要,因此下面介绍前四个。
对于编译器自己生成的函数我们需要了默认生成的函数的行为是什么,能否满足我们的需要,如果不满足自己显示写该如何写。
(一)、构造函数
- 构造函数并不是构造一个对象,它的作用类似于 stack 的 Init 函数,是完成对象的初始化工作。
- 构造函数的特点(也可以说是定义的方式)
- 函数名和类名相同。
- 没有返回值。
- 可以重载。
- 对象定义的时候自动调用。
- 用户不自己写编译器会自己生成。
- 编译器生成构造函数的行为
- 内置类型不做处理。
- 自定义类型调用他们的构造函数。
- 无参、全缺省、编译器自动生成的这三位构造函数叫做默认构造函数(其实就是不传参的),如果用户在定义对象的时候什么都不传编译器会自动调用默认构造函数,前两个和编译器生成的那个不能同时存在,无参和全缺省的虽然构成函数重载但是调用会存在歧义。
#include <iostream>using namespace std;class Date
{
public:Date(int year = 1, int month = 1, int day = 1){_year = year;_month = month;_day = day;}void print(){cout << _year << " " << _month << " " << _day << endl;}
private:int _year;int _month;int _day;
};int main()
{//Date d1(); // 不能这样定义,会和函数声明分不清Date d1;d1.print();Date d2(2025, 11, 12);d2.print();return 0;
}
(二)、析构函数
- 析构函数的作用也不是销毁一个对象,而是在对象声明周期结束的时候清理对象中所调用的资源。
- 析构函数的特点(定义方式)
- 函数名为:~类名。
- 没有参数。
- 没有返回值。
- 对象声明周期结束的时候自动调用。
- 不写编译器会自动生成,其行为对于内置类型不做处理,对于自定义类型调用他们的析构函数。一般没有资源占用的可以不写,编译器生成的就够用了,但是像 stack 这样的需要申请堆上的空间的类需要自己实现来完成资源的释放。
- C++ 规定对于多个对象,后定义的先析构。
#include <iostream>
#include <stdlib.h>
using namespace std;class Stack
{
public:Stack(int n = 4){cout << "Stack(int n = 4)" << endl;_a = (int*)malloc(sizeof(int) * n);_capacity = n;_size = 0;}~Stack(){cout << "~Stack()" << endl;free(_a);_a = nullptr;_capacity = 0;_size = 0;}private:int* _a;int _size;int _capacity;
};class Date
{
public:Date(int year = 1, int month = 1, int day = 1){// 构造函数会先调用自定义类型的构造函数cout << "Date(int year = 1, int month = 1, int day = 1)" << endl;_year = year;_month = month;_day = day;}~Date(){// 析构函数后调用自定义类型的析构函数cout << "~Date" << endl;// 内置类型不做处理// 自定义类型调用他们的析构函数}void print(){cout << _year << " " << _month << " " << _day << endl;}
private:int _year;int _month;int _day;Stack st;
};int main()
{Date d1;return 0;
}
(三)、拷贝构造
- 拷贝构造的功能是用另外一个对象创建初始化一个新的对象。
- 拷贝构造是构造函数的一个重载函数,它的参数固定为自身类型的重载,外加一些有缺省值的参数。
- 如果参数不是自身类型的引用,传值传参或者传值返回会调用拷贝构造,这样就会递归下去,因此这样写不合法。
- 若为显示定义拷贝构造编译器会自动生成,对内置类型完成浅拷贝工作,自定义类型调用他们的拷贝构造。
- 对于一些指向资源的类型,单单的浅拷贝会导致这些成员指向同一块空间,这时需要深拷贝来为成员变量重新申请空间。
#include <iostream>using namespace std;class Date
{
public:// 构造函数Date(int year = 1, int month = 1, int day = 1, int n = 4) {_year = year;_month = month;_day = day;_size = n;_a = (int*)malloc(n * sizeof(int));}void print(){cout << _year << '/' << _month << '/' << _day << '/' << endl;}// 拷贝构造//Date(const Date d) // 这样写会无穷递归Date(const Date& d){_year = d._year;_month = d._month;_day = d._day;_size = d._size;// 针对于指向资源成员的拷贝不能用浅拷贝,因为资源是相互独立的// 可以为这个对象重新分配资源_a = (int*)malloc(d._size * sizeof(int));}~Date(){free(_a);_a = nullptr;}private:int _year;int _month;int _day;int* _a;int _size;
};int main()
{Date d1(2025, 1, 1); // 构造Date d2(d1); // 拷贝构造Date d3 = d1; // 拷贝构造 return 0;
}
(四)、赋值运算符重载
1. 运算符重载
- C 语言的运算符对于自定义类型的不可用,C++ 允许用户自定义这些运算符的行为,使其满足用户自己逻辑上的行为,这个过程就叫做运算符的重载。
- 运算符重载是具有特殊名字的函数,它的名字是由operator和后面要定义的运算符共同构成。和其他函数一样,它也具有返回类型和参数列表以及函数体。
- 运算符重载的参数数量要求和操作数的数量一致,两个或以上的操作数要求运算符使用是参数的位置和参数列表的位置严格匹配。
- 如果把运算符重载函数定义在类的外面会导致私有的成员变量无法访问,可以通过函数返回成员变量拷贝、友元、重载成成员函数的方式解决,一般会选择最后一种。
- 成员函数会默认传 this 指针,因此第一个运算对象会传给 this,重载时参数的数量为运算数数量减一。
- 运算符重载后,优先级和结合性和原来保持一致。
- 运算符重载仅支持重载存在的运算符,不支持创建新的运算符。
- .* **:: sizeof ?: . **不支持重载。
- 重载的运算符必须有一个自定义类型,也就是说不能重载内置类型的运算符(你不能比祖师爷更懂运算符!!!)。
- 重载前置++ 和 后置++ 时,为了区分,后置++ 用一个类型为 int 的形参强行构成重载。
- 重载 << 和 >> 时,需要重载成全局函数,因为流插入和提取的方向时指向流对象的,如果重载成成员函数第一个参数为 this 指向就反了(如果你是左撇子当我没说)。
2. 赋值运算符重载
赋值运算符重载用于已经存在的两个对象的拷贝。
- 赋值运算符重载是一个运算符重载,规定必须为成员函数。赋值运算符重载的参数建议为const 自身类型的引用, 减少传参拷贝。
- 有返回值,建议写成自身类型的引用,这样就既可以减少拷贝,又能支持连续的赋值。
- 没有显示写,编译器生成的赋值运算符重载的行为是对内置类型不做处理,自定义类型调用他们的赋值运算符重载。
- 还是那句话,如果类对象有指向什么资源就自己实现,否则编译器生成的就够用。
(五)、取地址运算符重载
- 这个成员函数编译器默认生成的就够用,普通成员函数的 this 指针类型为 Date const * ,即指针的指向不能改变。
- const 成员变量的 this 类型为 const Date * const ,指向的内容也不能改变。
- 由于 this 不能显示写,如果想限制 this 指向的内容不能修改,要在成员函数参数列表的后面加上 const,这种成员函数叫做 const 成员函数。
二、日期类的实现
#include "Date.h"Date::Date(int year, int month, int day)
{_year = year;_month = month;_day = day;if (!checkdate()){cout << "非法日期:" << *this << endl;}
}
Date::Date(const Date& d)
{_year = d._year;_month = d._month;_day = d._day;
}
void Date::print() const
{cout << _year << '/' << _month << '/' << _day << endl;
}bool Date::checkdate() const
{return (_month >= 1 && _month <= 12 && _day >= 1 && _day <= get_monthday());
}bool Date::operator==(const Date& d) const
{return (_year == d._year)&& (_month == d._month)&& (_day == d._day);
}
bool Date::operator!=(const Date& d) const
{return !(*this == d);
}
bool Date::operator<(const Date& d) const
{if (_year != d._year) return _year < d._year;if (_month != d._month) return _month < d._month;if (_day != d._day) return _day < d._day;return false;
}
bool Date::operator<=(const Date& d) const
{return (*this < d) || (*this == d);
}
bool Date::operator>(const Date& d) const
{return !(*this <= d);
}
bool Date::operator>=(const Date& d) const
{return !(*this < d);
}Date& Date::operator+=(const int day)
{if (day < 0) return *this -= -day;_day += day;int monthday = -1;while (_day > (monthday = this->get_monthday())){_day -= monthday;_month++;if (_month == 13){_month = 1;_year++;}monthday = this->get_monthday();}return *this;
}
Date Date::operator+(const int day) const
{Date tmp(*this);tmp += day;return tmp;
}Date& Date::operator-=(const int day)
{if (day < 0) return *this += -day;_day -= day;int monthday = -1;while (_day <= 0){_month--;if (_month <= 0){_year--;_month = 12;}monthday = this->get_monthday();_day += monthday;}return *this;
}
Date Date::operator-(const int day) const
{Date tmp(*this);tmp -= day;return tmp;
}Date& Date::operator--()
{*this -= 1;return *this;
}
Date Date::operator--(int)
{Date tmp(*this);*this -= 1;return tmp;
}
Date& Date::operator++()
{*this += 1;return *this;
}
Date Date::operator++(int)
{Date tmp(*this);*this += 1;return tmp;
}int Date::operator-(Date& d) const
{Date tmp1(*this);if (tmp1 < d) return -(d - tmp1);// d > *thisint day = 0;while (d != tmp1){day++;tmp1--;}return day;
}ostream& operator<<(ostream& out, const Date& d)
{out << d._year << "年" << d._month << "月" << d._day << "日" << endl;return out;
}istream& operator>>(istream& in, Date& d)
{cout << "请输入日期:" << endl;while (1){in >> d._year >> d._month >> d._day;if (d.checkdate()){break;}else{cout << "非法日期请重新输入" << endl;}}return in;
}
#pragma once
#include <iostream>
#include <assert.h>
using namespace std;class Date
{// 友元函数声明// 允许类外函数访问类的私有成员friend ostream& operator<<(ostream& out, const Date& d);friend istream& operator>>(istream& in, Date& d);public:Date(int year = 1, int month = 1, int day = 1);Date(const Date& d);void print() const;bool checkdate() const;int get_monthday() const{assert(_year > 0 && _month <= 12 && _month >= 1);static int monthday[13] = { -1, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31 };if (_month == 2 && ((_year % 4 == 0 && _year % 100 != 0) || (_year % 400 == 0))) return 29;return monthday[_month];}//// 取地址运算符重载//Date* operator&()//{// return this;//}//const Date* operator&() const//{// return this;//}bool operator==(const Date& d) const;bool operator!=(const Date& d) const;bool operator<(const Date& d) const;bool operator<=(const Date& d) const;bool operator>(const Date& d) const;bool operator>=(const Date& d) const;Date& operator+=(const int day);Date operator+(const int day) const;Date& operator-=(const int day);Date operator-(const int day) const;int operator-(Date& d) const;Date& operator--(); // 前置Date operator--(int); // 后置Date& operator++();Date operator++(int);private:int _year;int _month;int _day;
};ostream& operator<<(ostream& out, const Date& d);
istream& operator>>(istream& in, Date& d);