天天看点

C++语法系列——类的五大拷贝控制成员:拷贝/移动构造函数,拷贝/移动赋值运算符,析构函数

目录

(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)。