目录结构:
contents structure [-]
动态内存和智能指针
使用 shared_ptr 管理内存
使用 new 直接管理内存
shared_ptr 和 new 结合使用
- unique_ptr
- weak_ptr
程序异常情况下的资源释放处理
使用智能指针的陷阱
动态数组
new 管理动态数组内存
allocator 管理动态数组内存
静态内存用来保存局部 static 对象, 类 static 数据成员以及定义在任何函数之外的变量. 栈内存用来保存定义在函数内的非 static 对象. 分配在静态和栈内存中的对象由编译器自动创建和销毁. 对于栈对象, 仅在其定义的程序块运行时才存在; static 对象在使用之前分配. 在程序结束时销毁.
除了静态内存和栈内存, 每个程序还拥有一个内存池. 这部分内存被称为自由空间 (free store) 或堆 (heap). 程序用堆来存储动态分配(dynamically allocate) 的对象, 也就是那些在程序运行时分配的对象. 动态内存的生存期由程序来控制, 也就是说, 当动态内存不再使用时, 我们的代码必须显式地销毁它们.
1 动态内存和智能指针
在 C++ 中, 动态内存的管理是通过一对运算符来完成的: new 和 delete.
new: 在动态内存中为对象分配空间并返回一个指向该对象的指针, 我们可以选择对对象进行初始化.
delete: 接受一个动态对象的指针, 销毁该对象, 并释放与之有关的内存.
动态内存的使用非常容器出现问题, 因为确保在正确的时间释放内存是极其困难的. 为了更好的管理动态内存, C++ 标准库在 < memory > 模块中提供了大量的智能指针类型, 这里笔者就介绍几种较常见的: shared_ptr 允许多个指针指向同一个对象, unqiue_ptr 则 "独占" 所指向的对象. 标准库还定义一个名为 weak_ptr 的伴随类, 它是一种弱引用, 指向 shared_ptr 所管理的对象.
1.1 使用 shared_ptr 管理内存
shared_ptr 是一个智能指针类, 它可以和其他的 shared_ptr 共享同一个动态内存的所有权. 出现如下两种情况的话, 动态内存会被自动释放:
a. 最后一个保留动态内存的 shared_ptr 对象被销毁.
b. 最后一个保留动态内存的 shared_ptr 对象重新保存另一个动态内存(通过 = 或 reset())
我们可以这样认为, 每个 shared_ptr 都有一个关联的计数器, 通常称为引用计数 (reference count). 无论何时我们拷贝一个 shared_ptr, 计数器都会增加. 例如, 当用一个 shared_ptr 初始化另一个 shared_ptr, 或将它作为参数传递给一个函数以及作为函数的返回值时, 它所关联的计数器就会递增. 我我们给 shared_ptr 赋予一个新值或是 shared_ptr 被销毁(一个局部的 shared_ptr 离开作用域) 时, 计数器就会递减.
一旦一个 shared_ptr 的计数器变为 0, 它就会自动释放自己所管理的对象.
在使用 shared_ptr 时, 我们无需关心内存的释放问题. 程序会自动帮助我们在合适的时机释放内存. 因此推荐在程序中使用 shared_ptr 来管理动态内存.
创建 shared_ptr 对象既可以通过它的构造方法, 也可以通过 make_shared 方法.
- #include <iostream> /*cout*/
- #include <memory> /*shared_ptr,make_shared*/
- #include <string> /*string*/
- using namespace std;
- int main(int argc,char* argv[]){
- shared_ptr<string> sp1 = make_shared<string>("hello");// 通过 make_shared 创建
- // {} 块代码
- {
- shared_ptr<string> sp2; //shared_ptr 的默认构造
- sp2 = sp1;// 将 sp1 复制给 sp2,sp1 和 sp2 指向相同的动态内存
- }// 退出块, sp2 对象被销毁. sp2 指向的动态内存不会被销毁(因为指向该内存的还有 sp1, 所以程序不会自动该动态内存)
- // 现在只有 sp1 对象指向该动态内存了
- cout <<*sp1 << endl;// 打印 sp1 中的动态管理的值
- return 0;// 退出方法, 离开 sp1 对象作用域, sp1 对象被销毁. 由于 sp1 对象是最后一个指向动态内存的 shared_ptr 对象, 所以该动态内存被释放.
- }
shared_ptr(以及其他的智能指针)除了可以管理 new 分配的资源, 也可以管理不是 new 分配的资源, 这时候记得传递给它一个删除器(因为默认的删除器, 是针对 new 分配资源的删除器, 也就是调用 delete), 例如下面一个网络库代码:
- struct destination; // 表示连接的目标信息
- struct connection; // 使用连接所需信息
- connection connect(destination*); // 打开连接
- void disconnect(connection); // 关闭给定连接
- void end_connection(connection *p){
- diconnect(*p);
- }
- void f(destination &d){
- // 未使用 shared_ptr
- /*
- // 获得一个连接; 记住使用完后要关闭它
- connection c = connect(&d);
- // 使用连接
- // 如果我们在 f 退出之前, 忘记调用 disconnect, 就无法关闭 c 了.
- */
- // 使用 shared_ptr
- connection c = connect(&d);
- shared_ptr<connection> p(&c,end_connection);// 一定要传入自定义的删除器, 也可以用 lambda 表达式
- // 使用连接
- // 当 f 退出时(即使是由于异常而退出),connection 会被正确关闭
- }
1.2 使用 new 直接管理内存
C++ 语言定义两个运算符来分配和释放动态内存. 运算符 new 分配内存, delete 释放 new 分配的内存.
在堆中分配的内存是无名的, 因此 new 无法为其分配的对象命令, 而是返回一个指向该对象的指针:
int *pi = new int;//pi 指向一个动态的, 未初始化的无名对象
我们可以采用直接构造的方式来初始化一个动态分配对象, 我们可以使用传统的的构造方式(使用圆括号), 我们还可以使用列表初始化的方法(花括号):
- int *p = new int; //p 指向一个未初始化的 int
- int *p2 = new int(); //p2 指向的对象的值为 0
- int *pi = new int(1024); //pi 指向的对象的值为 1024
- string *ps = new string(10,"9"); //*ps 为 999999999
- //vector 有 10 个元素, 依次从 0 到 9
- vector<int> *pv = new vector<int>{
- 0,1,2,3,4,5,6,7,8,9
- }
如果我们提供了一个括号包围的初始化器, 就可以使用 auto 从此初始化器来推断我们想要分配的对象的类型. 但是, 由于编译器要使用初始化器的类型来推断我们想要的创建的类型, 只有当括号中有单一初始化器时才可用 auto
- auto p1 = new auto(obj); //p 指向一个与 obj 相同类型的对象
- auto p2 = new auto{
- a,b,c
- }; // 错误, 括号中只能有单一初始化器
p1 的类型是指针, 指向从 obj 推断出来的类型. 若 obj 是 int, 那么 p1 就是 int * 类型, 若 obj 是 string, 那么 p1 就是 string * 类型.
动态分配的 const 对象
用 new 分配 const 对象是合法的:
- // 分配并初始化一个 const int
- const int* pci = new const int(1024);
- // 分配并默认初始化一个 const 的空 string
- const string* pcs = new const string();
和其它 const 一样, 一个动态的 const 对象必需要初始化. 对于一个定义了默认构造函数的类类型, 其 const 对象可以隐式初始化, 而其他的类型必须显示初始化. 由于分配的对象是 const 的, new 返回的指针是一个指向 const 的指针.
内存耗尽
虽然现代计算机通常都具备大容量内存, 但是自由空间被耗尽的情况还是有可能发生. 一旦一个程序用光了它所有的内存, new 表达式就会失败. 默认情况下, 如果 new 不能分配所要求的空间, 它就会抛出一个类型为 bad_alloc 的异常. 我们可以改变 new 的使用方式来阻止它:
- // 如果分配失败, new 返回一个空指针
- int *p1 = new int; // 如果分配失败, 抛出 std::bad_alloc 的异常.
- int *p2 = new (nothrow) int; // 如果分配失败, new 返回一个空指针
释放动态内存
为了防止内存耗尽, 在动态内存使用完毕后, 必须将其归还给系统. 我们通过 delete 表达式 (delete expression) 来将动态内存归还给系统. delete 表达式接受一个指针, 指向我们想要释放的对象:
delete p;//p 必须指向一个动态内存分配的对象或是一个空指针
与 new 类型类似, delete 表达式也执行两个动作: 销毁给定的指针指向的对象; 释放对应的内存
悬空指针
当我们 delete 一个指针后, 指针值就变为无效了. 虽然地址以及无效, 但在很多机器上仍然保存着 (以及释放的) 动态内存的地址. 在 delete 之后, 指针就变成人们所说的悬空指针(dangling pointer), 即, 指向一块曾经保存数据对象但现在已经无效的内存指针.
我们可以在指针即将离开其作用域之前释放掉它所关联的内存. 这样关联指针的内存被释放后, 就没机会继续使用指针了. 我们可以将 nullptr 赋予指针, 就清楚的指出指针不再指向任何对象.
下面使用一个 new 和 delete 的完整案例:
- using namespace std;
- int main(int argc,char *argv[]){
- int *p(new int(42)); // 指向动态内存
- auto q = p; //q 和 p 指向相同的内存
- // 在程序退出之前, 一定要 delete
- delete p; //p 和 q 均无效
- p = nullptr; // 指出 p 不再绑定到任何对象
- return 0;
- }
1.3 shared_ptr 和 new 结合使用
我们可以用 new 返回的指针来初始化智能指针
- shared_ptr<double> p1; //shared_ptr 可以指向一个 double
- shared_ptr<double> p2(new int(42)); //p2 指向一个值为 42 的 int
默认情况下, 一个用来初始化智能指针的普通指针必须指向动态内存, 因为智能指针默认使用 delete 释放它所关联的对象. 我们可以将智能指针绑定到一个指向其他类型的资源的指针上, 但是为了这么做, 必须提供自己的操作来代替 delete.
shared_ptr 类和 new 之间提供很多的相互转化操作, 比如 shared_ptr 的构造函数接受一个 new 的动态指针. shared_ptr 的 get()方法返回它所管理的动态指针. 虽然 shared_ptr 提供了丰富的相互转化操作, 但是笔者建议不要混合使用普通指针和智能指针, 混合使用将会使动态内存的释放问题更加复杂.
例如下面这个程序, 在不经意间就会造成指向已经释放内存的错误:
- shared_ptr<int> p(new int(42)); // 引用计数为 1
- int *q = p.get(); // 正确: 但使用 q 要注意, 不要让它管理的指针被释放
- { // 新的块
- shared_ptr<int>(q); // 两个独立的 shared_ptr 指向相同的内存
- }// 程序块结束, q 被销毁, 它指向的内存被释放
- int foo = *p; // 未定义: p 指向的内存已经被释放了
p 和 q 指向相同的内存. 由于它们是相互独立创建的, 因此各自的引用计数都是 1. 当 q 所在的程序块结束时, q 被销毁, 这会导致 q 指向的内存被释放. 从而 p 变成一个悬空指针, 意味着当我们试图使用 p 时, 将发生未定义的行为. 而且, 当 p 被销毁时, 这块内存会被第二次 delete.
get 用来将指针的访问权限传递给代码, 你只有在确定代码不会 delete 指针的情况下, 才能使用 get. 特别是, 永远不要使用 get 初始化另一个智能指针或者为另一个智能指针赋值.
- #include <iostream>
- #include <string>
- #include <memory>
- using namespace std;
- int main(int argc,char* argv[]){
- shared_ptr<int> p(new int(42));
- shared_ptr<int> q(p);
- if(!p.unique())
- p.reset(new string(*p)); // 如果我们不是唯一的用户, 分配新的拷贝
- *p += "0";// 我们知道自己是唯一的用户了, 可以改变对象的值
- return 0;
- }
- 1.4 unique_ptr
一个 unique_ptr"拥有" 它所指向的对象. 与 shared_ptr 不同, 某个时刻只能有一个 unique_ptr 指向一个给定对象. 当 unique_ptr 被销毁时, 它所指向的对象也被销毁.
与 shared_ptr 不同, 没有类似的 make_shared 的标准库函数返回一个 unique_ptr. 当我们定义一个 unique_ptr 时, 需要将其绑定一个 new 返回的指针上. 类似 shared_ptr, 初始化 unique_ptr 必须采用直接初始化.
接下来是一个使用案例:
- #include <string>
- #include <memory>
- using namespace std;
- int main(int argc,char *argv[]){
- unique_ptr<string> p1(new string("hello"));
- // 将所有权从 p1(指向 string hello)转移给 p2
- unique_ptr<string> p2(p1.release());//release 将 p1 置为空
- unique_ptr<string> p3(new string("world"));
- // 将所有权从 p3 转义给 p2
- p2.reset(p3.release());//reset 释放了 p2 原来指向的内存
- return 0;
- }
- 1.5 weak_ptr
weak_ptr 是一种不控制所指向对象生存期的智能指针, 它指向由一个 shared_ptr 管理的对象. 将一个 weak_ptr 绑定到一个 shared_ptr 不会改变 shared_ptr 的引用计数. 一旦最后一个指向对象的 shared_ptr 被销毁, 对象就会被释放. 即使有 weak_ptr 指向对象, 对象也还是会被释放, 因此, weak_ptr 的名字抓住这种智能指针 "弱" 共享对象的特点.
当我们创建一个 weak_ptr 时, 要用一个 shared_ptr 来初始化它:
- shared_ptr<int> p = make_shared<int>(42);
- weak_ptr<int> wp(p); //wp 弱共享 p;p 的引用计数未改变
wp 和 p 指向相同的对象. 由于是弱共享, 创建 wp 不会改变 p 的引用计数; wp 指向的对象可能会被释放掉.
由于对象可能不存在, 我们不能在 weak_ptr 上直接访问对象, 而是必需要调用 lock.
例如:
- if(shared_ptr<int> np = wp.lock()){// 如果 np 不为空, 则条件成立
- //np 与 p 共享
- }
标准库还提供了 weak_ptr 大量的相关操作函数, 读者可以自行翻阅.
1.6 程序异常情况下的资源释放处理
在我们的程序中, 当一个程序发生异常时要令发生异常后的程序流继续. 我们注意到, 这种程序需要确保在异常发生后资源能够被正确的释放. 一个简单的确保资源被释放的方法是使用智能指针.
如果使用智能指针, 即使程序块过早结束, 智能指针类也能确保在不存不再需要时将其释放掉:
- void f(){
- shared_ptr<int> sp(new int(42)); // 分配一个新对象
- // 这段代码抛出一个异常, 且在 f 中未被捕获
- }// 在函数结束时, shared_ptr 自动释放内存
函数的退出有两种情况, 正常处理结束或发生了异常, 无论哪种情况, 局部对象都会被销毁. 在上面的程序中, sp 是一个 shared_ptr, 因此 sp 销毁时检查引用计数. 在此例中, sp 是指向这块内存的唯一指针, 因此内存会被释放掉.
- void f(){
- int *ip = new int(42); // 动态分配一个新对象
- // 这段代码抛出一个异常, 且在 f 中未捕获
- delete ip; // 在退出之前释放内存
- }
如果在 new 和 delete 之间发生异常, 且未在 f 中捕获异常, 则内存就永远不会被释放了.
1.7 使用智能指针的陷阱
在使用智能指针时, 我们必需坚持一些基本规范:
1. 不使用相同的内置指针初始化多个智能指针.
2. 不 delete get()返回的指针.
3. 不使用 get()初始化或 reset 另一个智能指针.
4. 如果你使用 get 返回的指针, 记住当最后一个对于的智能指针销毁后, 你的指针就变成无效了.
5. 如果你使用智能指针管理的资源不是 new 分配的内存, 记住传递给它一个删除器.
2 动态数组
new 和 delete 运算符一次分配 / 释放一个对象, 但某些应用需要一次为很多对象分配内存的功能. 例如, vector 和 string 都是连续在内存中保存它们的元素.
c++ 语句定义了另外一种 new 表达式语法, 可以分配并初始化一个对象数组. 标准库中包含一个名为 allocator 类, 允许我们将分配和初始化分离.
2.1 new 管理动态数组内存
为了让 new 分配一个对象数组, 我们要在类型名之后跟一对方括号, 在其中指明要分配对象的数目. 例如:
- // 调用 get_size 确定分配多少个 int
- int *pia = new int[getsize()]; //pia 指向第一个 int
方括号的大小必须是整数, 不必是常量.
也可以使用数组的类型别名:
- typedef int arrT[42]; //arrT 表示 42 个 int 的数组类型
- int *p = new arrT; // 分配一个 42 个 int 的数组; p 指向第一个 int
我们通常称 new T[]分配的内存为 "动态数组", 但这种叫法某种程度上有些误导. 当用 new 分配一个数组时, 我们并未得到一个数组类型的对象, 而是得到一个数组类型的指针.
释放动态数组
为了释放动态数组, 我们使用一种特殊的形式的 delete - 在指针前加上一个空括号对:
- delete p; //p 必须指向一个动态分配的对象或为空
- delete []pa; //pa 必须指向一个动态分配的数组或为空
当我们释放一个指向数组的指针时, 空方括号是必须的: 它指示编译器此指针指向一个对象数组的第一个元素. 如果我们在 delete 一个指向数组的指针时忽略了方括号(或者在 delete 一个指向单一对象的指针时使用了方括号), 其行为都是未定义的.
- typedef int arrT[42]; //arrT 是 42 个 int 的数组的类型别名
- int *p = new arrT; // 分配一个 42 个 int 的数组; p 指向第一个元素
- delete[] p; // 方括号是必须的, 因为我们分配是的是一个数组
在最后说一个 shared_ptr 对动态数组的操作, shared_ptr 不支持直接管理动态数组, 要使用动态数组, 必需自定义删除器:
- #include <memory>
- using namespace std;
- int main(int argc,char* argv[]){
- // 提供一个删除器, 默认的删除器是 delete T, 我们这里是数组, 也就应该是 delete[] T, 所以应该提供 delete[]格式的删除器
- shared_ptr<int> sp(new int[10],[](int *p){delete []p}):
- //shared_ptr 未定义下标运算符, 并且不支持指针的算术运算
- for(size_t i = 0; i != 10; i++) {
- *(sp.get() + i) = i; //get()获取一个内置指针
- }
- sp.reset(); // 使用我们的 lambda 释放数组, 它使用 delete[]
- return 0;
- }
2.2 allocator 管理动态数组内存
new 有一些灵活性上的局限, 其中一方面表现在它将内存分配和对象构造组合在了一起. 类似的, delete 将对象析构和内存释放组合在一起.
标准库的 allocator 类帮助我们将内存分配和对象构造分离开来. 它提供一个类型感知的内存分配方法, 他分配的内存是原始的, 未构造的.
- #include <memory>
- #include <iostream>
- #include <string>
- using namespace std;
- int main()
- {
- int n = 3;
- allocator<string> alloc;
- string* const p = alloc.allocate(n);
- // 为了使用 allocate 分配的内存, 必须使用 construct 来构造对象.
- string* q = p;
- alloc.construct(q++); //*q 为空字符串
- alloc.construct(q++,5,'c'); //*q 为 ccccc
- alloc.construct(q++,"hi"); //*q 为 hi
- cout << *p << endl; // 正确
- cout << *q << endl; // 灾难: 指向未构造的内存
- while(q != p){
- alloc.destroy(--q); // 释放我们构造的 string
- }
- // 一旦元素被释放后, 我们就可以使用这部分内存来保存其它 string,
- // 也可以将其归还给给系统
- // 归还内存给系统
- alloc.deallocate(p,n);
- }
来源: http://www.bubuko.com/infodetail-3095863.html