面试题:String类的浅拷贝、深拷贝、写时拷贝

String的拷贝是面试中的经常会被问到的问题,所以,学懂String类是非常重要的。

下面我们先来看一段代码:

class String
{
public:
    String(const char* pStr = "")//构造函数
    {
        if (pStr == NULL)
        {
            _pStr = new char[1];
            *_pStr = '\0';
        }
        else
        {
            _pStr =new char[strlen(pStr) + 1];
            strcpy(_pStr, pStr);
        }
    }

    String(const String &s)//拷贝构造函数,相当于系统默认合成
        :_pStr(s._pStr){}

    String& operator = (const String& s)//赋值运算符的重载
    {
        if (this != &s)
        {
            _pStr = s._pStr;
        }
        return *this;
    }

    ~String()//析构函数
    {
        if (_pStr)
        {
            delete[] _pStr;
            _pStr = NULL;
        }
    }

private:
    char* _pStr;
};

void Funtest()
{
    String s1;
    String s2("1234");
    String s3(s2);//程序崩溃
    /*String s4;
    s4 = s2;*/
}

int main()
{
    Funtest();
    system("pause\n");
    return 0;
}

一运行发现程序崩溃了,那么问题到底出在哪里呢?对代码进行调试后我们可以发现如下现象:
这里写图片描述
可以看到,s2和s3的地址是指向同一块空间的,那在调用析构函数时岂不是对一段空间析构了两次吗?这里不止是程序崩溃,还出现了内存泄漏。这就是String的经典反例:浅拷贝。面试的时候可千万别写出这样的代码哦。

那么解决这种问题的方案是什么呢?下面就来介绍:深拷贝
(1)普通版本

class String
{
public:
    String(const char* pStr = "")
    {
        if (pStr == NULL)
        {
            _pStr = new char[1];
            *_pStr = '\0';
        }
        else
        {
            _pStr = new char[strlen(pStr) + 1];
            strcpy(_pStr, pStr);
        }
    }

    String & operator = (const String &s)
    {
        if (this != &s)
        {
            char *pTmp = new char[strlen(s._pStr) + 1];//新开辟一块空间
            strcpy(pTmp, s._pStr);
            delete[] _pStr;
            _pStr = pTmp;
        }
        return *this;
    }

    ~String()
    {
        if (_pStr)
        {
            delete[] _pStr;
            _pStr = NULL;
        }
    }

    String(const String &s)
        :_pStr(new char[strlen(s._pStr) + 1])
    {
        strcpy(_pStr, s._pStr);
    }

private:
    char *_pStr;
};

void Funtest()
{
    String s1;
    String s2("1234");
    String s3(s2);
    String s4;
    s4 = s3;
}

int main()
{
    Funtest();
    return 0;
}

调试结果如下:
这里写图片描述
此时,s2,s3,s4不再使用同一块空间,这便解决了浅拷贝中内存泄漏以及程序崩溃的问题,深拷贝还有一个简洁版,如下:
(2)简洁版

class String
{
public:
    String(const char* pStr = "")
    {
        if (pStr == NULL)
        {
            _pStr = new char[1];
            *_pStr = '\0';
        }
        else
        {
            _pStr = new char[strlen(pStr) + 1];
            strcpy(_pStr, pStr);
        }
    }

    String(const String &s)
        :_pStr(NULL){}
    {
        _pStr = new char[1];
        String strTmp(s._pStr);
        std::swap(_pStr, strTmp._pStr);
    }

    String &operator = (const String &s)
    {
        if (this != &s)
        {
            String strTmp(s);
            std::swap(_pStr, strTmp._pStr);
        }
        return *this;
    }

    ~String()
    {
        if (_pStr)
        {
            delete[] _pStr;
            _pStr = NULL;
        }
    }

private:
    char *_pStr;
};

当我们需要写的时候才去新开辟内存空间。这种方法就是写时拷贝。这也是一种解决由于浅拷贝使多个对象共用一块内存地址,调用析构函数时导致一块内存被多次释放,导致程序奔溃的问题。这种方法需要用到引用计数:使用int *保存引用计数;采用所申请的4个字节空间。

class String
{
public:
    String(const char *pStr = "")
    {
        if (pStr == NULL)
        {
            _pStr = new char[1 + 4];
            *((int *)pStr) = 1;
            _pStr = (char*)(((int *)_pStr) + 1);
            *_pStr = '\0';
        }
        else
        {
            _pStr = new char[strlen(pStr) + 1 + 4];
            strcpy(_pStr, pStr);
            *((int *)_pStr - 1) = 1;
        }
    }

    String(const String &s)
        :_pStr(s._pStr)
    {
        ++Getcount();
    }

    String& operator = (const String &s)
    {
        if (this != &s)
        {
            Release;
            _pStr = s._pStr;
            --Getcount();
        }
        return *this;
    }

    char& operator[](size_t index)
    {
        if (Getcount() > 1)
        {
            char *pTmp = new char[strlen(_pStr) + 1 + 4];
            strcpy(pTmp + 4, _pStr);
                --Getcount();
            _pStr = pTmp + 4;
            Getcount() = 1;
        }
        return _pStr[index];
    }

    const char & operator[](size_t index)const
    {
        return _pStr[index];
    }

    friend ostream operator<<(ostream& output, const String& s)
    {
        output << s._pStr;
        return output;
    }
private:
    char *_pStr;
    int& Getcount()
    {
        return *((int*)_pStr - 1);
    }
    void Release()
    {
        if (_pStr && (0 == --Getcount()))
        {
            _pStr = (char *)((int *)_pStr - 1);
            delete _pStr;
        }
    }
};

修改 String 数据时,先判断计数器是否为 0( 0 代表没有其他对象共享内存空间),为 0 则可以直接使用内存空间(如上例中的 s2 ),否则触发写时拷贝,计数 -1 ,拷贝一份数据出来修改,并且新的内存计数器置 0 ; string 对象析构时,如果计数器为 0 则释放内存空间,否则计数也要 -1 。

写时拷贝存在的线程安全问题

线程安全就是多线程访问时,采用了加锁机制,当一个线程访问该类的某个数据时,进行保护,其他线程不能进行访问直到该线程读取完,其他线程才可使用。不会出现数据不一致或者数据污染。 线程不安全就是不提供数据访问保护,有可能出现多个线程先后更改数据造成所得到的数据是脏数据。

  • 2
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值