复制构造函数、赋值操作符和析构函数总称为复制控制。编译器自动实现这些操作,但类也可以定义自己的版本。
复制构造函数是一种特殊构造函数,具有单个形参,该形参(常用 const 修饰)是对该类类型的引用。
析构函数是构造函数的互补:当对象超出作用域或动态分配的对象被删除时,将自动应用析构函数。不管类是否定义了自己的析构函数,编译器都会自动为类中非 static 数据成员执行析构函数。
赋值操作符与构造函数一样,赋值操作符可以通过指定不同类型的右操作数而重载。右操作数为类类型的版本比较特殊:如果我们没有编写这种版本,编译器将为我们合成一个。
{
public:
myclass(const myclass &obj){}; // 复制构造函数
~myclass(){}; // 析构函数
myclass& operator=(const myclass &obj){}; // 赋值操作符
private:
int age;
string name;
}
13.1 复制构造函数
编译器合成的复制控制函数是非常精练的——它们只做必需的工作。但对某些类而言,依赖于默认定义会导致灾难。实现复制控制操作最困难的部分,往往在于识别何时需要覆盖默认版本。有一种特别常见的情况需要类定义自己的复制控制成员的:类具有指针成员。
复制构造函数在下列情况下会被调用:
myclass obj2 = obj1; // 根据另一个同类型的对象显式或隐式初始化一个对象
myclass fun(myclass par)
{
// ...
return par; // 从函数返回时复制一个对象
}
fun(obj1); // 复制一个对象,将它作为实参传给一个函数
vector<string> svec(5);; // 编译器首先使用 string 默认构造函数创建一个临时值来初始化 svec,然后使用复制构造函数将临时值复制到 svec 的每个元素
myclass ls[]{obj1,obj1,obj1,obj1};// 根据对象初始化数组
myclass ls[]{myclass(),myclass(),myclass()}; // 按照书上说是会调用复制构造函数但实际不会调用,据说是做了优化
如果不提供显示的复制构造函数系统会合成一个。合成的构造函数会在上述情况发生时会赋值对象副本并将对象数据成员逐一初始化成与原对象相同的值。
有个有趣的现象:数组是不能复制的,但如果对象数据成员是个数组类型却可以复制数组给对象副本的对应成员,合成复制构造函数模型如下
{
// obj是源对象,用它来复制副本
}
如果想禁止复制可以显示声明私有的复制构造函数(最好不要这么做否则类只能作为指针或引用传递),复制构造函数属于构造函数,一旦定义复制构造函数应该给类显示同时定义一个默认构造函数。
13.2 赋值操作符
类的赋值操作符实际上是操作符重载(operator=)
赋值操作结果和拷贝构造函数类似,它会执行逐个成员赋值(复制构造是逐个成员初始化,然后也允许重新赋值)
{
// obj是源对象,用它来为操作符左面对象赋值
age = obj.age;
name = obj.name;
return *this;
}
赋值操作符和复制构造函数几乎可以看做一个整体,如果需要其中一个几乎肯定也需要另外一个。
关于操作符重载会在后续章节做详细介绍。
13.3 析构函数
析构函数一个用途是对象在销毁之前做一些相关操作,比如清理资源,刷新缓冲区等。析构函数在对象即将销毁前执行
{
public:
string name;
~he(){cout << name << " is delete!" << endl;};
};
int main()
{
he cls;
cls.name = "zhang san";
he *ls = new he[4];
ls[0].name = "item0";
ls[1].name = "item1";
ls[2].name = "item2";
ls[3].name = "item3";
delete [] ls;
cout << "delete list" << endl;
he *pr = new he();
pr->name = "li si"
cout << "delete li si" << endl;
}
// 输出:
item3 is delete!
item2 is delete!
item1 is delete!
delete list
li si is delete!
delete li si
zhang san is delete!
赋值操作和复制(拷贝)构造函数效果类似,在使用=号操作时有时候会调用赋值有时候会调用复制构造函数,怎么区分调用方式呢?
复制(拷贝)构造函数,是用一个已知的对象去初始化另一个正在创建的对象;赋值操作,是用一个已经存在的对象去更新另一个已经存在的对象。
myclass a ;
myclass b = a ; // 用一个已知的对象去初始化另一个正在创建的对象,调用复制构造函数
b = a ; // 用一个已经存在的对象去更新另一个已经存在的对象,调用赋值操作
赋值操作符可以通过指定不同类型的右操作数而重载,看代码
{
public:
myclass& operator=(const myclass &obj){ name = obj.name; return *this;}; // 赋值操作符
myclass& operator=(string str){ name = str; return *this;}; // 赋值操作符重载
private:
string name;
}
myclass a;
myclass b;
b = a; // 调用 operator=(const myclass &obj)版
b = "tom"; // 调用 operator=(string str)版
本章最后介绍了智能指针的概念。它不是c++具体技术而是解决拷贝对象时指针字段会可能会引发错误的解决方案
class myclass
{
public:
string name;
int age;
};
// 智能指针
class curr
{
// 将具体类设置成智能指针的友元类
friend class test;
private:
curr(myclass *ip):cur(ip),used(1) {};
// 最后一个拥有指针成员的对象消亡时会删除智能指针对象,析构函数执行删除真正指向的类对象
~curr()
{
cout << "已经没有任何指针指向myclass对象!"<< endl;
delete cur; // 构造函数参数*ip必须是动态创建的对象指针 delete才能正确删除,否则会产生无法预知的运行时错误
};
myclass *cur;
int used;
};
// 具体类
class test
{
public:
test(myclass *ip, string stname,int stage): pro(new curr(ip)) ,name(stname) ,age(stage) {};
test(const test &t): pro(t.pro) ,name(t.name) ,age(t.age)
{
++pro->used;
};
~test()
{
--pro->used;
// 最后一个引用对象消失,智能指针计数器等于0,删除智能指针动态对象(智能指针删除时会出发自身的析构函数,析构函数中负责删除类成员)
if(pro->used == 0)
{
cout << "over" << endl;
delete pro;
}
};
void show(){cout << pro->used << endl;};
private:
curr *pro;
string name;
int age;
};
int main()
{
myclass *pr = new myclass();
test *t1 = new test(pr,"tom",21) ;
t1->show(); // 1
test *t2 = new test(*t1);
t1->show(); // 2
t2->show(); // 2
delete t1;
t2->show(); // 1
delete t2; // over 已经没有任何指针指向myclass对象!
}
智能指针基本思路是用智能指针对象替换数据成员类对象指针,由智能指针维护对象指向。当具体类发生拷贝或删除时更新智能指针维护的计数器。如果计数器==0说明所有具体类都消亡,删除智能指针。智能指针再负责删除数据成员对象。