内存管理
C++使用new和delete两个运算符进行内存管理。
使用new进行动态分配和初始化对象
在自由空间内分配的内存是无名的,因此new无法为其分配的对象进行命名,而是返回一个指向该对象的指针:
int *pi=new int;
上述表达式在自由空间(栈空间?堆空间?)内构造一个 int对象,并且返回一个指向这个对象的指针。
默认情况下,动态分配的对象是默认初始化的,这就意味着内置类型(int)或者是组合类型的对象的值是未定义的,而类类型的对象将会使用默认构造函数进行初始化:
string *ps=new string;
int *pi=new int;
旧标准下,可以使用直接初始化的方式进行初始化一个动态分配对象。也可以使用传统的构造方式(使用圆括号)
int *pi=new int(1024); //使用直接初始化的方式初始化一个动态分配对象
string *ps=new string(10,'9'); //使用传统的构造方式进行初始化的过程
vector<int> *pv=new vector<int>{0,1,2,3,4,5,6}; //使用列表初始化的形式
也可以对动态分配的对象进行值初始化,只需要在类型名之后添加括号即可
string *ps1=new string; //默认初始化为空 string
string *ps=new string(); //值初始化为空string
int *pi1 = new int; //默认初始化,*pi1的值未定义
int *pi2 = new int(); //值初始化为0. *pi2 为 0 这种方式只能用于初始化指针,不能用于初始化int对象
值初始化内置类型对象具有良好定义的值,默认初始化对象的值则是未定义的。对于类中那些依赖编译器射程的默认构造函数的内置类型成员,没有进行类内初始化的时候,他们的值也是未定义的。
如果具有括号包围的初始化器,可以使用auto来推断我们想要分配的对象的类型。(需要保证使用单一初始化器)
auto p1=new auto(obj); //p指向一个和obj类型相同的对象,该对象使用obj进行初始化。
auto p2=new auto{a,b,c};//括号中只能有单个初始化器
动态分配的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的使用方式阻止其抛出异常:
int *pi=new int();//如果分配失败,new将会抛出std::bad_alloc
int *p2=new (nothrow) int();//如果分配失败,new返回一个空指针。
我们将这种形式的new称为定位new。定位new 表达式允许我们想new传递额外的参数。在上述例子中,我们传递给它一个标准库定义的名为nothrow的对象。
释放动态内存
为了防止内存耗尽,在动态内存使用完毕之后,必须要将其归还给系统,我们通过使用delete表达式,将动态内存归还给系统。delete表达式接收一个指针,指向我们想要释放的对象。
delete p;//p需要指向一个动态分配对象或者是一个空指针
delete表达式执行两个操作:销毁给定指针指向的对象;释放对应的内存。
指针值和delete
传递给delete的指针必须指向动态分配的内存,或者是一个空指针。释放一块并非new分配的内存,或者是将相同的指针释放多次,其行为是未定义的:
int i,*pi1 = &i,*pi2 = nullptr;
double *pd = new double(33), *pd2 = pd;
delete i;//错误:i并非一个指针
delete pi1;//未定义:pi1指向一个局部变量
delete pd;//正确
delete pd2;//未定义: pd2指向的内存已经被释放了
delete pi2;//正确: 释放一个空指针总是没有问题的
对于delete pi1和pd2所产生的错误是具有潜在危害的:通常情况下,编译器不能分辨一个指针指向静态还是动态分配的对象。
虽然一个const对象的值不能被改变,但是是可以被销毁的。
动态对象的生存期直到被释放为止
调用者使用返回指向动态内存的指针的函数的时候,需要进行手动的内存释放。
//factory 返回一个指针,指向一个动态分配的对象
Foo* factory(T arg){
return new Foo(arg);//调用者负责释放这个内存
}
调用者需要避免使用了但是未释放的情况,如下所示:
void use_factory(T arg){
Foo *p=factory(arg);//使用了p 但是没有进行delete
}//p离开了它的作用域,但是它所指向的内存并没有被释放。
内置类型的对象被销毁的时候什么也不会发生,特别是,当一个指针离开其作用域的时候,它所指向的对象什么也不会发生。如果这个指针指向的是动态内存,内存将不会被自动释放。
使用new和delete进行动态内存管理时常见的错误
- 没有使用delete进行内存释放,导致发生内存泄漏
- 使用已经释放掉的对象,通过释放内存之后将指针置为空
- 同一块内存释放两次。当有两个指针指向相同的动态分配对象时候,有可能会对其中一个指针进行了delete操作,随即又对第二个指针进行了delete操作,导致自由空间被破坏。
坚持只使用只能指针,可以避免这些所有的问题,对于一个内存,只有在没有任何智能指针指向他的时候,才会自动进行释放。
delete之后重置指针值
当我们delete一个指针之后,指针值就变成了无效。虽然指针已经无效,但是指针仍然保存(已经释放了的)动态内存的地址。意味着这时候指针变成了空悬指针(指向一块曾经保存数据对象但是已经无效的内存的指针)。
未初始化指针的所有缺点,空悬指针也有。为了解决这个问题,可以在delete之后,将nullptr赋予指针,清楚的指向指针不指向任何对象。
动态指针的问题在于可能有多个指针指向了同一个内存。delete内存之后重置指针的方式只对当前指针有效,对于仍然指向(已经释放掉)内存的指针是没有任何作用的。
同时在实际系统中,查找指向相同内存的所有指针是异常困难的。
智能指针
c++标准库提供了两种智能指针来管理内存对象。智能指针的功能类似于常规指针,但是只能指针可以自动释放所指向的对象。
shared_ptr允许多个指针指向同一个对象;unique_ptr则是独占所指向的对象。
shared_ptr
只能指针是一种模板,当创建了一个智能指针的时候,必须提供额外的信息-指针所指向的类型。
shared_ptr<string> p1; //shared_ptr 可以指向string
shared_ptr<list<int>> p2; //shared_ptr 可以指向int的list
默认初始化的智能指针中保存一个空指针。智能指针的使用方式和普通指针是类似的,通过解引用一个智能指针返回它所指向的对象。也可以用作判断条件表明当前智能指针是否为空。
//如果p1不为空,检查它是否指向一个空string
if(p1 && p1->empty())
*p1 = "hi"; //如果p1指向一个空string ,解引用p1,将一个新值赋予string
shared_ptr和uniqu_ptr都具有的操作如下:
shared_ptr<T> sp | 空智能指针,指向类型为T的对象 |
---|---|
unqiue_ptr<T> up | 空智能指针,指向类型为T的对象 |
p | 将p作为一个条件判断,如果p指向一个对象,则为true |
*p | 解引用p,获取其指向的对象 |
p->mem | 等价于(*p).mem |
p.get() | 返回p中保存的指针 |
swap(p,q) | 交换p,q保存的指针 |
p.swap(q) | 交换p,q保存的指针 |
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,将其管理的内存进行释放 |
p.unique() | 如果p.use_count()为1,返回true,否则返回false; |
p.use_count() | 返回和p共享对象的智能指针的数量;主要用于调试 |
make_shared函数
最安全的分配和使用动态内存的方法是调用一个名为make_shard的标准库函数。这个函数在动态内存中分配一个对象并进行初始化。返回指向这个对象的shared_ptr。
使用make_shared函数的时候,必须制定想要创建的对象的类型。定义的方式和模板类相同。
//指向一个值为42的int的shared_ptr
shared_ptr<int> p3= make_shared<int>(42)
//p4指向一个值为“999999999”的string
shared_ptr<string> p4 = make_shared<string>(10,'9');
//p5指向一个值初始化的int,即值为0
shared_ptr<int> p5 =make_shared<int>();
shared_ptr的拷贝和赋值
当进行拷贝和赋值操作的时候,每个shared_ptr都会记录有多少个其他shared_ptr和其指向的是相同的对象:
我们可以认为每个shared_ptr都是具有一个关联计数器的,称其为引用计数。无论何时我们拷贝一个shared_ptr,引用计数的值都会递增。例如:
- 使用一个shared_ptr初始化另一个shared_ptr;
- 将shared_ptr作为一个参数传递给一个函数;
- 作为函数的返回值;
当我们给一个shared_ptr赋予一个新值或者是shared_ptr被销毁 (一个局部的shared_ptr)离开其作用域的时候,计数器就会进行递减。
一旦一个shared_ptr的计数器变为0,就会自动释放所管理的对象。
shared_ptr自动销毁所管理的对象
当指向一个对象的最后一个shared_ptr被销毁的时候,shared_ptr将会自动销毁这个对象。这是通过特殊的成员函数析构函数完成销毁的动作。
shared_ptr自动释放相关联的内存
当动态对象不再被使用的时候,shared_ptr类将会自动释放动态对象,这一特性使得动态内存的使用变得较为简单。
//factory返回一个shared_ptr,指向一个动态分配的对象
shared_ptr<Foo> factory(T arg){
return make_shared<Foo>(arg);
}
由于factory返回一个shared_ptr,所以我们确保它分配的对象在恰当的时候被释放。
由于在最后一个shared_ptr销毁前内存都不会释放,保证shared_ptr在无用之后不再保留是非常重要的。
shared_ptr在无用之后的一保留的一种可能的情况是,将shared_ptr存放在了一个容器之中,随后排了容器,从而不再需要某些元素,在这种情况下应该确保使用erase删除不再需要的shared_ptr元素
不要使用get初始化另一个智能指针或者是为智能指针赋值
智能指针类型定义了一个名为get的函数,返回一个内置指针,指向智能指针管理的对象。这种情况是为了这样的一种情况设计:我们需要向不能使用智能指针的代码传递一个内置指针。使用get返回指针的代码不能delete这个指针。
使用了动态生存期资源的类
程序使用动态内存主要出于以下的原因:
- 程序不知道自己需要使用多少对象
- 程序不知道所需对象的准确类型
- 程序需要在多个对象之间共享数据。
智能指针和异常
如果使用智能指针,即使程序块过早结束,智能指针类也能确保内存在不再需要的时候将其释放:
void f(){
shared_ptr<int> sp(new int(42));//分配一个新对象
//这段代码抛出一个异常,且在f中没有被捕获
}//在函数结束的时候,shared_ptr将会自动释放内存
函数对象的退出有两种可能,正常处理结束或者是发生了异常,无论哪种情况,局部对像都会被销毁,而在局部对象销毁的过程中将会检查引用计数,就会保证了智能指针在异常情况下,可以释放掉内存。