目录
一、认识拷贝构造函数
1、什么是拷贝构造
2、深拷贝与浅拷贝
3、编译器可以绕过拷贝构造函数(C++ Primer P442)
4、explicit修饰
二、认识赋值运算符重载
1、赋值运算符重载格式
2、默认赋值运算符重载
3、赋值运算符都必须定义为成员函数
三、现代版拷贝构造与赋值运算符重载写法分析
①当用一个已存在的对象创建一个新对象时,②当函数参数类型为类类型对象时,③当函数返回值类型为类类型对象时,编译器会自动调用拷贝构造函数;
(如下代码为,一个模拟string类的构造函数与拷贝构造⬇️)
namespace test {
class string {
public:
//构造函数
string(const char* str = "")
{
_size = strlen(str);
_capacity = _size;
_str = new char[_size + 1];
memcpy(_str, str, _size + 1);
}
//拷贝构造
string(const string& s) { //考虑为什么要加 const 与 引用
_str = new char[s._capacity + 1];
memcpy(_str, s._str, s._size + 1);
_size = s._size;
_capacity = s._capacity;
}
//析构函数
~string() {
delete[] _str;
_str = nullptr;
_size = _capacity = 0;
}
private:
size_t _capacity;
size_t _size;
char* _str;
};
}
int main() {
test::string s1("for_test"); //调用构造函数
test::string s2(s1); //调用拷贝构造
return 0;
}
拷贝构造传入单个形参(const string& s),该形参是改类类型的对象的引用,函数传参时如果不传入引用,会发生对象的拷贝(拷贝函数参数压入函数栈帧中),拷贝对象时又会再次调用拷贝构造,不断重复此步骤,引发无穷递归,导致栈溢出。
const:由于是传入对象的引用,所以加上const,保证改对象在函数中不被修改;
拷贝构造函数是构造函数的一个重载,与构造函数一样,如果不显式定义,编译器会自动生成默认的拷贝构造函数,默认的拷贝构造将内置类型的成员变量按照内存的字节序进行拷贝,而自定义类型会调用他自己的拷贝构造;
那当我们不显示定义拷贝构造函数会出什么问题呢?⬇️
1️⃣成员变量所指向的地址空间相同:
无显示定义拷贝构造时(浅拷贝):
显示定义拷贝构造时:
2️⃣调用多次析构函数,对同一块空间进行多次释放:
如下方动画所示,当类中没有显式定义拷贝构造时,两个对象(s1, s2)的成员_str指向同一块空间,函数结束时,分别调用s2与s1的析构函数,导致_str所指向的空间被释放两次;
在拷贝初始化过程中,编译器可以跳过拷贝/移动构造函数,直接创建对象,即:
/*编译器被允许将第一行代码改写为第三行代码*/
test::string s3 = "abcd"; //隐式调用拷贝构造
test::string s4("abcd"); //编译器略过拷贝构造
两者汇编代码完全相同
上述第三点中的第一行代码,实质是将“abcd”实例化一个对象,再调用拷贝构造;
对于单个参数或者出第一个参数无默认值其余均有默认值的构造函数,具有上述类似的类型转换的作用;
当用explicit修饰构造函数后,该类型转换会被禁止!
1️⃣我们以上述代码为例:
2️⃣同样的例子我们还可以从vector的源代码中找到:
在vs编译阶段报错:
/**************以下为模拟实现string类的赋值运算符重载************/
string& operator=(const string& s) {
if (this == &s) return *this; //判断是否自己给自己赋值
char* tmp = new char[s._capacity + 1]; //注意存放'\0'的空间
memcpy(tmp, s._str, s._capacity + 1);
delete[] _str; //释放之前的空间
_str = tmp;
_size = s._size;
_capacity = s._capacity;
return *this; //返回左值引用
}
①参数类型:const T&, 传入引用提高传参效率
②返回值类型: T&, 返回一个指向左侧运算对象的引用(*this), 为了与内置类型的赋值运算符保持一致,即可连续赋值
③检查是否自己给自己赋值
与处理拷贝构造函数一样,如果一个类未定义自己的拷贝赋值运算符,编译器会为他生成一个合成拷贝赋值运算符(默认),以值的方式逐字节拷贝,同时也要注意深浅拷贝问题;
我们可以重载赋值运算符。不论形参的类型是什么,赋值运算符都必须定义为成员函数,因为如果用户在类外自己实现一个全局的赋值运算符,就和编译器在类中生成的默认赋值运算符重载冲突了
//传参为类的引用的拷贝构造
string(const string& s) :_str(nullptr) {
string tmp(s._str); //调用传参为字符串的拷贝构造
std::swap(tmp._str, _str);
std::swap(tmp._size, _size);
std::swap(tmp._capacity, _capacity);
}
string& operator=(string tmp) { //调用拷贝构造 string tmp(s);
std::swap(tmp._str, _str);
std::swap(tmp._size, _size);
std::swap(tmp._capacity, _capacity);
return *this;
//函数结束tmp对象销毁
}
参考文献:《C++ Primer》