第十二章-动态内存

  • 动态分配的对象的生存期与它们在哪里创建是无关的,只有当显式地被释放时才会被销毁。

动态内存和智能指针

  • 动态内存管理通过一对运算符完成,new:在动态内存中为对象分配空间并返回一个指向该对象的指针。delete:接受一个动态对象的指针,销毁该对象,并释放与之关联的内存。
  • 忘记释放内存会导致内存泄露;在尚有指针引用内存的情况下就释放了内存,会产生引用非法内存的指针。
  • 为了更安全地使用指针,可以使用智能指针,智能指针能负责自动释放对象。shared_ptr允许多个指针指向同一个对象;unique_ptr独占所指向的对象;还有伴随类weak_ptr,是一种弱引用,指向shared_ptr所管理的对象。
  • 智能指针的使用方法和普通指针类似,只是多了一些操作
p.get()
//返回p中保存的指针
//要小心使用,若智能指针释放了其对象,返回的指针所指向的对象也就消失了

swap(p, q)
p.swap(q)
//交换p和q中的指针

p.use_count() //返回与p共享对象的智能指针数量
p.unique() //若use_count()为1则返回true

shared_ptr类

  • make_shared(init)是最安全的分配和使用动态内存的方法,make_shared函数在动态内存中分配一个对象,并用init初始化它,最后返回指向这个对象的shared_ptr。
shared_ptr<int> p3 = make_shared<int>(42);
shared_ptr<string> p4 = make_shared<string>(10, '9'); //p4指向值为“999999999”的string
shared_ptr<int> p5 = make_shared<int>(); //指向值初始化的int,即为0
auto p6 = make_shared<vector<string>>(); //指向一个动态分配的空vector<string>
  • 每个shared_ptr都有个关联的计数器,称为引用计数。进行拷贝或赋值操作时,每个shared_ptr都会记录有多少个其他shared_ptr指向相同的对象
  • 当用一个shared_ptr初始化另一个shared_ptr,或将它作为参数传递给一个函数以及作为函数的返回值时,它的计数器就会递增。给shared_ptr赋予一个新值或shared_ptr被销毁时,计数器会递减,递减到0就会自动释放所管理的对象。
auto r = make_shared<int>(42);
r = q;
//给r赋值,令它指向另一个地址
//递增q指向的对象的引用计数
//递减r原来指向的对象的引用计数
//因为r原来指向的“42”已经没别的引用者,所以被自动释放
  • 在最后一个shared_ptr销毁前内存都不会释放,所以忘记销毁不再需要的shared_ptr会导致浪费内存。比如将shared_ptr存入容器后操作,最后忘了erase,就会浪费内存。
  • 程序使用动态内存有3种原因:1.程序不知道自己需要使用多少个对象 2.程序不知道所需对象的准确类型 3.程序需要在多个对象间共享数据。容器类就是出于第一个原因来使用动态内存的,而常见原因是允许多个对象共享相同的状态

直接管理内存

  • 自由空间分配的内存是无名的,所以new无法为其分配的对象命名,而是返回指向该对象的指针:
int *pi = new int; //pi指向动态分配且没有初始化的无名对象
string *ps = new string; //ps指向初始化为空的string
//动态分配内置类型或组合类型的值是未定义的
//但类类型对象将由默认构造函数初始化
  • 可以通过在类型名之后加上括号进行值初始化
int *pi = new int(1024);
int *pi1 = new int; //默认初始化,值未定义
int *pi2 = new int(); //值初始化,值为0
//新标准可以用列表初始化
vector<int> *pv = new vector<int>{1, 2, 3};
  • 可以用auto从初始化器推断想要分配的对象的类型
auto p1 = new auto(obj);
//p指向一个与obj类型相同的对象,且对象用obj初始化
auto p2 = new auto{a, b, c};
//错误,括号中只能有一个初始化器

动态分配的const对象

  • 一个动态分配的const对象必须进行初始化。如果是一个定义了默认构造函数的类类型,其const对象可以隐式初始化
const int *pci = new const int(1024);
const string *pcs = new const string;
  • 由于分配的对象是const的,new返回的指针式一个指向const 的指针

