天天看点

c++string类(浅拷贝,深拷贝,写实拷贝的优点和缺陷)

前言

楼主现在大三,鉴于年后想找一个实习,故寒假在家复习一些学过的知识点,前几天看到一篇关于string类的文章,细读之后,发现我还是有许多遗漏的地方,经过这几天的学习,想通过这篇文章,将有关string类的东西都记录一下。

重点介绍:

  1. 浅拷贝
  2. 深拷贝
  3. 写时拷贝
  4. 写时拷贝隐含的问题

下面的文章将会以上面的四点进行展开,至于代码,我会在linux和vs下都跑一下,因为我发现编译器不同。有些问题是不易被发现的。

浅拷贝问题

代码贴一下,然后再解释

/*
* string类的浅拷贝
*/

class String
{
public:
    String()
        :str(new char[])
    {
        *str = '\0';
    }
    String(const char* str)
        :str(new char[strlen(str) + ])
    {
        strcpy(str, str);
    }

    String(const String& str)
        :str(str.str)
    {}

    String& operator=(const String& str)
    {
        if (this != &str)
        {
            str = str.str;
        }
        return *this;
    }

    ~String()
    {
        if (str)
        {
            printf("~String()\n");
            delete[] str;
            str = nullptr;
        }
    }
private:
    char* str;
};
           

缺点:多个对象指向同一个地址,当析构时,就会出现对同一块内存多次释放的问题。

vs2013:

c++string类(浅拷贝,深拷贝,写实拷贝的优点和缺陷)

调用两次析构函数,然后程序崩溃了。

linux下:

c++string类(浅拷贝,深拷贝,写实拷贝的优点和缺陷)

调用两次析构函数,程序运行结束,并没有崩溃。

看源代码可知,浅拷贝存在这种多个对象指向同一块地址的问题,会引发多次析构的问题,然后vs和gcc两个不同的编译器对于这一现象的默认行为是不同的。

询问了一下老师,说对于这种行为,是标准手册中未定义的,则具体的行为由编译器决定。

深拷贝

对于深拷贝,我实现的函数比较多,因为主要目的还是为了复习知识点,一般再面试中,如果面试官让你实现string类也是实现简易的深拷贝,至于为什么不写高大山的COW(Copy On Write)即写时拷贝,下文会提到。

// Sting类的深拷贝实现
class String
{
public:
    String()
        :_str(new char[])
    {
        *_str = '\0';
    }

    String(const char* str)
        :_str(new char[strlen(str) + ])
    {
        strcpy(_str, str);
    }

    String(const String& rhs)
        :_str(new char[rhs.Size() + ])
    {
        strcpy(_str, rhs.C_Str());
    }

    ~String()
    {
        delete[] _str;
    }

    // 最传统的写法
    String& operator=(const String& rhs)
    {
        if (this != &rhs)
        {
            char* tmp = new char[rhs.Size() + ];
            delete[] _str;
            strcpy(tmp, rhs.C_Str());
            _str = tmp;
        }
        return *this;
    }

    //现代写法
    /*String& operator=(const String& rhs)
    {
        String tmp(rhs);
        Swap(tmp);
        return *this;
    }*/

    //最简洁的写法
    /*String& operator=(String rhs)
    {
        Swap(rhs);
        return *this;
    }*/

    // C++ 11中的实现
    /*String(String&& rhs)
        : _str(rhs._str)
    {
        rhs._str = nullptr;
    }
    String& operator=(String&& rhs)
    {
        Swap(rhs);
        return *this;
    }*/


    //辅助函数的实现
    size_t Size()const
    {
        return strlen(_str);
    }

    const char* C_Str()const
    {
        return _str;
    }

    void Swap(String& rhs)
    {
        std::swap(_str, rhs._str);
    }

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

    //重载了一些运算符
    friend std::ostream& operator<<(std::ostream& out, const String& rhs);
    friend std::istream& operator>>(std::istream& in, const String& rhs);
    friend String operator+(const String& str1, const String& str2);
    friend String operator+=(String& str1, const String& str2);
    friend bool operator ==(const String& str1, const String& str2);
    friend bool operator !=(const String& str1, const String& str2);
private:
    char* _str;
};

std::ostream& operator<<(std::ostream& out, const String& rhs)
{
    out << rhs._str;
    return out;
}

std::istream& operator>>(std::istream& in, const String& rhs)
{
    in >> rhs._str;
    return in;
}

String operator+(const String& str1, const String& str2)
{
    String tmp;
    delete[] tmp._str;
    size_t new_length = str1.Size() + str2.Size();
    tmp._str = new char[new_length + ];
    strcpy(tmp._str, str1._str);
    strcat(tmp._str, str2._str);
    return tmp;
}

String operator+=(String& str1, const String& str2)
{
    str1 = str1 + str2;
    return str1;
}

bool operator==(const String& str1, const String& str2)
{
    return strcmp(str1._str, str2._str);
}

bool operator!=(const String& str1, const String& str2)
{
    return !(strcmp(str1._str, str2._str));
}
           

