目录
编辑
一、拷贝构造函数
1.1 拷贝构造函数概念
1.2 拷贝构造的传引用传参
1.3 拷贝构造函数的特性
二、浅拷贝与深拷贝
一、拷贝构造函数
1.1 拷贝构造函数概念
只有单个形参,该形参是对本类类型对象的引用(一般常用const修饰),在用已存在的类类型对象创建新对象时由编译器自动调用。拷贝构造也是一个构造函数,可以说,拷贝构造函数是构造函数的一个重载形式。
程序演示如下:
#include<iostream>
using namespace std;class Date
{
public:Date(int year = 2025, int month = 3, int day = 6){_year = year;_month = month;_day = day;}
private:int _year;int _month;int _day;
};int main()
{Date d1(2024, 1, 28);//普通构造Date d2(d1);return 0;
}
拷贝构造就是用同类型的其他对象来构造。main函数中Date d2(d1)的对象d2中的内容就是由d1拷贝过去的。
拷贝构造要传引用传参,拷贝构造也是一个构造函数,要求和构造函数一样,函数名和类型相同,小括号中的参数随便写,其定义形式为:
//main函数中
ClassName d2(d1);///类中
ClassName(const ClassName& other);
其中:ClassName是类的名称;other是一个引用参数,传过去以后,d2就是this,d1就是other;const为了保护原对象other不被更改,一般情况下,拷贝构造需要加const。
所以,上述程序拷贝构造函数的完整写法为:
Date(const Date& d)
{this->_year = year;this->_month = month;this->_day = day;
}
1.2 拷贝构造的传引用传参
通过程序了解传值传参和传引用传参。
#include<iostream>
using namespace std;class Date
{
public:Date(int year = 2025, int month = 3, int day = 6){_year = year;_month = month;_day = day;}Date(Date& d){}
private:int _year;int _month;int _day;
};void func1(Date d)
{}void func2(Date& d)
{}int main()
{Date d1(2024, 1, 28);//普通构造func1(d1);func2(d1);return 0;
}
C++规定,自定义类型的传值传参,都会调用它的拷贝构造。
所以在程序中:
传值传参:调用func1函数之前要先传参,但在传参这个过程中(d1传给d的过程中)调用的是拷贝构造函数,可以调试F11看一下。
箭头指向func1(d1);程序后再按F11,得到下图:即:在传参这个过程中调用了自定义类型Date的拷贝构造函数。
接着再按F11,走完拷贝构造函数后,程序箭头将继续指向func1(d1);程序。
再按F11,此时开始调用func1函数。
整体流程图为:
传引用传参: 调试发现没有调用拷贝构造。rd是d1的别名,没有传参,直接就完成调用了。
C++为什么要这么做呢?
C语言是有bug的,C语言结构体可以传值传参,但是C语言传值传参会导致浅拷贝的问题。所以,对于自定义类型的传值传参,要先调用他的拷贝构造。
1.3 拷贝构造函数的特性
拷贝构造是一个特殊的构造函数,符合构造的各种特性:
拷贝构造函数的参数只有一个且必须是类类型对象的引用,使用传值方式编译器直接报错,因为会引发无穷递归调用。例如:
#include<iostream>
using namespace std;class Date
{
public:Date(int year = 2025, int month = 3, int day = 7){_year = year;_month = month;_day = day;}Date(Date d){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;
}
调用d2函数之前,要先传参。但是这里的传值传参会形成一个新的拷贝构造;所以,Date d2(d1);传值传参调用自定义构造函数Date(Date d),在调用之前,再先传值传参,在此过程中会调用他的拷贝构造函数(即,还是Date(Date d);
调用他的拷贝构造函数(拷贝构造函数是一个特殊的构造函数),还要先传参,这里传值传参,又会形成一个新的拷贝构造,以此递归下去,每次都进不去Date函数中。如图所示:
所以,引用是最完美的解决方案。
综上,构造函数的特性:
- 拷贝构造函数是构造函数的一个重载形式;
- 拷贝构造函数的参数只有一个且必须是类类型对象的引用,使用传值方式编译器直接报错,因为会引发无穷递归调用;
- 若未显式定义,编译器会生成默认的拷贝构造函数。
二、浅拷贝与深拷贝
编译器生成的默认的拷贝构造函数对象按内置类型成员按字节序完成拷贝,这种拷贝叫做浅拷贝,或者值拷贝。程序举例如下:
#include<iostream>
using namespace std;class Date
{
public:Date(int year = 1, int month = 1, int day = 1){_year = _year;_month = _month;_day = _day;}/*Date(const Date& d){this->_year = d._year;this->_month = d._month;this->_day = d._day;}*/void Print(){cout << _year << "-" << _month << "-" << _day << endl;}private:int _year;int _month;int _day;
};int main()
{Date d1(2025, 3, 7);Date d2(d1);d1.Print();d2.Print();return 0;
}
运行结果为:
程序把拷贝构造屏蔽以后,仍然能打印出两个2025-3-7。拷贝构造是一个默认成员函数,不写,编译器会生成一份。但拷贝构造和构造、析构还不一样。
默认生成的对内置类型竟然处理了。内置类型成员完成值拷贝(浅拷贝,就是按照二进制逐个对应字节拷贝)。
然而,在有些场景下,默认生成的拷贝构造(把拷贝构造注释掉以后)是会崩溃的。下列程序可以调试观察下:
#include<iostream>
using namespace std;class Stack
{
public:Stack(int capacity = 10){_arry = (int*)malloc(sizeof(int) * capacity);if(_arry == NULL){perror("malloc");return;}_size = 0;_capacity = capacity;}/*//Stack st2(st1);Stack(const Stack& s){int* tmp = (int*)malloc(sizeof(int) * d._capacity){if(tmp == NULL){perrot("malloc fail");return;}memcpy(tmp, s._arry, sizeof(int) * s._size);_arry = tmp;_size = s._size;_capacity = s._capacity;}}*/~Stack(){if(_arry){free(_arry);_arry = nullptr;_size = 0;_capacity = 0;}}void Push(int x){_arry[_size] = x;_size++;}private:int* _arry;int _size;int _capacity;
};int main()
{return 0;
}
调试发现:
_size和_capacity怎么拷贝都没有问题,问题在于_array是一个指针。
值拷贝相当于把st1->_array拷贝到st2->array中去了,即两个对象指向同一块空间。
出了作用域析构的时候,先对st2->array指向的对象进行置空,但是st1->_array仍然指向被置空的空间,导致st1->_array变成了野指针。同一块空间不能释放两次。
所以,动态开辟资源的浅拷贝必须解决深拷贝的问题,这里的深拷贝就是_size和_capacity直接拷贝,_array就看之前的st1->_array是多大的一块空间,然后另开一块一样大空间,把值拷贝下来并st2->_array指向一块独立的空间。
三、拷贝构造函数典型调用场景
- 使用已存在对象创建新对象
- 函数参数类型为类类型对象
- 函数返回值类型为类类型对象
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);// 2. 调用拷贝构造函数return temp;// 3. 调用拷贝构造函数
}int main()
{Date d1(2022,1,13);Test(d1);// 1. 调用拷贝构造函数return 0;
}
1. 使用已存在对象创建新对象:调用拷贝构造函数创建temp,并将d中的数据拷贝给temp;
2. 函数参数类型为类类型对象:调用自定义类型Test函数,d1传值传参的过程中会调用它的拷贝构造,创建一个临时的Date类型的对象。然后这个临时对象作为参数传递,Test函数来接收。
3. 函数返回值类型为类类型对象:函数返回时,函数的生命周期结束。此时,为了能拿到返回值,会调用它的拷贝构造函数(可能会存放在寄存器中)。
四、补充
引用也是有一些其他的隐患的。
void func(const int& x)
{}
int main()
{int a = 0;int& b = a;b++;func(a);//权限的缩小 - 可以的const int& c = a;//a是可读可写,但是给了c以后,就变为只读的了,不能通过c去修改aconst int x = 10;//权限的放大 - 不可以的//int& y = x;//errconst int& y = x;//okconst int& z = 10;//z是常量10的别名const int& m = a + x;//ok,(a+x)运算完成后会有一个结果,这个结果是一个临时变量,临时变量具有常性,是不能被修改的//权限的放大 a+x表达是的返回值是临时对象,临时对象具有常性//int& n = a + x;//errfunc(10);//const引用来接收,就没有权限放大,程序会正常运行func(a + x);return 0;
}
int main()
{double d = 1.1;int i = d;//d可以给i,因为有隐式类型转换//因为类型转换的时候,中间会产生临时变量。所以严格来讲,并不是把d给i,类型转换,会把d构造一个int的临时变量,再拷贝给iint& ri = d;//不能,因为类型不同const int& ri = d;//加const为什么就可以了?//同理,先把d拷贝给临时变量,此时的ri就是临时变量的别名,临时变量具有常性,所以就要用const修饰return 0;
}
为什么要产生临时变量呢?我们通过程序来了解:
int main()
{int i = 97;char ch = 'a';if (i == ch){cout << "相等" << endl;}return 0;
}
i和ch不能直接比较的。操作符两边的操作数类型不一样的时候,相近类型可以进行类型提升或截断。此处,ch并不会变,而是通过临时变量的整形提升的规则来比较的。临时变量4个字节,按整形提升的规则,最高位即符号位往上补0,补充至4个字节,还是97。