【C++基本语法】BIG FIVE: 安全的构造、赋值、移动和析构

本文详细介绍了C++中的构造函数(包括默认构造、拷贝构造和移动构造)、赋值运算符以及析构函数,强调了异常处理、self-assignment和有效编程实践,如《EffectiveC++》中的建议。作者分享了代码示例,并讨论了可能存在的问题和改进点。
摘要由CSDN通过智能技术生成

【C++基本语法】系列用来记录C++常用基本语法,惯用法。本文主要回忆我们最常写的构造函数(尤其是拷贝构造和移动构造),赋值运算和析构函数。由于本人长期cv,以至于最近在裸写这些函数的时候发现竟然不是一气呵成。
本文的示例也是初、中级程序员的面试题。另外文中会参考《Effective C++》的表述。

1. 构造函数和operator =

假设这样一个类,你会如何写它的构造函数和赋值运算符。你能一次写对吗?本文的源码和测试代码放在文章末尾,大家可以拷下来测试验证。


class A{
public: //这里把成员变量定义成public是偷懒,为了不写get, set
    int* data_ = nullptr;
    int size_;
}

C++11之后,一般我们会写这些: 构造函数、拷贝构造函数、拷贝赋值函数、移动构造函数、移动赋值函数、析构函数。
成员中是否有指针对这些函数的影响较大,这里我们以有指针为例探讨。

1.1 默认构造

考虑如何安全地进行默认构造;

    A::A(){
        cout<<"调用默认构造函数, ";
        size_ = 10;
        data_ = new int[size_];
        for(int i = 0; i < size_; ++i){
            data_[i] = i;
        }
        printData();
    }

1.2 拷贝构造函数

安全的拷贝构造应该考虑什么问题?

A::A(const A& a){
        cout<<"调用拷贝构造函数, ";
        size_ = a.size_;
        if (size_ <= 0){
            size_ = 0;
            data_ = nullptr;
            std::cerr<<"size "<<size_<<"is illegal\n"; //构造函数中调用打印合适吗,安全吗
        }else{
            data_ = new int[size_]; 
            for(int i = 0; i < size_; ++i){
                data_[i] = a.data_[i];
            }
            printData();
        }
    }

这是我现写的拷贝构造函数,对异常的处理并不漂亮。关于构造函数有几点注意:

  • 函数声明中入参为 const A&类型,拷贝构造一般不改变入参
  • 拷贝构造的拷贝应是深拷贝,尤指指针对象
  • 注意下面这两种写法,A a2 = a1;并非是拷贝赋值,二是拷贝构造
    A a1;
    A a2 = a1;   //这是拷贝构造,不是赋值!
    A a3;
    a3 = a2;     //这里是拷贝赋值

1.3 拷贝赋值运算符

下面是我的代码,并不优雅。但有两条应牢记

A& A::operator=(const A& a){
        cout<<"调用拷贝赋值运算符, ";
        if (this == &a){
            cout<<" 赋值给自己了!\n";
            return *this;
        }
        size_ = a.size_;
        if (size_ > 0){
            delete [] data_;  // 这里为什么要delete
            data_ = new int[size_];
            for(int i = 0; i < size_; ++i){
                data_[i] = a.data_[i];
            }
            printData();
        }
        return *this;
    }

1.3.1 《Effective C++》条款10*请记住:令operator = 返回一个reference to this

为了实现连锁赋值(x = y = z = 15 或者 x = (y = (z = 15))),赋值运算符必须返回一个reference指向操作符左侧的实参,所以我们应当遵循这样的协议:

Widget operator=(const Widget& rhs){
    ...
    return *this
}

当然,该协议也适用于所有的赋值运算,比如 operator+=。该协议非强制性,但所有人都遵守,我们最好也这么做。

1.3.2 《Effective C++》条款11在operator = 处理自我赋值

