C++构造、析构、拷贝构造函数详解
- C++构造、析构、拷贝构造函数详解
- 前言
- 引出
- 构造函数与析构函数
- 构造函数
- 概念及定义
- 特性
- 构造函数重载
- 编译器自动生成的构造函数
- 显式调用
- 析构函数
- 概念与定义
- 特性
- 编译器自动生成的析构函数
- 总结
C++构造、析构、拷贝构造函数详解
前言
如果一个类中没有任何成员变量和成员函数,则称为空类。但空类并非"完全为空",编译器会自动生成以下6个默认成员函数:
- 构造函数
- 析构函数
- 拷贝构造函数
- 赋值运算符重载
- const成员函数
- 取地址及const取地址操作符重载
默认成员函数:用户没有显式实现,编译器会生成的成员函数称为默认成员函数。
本文重点解析构造函数、析构函数的核心用法与原理。
引出
看一下上文中我们的示意栈类:
class Stack {//成员方法void Init(int defaultCapacity = 4) {base = (int*)malloc(sizeof(int) * defaultCapacity);if (base == nullptr) {perror("malloc failed\n");return;}this->size = 0;this->capacity = defaultCapacity;}void Destroy() {free(this->base);this->base = nullptr;this->top = this->capacity;}//成员变量int* base;int top;int capacity;
};
即便是这样实现,也会有很多不便的地方:
- 我们在使用时经常忘记
Init
或者忘记Destroy
,尝尝会忘记初始化或忘记释放内存,这会导致我们的程序错误或者内存泄漏。 - 每次使用前需要调用
Init
函数,使用结束后需要调用Destroy
函数。
int main(){Stack st1;st1.Init();Stack st2;st2.Init();st1.Destroy();st2.Destroy();return 0;
}
可以通过
Init
和Destroy
公有方法初始化栈,但如果每次创建对象时都调用该方法,未免有点麻烦,那能否在对象创建或销毁时,自动调用呢?
C++的构造函数和析构函数就是为了解决这样的问题。
构造函数与析构函数
构造函数
概念及定义
构造函数是一个特殊的成员函数,名字与类名相同,创建类类型对象时由编译器自动调用,以保证每个数据成员都有 一个合适的初始值,并且在对象整个生命周期内只调用一次。
构造函数的作用:在对象创建时自动完成初始化。
特性
构造函数是特殊的成员函数,需要注意的是,构造函数虽然名称叫构造,但是构造函数的主要任务并不是开空间创建对象,而是初始化对象。
特征如下:
- 函数名与类名相同。
- 无返回值,也不需要写void。
- 对象实例化时,自动调用对应的构造函数。
- 构造函数可以重载,可以有缺省参数。
- 如果类中没有显式定义构造函数,则C++编译器会自动生成一个无参的默认构造函数,一旦用户显式定义编译器将不再生成。
- 构造函数不开辟空间,只完成对象的初始化。
class Date {
public:Date(int year = 2025, int month = 2, int day = 2) {this->_year = year;this->_month = month;this->_day = day;}void Print() {cout << this->_year << "--" << this->_month << "--" << this->_day << endl;}
private:int _year;int _month;int _day;
};
int main() {Date d1;d1.Print();return 0;
}
以上代码的运行结果如下:
可以看到,main
函数中,d1
对象创建后,我们未进行初始化,调用Print方法后,输出了构造函数缺省参数中的值。
因此我们可以看到,构造函数会在对象创建的时候自动调用,为对象进行初始化。
构造函数重载
class Stack {typedef int StackDataType;
private:StackDataType* base;int top; //top表示栈顶元素的下一个位置int capacity;
public://构造函数Stack(int defaultCapacity = 4) { //可能需要多种方式的初始化,因此构造函数可以重载cout << "Stack defaultCapacity = 4" << endl;this->base = (StackDataType*)malloc(sizeof(StackDataType) * defaultCapacity);if (this->base == nullptr) {cout << "malloc failed" << endl;}this->capacity = defaultCapacity;this->top = 0;}Stack(StackDataType* array, int num) { //重载版本,用现有的数组进行初始化cout << "Stack(array, num)" << endl;this->base = (StackDataType*)malloc(sizeof(StackDataType) * num);if (this->base == nullptr) {cout << "malloc failed" << endl;}assert(this->base);memcpy(this->base, array, sizeof(StackDataType) * num);this->capacity = num;this->top = 0;}//析构函数,稍后介绍。~Stack() {cout << "~Stack" << endl;free(this->base);this->base = nullptr;this->capacity = 0;this->top = 0;}
};
可以看到,我们用三种方式初始化了Stack类对象,三个对象,因此构造函数和析构函数自动调用三次,这就是可重载的构造函数的好处。
以上我们显式指定构造函数的情景,那么当我们不显式指定,编译器自动生成的构造函数会做些什么呢?
编译器自动生成的构造函数
到这里,我们要对默认构造函数有一个全新的认识:
默认构造函数
,以下三种函数都可以被称作默认构造函数。
- 无参构造函数。
- 全缺省构造函数。
- 我们没写编译器默认生成的构造函数。
看以下运行结果。
上图的运行结果,当我们使用编译器自动生成的构造函数时,我们的成员变量的值出现了随机值,可见,编译器自动生成的构造函数,并未对这里的成员变量进行初始化。其实这是有渊源的。
C++标准规定:
C++把类型分成内置类型(基本类型)和自定义类型。内置类型就是语言提供的数据类型,如:
int/char/double......以及任意类型的指针变量
,自定义类型就是我们使用class/struct/union
等自己定义的类型。
编译器生成默认的构造函数,对内置类型不做处理,对自定义数据类型会调用他的默认构造函数。
编译器生成默认的构造函数的行为:
- 对内置类型不做处理。
- 对自定义数据类型会调用其构造函数。
观察以下运行结果:
可以看到:当成员变量中,存在自定义类型时,自定义类型会调用其默认构造函数(包括三种,无参,全缺省,编译器生成), 但我们惊奇的发现,变量_year, _month, _day
也被进行了初始化。
其实这是一种C++标准未定义的行为,VS的编译器,在自定义类型和内置类型同时存在时,会调用自定义类型的构造函数,同时也对内置类型进行了初始化。这样的行为只是VS编译器的个人行为,C++标准中并未这样定义。
C++把类型分成内置类型(基本类型)和自定义类型。内置类型就是语言提供的数据类型,如:
int/char/double......以及任意类型的指针变量
,自定义类型就是我们使用class/struct/union
等自己定义的类型。
编译器生成默认的构造函数,对内置类型不做处理,对自定义数据类型会调用他的默认构造函数。
结论:
- 一般情况下, 有内置类型成员,需要自己写构造函数,不能用编译器默认生成的。
- 类内全部都是自定义类型时, 可以考虑让编译器自己生成,前提是自定义类型的构造函数已实现。
显式调用
有如下Date
类
//自动生成的构造函数
class Date {
public:Date(int year = 2205, int month = 2, int day = 11) {this->_year = year;this->_month = month;this->_day = day;}//语法上,该重载函数可以存在,但是,无参调用时会存在歧义,因此不能存在//Date() { // this->_year = 3;// this->_month = 3;// this->_day = 3;//}//和缺省值参数相比,这个函数就没有存在的必要了/*Date(int year, int month, int day) { this->_year = year;this->_month = month;this->_day = day;}*/
private:int _year;int _month;int _day;
};
关于Date的构造函数注意事项已在注释中给出,我们来看以下几种显式调用。:
int main(){//构造函数的调用和普通函数不同,构造函数是对象+初始化参数 或者 自动调用Date d1;//Date d1(); //不能这样写,这样会和函数声明区分不开Date d2(2025, 2025, 2025); //构造函数的调用,对象名接参数列表Date d3;//d3.Date(2025, 2025, 2025);//这样写毫无意义,首先这种写法忽视了自动调用的问题,其次,这种写法Date为什么不叫Init
}
- 错误:
Date d1()
, 这种写法会和函数声明区分不开。 - 正确:
Date d2(2025, 2025, 2025)
, 构造函数的调用,对象名接参数列表, - 错误:
d3.Date(2025, 2025, 2025)
, 这样写毫无意义,首先这种写法忽视了自动调用的问题,其次,这种写法中,构造函数Date为什么不叫Init?
析构函数
通过前面构造函数的学习,我们知道一个对象是怎么来的,那一个对象又是怎么没的呢?
概念与定义
析构函数:与构造函数功能相反,析构函数不是完成对对象本身的销毁,局部对象销毁工作是由编译器完成的。
而对象在销毁时会自动调用析构函数,完成对象中资源的清理工作, 主要是堆上申请的资源需要清理,栈区的空间会在生命周期结束时自动释放。
特性
析构函数是特殊的成员函数,其特征如下:
- 析构函数名是在类名前加上字符 ~。
- 无参数,无返回值类型,也无需写void。
- 一个类只能有一个析构函数。若未显式定义,系统会自动生成默认的析构函数。注意:析构函数不能重载
- 对象生命周期结束时,C++编译系统系统自动调用析构函数。
- 如果类中没有申请资源时,析构函数可以不写,直接使用编译器生成的默认析构函数,比如Date类;有资源申请时,一定要写,否则会造成资源泄漏,比如Stack类。
class Stack {typedef int StackDataType;
private:StackDataType* base;int top; //top表示栈顶元素的下一个位置int capacity;
public://构造函数Stack(int defaultCapacity = 4) { //可能需要多种方式的初始化,因此构造函数可以重载cout << "Stack defaultCapacity = 4" << endl;this->base = (StackDataType*)malloc(sizeof(StackDataType) * defaultCapacity);if (this->base == nullptr) {cout << "malloc failed" << endl;}this->capacity = defaultCapacity;this->top = 0;}Stack(StackDataType* array, int num) { //重载版本,用现有的数组进行初始化cout << "Stack(array, num)" << endl;this->base = (StackDataType*)malloc(sizeof(StackDataType) * num);if (this->base == nullptr) {cout << "malloc failed" << endl;}assert(this->base);memcpy(this->base, array, sizeof(StackDataType) * num);this->capacity = num;this->top = 0;}//析构函数~Stack() {cout << "~Stack" << endl;free(this->base);this->base = nullptr;this->capacity = 0;this->top = 0;}
};
可以看到,我们创建了三个对象,main函数结束时,对象销毁,析构函数也就调用了三次。
析构函数较构造函数,并没有那么复杂,因此我们只要熟记特性,记得在析构函数中完成对象中资源的清理即可。
编译器自动生成的析构函数
关于编译器自动生成的析构函数,是否会完成一些事情呢?下面的程序我们会看到,编译器生成的默认析构函数,对自定义类型成员调用它的析构函数。
class Time{
public:~Time(){cout << "~Time()" << endl;
}
private:int _hour;int _minute;int _second;
};
class Date{
private:// 基本类型(内置类型)int _year;int _month;int _day;// 自定义类型Time _time;
};
程序运行结束后输出:
~Time()
,但在main
函数中根本没有直接创建Time类的对象,为什么最后会调用Time类的析构函数?
这是因为:main
函数中创建了Date对象d
,而d
中包含4个成员变量,其中_year, _month, _day
三个是内置类型,销毁时不需要资源清理,最后系统直接将其内存回收即可;
而time
是Time
类对象,所以在d
销毁时,要将其内部包含的Time类
的time对象
销毁,所以要调用Time类
的析构函数
。
但是:main
函数中不能直接调用Time
类的析构函数,实际要释放的是Date
类对象,所以编译器会调用Date
类的析构函数,而Date
类没有显式提供,则编译器会给Date
类生成一个默认的析构函数,目的是在其内部调用Time类的析构函数。
即当Date
对象销毁时,要保证其内部每个自定义对象都可以正确销毁。
main
函数中并没有直接调用Time
类析构函数,而是显式调用编译器为Date
类生成的默认析构函数。
注意:创建哪个类的对象则调用该类的析构函数,销毁那个类的对象则调用该类的析构函数
总结
- 构造函数:初始化对象,支持重载,内置类型需手动初始化
- 析构函数:清理资源,动态内存必须手动释放
关键点:理解编译器默认行为,根据需求选择显式实现!
文章到此结束啦,欢迎各位大佬在评论区讨论交流,如果觉得文章写的不错,还请留下免费的赞和收藏!