《C++ Primer第五版》读书笔记(10)-DYNAMIC MEMORY

来源:互联网 发布:新浪微博个性域名删除 编辑:程序博客网 时间:2024/05/18 00:39

内存管理一直是C/C++比较让人痛苦的部分,在《C++ Primer第四版》的时候介绍的auto_ptr还是有很大的局限,现在引入了更为强大的shared_ptr、unique_ptr、weak_ptr,齐活了!

12 DYNAMIC MEMORY


12.1 DYNAMIC MEMORY AND SMART POINTERS


To make using dynamic memory easier (and safer), the new library provides two smart pointer types that manage dynamic objects.A smart pointer acts like a regular pointer with the important exception that it automatically deletes the object to which it points. The new library defines two kinds of smart pointers that differ in how they manage their underlying pointers:shared_ptr, which allows multiple pointers to refer to the same object, andunique_ptr, which “owns” the object to which it points. The library also defines a companion class namedweak_ptr that is a weak reference to an object managed by a shared_ptr.


12.1.1 THE SHARED_PTR CLASS


shared_ptr<string>p1;  // shared_ptr that can point at a string
shared_ptr<list<int>> p2; // shared_ptr that can point at a list of ints
 


 
The safest way to allocate and use dynamic memory is to call a library function named make_shared.This function allocates and initializes an object in dynamic memory and returns a shared_ptr that points to that object.


// shared_ptr that points to an int with value 42
shared_ptr<int> p3 = make_shared<int>(42);
// p4 points to a string with value 9999999999
shared_ptr<string> p4 = make_shared<string>(10, '9');
// p5 points to an int that is value initialized (§ 3.3.1 (p. 98)) to 0
shared_ptr<int> p5 = make_shared<int>();


Of course, ordinarily we use auto to make it easier to define an object to hold the result of make_shared:
// p6 points to a dynamically allocated, empty vector<string>
auto p6 = make_shared<vector<string>>();


Copying and Assigning shared_ptrs


We can think of a shared_ptr as if it has an associated counter, usually referred to as a reference count. Whenever we copy a shared_ptr, the count is incremented.For example, the counter associated with a shared_ptr is incremented when we use it to initialize another shared_ptr, when we use it as the right-hand operand of an assignment, or when we pass it to or return it from a function by value. The counter is decremented when we assign a new value to the shared_ptrand when the shared_ptr itself is destroyed, such as when a local shared_ptr goes out of scope.


Once a shared_ptr’s counter goes to zero, the shared_ptr automatically frees the object that it manages:


auto r = make_shared<int>(42); // int to which r points has one user
r = q;  // assign to r, making it point to a different address
// increase the use count for the object to which q points
// reduce the use count of the object to which r had pointed
// the object r had pointed to has no users; that object is automatically freed


Programs tend to use dynamic memory for one of three purposes:
1. They don’t know how many objects they’ll need
2. They don’t know the precise type of the objects they need
3. They want to share data between several objects


As we’ve seen, copying a shared_ptr increments its reference count; assigning one shared_ptr to another increments the count of the right-hand operand and decrements the count in the left-hand operand; and destroying a shared_ptr decrements the count.
If the count in a shared_ptr goes to zero, the object to which that shared_ptr points is automatically destroyed.


12.1.2 MANAGING MEMORY DIRECTLY
We can initialize a dynamically allocated object using direct initialization. We can use traditional construction (using parentheses), and under the new standard, we can also use list initialization (with curly braces).
int *pi = new int(1024); // object to which pi points has value 1024
string *ps = new string(10, '9');  // *ps is "9999999999"
// vector with ten elements with values from 0 to 9
vector<int> *pv = new vector<int>{0,1,2,3,4,5,6,7,8,9};
string*ps1 = new string;  // default initialized to the empty string
string *ps = new string(); // value initialized to the empty string
int *pi1 = new int;  // default initialized; *pi1 is undefined
int *pi2 = new int();  // value initialized to 0; *pi2 is 0


Memory Exhaustion
Once a program has used all of its available memory, new expressions will fail. By default, ifnew is unable to allocate the requested storage, it throws an exception of type bad_alloc.


We can prevent new from throwing an exception by using a different form of new:
// if allocation fails, new returns a null pointer
int *p1 = new int; // if allocation fails, new throws std::bad_alloc
int *p2 = new (nothrow) int; // if allocation fails, new returns a null pointer
After the delete, the pointer becomes what is referred to as a dangling pointer. A dangling pointer is one that refers to memory that once held an object but no longer does so.
If we need to keep the pointer around, we can assign nullptr to the pointer after we use delete. Doing so makes it clear that the pointer points to no object。


12.1.3 USING SHARED_PTRS WITH NEW


we can also initialize a smart pointer from a pointer returned by new:
shared_ptr<double>p1; // shared_ptr that can point at a double
shared_ptr<int> p2(new int(42)); // p2 points to an int with value 42
Table12.3. Other Ways to Define and Change shared_ptrs
 
