如果一个类中什么成员都没有,简称为空类。空类中真的什么都没有吗?并不是,在空类情况下编译器会自动生成以下6个默认成员函数。
默认成员函数:用户没有显式实现,编译器会生成的成员函数称为默认成员函数。
ps:本文以C++98标准讲解,C++11新增移动构造和移动赋值重载我们后续讲解
构造函数和析构函数
构造函数和析构函数负责类对象的初始化操作和清理操作
构造函数 - 自动调用
构造函数就是在创建类对象的时候,由编译器自动调用,为对象进行初始化的一个特殊成员函数。它的名称和类名相同,并且在对象的声明周期内只调用一次。构造函数的主要任务并不是开空间创建对象,而是初始化对象
定义: 函数名就是类名 默认构造函数的两种形式:无参
, 有参(缺省)
1. Date(); 2. Date(int year = 1, int month = 1, int day = 1);
对构造函数如果自己给了缺省值,会用缺省值进行构造
特性
1. 函数名与类名相同。
2. 无返回值。
3. 对象实例化时编译器自动调用对应的构造函数。
4. 构造函数可以重载
5.如果用户没有自己定义构造函数,那么编译器会自动生成一个无参的默认构造,若用户定义了,则编译器不再自动生成。
class Date { public: // 1.无参构造函数 Date() {} // 2.带参构造函数 Date(int year, int month, int day) { _year = year; _month = month; _day = day; } private: int _year; int _month; int _day; }; void TestDate() { Date d1; // 调用无参构造函数 Date d2(2015, 1, 1); // 调用带参的构造函数 // 注意:如果通过无参构造函数创建对象时,对象后面不用跟括号,否则就成了函数声明 // 以下代码的函数:声明了d3函数,该函数无参,返回一个日期类型的对象 // warning C4930: “Date d3(void)”: 未调用原型函数(是否是有意用变量定义的?) Date d3(); }
如果类中没有显式定义构造函数,则C++编译器会自动生成一个无参的默认构造函数,一旦用户显式定义编译器将不再生成。
我们一般情况下不会让无参构造函数和全缺省构造函数同时存在 会存在歧义(编译器调用不能确定调用哪个)
问题1 不实现构造函数的情况下,编译器会生成默认的构造函数。但是看起来默认构造函数又没什么用?我们为什么要自己实现构造函数?
答:对象实例化必须调用构造函数,若自己写了构造函数,编译器就不会给写构造函数(如果自己写的有参构造函数,对象实例化化时必须传参,否则编译器无法调用到构造函数)
C++把类型分成内置类型(基本类型)和自定义类型。内置类型就是语言提供的数据类型,如:int/char...,自定义类型就是我们使用class/struct/union等自己定义的类型。默认生成构造函数: 对内置类型成员不处理不给初始化(所以打印出来内置类型成员是随机值) 对自定义类型成员会去调用他的默认构造函数。在C++11 中针对内置类型成员不初始化的缺陷,又打了补丁,即:内置类型成员变量在类中声明时可以给默认值。
问题2 什么时候我们直接用默认生成构造函数呢?
我们没写编译器默认生成的构造函数,都可以认为是默认构造函数
系统默认的构造函数一般是直接用0填充这个对象所占用的内存。
如果你需要在这个对象一定义的时候就给它的某个变量赋值,或是给对象中的某个指针分配一段内存空间,或是别的什么特殊功能,你就需要用自己定义的构造函数了。
需要注意的是,如果你在构造函数里为某个指针分配了内存,你就一定得用自己写的析构函数把那段内存回收回来。否则,就会内存泄露了。
构造函数初始化方式
构造函数有两种初始化方式,一种是我们熟悉的赋值初始化;另一种是C++提供的初始化列表
赋值初始化
class Date { public: Date(int year) { _year = year; } private: int _year; };
虽然上述构造函数调用之后,对象中已经有了一个初始值,但是不能将其称为对对象中成员变量的初始化,构造函数体中的语句只能将其称为赋初值,而不能称作初始化。因为初始化只能初始化一次,而构造函数体内可以多次赋值
初始化类表
规定:以一个冒号开始,接着是一个以逗号分隔的数据成员列表,每个"成员变量"后面跟一个放在括号中的初始值或表达式
class Date { public: Date(int year, int month, int day) : _year(year) , _month(month) , _day(day) { } private: int _year; int _month; int _day; };
注:
1. 每个成员变量在初始化列表中只能出现一次(初始化只能初始化一次)
2.以下三类成员 必须在初始化列表 初始化
const成员变量 const int x;
引用成员变量 int& _ref;
没有默认构造函数的自定类型成员
3.成员变量在类中声明次序就是其在初始化列表中的初始化顺序,与其在初始化列表中的先后次序无关
对初始化列表的建议:能在初始化列表初始化就在初始化列表初始化,同时注意顺序 初始化顺序是声明顺序
析构函数 - 自动调用
析构函数:与构造函数功能相反,析构函数不是完成对对象本身的销毁,局部对象销毁工作是由编译器完成的。而对象在销毁时会自动调用析构函数,完成对象中资源的清理工作。防止内存泄漏问题
定义: 析构函数名为 ~类名
1. ~SeqList() 2. { 3. free(_pData ); // 释放堆上的空间 4. }
特性
1. 析构函数名是在类名前加上字符 ~。
2. 无参数无返回值类型。
3. 一个类只能有一个析构函数。若未显式定义,系统会自动生成默认的析构函数。注意:析构函数不能重载
4. 对象生命周期结束时,C++编译系统系统自动调用析构函数
问题1 那么何时会调用析构函数呢?
在类的成员变量的生命周期结束时,会自动调用析构函数 。
问题2 什么时候回来需要自己实现析构函数?
如果类中没有申请资源时,析构函数可以不写,直接使用编译器生成的默认析构函数。当类中需要申请资源时,我们需要自己动手写析构函数,如果不写,可能造成内存泄漏,野指针等问题。比如Date类;有资源申请时,一定要写,否则会造成资源泄漏,比如Stack类。
关于编译器自动生成的析构函数,会帮助我们做什么?
下面的程序我们会看到,编译器生成的默认析构函数,对自定类型成员调用它的析构函数
class Time { public: ~Time() { cout << "~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 d; return 0; }
程序运行结束后输出:~Time()
在main方法中根本没有直接创建Time类的对象,为什么最后会调用Time类的析构函数?
因为:main方法中创建了Date对象d,而d中包含4个成员变量,其中_year, _month,_day三个是内置类型成员,销毁时不需要资源清理,最后系统直接将其内存回收即可;而_t是Time类对象,所以在d销毁时,要将其内部包含的Time类的_t对象销毁,所以要调用Time类的析构函数。但是:main函数中不能直接调用Time类的析构函数,实际要释放的是Date类对象,所以编译器会调用Date类的析构函数,而Date没有显式提供(自己创造),则编译器会给Date类生成一个默认的析构函数,目的是在其内部调用Time 类的析构函数,即当Date对象销毁时,要保证其内部每个自定义对象都可以正确销毁main函数中并没有直接调用Time类析构函数,而是显式调用编译器为Date类生成的默认析构函数
注意:创建哪个类的对象则调用该类的析构函数,销毁那个类的对象则调用该类的析构函数
拷贝构造函数
在创建对象时,可否创建一个与已存在对象一某一样的新对象呢?我们可以利用拷贝构造函数实现。
拷贝构造函数:只有单个形参,该形参是对本类类型对象的引用(一般常用const修饰),在用已存在的类类型对象创建新对象时由编译器自动调用。
//两种实现方式 Date d2(d1); Date d3 = d1;
特征:
1. 拷贝构造函数是构造函数的一个重载形式。
2. 拷贝构造函数的参数只有一个且必须是类类型对象的引用,使用传值方式编译器直接报错,因为会引发无穷递归调用。
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; };
相关问题
问题1 拷贝构造函数:为什么不能用传值传参?防止无穷递归问题(为什么会出现无穷递归)
在默认情况下,编译器会自动生成一个拷贝构造函数和赋值运算符,用户可以用delete来不生成。调用拷贝构造函数,先将实参传递给形参,这个传递又要调用拷贝函数,会导致不断循环直到调用栈满。
问题2 拷贝构造函数参数为什么加const ?
答:权限缩小,防止原对象数据被误操作修改;而且const对象和普通对象都能传入
默认的拷贝我们叫浅拷贝或者值拷贝,自己写的我们叫深拷贝构造函数(为了解决深浅拷贝问题:浅拷贝会造成数据冲突问题和二次析构问题)
拷贝有浅拷贝和深拷贝,系统默认生成的拷贝构造函数就是浅拷贝:按字节序来拷贝值。但有些情况是需要深拷贝的,这就需要我们来自己动手写拷贝构造,就比如类中有指针,我们如果用前拷贝会导致俩个对象的指针指向同一处地址,这是非常危险的行为,首先在析构的时候就会出现问题,一块空间被释放了两次,这会导致空指针的问题;其次,俩个对象同用一块地址,可能在不经意间动了不该动的对象。
问题3 什么情况下需要深拷贝构造?
类中如果没有涉及资源申请时,拷贝构造函数是否写都可以;一旦涉及到资源申请时,则拷贝构造函数是一定要写的,否则就是浅拷贝。
拷贝构造函数典型调用场景:
使用已存在对象创建新对象
函数参数类型为类类型对象
函数返回值类型为类类型对象
为了提高程序效率,一般对象传参时,尽量使用引用类型,返回时根据实际场景,能用引用尽量使用引用
class Date { public: Date(int year, int minute, int day) { cout << "Date(int,int,int):" << this << endl; } Date(const Date& d) { cout << "Date(const Date& d):" << this << endl; } ~Date() { cout << "~Date():" << this << endl; } private: int _year; int _month; int _day; }; Date Test(Date d) { Date temp(d); return temp; } int main() { Date d1(2022,1,13); Test(d1); return 0; }