目录
(1)都长什么样?有何用处?
(2)何时会被用到?
(1)都长什么样?有何用处?
a. 拷贝构造函数,要求第一个形参是类类型的(const)左值引用,若有其他形参(通常没有),则必须有默认值。形式如下:
class A
{
private:
int i;
char *ptr;
static string str;
};
A(const A& temp, int useless = 0):i(temp.i), ptr(new char[strlen(temp.ptr) + 1]) //拷贝构造函数,拷贝资源
{
strcpy(ptr,temp.ptr);
}
可见,拷贝构造函数用于,将其他对象的非static数据成员及动态资源逐个拷贝到正在创建的对象中。构造结束后两个对象的数据成员和动态资源相互独立(其实可以共享)。
b. 移动构造函数对形参的要求与拷贝构造函数类似,唯一区别是第一个形参须是类类型的右值引用。形式如下:
A(A&& temp, int useless = 0):i(temp.i), ptr(temp.ptr) //移动构造函数,接管而非拷贝资源
{
temp.ptr = NULL; //确保销毁安全
}
正如上述代码所示,移动构造函数实现了,其他对象的非static数据成员及动态资源被正在创建的对象所接管。资源被接管的对象通常不会再被使用,而是被销毁,因此为了确保此对象销毁后不影响被接管的动态资源,管理动态资源的数据成员应不再指向此资源。
c. 拷贝赋值运算符,通常接受右侧运算对象的(const)左值引用,并返回左侧运算对象的左值引用。示例如下:
A& operator=(const A& temp)
{
if(this != &temp) //检查自赋值
{
free(ptr);
ptr = new char[strlen(temp.ptr) + 1]; //拷贝资源
strcpy(ptr, temp.ptr);
i = temp.i;
}
return *this;
}
可见,拷贝赋值运算符用于,释放左侧运算对象管理的动态资源,并把右侧运算对象的非static数据成员及动态资源拷贝给左侧运算对象。为了顺利完成资源拷贝,检查自赋值情况是必要的。
d. 移动赋值运算符与拷贝赋值运算符有一点类似,即通常返回左侧运算对象的左值引用,但接受的往往是右侧运算对象的右值引用。示例如下:
A& operator=(A&& temp)
{
if(this != &temp) //检查自赋值
{
free(ptr);
ptr = temp.ptr; //接管资源而非拷贝资源
temp.ptr = NULL;
i = temp.i;
}
return *this;
}
移动赋值运算符会先释放左侧运算对象管理的动态资源。接着,与移动构造函数类似,实现了左侧运算对象对右侧运算对象的非static数据成员及动态资源的接管。同样地, 为了顺利完成资源接管,先要检查自赋值情况。为了确保右侧运算对象销毁后不影响被接管的动态资源,管理动态资源的数据成员应不再指向此资源。
e. 析构函数形式简单,形参也不需要。如下:
~A()
{
free(ptr);
}
析构函数的函数体,负责销毁对象管理的动态资源。而对象所拥有的非static数据成员的销毁不需要程序员操心,它们是在析构函数体外隐含的析构阶段被自动销毁的。
(2)何时会被用到?
a. 由于拷贝/移动构造函数本身属于构造函数,因此当定义对象时传入的参数是左值/右值,则拷贝/移动构造函数相应被调用;
b. 拷贝/移动构造函数与对象拷贝初始化这一概念密切相关。先解释何为对象拷贝初始化,以及与其相近的概念——直接初始化:
拷贝初始化和直接初始化都是初始化对象的方式:
---- 直接初始化意味着,定义对象时直接传入某种形式的参数,引发相应构造函数的调用;
---- 拷贝初始化是将赋值号右侧的运算对象拷贝到正在创建的对象中,右侧运算对象不一定是类型正确的对象,还可能是对象中的某一数据成员,凭借隐式类类型转换可变为类型正确的对象。
具体示例如下:
string s1(10,'c'); //直接初始化
string s2(s1); //直接初始化
string s3 = s2; //拷贝初始化
string s4 = "hello"; //拷贝初始化
当拷贝初始化发生时,被拷贝的对象是左值/右值,则拷贝/移动构造函数相应被调用。拷贝初始化会在以下情况中出现:
---- 定义对象时使用=;
---- 将对象作为实参传递给非引用类型的实参;
---- 从一个返回类型为非引用类型的函数返回对象;
---- 用花括号列表初始化一个数组中的元素或聚合类中的成员;
---- 某些容器在创建对象时;
前三种情况顾名思义,不赘述,后两种情况示例如下:
string s1("hello"), s2("world"), s3("everyone");
string s[] = { s1, s2, s3 }; //数组初始化
struct one
{
int i;
string s;
};
one temp = {5, s1}; //初始化聚合类对象
vector<string> vr;
vr.push_back(s1); //容器创建对象
以上五种情况发生时,拷贝/移动构造函数相应被调用。
c. 拷贝/移动赋值运算符在赋值发生时被调用,赋值号右侧的运算对象是左值/右值,则拷贝/赋值运算符相应被调用。示例如下:
string s1("hello"), s2("nothing");
s1 = s2; // =右侧是左值,拷贝赋值运算符被调用
s1 = std::move(s2); // =右侧是右值,移动赋值运算符被调用
d. 析构函数在对象被销毁时调用,而对象在以下情况中被销毁:
---- 对象在指令执行超出作用域后被自动销毁;
---- 临时对象在完成任务后被自动销毁;
---- 动态分配的对象被手动销毁(调用delete)。