动态内存

一、C++中的三种内存

1、静态内存

静态内存用来保存局部static对象、类static数据成员以及定义在任意函数之外的变量。这些对象由编译器自动创建和销毁。

注意:静态内存中保存的对象会被赋予默认的初值。

2、栈内存

栈内存用来保存定义在函数体内的非static对象。这些对象由编译器自动创建和销毁。

注意:栈内存中保存的对象不会被赋予默认的初值,其值是未定义的。

3、堆内存(自由空间)

在C++中,对于不显式分配内存的对象默认使用静态内存或栈内存。

程序用堆来存储动态分配的对象——即那些在程序运行时分配的对象。这些对象的创建和销毁由程序来控制。

注意:使用malloc分配的堆内存是未初始化的,而用calloc或new分配的堆内存会被初始化为0。

二、智能指针——间接管理内存(安全)

memory头文件中定义了两种智能指针类:shared_ptr和unique_ptr。它们的行为与普通指针类似,所不同的是它们负责自动释放所指向的对象。

shared_ptr允许多个指针指向同一个对象,unique_ptr则“独占“所指向的对象。

1、shared_ptr类

a)、智能指针也是模板,在创建时必须提供额外的信息——指针可以指向的类型。

shared_ptr p1;//定义一个shared_ptr类p1,它可以指向string类型的对象

b)、安全的分配和使用动态内存:使用make_shared函数,在动态内存中分配一个对象并初始化它,返回此对象的shared_ptr

shared_ptr p2 = make_shared(42);//使用make_shared必须指定想要创建的对象类型,其通过传递的参数来构造给定类型的对象

c)、shared_ptr进行拷贝或赋值操作时,每个shared_ptr都会记录有多少个其他shared_ptr指向相同的对象。

每个shared_ptr都有一个关联的计数器,称为引用计数。

无论何时我们拷贝一个shared_ptr,计数器都会递增。同样,当shared_ptr被赋予新值或者被销毁,计数器就会递减。

一旦一个shared_ptr的计数器变为0,它就会自动释放自己所管理的对象。

shared_ptr的析构函数会递减它所指向对象的引用计数。如果引用计数已经为0,则shared_ptr的析构函数就会销毁对象,并释放它的内存。

例子:

//shared_ptr返回一个shared_ptr,指向一个动态分配的对象
shared_ptr factory(T arg)
{
//适当地处理arg
//shared_ptr负责释放内存
return make_shared (arg);
}
void use_factory(T arg)
{
shared_ptr p = factory(arg);
//使用p
}//p离开了作用域,它指向的内存会被自动释放掉

但是如果也有其他shared_ptr指向这块内存,它就不会被释放掉:

void use_factory(T arg)
{
shared_ptr p = factory(arg);
//使用p
return p;//当我们返回p时,引用计数进行了递增操作
}//p离开了作用域,但它指向的内存不会被自动释放掉

使用动态内存的一个常见原因是允许多个对象共享相同的状态。这个共享的数据可以放在动态内存中,编译器不会删除,只有程序员在需要的时候可以将其删除。

2、unique_ptr

一个unique_ptr“拥有”它所指向的对象。与shared_ptr不同,每个时刻只能有一个unique_ptr指向一个给定对象。当unique_ptr被销毁时,它所指向的对象也被销毁。

当我们定义unique_ptr时,需要将其绑定到一个new返回的指针上,而不像shared_ptr那样有一个make_shared函数。

unique_ptr p2(new int(42));//p2指向一个值为42的int
注意:由于unique_ptr拥有它所指向的对象,因此unique_ptr不支持普通的拷贝和赋值操作:

unique_ptr p2(p1);//错误!unique_ptr不支持拷贝
unique_ptr p3;
p3 = p1;//错误!unique_ptr不支持赋值

unique_ptr支持的特有操作:

unique_ptr u;
u.release();//u放弃对指针的控制权,返回指针,并将u置为空
u.reset();//释放u指向的对象
u.reset(q);//释放u指向的对象,并令u指向q
u.reset(nullptr);//释放u指向的对象,并令u为空

注意:release不会释放内存,而reser会释放内存。

虽然我能不能直接对unique_ptr进行拷贝和赋值,但可以通过release和reset来实现指针所有权的转移。

unique_ptr p2(p1.release());//release将p1置为空,并将所有权从p1转移给p2
unique_ptr p3(new string("Trex"));
p2.reset(p3.release());//reset释放了p2原来指向的内存,并将所有权从p3转移给p2

3、weak_ptr

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

一旦最后一个指向对象的shared_ptr被销毁,对象就会被释放,即使仍有weak_ptr指向对象。

当我们创建一个weak_ptr时,要用一个shared_ptr来初始化它:

