一、构造函数
【作用】:完成对象的初始化
【特征】:
① 没有返回值,可以带参也可以不带参
② 函数名与类名相同
class Date { public: Date() // 注意不带参,void也不要加上 { _year = 2022; _month = 7; _day = 2; } private: int _year; int _month; int _day; };
③ 可以构成函数重载,可以有缺省值,一般来说全缺省最常用。但需要注意的是,无参和全缺省不能同时出现,因为编译器分辨不出应该调用哪一个
// …… Date() // 无参 { _year = 2022; _month = 7; _day = 2; } Date(int year = 2022, int month = 7, int day = 2) // 全缺省 { _year = year; _month = month; _day = day; } Date(int year, int month = 7, int day = 2) // 半缺省 { _year = year; _month = month; _day = day; } // ……
④ 构造函数在对象实例化的时候自动调用。如果构造函数为无参的,那么创建对象时应该写成
Date d1
,但是不能写成Date d1()
,因为这可能被理解成函数声明(函数名为 d1,返回值为Date 的函数)int main() { Date d1; // 无参的 Date d2(2022, 7, 2) // 有参的 return 0; }
⑤ ⭐默认构造函数的作用
如果没有显式的写出构造函数,那么C++编译器会自动生成一个无参的默认构造函数。无参构造函数、全缺省构造函数、编译器默认生成的构造函数都可以认为是默认构造函数,本质上不传参就可以调用的函数就是默认构造函数。
【疑问】为什么编译器默认生成的构造函数看似没有起到任何作用呢?
编译器默认生成的构造函数在初始化时只针对自定义类型,处理的方式是调用其构造函数,但不会初始化内置类型,
在C++中只有少数几种情况推荐使用编译器的默认构造函数:
所有的成员变量都是自定义类型,并且都提供了默认构造函数
内置类型的成量给了缺省值。C++11标准引入了内置类型成员变量“缺省值”的概念:
class Date { public: Date(int year = 2022, int month = 7, int day = 2) { _year = year; _month = month; _day = day; } private: int _year = 2022; // 这就是缺省值 int _month = 7; int _day = 2; }; class Time { private: int a; // 内置类型不处理 Date date; // 自定义类型调用自定义类型的构造函数 }; int main() { Time t; return 0; }
编译器默认生成的构造函数的特点是只会初始化自定义类型,初始化的方式是调用其默认构造函数(只有上面提到的三种,如半缺省就不行)。
如果没有默认构造函数(例如将上面的全缺省改成半缺省),就会出现找不到默认构造函数的错误。
二、析构函数
【作用】:完成类的资源清理(不是完成对象的销毁,对象销毁由编译器完成)
【特征】:
① 对象生命周期结束后自动调用
② 析构函数名和是在类型前加上 “~”
③ 无参数无返回值
class Stack { public: ~Stack() { free(arr); arr = nullptr; } private: int* arr; int sz; int capacity; };
④ 若没有显式的写出析构函数,则C++编译器会自动生成一个无参的默认析构函数。编译器默认生成的构造函数只会处理自定义类型,处理的方式是调用其默认构造函数
⑤ 先构造的后析构
C c; int main() { A a; B b; static D d; return 0; } // A B C D 四个类析构的顺序是 B A D C
三、拷贝构造函数
【背景】
在函数进行传值传参的时候,对于内置类型来说,形参是实参的直接拷贝,但是自定义类型的变量在传参的时候必须要调用其拷贝构造函数。
从本质上讲,当类的对象需要拷贝时,拷贝构造函数将会被调用,所以函数的返回值是对象时,也会调用相应的拷贝构造函数。
【作用】创建一个与原对象一模一样的的新对象
【特征】
拷贝构造函数是构造函数的一个重载形式。
用已存在类的对象创建新对象时由编译器自动调用
只有单个形参,该形参是对同类对象的引用(一般常用const修饰) 。
必须采用引用传参,采用传值调用会引发无穷递归。因为传值调用在拷贝形参时仍会调用其拷贝构造函数,如此不断递归下去,程序就会崩溃。而采用引用传参则不需要拷贝,因此也就不需要调用拷贝函数。
class Date { Date (const Date& d) // const Date d (X) { _year = d._year; _month = d._month; _day = d._day; } private: int _year; int _month; int _day; };
若未显式定义,编译器会生成默认的拷贝构造函数。默认拷贝构造函数的作用是对于内置类型和自定义类型的处理是不同的:
- 对于自定义类型——调用其拷贝构造函数
- 对于内置类型,按字节序完成拷贝(相当于memcpy的作用),这种拷贝我们叫做浅拷贝,或者值拷贝。
【总结】一般的类,使用默认生成的拷贝构造函数就够用了,但是需要直接管理内存资源的的类,例如自己写的栈,那我们就需要自己写拷贝构造函数了(需要实现深拷贝)。
否则拷贝后,两个栈所指向的空间也会是一样的,会有以下两个弊端:
- 修改数据时会相互影响
- 在析构时,同一块空间有可能被释放两次
四、赋值运算符重载
① 什么是运算符重载
【背景】
假设我们要判断两个日期类对象是否相等,虽然可以通过自定义函数来实现,但是如果我们可以用运算符直接比较,那么代码的可读性会大大提高。
而运算符重载的出现就是为了解决自定义类型不能使用各种运算符的问题。
【作用】增强代码的可读性
【特征】:
① 运算符重载本质是具有特殊函数名的函数。函数原型为
返回值类型 + operator + 需要重载的运算符符号 + 参数列表
② 由于类外的函数不能访问私有成员变量,所以最好的解决办法是将运算符重载函数写在类里面(或者使用友元、或者写一个函数返回成员变量的值,但都不推荐)
class Date { public: Date(int month = 7, int day = 3) { _month = month; _day = day; } int operator==(const Date& d1, const Date& d2) { if (d1._month == d2._month && d1._day == d2._day) return 1; else return 0; } private: int _month; int _day; };
③ 上面运算符重载的写法仍然是有问题的,错误提示是参数过多。原因在于每个非静态成员函数都有一个隐藏参数 this 指针(用于指向当前对象),所以正确的写法应该是:
int operator==(const Date& d2) //为了避免拷贝,推荐使用引用,最好再加上const { if (_month == d2._month && _day == d2._day) return 1; else return 0; }
④ 有了运算符重载就可以直接使用运算符比较自定义类型了
int main() { Date d1; Date d2; if (d1 == d2) // 等价于 d1.operator== (d2) cout << "==" << endl; return 0; }
⑤ 优先在类里面找对应运算符重载,再到全局里搜索
⑥ 有五个运算符不能重载:***** :: sizeof ?: .
② 赋值运算符重载
class Date { public: // …… Date& operator= (const Date& d2) { if (&d2 != this) { _month = d2._month; _day = d2._day; } return *this; } // …… };
【注意点】
考虑到连续赋值的情况(d1 = d2 = d3),我们需要给赋值运算符重载设计返回值。为了减少拷贝,返回值采用引用的方式
为了避免自己给自己赋值的情况,最好先进行判断
如果没有显式的定义赋值运算符符重载,编译器默认生成的赋值运算符重载会完成对象按字节序的值拷贝 (所以涉及对内存直接管理的类,我们有必要自己写运算符重载)
与拷贝构造的区别:
拷贝构造是使用一个存在的对象去初始化一个正在创建的对象,赋值重载是两个存在的对象之间的赋值
五、取地址及const取地址操作符重载
Date* operator& (Date& d) { return this; } const Date* operator& (Date& d) const { return this; }
【注意】
- 取地址及const取地址操作符重载编译器默认生成的就够用了(下图是默认生成的取地址操作符重载)
- 只有极少数的情况需要自己写取地址操作符重载,例如不想让人得到对象的真实地址。