一、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的拷贝