auto  p = make_shared(42);
weak_ptr wp(p);//wp弱共享p;p的引用计数未改变
weak_ptr支持的操作:
weak_ptr w;
w.reset();//将w置为空
w.use_count();//与w共享对象的shared_ptr数量
w.expired();//若w.use_count()为0,返回ture,否则返回false
w.lock();//若w.expired()为ture,返回一个空shared_ptr;否则返回一个指向w的对象的shared_ptr 

三、new和delete——直接管理内存

C++定义了两个运算符用来直接管理内存。元算符new用来分配内存,运算符delete用来释放new分配的内存。

1、使用new动态分配和初始化对象

使用new分配的对象可以进行初始化。默认情况下,这些对象执行的是默认初始化。如:

string *ps = new string;//ps指向一个空的string
int *pi = new int;//pi指向一个未初始化的int

我们可以使用直接初始化的方式来初始化一个动态分配的对象。

a)、用传统的构造方式(使用圆括号)

int *pi = new int(1024);//pi指向的对象的值为1024
string *ps = new string(10, '9');//ps指向的对象为“9999999999” 

b)、用列表初始化(使用花括号)

vector *pv = new vector{0,1,2,3,4,5,6,7,8,9};//pv指向的vector中有10个元素,分别为0~9
我们可以使用new来动态分配const对象。

const int *pci = new const int(1024);
const string *pcs = new const sring;

同往常一下,使用const对象必须初始化。

注意,这里new返回的是一个指向const对象的指针。

2、使用delete释放动态内存

delete表达式接受一个指针,指向我们想要释放的对象:

delete p;//p必须指向一个动态分配的对象或是一个空指针
delete包含两个步骤:销毁指针指向对象、释放对应内存

const对象也可以通过delete销毁:

const int *pci = new const int(1024);
delete pci;//虽然我们不能改变const对象的内容,但它本身是可以被销毁的。

3、动态分配对象的生存周期

对于一个由new分配的、内置指针(而不是智能指针)管理的动态对象,在其被显式释放前都是存在的。

如果我们调用了一个返回指向动态内存指针的函数,那么一定要记得在之后显式释放这个指针所分配的内存,否则会造成内存泄露。如:

Foo* factory(T arg)//定义了一个返回动态内存指针的函数
{
return new Foo(arg);//函数用new分配了内存,但并不释放它
}
void use_factory(T arg)
{
Foo *p = factory(arg);
//使用p但不delete它
}//p离开了它的作用域,但它所指向的内存没有被释放!

4、shared_ptr和new结合使用

除了使用make_shared函数,我们还可以使用new返回的指针来初始化智能指针。

shared_ptr p2(new int(42));//p2指向一个值为42的int
注意:接受指针参数的智能指针构造函数是explicit的。也就是说,我们不能将一个内置指针隐式地转换为智能指针。

shared_ptr p1 = new int(1024);//错误!内置指针不能隐式转换为智能指针,必须使用直接初始化形式
shared_ptr p1(new int(1024));//正确!将指针作为参数传递给智能指针的构造函数,即直接初始化

默认情况下,一个用来初始化智能指针的普通指针必须指向动态内存,因为智能指针默认使用delete释放它所关联的对象。

我们应该尽量不要混合使用普通指针和智能指针,也不要使用get初始化另一个智能指针或为智能指针赋值。

 我们可以使用reset来改变shared_ptr的内容:

p.reset(new int(1024));//p指向一个新的对象,其值为1024

5、智能指针陷阱

使用智能指针的几个基本原则:

a)、不使用相同的内置指针初始化(或reset)多个智能指针。

b)、不delete get返回的指针。

c)、不使用get()初始化或reset另一个智能指针。

d)、如果你使用get()返回的指针,记住当最后一个对应的智能指针销毁后,你的指针就变为无效了。

e)、如果你使用智能指针管理的资源不是new分配的内存,记住传递给它一个删除器。

四、动态数组

C++和标准库提供了两种一次分配一个对象数组的方法:new表达式和allocator类。

其中,new可以分配并初始化一个对象数组;而allocator允许我们将分配和初始化分离。

使用allocator通常会提供更好的性能和更灵活的内存管理能力。

需要注意的是,很多需要动态内存的类能(而且应该)使用vector对象或string对象管理必要的存储空间。使用标准库容器的类能避免分配和释放内存带来的复杂性。

1、使用new分配动态数组

a)、格式

在new表达式的类型名后面加一对[ ],在其中指明要分配的对象数目(必须是整型,但不必是常量):

int *pia = new int[get_size()];//由get_size()确定分配多少个int,返回第一个元素的指针

也可以用一个表示数组类型的类型别名来分配一个数组:

