Deep and shallow copy of String class in C++

1. String class, only the constructor and destructor are given, the copy constructor and assignment operator overloading are all synthesized by the compiler.

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

private:
    char* _str;
};

int main()
{
    String s1;
    String s2("123456");
    String s3(s2);
    s1 = s2;
    return 0;
}

The above code compiles with no errors, but an error occurs when the program runs. The program calls the constructor to generate the object s1. Since our constructor is the default constructor, a space will be opened to store '\0'. s2 also calls the constructor to generate the object s2, and has its own memory to store the string "123456\0". Since the above code has no explicit copy constructor definition and assignment operator overloading, s3 is generated by the copy constructor synthesized by the compiler and the copy constructor s2. s1 assignment operation s2 gets the content. The overloaded assignment operator synthesized by the compiler just points the _str of s1 to the space of s2, and does not release and mark the space of s1, so the space of s1 cannot be found and the space is leaked.

You can see that the content of the object s1s2s3 is "123456"

It can be seen that the contents of objects s1, s2, and s3 are all "123456".
Since three objects are generated, the compiler will automatically call the destructor at the end of the program. What the destructor performs is to free the space of the current object and set the _str pointer in the object to NULL. When calling the destructor, first destruct s3, release the memory pointed to by _str in object s3, and point to NULL. When destructing s2 again, I want to release the memory pointed to by _str in s2, and an error occurs.
We can see that the _str of the 3 objects all point to the same piece of memory:

write picture description here

Since the s3 object has already released the space when it is destructed, it cannot be released when it is released in s2. So we can see that the overloading of the assignment operator and the copy constructor synthesized by the compiler itself just gives the value of the object directly to the current object, and does not open up space for the current object. Then there is a space that is used by multiple objects.
This is called a shallow copy, where a block of space is used by multiple objects. When we call the destructor, if we do not deal with this situation, we directly release the space, which will cause the program to crash.


2. Solve the shallow copy method 1: ordinary deep copy

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

    String& operator=(const String& s)
    {
        if (&s != this)
        {
            if (_str)
                delete _str;//释放原有空间
            _str = new char[strlen(s._str) + 1];
            strcpy(_str, s._str);
        }
        return *this;
    }


    ~String()
    {
        if (_str != NULL)
        {
            delete _str;
            _str = NULL;
        }
    }

private:
    char* _str;
};

int main()
{
    String s1;
    String s2("123456");
    String s3(s2);
    s1 = s2;
    return 0;
}

The String class is deeply copied, explicitly defined by itself, and the copy constructor and assignment operator are overloaded. When calling the copy constructor and the overloaded assignment operator, they open up their own memory to store strings. It solves the problem that multiple objects share the same space during shallow copying. When an object is deleted, the destructor releases the object's own space.

Each object has its own space: the
write picture description here
destructor is called, which frees its own space:
write picture description here

write picture description here


3. Solve the shallow copy method 2: deep copy of the introduction version

class String
{
public:
    String(const char* str = "")
    {
        if (str == NULL)
        {
            _str = new char[1];
            _str = '\0';
        }
        else
        {
            _str = new char[strlen(str) + 1];
            strcpy(_str, str);
        }
    }
    String(const String& s)
                :_str(NULL)                //一定要初始化,否则该对象和tmp交换_str的时候,                             
    {                                    //tmp调用析构函数时找不到该对象原来_str所指向的地方
        String tmp(s._str);
        std::swap(_str, tmp._str);
    }
    String& operator=(String s)
    {
        std::swap(_str, s._str);
        return *this;
    }
    ~String()
    {
        if (_str != NULL)
        {
            delete _str;
            _str = NULL;
        }
    }
private:
    char* _str;
};

int main()
{
    String s1;
    String s2("123456");
    String s3(s2);
    s1 = s2;
    return 0;
}

The compact version of the deep copy, and the normal version of the deep copy, both solve the problem of multiple objects sharing a space in the shallow copy.
The concise version of deep copy, when copying the constructor, constructs a temporary object, copies the value of s2 into it, and realizes the copy construction by exchanging the pointer of the temporary object and the _str of the s3 object, while s3 and s2 do not share the same space. The copy constructor must initialize the _str pointer of the object, otherwise, after the swap, the _str of the temporary variable tmp will point to an inaccessible space, causing the program to crash.
The concise version of deep copy, when the assignment operator is overloaded, the parameter is an object s constructed by copying, and the _str of the object s is pointed to by the _str of the object. Compared with the ordinary deep copy, the ordinary deep copy method first releases the original space, then applies for a new space, and then copies. Applying for space may fail and is not safe. So the concise version is safer and more concise.