内存耗尽

  • 如果内存用光,new不能分配要求的内存空间,它会抛出类型为bad_alloc的异常
  • 写成下面这种形式能阻止new抛出异常,并且如果不能分配所需内存,它会返回一个空指针
int *p = new (nothrow) int;
//这种给new传递参数的形式称为定位new
//nothrow定义在头文件new里

释放动态内存

  • 传递给delete的指针必须指向动态分配的内存,或是空指针
int i, *pi1 = &i;
int *pi2 = nullptr;
int *pi3 = new int(41), *pi4 = pi3;
delete i; //错误
delete pi1; //错误
delete pi2; //正确
delete pi3; //正确
delete pi4; //错误,释放了两次
  • 指向const动态对象的指针也能被delete
  • 返回动态内存的指针的函数要注意记得释放内存
Foo* factory(T arg)
{
	return new Foo(arg);
}
void use_factory(T arg)
{
	Foo *p = factory(arg);
	//如果没有delete p;
}//p离开作用域,删除这个指针,但是它指向的动态内存没有被delete
  • 由内置指针管理的动态内存在被显式释放前将一直都存在
  • delete一个指针后指针已经无效,但是会变成“空悬指针”。就是指向一块曾经保存数据对象但现在已经无效的内存的指针。避免空悬指针的方法是在指针即将离开其作用域之前再释放掉它关联的内存
  • 如果需要保留指针,可以在delete后将nullptr赋予指针,表明指针不指向任何对象
int *p(new int(42));
delete p;
p = nullptr;

shared_ptr和new结合使用

  • 接受指针参数的智能指针构造函数是explicit的,所以不能讲一个内置指针隐式转换为一个智能指针,必须使用直接初始化形式来初始化一个智能指针
shared_ptr<int> p1(new int(1024));
//正确,使用了直接初始化
shared_ptr<T> p(q)
//这里q是内置指针,q必须指向new分配的内存,且能够转换为T*类型

shared_ptr<int> p2 = new int(1024);
//错误!必须直接初始化
//这里右边new返回int*,赋给p1将隐式的转换为shared_ptr
//但是不能进行内置指针到智能指针间的隐式转换
//下面同理
shared_ptr<int> clone(int p) { return new int(p); }

不要混合使用普通指针和智能指针

  • shared_ptr可以协调对象的析构,但是仅限于自身的拷贝。所以最好使用make_shared而不是new,这样就能在分配对象的同时将shared_ptr与之绑定
  • 正确操作传递shared_ptr的函数
void process(shared_ptr<int> ptr)
{
	//...
}
  • 上面的函数传参过程中会将实参拷贝到ptr中,因为拷贝所以会递增其引用计数,也就是说在precess运行过程中引用计数至少为2,这样当process结束时局部变量ptr被销毁,引用计数减1,ptr指向的内存不会被释放。使用这个函数的正确方法是传递给它一个shared_ptr。
shared_ptr<int> p(new int(42));
process(p);
int *i = p;
  • 不能给process传递一个内置指针,但是能传递一个临时的shared_ptr,这样就很可能导致错误
int *x(new int(1024));
process(shared_ptr<int>(x));
int *i = x; //错误!
//临时shared_ptr在函数中的引用计数是1
//函数结束时引用计数递减为0,自动销毁所指内存
//所以x变成了一个空悬指针

不要使用get初始化另一个智能指针,或为智能指针赋值

  • 智能指针类型有个get函数,返回一个指向智能指针管理对象的内置指针,目的是向不需要使用智能指针的代码传递一个内置指针。但是get返回的指针的代码不能delete此指针!
shared_ptr<int> p(new int(1024));
int *q = p.get();
{ //某程序块
	shared_ptr<int>(q);
	//用q指向的动态内存初始化
	//程序块结束后自动释放
	//导致p变成空悬指针
	//还有销毁p时,会二次delete
}
int foo = *p //错误!
  • 只有在确定代码不会delete指针的情况下才能使用get。并且永远不要用get初始化另一个智能指针或者为另一个智能指针赋值

其他shared_ptr操作

  • reset可以将一个新的指针赋予一个shared_ptr,并且更新计数。p.reset(new int(1024));
  • reset一般配合unique使用
if (!p.unique())
	//如果p不是当前对象仅有的用户
	//就制作一份新的拷贝,在拷贝上操作,不影响本体
	p.reset(new string(*p));
