[C++学习笔记] 动态内存

静态内存用于保存局部static对象类中的static数据成员以及定义在任何函数之外的变量
栈内存用于保存定义在函数之内的非static对象
分配在这两者中的对象由编译器自动创建和销毁

除此之外 每个程序还有一个内存池 被称作自由空间或堆
用于储存动态分配的对象 这些对象是程序运行时分配的对象
由程序来控制生存期 即必须被代码显式地销毁

C++中动态内存管理是通过一对运算符newdelete来完成的

智能指针

为了更安全更容易地使用动态内存 新标准还提供了两种智能指针: shared_ptrunique_ptr 它们负责自动释放所指向的对象
这两者的区别在于管理底层指针的方式:

  • shared_ptr允许多个指针指向同一对象
  • unique_ptr独占所指向的对象

这两者都是模板类 定义在头文件memory

默认初始化的智能指针保存着一个空指针

shared_ptr

最安全的分配和使用动态内存的方法是调用make_shared()

shared_ptr<int> p = make_shared<int>(42);

当对shared_ptr进行拷贝和赋值时 每个shared_ptr都会记录当前有多少个其他shared_ptr指向相同的对象
可以认为每个shared_ptr都有一个关联计数器 称之为引用计数

当进行拷贝初始化或将其作为参数传递给另一个函数以及作为函数返回值时 引用计数+1
当对其赋予新值或是其被销毁时(如局部shared_ptr离开作用域) 引用计数-1

一旦shared_ptr的引用计数为0 则自动释放它所管理的对象
shared_ptr是通过析构函数来完成销毁工作的

使用动态内存的一个常见目的是允许多个对象共享相同状态
在自由空间分配的内存是无名的 因此new无法为其分配的对象命名 而是返回一个指向该对象的指针

通过new可以分配动态数组 int *p = new int[12];
但事实上动态数组并不是数组类型 而是其对应元素类型的指针
故不能使用一些数组操作 比如对其使用begin()end()函数 也不能使用for范围语句来处理

new分配的对象 不管是单个的还是动态数组 都是默认初始化的
因此可能会出现未定义的情况 最好是定义时进行初始化
如果初始化器数目小于元素数目 剩余元素将进行默认值初始化 **
若大于 则抛出异常并
且不会分配任何内存**

int *pi = new int(1024); //pi指向的对象的值为1024
string *ps = new string(3,'a'); // *ps为"aaa"
vector<int> *pv = new vector<int>{0,1,2,3};
int *pia = new int[3]{0,1}; //列表初始化动态数组 前两个元素为对应初始化器元素 第三个元素值初始化为0

虽然不能定义长度为0的数组 但可以定义长度为0的动态数组
因为动态数组本质上是个指针而不是数组类型 长度为0的动态数组返回合法的非空指针 类似于尾后指针 不可对其解引用

相对应的 与释放单个对象不同 释放动态数组时需要在指针前加一个方括号

delete p;//删除单个对象 
delete [] pa; //删除动态数组

删除动态数组时 数组中的元素按逆序销毁
即最后一个先被销毁 然后是倒数第二个 以此类推

可以使用直接初始化的方式来利用new来初始化智能指针

shared_ptr<int> p1(new int(1024)); 
//注意 不能使用赋值初始化 因为指针参数的构造函数是explicit的 不接受隐式转换
shared_ptr<int> p1=new int(1024) //这种方式是错的 不能使用赋值初始化

使用内置指针初始化智能指针时 要么内置指针指向的是动态内存 要么就必须自己为其提供定义delete操作

shared_ptr<T> p(q, del) //del为自定义的释放操作

使用delete释放内存时 释放非自由空间的内存或是多次释放同一内存 都是未定义的行为

不要混用普通指针和智能指针
因为将一个智能指针绑定到一个普通指针时 就将内存的管理责任也移交了
一旦这么做了 就不应该再使用内置指针来访问内存了 因为可能会被智能指针释放

unique_ptr

可以使用unique_ptr来管理new分配的动态数组

unique_ptr<int []> up(new int[10]);

unique_ptr定义了下标运算符 可以直接通过up[i]来访问动态数组中的元素

shared_ptr不直接支持管理动态数组
如果要使用shared_ptr管理动态数组 必须提供自己定义的删除器

shared_ptr<int> sp(new int[10], [](int *p){delete []p});

shared_ptr不支持下标 并且智能指针类型不支持算术运算
因此必须使用get来返回一个内置指针 通过这个内置指针访问元素

for(int i = 0; i != 10; ++i) {
	*(sp.get( ) + i) = i;
}

使用.get()函数可以从智能指针返回一个普通指针提供给那些不能使用智能指针的代码
但永远不要使用get初始化另一个智能指针或为另一个智能指针赋值

通过.reset()函数可以改变shared_ptr的指向
.reset() 将指针置空
.reset(q) 将指针指向q
.reset(q, d) 利用d而非delete释放q 因为如果使用智能指针管理的资源不是new分配的动态内存 要传递一个删除函数

不要使用相同的内置指针初始化或reset多个智能指针

shared_ptr不同 没有类似于make_shared这种库函数
当定义一个unique_ptr时 需要将其绑定到一个new返回的指针上
并且必须使用直接初始化方式 不能赋值初始化

不能直接拷贝或者赋值unique_ptr
但可以通过调用.release().reset()将指针的所有权从一个非constunique_ptr转移给另一个

.release()返回当前保存指针并将其置空 可以切断unique_ptr与当前管理对象的联系
但不会释放内存 如果我们不用另一个智能指针来保存.release()返回的指针 就必须通过程序来负责资源的释放

shared_ptr不同 向unique_ptr传递删除器会影响到unique_ptr的类型
所以 与重载关联容器的比较操作类似 必须在类型定义时 即unique_ptr后的尖括号中 在类型之后额外指定删除器类型
在创建和reset一个这种unique_ptr对象时 都必须提供一个指定类型的可调用对象用作删除器

unique_ptr<int, decltype(end_connection)*> p(&c, end_connection);

weak_ptr

weak_ptr是一种不控制所指向对象生存期的智能指针
它指向于一个shared_ptr管理的对象

将一个weak_ptr绑定到一个shared_ptr上不会改变shared_ptr的引用计数
即使有weak_ptr指向对象 也不影响引用计数归0后shared_ptr释放内存 所以是一种"弱"共享

由于对象可能不存在 不能使用weak_ptr直接访问对象
而必须使用.lock()函数 此函数检测weak_ptr指向的对象是否存在 存在则返回指向该对象的shared_ptr 否则返回一个空的shared_ptr

weak_ptr可以用定义核查指针 在使用前检测某个对象是否存在

allocator

通过标准库的allocator类 可以将内存分配和对象构造分离开来
它分配的是原始的 未构造的内存 它是一个模板类

allocator<string> alloc; 
auto const p = alloc.allocate(n);

为了使用allocator分配的内存 必须通过.construct()allocator分配的内存构造对象

auto q = p; //q指向最后一个元素之后的位置
alloc.construct(q++); //构造空串
Alloc.construct(q++, 3, 'c'); // 构造了一个 ccc 的字符串
Alloc.construct(q++, "hi"); // 构造了一个 hi 的字符串

使用结束后 通过.destory()可以销毁单个对象
销毁元素以后 可以通过.deallocate(p, n)释放从p开始的n块内存