The smart pointer constructors that take pointers are explicit.Hence, we cannot implicitly convert a built-in pointer to a smart pointer; we must use the direct form of initialization to initialize a smart pointer:
shared_ptr<int> p1 = new int(1024);  // error: must use direct initialization
shared_ptr<int> p2(new int(1024));  // ok: uses direct initialization


Don’t Mix Ordinary Pointers and Smart Pointers


When we bind a shared_ptr to a plain pointer, we give responsibility for that memory to that shared_ptr. Once we give shared_ptr responsibility for a pointer, we should no longer use a built-in pointer to access the memory to which the shared_ptr now points.
int*x(new int(1024)); // dangerous: x is a plain pointer, not a smart pointer
process(x);  // error: cannot convert int* to shared_ptr<int>
process(shared_ptr<int>(x)); // legal, but the memory will be deleted!
int j = *x;  // undefined: x is a dangling pointer!


and Don’t Use get to Initialize or Assign Another Smart Pointer


The smart pointer types define a function named get that returns a built-in pointer to the object that the smart pointer is managing.This function is intended for cases when we need to pass a built-in pointer to code that can’t use a smart pointer. The code that uses the return from get must not delete that pointer. In particular, never use get to initialize or assign to another smart pointer.
shared_ptr<int>p(new int(42)); // reference count is 1
int *q = p.get();  // ok: but don't use q in any way that might delete its pointer
{ // new block
// undefined: two independent shared_ptrs point to the same memory shared_ptr<int>(q);
} // block ends, q is destroyed, and the memory to which q points is freed

int foo = *p; // undefined; the memory to which p points was freed


We can use reset to assign a new pointer to a shared_ptr:
p= new int(1024);  // error: cannot assign a pointer to a shared_ptr
p.reset(new int(1024));  // ok: p points to a new object


12.1.4 SMART POINTERS AND EXCEPTIONS


Smart pointers can provide safety and convenience for handling dynamically allocated memory only when they are used properly. To use smart pointers correctly, we must adhere to a set of conventions:
•Don’t use the same built-in pointer value to initialize (or reset) more than one smart pointer.
•Don’t delete the pointer returned from get().
•Don’t use get() to initialize or reset another smart pointer.
•If you use a pointer returned by get(), remember that the pointer will become invalid when the last corresponding smart pointer goes away.
•If you use a smart pointer to manage a resource other than memory allocated by new, remember to pass a deleter


12.1.5 UNIQUE_PTR
A unique_ptr “owns” the object to which it points. Unlike shared_ptr, only one unique_ptr at a time can point to a given object. The object to which a unique_ptr points is destroyed when the unique_ptr is destroyed.
 
unique_ptr<double> p1;  // unique_ptr that can point at a double
unique_ptr<int>  p2(new int(42)); // p2 points to int with value 42
unique_ptr<string>p1(new string("Stegosaurus"));
unique_ptr<string> p2(p1);  // error: no copy for unique_ptr
unique_ptr<string> p3;
p3 = p2;  // error: no assign for unique_ptr


// transfers ownership from p1 (which points to the string Stegosaurus) to p2
unique_ptr<string> p2(p1.release()); // release makes p1 null
unique_ptr<string> p3(new string("Trex"));
// transfers ownership from p3 to p2
p2.reset(p3.release()); // reset deletes the memory to which p2 had pointed


Often the pointer returned by release is used to initialize or assign another smart pointer. In that case, responsibility for managing the memory is simply transferred from one smart pointer to another. If we do not use another smart pointer to hold the pointer returned from release, our program takes over responsibility for freeing that resource:
p2.release();// WRONG: p2 won't free the memory and we've lost the pointer
auto p = p2.release(); // ok, but we must remember to delete(p)


Passing and Returning unique_ptrs
There is one exception to the rule that we cannot copy a unique_ptr:We can copy or assign a unique_ptr that is about to be destroyed. The most common example is when we return a unique_ptr from a function:
unique_ptr<int>clone(int p) {
// ok: explicitly create a unique_ptr<int> from int*
return unique_ptr<int>(new int(p));
}
Alternatively,we can also return a copy of a local object:
unique_ptr<int>clone(int p) {
    unique_ptr<int> ret(new int (p));
    // . . .
    return ret;
}


Earlier versions of the library included a class named auto_ptr that had some, but not all, of the properties of unique_ptr. In particular, it was not possible to store an auto_ptr in a container, nor could we return one from a function.


Although auto_ptr is still part of the standard library, programs should use unique_ptr instead.


Passing a Deleter to unique_ptr


// p points to an object of type objT and uses an object of type delT to free that object
// it will call an object named fcn of type delT
unique_ptr<objT, delT> p (new objT, fcn);
void f(destination &d /* other needed parameters*/)
{
    connection c = connect(&d);  // open the connection
    // when p is destroyed, the connection will be closed
    unique_ptr<connection, decltype(end_connection)*>
    p(&c, end_connection);
    // use the connection
    // when f exits, even if by an exception, the connection will be properly closed
}


12.1.6 WEAK_PTR


