动态内存和智能指针

Smart pointers, as one of the most important features of C++11, were originally recorded in C++11 Syntactic Sugar, but this part is so important that I have recently been relatively free (逃) and decided to summarize it in detail separately.

In C, dynamic memory allocation and deallocation are achieved through malloc and free:

1
void* malloc( size_t size );

We can allocate a contiguous block of memory for our use:

1
2
3
4
5
6
7
8
9
10
int *p1 = malloc(4*sizeof(int));
for(int n=0; n<4; ++n){ // populate the array
p1[n] = n*n;
printf("p1[%d] == %d\n", n, p1[n]);
}
// Output results
p1[0] == 0
p1[1] == 1
p1[2] == 4
p1[3] == 9

In C++, dynamic memory management is done through another pair of operators: new allocates space for an object in dynamic memory and returns a pointer to that object, allowing us to optionally initialize the object; delete takes a pointer to a dynamic object, destroys that object, and frees the associated memory.

The following code demonstrates this:

1
2
3
4
5
6
7
8
9
// Use new to create a string object and assign it to a string pointer
string *testNew=new string("HelloWorld");
cout<<testNew<<"\t"<<*testNew<<endl;
// delete the previously created object (the object pointed to by testNew)
delete(testNew);
cout<<testNew<<"\t"<<*testNew<<endl;
testNew=nullptr;
// Set dynamic object to nullptr
cout<<testNew<<"\t"<<*testNew<<endl;

As seen in the output, the data stored in the location pointed to by testNew before and after the delete operation (on Windows):

The value stored at the memory address pointed to after releasing with delete (Dangling Pointer: a pointer that points to a block of memory that once held a data object but is now invalid) is undefined.

On Linux, compiling and running this would result in Segmentation fault (core dumped).

Therefore, when we use delete to release a dynamic object, it is best to immediately set it to nullptr (null pointer).

As you can see, when using new/delete to manually manage memory, it is essential to remember that new/delete must appear in pairs to avoid creating a dynamic object with new without subsequently deleting it, resulting in memory leaks.

You can also check out my two blog posts discussing issues related to new/delete:

  1. Memory leaks caused by deleting void* pointers
  2. Memory leaks caused by STL releasing pointer elements

Using dynamic memory is very prone to issues, as ensuring memory is released at the correct time is not easy. Sometimes we forget to release memory, causing memory leaks; at other times, we release it while still having pointers referencing that memory, leading to illegal memory references (dangling pointers).

Fortunately, to use dynamic memory more safely, C++11 introduced a new feature—smart pointers.

C++11 provides two types of smart pointers (shared_ptr and unique_ptr) to manage dynamic objects. Smart pointers behave similarly to conventional pointers (raw pointers), with the most important difference being that smart pointers can automatically release the object they point to.

The two types of smart pointers provided by C++11 differ in how they manage underlying pointers:

shared_ptr allows multiple pointers to point to the same object.
unique_ptr, on the other hand, exclusively owns the pointed-to object.

The standard library also defines a companion classweak_ptr, which is a weak reference pointing to the object managed by shared_ptr.

Note: These three types are all defined in memory.

shared_ptr Class

Similar to vector / string / list / deque that we have come across earlier, smart pointers are also templates. Therefore, when we create a smart pointer, we must provide additional definitions—the type the pointer can point to. Like with vectors, we specify the type inside angle brackets, followed by the name of the defined smart pointer.

1
2
3
4
// shared_ptr can point to string
shared_ptr<string> strp;
// shared_ptr can point to a vector of strings
shared_ptr<vector<string>> vecStrp;

The default initialized smart pointer holds a null pointer (nullptr)

The usage of smart pointers is similar to that of ordinary pointers. Dereferencing a smart pointer returns the object it points to. If a smart pointer is used in a conditional check, it effectively checks whether it is null.

1
2
3
4
5
6
7
8
shared_ptr<string> strp=make_shared<string>();
// If strp is not null, check whether it points to an empty string
if(strp&&strp->empty()){
// If strp points to an empty string, dereference strp and assign a new value to the string object pointed to by strp
*strp="helloworld";
}
cout<<*strp<<endl;
// The result is helloworld

Operations supported by both shared_ptr and unique_ptr.

Operation Meaning
shared_ptr<T> sp;

unique_ptr<T> up;
An empty smart pointer that can point to an object of type T
p Uses p as a condition check; if p points to an object, this evaluates to true
*p Dereference p to obtain the object it points to
p->mem Equivalent to (*p).mem
p.get() Returns the pointer stored in p; use with caution—if the smart pointer releases its object, the returned pointer will also disappear (becoming a dangling pointer)
swap(p,q)

p.swap(q)
Swaps the pointers in p and q

Operations unique to shared_ptr

