深拷贝和浅拷贝的再次辨析


深拷贝和浅拷贝的再次辨析

最近发现,以前学过的很多C++深层类和对象的特性又逐渐的消失在记忆中,代码能力非常的弱,所以想要自己实现一个资源管理的类(如String)。

本文记录我实现String类过程中的真实问题,从而对深拷贝和浅拷贝进行一次从类和对象的基本特性到内存模型的梳理。

问题的起点

在实现一个简化版的 String 的类时,一些看似能够运行的代码,实际会出现许多问题。

class String{
private:
    char* m_data;
}

涉及的主要问题是:构造、拷贝构造、拷贝赋值运算符操作、移动构造、析构,Rule of three

什么是深拷贝和浅拷贝

浅拷贝是指只拷贝对象的值,不拷贝对象所指向的资源;深拷贝不仅拷贝对象的值,还为对象的资源重新分配内存,使每个对象都拥有独立内存。如果一个对象有自己独立的内存资源(指针指向一块内存),这时如果发生两次析构,资源就会出现未定义的行为。

深拷贝和浅拷贝不是简单的语法问题,而是对象生命周期和资源所有权的设计问题。

String 类的实现

class String{
private:
    char* m_data;
public:
    String():m_data(nullptr){}
    String(const char* str){
        if(str) {
            size_t len = strlen(str);
            m_data = new char[len+1];
            memcpy(m_data, str, len+1);
        } else {
            m_data = nullptr;
        }
    }
    String(const String& other) {
        std::cout << "拷贝构造" << std::endl;
        if(other.m_data == nullptr) {
            m_data= nullptr;
            return;
        }
        size_t len = strlen(str);
        m_data = new char[len+1];
        memcpy(m_data, str, len+1);
    }
    String& operator=(const String& other) {
        std::cout << "拷贝赋值" << std::endl;
        if(this == &other) return *this;
        if(m_data) {
            delete[] m_data;
            m_data = nullptr;
        }
        if(!other.m_data) {
            m_data = nullptr;
            return *this;
        }
        size_t len = strlen(other.m_data);
        m_data = new char[len+1];
        memcpy(m_data, other.m_data, len+1);
        return *this;
    }
    String(String&& other) {
        if(!other.m_data) {
            m_data == nullptr;
            return;
        }
        m_data = other.m_data;
        other.m_data = nullptr;
    }
    ~String() {
        if(m_data) {
            delete[] m_data;
            m_data = nullptr;
        }
    }
};

operator=的语义:将等号右边的对象完全赋值给左边的对象,同时要注意原左边对象的空间的清理以及自赋值操作。

移动构造的语义:将指针改变不需要重新分配内存。

解释为什么一定要检查自赋值

operator=函数为以下实现,注释掉自赋值操作。

String& operator=(const String& other) {
    //if(this == &other) return *this;
    if(m_data) {
        delete[] m_data;
        m_data = nullptr;
    }
    size_t len = strlen(other.m_data);
    m_data = new char[len+1];
    memcpy(m_data, other.m_data, len+1);
    return *this;
}

此时,有以下代码:

void func(){
    String a="1";
    String b="2";
    a=a;//触发operator=的情况之一
}

此时会将a.m_data的内存清理,m_data=nullptr,strlen就会出现未定义的行为。而判断m_data是否为空也是必要的,因为要把之前的内存清理,否则a原来的内存空间就泄露了。

拷贝构造是生孩子

赋值构造是换衣服

总结

代码:深拷贝和浅拷贝

拷贝构造是调用的时机要看等号左边的对象是否之前已经构造了,如已构造,则调用operator=,如未构造,此时才会调用拷贝构造。

Rule of three,一种重要的内存管理法则,指的是如果一个类需要实现以下三个函数中的一个,就必须实现三个: 拷贝构造、拷贝赋值运算符、析构函数。

后续继续打磨,打磨成的stl中的String(完善tinystl项目),比如内存交换swap、Rule of five(移动语义)、Rule of Zero(现代C++语义).


文章作者: AllenMirac
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 AllenMirac !
  目录