“自我赋值”发生在对象被赋值给自己时`

// 情形1
Widget w;
w = w; // 自我赋值

// 情形2
a[i] = a[j]; // i == j 时,潜在的自我赋值

// 情形3
*px = *py; //px 和py指向同一对象时,潜在的自我赋值

// 情形4
class Base{};
clase Derived : public Base{...};
void doSomething(const Base& rb, Derived * pd); // rb 和 *pd有可能指向同一对象

下面是这个实现看起来合理,但自我赋值时会有什么安全问题?

A& A::operator=(const A& rhs){
    delete data_; //销毁当前对象的data_
    data_ = new int[rhs.size_]; // 假设size_合法,使用rhs的副本
    //copy rhs->data to this->data_
    return *this;
}

这里自我赋值的问题是,如果*this 和rhs是同一个对象,那么delete操作就销毁了当前对象的data_。如何解决呢,可以通过“识别自我的方式”:

A& A::operator=(const A& rhs){
    if (this == &rhs) return *this; //证同测试(identity test)
    ...
    data_ = new int[rhs.size_]; // 假设size_合法,使用rhs的副本
    //copy rhs->data to this->data_
    return *this;
}

这个版本仍然在异常情况存在麻烦,书上推荐把注意力放在针对异常安全性(exception safety)(条款29),于是下面这种写法被提出(这种方式并不高效,其重点是在赋值rhs的对象内容之前不删除当前对象管理的内容):

A& A::operator=(const A& rhs){
    if (this == &rhs) return *this; //证同测试(identity test)
    ...
    int * tmp = data_; //记住原先的data
    data_ = new int[rhs.size_]; // 假设size_合法,使用rhs的副本
    //copy rhs->data to this->data_
    delete tmp; //删除原先的data
    return *this;
}

书上最后介绍了 copy and swap技术,这里不展开了。

看到这里之后,我把我的代码改成了如下。但在这个例子中,感觉意义不大,如果new失败了,data_是否指向原内容也就不重要了。

A& A::operator=(const A& rhs){
        cout<<"调用拷贝赋值运算符, ";
        if (this == &rhs){
            cout<<" 赋值给自己了!\n";
            return *this;
        }
        int * tmp_data = data_; //先保存当前内容
        if (rhs.size_ > 0){
            size_ = rhs.size_;
            data_ = new int[size_];
            for(int i = 0; i < size_; ++i){
                data_[i] = rhs.data_[i];
            }
            delete [] tmp_data; // 拷贝完之后再删除
            printData();
        }
        return *this;
    }

1.4 移动构造函数

A::A(A&& rhs){
        cout<<"调用移动构造函数, ";
        if(this != &rhs){
            delete[] data_;
            data_ = rhs.data_;
            size_ = rhs.size_;
            rhs.data_ = nullptr; //这句不可缺少
            rhs.size_ = 0;
        }
        printData();

    }
  • 移动构造的形参非const类型
  • 移动构造一般是浅拷贝过程
  • 注意:移动构造后,rhs的指针必须置空,否则rhs析构时,this->data_管理的内容也将被释放,从而引起coredump。

1.5 移动赋值运算符

A& A::operator=(A&& rhs){
        cout<<"调用移动赋值运算符, ";
        if (this != &rhs){
            delete[] data_;
            data_ = rhs.data_;
            size_ = rhs.size_;
            rhs.data_ = nullptr;
            rhs.size_ = 0;
        }
        printData();
        return *this;

    }
  • 移动赋值运算符,同样要注意rhs.data_=nullptr不可少
  • 同样要注意自我赋值

1.6 析构函数

    ~A(){
        // cout<<"调用析构函数\n";
        delete[] data_; // 无需判断data_是否为nullptr,delete已做相关处理
        data_ = nullptr;
    }

2 源码加测试代码

#include <iostream>

using namespace std;

class A{
public:
    int* data_ = nullptr;
    int size_;

public:
    A(){
        cout<<"调用默认构造函数, ";
        size_ = 10;
        data_ = new int[size_];
        for(int i = 0; i < size_; ++i){
            data_[i] = i;
        }
        printData();
    }
    A(int size): size_(size){
        if (size_ <= 0){
            size_ = 10;
            std::cout<<"size "<<size<<"is illegal\n";
        }
        data_ = new int[size_];
        for(int i = 0; i < size_; ++i){
            data_[i] = i + size;
        }
        cout<<"调用带参构造函数: ";
        printData();
    }

    A(const A& rhs){
        cout<<"调用拷贝构造函数, ";
        size_ = rhs.size_;
        if (size_ <= 0){
            size_ = 0;
            data_ = nullptr;
            std::cerr<<"size "<<size_<<" is illegal\n";
        }else{
            data_ = new int[size_];
            for(int i = 0; i < size_; ++i){
                data_[i] = rhs.data_[i];
            }
            printData();
        }
    }

    A& operator=(const A& rhs){
        cout<<"调用拷贝赋值运算符, ";
        if (this == &rhs){
            cout<<" 赋值给自己了!\n";
            return *this;
        }
        int * tmp_data = data_;
        if (rhs.size_ > 0){
            size_ = rhs.size_;
            data_ = new int[size_];
            for(int i = 0; i < size_; ++i){
                data_[i] = rhs.data_[i];
            }
            delete [] tmp_data;
            printData();
        }
        return *this;
    }

    A(A&& rhs){
        cout<<"调用移动构造函数, ";
        if(this != &rhs){
            delete[] data_;
            data_ = rhs.data_;
            size_ = rhs.size_;
            rhs.data_ = nullptr;
            rhs.size_ = 0;
        }
        printData();

    }

    A& operator=(A&& rhs){
        cout<<"调用移动赋值运算符, ";
        if (this != &rhs){
            delete[] data_;
            data_ = rhs.data_;
            size_ = rhs.size_;
            rhs.data_ = nullptr;
            rhs.size_ = 0;
        }
        printData();
        return *this;

    }

    void printData(){
        std::cout<<"data is : ";
        for (int i = 0; i < size_; ++i){
            std::cout<<data_[i]<<" ";
        }
        std::cout<<std::endl;
    }

    ~A(){
        // cout<<"调用析构函数\n";
        delete[] data_; // 无需判断data_是否为nullptr,delete已做相关处理
        data_ = nullptr;
    }
};


void test()
{
    // 默认构造测试;
    cout<<"case 1 默认构造测试: \n";
    A a;
    // 带参构造函数测试
    cout<<"\ncase 2 带参构造函数测试: \n";
    A a1(10);

    // 拷贝构造函数测试
    cout<<"\ncase 3 拷贝构造函数测试: \n";
    A a2(a1);
    A a3 = a1;   //这是拷贝构造,不是赋值!

    // 移动赋值运算符
    cout<<"\ncase 4 拷贝赋值运算符测试: \n";
    A a4;
    a4 = a3;
    a4 = a4;

    // 移动构造函数
    cout<<"\ncase 5 移动构造函数测试: \n";
    A tmp;
    A a6(std::move(tmp));

    // 移动赋值运算符
    cout<<"\ncase 5 移动赋值运算符测试: \n";
    A a7(30), a8;
    a8 = std::move(a7);

}

void test_illegal()
{
    A a1(-1);
    A a2(a1);
    a2 = a1;

}

int main()
{
    test();
    test_illegal();
    return 0;
}

执行结果


case 1 默认构造测试: 
调用默认构造函数, data is : 0 1 2 3 4 5 6 7 8 9 

case 2 带参构造函数测试: 
调用带参构造函数: data is : 10 11 12 13 14 15 16 17 18 19 

case 3 拷贝构造函数测试: 
调用拷贝构造函数, data is : 10 11 12 13 14 15 16 17 18 19 
调用拷贝构造函数, data is : 10 11 12 13 14 15 16 17 18 19 

case 4 拷贝赋值运算符测试: 
调用默认构造函数, data is : 0 1 2 3 4 5 6 7 8 9 
调用拷贝赋值运算符, data is : 10 11 12 13 14 15 16 17 18 19 
调用拷贝赋值运算符,  赋值给自己了!

case 5 移动构造函数测试: 
调用默认构造函数, data is : 0 1 2 3 4 5 6 7 8 9 
调用移动构造函数, data is : 0 1 2 3 4 5 6 7 8 9 

case 5 移动赋值运算符测试: 
调用带参构造函数: data is : 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 
调用默认构造函数, data is : 0 1 2 3 4 5 6 7 8 9 
调用移动赋值运算符, data is : 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 
size -1 is illegal
调用带参构造函数: data is : -1 0 1 2 3 4 5 6 7 8 
调用拷贝构造函数, data is : -1 0 1 2 3 4 5 6 7 8 
调用拷贝赋值运算符, data is : -1 0 1 2 3 4 5 6 7 8 

关于这个主题,我也没有进行深入研究,上述代码一定有不合理,不优雅的地方,请有缘人指正。

参考链接/文献

《Effective C++》

  • 23
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值