typedef int arrT[42];//arrT表示42个int的数组类型
int *p = new arrT;//分配一个42个int的数组;p指向其中第一个int

注意:分配一个数组实际得到的是一个元素类型的指针。也就是说“动态数组”并不是数组类型!因此,对动态数组不能使用begin和end函数,也不能使用范围for 语句。

b)、初始化

与new分配普通内存对象一样,new分配的数组默认情况下也是默认初始化的。我们可以对数组元素进行值初始化,方法是在大小之后跟一对空括号。

int *pia = new int[10];//默认初始化,10个未定义的int
int *pia2 = new int[10] ();//值初始化,10个int均为0
string *psa = new string[10];//默认初始化,调用string类的默认构造函数,生成10个空string
string *psa2 = new string[10] ();//值初始化,10个string均为空

我们还可以提供一个元素初始化器的花括号列表:

int *pia3 = new int[10] {0,1,2,3,4,5,6,7,8,9};//10个int分别用列表中对应的初始化器初始化
string *psa3 = new string[10] {"a","an","the"};//10个string,前3个用列表中对应的初始化器初始化,剩余的进行值初始化

c)、分配一个空数组是合法的

可以用任意表达式来确定要分配的对象数目,包括值为0的表达式。

char arr[0];//错误!不能定义长度为0的数组
char *cp = new char[0];//正确!但cp不能解引用

对于零长度的数组来说,此指针就像尾后指针一样,我们可以像使用尾后迭代器一样使用这个指针。

d)、释放动态内存

使用一种特殊形式的delete语句来释放动态内存,在指针前加上一个空方括号对:

delete p;//p必须指向一个动态分配的对象或为空
delete [ ] pa;//pa必须指向一个动态分配的数组或为空

注意:使用delete释放动态内存数组时,其中的元素是按逆序销毁的,也就是最先销毁最后一个,然后倒数第二个,以此类推。

e)、使用智能指针管理动态数组

unique_ptr可以直接管理new分配的数组,格式是在<>中的对象类型后面加一对[ ]:

unique_ptr up(new int[10]);//up指向10个未初始化的int的数组
up.release();//自动使用delete[ ]销毁其指针

由于up指向一个数组,当up销毁它管理的指针时,会自动使用delete[ ]。 

注意:shared_ptr不直接支持管理动态数组。

如果希望使用shared_ptr管理一个动态数组,必须提供自己定义的删除器。

shared_ptr sp(new int [10], [ ](int *p) { delete[ ] p; });
sp.reset();//使用我们提供的lambda释放数组,它使用delete[ ]

2、使用allocator类

使用new分配内存的缺陷:

new缺乏灵活性,它将内存分配和对象构造组合在了一起。

在一些项目中,我希望一大块内存先分配,在需要的时候再使用(执行对象的创建操作)。这种情况下,new操作符是不能完成的,它会执行默认的初始化操作。

而且,将内存分配和对象构造组合在一起可能导致不必要的浪费。

allocator类将内存分配和对象构造分离开来,它分配的内存是原始的、未构造的。它通过4个函数实现内存管理和对象管理的分离:

allocator a;
a.allocate(n);//分配内存,可以保存n个T对象,但内存中内容未定义
a.deallocate(p, n);//释放p指向的、能放n个T对象的内存,p必须是由allocate返回的指针
a.construce(p, arge);//构造对象,在p指向的内存位置使用arge初始化表构造T类型的对象
a.destroy(p);//销毁对象,对p(T*类型的指针)指向的对象执行析构函数

注意:为了使用allocate返回的内存,我们必须用construct构造对象。使用未构造的内存,其行为是未定义的。

同样的,在释放内存时,要保证其中的每个对象都已被销毁,即已经调用了destroy函数。

用allocator对象分配内存时,它会根据给定的对象类型来确定恰当的内存大小和对应位置:

allocator alloc;//可以分配string的allocator对象
auto const p = alloc.allocate(n);//分配n个未初始化的string

使用已有容器拷贝、填充未初始化的动态内存:

uninitialized_copy(b,e,b2);//b,e是迭代器,b2是动态内存指针,将b和e之间的元素拷贝到b2开始的动态内存中
uninitialized_copy_n(b,n,b2);//b是迭代器,n是要拷贝的元素数目,将b开始的n个元素拷贝到b2开始的动态内存中
uninitialized_fill(b,e,t);//b,e是迭代器,t是要填充的元素,在b和e指定的原始内存中创建对象,对象的值均为t的拷贝
uninitialized_fill_n(b,n,t);//b是迭代器,n是要填充的元素数目,从b指向的内存地址创建n个对象,对象的值均为t的拷贝


你可能感兴趣的:(c++11)