Operation Meaning
make_shared<T> (args) Returns a shared_ptr pointing to a dynamically allocated object of type T. The object is initialized using args.
shared_ptr<T> p(q) p is a copy of shared_ptr q; this operation increments the counter in q. The pointer in q must be convertible to T* (for more details, refer to A Detailed Analysis of Pointer Conversion in C++)
p=q Both p and q are shared_ptr; your pointers must be convertible to each other. This operation decrements p’s reference count and increments q’s; if p’s reference count becomes 0, the original memory it managed will be released.
p.unique() Returns true if p.use_count() is 1; otherwise returns false
p.use_count() Returns the number of smart pointers that share the object with p; it can be slow and is mainly used for debugging

We can also implement a simple class similar to shared_ptr

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
template<typename T>
class smartPoint{
public:
smartPoint():ObjectPoint(nullptr),use_count(new size_t(1)){}
smartPoint(T *i):ObjectPoint(i),use_count(new size_t(1)){}
smartPoint(const smartPoint &i):ObjectPoint(i.ObjectPoint),use_count(i.use_count){++*use_count;}
~smartPoint(){decr_use();}

smartPoint& operator=(const T &rhs){
++*rhs.use_count;
decr_use();
ObjectPoint=rhs.ObjectPoint;
use_count=rhs.use_count;
return this;
}
const T* operator->() const{
if(ObjectPoint)
return ObjectPoint;
}
const T& operator*() const{
if(ObjectPoint)
return *ObjectPoint;
}
size_t count(){
return *use_count;
}
private:
T *ObjectPoint;
size_t *use_count;
void decr_use(){
if(--*use_count==0){
delete ObjectPoint;
delete use_count;
}
}
};

The above implements only simple reference counting and provides the smartPointer::count() member function to get the number of references to the pointed object.

Here’s a simple test code:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
struct test{
test(){cout<<"construction"<<endl;}
~test(){cout<<"destruction"<<endl;}
};
smartPoint<test> funa(void){
smartPoint<test> strp=new test();
// Use strp
// When we return strp, the reference count is incremented
return strp;
}// strp leaves the scope but the memory it points to will not be released
void funb(void){
smartPoint<test> strp(new test());
// Use strp
// strp leaves the scope, and the reference count will be decremented, so strp will be automatically released.
}

int main(void){
auto x=funa();
cout<<"--------"<<endl;
funb();
cout<<"--------"<<endl;
}
// Output results
/*
construction (1)
------------
construction (2)
destruction (3)
------------
destruction (4)
*/
// 1. The object created in funa; 4. The object created in funa is destroyed when it leaves the main function's scope (X)
// 2. The object created in funb; 3. The object created in funb is destroyed when leaving the funb scope.

make_shared Function

The safest way to allocate and use dynamic memory is to call a standard library function called make_shared.

This function allocates an object in dynamic memory, initializes it, and returns a shared_ptr pointing to this object. Like smart pointers, make_shared is also defined in the memory header.

When using make_shared, you must specify the type of the object to create. The definition is similar to templates, following the function name with angle brackets, in which you provide the type.

1
2
3
4
5
6
7
8
// Points to an int initialized with a value of 0
shared_ptr<int> ivalp1=make_shared<int>();
// Points to an int with a value of 42
shared_ptr<int> ivalp2=make_shared<int>(42);
// Points to a string with the value "HelloWorld"
shared_ptr<string> strvalp=make_shared<string>("HelloWorld");
// Points to a string with the value "9999999999"
shared_ptr<string> strvalp=make_shared<string>(10,'9');

Similar to the emplace member of sequence containers (which directly calls the constructor of the element type to construct elements in the container, for detailed information refer to C++11 Syntactic Sugar: emplace Operation), make_shared uses its parameters to construct an object of the given type.

For example:

  1. The parameters passed when calling make_shared<string> must match one of the constructors for string.
  2. The parameters passed when calling make_shared<int> must be able to initialize an int, and so on.
  3. If we do not pass any parameters, the object will be value-initialized.

We can also use auto to define an object that holds the result of make_shared (i.e., the smart pointer object), which is a simpler method compared to the previous ones:

1
2
// strp points to a dynamically allocated empty vector<string>
auto strp=make_shared<vector<string>>();

Copying and Assigning shared_ptr

When a copy or assignment operation is performed, each shared_ptr records how many other shared_ptrs point to the same object.

As mentioned in the previous table, we can call the use_count member function of shared_ptr to get the number of smart pointers sharing the object.

1
2
3
4
5
6
7
// The object pointed to by p has only one reference
auto p=make_shared<int>(42);
cout<<p.use_count()<<" ";
// p and q point to the same object, which has two references
auto q(p);
cout<<p.use_count()<<endl;
// The output results are 1 2

Each shared_ptr can be thought of as having an associated counter, commonly referred to as the reference count. Whenever we copy a shared_ptr, the counter gets incremented.

For example:

  1. When one shared_ptr is initialized with another shared_ptr.

  2. When a shared_ptr is passed as a parameter to a function.

  3. When it is the return value of a function.

The associated counter will increment.

When we

  1. Assign a new value to shared_ptr
  2. The shared_ptr is destroyed (e.g., a local shared_ptr leaves its scope)

the counter will decrement.

