C++中类的几个默认成员函数
1. 构造函数
构造函数是一个特殊的成员函数,名字与类名相同,创建 类类型对象 时由编译器自动调用,以保证每个数据成员都有 一个合适的初始值,并且在对象整个生命周期内只调用一次。
构造函数的主要任务并不是开空间创建对象,而是初始化对象。
- 构造函数函数名和类名相同。
- 构造函数没有返回值。
- 在类实例化对象时编译器会自动调用构造函数初始化该对象。
- 构造函数可以重载。
- 无参的构造函数和全缺省的构造函数都称为默认构造函数,并且默认构造函数只能有一个,也有可能没有。注意:无参的构造函数、全缺省构造函数、我们没写编译器默认生成的构造函数,都可以认为是默认构造函数。也就是说构造函数和默认构造函数是不一样的!!!
- 如果类中没有显式定义构造函数,则C++编译器会自动生成一个无参数的默认构造函数,一旦用户显式定义编译器将不再生成。 默认生成的构造函数对内置类型不做处理,对自定义类型会调用它的默认构造函数。
简单来说:默认构造函数就是不传参数就可以初始化对象。
如果一个类没有默认构造函数,就是自己写了构造函数,但是该构造函数不是无参数的构造函数也不是全缺省的构造函数,那么这个类在实例化的时候就必须要传相对应的参数!!不然会报错。(推荐 类内都要有默认构造函数)
2. 析构函数
清理对象中的资源,==不是对 对象本身 的销毁,而是清理对象中的资源,比如堆空间的释放。==在对象销毁的时候会自动调用析构函数。
- 析构函数名:
~类名
- 析构函数没有参数也没有返回值。
- 一个类中只能有一个析构函数,不能重载,如果不显示定义,系统会自动生产默认的析构函数。
- 系统自动生成的析构函数对内置类型不做处理,对自定义类型会调用它的析构函数。
- 如果类中没有申请动态资源时(如堆空间),析构函数可以不写,直接使用编译器生成的默认析构函数就可以,但是类中有申请动态资源时必须自己显式的写析构函数释放资源,不然会造成内存泄漏!!
- 在对象的生命周期结束时会系统会自动调用析构函数清理对象中的资源。
同一个作用域中多个对象的析构顺序:后进先出!最后构造的对象最先析构。(栈帧)
3. 拷贝构造(C++规定对象之间的拷贝必须调用拷贝构造函数)
拷贝构造函数:只有单个形参,该形参是对本类类型对象 的引用(一般用const修饰),在用已存在的 类类型对象 创建新对象时由编译器自动调用。
- 拷贝构造函数本质就是构造函数的一个重载。
- 拷贝构造函数的参数有且仅有一个且必须是 类类型对象 的引用,且建议用
const
修饰。使用传值方式编译阶段会报错,因为会引发无穷递归调用。 - 如果没有显式定义,编译器会自动生成默认的拷贝构造函数。 默认的拷贝构造函数对内置类型**按内存存储按字节序完成拷贝(
memcopy
),这种拷贝叫做浅拷贝,或者值拷贝。**对自定义类型调用它的拷贝构造函数。
为什么拷贝构造函数的参数必须是 类类型对象 的引用:因为如果使用传值传参,在传参数的时候就要把实参拷贝给形参,而==拷贝构造函数形参类型就是类类型本身,拷贝的时候就要调用该类的拷贝构造函数!!!==调用拷贝构造函数就要传参,传参又要调用拷贝构造…所以就会引发无穷递归调用!使用引用传参就可以避免这个问题。
深浅拷贝问题:
==当类成员变量有在堆上动态开辟空间存值时,拷贝构造就必须要是自己写的深拷贝!!==编译器生成的那个默认拷贝构造就不行了!会有内存问题—两个对象中的成员变量指向了同一块堆空间!!
- 其中一个对象对其作了改变会影响另一个。
- 两个对象销毁时都会调用析构函数,释放两次同一块空间,就会报错!!
4. 赋值重载
1. 运算符重载
C++为了增强代码的可读性引入了运算符重载函数,运算符重载函数就是具有特殊函数名的函数,目的就是为了使自定义类型也能正常的使用各种运算符!
这个重载和 函数重载没有任何关系!!这里的意思是 针对不同的自定义类型重新定义各个运算符的意义!
运算符重载后 类类型 就可以正常使用该运算符了!编译器会自动解析成对应的函数 调用!
// 参数个数由 操作数 决定 // 函数原型:返回值类型 operator操作符(参数列表) bool operator==(const Date& d1, const Date& d2) { // 针对 Date 类型重新定义 == 运算符的意义!!方便使用 // ... } int main() { Date d1; Date d2; // 用的时候直接这样写!编译器会解析成函数调用:operator==(d1, d2) if(d1 == d2) { // ... } }
operator
后只能跟运算符。- 运算符重载函数必须要有 类类型 的参数(针对不同的自定义类型重新定义各个运算符的意义)。
- 运算符重载函数可以是全局的函数,也可以是类内的成员函数(更推荐)。
- 运算符重载函数 作为类内 成员函数 重载时,其形参看起来比操作数数目少1,因为成员函数的第一个参数为隐藏的
this
指针。 - 有5个运算符不能重载:
. .* :: sizeof ?:
(sizeof
是运算符!)
2. 赋值运算符重载
就是一个特殊的运算符重载,重载的是赋值运算符!
// 参数:const T& 引用减少传值消耗,并且用 const 修饰 // 返回值:有返回值是为了支持连续赋值! T& 返回引用减少消耗, class Date { public: // ... Date& operator=(const Date& d) { // 检测一下是否是自己给自己赋值!提高效率 if(this != &d) { year_ = d.year_; month_ = d.month_; day_ = d.day_; } // 有返回值是为了支持连续赋值! return *this; // this 就是指向对象的指针,就好像 i=j 会返回 i } private: int year_; int month_; int day_; };
- 用户没有显式实现赋值重载时,编译器会生成一个默认赋值运算符重载函数,对内置类型按内存存储按字节序完成拷贝(
memcopy
),也就是浅拷贝。 - ==赋值运算符只能重载成类的成员函数不能重载成全局函数。==因为赋值运算符如果不显式实现,编译器会生成一个默认的。此时用户再在类外自己实现一个全局的赋值运算符重载,就和编译器在类中生成的默认赋值运算符重载冲突了???(但是其他的运算符重载可以在类内和全局都实现一份,编译器会优先调用类内的!猜测:应该是类内的运算符重载会被编译成内联函数,就不和全局的冲突了!也不完全是!) 这里总是怪怪的!!!就当作是一个特性吧,赋值重载是特殊的成员函数,不能用常理对待。
赋值重载是把一个对象的值赋给另一个已经存在的对象!而拷贝构造是在实例化一个新的对象的时候调用!!
class Date { // ... }; int main() { Date d1; // 这里调用的是拷贝构造!初始化一个新的对象! Date d2 = d1; // => Date d2(d1); Date d3; // 这个调用的是赋值重载!把一个对象的值赋给另一个已经存在的对象 d3 = d1; return 0 }
**赋值运算符重载也有深浅拷贝问题:**和拷贝构造那里一样,当类成员变量有在堆上动态开辟空间存值时,编译器默认生成的赋值重载函数会导致多个对象中的成员变量指向同一块堆空间!!
- 其中一个对象对其作了改变会影响另一个。
- 两个对象销毁时都会调用析构函数,释放两次同一块空间,就会报错!!
这时要自己写赋值重载进行深拷贝!
5. 普通对象取地址重载
一般用默认生成的就可以。
class Date { public : Date* operator&() { return this ; } private : int _year ; int _month ; int _day ; };
6. const
对象取地址重载
一般用默认生成的就可以。
class Date { public : const Date* operator&() const { return this ; } private : int _year ; int _month ; int _day ; };
最后挂个链接,欢迎一起学习,一起进步!https://xxetb.xet.tech/s/4G6TWG