C++ 学习笔记之(12) - 动态内存、智能指针和动态数组
程序中所使用的对象都有严格定义的生存期。
- 全局对象:在程序启动时分配,程序结束时销毁
- 局部自动对象:程序进入其定义所在块时创建,离开块时销毁
- 局部
static
对象:第一次使用前分配,程序结束时销毁 - 动态分配对象:显示创建,显示释放
内存存放区间
- 静态内存:局部
static
对象、类static
数据成员以及定义在任何函数之外的而变量 - 栈内存:定义在函数内的非
static
对象 - 自由空间(堆):动态分配对象,动态分配对象的生存期由程序控制
动态内存与智能指针
动态内存的管理是通过一对运算符完成的
- new:在动态内存中为对象分配空间并返回一个指向该对象的指针,用来对对象进行初始化
- delete:接受一个动态对象的指针,销毁该对象,并释放与之关联的内存
动态内存的使用容器出问题
- 内存泄漏:忘记释放内存
- 非法内存指针:在尚有指针引用内存的情况下就释放了呢村
新标准库提供了两种智能指针类型管理动态对象,智能指针与常规指针的区别在于它负责自动释放所指向的对象
shared_ptr
:允许多个指针指向同一个对象unique_ptr
:独占所指向的对象weak_ptr
:一种弱引用,指向shared_ptr
所管理的对象
shared_ptr
类
创建智能指针
shared_ptr<string> p1; // shared_ptr, 可以指向 string
shared_ptr<list<int>> p2; // shared_ptr, 可以指向 int 的 list
make_shared
函数
make_shared
在动态内存中分配一个队形并初始化它,返回指向此对象的shared_ptr
, 用其参数来构造给定类型的对象
// 指向一个值为“9999999999” 的 string
shared_ptr<string> p4 = make_shared<string>(10, '9');
// 也可使用 auto, p6 指向一个动态分配的空 vector<string>
auto p6 = make_shared<vector<string>>();
引用计数
- 拷贝时计数器递增
- 一个
shared_ptr
初始化另一个shared_ptr
- 将
shared_ptr
作为参数传递给函数 - 将
shared_ptr
作为函数的返回值
- 一个
- 计数器递减
- 给
shared_ptr
赋予新值时 shared_ptr
销毁时(比如局部shared_ptr
对象离开其作用域)
- 给
auto r = make_shared<int>(42); // r 指向的 int 只有一个引用者
r = q; // 给 r 赋值,令它指向另一个地址
// 递增 q 指向的对象的引用计数
// 递减 r 原来指向的对象的引用计数
// r 原来指向的对象已没有引用者,会自动释放
shared_ptr
销毁和释放内存
shared_ptr
的析构函数会递减它所指向的对象的引用计数,如果引用技术变为0
,shared_ptr
的析构函数就会销毁对象,并释放其占用的内存
void function(T arg)
{
shared_ptr<int> p = make_shared<int>(125);
// 情况1:不返回, p 为局部变量,函数结束时被销毁,p被销毁时,引用计数递减为0,故内存被释放
return p; // 情况2:返回 p 的拷贝, 引用计数进行递增操作,故即使 p 被销毁,但内存还有其他使用者,不会被释放
}
使用动态内存的原因
- 程序不知道自己需要使用多少对象, 比如容器类
- 程序不知道所需对象的准确类型
- 程序需要在多个对象间共享数据
直接管理内存
C++语言定义了两个运算符来分配和释放动态内存,相对于智能指针,直接管理容器出错
new
:分配内存,并返回一个指向该对象的指针。// 默认情况下,动态分配对象是默认初始化的,即内置类型或组合类型的对象的值将未定义,类使用默认构造函数 int *pi = new int; // pi 指向一个动态分配的、未初始化的无名对象,即默认初始化,`*pi值未定义 // 也可对动态分配对象进行值初始化,只需在类型名之后跟一对控括号 int *pi2 = new int(); // 值初始化为0, *pi2 为0 ///若括号中仅有单一初始化器,可使用 auto 推断想要分配的对象类型 auto p1 = new auto(obj); // p1类型是指针,指向从 obj 自动推断出的类型 auto p2 new auto{a, b, c}; // 错误:括号中只能有单个初始化器 //用 new 分配 const 对象是合法的, 下为分配并初始化一个 const int const int *pci = new const int(1024);
delete
:销毁给定的指针指向的对象,释放对应的内存。必须是指向动态分配的指针,或空指针。释放一块并非new
分配的内存,或者将相同的指针释放多次,其行为是未定义的空悬置镇:指向一块曾经保存数据对象但现在已经无效的内存的指针
shared_ptr
和new
结合使用
接受指针参数的智能指针构造函数是
explicit
的,故不能讲一个内置指针隐式转换为智能指针,必须使用直接初始化方式shared_ptr<int> p1 = new int(1024); // 错误:必须使用直接初始化方式 shared_ptr<int> p2(new int(1024)); // 正确:使用了直接初始化方式
不要混合使用普通指针和智能指针,因为内置指针访问智能指针所负责的对象是很危险的,因为无法知道对象何时会被销毁
不要使用
get
初始化另一个智能指针或为智能指针赋值。get
定义在智能指针类型中,迎来返回一个内置指针,指向智能指针管理的对象。shared_ptr<int> p(new int(42)); // 引用计数为 1 int *q = p.get(); // 正确:但使用 q 时要注意,不要让他管理的指针被释放 {// 新程序块 // 未定义:两个独立的`shared_ptr`指向相同的内存 shared_ptr<int>(q) }// 程序块结束,q 被销毁,它指向的内存被释放 int foot = *p; // 未定义:p 指向的内存已经被释放了
智能指针和异常
在发生异常时,智能指针仍能正确释放,但直接管理的内存不会自动释放
void f() { shared_ptr<int> sp(new int(42)); // 分配一个新对象 int *ip = new int(42); // 动态分配一个新对象 // 这段代码抛出一个异常,且在 f 中未被捕获 delete ip; // 在推出之前释放内存,发生异常时,没有执行,被跳过 } // 在函数结束时 shared_ptr 自动释放内存
删除器:释放
shared_ptr
中保存的指针,用来取代shared_ptr
被销毁时默认进行的delete
操作,在创建shared_ptr
时即可指定正确使用智能指针的接本规范
- 不使用相同的内置指针值初始化(或
reset
)多个智能指针 - 不
delete``get()
返回的指针 - 不使用
get()
初始化或reset
另一个智能指针 - 如果你使用
get()
返回的指针,记住当最后一个对应的智能指针销毁后,它会失效 - 如果智能指针管理的资源表示
new
分配的内存,记住传递给他一个删除器
- 不使用相同的内置指针值初始化(或
unique_ptr
unique_ptr
拥有它所指向的对象,即某时刻只能有一个unique_ptr
指向某对象,当其被销毁时,所指向的对象也被销毁
unique_ptr
无类似make_shared
函数。定义时,需要将其绑定到new
返回的指针上类似
shared_ptr
,初始化unique_ptr
需采用直接初始化形式unique_ptr
独占其指向的对象,故不支持普通的拷贝或赋值操作unique_ptr<string> p1(new string("hao")); // p2 指向一个值为 hao 的 string unique_ptr<string> p2(p1); // 错误:unique_ptr 不支持拷贝 unique_ptr<string> p3; p3 = p2; // 错误:unique_ptr 不支持赋值
unique_ptr
虽然不能赋值或拷贝,但可以调用release
或reset
将所有权从非const
的unique_ptr
转移到另一个unique_ptr
// 接上 unique_ptr<string> p2(p1.release()); // release 将 p1 置为空,并将所有权转移给 p2
unique_ptr
在将被销毁时可以拷贝或赋值, 比如从函数返回unique_ptr
unique_ptr<int> clone(int p) { return unique_ptr<int>(new int(p)); }
类似
shared_ptr
,unique_ptr
也可重载其默认的删除器// p 指向一个类型为 objT 的对象,并使用一个类型为 delT 的对象释放 objT对象 // 它会调用一个名为 fcn 的 delT 类型对象 unique_ptr<objT, delT> p (new objT, fcn);
weak_ptr
一种不控制所指向对象生存期的智能指针,指向由一个shared_ptr
管理的对象。且绑定后不会改变shared_ptr
的引用计数
auto p = make_shared<int>(42);
weak_ptr<int> wp(p); // wp 弱共享 p; p 的引用计数未改变
// 由于对象可能不存在,故不能使用 weak_ptr 直接访问队形,而必须调用 lock
if(shared_ptr<int> np = wp.lock()) // 如果 np 不为空则条件成立
{
// 在 if 中, np 与 p 共享对象
}
动态数组
C++语言和标准库提供了两种一次分配一个对象数组的方法。
- C++ 语言定义了另一种
new
表达式语法,可以分配并初始化一个对象数组 - 标准库提供了
allocator
类,允许分配和初始化分离,性能更好且更灵活
new
和数组
new
分配数组int *pia = new int[5]; // pia 指向第一个 int typedef int arrT[42]; // arrT 表示 42 个 int 的数组类型 int *p = new arrT; // 分配一个 42 个int 的数组; p 指向第一个 int
虽然通常称
new T[]
分配的内存为动态数组,但并未得到数组类型对象,而是得到一个数组元素类型的指针,即动态数组并不是数组类型,不能使用某些函数比如begin
、end
或范围for
语句默认,
new
分配的对象,都执行默认初始化。也可以执行值初始化,只要在后面加()
即可int *pia = new int[10]; // 10 个未初始化的 int int *pia2 = new int[10](); // 10 个值初始化为 0 的 int int *pia3 = new int[10]{0, 1, 2, 3, 4, ,5}; // 列表初始化,剩余元素执行值初始化
不能在括号中给出初始化器,即不能用
auto
分配数组释放动态数组时要在指针前加一个空方括号,即使使用类型别名定义数组,元素逆序销毁
typedef int arrT[42]; // arrT 表示 42 个 int 的数组类型 int *p = new arrT; // 分配一个 42 个int 的数组; p 指向第一个 int delete [] p; // 方括号必需,因为分配的是一个数组
标准库提供了可管理
new
分配的数组的unique_ptr
版本unique_ptr<int[]> up(new int[10]); // up 指向一个包含10个未初始化 int 的数组 up.release(); // 自动用 delete[] 销毁其指针
若希望使用
shared_ptr
管理动态数组,必须提供自定义的删除器shared_ptr<int> sp(new int[10], [](int *p){delete[] p;}); // 提供自定义删除器 sp.reset(); // 使用自定义的 lambda 释放数组,它使用 delete[]
allocator
类
allocator
类将内存分配和对象构造分离,其分配的内存是原始的、未构造的。
allocator<string> alloc; // 可以分配 string 的 allocator 对象
auto const p = alloc.allocate(n); // 分配 n 个未初始化的 string
auto q = p; // q 指向最后构造的元素之后的位置
alloc.construct(q++); // *q 为空字符串
alloc.construct(q++, 10, 'c'); // *q 为 cccccccccc
alloc.construct(q++, "hi"); // *q 为 hi
while(q != p)
alloc.destroy(--q); // 释放我们真正构造的 string
alloc.deallocate(p, n); // 释放内存
为了使用
allocator
返回的内存,必须用construct
构造对象,使用位构造的内存,行为未定义使用完,必须对每个构造的元素调用
destroy
来销毁,destroy
接受一个指针,对指向的对象执行析构函数销毁后,可重新使用这部分内存保存其他
string
, 也可以释放内存还给系统拷贝和填充未初始化内存的算法
vector<int> vi{1, 2, 3}; allocator<int> alloc; auto p = alloc.allocate(vi.size() * 2); // 分配比 vi 中元素所占空间大一倍的动态内存 auto q = alloc.unintialized_copy(vi.begin(), vi.end(), p); //拷贝vi中元素构造从p开始的元素 uninitialized_fill_n(q, vi.size(), 42); // 将剩余元素初始化为42
结语
在C++中,内存通过new 表达式分配,通过
delete表达式释放。标准库还定义了一个``allocator
类来分配动态内存块
现在C++程序应尽可能使用智能指针,因为直接管理很容易出错