Generally speaking, there are two options for copy semantics of type objects: you can define copy operations to make the behavior of a class look like a value or a pointer.
Class behaves like a value, meaning that it should also have its own state. When we copy an object like value, the copy and the original object are completely independent. Changing the copy also changes the original, and vice versa.
Classes that behave like pointers share state. The replica uses the same underlying data as the original, and changing the replica will change the original.
In the standard library classes we used, the standard library container and string class behave like a value. The shared_ptr class provides pointer like behavior. I/O types and unique_ptr do not allow copying or assignment, so they do not behave like values or pointers.
Exercise 13.22: suppose we want HasPtr to behave like a value. That is, each object has its own copy of the string member it points to. Write copy constructor and copy assignment operator for HasPtr class.
#include <iostream> #include <string> using namespace std; class HasPtr { public: HasPtr(const string &s = string()):ps(new string(s)),i(0){} //Default constructor HasPtr(const HasPtr& p) { //copy constructor ps = new string(*p.ps); i = p.i; } HasPtr& operator=(const HasPtr&); //copy assignment operator HasPtr& operator=(const string&); //Assign new string string& operator*(); //Quoting ~HasPtr(); //Destructor private: string* ps; int i; }; HasPtr::~HasPtr() { delete ps; //Free string memory } inline HasPtr& HasPtr::operator=(const HasPtr &rhs) { auto newps = new string(*rhs.ps); //Object to which the copy pointer points delete ps; //Destroy the original string ps = newps; //Point to new string i = rhs.i; //Use built-in int assignment return *this; //Returns a reference to this object } HasPtr& HasPtr::operator=(const string& rhs) { *ps = rhs; return *this; } string& HasPtr::operator*() { return *ps; } int main() { HasPtr h("hi mom"); HasPtr h2(h); //Behavior class values, h2, h3 and h point to different string s HasPtr h3 = h; h2 = "hi dad"; h3 = "hi son"; cout << "h: " << *h << endl; cout << "h2: " << *h2 << endl; cout << "h3: " << *h3 << endl; return 0; }
Class of behavior image value
To provide the behavior of class values, each object should have its own copy for class managed resources. This means that each HasPtr object must have its own copy for the string pointed to by ps. in order to change the copy, HasPtr needs to:
1. Define a copy constructor to copy the string instead of the copy pointer
2. Define a destructor to release string
3. Define a copy assignment operator to release the current string of the object and copy the string from the right operand
class HasPtr { public: HasPtr(const string& s = string()) :ps(new string(s)), i(0) {} //Default constructor HasPtr(const HasPtr& p) { //copy constructor ps = new string(*p.ps); //For the string pointed to by ps, each HasPtr object has its own copy i = p.i; } HasPtr& operator=(const HasPtr&); //copy assignment operator ~HasPtr() { delete ps; } //Destructor private: std::string* ps; int i; };
Class value copy assignment operator
Assignment operators usually combine the operations of destructors and constructors. Like a destructor, assignment destroys the resource of the left operand. Similar to the copy constructor, assignment copies data from the right operand. But it should be ensured to operate with correct data
HasPtr& HasPtr::operator=(const HasPtr &rhs) { auto newps = new string(*rhs.ps); //Copy the underlying string delete ps; //Destroy old memory ps = newps; //Copy data from right operand to local i = rhs.i; return *this; //Return this object }
When writing assignment operators, there are two things to remember:
1. If you assign an object to itself, the assignment operator must work correctly
2. Most assignment operators combine the work of destructors and copy constructors
When writing an assignment operator, a good pattern is to copy the right operand into a local temporary object. When the copy is complete, destroying the existing members of the left operand is complete. Once the resource of the left operand is destroyed, only the data is copied from the temporary object to the members of the left operand.
//Write wrong assignment operator case HasPtr& HasPtr::operator=(const HasPtr &rhs) { delete ps; //Destroy the original string //If rhs and * this are the same object, we will copy the data from the released memory auto newps = new string(*rhs.ps); //Object to which the copy pointer points ps = newps; //Point to new string i = rhs.i; //Use built-in int assignment return *this; //Returns a reference to this object }
Therefore, you should copy the right operand before destroying the left operand resource
Exercise 13.24 what happens if the destructor and copy constructor are not defined in the HasPtr version?
If no destructor is defined, the destructor synthesized when destroying the HasPtr object will not release the memory pointed to by the pointer ps, resulting in a memory leak.
If the copy constructor is not defined, when copying the HasPtr object, the synthesized copy constructor simply copies the value of ps so that two hasptrs point to the same string. When one HasPtr modifies the string content, the content of the string in the other HasPtr is also modified. When one HasPtr is destroyed, the ps in the other HasPtr becomes a null dangling pointer
Define a class that behaves like a pointer
Our class still needs a destructor to release the memory allocated by the constructor that takes the string parameter. In this case, however, the destructor cannot unilaterally release the associated string. Only when the last HasPtr pointing to string is destroyed can the string be released.
The best way to make a class behave like a pointer is to manage the resources in the class by using shared ﹐ PTR. The shared < PTR class records how many users share the object it points to. When there is no object to use, the shared \ u PTR class is responsible for releasing resources.
When we want to manage resources directly, we can use reference counting
Reference counting
1. In addition to initializing objects, each constructor creates a reference count. Used to record how many objects share state with the object being created. When we create an object, only one object shares the state, so set the counter to 1.
2. The copy constructor does not assign a new counter. Instead, copy the data members of a given object, including counters. The copy constructor increments the share counter, indicating that the state of the changed object is shared by another user.
3. Destructor decrement counter, indicating that there is one less user in the shared state. If the counter changes to 0, the destructor releases the state.
4. The copy assignment operator increments the counter of the right operand and decrements the counter of the left operand. If the counter of the left operand changes to 0, which means there is no user in its shared state, the copy assignment operator must destroy the state.
The only problem is where to store the reference counter. Counter cannot be a member of HasPtr object directly
The following example explains why:
HasPtr p1("Hi"); HasPtr p2(p1); HasPtr p3(p1); //p1, p2, p3 all point to the same string
If the counter is saved in each object, when p2 is created, we can increment the counter in p1 and copy it to p2, but when p3 is created, we are wrong if the counter in p1 is copied to p3.
One solution is to keep the counters in dynamic memory. When we create an object, we also assign a counter. When copying or assigning objects, we copy the pointer to the counter. In this way, both the copy and the original object point to the same counter.
class HasPtr { public: //Constructor assigns a new string and a new counter, setting the counter to 1 HasPtr(const string& s = string()) :ps(new string(s)), i(0), use(new std::size_t(1)) {} HasPtr(const HasPtr& p) : ps(p.ps), i(p.i), use(p.use) {++*use;} HasPtr& operator=(const HasPtr&); //copy assignment operator ~HasPtr(); //Destructor private: std::string* ps; int i; std::size_t *use; //Used to record how many members of the object share *ps };
Copy member tamper reference count of class pointer
When copying or assigning a HasPtr object, we want both to point to the same string. That is, when copying a HasPtr, we will copy the ps itself instead of the string pointed by ps. when copying, we will also increase the counter associated with the string.
The destructor cannot delete ps unconditionally. There may be other objects pointing to this memory. The destructor should decrement the reference count to indicate that there is one less object sharing the string. If the counter changes to 0, the destructor frees the memory pointed to by ps and use:
HasPtr::~HasPtr(){ if (--*use == 0){ //If the counter changes to 0 delete ps; //Free string memory delete use; //Free counter memory } }
The copy assignment operator does the same as usual for copy constructors and destructors. That is, it must increment the reference count of the right operand (that is, the work of the copy function), decrement the reference count of the left operand, and release memory if necessary (that is, the work of the destructor)
Moreover, assignment operators must handle word assignments. We do this by incrementing the count in the rhs and then decrementing the count in the left operand. In this way, when two objects are the same, the counter has been incremented before we check whether ps and use should be released:
HasPtr& HasPtr::operator=(const HasPtr &rhs) { ++*rhs.use; //Increment the reference count of the right operand if (--*use == 0){ //Then decrement the reference count of this object delete ps; delete use; } ps = rhs.ps; i = rhs.i; use = rhs.use; return *this; //Return this object }
Exercise 13.27: define HasPtr using the reference count version
#include <iostream> #include <string> using namespace std; class HasPtr { public: //Constructor assigns a new string and a new counter, setting the counter to 1 HasPtr(const string& s = string()) :ps(new string(s)), i(0), use(new std::size_t(1)) {} HasPtr(const HasPtr& p) : ps(p.ps), i(p.i), use(p.use) { ++* use; } HasPtr& operator=(const HasPtr&); //copy assignment operator HasPtr& operator=(const string& rhs); //Assign new string ~HasPtr(); //Destructor string& operator*(); //Quoting private: std::string* ps; int i; std::size_t* use; //Used to record how many members of the object share *ps }; HasPtr::~HasPtr() { if (-- * use == 0) { //If the counter changes to 0 delete ps; //Free string memory delete use; //Free counter memory } } HasPtr& HasPtr::operator=(const HasPtr& rhs) { ++ * rhs.use; //Increment the reference count of the right operand if (-- * use == 0) { //Then decrement the reference count of this object delete ps; delete use; } ps = rhs.ps; i = rhs.i; use = rhs.use; return *this; //Return this object } HasPtr& HasPtr::operator=(const string& rhs) { *ps = rhs; return *this; } string& HasPtr::operator*() { return *ps; } int main() { HasPtr h("hi mom"); HasPtr h2(h); //Behavior class pointer, h2, h3 and h point to the same string HasPtr h3 = h; h = "hi dad"; //If you change the value pointed to by the string pointer in the h object, then h2 and h3 will also change to the same value cout << "h: " << *h << endl; cout << "h2: " << *h2 << endl; cout << "h3: " << *h3 << endl; return 0; }
Exercise 13.28: given the following classes, implement a default constructor and the necessary copy control members for them
class TreeNode{ private: std::string value; int count; TreeNode *left; TreeNode *right; }; class BinStrNode{ private: TreeNode *root; };
This structure is a binary tree data structure, which implements class pointer behavior, and count is used as reference count
Default constructor:
TreeNode::TreeNode() : value(""), count(1), left(nullptr), right(nullptr) {} BinStrTree::BinStrTree() : root(nullptr) {}
Copy constructor:
BinStrTree::BinStrTree(const BinStrTree& bst) : root(bst.root) { //Copy the whole tree root->CopyTree(); //The entire tree should be copied, not the root node } void TreeNode::CopyTree(void) { //Copy the subtree whose root is this node, and increase the reference count if (left) { left->CopyTree(); //Left subtree is not empty, copy left subtree } if (right) { right->CopyTree(); //Right subtree is not empty, copy right subtree } count++; } //Copy a shell tree from a node TreeNode::TreeNode(const TreeNode &tn) : value(tn->value), count(1), left(tn->left), right(tn->right) { if (left) { left->CopyTree(); //Left subtree is not empty, copy left subtree } if (right) { right->CopyTree(); //Right subtree is not empty, copy right subtree } }
Destructor:
BinStrTree::~BinStrTree() { //Release the whole tree if (!root->ReleaseTree()) { //Release the entire tree, not just the root node delete root; //Free node space with reference count of 0 } } int TreeNode::ReleaseTree() { //Release the subtree with this node as the root if (left) { if (!left->ReleaseTree()) { delete left; //Left child reference count is 0, freeing its space } } if (right) { if (!right->ReleaseTree()) { delete right; } } count--; return count; } TreeNode::~TreeNode() { if (count) ReleaseTree(); }