13.1 拷贝、赋值与销毁
13.1.1 拷贝构造函数
拷贝构造函数的第一个参数必须是引用类型,通常还是const的引用。因为对于函数参数传递时,非引用类型的参数需要进行拷贝初始化,而如果拷贝构造函数的参数是非引用类型,就会陷入死循环。 除了第一个参数,其他参数都要有默认实参。 如果没有定义拷贝构造函数,编译器会自动合成一个。 合成拷贝构造函数:从给定对象中一次将每个非static成员拷贝到当前对象。 拷贝初始化发生情况:
用 = 初始化对象时。 非引用参数传递。 非引用函数返回值。 列表初始化数组元素或者聚合类的成员。
struct A
{
int n, i;
A(int n = 1) : n(n) { }
A(const A& a, int i = 3) : n(a.n), i(i) { } // 第一个参数必须为引用,其他参数要有默认实参(隐式转换时的作用)
};
void main()
{
A a(2); // 直接初始化
A b(a); // 拷贝初始化
//A b = a; // 等价上面
cout << b.n << " " << b.i << endl; // 输出 2 3
}
13.1.2 拷贝赋值运算符
如果运算符是一个成员函数,其左侧运算对象就绑定到隐式的this参数。 赋值运算符通常应该返回一个指向其左侧运算对象的引用。 如果未定义自己的拷贝赋值运算符,编译器会生成一个合成拷贝赋值运算符。
13.1.3 析构函数
用于释放对象使用的资源,销毁对象的非static数据成员。 隐式销毁一个内置指针类型的成员不会delete他所指向的对象。 智能指针成员在析构阶段被自动销毁。 调用析构函数时间(对象被销毁):
变量离开作用域。 对象被销毁,成员也跟着销毁。 容器被销毁,元素也被销毁。 动态分配的对象,当对指向它的指针应用delete。 临时对象在创建完整表达式结束时。 指向对象的引用或指针离开作用域时,析构函数不会执行。
13.1.4 三/五法则
如果一个类需要析构函数,可以肯定也需要拷贝构造函数和拷贝赋值运算符(如:析构要销毁指针,说明每个对象指针都是属于自己的,那拷贝的时候就不能直接复制指针,要新的指针)。 如果一个类需要拷贝运算符,也肯定需要拷贝构造函数,反之亦然。但不一定需要析构函数(如:拷贝时需要标明每个对象不同序号,需要重新拷贝,但和析构无关)。
13.1.5 使用=default
类似构造函数,=default可以要求编译器生成合成版本的拷贝控制成员。 默认是隐式内联的。对成员的类外定义就不是内联的。
13.1.5 阻止拷贝
定义删除的函数来阻止拷贝。在函数参数列表后面加上 =delete。 不同于=default,=delete必须出现在函数第一次声明的时候。 可以对任何函数指定=delete。 指定了删除的析构函数,就无法销毁此类型的对象,也不能释放其变量和动态分配的对象的指针。 当不可能拷贝、赋值、销毁类的成员时,类的合成拷贝控制成员就被定义为删除的。 声明private的拷贝构造函数和拷贝赋值运算符也可以阻止拷贝,但不建议。
13.2 拷贝控制和资源管理
13.2.1 行为像值的类
对于指针成员,应该是创建新的指针,而不是直接拷贝,否则指向的值还是同一个。 如果一个对象赋予它自身,赋值运算必须正确。 大多数赋值运算组合了析构函数和拷贝构造函数的工作。
struct Value
{
int * p;
Value(int n = 0) :p(new int(n)) {}
Value(const Value &value) :p(new int(*value.p)) {} // 值类型的拷贝构造函数:指针要重新建
Value& operator=(const Value &);
~Value() { delete p; }
};
Value& Value::operator=(const Value &rv)
{
// 先拷贝值,再清除原有值,避免处理的对象是本身。如果先清除后拷贝,对于处理本身对象,就会出错
auto newp = new int(*rv.p); // 先拷贝传入的指针的值,指针需要重新创建
delete p; // 再删除原有值。
p = newp; // 最后再赋值
return *this;
}
void main()
{
Value v1(3); // 构造函数
Value v2(v1); // 拷贝构造函数
Value v3;
v3 = v1; // 拷贝赋值运算符
cout << *v1.p << " " << *v2.p << " " << *v3.p << endl; // 输出 3 3 3
v1 = v1; // v1 赋值给自身,正确
*v2.p = 123; // 修改v2和v3的指针指向的值
*v3.p = 666;
cout << *v1.p << " " << *v2.p << " " << *v3.p << endl; // 输出 3 123 666
}
13.2.2 定义行为像指针的类
模仿shared_ptr,设计自己的引用计数。计数器为指针,动态分配,以共享。
创建一个对象时(构造函数),计数器初始化为1。 拷贝构造函数,共享计数器,计数器增加。 析构函数,计数器减小,为0时释放对象。 拷贝赋值运算符,增加右侧对象的计数器(类似拷贝构造函数),减小左侧的计数器(类似析构函数)。
class Pointer
{
public:
int *p;
Pointer(int n = 0) :p(new int(n)), count(new std::size_t(1)) {} // 构造函数,初始化计数器为1
Pointer(const Pointer &ptr) :p(ptr.p), count(ptr.count) { ++*count; } // 拷贝构造函数,指针直接复制,计数器共享且递增
Pointer& operator=(const Pointer&);
~Pointer();
private:
std::size_t *count;
};
Pointer::~Pointer()
{
if (--*count == 0) // 当析构最后一个对象
{
delete p; // 销毁共享数据
delete count; // 销毁计数器
}
}
Pointer & Pointer::operator=(const Pointer & rp)
{
++*rp.count; // 先递增右值,再递减左值。避免赋值本身时出错。
if (--*count == 0) // 递减同析构函数
{
delete p;
delete count;
}
p = rp.p; // 最后再复制指针和计数器(都指向同一个值)
count = rp.count;
return *this;
}
void main()
{
Pointer p1(8); // 构造函数
Pointer p2(p1); // 拷贝构造函数
Pointer p3;
p3 = p1; /