13.2 Member pointer management

Classes that contain pointer members should pay special attention to the behavior of copy control. By default, copying a pointer only copies the pointer address, not the object pointed to by the pointer. Two pointers pointing to the same object will have typical shallow copy behavior, no isolation of modifications, and dangling pointers.

There are three common pointer member management strategies:

  1. For regular pointers, copy the pointer address. After copying, the two pointers point to the same object. This is also the default behavior.
  2. Smart pointers, two pointers still point to the same object, but can automatically avoid dangling pointers
  3. Value type class, directly copies the base object, and the two objects are independent of each other
1. Ordinary pointers

Ordinary pointers only copy the pointer address, exactly the same as ordinary pointers. Because only the address is copied, the two objects will share the base object.

class Pointer {
private:
    int* a;
    int b;
public:
    Pointer(int *sa, int sb): a(sa), b(sb) {}
    int get_b() {return b;}
    void set_b(int b) {this->b = b;}
    int* get_a() {return a;}
    void set_a(int* a) {this->a = a;}
    int get_a_val() {return *a;}
    void set_a_val(int a) {(*this->a) = a;}
    std::string str() {
        std::ostringstream os;
        os << "Pointer(a=" << *a << ",b=" << b << ")" << std::endl;
        return os.str();
    }
};

We define object p1 and copy object p2 from p1. Data member b is independent of data member a. If the value of data member a of p1 is modified, the value of p2 will also change because they point to the same object.

int main() {
    int* pi = new int(1);
    Pointer p1(pi, 2);
    Pointer p2(p1);

    // Non-pointer type, the two data are isolated, the modification of b by p1 does not affect p2
    p1.set_b(3);
    std::cout << "p1:" << p1.str() << "p2:" << p2.str() << std::endl;
    // Pointer type, because it points to the same object, when p1 modifies a, the value of p2 is also changed.
    p1.set_a_val(100);
    std::cout << "p1:" << p1.str() << "p2:" << p2.str() << std::endl;
}

Because the data pointed to by the pointer is shared, it is difficult to recycle the object pointed to by the pointer member. When p1 and p2 are also created, if the pointer member a is recycled in the destructor, then when p2 goes out of scope, the pointer member of p1 It becomes a dangling pointer; if the pointer is not recycled in the destructor, then when p1 and p2 are out of scope, no one will ensure that the object pointed to by the pointer member a can be recycled correctly.

int* pi = new int(1);
Pointer p1(pi, 2);
    {
    Pointer p2(p1);
    } // p2 exceeds the scope, the destructor will be called, pointer type data, the pointer will be deleted after the default destructor, and the object pointed to by the pointer will not be deleted.
std::cout << "p1:" << p1.str() << std::endl;
2. Smart pointer

Smart pointers solve to a certain extent the dilemma encountered by ordinary pointers when recycling objects pointed to by pointer members. Smart pointers provide a smart pointer class, count the number of references pointing to the basic object, and provide a destructor. When the number of references is 0, the destructor can recycle the object pointed to by the pointer.

1. Reference counting

The function of the smart pointer class is to bind pointers and reference counting together. The operations to be processed mainly include:

  1. During initialization, use the smart pointer class to save the pointer of the actual data, and set the reference count to 1
  2. When copying an object, copy the pointer address and add the smart pointer class reference pointed to by the pointer + 1
  3. During the assignment operation, the reference count of the left operand is decremented by 1. If the reference count is reduced to 0, the smart pointer type data of the left operand is recycled; the pointer of the right operand is copied and the reference count + 1
  4. When calling the destructor, determine the value of the reference count. If it is 0, delete the base object pointed to by the pointer.

Smart pointer classes hold pointers to data internally and save reference counts to basic objects, such as the RefCount class. To avoid being accessed by other classes, we set all members to private and use Pointer as a friend.

class RefCount {
    friend class Pointer;
private:
    int* a;
    unsigned count;
    RefCount(int* p) : a(p), count(1) {}
    ~RefCount() {
        delete a;
        std::cout << "RefCount destruct" << std::endl;
    }
};

The Pointer class no longer directly holds the int* pointer, but instead holds the pointer of the RefCount object. When initializing from the int* pointer, the RefCount object is automatically constructed. The RefCount constructor defaults the reference number to 1.

Pointer(int *sa, int sb): ref(new RefCount(sa)), b(sb) {}

In addition, we need to define a copy constructor to copy the ref to the new object, with the reference number + 1

Pointer(Pointer & amp;p1): ref(p1.ref), b(p1.b) {
    ref->count + + ;
}

When defining an assignment operation, the reference number of the left operand should be -1. If the reference number drops to 0 during the operation, delete the ref object; assign the right operand to the left operand, and the reference number + 1

Pointer & amp; operator=(Pointer p1) {
    if(--ref->count == 0) {
        delete ref;
    }
    ref = p1.ref;
    b = p1.b;
    ref->count + + ;
    return *this;
}

When destructing, decrement the reference count by 1. If the reference count is 0, delete the RefCount pointer.

~Pointer() {
    if(--ref.count == 0) {
        delete ref;
    }
}

At this point, the construction, copying, assignment, and out-of-scope destruction of Pointer will update the reference number. When the last object is destroyed, RefCount and the int * it points to will be recycled at the same time.

Putting all the code together, let’s test the program

#include <iostream>
#include <string>
#include <ostream>
#include <sstream>

class RefCount {
friend class Pointer;
private:
    int* a;
    unsigned count;
    RefCount(int* p) : a(p), count(1) {}
    ~RefCount() {
        delete a;
        std::cout << "RefCount destruct" << std::endl;
    }
};

class Pointer {
private:
    RefCount *ref;
    int b;
public:
    Pointer(int *sa, int sb): ref(new RefCount(sa)), b(sb) {}
    Pointer(Pointer & amp; p1) :ref(p1.ref), b(p1.b) {
        ref->count + + ;
    }
    ~Pointer() {
        std::cout << "pointer destruct" << std::endl;
        if (--ref->count == 0) {
            delete ref;
        }
    }
    Pointer & amp; operator=(const Pointer & amp;r) {
        std::cout << "operator=" << std::endl;
        r.ref->count + + ;
        if (--ref->count == 0)
            delete ref;
        ref = r.ref;
        b = r.b;
        return *this;
    }
};


int main() {
    int* pi = new int(1);
    {
        Pointer p1(pi, 2);
        Pointer p2(p1);
        Pointer p3 = p1; // copy constructor
        p3 = p2; // assignment operator
    }
}

The output of the program is like this. When p1 is built, the pointer to the RefCount object is automatically created, and the reference count is set to 1; when p2 is created, the reference count is + 1 through the copy constructor; when p3 is created, the copy constructor is used, and the reference count is + 1; finally used Assignment operator, the operation reference is -1, the right operand reference + 1, the final output is shown in the figure

3. Value type

Modifications to a value class object will result in a new copy and will not modify the original object. When copying a pointer member, you must copy the object pointed to by the pointer. In this way, the base objects pointed to by the newly created pointer members are not shared and will not affect each other. The disadvantage of this is that copying the object consumes some resources.

class Pointer {
private:
    int *a;
    int b;
public:
    Pointer(int *sa, int sb): a(new int(*sa)), b(sb) {}
    Pointer(Pointer & amp; p1) :a(new int(*p1.a)), b(p1.b) {}
    ~Pointer() {
        delete a;
    }
    Pointer & amp; operator=(const Pointer & amp;r) {
        *a = *r.a;
        b = r.b;
    }
};