*p += newVal;
  • 使用智能指针,即使程序块因为某些原因提前结束,智能指针类也能确保在内存不再需要时释放
void f()
{
	shared_ptr<int> sp(new int(42));
	//在这里发生异常
}//函数结束时shared_ptr还是能正确释放
//但是直接自己管理的内存就不行
void f()
{
	int *ip = new int(42);
	//发生异常
	delete ip;
}//这里异常要是未被捕获,将永远不会释放ip

shared_ptr注意事项

  1. 不使用相同的内置指针初始化(或reset)多个智能指针
  2. 不delete get()返回的指针
  3. 不使用get()初始化或reset另一个智能指针
  4. 如果使用get()返回的指针,记住当最后一个对应的智能指针销毁后,get()返回的指针就变为无效了

unique_ptr

  • unique_ptr没有类似make_shared的函数,如果要定义一个unique_ptr,需要将其绑定到一个new返回的指针上。初始化unique_ptr也必须使用直接初始化
unique_ptr<int> p2(new int(42));
  • 因为unique_ptr只能有一个对象,所以unique_ptr不支持普通的拷贝和赋值操作
unique_ptr<string> p1(new string("***"));
unique_ptr<string> p2(p1); //错误!不能拷贝
unique_ptr<string> p3;
p3 = p1; //错误!不能赋值
  • 虽然不能拷贝或赋值,但是可以通过release或reset将指针的所有权从一个(非const的)unique_ptr转移给另一个unique_ptr
u.release() //u放弃对指针的控制权,返回指针,并将u置空
u.reset() //释放u指向的对象
u.reset(q) //如果提供了内置指针q,令u指向这个对象,否则将u置空

unique_ptr<string> p1(new string("***"));
unique_ptr<string> p2(p1.release());
//p1将指针所有权转移给p2,并将p1置空
unique_ptr<string> p3(new string("###"));
p2.reset(p3.release());
//将所有权从p3转给p2
//然后释放了p2原来指向的内存
  • 调用release会切断unique_ptr和原来管理的对象间的联系。release返回的指针通常用来初始化另一个智能指针或给另一个智能指针赋值。
p2.release(); //错误,p2不会释放内存并且丢失了指针
auto p = p2.release(); //正确,但要记得delete(p)

weak_ptr

  • weak_ptr是不控制所指向对象生存期的智能指针,它指向一个shared_ptr管理的对象
  • 将weak_ptr绑定到shared_ptr不会改变shared_ptr的引用计数。且最后一个指向对象的shared_ptr被销毁,即使weak_ptr指向着对象,对象还是会被释放。所以它是一种弱共享
  • 创建一个weak_ptr要用shared_ptr来初始化它
auto p = make_shared<int>(42);
weak_ptr<int> wp(p);
  • 因为对象可能不存在,所以不能用weak_ptr直接访问对象,而要用lock。lock函数检查weak_ptr指向的对象是否存在,若存在就返回指向共享对象的shared_ptr
if (shared_ptr<int> np = wp.lock())//np为空就不执行
	//...
  • weak_ptr的作用就是阻止用户访问一个不再存在的对象

智能指针和哑类

  • 不是所有类都能良好的定义析构函数,如果没有定义析构函数释放资源,可能会遇到和动态内存相同的错误。可以使用类似于管理动态内存的方法来管理没有定义良好析构函数的类
//假设以下为一个网络库的代码
struct destination; //表示在连接什么
struct connection; //使用连接所需要的信息
connection connect(destination*); //打开连接
void disconnect(connection); //关闭连接
void f(destination &d)
{
	//获得一个连接,使用完后关闭
	connection c = connect(&d);
	//使用连接
	//如果在f退出前忘记调用disconnect,就不能关闭c了
}
  • 可以使用shared_ptr来保证connection被正确关闭
//为了使用shared_ptr来管理一个connection
//必须定义一个函数来代替delete
//这个删除器函数必须能够对shared_ptr中保存的指针进行释放
//本题中删除器接受单个类型为connection*的参数
void end_connection(connection *p) { disconnect(*p); }
//之后创建shared_ptr时,可以传递一个指向删除器函数的参数