write picture description here


4. Solve the problem of shallow copy mode: reference counting implementation (shallow copy)

1. Using non-static member variable counters, each class has an independent counter, and when copying and assigning objects, the value of the counter needs to be modified, and there is a lack of commonality between object counters.
2. Using static member variables, different objects need independent memory blocks and independent counters, which lack independence.
3. The use of member pointers satisfies commonality and independence.

class String
{
public:
    String(const char* str = "")
    {
        if (str == NULL)
        {
            _str = new char[1];
            _str = '\0';
        }
        else
        {
            _str = new char[strlen(str) + 1];
            strcpy(_str, str);
        }
        _pCount = new int[1];
        (*_pCount) = 1;
    }
    String(const String& s)
    {
        _str = s._str;
        _pCount = s._pCount;
        (*(s._pCount))++;
    }
    String& operator=(const String& s)
    {

        if (&s != this)
        {
            if (*_pCount == 1)
            {
                delete _str;
                delete _pCount;
            }
            _str = s._str;
            _pCount = s._pCount;
            (*(s._pCount))++;
        }
        return *this;
    }
    ~String()
    {
        if ((_str != NULL)&&((--(*_pCount)) == 0))//判断是否为空,及引用计数是否为0
        {
            delete _str;
            delete _pCount;
            _pCount = NULL;
            _str = NULL;
        }
    }
private:
    char* _str;
    //int _count; 
    // static int _count; 
     int *_pCount; 
};

int main()
{
    String s1;
    String s2("123456");
    String s3(s2);
    s1 = s2;
    system("pause");
    return 0;
}

Using reference counting also needs to open up space for pointers, resulting in a lot of memory fragmentation, so we can optimize so that the counter and the string exist in the same piece of memory. The optimization is as follows:

class String
{
public:
    String(const char* str = "")
    {
        if (str == NULL)
        {
            _str = new char[4+1];//4个字节是开辟给计数器的
            _str += 4;     //把指针移到字符串开始的位置
            *((int *)(_str - 4)) = 1;
            _str = '\0';
        }
        else
        {
            _str = new char[strlen(str) + 1 + 4];
            _str += 4;
            *((int *)(_str - 4)) = 1;
            strcpy(_str, str);
        }
    }
    String(const String& s)
    {
        _str = s._str;
        ++(*((int *)(_str - 4)));
    }
    String& operator=(const String& s)
    {
        if (_str != s._str)
        {
            if (*((int *)(_str - 4)) == 1)
            {
                delete[](_str - 4);
            }
            _str = s._str;
            ++(*((int *)(_str - 4)));
        }
        return *this;
    }
    ~String()
    {
        if (((*((int *)(_str - 4)))--) == 1)
        {
            delete[](_str - 4);
            _str = NULL;
        }
    }
    char& operator[](size_t index)  //写时拷贝,如果改变一个对象的内容,再开辟另一块内存出来存放
    {
        if (*((int *)(_str - 4)) > 1)
        {
            char *tmp = new char[strlen(_str) + 1 + 4];
            tmp += 4;
            *((int *)(tmp - 4)) = 1;
            strcpy(tmp, _str);
            *((int *)(_str - 4)) -= 1;
            _str = tmp;
        }
        return _str[index];
    }
private:
    char* _str;
    //int _count; 
    // static int _count; 
    // int *_pCount; 
};


int main()
{
    String s1;
    String s2("123456");
    String s3(s2);
    s1 = s2;
    S1[3] = 'A';
    system("pause");
    return 0;
}

ps: The last implemented String class has thread safety issues. Why is there a thread safety issue?
Because in the thread, each thread is running in a time slice switching in turn. If a thread just wants to generate object s3 by copying s2, the time slice is just to call the copy constructor, and the parameters are passed. At this time, the time slice is over, and it is the turn of the next thread, and this thread is destructing s2 and has finished running. At this time, it is the turn of the first thread of the time slice, and it continues to pick up the position it ran last time. At this time, it appears Error, found that s2 is gone.

The above is the string class I summarized, I hope it will be helpful to those who are learning C++ deep and shallow copying.

Guess you like

Origin http://43.154.161.224:23101/article/api/json?id=325169608&siteId=291194637