Once a shared_ptr’s counter becomes 0, it will automatically release the object it manages.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
struct test{
test(){cout<<"construction"<<endl;}
~test(){cout<<"destruction"<<endl;}
};

// The following code
auto r=make_shared<test>();
r=make_shared<test>();
// The output results
construction
construction
destruction
destruction
/*
The order is:
1. Create a shared_ptr that can point to a test type object (value initialization) and assign it to r, assuming r points to A
2. Create another shared_ptr that can point to a test type object (value initialization) and reassign it to r, assuming r points to B
3. After r is reassigned to point to B, the destructor for A (test) is called to release A
4. At the end of the program, the destructor for B (test) is called to release B
*/

Note: Whether to use a counter or another data structure to record how many pointers share the object is entirely up to the specific implementation of the standard library. The key is that smart pointers can record how many shared_ptrs point to the same object and can release them at the appropriate time.

Automatic Destruction of Objects Managed by shared_ptr

Like all defined local variables, local objects are destroyed when they leave their scope.

1
2
3
4
void func(void){
int ival;
}// When leaving the scope of func, local variables defined within it will be destroyed

Smart pointers behave similarly, but when they leave their scope or are assigned a new value, they decrement the object’s internal reference count, and only when the count reaches 0 does the object pointed to by the smart pointer get destroyed.
We can see the order of decrementing the reference count and destroying objects with the following code:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
smartPoint<test> funa(void){
smartPoint<test> val=new test();
return val;
}
void funb(void){
cout<<"run funa before."<<endl;
smartPoint<test> temp=funa();
cout<<"run funa after."<<endl;
}

int main(void){
cout<<"run funb before"<<endl;
funb();
cout<<"run funb after."<<endl;
return 0;
}

// Output results
run funb before
run funa before.
construction
run funa after.
destruction
run funb after.

As seen, we call funa in funb, where a smart pointer is created in funa that points to an instance of the test object, and we return this smart pointer to the invocation site of funb. Upon returning, the reference count of the smart pointer created in funa increments (the return is actually a copy constructor creating a new object as a value pass. If you’re interested, you can try returning from funa without receiving (using) that return value at the invocation site to see when the dynamic object created in funa gets destroyed) and then decrements (when leaving the funa scope), so the object pointed to by the smart pointer will not be destroyed when leaving the funa scope. But subsequently, when leaving the funb scope, the smart pointer is destroyed along with the dynamic memory object it points to (because the reference count decrements to 0 when leaving the funb scope, thus calling the destructor of the object pointed to by the smart pointer to release that object).

When the last shared_ptr pointing to an object is destroyed, the shared_ptr class will automatically destroy this object. It does this through a special member function—the destructor. Similar to constructors, every class has a destructor. As constructors control initialization, destructors control what actions to take when an object of this type is destroyed.

The destructor of shared_ptr will decrement the reference count of the object it points to. If the reference count drops to 0, the shared_ptr’s destructor will destroy the object and free the memory it occupied.

When dynamic objects are no longer needed, shared_ptr will automatically release them, making dynamic memory usage very easy, as we don’t always have to remember when we created (new) a dynamic object and have yet to release (delete) it.

The destructor of shared_ptr will decrement the reference count of the object it points to. If the reference count drops to 0, the shared_ptr’s destructor will destroy the object and free the memory it occupied.

If you store a shared_ptr in a container and later no longer need all of the elements while only using some, remember to use erase to delete those elements you no longer need.

Classes Using Dynamic Memory for Resource Generation

Programs use dynamic memory for one of the following reasons:

  1. The program does not know how many objects it needs.
  2. The program does not know the exact type of the required objects.
  3. The program needs to share data among multiple objects.

Container classes are a typical example of using dynamic memory for the first reason.

In the example below, we will define a class that uses dynamic memory to allow multiple objects to share the same underlying data.

The containers we often use (vector / string / map / set / deque / list) allocate resources that have the same lifetime as the object.

For instance, each vector object contains its own elements. When we copy a vector, the elements of the original vector and the new copy are separated.

1
2
3
4
5
6
vector<string> v1;
{// New scope
vector<string> v2={"a","an","the"};
v1=v2;
}// v2 is destroyed, and the elements within it are also destroyed
// v1 contains three elements, which are copies of the original elements from v2

Elements allocated from a vector only exist while the vector is present. When a vector is destroyed, the elements within that vector also get destroyed.

However, certain classes allocate resources that have a lifetime independent of their original objects. Generally, if two objects share underlying data, we cannot unilaterally destroy the underlying data when one of the objects is destroyed:

1
2
3
4
5
Blob<string> b1;
{
Blob<string> b2={"a","an","the"};
b1=b2;
}// b2 is destroyed
The article is finished. If you have any questions, please comment and communicate.

Scan the QR code on WeChat and follow me.

Title:动态内存和智能指针
Author:LIPENGZHA
Publish Date:2016/08/25 08:18
World Count:9.6k Words
Link:https://en.imzlp.com/posts/4280/
License: CC BY-NC-SA 4.0
Reprinting of the full article is prohibited.
Your donation will encourage me to keep creating!