String summary of main components of C++ STL (Part 2: deep and shallow copy problems and overload of assignment operators)

Posted by ch3m1st on Sun, 03 Nov 2019 12:24:18 +0100

Part I: https://blog.51cto.com/14232799/2447326

Simulation of String

Next to the first step is the simulation implementation part, which mainly reflects the mastery of the first part. It is highly recommended that the friends who are learning string like me do it by themselves. Because in the interview, the interviewer always likes to let us simulate the implementation of string class.

To implement String by yourself is mainly to realize the construction of String class, copy construction, overload of assignment operator (method at the beginning of operator in the first part) and destructor.

The following is the basic simulation implementation I completed

#include<iostream>
#include<assert.h>
using namespace std;

namespace self{
    class string{
    public:
        string(const char* s = " "){
            if (s == nullptr){
                assert(false);
                return;
            }
            _s = new char[strlen(s) + 1];
            strcpy(_s, s);
        }
        ~string(){
            if (_s){
                delete[] _s;
                _s = nullptr;
            }
        }
    private:
        char* _s;
    };
}
int main(){
    self::string k = "hello";
    self::string i("world");
    self::string m;
    //self::string l(k);
    return 0;
}

The above is the string class simulation implementation without overloading the assignment operator and explicitly defining the copy constructor. Basically complete simulation implementation will be given at the end of this article (of course, there are mistakes, if you find out, please remind everyone)
The main function in the above code has a comment statement / / self::string l(k); I comment it because if I add this sentence to the code, the program will run and crash!!!!
The reason for the program crash is: when we do not explicitly define the copy construction method, the system will generate the default copy constructor, which is a shallow copy, and the final result is that object l and object k share the same memory space. Doesn't seem to be a problem?
But! When the function ends, there are big problems with the operation of calling the destructor.
Originally, every object would call a destructor to clean up its own space. But when two objects occupy the same block of space, when one object calls the destructor and the other object calls the destructor, the same block of space will be released many times! Cause program crash!
Therefore, the copy construction method cannot be used in the above code. This question leads to the next part to summarize: shallow copy and deep copy

III. shallow copy and deep copy

1. shallow copy
Here is just a definition:
Shallow copy: also known as bit copy, the compiler just copies the values in the object. If a resource is managed in an object, multiple objects will finally share the same resource. When an object is destroyed, the resource will be released. At this time, other objects do not know that the resource has been released and think it is still valid. Therefore, when the resource input operation continues, an access violation will occur. So we need to solve the problem of shallow copy and introduce deep copy into C + +.
(the string simulation in the second part is an example in advance)
Place a picture:

2. deep copy
"If a class involves resource management, its copy constructor, assignment operator overload and destructor must be explicitly given. Generally, it is provided in the form of deep copy
The above sentence is the truth!
First, the implementation of deep copy in string 'class is given

String(const String& s)
 : _str(new char[strlen(s._str)+1])   // When you see this step, you will know it's a deep copy
 {
 strcpy(_str, s._str);
 }

Let's talk about the definition of deep copy:
Each string needs space to hold the string, while using one string class object to construct another string class object. Deep copy is used: each object is allocated resources independently to ensure that there is no program crash caused by multiple space releases caused by sharing resources among multiple objects.

3. Overload of assignment operator in String
1. First, several overloads of assignment operators commonly used in string are given:
<1> <<

ostream& bit::operator<<(ostream& _cout, const self::String& s)
{
 cout << s._str;
 return _cout;
}

The overload of < is quite special, because the ostream type will be used, so expand the description here:
ostream is short for output stream, that is, output stream. A typical output stream object is the standard output stream cout in C + +.
In C + +, there are few custom ostream objects, but more direct use of cout.
ostream, as a kind of friend function of a class, often appears in < operation overload.
For example, for class A, you can define ostream & operator < (ostream & OS, const A & A);
In this way, when calling the var object of A, you can use
cout &lt;&lt; var ;

<2> =

String& operator=(String s)
 {
 swap(_str, s._str); 
 return *this;
 }

<3> +=

string& operator+=(char ch)
        {
            push_back(ch);
            return *this;
        }

<4> [ ]

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

Finally, a more complete simulation implementation of string class is given:

namespace bit
{
 class String
 {
 public:
 typedef char* iterator;
 public:
 String(const char* str = "")
 {
 _size = strlen(str);
 _capacity = _size;
 _str = new char[_capacity+1];
 strcpy(_str, str);
 }
 String(const String& s)
 : _str(nullptr)
 , _size(0)
 , _capacity(0)
 {
 String tmp(s);
 this->Swap(tmp);
 }
 String& operator=(String s)
 {
 this->Swap(s)
 return *this;
 }
 ~String()
 {
 if (_str)
 {
 delete[] _str;
 _str = nullptr;
 }
 }
 /////////////////////////////////////////////////////////////////
 // iterator
 iterator begin() {return _str;}
 iterator end(){return _str + _size;}
 /////////////////////////////////////////////////////////////////
 // modify
 void PushBack(char c)
 {
 if (_size == _capacity)
 Reserve(_capacity*2);

 _str[_size++] = c;
 _str[_size] = '\0';
 }
 String& operator+=(char c)
 {
 PushBack(c);
 return *this;
 }
 void Clear()
 {
 _size = 0;
 _str[_size] = '\0';
 }
 void Swap(String& s)
 {
 swap(_str, s._str);
 swap(_size, s._size);
 swap(_capacity, s._capacity);
 }
 const char* C_Str()const
 {
 return _str;
 }
 size_t Size()const
 size_t Capacity()const
 bool Empty()const

 void Resize(size_t newSize, char c = '\0')
 {
 if (newSize > _size)
 {
 // If the newSize is larger than the size of the underlying space, you need to re open the space
 if (newSize > _capacity)
 {
 Reserve(newSize);
 }
 memset(_str + _size, c, newSize - _size);
 }
 _size = newSize;
 _str[newSize] = '\0';
 }
 void Reserve(size_t newCapacity)
 {
 // If the new capacity is larger than the old capacity, open up space
//Bit technology
 if (newCapacity > _capacity)
 {
 char* str = new char[newCapacity + 1];
 strcpy(str, _str);
 // Release the old space and use the new space
 delete[] _str;
 _str = str;
 _capacity = newCapacity;
 }
 }
 char& operator[](size_t index)
 {
 assert(index < _size);
 return _str[index];
 }
 const char& operator[](size_t index)const
 {
 assert(index < _size);
 return _str[index];
 }
 private:
 friend ostream& operator<<(ostream& _cout, const bit::String& s);
 private:
 char* _str;
 size_t _capacity;
 size_t _size;
 };
}
ostream& bit::operator<<(ostream& _cout, const bit::String& s)
{
 cout << s._str;
 return _cout;
}

Topics: C++