Reading Note of CppPrimer-Chapter12
Dynamic Memory
Dynamic Memory
使用动态内存的一个很常见的原因是允许对象共享相同的数据和状态;
动态分配的内存是无名的,因此 new 无法为其分配的对象命名,只能返回指向该对象的指针;
new 的初始化
默认情况下,动态分配的对象是默认初始化的,这意味着内置类型或者组合类型将会是未定义的;类类型是执行默认构造函数,类内的内置类型也是未定义的;当然,可以直接初始化,也可以列表初始化
1
2
3
4
5
6
7
8
9
10
11
12
13
14// 默认初始化
int *pi = new int; // 未定义
std::string *str = new std::string; // 默认构造为""
// 直接初始化
int *pi = new int(1024); // pi指向1024
std::string *str = new std::string(10, '9'); // str指向"9999999999"
// 值初始化
int *pi = new int(); // 指向0
std::string *str = new std::string(); // 指向""
// 列表初始化
std::vector<int> *vec = new vector<int>{0,1,2,3,4,5,6,7,8,9};动态分配 const 对象
用 new 分配 const 对象是合法的;类似于其他 const 对象,一个动态分配的 const 对象必须初始化;
1
2const int *pci = new const int(1024);
const std::string *psc = new const string;内存耗尽分配失败的处理
内存耗尽 new 表达式会失败,并抛出一个
std::bad_alloc
的异常;也可以使用nothrow
让 new 不抛出异常1
2
3
int *p1 = new int; // 分配失败会throw std::bad_alloc exception
int *p2 = new (nothrow) int; // 分配失败会返回空指针placement new
Placement new allows you to construct an object in memory that’s already allocated.
You may want to do this for optimization when you need to construct multiple instances of an object, and it is faster not to re-allocate memory each time you need a new instance. Instead, it might be more efficient to perform a single allocation for a chunk of memory that can hold multiple objects, even though you don’t want to use all of it at once.
Standard C++ also supports placement new operator, which constructs an object on a pre-allocated buffer. This is useful when building a memory pool, a garbage collector or simply when performance and exception safety are paramount (there’s no danger of allocation failure since the memory has already been allocated, and constructing an object on a pre-allocated buffer takes less time):
1
2
3
4
char *buf = new char[sizeof(string)]; // pre-allocated buffer
string *p = new (buf) string("hi"); // placement new
string *q = new string("hi"); // ordinary heap allocationIf a non-throwing allocation function (e.g. the one selected by new(std::nothrow) T) returns a null pointer because of an allocation failure, then the new-expression returns immediately, it does not attempt to initialize an object or to call a deallocation function.
1
int *p2 = new (nothrow) int;
delete
释放分配的内存可以用 delete;传递给
delete
的指针必须为动态分配的内存,或者是一个空指针;释放非new
分配的内存或者将相同的指针释放多次,都是不合法的;delete 一个不是 new 分配的内存,很多编译器会通过,但是错误的;虽然 const 本身不能被修改,但是动态分配的 const 的内存是可以销毁的;
1
2
3
4
5
6
7
8
9int i, *pi=&i, *pi2=nullptr;
double *pd = new double(0.0);
delete i; // invalid
delete pi; // invalid
delete pi2; // valid
delete pd; // valid
const int *pci = new const int(1024);
delete pci; // valid当我们 delete 一个指针之后,指针值就无效了。虽然指针无效,但是很多机器上还保存着指针原来指向的地址,变成悬空指针 (dangling pointer);未初始化的指针的缺点 dangling pointer 也有,delete 指针后应该习惯性的把指针置为 nullptr,这样就清楚指针不指向任何对象;
1
2
3
4int *p(new int(42));
delete p;
auto q = p;
p = nullptr; // now p is nullptr, but q is a dangling pointerdifference between
new/delete
vsmalloc/free
- malloc(): It is a C library function that can also be used in C++, while the “new” operator is specific for C++ only.
- both malloc() and new are used to allocate the memory dynamically in heap. But “new” does call the constructor of a class whereas “malloc()” does not.
- free() is a C library function that can also be used in C++, while “delete”is a C++ keyword.
- free() frees memory but doesn’t call Destructor of a classwhereas “delete” frees the memory and also calls the destructor of the class.
Technically, memory allocated by
new
comes from the Free Store while memory allocated bymalloc
comes from the Heap. Whether these two areas are the same is an implementation detail, which is another reason thatmalloc
andnew
cannot be mixed.Feature new
/delete
malloc
/free
Memory allocated from Free Store Heap Returns Fully typed pointer void*
On failure Throws (never returns NULL
except nothrow)Returns NULL
Required size Calculated by compiler Must be specified in bytes Handling arrays Has an explicit version Requires manual calculations Reallocating Not handled intuitively Simple (no copy constructor) Call of reverse Implementation defined No Low memory cases Can add a new memory allocator Not handled by user code Overridable Yes No Use of constructor / destructor Yes No C++ Memory Areas
The following summarizes a C++ program’s major distinct memory areas. Note that some of the names (e.g., “heap”) do not appear as such in the draft [standard].
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
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76Memory Area Characteristics and Object Lifetimes
-------------- ------------------------------------------------
Const Data The const data area stores string literals and
other data whose values are known at compile
time. No objects of class type can exist in
this area. All data in this area is available
during the entire lifetime of the program.
Further, all of this data is read-only, and the
results of trying to modify it are undefined.
This is in part because even the underlying
storage format is subject to arbitrary
optimization by the implementation. For
example, a particular compiler may store string
literals in overlapping objects if it wants to.
Stack The stack stores automatic variables. Typically
allocation is much faster than for dynamic
storage (heap or free store) because a memory
allocation involves only pointer increment
rather than more complex management. Objects
are constructed immediately after memory is
allocated and destroyed immediately before
memory is deallocated, so there is no
opportunity for programmers to directly
manipulate allocated but uninitialized stack
space (barring willful tampering using explicit
dtors and placement new).
Free Store The free store is one of the two dynamic memory
areas, allocated/freed by new/delete. Object
lifetime can be less than the time the storage
is allocated; that is, free store objects can
have memory allocated without being immediately
initialized, and can be destroyed without the
memory being immediately deallocated. During
the period when the storage is allocated but
outside the object's lifetime, the storage may
be accessed and manipulated through a void* but
none of the proto-object's nonstatic members or
member functions may be accessed, have their
addresses taken, or be otherwise manipulated.
Heap The heap is the other dynamic memory area,
allocated/freed by malloc/free and their
variants. Note that while the default global
new and delete might be implemented in terms of
malloc and free by a particular compiler, the
heap is not the same as free store and memory
allocated in one area cannot be safely
deallocated in the other. Memory allocated from
the heap can be used for objects of class type
by placement-new construction and explicit
destruction. If so used, the notes about free
store object lifetime apply similarly here.
Global/Static Global or static variables and objects have
their storage allocated at program startup, but
may not be initialized until after the program
has begun executing. For instance, a static
variable in a function is initialized only the
first time program execution passes through its
definition. The order of initialization of
global variables across translation units is not
defined, and special care is needed to manage
dependencies between global objects (including
class statics). As always, uninitialized proto-
objects' storage may be accessed and manipulated
through a void* but no nonstatic members or
member functions may be used or referenced
outside the object's actual lifetime.
Smart Pointer
智能指针的使用方式和普通指针类似,重载了
->
,*
等运算符:1
2
3
4
5
std::shared_ptr<int> p1;
if (p1 && p1->empty()){
// do something here
}shared_ptr
可以多个指针指向同一个对象;unique_ptr
仅可以最多一个指针指向同一个对象;weak_ptr
是一种弱引用,指向shared_ptr
指向的对象;
shared_ptr
智能指针
shared_ptr
的引用计数机制无论何时对
shared_ptr
进行拷贝,计数器都会递增:- 比如拷贝初始化另一个
shared_ptr
- 函数参数的值传递的拷贝
- 作为函数返回值返回等涉及到拷贝的地方
当我们对一个
shared_ptr
进行以下操作的时候,计数器会递减;一旦计数器变为 0,他就会自动释放自己管理的对象:- 被赋值
- 离开作用域的时候 shared_ptr 被销毁的时候
- 比如拷贝初始化另一个
shared_ptr 构造
shared_ptr
可以接受一个内置指针作为初始化参数;因为
shared_ptr
的这个构造函数是 explicit 的,所有不可以将内置指针隐式的转换为一个智能指针;同样的,返回值为智能指针的函数也不可以直接返回一个内置指针;用来初始化智能指针的内置指针必须是指向动态分配的内存的指针,因为默认使用
delete
来释放关联的对象;1
2
3
4
5
6
7
8std::shared_ptr<int> p1 = new int(42); // invalid
std::shared_ptr<int> p2(new int(42)); // valid
std::shared_ptr<int> clone(int p){
return new int(p); // invalid
}
std::shared_ptr<int> clone(int p){
return std::shared_ptr<int>(new int(p)); // valid
}最安全的使用动态内存构建
shared_ptr
的方法是使用make_shared
,他直接初始化一个对象,返回指向这个对象的shared_ptr
;不推荐 new 和 std::shared_ptr 混着用,可以避免将同一块内存绑定到多个独立创建的智能指针上面;1
auto p6 = make_shared<vector<string>>();
当我们将一个
shared_ptr
绑定到一个普通指针的时候,我们就将内存管理的权限转移给了shared_ptr
;一旦这样做了,我们就不应该再使用内置指针来访问shared_ptr
所指向的内存了;1
2
3int *x(new int(1024));
process(std::shared_ptr<int>(x)); // bind x to a tmp shared pointer, when func call finish, x is freed.
int j = *x; // invalid, x now becomes a dangling pointershared_ptr::get
返回这个智能指针指向的内置指针;
这样设计的使用场景是:我们需要向不能使用智能指针的代码传递一个内置指针;使用这个 get 返回的内置指针的代码不能 delete 这个指针;永远不要使用 get 初始化另一个智能指针或者为另一个智能指针赋值;
1
2
3
4
5
6
7std::shared_ptr<int> p(new int(42));
int *q = p.get();
{
std::shared_ptr<int> tmp_ptr(q);
// when leaved the block, the ptr managed by p had be freed by tmp_ptr;
}
int foo = *p; // dangling pointershared_ptr::reset
如果参数为空,则相当于
std::shared_ptr().swap(ptr)
;如果参数不为空,则使用参数来绑定指针使指针指向的新地址;旧的管理的对象绑定的 shared_ptr 计数器会减 1,如果计数器为 0 也会自动释放旧的管理的对象;
1
2
3int *ptr = new int(10), *ptr1 = new int(11);
std::shared_ptr<int> p(ptr);
p.reset(ptr1); // now, ptr1 is managed by p, ptr will be freed automatically指定 deleter
使用智能指针,即使程序遇到异常,也可以正常释放分配的资源:
shared_ptr
自动销毁所管理的内存的时候,是通过调用对象的析构函数 (destructor);如果智能指针指向的对象是 new 分配的或者有析构函数,则智能指针可以调用默认的 deleter;
如果不是 new 分配的且没有自己的析构函数,则可以在创建智能指针的时候手动指定 deleter;
1
2
3
4
5
6
7
8
9
10
11struct connection; // a class has no destructor, 使用完需要手动关闭
// self-defined deleter
void end_connection(connection *p){
disconnect(*p); // 手动关闭
}
void f(dest_address &d){
connection x = connect(&address);
std::shared_ptr<connection> p(&x, end_connection); // create smart pointer and specify deleter
// do something ...
// 当f正常退出或者异常退出,connection都会被正常关闭
}智能指针
shared_ptr
使用基本规范:- 不使用相同的内置指针初始化或者 reset 多个智能指针;
- 不使用 get 返回的指针初始化或者 reset 其他智能指针;
- 不 delete get 返回的指针;
- 如果管理的对象不是 new 分配的,要手动指定一个删除器;
如果将
shared_ptr
放在一个容器里面,而后不需要全部元素,而只使用其中的一部分,这个时候需要手动 erase 掉不用的元素以便释放管理的内存;
unique_ptr
uniptr_ptr 所有权转移
unique_ptr
要求,每时每刻只能有一个unique_ptr
指向同一个对象;当
unique_ptr
被销毁的时候,指向的对象也被销毁了;unique_ptr
不支持拷贝和赋值,但是可以通过release
和reset
将指针的所有权从一个unique_ptr
转移到另一个unique_ptr
上面;1
2
3
4std::unique_ptr<int> p1(new int(42));
std::unique_ptr<int> p2(p1.release()); //调用release切断了unique_ptr和他管理的对象间的联系
std::unique_ptr<int> p3(new int(1024));
p2.reset(p3.release());不能拷贝
unique_ptr
的规则有一个例外,即可以拷贝或者赋值一个即将被销毁的unique_ptr
,最常见的是返回一个unique_ptr
1
2
3
4
5
6
7std::unique_ptr<int> clone(int p){
return std::unique_ptr<int>(new int(p));
}
std::unique_ptr<int> clone(int p){
std::unique_ptr<int> q(new int(p))
return q;
}向 unique_ptr 传递 deleter:
deleter 会影响到 unique_ptr 的类型以及如何构造该类型的对象
1
2
3
4
5
6
7void f(dest_address &d){
connection x = connect(&address);
// create smart pointer and specify deleter
std::unique_ptr<connection, decltype(end_connection)*> p(&x, end_connection);
// do something ...
// 当f正常退出或者异常退出,connection都会被正常关闭
}
weak_ptr
weak_ptr
指向一个shared_ptr
,但是不控制指向对象的生命周期,即不影响shared_ptr
的引用次数;一旦最后一个指向对象的shared_ptr
被销毁,对象就被销毁释放了;weak_ptr
可以用于伴随指针类,当你想使用对象,但是并不管理对象,并且在需要时可以返回对象的shared_ptr
的时候就可以用weak_ptr
1
2
3
4
5weak_ptr<T> w;
w.reset(); // set w to nullptr
w.use_count(); // use_count of shared_ptr
w.expired(); // whether w.use_count() == 0
w.lock(); // get shared_ptr访问 weak_ptr
要访问
weak_ptr
指向的对象,必须把weak_ptr
转换为shared_ptr
,不能直接使用weak_ptr
,因为weak_ptr
指向的对象可能不存在;weak_ptr.lock()
返回一个指向一个对象的shared_ptr
,引用计数会 + 11
2
3
4
5
6
7
8
9
10// init weak_ptr from shared_ptr
auto sp = std::make_shared<int>(42);
std::weak_ptr<int> wp(sp); // wp弱共享sp,sp引用次数不变
// access weak_ptr
if (std::shared_ptr<int> np == wp.lock()){
// weak_ptr is converted to std::shared_ptr to assume temporary ownership.
// If the original std::shared_ptr is destroyed at this time, the object's
// lifetime is extended until the temporary std::shared_ptr is destroyed as well.
}1
2
3
4
5
6
7
8
9
10
11
12
13// weak_ptr lock() return a shared_ptr
std::shared_ptr<int> p(new int(10));
std::cout << *p << ", " << p.use_count() << ", unique: " << p.unique() << std::endl;
{
std::weak_ptr<int> q(p);
auto qq = q.lock();
std::cout << *p << ", " << p.use_count() << ", unique: " << p.unique() << std::endl;
}
std::cout << *p << ", " << p.use_count() << ", unique: " << p.unique() << std::endl;
// 10, 1, unique: 1
// 10, 2, unique: 0
// 10, 1, unique: 1shared_ptr
的循环引用问题可以用weak_ptr
来解决1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17struct Node {
int _data;
std::shared_ptr<Node> _next;
std::shared_ptr<Node> _prev;
};
std::shared_ptr<Node> sp1(new Node);
std::shared_ptr<Node> sp2(new Node);
// sp1 refer to sp2, sp2 refer to sp1, when exit,
// both memory cannot be release, result in memory leak
sp1->_next = sp2;
sp2->_prev = sp1;
system("pause");
// if we are going to delete sp2, then we must erase sp2->prev;
// if we must delete sp2->prev, we must delete sp1;
// if we must delete sp1, we must delete sp1->next;
// if we must delete sp1->next, we must delete sp2, going back to the start pointsolution: define
Node
usingweak_ptr
1
2
3
4
5
6
struct Node {
int _data;
std::weak_ptr<Node> _next;
std::weak_ptr<Node> _prev;
};
Dynamic Array
new 分配数组
类型名之后紧跟一对方括号
[num]
,new
分配给定数量 (num) 的对象并返回指向第一个对象的指针;与数组不同,指定的分配数量的数目不一定需要常量 constexpr注意:分配得到的类型并不是数组类型,所有 stl 里面 array 的相关方法都不能使用;可以使用指针的算术运算来进行动态数组的遍历;
1
2
3
4
5
6
7
8
9int *pia = new int[get_size()];
typedef int arr[10];
int *pib = new arr; // 编译器执行的时候还是用new []
// 初始化
int *pic = new int[10]; // 默认初始化
int *pid = new int[10](); // 值初始化
std::string *pie = new string[10]{"a", "an", "and"}; // 列表初始化,剩余的值初始化
std::string *pie = new string[1]{"a", "an", "and"}; // 列表初始化,new表达式失败,不会分配任何内存, error1
2
3error: excess elements in array initializer
std::string *pie = new std::string[1]{"a", "an", "and"};
^~~~相对于数组不可以分配 0 个元素,new 动态分配 0 个元素是合法的;
1
2int a[0]; // invalid
int* p = new int[0]; // valid, p = nullptrdelete 释放数组
当我们释放一个动态数组的时候,空方括号对
[]
是必须的;如果在 delete 一个指向动态数组的指针时忽略了方括号[]
,则行为是 UB1
2delete pa; // pa指向一个动态分配的对象或者为空
delete [] pia; // pia指向动态分配数组或者为空智能指针管理动态数组
unique_ptr
可以原生支持管理动态数组;可以直接使用 unique_ptr 下标访问数组元素;shared_ptr
不能原生支持管理动态数组,但是可以通过指定deleter
的方式来管理;C++17 开始,shared_ptr
可以原生支持了这两种情况
unique_ptr
和shared_ptr
都没有点运算符.
和箭头运算符->
了,遍历动态数组可以通过内置指针解引用与算数运算;1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22// unique_ptr
std::unique_ptr<int[]> up(new int[10]);
up.release(); // equivalent to delete[]
for (size_t i=0; i<10; i++){
// if up.release() is called, the access is invalid
std::cout << up[i] << std::endl; // 直接使用unique_ptr下标访问数组元素
}
// shared_ptr manager dynamic array c++11 version
shared_ptr<int> sp(new int[10], [](int *p){delete [] p;}); // 指定deleter
sp.reset(); // 使用指定的deleter释放数组
for (size_t i=0;i<10; i++){
// if sp.reset() is called, the access is invalid
std::cout << *(sp.get()+i) << std::endl;
}
// shared_ptr manager dynamic array c++17 version
shared_ptr<int[]> sp(new int[10]);
sp.reset(); // equivalent to delete[]
for (size_t i=0;i<10; i++){
std::cout << sp[i] << std::endl; // 直接使用shared_ptr下标访问数组元素
}如果
shared_ptr
管理动态数组但没有指定deleter
(before C++17),valgrind 可以检查出问题1
2
3
4
5
6
7
8
9
10
11==29081== Mismatched free() / delete / delete []
==29081== at 0x4C33E09: operator delete(void*) (vg_replace_malloc.c:802)
==29081== by 0x109FD5: std::_Sp_counted_ptr<int*, (__gnu_cxx::_Lock_policy)2>::_M_dispose() (in /home/pangwong/test)
==29081== by 0x109907: std::_Sp_counted_base<(__gnu_cxx::_Lock_policy)2>::_M_release() (in /home/pangwong/test)
==29081== by 0x1096BA: std::__shared_count<(__gnu_cxx::_Lock_policy)2>::~__shared_count() (in /home/pangwong/test)
==29081== by 0x1095FB: std::__shared_ptr<int, (__gnu_cxx::_Lock_policy)2>::~__shared_ptr() (in /home/pangwong/test)
==29081== by 0x109617: std::shared_ptr<int>::~shared_ptr() (in /home/pangwong/test)
==29081== by 0x10946E: main (in /home/pangwong/test)
==29081== Address 0x5b841e0 is 0 bytes inside a block of size 40 alloc'd
==29081== at 0x4C3299F: operator new[](unsigned long) (vg_replace_malloc.c:579)
==29081== by 0x1093F6: main (in /home/pangwong/test)动态 new 数组的缺点:
内存分配和对象构造没有分离,导致在内存分配的时候就做了默认初始化,创建了我们可能永远用不上的对象;
对于我们需要使用的对象,我们在初始化之后立即对他们赋值,这样每个元素被赋值了两次;
对于没有默认构造函数的类,不能动态分配数组;
std::allocator
allocator
分配的内存是未构造的 (unconstructed),我们按需要在此内存中构造对象;不能直接使用未构造的内存;构造对象使用
construct
函数,释放对象使用destroy
函数;我们只能对真正构造了的元素进行destroy
操作;归还内存给系统使用
deallocate
函数;1
2
3
4
5
6
7
8
9// interface
std::allocator<T> a;
a.allocate(n); // allocate raw, unconstructed memeory
a.deallocate(p, n); // deallocate n object from p
a.construct(p, args); // construct obj using args at p, call constructor
a.destroy(p); // call descructor of object at p
uninitialized_copy(iter_begin, iter_end, p); // copy objs to unconstructed memory from begin to end
uninitialized_full(iter_begin, iter_end, obj); // fill unconstructed memory using obj from begin to end1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
std::allocator<std::string> alloc;
auto const p = alloc.allocate(n); // 分配了n个未构造的string
auto q = p; // q point to unconstructed memory
alloc.construct(q++); // empty string
alloc.construct(q++, 10, 'c'); // "cccccccccc"
std::cout << *p << std::endl; // valid
std::cout << *q << std::endl; // invalid, q not construct yet
while (q!=p){
alloc.destroy(--q);
}
alloc.deallocate(p, n); // deallocate的时候释放的大小必须和分配的时候的大小一致;
std::vector<std::string> vec = {"1", "2", "3"};
auto p = alloc.allocate(vec.size() * 2);
// copy from constructed memory to unconstructed memory
auto q = uninitialized_copy(vec.begin(), vec.end(), p);
// fill unconstructed memory
uninitialized_fill(q, vec.size(), "");What’s the advantage of using
std::allocator
instead ofnew array
in C++?Question
I’ve just read about
std::allocator
. In my opinion, it is more complicated to use it instead of usingnew
anddelete
.With
allocator
we must explicitly allocate heap memory, construct it, destroy it, and then finally deallocate the memory. So why was it created?In which cases can it be used and when should it be used instead of new and delete ?
Answer
In my opinion, it is more complicated to use it instead of using new and delete.
Yes, but it is not meant to replace
new
anddelete
, it serves a different purpose.With allocator we must explicitly allocate heap memory, construct it, destroy it, and then finally deallocate the memory.
So why was it created?
Because sometimes you want to separate allocation and construction into two steps (and similarly to separate destruction and deallocation into two steps). If you don’t want to do that, don’t use an allocator, use
new
instead.In which cases can it be used and when should it be used instead of new and delete?
When you need the behaviour of an allocator, not the behaviour of
new
anddelete
, obviously! The typical case is when implementing a container.Consider the following code:
1
2
3
4
5std::vector<X> v;
v.reserve(4); // (1)
v.push_back( X{} ); // (2)
v.push_back( X{} ); // (3)
v.clear(); // (4)Here line (1) must allocate enough memory for four objects, but not construct them yet.
Then lines (2) and (3) must construct objects into the allocated memory.
Then line (4) must destroy those objects, but not deallocate the memory.
Finally, in the vector’s destructor, all the memory can be deallocated.
So the vector cannot just use
new X()
ordelete &m_data[1]
to create and destroy the objects, it must perform allocation/deallocation separately from construction/destruction. A container’s allocator template argument defines the policy that should be used for (de)allocating memory and constructing/destructing objects, allowing the container’s use of memory to be customised. The default policy is thestd::allocator
type.So you use an allocator when an allocator is required (such as when using a container) and you use
std::allocator
when you don’t want to provide a custom allocator and just want the standard one.You don’t use an allocator as a replacement for
new
anddelete
.