C++动态内存与智能指针
为了更容易同时也更安全地使用动态内存,新的标准库提供了两种智能指针(smart pointer)类型来管理对象。智能指针的行为类似于常规指针,重要的区别是它负责自动释放内存。
新标准库提供的这两种智能指针的区别在于管理底层指针的方式:shared_ptr允许多个指针指向同一个对象;unique_ptr则“独占”所指向的对象。标准库还定义了一个名为weak_ptr的伴随类,它是一种弱引用,指向shared_ptr所管理的对象。这三种类型都定义在memory文件中。
shared_ptr和unique_ptr都支持的操作 | |
shared_ptr<T> sp unique_ptr<T> up | 空指针类型,可以指向类型为T的对象 |
p | 将p用作一个条件判断,若p指向一个对象,则为true |
*p | 解引用p,获得它所指向的对象 |
p->mem | 等价于(*p).mem |
p.get() | 返回p中保存的指针,要小心使用,若智能指针释放了其对象,返回的指针所指向的对象也就消失了。 |
swap(p,q) p.swap(q) | 交换p和q中的指针 |
shared_ptr类
shared_ptr独有的操作 | |
make_shared<T>(args) | 返回一个shared_ptr,指向一个动态分配的类型为T的对象,使用args初始化此对象。 |
shared_ptr<T> p(q) | p是shared_ptr q的拷贝;此操作递增q中引用计数;q中的指针必须能转换为T*。 |
p = q | p和q都是shared_ptr,所保存的指针必须能相互转换。此操作会递减p的引用计数,递增q的引用计数;若p的引用计数变为0,则将其管理的原内存释放。 |
我们可以认为每个shared_ptr都有一个关联的计数器,通常称其为引用计数(reference count),无论何时我们拷贝一个shared_ptr,计数器都会递增。例如,当用一个shared_ptr初始化另一个shared_ptr,或者将它作为参数传递给一个函数,以及作为函数的返回值时,它所关联的计数器就会递增。
Note:如果将shared_ptr存放于容器中,而后不再需要全部元素,而只使用其中一部分,要记得使用erase删除不再需要的那些元素。
程序使用动态内存出于以下三个原因之一:
(1).程序不知道自己要使用多少对象;
(2).程序不知道所需对象的实际类型;
(3).程序需要在多个对象间共享数据。
直接管理内存
C++语言定义了两个运算符来分配和释放动态内存。运算符new分配内存,delete释放new分配的内存。
Best Practices:使用智能指针来管理内存,否则不要分配动态内存。
1. 使用new动态分配和初始化对象
在自由空间分配的内存是无名的,因此new无法为其分配的对象命名,而是返回一个指向该对象的指针:
int *pi = newint; //pi指向一个动态分配的、未初始化的无名对象
默认初始化:默认情况下,动态分配的对象是默认初始化的,这意味着内置类型或组合类型的对象的值将是未定义的,而类类型对象将用默认构造函数进行初始化。
直接初始化:我们可以使用传统的构造方式(使用圆括号),在新标准下,也可以使用列表初始化(使用花括号):
int *pi = newint(1024);
string *ps = newstring(10, ‘0’);
类似于其他任何const对象,一个动态分配的const对象必须进行初始化。由于分配的对象是const的,new返回的指针是一个指向const的指针。
const int *pci =new const int(1024);
const string *pcs= new const string;
内存耗尽:默认情况下,如果new不能分配所要求的内存空间,它会抛出一个类型为bad_alloc的异常。我们可以改变使用new的方式来阻止它抛出异常:
int *pi = new int; //如果分配失败,new会抛出std::alloc异常
int *p2 = new (nothrow) int; //如果分配失败,new返回一个空指针
bad_alloc和nothrow都定义在头文件new中。
2. 释放动态内存
释放一块并非new分配的内存,或者将相同的指针值释放多次,其行为是未定义的。
Warning:由内置指针(而不是智能指针)管理的动态内存在被显式释放前一直都会存在。
使用new和delete管理内存存在三个常见问题:
(1).忘记delete内存;
(2).使用已经释放的对象;
(3).同一内存释放两次。
定义和改变shared_ptr的其他方法 | |
shared_ptr<T> p(q) | p管理内置指针q所指向的对象;q必须指向new分配的内存,且能够转换为T*类型。 |
shared_ptr<T> p(u) | p从unique_ptr u那里接管了对象的所有权;将u置为空。 |
shared_ptr<T> p(q, d) | p接管内置指针q所指向的对象的所有权。q必须能转换为T*类型。p将使用可调用对象d来代替delete。 |
shared_ptr<T> p(p2, d) | p是shared_ptr p2的拷贝,唯一的区别是p将用可调用对象d来代替delete |
p.reset() p.reset(q) p.reset(q, d) | 若p是唯一指向其对象的shared_ptr,reset会释放此对象。若传递了可选的参数内置指针q,会令p指向q,否则会将p置位空。若还传递了参数d,将会调用d而不是delete来释放q。 |
智能指针与异常
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中被捕获,则内存就永远不会被释放了。在函数F之外没有指向这块内存,因此就无法释放了。
unique_ptr
一个unique_ptr拥有它所指向的对象。与shared_ptr不同,某个时刻只能有一个unique_ptr指向一个给定对象。当unique_ptr被销毁时,它所指向的对象也被销毁。
由于一个unique_ptr拥有它指向的对象,因此unique_ptr不支持普通的拷贝或赋值操作:
unique_ptr<string>p(new string(“maple”));
unique_ptr<string>p2(p); //错误,unique_ptr不支持拷贝
unique_ptr<string>p3;
p3 = p2; //错误,unique_ptr不支持赋值
unique_ptr操作 | |
unique_ptr<T> u1; | 空unique_ptr,可以指向类型为T的对象。u1会使用delete释放它的指针 |
unique_ptr<T, D> u2 | 空unique_ptr,u2会用一个类型为D的可调用对象来释放它的指针。 |
unique_ptr<T, D> u(d) | 空unique_ptr,指向类型为T的对象,用类型为D的对象d代替delete |
u = nullptr | 释放u指向的对象,将u置位空 |
u.release() | u放弃对指针的控制权,返回指针,并将u置为空。但是,如果我们不用另一个智能指针来保存release返回的指针,我们的程序就要负责资源的释放。 |
u.reset() | 释放u指向的对象 |
u.reset(q) | 如果提供了内置指针q,令u指向这个对象;否则将u置为空。 |
u.reset(nullptr) |
|
传递unique_ptr和返回unique_ptr参数
不能拷贝unique_ptr的规则有一个例外:我们可以拷贝或赋值一个将要被销毁的unique_ptr。最常见的例子是从函数返回一个unique_ptr.
unique_ptr<int>clone(int p)
{
//正确:从int*创建一个unique_ptr<int>
return unique_ptr<int>(new int(p));
}
还可以返回一个局部对象的拷贝:
unique_ptr<int>clone(int p)
{
unique_ptr<int> ret(new int(p));
return ret;
}
智能指针陷阱
智能指针可以提供对动态分配的内存安全而有方便的管理,但这建立在正确使用的前提下。为了正确使用智能指针,我们必须坚持一些基本规范:
1. 不使用相同的内置指针值初始化(或reset)多个智能指针。
2. 不delete get()返回的指针。
3. 不使用get()初始化或reset另一个智能指针。
4. 如果你使用get()返回的指针,记住当最后一个对应的智能指针销毁后,你的指针就变得无效了。
5. 如果你使用的智能指针管理的资源不是new分配的内存,记住传递给它一个删除器。
动态数组
为了支持为很多对象分配内存的功能,C++语言和标准库提供了两种一次分配一个对象数组的方法:C++语言定义了另一种new表达式语法,可以分配并初始化一个对象数组。标准库中包含一个名为allocator的类,允许我们将分配和初始化隔离。
Best Practice:大多数应用应该使用标准库容器而不是动态分配的数组。使用容器更为简单,更不容易出现内存管理错误而且可能有更好的性能。
1. new和delete
为了让new分配一个对象数组,我们要在类型名之后跟一对方括号,在其中指明要分配的对象的数目。在下例中,new分配要求数量的对象并(假定分配成功后)返回指向第一个对象的指针:
int *pia = newint[get_size()]; //方括号中的大小必须是整型,但不必是常量
Warning:要记住我们所说的动态数组不是数组类型,这是很重要的。
初始化动态分配的数组:
默认情况下,new分配的对象,不管是单个分配的还是数组中的,都是默认初始化的。可以对数组中的元素进行值初始化,方法是在大小之后跟一对空括号。
int *pia = newint[10]; //10个未初始化的int
int *pia = newint[10](); //10个值初始化为0的int
动态分配一个空数组是合法的:
可以用任意表达式来确定要分配的对象的数目:
size_t n =get_size();
int *p = newint(n);
for(int *q = p; q!= p+n; q++);
虽然我们不能创建一个大小为0的静态数组对象,但当n等于0时,调用new[n]是合法的:
char arr[0]; //错误,不能定义长度等于0的数组
char *cp = newchar[0]; //正确,但cp不能解引用
释放动态数组:
为了释放动态数组,我们使用一种特殊形式的delete——在指针前加上一个方括号对:
delete p; //p必须指向一个动态分配的对象或为空
delete [] p; //p必须指向一个动态分配的数组或为空,而且数组中的元素按逆序销毁,即,最后一个元素首先被销毁,然后是倒数第二个,依次类推。
2. allocator类
new将内存分配和对象构造组合在了一起,delete将对象析构和内存释放组合在了一起。此时,有两个弊端:一是我们分配了大块的内存,可能存在浪费;二是每个元素都被赋值了两次,第一次是在默认初始化时,随后是在赋值时。
标准库allocator类定义在头文件memory中,它帮助我们将内存分配和对象构造分离开来。
标准库allocator类及其操作 | |
allocator<T> a | 定义了一个名为a的allocator对象,它可以对类型为T的对象分配内存。 |
a.allocate(n); | 分配一段原始的未构造的内存,保存n个类型为T的对象 |
a.deallocate(p, n); | 释放从T*指针p中地址开始的内存,这块内存保存了n个类型为T的对象;p必须是一个先前有allocate返回的指针。 |
a.construct(p, args); | p必须是一个类型为T*的指针,指向一块原始内存;args被传递到类型为T的构造函数,用来在p指向的内存中构造一个对象。 |
a.destroy(p) | p为T*类型的指针,此算法对p指向的对象执行析构函数。 |
使用allocator类操作的顺序:
(1).alloc.allocate(n);
(2).alloc.construct(p,args);
(3).alloc.destroy(p);
(4).alloc.deallocate(p,n);
Warning:
(1).为了使用allocate返回的内存,我们必须用construct构造对象。使用未构造的内存,其行为是未定义的。
(2).我们只能对真正构造了的元素进行destroy操作。