A weak_ptr(Table 12.5) is a smart pointer that does not control the lifetime of the object to which it points. Instead, a weak_ptr points to an object that is managed by a shared_ptr. Binding a weak_ptr to a shared_ptr does not change the reference count of that shared_ptr. Once the last shared_ptr pointing to the object goes away, the object itself will be deleted. That object will be deleted even if there are weak_ptrs pointing to it—hence the name weak_ptr, which captures the idea that a weak_ptr shares its object “weakly.”  


auto p = make_shared<int>(42);
weak_ptr<int> wp(p);  // wp weakly shares with p; use count in p is unchanged


Because the object might no longer exist, we cannot use a weak_ptr to access its object directly. To access that object, we must call lock. The lock function checks whether the object to which the weak_ptr points still exists.
As with any other shared_ptr, we are guaranteed that the underlying object to which that shared_ptr points continues to exist at least as long as that shared_ptr exists. For example:


if(shared_ptr<int> np = wp.lock()) { // true if np is not null
// inside the if, np shares its object with p
}


12.2 DYNAMIC ARRAYS


Most applications should use a library container rather than dynamically allocated arrays. Using a container is easier, less likely to contain memorymanagement bugs, and is likely to give better performance.


12.2.1 NEW AND ARRAYS


We ask new to allocate an array of objects by specifying the number of objects to allocate in a pair of square brackets after a type name.


// call get_size to determine how many ints to allocate
int *pia = new int[get_size()]; // pia points to the first of these ints
typedef int arrT[42]; // arrT names the type array of 42 ints
int *p = new arrT;   // allocates an array of 42 ints; p points to the first one


Although it is common to refer to memory allocated by new T[]as a “dynamic array”, this usage is somewhat misleading. When we use newto allocate an array, we do not get an object with an array type. Instead, we get a pointer to the element type of the array. Because the allocated memory does not have an array type, we cannot call begin or end  on a dynamic array. These functions use the array dimension (which is part of an array’s type) to return pointers to the first and one past the last elements, respectively. For the same reasons, we also cannot use a range forto process the elements in a (so-called) dynamic array.


int *pia = new int[10];  // block of ten uninitialized ints
int *pia2 = new int[10]();  // block of ten ints value initialized to 0
string *psa = new string[10];  // block of ten empty strings
string *psa2 = new string[10](); // block of ten empty strings


Under the new standard, we can also provide a braced list of element initializers:
// block of ten ints each initialized from the corresponding initializer
int *pia3 = new int[10]{0,1,2,3,4,5,6,7,8,9};
// block of ten strings; the first four are initialized from the given initializers
// remaining elements are value initialized
string *psa3 = new string[10]{"a", "an", "the",string(3,'x')};


If there are fewer initializers than elements, the remaining elements are value initialized. If there are more initializers than the given size, then thenew expression fails and no storage is allocated. In this case, new throws an exception of type bad_array_new_length. Like bad_alloc, this type is defined in the newheader.


Freeing Dynamic Arrays


To free a dynamic array, we use a special form of delete that includes an empty pair of square brackets:
delete p;  // p must point to a dynamically allocated object or be null
delete [] pa; // pa must point to a dynamically allocated array or be null


Smart Pointers and Dynamic Arrays
To use a unique_ptr to manage a dynamic array, we must include a pair of empty brackets after the object type:
// up points to an array of ten uninitialized ints
unique_ptr<int[]> up(new int[10]);
up.release();  // automatically uses delete[] to destroy its pointer
 


Unlike unique_ptr, shared_ptrs provide no direct support for managing a dynamic array. If we want to use a shared_ptr to manage a dynamic array, we must provide our own deleter:
// to use a shared_ptr we must supply a delete
shared_ptr<int> sp(new int[10], [](int *p) { delete[] p; });
sp.reset(); // uses the lambda we supplied that uses delete[] to free the array
// shared_ptrs don't have subscript operator and don't support pointer arithmetic
for (size_t i = 0; i != 10; ++i)
    *(sp.get() + i) = i;  // use get to get a built-in pointer


There is no subscript operator for shared_ptrs, and the smart pointer types do not support pointer arithmetic. As a result, to access the elements in the array, we must use get to obtain a built-in pointer, which we can then use in normal ways.


12.2.2 THE ALLOCATOR CLASS
The library allocator class lets us separate allocation from construction. It provides type-aware allocation of raw, unconstructed, memory.
 
allocator<string> alloc;  // object that can allocate strings
auto const p = alloc.allocate(n); // allocate n unconstructed strings
auto q = p; // q will point to one past the last constructed element
alloc.construct(q++);  // *q is the empty string
alloc.construct(q++, 10, 'c');  // *q is cccccccccc
alloc.construct(q++, "hi");  // *q is hi!


We must construct objects in order to use memory returned by allocate. Using unconstructed memory in other ways is undefined.
We may destroy only elements that are actually constructed.


while(q != p)
  alloc.destroy(--q);    // free the  strings  we actually allocated


Once the elements have been destroyed, we can either reuse the memory to hold other strings or return the memory to the system. We free the memory by calling deallocate:
alloc.deallocate(p,n);


As a companion to the allocator class, the library also defines two algorithms that can construct objects in uninitialized memory.
 


0 0