用编译器自动分配的缺省拷贝构造函数已经可以满足绝大部分的场景。但如果类当中封装了,指针形式的成员变量,那么就要注意避免浅拷贝问题。
1. 深拷贝和浅拷贝
1. 浅拷贝
- 浅拷贝的可能出现的两个错误:
- 1. 逻辑错误: 导致不同对象之间的数据共享
- 2. 语法错误: 对象销毁时出现 “double free” 的异常
#include <iostream>
#include <unistd.h>
using namespace std;
class Integer{
public:
Integer(int pi=0){
// 这下就有了对象维护的动态资源
m_pi = new int(pi);
}
// 对象被销毁时,析构函数自动执行
~Integer(void){
cout << "析构函数" << endl;
delete(m_pi);
}
// // 这个自定义的浅拷贝构造 和 系统自动分配的效果是一样的
// // 这里显示的把它写出来,便于理解
// Integer(const Integer& that){
// cout << "浅拷贝构造函数" << endl;
// m_pi = that.m_pi;
// }
void print(void){
cout << *m_pi << endl;
}
private:
int* m_pi;
};
int main(void){
// 栈区对象
Integer i1(100);
i1.print();
// 拷贝构造
// 缺省拷贝构造函数,对指针形式的成员变量m_pi是按字节复制的,相当于只是拷贝了地址
// i1 和 i2 的 m_pi 是相同的,指向同一块内存
// 那么i1 和 i2两个独立的对象共享数据m_pi, 产生逻辑错误,这样不好
Integer i2(i1);
i2.print();
return 0;
} // i1 和 i2离开主函数的时候都将调用析构函数,~Integer ,将同一块内存释放了两次
// 会产生double free的错误,语法错误
$ ./a.out
100
100
析构函数
析构函数
*** Error in `./a.out': double free or corruption (fasttop): 0x0000000000db7c20 ***
======= Backtrace: =========
....
Aborted
2. 深拷贝
#include <iostream>
#include <unistd.h>
using namespace std;
class Integer{
public:
Integer(int pi=0){
// 这下就有了对象维护的动态资源
m_pi = new int(pi);
}
// 对象被销毁时,析构函数自动执行
~Integer(void){
cout << "析构函数" << endl;
delete(m_pi);
}
// 自定义的深拷贝构造
Integer(const Integer& that){
cout << "深拷贝构造函数" << endl;
// 在拷贝数据之前,需要给新对象 i2 的 m_pi 指向一块新的内存去接收这个数据
// 不然新的对象 i2 的 m_pi, 没法接收这个数据, 强行拷贝过去会造成非法内存访问
m_pi = new int;
*m_pi = *that.m_pi;
}
void print(void){
cout << *m_pi << endl;
}
private:
int* m_pi;
};
int main(void){
// 栈区对象
Integer i1(100);
i1.print();
Integer i2(i1);
i2.print();
return 0;
}
$ ./a.out
100
深拷贝构造函数
100
析构函数
析构函数
moonx@moonx
- 解释下上面代码中说的 非法内存访问
#include <iostream>
using namespace std;
int main(void) {
int *a;
// 上面只是定义了整数类型指针变量a
// 但a 中的地址还没有指向任何存储区
// 下面直接用 10 初始化 a所指向的存储区是不对的
*a = 10;
return 0;
}
$ ./a.out
Segmentation fault
2. 拷贝赋值
2.1 浅拷贝赋值函数
- 比浅拷贝还多了一个内存泄露的问题
#include <iostream>
#include <unistd.h>
using namespace std;
class Integer{
public:
Integer(int pi=0){
// 这下就有了对象维护的动态资源
m_pi = new int(pi);
}
// 对象被销毁时,析构函数自动执行
~Integer(void){
cout << "析构函数" << endl;
delete(m_pi);
}
// 自定义的深拷贝构造
Integer(const Integer& that){
cout << "深拷贝构造函数" << endl;
m_pi = new int;
*m_pi = *that.m_pi;
}
/*// 这个自定义的缺省拷贝赋值函数 和 系统自动分配的缺省拷贝赋值函数 效果是一样的
// 这里显示的写出来是为了便于理解系统自动分配的 缺省拷贝赋值函数
// i3 = i2 ==> i3.operator=(i3)
// (i3 = i2) 返回值是 i3, 所以 operator= 的返回值类型是Integer&
// 所以return *this, 目的是和内置基本类型保持语义的一致性
// 参数中 const是为了接收常量型实参,&为了提高传参效率
Integer& operator=(const Integer& that){
// 按字节复制,浅拷贝
m_pi = that.m_pi;
cout << "自定义缺省拷贝赋值函数" << endl;
return *this;
}
*/
void print(void){
cout << *m_pi << endl;
}
private:
int* m_pi;
};
int main(void){
// 栈区对象
Integer i1(100);
i1.print();
//拷贝构造,效果等同于 Interger i2 = i1; 这个=不是赋值,是初始化
Integer i2(i1);
i2.print();
Integer i3;
// 拷贝赋值函数(左调右参)
// i3, i2 都是已经存在的对象,但不是基本类型,编译器不知道如何完成赋值操作
// 编译器会将i3 = i2转为,i3.operator=(i2); i3当做函数的调用对象, 右边的对象当做参数。
// 如果没有定义自己的拷贝赋值操作符函数,编译器会为该类提供缺省的拷贝赋值操作符函数 operator=
// 但这个缺省拷贝赋值函数也是浅拷贝,需要自己定义深拷贝赋值函数
i3 = i2;
return 0;
}
$ ./a.out
100
深拷贝构造函数
100
析构函数
析构函数
*** Error in `./a.out': double free or corruption (fasttop): 0x000000000075a050 ***
- 上面的代码只演示了浅拷贝赋值函数的一个问题: 产生过数据共享, double free 的异常
- 除了这个double free 的异常, 还可能产生内存泄露
- 上图中 如果 Integer i2(200), 那么i2对象的 m_pi 指向放着数字200的存储区,经过浅拷贝赋值 i2 = i1, i2对象的m_pi指向放着数字100的存储区。那么就会造成内存泄露。
- 而深拷贝是把i2原本指向的存储区的内容,从200 改成了100。
2.2 深拷贝赋值函数
- 对比两个对象地址,防止自赋值
- 释放旧内存的目的是,可能要拷贝到旧内存中的数据, 和旧内存中原本数据的类型、大小都不一致
- 本例子中写 释放旧内存 和 分配新内存显得有些多余,但如果 this 和 that中 m_pi 执行的内存中数据类型和大小都不一样,就不多余了。
#include <iostream>
#include <unistd.h>
using namespace std;
class Integer{
public:
Integer(int pi=0){
// 这下就有了对象维护的动态资源
m_pi = new int(pi);
}
// 对象被销毁时,析构函数自动执行
~Integer(void){
cout << "析构函数" << endl;
delete(m_pi);
}
// 自定义的深拷贝构造
Integer(const Integer& that){
cout << "深拷贝构造函数" << endl;
m_pi = new int;
*m_pi = *that.m_pi;
}
// 自定义深拷贝赋值函数
// i3 = i2 ==> i3.operator=(i3)
// (i3 = i2) 返回值是 i3, 所以 operator= 的返回值类型是Integer&,return *this, 目的是和内置基本类型保持语义的一致性
// 参数中 const是为了接收常量型实参,&为了提高传参效率
Integer& operator=(const Integer& that){
if(this != &that){ //防止自赋值
delete m_pi; // 释放旧内存
m_pi = new int; // 分配新内存
*m_pi = *that.m_pi; // 拷贝新数据
cout << "自定义深拷贝赋值函数" << endl;
}
return *this; // 返回自引用
}
void print(void){
cout << *m_pi << endl;
}
private:
int* m_pi;
};
int main(void){
// 栈区对象
Integer i1(100);
i1.print();
//拷贝构造,效果等同于 Interger i2 = i1; 这个=不是赋值,是初始化
Integer i2(i1);
i2.print();
Integer i3;
i3 = i2;
return 0;
}
3. String 类的实现
- s实现一个字符串类String, 为其提供可接收C风格字符串的 构造函数、析构函数、拷贝构造函数、拷贝赋值函数
3.1 构造函数
3.2 析构函数
3.3 拷贝构造函数
3.4 拷贝赋值函数
#include <iostream>
#include <cstring>
using namespace std;
class String{
public:
// 构造函数
String(const char* str = ""){
// new char[3] 分配一个字符数组,数组中有3个连续的存储区,每个存储区只能保存单个字符
// m_str 指向字符数组中第一个元素的存储区
m_str = new char[strlen(str) + 1];
// 所以这里直接赋值 *m_str = *str, 仅仅是拷贝首元素的值
// strcpy(3)可以把字符串拷贝到字符数组中
strcpy(m_str, str);
}
// 析构函数
~String(void){
delete[] m_str;
m_str = NULL;
}
// 拷贝构造函数(深拷贝)
String(const String& that){
m_str = new char[strlen(that.m_str) + 1];
strcpy(m_str, that.m_str);
}
// 拷贝赋值函数(深拷贝)
String& operator=(const String& that){
if(this != &that){
// 没有办法保证this和that中的m_str指向的字符串长度是一样的,直接拷贝可能放不下,或者不能完全覆盖
// 所以先把this中m_str指向的存储区释放掉,再重新分配和that中m_str指向的存储区一样长度的存储区
delete[] m_str;
m_str = new char[strlen(that.m_str) + 1];
strcpy(m_str, that.m_str);
/*
//这里还要一个更高级的写法
// 深拷贝构造完成了,新内存的分配,和数据的拷贝。 但这都是针对tmp对象的
String tmp(that);
// swap 会再让 this->m_str 指向 tmp对象 维护的动态内存 *tmp.m_str
// 然后再把 tmp.m_str 指向 this->m_str 原来指向的内存
swap(m_str, tmp.m_str);
// 对象tmp离开作用域时,调用析构函数,释放tmp.m_str指向的内存。
// 即this.m_str原先指向的存储区被释放,防止了内存泄露的问题
*/
}
return *this;
}
// 向下(C)访问的接口
const char* c_str(void){
return m_str;
}
private:
char* m_str;
};
int main(void){
String s1 = "xuehui"; // or String s1("xuehui")
cout << s1.c_str() << endl;
String s2(s1); // or String s2= s1;
cout << s2.c_str() << endl;
String s3 = "xue";
// 下面语句编译时会转为 s3.operator=(s1)
s3 = s1;
cout << s3.c_str() << endl;
}
$ ./a.out
xuehui
xuehui
xuehui
4. 忠告和建议
- 拷贝构造 和 拷贝赋值 通常是成对出现,且逻辑相同的。