C++基础精讲篇第6讲:类中构造函数和析构函数特性详解_King_lm_Guard的博客-CSDN博客https://blog.csdn.net/King_lm_Guard/article/details/126043552 本讲内容基于上一讲中分析的C++类中6个默认的成员函数,在上一讲中详细分析了构造函数和析构函数,这一讲博主接着分析拷贝构造函数相关特性。
目录
1、拷贝构造函数基本概念
通俗讲解:就是利用已经创建好的对象d1来创建新的对象d2。对象d2与对象d1特性一样。
严谨讲解:拷贝构造函数只有单个形参,该形参是对本类类型对象的引用(一般常用const修饰),在用已存在的类类型对象创建新对象时由编译器自动调用。
2、特性分析
拷贝构造函数也是特殊的成员函数,其特征如下:
1. 拷贝构造函数是构造函数的一个重载形式。
2. 拷贝构造函数的参数只有一个且必须是类类型对象的引用,使用传值方式编译器直接报错,因为会引发无穷递归调用。所以可利用引用终止这种递归状态。下面通过代码具体给大家展开分析:#define _CRT_SECURE_NO_WARNINGS #include<iostream> using namespace std; class Date { public: //构造函数 Date(int year = 2022, int month = 7, int day = 29) { cout << "构造函数:>" << endl; _year = year; _month = month; _day = day; } //拷贝构造函数,正确演示 //Date(const Date& d) //{ // cout << "拷贝构造函数:>" << endl; // this->_year = d._year; // this->_month = d._month; // this->_day = d._day; //} //拷贝构造函数,递归演示 Date(const Date d) { cout << "拷贝构造函数:>" << endl; this->_year = d._year; this->_month = d._month; this->_day = d._day; } private: int _year; int _month; int _day; }; int main() { Date d1; Date d2(d1); return 0; }
错误演示:无穷递归
调用拷贝构造函数需要先传参,而在这里演示的传值传参就是一个拷贝构造,所以会发生无穷递归。
正确演示:利用引用传参
补充:如果要实现上面分析拷贝,除了使用引用,原则上可以使用指针实现拷贝,如下代码所示,但我们会发现采用指针,此时在函数Date中的形参类型是Date*,这不符合拷贝构造函数的特性,这是需要读者们注意的。
//指针演示拷贝 Date( const Date* d) { this->_year = d._year; this->_month = d._month; this->_day = d._day; }
3、若未显式定义,编译器会生成默认的拷贝构造函数。 默认的拷贝构造函数对象按内存存储按字节序完成拷贝,这种拷贝叫做浅拷贝,或者值拷贝。也就是说对内置类型就是考虑浅拷贝或值拷贝。
如上图案例所示:用已经存在的d1拷贝构造d2,此处会调用Date类的拷贝构造函数,但Date类并没有显式定义拷贝构造函数,则编译器会给Date类生成一个默认的拷贝构造函数。
补充:在编译器生成的默认拷贝构造函数中,内置类型是按照字节方式直接值拷贝的,而自定义类型是调用其拷贝构造函数完成拷贝的。
4、针对日期类这样的类,就没必要自己显示实现拷贝构造函数了,如3中的例子所示,但如果是用户自定义类型,则需要用户显示实现,如果不手动显示实现,还是按照编译器默认的浅拷贝,则会出现程序崩溃。下面举例说明:
#define _CRT_SECURE_NO_WARNINGS #include<iostream> using namespace std; typedef int DataType; class Stack { public: //构造函数 Stack(size_t capacity = 10) { _array = (DataType*)malloc(capacity * sizeof(DataType)); if (nullptr == _array) { perror("malloc申请空间失败"); return; } _size = 0; _capacity = capacity; } //入栈 void Push(const DataType& data) { // CheckCapacity(); _array[_size] = data; _size++; } //析构函数 ~Stack() { if (_array) { free(_array); _array = nullptr; _capacity = 0; _size = 0; } } private: DataType *_array; size_t _size; size_t _capacity; }; int main() { Stack s1; s1.Push(1); s1.Push(2); s1.Push(3); s1.Push(4); Stack s2(s1); return 0; }
程序运行结果:
上面的测试程序是创建数据结构中的栈采用C++写的,当我们按照编译器默认生成的拷贝构造函数(在这里也就是值拷贝或者浅拷贝),会发现程序崩了,这是为什么呢?难道不能采用将一个字节一个字节拷贝的方式吗?下面跟着我一起详细分析其原因:
1、s1对象调用构造函数初始化时,默认申请了10个元素的空间,然后利用入栈函数,在里面放置了4个元素1 2 3 4 ;
2、s2对象使用s1拷贝构造,而在Stack类中没有显示定义拷贝构造函数,则编译器会给在Stack类生成一份默认的拷贝构造函数,默认拷贝构造函数是按照值拷贝的,也就是说将s1中内容原封不同的拷贝到s2中,因此s1和s2指向了同一块内存空间;
3、当程序退出时,s1和s2要销毁,根据上一讲分析的在栈区开辟的空间满足后构造先析构的特点,所以s2先销毁,s2销毁时调用析构函数,也就是将0x00cc6580的空间释放了,但此时s1并不知道该空间已经被销毁,所以当到s1销毁时,会将0x00cc6580空间再释放一次,当一块空间被多次释放,肯定会造成程序崩溃。
4、针对这种空间被多次释放的情况,需要用户手动实现深拷贝,但由于深拷贝内容复杂,所以博主计划在后面的章节中再为大家带来详细讲解。
注意:类中如果没有涉及资源申请时,拷贝构造函数是否写都可以;一旦涉及到资源申请时,则拷贝构造函数是一定要写的,否则就是浅拷贝。
3、调用场景特性分析
先阅读下面的程序:
class Date { public: //构造函数 Date(int year , int month , int day ) { cout << "构造函数:>" <<this<< endl; _year = year; _month = month; _day = day; } //拷贝构造函数 Date(const Date& d) { cout << "拷贝构造函数:>" << this<<endl; this->_year = d._year; this->_month = d._month; this->_day = d._day; } //析构函数 ~Date() { cout << "析构函数:>" << this << endl; } private: int _year; int _month; int _day; }; //修改前,传值传参 Date Test(Date d) { Date temp(d); return temp; } //修改后,传引用且引用做返回值 //Date& Test(Date& d) //{ // static Date temp(d); // return temp; //} int main() { Date d1(2022,7,29); Test(d1); return 0; }
拷贝构造函数典型调用场景:
1、使用已存在对象创建新对象;
2、函数参数类型为类类型对象;
3、函数返回值类型为类类型对象;传值传参+传值做返回值:
传引用传参+引用做返回值:(最好在Date temp(d)前面加上static 原因在C++中引用介绍有详细讲解。)
总结:从上面演示的程序代码中可以看出,使用传值传参,对空间的消耗很大,而且,需要多个拷贝,效率低下,因为为了提高程序效率,一般对象传参时,尽量使用引用类型,返回时根据实际场景,能用引用尽量使用引用。
4、总结
今天这一讲主要讲解了拷贝构造函数的用法及特性,但要注意,拷贝构造还有深拷贝部分需要深入学习,具体会在后面的学习过程中为大家带来详细讲解。欢迎点赞、关注、支持!!!