目录
九、类的 6 个默认成员函数
默认成员函数:用户没有显式实现,编译器会生成的成员函数称为默认成员函数。
一个类,即使是空类,也有6个默认成员函数:构造函数(Constructor)、析构函数(Destructor)、拷贝构造函数(Copy Constructor)、赋值运算符(Assignment Operator)、取地址操作符重载及const取地址操作符重载。
十、构造函数
10.1 概念
构造函数是一个特殊的成员函数,名字与类名相同,创建类类型对象时由编译器自动调用,以保证每个数据成员都有一个合适的初始值,并且在对象整个生命周期内只调用一次。
构造函数也在公共代码区。
10.2 特性
构造函数是特殊的成员函数,需要注意的是,构造函数虽然名称叫构造,但是构造函数的主要任务并不是开空间创建对象,而是初始化对象。
其特征如下:
1. 函数名与类名相同。
2. 无返回值。(不需要写void)
3. 对象实例化时编译器自动调用对应的构造函数。
4. 构造函数可以重载。(可以写多个构造函数)
class Date
{
public:
Date()//无参构造函数
{
//可以对其初始化
_year = 0;
_month = 0;
_day = 0;
}
Date(int year, int month, int day)//有参构造函数
{
_year = year;
_month = month;
_day = day;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
Date d1;//调用无参构造函数
Date d2(2023, 10, 21);//调用有参构造函数
return 0;
}
注意,调用无参构造函数不能写成Date d1(); 因为编译器会将这看作函数的声明,所以必须去掉括号。
也可以将这两个构造函数合并成一个全缺省构造函数,更加方便:
Date(int year = 0, int month = 0, int day = 0)
{
_year = year;
_month = month;
_day = day;
}
如果声明和定义分离,缺省值要在声明给,定义不给。
5. 如果类中没有显式定义构造函数,则C++编译器会自动生成一个无参的默认构造函数,一旦用户显式定义编译器将不再生成。
当不写构造函数时,编译器会默认生成一个构造函数。写了就不会生成了。
6. 编译生成的默认构造的特点:
- 内置类型(int、double...)不会处理(有些编译器处理)
- 自定义类型(struct、class..)会处理,调用这个成员的构造函数。
C++11 中针对内置类型成员不初始化的缺陷,又打了补丁,即:内置类型成员变量在类中声明时可以给默认值。
例如:
class Date
{
private:
// 基本类型(内置类型)
int _year;//声明没有给缺省值,如果在构造函数中没有给值,那么就是随机值
int _month = 1;//可在声明时给缺省值
int _day = 1;
// 自定义类型
Time _t;//自动调用其构造函数
};
7. 无参的构造函数和全缺省的构造函数都称为默认构造函数,并且默认构造函数只能有一个。
注意:无参构造函数、全缺省构造函数、我们没写编译器默认生成的构造函数,都可以认为是默认构造函数。
即不传参就可以调用的构造就是默认构造函数。
十一、析构函数
11.1 概念
与构造函数功能相反,析构函数不是完成对对象本身的销毁,局部对象销毁工作是由编译器完成的。而对象在销毁时会自动调用析构函数,完成对象中资源的清理工作。
析构函数写在类中要是public,否则无法调用。
11.2 特性
析构函数是特殊的成员函数,其特征如下:
1. 析构函数名是在类名前加上字符 ~。
2. 无参数,无返回值类型。
3. 一个类只能有一个析构函数。若未显式定义,系统会自动生成默认的析构函数。注意:析构函数不能重载。
4. 对象生命周期结束时,C++编译系统系统自动调用析构函数。
以日期类举例:对象d在销毁时自动调用了析构函数
实际上日期类是无需写析构函数的,因为它的成员变量都是内置类型成员,那么什么情况下需要写呢?
- 当一个类被作为基类并且该基类对派生类的对象进行操作时,防止只析构基类而不析构派生类的状况发生。这种情况下,把基类的析构函数设计为虚函数可以在基类的指针指向派生类对象时,用基类的指针删除派生类对象,避免内存泄漏。
- 当需要释放动态分配的内存时,析构函数可以用来释放这些内存。比如写一个栈等。
5. 关于编译器自动生成的析构函数,对内置类型成员不会处理,对自定类型成员会调用它的析构函数。
class Time
{
public:
~Time()
{
cout << "~Time()" << endl;
}
};
class Date
{
private:
int _year;
int _month;
int _day;
Time _t;//销毁时自动调佣析构函数
};
int main()
{
Date d;
return 0;
}
我们知道是Date的默认析构函数调用了Tine析构函数,但当我们写了一个Date析构函数替换默认析构函数并且在其中没有写调用Time析构函数的代码时,Time的析构函数依旧调用了:
这是为什么呢?
这是因为 Time 析构函数的调用是隐式的,也就是说,它是由编译器自动插入的,而不是由你手动写的。并且我们发现,先打印了Date后打印了Time,说明编译器是在你的析构函数的最后一行之前插入一些代码,用来调用 Date 类的所有成员对象和基类对象的析构函数。
6. 在同一个生命周期内,先定义的对象后被析构,后定义的对象先被析构。
这是因为对象的创建和销毁遵循了栈的数据结构,后进先出。如果先定义了Date d1后定义Date d2,那么就会先析构d2,后析构d1。
十二、拷贝构造函数
12.1 概念
拷贝构造函数(copy constructor)是C++中的一个特殊构造函数,用于创建一个新对象,该对象的内容是另一个已存在的对象的复制。
拷贝构造函数只有单个形参,该形参是对本类类型对象的引用(一般常用const修饰),在用已存在的类类型对象创建新对象时,由编译器自动调用。
12.3 特性
1. 拷贝构造函数是构造函数的一个重载形式。
2. 拷贝构造函数的参数只有一个且必须是类类型对象的引用,使用传值方式编译器直接报错,因为会引发无穷递归调用。
在c语言中,结构体类型的变量可以通过赋值的方式来拷贝,这种方式为浅拷贝,例如:
struct A {
int a;
int* data;
};
int main() {
A d1;
d1.data = (int*)malloc(sizeof(int) * 4);
A d2 = d1;
return 0;
}
但是这种方式的拷贝放到C++中有一个弊端,我们知道变量销毁时会调用析构函数,会释放资源申请的内存,然而浅拷贝d2.data是将d1.data的值复值过来,即它们指向的是同一块地址。那么d2调用析构函数先释放了空间,d1再调用析构函数释放空间就会报错,因为这块空间已经被释放过了。
所以C++引入了拷贝构造函数来创建新对象解决这个问题,对d2开一块和d1等大的空间。无论是使用拷贝构造形式还是赋值形式都会调用拷贝构造函数。
这样一来又有一个问题,如果参数传值,就会引发无穷递归调用。因为参数传值会调用拷贝构造函数,然而拷贝构造函数中的参数又会调用拷贝构造函数...
class Date
{
public:
Date(int year = 1900, int month = 1, int day = 1)
{
_year = year;
_month = month;
_day = day;
}
// Date(const Date& d) // 正确写法
Date(const Date d) // 错误写法:编译报错,会引发无穷递归
{
_year = d._year;
_month = d._month;
_day = d._day;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
Date d1;
Date d2(d1);
return 0;
}
所以拷贝构造函数的参数只能传引用,引用也不会调用析构函数。
3. 若未显式定义,编译器会生成默认的拷贝构造函数。 默认的拷贝构造函数对象按内存存储按字节序完成拷贝,这种拷贝叫做浅拷贝,或者值拷贝。
注意:在编译器生成的默认拷贝构造函数中,内置类型是按照字节方式直接拷贝的,而自定义类型是调用其拷贝构造函数完成拷贝的。
这里的浅拷贝就和上面提及的浅拷贝一样,只会把值复值过来,相当于c语言中的赋值拷贝。因此类中如果没有涉及资源申请时,拷贝构造函数是否写都可以;一旦涉及到资源申请
时,则拷贝构造函数是一定要写的,否则就是浅拷贝。涉及资源申请时一定要显示定义拷贝构造函数,要用以后说的深拷贝去解决。一个类中只有内置类型和自定义类型而没有资源申请就可以不写,即使这个自定义类型中有资源申请,这个类的默认拷贝构造函数就也会调用它其中的自定义类型的拷贝构造函数。
class Time
{
public:
Time()
{
_hour = 1;
_minute = 1;
_second = 1;
}
Time(const Time& t)
{
_hour = t._hour;
_minute = t._minute;
_second = t._second;
cout << "Time::Time(const Time&)" << endl;
}
private:
int _hour;
int _minute;
int _second;
};
class Date
{
private:
// 基本类型(内置类型)
int _year = 1970;
int _month = 1;
int _day = 1;
// 自定义类型
Time _t;
};
int main()
{
Date d1;
// 用已经存在的d1拷贝构造d2,此处会调用Date类的拷贝构造函数
// 但Date类并没有显式定义拷贝构造函数,则编译器会给Date类生成一个默认的拷贝构造函数
Date d2(d1);
return 0;
}
4. 拷贝构造函数典型调用场景:
- 使用已存在对象创建新对象
- 函数参数类型为类类型对象
- 函数返回值类型为类类型对象
为了提高程序效率,一般对象传参时,尽量使用引用类型,返回时根据实际场景,能用引用尽量使用引用。
十三、赋值运算符重载
13.1 运算符重载
C++为了增强代码的可读性引入了运算符重载,运算符重载是具有特殊函数名的函数,也具有其返回值类型,函数名字以及参数列表,其返回值类型与参数列表与普通的函数类似。
函数名字为:关键字operator后面接需要重载的运算符符号。
函数原型:返回值类型 operator操作符(参数列表)
注意:
- 不能通过连接其他符号来创建新的操作符:比如operator@
- 重载操作符必须有一个类类型参数
- 用于内置类型的运算符,其含义不能改变,例如:内置的整型+,不 能改变其含义
- 不能改变操作符的操作数个数,一个操作符原本是几个操作数,那么重载的时候就有几个参数;
- 作为类成员函数重载时,其形参看起来比操作数数目少1,因为成员函数的第一个参数为隐藏的this
- .* :: sizeof ?: . ,以上5个运算符不能重载。
例如,对一个日期类进行比较,判断哪个日期小,直接比肯定是不行的:
class Date
{
public:
Date(int year = 1900, int month = 1, int day = 1)
{
_year = year;
_month = month;
_day = day;
}
Date(const Date& d)
{
_year = d._year;
_month = d._month;
_day = d._day;
}
int _year;
int _month;
int _day;
};
int main()
{
Date d1(2023, 11, 8);
Date d2(2023, 10, 4);
cout << (d1 < d2) << endl;//报错:没有与这些操作数匹配的 "<" 运算符
return 0;
}
所以这里就要用到重载,对'<'进行重载,返回值类型为bool,函数名为operator<,两个参数:
bool operator<(const Date& d1, const Date& d2)
{
if (d1._year < d2._year)
return true;
else if (d1._year == d2._year && d1._month < d2._month)
return true;
else if (d1._year == d2._year && d1._month == d2._month && d1._day < d2._day)
return true;
else return false;
}
int main()
{
Date d1(2023, 11, 8);
Date d2(2023, 10, 4);
cout << (d1 < d2) << endl;//cout << operator<(d1,d2) << endl;
return 0;
}
这里的d1 < d2会被自动转化为operator<(d1,d2),也可以直接写后一种方式。
但是我们会发现类成员变量是私有的,不能直接使用,为了保证封装性,我们可以把它写成类成员函数,需要注意的是,作为类成员函数时形参看起来要少一个,因为有一个是隐藏的this指针,(*this)就相当于原本的d1,d相当于d2。
示例:
class Date
{
//...构造函数等与上面一致,这里就省略了
bool operator<(const Date& d)
{
if (_year < d._year)
return true;
else if (_year == d._year &&_month < d._month)
return true;
else if (_year == d._year && _month == d._month && _day < d._day)
return true;
else return false;
}
//判断日期是否相等
bool operator==(const Date& d)
{
return _year == d._year && _month == d._month && _day == d._day;
}
//也可以复用写好的函数重载别的运算符来重载别的运算符
bool operator!=(const Date& d)
{
return !(*this == d);
}
bool operator>(const Date& d)
{
return !((*this) < d || (*this) == d);
}
bool operator>=(const Date& d)
{
return !((*this) < d);
}
//...
//
//某年某月的天数,下面要用到这个函数
int GetMonthDay(int year, int month)//获取某一年中某个月的天数
{
int monthArray[13] = { 0,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 monthArray[month];
}
//n天后的日期
Date& operator+=(const int n)
{
if(n < 0)//特殊情况
{
return *this -= (-n);
}
_day += n;
while (_day > GetMonthDay(_year, _month))
{
_day -= GetMonthDay(_year, _month);
_month++;
if (_month > 12)
{
_year++;
_month = 1;
}
}
return *this;
}
//n天前的日期
Date& operator-=(const int n)
{
if(n < 0)
{
return *this += (-n);
}
_day -= n;
while (_day <= 0)
{
_month--;
if (_month == 0)
{
_month = 12;
_year--;
}
_day += GetMonthDay(_year, _month);
}
return *this;
}
Date operator-(const int n)
{
Date tmp(*this);//与-=不同的是,-是创建了一个新对象,原来的日期还在,所以要拷贝一份
tmp -= n;//调用-=
return tmp;
}
//operator+同理
//计算两个日期之间相隔的天数 *this - d
int operator-(const Date d)
{
//默认*this > d
Date max = *this;
Date min = d;
int flag = 1;
if (*this < d)
{
max = d;
min = *this;
flag = -1;
}
int n = 0;
while (min != max)
{
++min;
++n;
}
return n * flag;
}
//...
}
13.2 赋值运算符重载
赋值运算符重载是运算符重载里的一种,它是默认成员函数,不写会自动生成。
1. 赋值运算符重载格式
- 参数类型:const T&,传递引用可以提高传参效率
- 返回值类型:T&,返回引用可以提高返回的效率,有返回值目的是为了支持连续赋值
- 检测是否自己给自己赋值
- 返回*this :要符合连续赋值的含义
例如:
class Date
{
//...
Date& operator=(const Date& d)
{
if (this != &d)//防止出现 d = d的情况
{
_year = d._year;
_month = d._month;
_day = d._day;
}
return *this;
}
//...
}
有返回值是为了连续赋值,类似于 i = j = 10; 这里的( j = 10) 的返回值为10,再将10赋值给i。
2. 赋值运算符只能重载成类的成员函数不能重载成全局函数
class Date
{
public:
Date(int year = 1900, int month = 1, int day = 1)
{
_year = year;
_month = month;
_day = day;
}
int _year;
int _month;
int _day;
};
// 赋值运算符重载成全局函数,注意重载成全局函数时没有this指针了,需要给两个参数
Date& operator=(Date& left, const Date& right)
{
if (&left != &right)
{
left._year = right._year;
left._month = right._month;
left._day = right._day;
}
return left;
}
// 编译失败:
// error C2801: “operator =”必须是非静态成员
编译失败原因:赋值运算符如果不显式实现,编译器会生成一个默认的。此时用户再在类外自己实现一个全局的赋值运算符重载,就和编译器在类中生成的默认赋值运算符重载冲突了,故赋值运算符重载只能是类的成员函数。
3. 用户没有显式实现时,编译器会生成一个默认赋值运算符重载,以值的方式逐字节拷贝。
注意:内置类型成员变量是直接赋值的,而自定义类型成员变量需要调用对应类的赋值运算符重载完成赋值。
4. 注意:如果类中未涉及到资源管理,赋值运算符是否实现都可以;一旦涉及到资源管理赋值运算符重载则必须要自己实现。
5. 区分拷贝构造和赋值重载:
拷贝构造:一个已经存在的对象去初始化另一个要创建的对象
例如,Date d1(2023,11,17);Date d2(2023,11,18)
Date ret = d2;这里就调用的拷贝构造函数,创建了新对象,开了空间
赋值重载:两个已经存在的对象进行拷贝
d1 = d2;这里调用的是赋值运算符重载。
13.3 前置++和后置++重载
前置++和后置++的函数名都是operator++,而前置++和后置++都是一元运算符,为了让前置++与后置++形成能正确重载,C++规定:后置++重载时多增加一个 int 类型的参数,但调用函数时该参数不用传递,编译器自动传递。
例如:
class Date
{
//...
//前置++,返回+1后的结果
Date& operator++()
{
*this += 1;
return *this;
}
//后置++,返回+1前的结果
Date operator++(int)
{
Date tmp(*this);
*this += 1;
return tmp;
}
//...
}
前置++中this指向的对象函数结束后不会销毁,故以引用方式返回提高效率;后置++要返回+1之前的旧值,故需在实现时需要先将this保存一份,然后给this+1,而tmp是临时对象,因此只能以值的方式返回,不能返回引用。
假如创建对象d,那么 ++d 则会被编译器转化为 d.operator++();
d++ 则会被编译器转化为 d.operator++(0); 这里的0其实可以是任何整型,因为加的 int 参数只是为了占位,跟前置++重载进行区分,故而只加了一个参数类型,也不需要变量来接收。故我们写代码只需要写一个 int 就可以。
十四、const成员函数
在讲最后两个默认成员函数之前,需要先明白const成员函数。
将const修饰的“成员函数”称之为const成员函数,const修饰类成员函数,实际修饰该成员函数隐含的this指针,表明在该成员函数中不能对类的任何成员进行修改。
class MyClass {
public:
void regularFunction(); // 普通成员函数
void constFunction() const; // const成员函数
};
void MyClass::regularFunction() {
// 这是一个普通的成员函数,可以修改对象的状态
}
void MyClass::constFunction() const {
// 这是一个const成员函数,不能修改对象的状态
}
const 实际上修饰的是隐含的 this 指针:
如果创建了一个const类型的对象,在调用普通成员函数时,就会报错:
class Date
{
public:
Date(int year, int month, int day)
{
_year = year;
_month = month;
_day = day;
}
void Print()
{
cout << "Print()" << endl;
cout << "year:" << _year << endl;
cout << "month:" << _month << endl;
cout << "day:" << _day << endl << endl;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
const Date d(2022, 1, 13);
d.Print();//报错:对象含有与成员函数"Date::Print"不兼容的类型限定符
}
因为普通函数的参数列表是 Date* this ,而我创建的对象d的类型是const Date,在调用时相当于d.Print(&d),传的参数类型是 const Date* this ,这种情况就是把权限放大了,原本是不能修改的,而传参过后却可以修改了。所以const修饰的对象必须调用const成员函数。
- 那非const对象能不能调用const成员函数呢?
答案是可以的。这样只是把权限缩小了,这样在const函数中不会修改对象的状态。
而且,同一个成员函数,普通的与const修饰的可以同时存在,它们互相构成重载。当非const对象调用这个函数时,如果普通成员函数(非const)存在,只会调用普通成员函数,否则才会调用const成员函数。
class Date
{
public:
Date(int year, int month, int day)
{
_year = year;
_month = month;
_day = day;
}
void Print()
{
cout << "Print()" << endl;
cout << "year:" << _year << endl;
cout << "month:" << _month << endl;
cout << "day:" << _day << endl << endl;
}
void Print() const
{
cout << "Print()const" << endl;
cout << "year:" << _year << endl;
cout << "month:" << _month << endl;
cout << "day:" << _day << endl << endl;
}
private:
int _year; // 年
int _month; // 月
int _day; // 日
};
int main()
{
Date d1(2022, 1, 13);
d1.Print();//调用Print()
const Date d2(2022, 1, 13);
d2.Print();//调用Print() const
}
- 需要注意的是,const成员函数不能调用非const成员函数。 因为同样会把权限放大。
十五、取地址及const取地址操作符重载
这两个默认成员函数一般不用重新定义 ,编译器默认会生成。所以只要知道就足够了。
拿日期类举例,假如定义了一个对象d,在 cout << &d; 时,应该是不行的,因为只有内置类型才有对应的运算符重载,但我们发现这样写是没问题的,就是因为这两个默认成员函数(不写自动生成)。
class Date
{
public:
Date* operator&()
{
return this;
}
const Date* operator&()const
{
return this;
}
private:
int _year; // 年
int _month; // 月
int _day; // 日
};
这两个运算符一般不需要重载,使用编译器生成的默认取地址的重载即可。
只有特殊情况,才需要重载,比如不想让别人取到有效地址:
class Date
{
public:
//...
Date* operator&()
{
return nullptr;
}
const Date* operator&() const
{
return nullptr;
}
//...
}