C++ Primer:第12章 动态内存
第12章 动态内存
- 全局对象在程序启动时分配,在程序结束时销毁;局部自动对象在进入其定义所在的程序块被创建,在离开块时销毁;局部static对象在第一次使用前分配,在程序结束时销毁。
- 动态分配对象的生存期与创建位置无关,只有当被显式释放时才会被销毁。
- 静态内存用来保存局部static对象、类static数据成员、在函数外定义的变量;栈内存用来保存在函数内定义的非static对象。分配在静态内存或栈内存的对象由编译器自动创建和销毁。对于栈对象,仅在其定义的程序块运行时才存在;static对象在使用前分配,在程序结束时销毁
- 堆内存用来保存动态分配的对象。动态分配的对象由程序手动控制,当不再使用该对象时必须对其显式销毁。
12.1 动态内存与智能指针
- new:在动态内存中为对象分配空间,根据需要选择性地初始化对象,返回一个指向该对象的指针;delete:接受一个指向动态对象的指针,销毁对象,释放与对象关联的内存。
- 动态内存的使用容易出现内存泄漏,引用非法内存的指针等错误。
- 智能指针负责自动释放所指对象,定义在头文件memory中。shared_ptr允许多个指针指向同一对象;unique_ptr则“独占”所指对象;weak_ptr是一种弱引用,指向shared_ptr管理的对象。
shared_ptr与unique_ptr都支持的操作 | 描述 |
---|---|
shared_ptr sp unique_ptr up | 空智能指针,指向类型为T的对象。 |
p | 以p作为判断条件,若p为空智能指针则返回false, 若p指向一个动态对象则返回ture。 |
*p | 解引用p,获取p指向的对象。 |
p->mem | 等价于(*p ).mem。 |
p.get() | 返回一个普通指针,指向p所指的对象。 慎用,若delete p则p.get()变成悬空指针。 |
swap(p, q) p.swap(q) | 交换p和q中的指针。 |
12.1.1 shared_ptr类
shared_ptr独有的操作 | 描述 |
---|---|
make_shared(args) | 返回一个shared_ptr,指向一个动态分配的对象, 该对象类型为T,以args初始化,若args为空,则值初始化对象。 |
shared_ptr p(q) | q是shared_ptr,将q拷贝给p,所指对象的引用计数递增。 要求q中的指针必须能转换成T*。 |
p=q | p和q所保存的指针必须可相互转换。 p改变前所指对象的引用计数递减,若为0则释放该对象的内存。 q所指对象的引用计数递增。 |
p.use_count() | 返回p所指对象的引用次数。 运行速度可能很慢,主要用于调试。 |
p.unique() | 若p所指对象的引用计数为1,返回ture,否则返回false。 |
shared_ptr<int> p1; // p1是空智能指针
shared_ptr<int> p2 = make_shared<int>(1); // p2指向一个值为1的对象
- 默认初始化的智能指针保存着一个空指针(p.get() = nullptr)。智能指针可作为条件判断其是否为空。
shared_ptr<int> p;
if (p) //若不是空指针,则对指针解引用
cout << "p is not nullptr : " << *p << endl;
else //若是空指针,则输出提示
cout << "p is nullptr" << endl;
- 拷贝一个shared_ptr(如用make_shared等初始化一个shared_ptr、用shared_ptr赋值另一个shared_ptr、shared_ptr作为参数传入函数、函数返回shared_ptr)时,引用计数+1;shared_ptr被赋值,或shared_ptr被销毁(如shared_ptr离开作用域)时,引用计数-1.
- 构造函数控制初始化,析构函数控制销毁操作,一般用来释放对象所分配的资源。析构函数销毁shared_ptr时,shared_ptr所指对象的引用计数-1。若引用计数为0,析构函数便会销毁对象,并释放对象所占内存。
shared_ptr<Foo> factory(T arg)
{
return make_shared<Foo>(arg);//返回shared_ptr,指向一个类型为Foo的对象,用类型为T的arg初始化
}
void use_factory(T arg)
{
shared_ptr<Foo> p = factory(arg);// 调用factory初始化p,p所指对象的引用计数+1变成1
}// use_factory执行结束,销毁p,p所指对象的引用计数-1变成0,销毁对象并释放内存
shared_ptr<Foo> use_factory(T arg)
{
shared_ptr<Foo> p = factory(arg);// 调用factory初始化p,p所指对象的引用计数+1变成1
return p;//返回p,p所指对象的引用计数+1变成2
}// use_factory执行结束,销毁p,p所指对象的引用计数-1变成1,不会销毁对象并释放内存
- 在容器中存入shared_ptr,若只需要使用其中一部分元素(如泛型算法unique),则记得使用erase删除不再需要使用的元素。
- 程序使用动态内存的原因:程序不知道自己需要使用多少对象(如容器类);程序不知道所需对象的准确类型(如第15章面向对象程序设计);程序需要在多个对象间共享数据(自定义类)。
12.1.2 直接管理内存
- new分配动态内存,delete释放new分配的内存。
- 若类中使用智能指针,则类可以使用默认版本的拷贝、赋值和销毁成员函数,而new和delete不行。故应尽量使用智能指针,而避免使用new和delete。
1. new
- 在自由空间(堆)中分配的内存是无名的,故new无法为其分配的对象命名,而是返回一个指向该对象的指针。若new分配的对象默认初始化,则内置类型或组合类型的对象的值未定义。
// 默认初始化
int *p1 = new int; // p1指向一个类型为int、未定义的无名对象
string *p2 = new string; // p2指向一个空串
// 值初始化
int *p3 = new int(); // p3指向值为0的对象
string *p4 = new string(); // p4指向空string
// 直接初始化
int *p5= new int(1); // p5指向值为1的对象
string *p6 = new string(3,'1');// p6指向值为"111"的对象
vector<int> *p7 = new vector<int>{1,2,3};// p7指向值为{1,2,3}的对象
- 只有当括号中仅有单一初始化器时才能对new使用auto。
auto p1 = new auto(1); // 正确
auto p2 = new auto{1, 2, 3}; // 错误
auto p3 = new vector<int>{1, 2, 3}; //正确
- 动态分配的const对象必须初始化。若类类型已定义默认构造函数,则其const动态对象可以隐式初始化,而其他类型的对象必须显式初始化。
- 若分配内存失败(如堆内存已耗尽),new返回空指针,抛出异常bad_alloc。使用定位new可传递额外参数nothrow避免抛出异常。
// 当堆内存已耗尽时
int *p1 = new int; // new返回空指针,抛出异常bad_alloc
int *p2 = new (nothrow) int; //定位new,传递nothrow避免抛出异常
2. delete
- delete执行的两个动作:销毁指针所指的对象;释放对应的内存。
- 传递给delete的指针必须指向动态分配的内存,或者一个空指针。释放一块并非new分配的内存,或者将所指对象释放多次,其行为都是未定义的。编译器通常无法分辨指针指向的是静态内存还是动态内存,也无法分辨指针所指的内存是否被释放过。
- 一个const对象的值不能被改变,但它本身是可以被销毁的。
- 与类类型不同,内置类型的对象被销毁时什么也不会发生,特别是,当一个指针离开其作用域时,它所指对象什么也不会发生。若指针指向动态内存,那么内存不会被自动释放。
Foo* factory(T arg)
{
return new Foo(arg);
}// 调用factory后,使用delete显式销毁动态分配的内存
void use_factory(T arg)
{
Foo *p = factory(arg);
}// 若在作用域内未delete p,离开作用域后,程序无法释放其内存
void use_factory(T arg)
{
Foo *p = factory(arg);
delete p;// 在作用域内释放其内存
}
void use_factory(T arg)
{
Foo *p = factory(arg);
return p;// 调用者必须释放内存。
}
- 使用new和delete管理动态内存常见3个问题:忘记delete内存;使用已释放掉的对象;同一内存释放多次。
- 空悬指针即指向一块曾经保存数据对象但现在已经无效的内存的指针。避免空悬指针的方法:避免过早delete p,在p即将离开其作用域之前再delete p。若需要保留指针,可在delete p后p=nullptr,该方法仅对p有效,对其它指向相同内存的指针无效。
12.1.3 shared_ptr和new结合使用
定义和改变shared_ptr的其它方法 | 描述 |
---|---|
shared_ptr p(q) shared_ptr p(q, d) | p管理内置指针q所指向的对象; q必须指向new分配的内存,且能够转换为T*类型; d是可调用对象,用来替换delete。 |
shared_ptr p(u) | u是unique_ptr, p从u接管对象的所有权,并将u置为空。 |
shared_ptr p1(p2, d) | p1是p2的拷贝, d是可调用对象,用来替换delete。 |
p.reset() p.reset(q) p.reset(q, d) | 与赋值类似, 若p原所指对象的引用计数为0,则调用delete或d释放内存。 |
- 接受指针参数的智能指针构造函数是explicit,因此不能将一个内置指针隐式转换为一个智能指针,必须使用直接初始化形式。一个返回shared_ptr的函数不能在其返回语句中隐式转换一个普通指针,必须将shared_ptr显式绑定返回的指针上。
shared_ptr<int> p1; // nullptr
shared_ptr<int> p2 = make_shared<int>(2); // make_shared
shared_ptr<int> p3(new int(3)); // new
shared_ptr<int> p4 = new int(4); // error
shared_ptr<int> clone(int p)
{
return new int(p); // error
return shared_ptr<int>(new int(p)); // ture
}
- 默认情况下,一个用来初始化智能指针的普通指针必须指向动态内存,因为智能指针默认使用delete释放其关联的对象。
- 不要混合使用普通指针和智能指针,推荐使用shared_ptr而不用new,避免将同一内存绑定到多个独立创建的shared_ptr(指向同一对象的指针个数与该对象的引用计数不一致)。
- get用来将指针的访问权限传递给代码,只有在确定代码不会delete指针的情况下才能使用get。不要使用get初始化另一智能指针或为智能指针赋值。
12.1.4 智能指针和异常
- 一种确保资源被释放的方法是使用智能指针。若使用智能指针,即使程序过早结束,智能指针也能确保在内存再需要时将其释放。
- 与管理动态内存类似,我们通常使用类似的技术管理不具有良好定义的析构函数的类(分配了资源,但未定义析构函数来释放这些资源的类)。
- 删除器必须能够完成释放shared_ptr中保存的指针的操作。
- 使用智能指针的基本规范:不使用相同的内置指针值初始化或reset多个智能指针;不 delete p.get();若使用p.get(),记得当最后一个对应的智能指针被销毁后,p.get()失效;若使用智能指针管理的资源不是new分配的内存,记得传递一个删除器。
12.1.5 unique_ptr
unique_ptr操作 | 描述 |
---|---|
unique_ptr u unique_ptr<T, D> u unique_ptr<T, D> u(d) | 空unique_ptr,可以指向类型为T的对象; 使用delete、类型为D的可调用对象、类型为D的可调用对象d释放指针。 |
u.reset() u.reset(nullptr) u.reset(p ) | 释放u原指向的对象,令u为空或指向p所指的对象。 |
u = nullptr | 释放u原指向的对象,将u置为空。 |
u.release() | u放弃对指针的控制权,返回指针,并将u置为空。 |
- unique_ptr"拥有"其所指的对象。某一时刻只能有一个unique_ptr指向一个给定对象。当unique_ptr被销毁时,其所指对象也被销毁。
- unique_ptr只能使用new直接初始化。unique_ptr不支持普通的拷贝或赋值操作。
- release切断unique_ptr与其所指对象间的联系,release返回的指针常用于初始化另一个智能指针或给另一智能指针赋值。
unique_ptr<int> p1;
unique_ptr<int> p2(new int(2));
p1 = p2; // error
p1.reset(p2.release());
unique_ptr<int> p3(p2); // error
unique_ptr<int> p4(p2.release());
p4.release(); // 将p4置为空,但不会释放内存
auto p5 = p4.release();
delete p5;
- 不能拷贝unique_ptr的例外:可以拷贝或赋值一个将要销毁的unique_ptr(unique_ptr可作为参数或返回值)
- unique_ptr和shared_ptr默认使用delete释放所指对象,也可重载删除器释来释放对象。unique_ptr重载删除器时需提供删除器类型D(decltype(d) *)。
12.1.6 weak_ptr
weak_ptr操作 | 描述 |
---|---|
weak_ptr w | 空weak_ptr,可指向类型为T的对象。 |
weak_ptr w(sp ) | sp是shared_ptr,与sp指向相同对象的weak_ptr。 要求T必须能转换为sp指向的类型。 |
w=p | p是shared_ptr或weak_ptr,w与p共享对象。 |
w.reset() | 将w置为空。 |
w.use_count() | 与w共享对象的shared_ptr个数。 |
w.expired() | 若w.use_count()==0,返回ture,否则返回false。 |
w.lock() | 若w.expired()==true,返回空shared_ptr,否则返回对应的shared_ptr。 |
- weak_ptr是一种不控制所指对象生存期的智能指针,它指向由一个shared_ptr管理的对象。weak_ptr“弱”共享对象,不会影响shared_ptr的销毁以及shared_ptr所指对象的引用计数。
- weak_ptr不能直接访问对象,必须调用lock()。
shared_ptr<int> p1 = make_shared<int>(1);
weak_ptr<int> p2(p1);
shared_ptr<int> p = p2.lock();
12.2 动态数组
- 当容器(如vector和string)需要重新分配内存时,必须一次性为多个元素分配内存,由此需要使用动态数组。
- 一次分配一个对象数组的两种方法:new和allocator。
- 大多数应用都没有直接访问动态数组的需求,当一个应用需要可变数量的动态对象时,通常使用vector等标准库容器。大多数应用应该使用标准库容器而不是动态数组,使用容器更不容易出现内存管理错误并且可能有更好的性能。使用容器的类可以使用默认版本的拷贝、赋值和析构操作,而分配动态数组的类必须定义自己版本的操作,在拷贝、复制以及销毁对象时管理所关联的内存。
12.2.1 new和数组
- 动态数组:new T[]分配的内存(返回一个数组元素类型的指针,而非一个数组类型的对象)。
- 动态数组不是数组类型,故不能对动态数组调用begin或end,也不能使用范围for。
int *p1 = new int[3];
typedef int arrT[3];
int *p2 = new arrT;
- new []分配的对象默认初始化,new []()值初始化,new []{}列表初始化。因为new []()不能使用初始化器,故不能用auto分配数组。
int *p1 = new int[3]; //默认初始化
int *p2 = new int[3](); //值初始化
int *p3 = new int[3](1); //error
int *p4 = new auto[3](1); //error
int *p5 = new int[3]{1,2,3}; //列表初始化
auto p6 = new auto(1); // error
- 不能创建大小为0的静态数组,但可以new T[0],其返回的指针类似于尾后指针。
int arr[0]; // error
int *p = new int[0]; // 正确,但不能*p
- delete[] p释放动态数组,数组中的元素按逆序销毁。delete一个指向动态数组的指针时忽略[],或者delete一个指向单一对象的指针时添加[],两者的行为都是未定义的。
- 当unique_ptr指向数组,不能使用成员访问运算符(点和箭头运算符),但可以使用下标运算符访问数组元素。
指向数组的unique_ptr | 描述 |
---|---|
unique_ptr<T[]> u | u指向一个动态数组,数组元素为T。 |
unique_ptr<T[]> u(p ) | u指向p所指的动态数组,p必须可转换成T*。 |
u[i] | 返回u所指数组中位置为i的对象。 |
unique_ptr<int[]> up(new int[3]);
up.release();
- shared_ptr不直接支持管理动态数组,需提供自定义的删除器后才能管理动态数组。
shared_ptr<int> sp( new int[3], [](int *p){delete[] p;} );
sp.reset();
*(sp.get()+i); // 未自定义下标运算符
12.2.2 allocator类
allocator类 | 描述 |
---|---|
allocator a | 定义一个allocator对象,可为类型为T的对象分配未构造的内存 |
p = a.allocate(n) | 分配一块原始的,未构造的内存,用于保存n个类型为T的对象。 |
a.construct(p, args) | 在p指向的内存中构建一个对象。 |
a.destroy(p ) | 析构对p所指的对象。 |
a.deallocate(p, n) | 释放从p开始的内存,该内存保存n个对象。 若已构造对象,需先a.destroy(p )。 |
allocator算法 | 描述 |
---|---|
uninitialized_copy(b1, e1, b2) | 将(b1,e1]中的元素拷贝到从b2开始的内存。 |
uninitialized_copy_n(b1, n, b2) | 从b1开始,拷贝n个元素到从b2开始的内存。 |
uninitialized_fill(b, e, t) | 在(b,e]内创建值为t的对象。 |
uninitialized_fill_n(b, n, t) | 从b开始,创建n个值为t的对象 |
- new将内存分配与对象构造相组合,delete将内存释放与对象析构相组合。allocator类定义在头文件memory中,可将内存分配与对象构造相分离。
- allocator提供一种类型感知的内存分配方法,其分配的内存是原始的,未构造的。使用未构造对象的内存,其行为是未定义的。只能对已构造对象进行destroy操作。
allocator<int> a;
size_t n = 3;
auto p1 = a.allocate(n); // 分配原始内存
auto p2 = p1;
for (size_t i = 0; i < n; ++i, ++p2) // 构造对象
a.construct(p2, i);
while (p2 != p2)
a.destroy(--p2); // 析构对象
a.deallocate(p1, n); // 释放内存
12.3 使用标准库:文本查询程序
- 在设计类时,先在程序中使用类, 再实现类的成员。