注:深拷贝解决了浅拷贝多个对象指向一块内存的问题,但是同时也引入了自己的问题。

即只要对象间进行拷贝或者赋值,都是重新开辟内存。

但有的字符串拷贝或赋值之后,并没有发生变化,故此引入了写时拷贝的概念。

对于上面的深拷贝,主要掌握拷贝构造和赋值运算符的几种实现,都了解一下思想。

写时拷贝(Copy On Write)

写时拷贝,即再对象的实现中加入一个引用计数,只要对象不进行写入操作,即[],就只将引用计数进行+1操作,只有当引用计数为1时,才真正释放对象。

对于写时拷贝的实现,由两种实现思路。

c++string类(浅拷贝,深拷贝,写实拷贝的优点和缺陷)

由于先在vs上跑,刚开始多开辟四个字节保存count,没什么毛病,拿到linux下跑,直接coredump了,所以一定要注意这个细节。

1.0引用计数分开

先来看第一种实现思路:

//1.0写实拷贝
class String
{
public:
    String()
        :_str(new char[])
        , _count(new int())
    {
        *_str = '\0';
    }

    String(const char* str)
        :_str(new char[strlen(str) + ])
        , _count(new int())
    {
        strcpy(_str, str);
    }

    ~String()
    {
        if (--(*_count) == )
        {
            delete[] _str;
            delete _count;
            _str = nullptr;
            _count = nullptr;
        }
    }

    String(const String& rhs)
        :_str(rhs._str)
        , _count(rhs._count)
    {
        (*_count)++;
    }

    String& operator=(const String& rhs)
    {
        if (this != &rhs)
        {
            if (--(*_count) == )
            {
                delete[] _str;
                delete _count;
            }
            _str = rhs._str;
            _count = rhs._count;
            (*_count)++;
        }
        return *this;
    }

    //只有当真正修改时,才重新开辟内存
    char& operator[](size_t index)
    {
        if (*_count > )
        {
            char* tmp = new char[strlen(_str) + ];
            strcpy(tmp, _str);
            _str = tmp;
            (*_count)--;
            _count = new int();
        }
        return *(_str + index);
    }

    const char* C_Str()
    {
        return _str;
    }
    friend std::ostream& operator<<(std::ostream& out,String& rhs);
private:
    char* _str;
    int*  _count;//思考为什么不用int _count或者static ——count
};

std::ostream& operator<<(ostream& out,String& rhs)
{
    out<<rhs._str;
    return out;
}
           

为了测试写时拷贝,所以我重载了标准输出和C_Str()函数,方便一会打印。

测试思路:

先只进行拷贝,然后打印地址。(按照写时拷贝的思想,没进行修改操作,不会重新开辟空间)

然后修改其中一个对象,再次打印地址。(这时应该重新开辟了空间)。

代码如下:

void TestCOW()
{
    String s1("hello");
    String s2;
    s2 = s1;
    printf("进行写入前:\n");
    cout<<"s1 :"<<s1;
    printf("   地址:%p\n",s1.C_Str());
    cout<<"s2 :"<<s2;
    printf("   地址:%p\n",s2.C_Str());

    s2[] = 'H';

    printf("进行写入后:\n");
    cout<<"s1 :"<<s1;
    printf("   地址:%p\n",s1.C_Str());
    cout<<"s2 :"<<s2;
    printf("   地址:%p\n",s2.C_Str());
}
           

linux下跑:

c++string类(浅拷贝,深拷贝,写实拷贝的优点和缺陷)

vs2013下跑:

c++string类(浅拷贝,深拷贝,写实拷贝的优点和缺陷)

结果都符合预期。

2.0引用计数和字符串一起开辟

class String
{
public:
    String()
        :str(new char[ + ])
    {
        str = str + ;
        *str = '\0';
        int count = Count(str);
        count = ;
    }

    String(char* str)
        :str(new char[strlen(str) +  + ])
    {
        str = str + ;
        strcpy(str, str);
    }

    String(const String& rhs)
        :str(rhs.str)
    {
        ++Count(str);
    }

    String& operator=(const String& rhs)
    {
        if (--Count(str) == )
        {
            delete[](str + );
            str = nullptr;
        }
        str = rhs.str;
        ++Count(str);
        return *this;
    }

    ~String()
    {
        if (--(Count(str)) == )
        {
            delete[] str;
            str = nullptr;
        }
    }

    char& operator[](size_t index)
    {
        if (Count(str) > )
        {
            char* tmp = new char[strlen(str) +  + ];


        }
    }

    int& Count(char* str)
    {
        return *(int*)(str - );
    }
    char* str;
};
           

由于Copt-On-Write本身存在的问题,C++11开始,已经摒弃了这一技术。

有兴趣去可以写个小例子测一下。

关于具体的开发中写时拷贝的缺陷,下面给出几篇比较好的博文,可以看一下。

写时拷贝的缺陷和问题:http://blog.jobbole.com/100844/

string类写时拷贝:https://coolshell.cn/articles/12199.html

面试中string类的书写及写时拷贝缺点:https://coolshell.cn/articles/10478.html

继续阅读