```cpp
void f(destination &d)
{
	connection c = connect(&d);
	shared_ptr<connection> p(&c, end_connection);
	//使用连接
	//f退出时(即使异常退出),connection也会被正确关闭
}
//这里p被销毁时不会对保存的指针执行delete
//而是调用end_connection
  • unique_ptr管理删除器的方式和shared_ptr不同
//p指向一个类型为objT的对象
//并使用一个类型为delT的对象释放objT对象
//它会调用一个名为fcn的delT的类型对象
unique<objT, delT> p (new objT, fcn);
//示例
void f(destination &d)
{
	connection c = connect(&d);
	unique_ptr<connection, decltype(end_connection)*> p(&c, end_connection);
	//使用连接
	//f退出时(即使异常退出),connection也会被正确关闭
}
//decltype(end_connection)*表示我们正在使用该函数类型的指针

动态数组

  • new分配要求数量的对象,并返回指向第一个对象的指针
int *pia = new int[10];
  • 动态数组实际上并不是一个数组。就像上面,用new分配一个数组时,其实得到的是一个int指针(个人理解其实就是给了10个int的动态内存,然后动态内存没有“数组”的概念)
  • 因为不是数组,所以不支持数组的一些用法,比如范围for语句、用begin和end等
  • 在大小后面跟一对括号可以实现值初始化
int *pia = new int[10]; //默认初始化
int *pia2 = new int[10](); //值初始化为0
  • 动态分配空数组是合法的。对长度为0的数组来说,此指针就像是尾后指针,它不能解引用
char arr[0];
char *cp = new char[0]; //合法
  • 使用delete []释放内存,数组中的元素按逆序销毁
delete [] pa;
  • 能配合使用智能指针unique_ptr和动态数组,但是当一个unique_ptr指向数组时,不能使用点和箭头成员运算符(因为指向的是整个数组而不是单个对象)。但是可以使用下标运算符
unique_ptr<int[]> up(new int[10]);
for (size_t i = 0; i != 10; i++)
	up[i] = i;
up.release(); //自动使用delete []

allocator类

  • 我们分配单个对象时,通常希望将内存分配和对象初始化组合在一起(我们知道对象应有什么值),但分配一大块内存时,我们往往计划在这块内存上按需构造对象→将内存分配和对象构造分离,即我们可以分配大块内存,但只在需要时才真正执行对象创建操作。allocator实现了这个操作
allocator<string> alloc;
auto const p = alloc.allocate(n); //分配n个未初始化的string
auto q = p; //p和q都指向最后的构造元素之后的位置
alloc.construct(q++); //*q为空
alloc.construct(q++, 10, 'c'); //*q为10个c
alloc.construct(q++, "hi"); //*q为hi
//q用来指向最后构造的元素之后的位置
  • 为了使用allocate返回的内存,必须用construct构造对象。使用未构造的内存,行为是未定义的
  • 用完对象后必须对每个构造的元素使用destroy来销毁(只能对构造了的元素进行destroy)
while (q != p)
	alloc.destroy(--q);
  • 销毁后可以用来保存其他string,也可以归还系统
//释放内存
alloc.deallocate(p, n);
//n必须和调用allocate分配内存时提供的大小参数n一样大
  • allocator的两个伴随算法
函数作用
uninitialized_copy(b, e, b2)从迭代器b和e指定的输入范围中拷贝元素到迭代器b2指定的未构造的原始内存中。b2指定的内存必须足够大,能容纳输入序列中元素的拷贝
uninitialized_copy_n(b, n, b2)从迭代器b指向的元素开始,拷贝n个元素到b2开始的内存中
uninitialized_fill(b, e, t)在迭代器b和e指定的原始内存范围中创建对象,对象的值均为t的拷贝
uninitialized_fill_n(b, n, t从迭代器b指向的内存地址开始创建n个对象,b必须指向足够大的未构造的原始内存,能够容纳给定数量的对象
//假设有个vector<int> vi
auto p = alloc.allocate(vi.size(), 2);
//分配比vi空间大一倍的动态内存
auto q = uninitialized_copy(vi.begin(), vi.end(), p);
//把vi中的元素拷贝到前半部分,返回的指针指向最后一个元素的后一个位置
uninitialized_fill_n(q, vi.size(), 42);
//从q指向的地址再创建vi大小那么多个对象,初始化为42
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值