第二十章 动态内存
因翻译太耗时,现做笔记如下:
12.1 动态内存和智能指针
new:在动态内存中为对象分配空间并返回一个指向该对象的指针
delete:接受一个动态内存对象的指针,销毁该对象,并释放与之关联的内存
使用动态内存容易出错,主要是容易忘记释放对象,和多次释放。
因此,提供了两种智能指针来管理动态内存。
shared_ptr:允许多个指针指向同一个对象
unique_ptr:一次只能一个指针指向对象
weak_ptr:一个弱引用,指向shared_ptr所管理的对象
12.1.1 shared_ptr
shared_ptr<string> p1;//shared_ptr,可以指向string
shared_ptr<list<int>> p2;//shared_ptr,可以指向int的list
默认初始化的智能指针保存着一个空指针。
智能指针的使用方法与普通的指针类似。解引用一个智能指针返回它指向的对象。如果在一个条件判断中使用了智能指针,效果就是检查他是否为空
if(p1 && p1->empty())
*p1 = "hi";
下表列出了shared_ptr和unique_ptr都支持的操作
下表列出了shared_ptr才支持的操作
make_shared函数
该函数在动态内存中分配内存,并用给定的参数初始化它。
//p3指向一个值为42的int
shared_ptr<int> p3 = make_shared<int>(42);
//p4指向一个值为9999999999的string
shared_ptr<string> p4 = make_shared<string>(10,'9');
//p5指向一个值初始化的int,值为0
shared_ptr<int> p5 = make_shared<int>();
注意:make_shared接受的参数,必须和需要构造的类型的某一个构造函数相同。
shared_ptr的拷贝和赋值
当进行拷贝后者赋值操作的时候,每个shared_ptr都会记录有多少个shared_ptr指向相同的对象。
auto p = make_shared<int>(42);
//p和q指向相同的对象,此对象有两个引用者
auto q(p);
我们可以认为,每一个shared_ptr对象都有一个关联的计数器,称之为引用计数。无论何时拷贝一个shared_ptr,计数器都会增加。
当给shared_ptr赋予一个新值或是shared_ptr被销毁时,计数器就会递减。
一旦一个shared_ptr的计数器变为0,他就会自动释放自己所管理的对象。
auto r = make_shared<int>(42);
r=q;
//给r赋值,令他指向另外一个地址
//递增q指向的对象
//递减r原来指向的对象的引用计数
//当r原来指向的对象已没有引用者,会自动释放
shared_ptr自动销毁所管理的对象
当引用计数变为0时,shared_ptr释放所指向的对象,在释放之前,会调用对象的析构函数。
12.1.2 直接管理内存
使用new动态分配和初始化对象
在自由空间分配的内存是无名的,因此new无法为其分配的对象命名,而是返回一个指向该对象的指针。
int *pi = new int;//pi指向一个动态分配的,未初始化的无名对象
默认情况下,动态内存分配的对象是默认初始化的。因此对于内置类型和组合类型的对象其值是未定义的。而类类型的对象,则使用默认构造函数进行初始化
string *ps = new string;//初始化为空
int *pi = new int; //pi指向一个未初始化的int
还可以进行直接初始化和列表初始化如下:
int *pi = new int(1024);//pi指向的对象的值为1024
string *ps = new string(10,'9');//*ps为999999999
vector<int> *pv = new vector<int>{0,1,2,3,4,5,6,7,8,9};
还可以进行值初始化,如下:
string *ps1 = new string;//默认初始化
string *ps = new string() ;//值初始化
int *pi1 = new int;//默认初始化,*pi1的值未定义
int *pi2 = new int();//值初始化,为0
注意上面的代码,对于类类型来说,值初始化和默认初始化,都会调用他们的默认构造函数,最后的结果是相同的。而对于内置类型来说,默认初始化和值初始化就不一样,默认初始化,其值是未定义的。
动态分配const对象
const int *pci = new const int(1024);
const string *pcs = new const string;
注意:如果new失败,会抛出一个bad_alloc的异常。可以改变new的方式来阻止这种情况
int *p1 = new int;//如果分配失败则,抛出std::bad_alloc
int *p2 = new (nothrow) int;//如果分配失败,则new返回一个空指针
上例子中,这种new称之为,定位new。定位new允许我们向其传递额外的参数。
这种new如果分配失败,则返回一个空指针。
释放动态内存
delete p;//p必须指向一个动态内存分配的对象或是一个空指针
使用new和delete管理动态内存存在三个常见的问题:
- 忘记delete内存。
- 使用已经释放的内存
- 同一块内存释放多次
delete之后重置指针
当我们delete一个指针之后,指针就变成无效的了。而此时指针的值还依然保存着
因此,如果不小心使用了这个指针,那么就会使用一个已经释放了的内存,所以
delete指针之后,需要将这个指针重置为空。
12.1.3 shared_ptr和new结合使用
可以使用new返回的指针,来初始化shared_ptr
shared_ptr<double> p1;//shared_ptr可以指向一个double
shared_ptr<int> p2(new int(42));//p2指向一个值为42的int
接受指针参数的智能指针的构造函数是explicit的,因此,注意下面的代码
shared_ptr<int> p1 = new int(1024);//错误:必须使用直接初始化形式
shared_ptr<int> p2(new int(1024));//正确:使用了直接初始胡形式
下面给出定义和改变shared_ptr的其他方法
不要混合使用智能指针和普通指针
考虑下面的例子:
void porcess(shared_ptr<int> ptr){
//使用ptr
}
int *x(new int(1024));//危险:x是一个普通指针,并不是一个智能指针
process(x);//错误:不能将int* 转换为一个shared_ptr<int>
process(shared_ptr<int>(x));//合法的,但是内存会被释放
int j = *x;//未定义的:x是一个无效的指针
在上面例中,构造了一个临时的shared_ptr传递给process函数,一旦函数运行
结束,那么这个临时的shared_ptr将被销毁,它指向的对象将会释放
此时,x指向的对象已经被释放,x是一个无效的指针。
也不要使用get初始化另外一个智能指针或为智能指针赋值
智能指针提供了一个get的成员,它返回智能指针指向的对象的地址。
这个成员函数是为了如下情况进行设计的:需要向不使用智能指针的代码
传递一个内置指针。
现在,思考下面的代码
shared_ptr<int> p(new int(42));
int *q = p.get();//正确,但使用q的时候请注意,不要释放它
{
shared_ptr<int> (q);
}//程序块结束,q指向的对象,被释放
int foo = *p;//未定义:p指向了一个已经被释放的内存
其他的shared_ptr操作
reset
p = new int(1024);//错误:不能将一个指针赋值给一个shared_ptr
p.reset(new int(1024));//正确:p指向一个新对象
reset与赋值类似,会更新引用计数,如果需要会释放p指向的对象。
12.1.4 智能指针和异常
void f(){
shared_ptr<int> sp(new int(42));
//其他代码
}
函数退出可能两种情况:1.正常结束;2.发生了异常。
无论哪种情况,局部对象都会被销毁,因此,sp会被销毁,它所指向的内存
也会被正确的释放。
如果上面的程序,变成下面这样:
void f(){
int *ip = new int(42);
//其他代码
delete ip;
}
在上面例子中,如果正常结束,那么ip会被正确释放,但是如果遇到异常,ip将不会正确释放
使用自定义的释放操作
默认情况下,shared_ptr假定他们指向的都是动态内存。因此当一个shared_ptr
被销毁的时,它默认对他管理的指针进行delete操作。
另外,我们还可以自定义自己的删除操作,来代替默认的delete操作。
当创建一个shared_ptr时,可以传递一个表示删除的函数,代替默认的delete操作。
void end_connection(connection *p){
disconnect(*p);
}
void f(destination &d /*其他参数*/){
connection c = connect(&d);
shared_ptr<connection> p(&c,end_connection);
//其他代码
}
当p被销毁的时候,不会直接调用delete函数,而是调用end_connection函数。
这样,不管f函数是正常结束 还是异常退出,end_connection必定
会被调用。
上面函数的写法可能看不出来有什么好处,那么作为对比,请看下面的写法
void f(destination &d,/*其他参数*/){
connection c = connect(&d);
//其他代码
//最后,必须调用disconnect()进行资源的释放
}
那么上面的例子,一旦中间出现异常,disconnect将不会被调用
为了能够正确的使用智能指针,请坚持一些基本规范:
- 不使用相同的内置指针初始化多个智能指针
- 不delete get()返回的指针
- 不使用get()初始化或reset另外一个智能指针
- 如果你使用了get()返回的指针,记住当最后一个对应的智能指针被销毁后,你的指针就会变为无效了
- 如果你使用了智能指针管理的资源不是new分配的内存,记住传递给他一个删除器
12.1.5 unique_ptr
与shared_ptr不同,某个时刻只能有一个unique_ptr指向一个给定的对象
当unique_ptr被销毁的时候,他所指的对象也被销毁
下表列出了unique_ptr特有的操作。与shared_ptr相同的操作在表12.1中
unique_ptr<double> p1;//可以指向一个double的unique_ptr
unique_ptr<int> p2(new int(42));//p2指向一个值为42的int
由于unique_ptr拥有它指向的对象,因此unique_ptr不支持普通的拷贝或者赋值操作:
unique_ptr<string> p1(new string("Stegosaurus")):
unique_ptr<string> p2(p1);//错误,unique_ptr不支持拷贝
unique_ptr<string> p3;
p2 = p2;//错误:unique_ptr不支持赋值
虽然不能拷贝和赋值unique_ptr,但是可以通过release或者reset
将指针的所有权从一个转移到另外一个。
unique_ptr<string> p2(p1.release());//release将p1置空
unique_Ptr<string> p3(new string("Trex"));
//将所有权从p3转移到p2
p2.reset(p3.release());//reset释放了p2原来指向的内存
传递unique_ptr参数和返回unique_ptr
不能拷贝uniqque_ptr 的规则有一个例外:我们可以拷贝或者赋值一个将要被
销毁的unique_ptr.最常见的例子是从函数返回一个unique_ptr:
unique_ptr<int> clone(int p){
return unique_ptr<int>(new int(p));
}
unique_ptr<int> clone(int p){
unique_ptr<int> ret (new int(p));
return ret;
}
向unique_ptr中传递删除器
//p指向一个类型为objT的对象,并使用一个类型为delT的对象释放objT对象
//它会调用一个名为fcn的delT类型对象
unique_ptr<objT,delT> p(new objT,fcn):
更具体的例子如下:
void f(destination &d/*其他参数*/){
connection c= connect(&d);
unique_ptr<connection,decltype(end_connection) *> p(&c,end_connection);
//其他代码
}
12.1.6 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);//wp弱共享p,p的引用计数未改变
因为对象可能不存在,所以不能直接通过weak_ptr来访问对象,需要调用其lock函数。如果存在,这个函数返回一个shared_ptr对象,然后使用这个对象来访问。
if(shared_ptr<int> np = wp.lock()){
//np和p共享同一个对象
}
12.2 动态数组
new还可以一次分配多个对象。
12.2.1 new和数组
int *pia = new int[get_size()];
为了分配一个数组,需要在类型名后面跟上一个方括号,在方括号内写上,需要的个数,成功返回第一个对象的地址。
typedef int arrT[42];
int *p = new arrT;//分配一个含有42个元素的int数组
由上面可以看到,这种分配方式,返回的类型并不是数组类型,而是数组中元素类型的指针。
初始化动态分配对象的数组
int *pia = new int[10];
int *pia2 = new int[10]();//值初始化
string *psa = new string[10];
string *psa2 = new string[10]();//值初始化
还可以进行列表初始化
int *pia3 = new int[10]{0,1,2,3,4,5,6,7,8,9};
//前4个,用给定的值,进行初始化,剩下的进行值初始化
string *psa3 = new string[]{"a","an","the",string(3,'x')};
当 new[n] n等于0时,依然合法。
当new分配一个大小为0的数组时,new返回一个合法的非空指针。此指针保证与new返回的其他任何指针都不相同。对于零长度的数组来说,此指针就像尾后指针一样。
释放动态数组
在delete后面加上方括号
delete p;//p必须指向一个动态分配的对象或为空
delete [] pa; //pa必须指向一个动态分配的数组或为空
第二条语句销毁pa指向的数组中的元素,并释放对应的内存。数组中的元素按照逆序进行销毁。
上面的方括号是必须的,如果忽略方括号,行为是未定义的。
typedef int arrT[42];
int *p = new arrT;
delete [] p;//此处的方括号也不能省略
智能指针和动态数组
unique_ptr
unique_ptr<int[] > up(new int[10]);
up.release();//自动调用delete[] 销毁其指针
当unique_ptr指向数组的时候,不能直接使用点运算符和尖头运算符。但是她能够使用下标运算符。
下图给出了支持的操作。
shared_ptr
shared_ptr不支持动态数组,如果要使用动态数组,必须自定义删除器。
shared_ptr<int> sp(new int[10],[](int *p){delete[] p;});
sp.reset();//使用我们提供的lambada释放数组,它使用delete[]
如果上面没有提供删除器,那么结果将是未定义的。
如果要访问此种的元素,下面是一个示例:
for(size_t i = 0;i != 10;++i){
*(sp.get() + i ) = i;//使用get获取内置指针
}
12.2.2 allocator类
new将内存分配和对象构造组合在了一起,有时候,我们希望这两者能够分开,此时可以使用allocator类。比如下面的例子,就不需要将内存分配和对象构造组合在一起
string *const p = new string[n];
string s;
string *q = p;
while(cin >> s && q != p +n)
*q++ = s;
const size_t size = q -p ;
deletep[] p;
上面例子进行了两处赋值:1.new 数组时,2.while循环体中读取到数据之后。
allocator类
allocator分配的内存是一种,原始的,未构造的。下表列出了allocator的操作
allocator分配未构造的内存
allocator<string> alloc; //可以分配string的allocator对象
auto const p = alloc.allocate(n);//分配n个未初始化的string
allocator在未初始化的内存中构造对象
allocator分配完内存之后,此时可以按照需要进行对象的构造。
auto q = p;
alloc.construct(q++);//*q为空字符串
alloc.construct(q++,10,'c');//*q为cccccccccc
alloc.construct(q++,"hi");//*q为hi
//q指向最后构造元素之后的位置
construct接受一个指向未初始化内存的指针,和零个或者多个额外的参数,这些参数,必须和需要构造的对象的某一个构造函数的参数类型相匹配。他们跟make_shared的参数类似。
cout << *p << endl;//正确:使用string的输出运算符
cout << *q << endl;//错误:q指向未构造的内存
注意:为了使用allocate返回的内存,必须使用construct构造对象。使用未构造的内存,其行为是未定义的。
当使用完对象之后,必须对每个构造的对象调用destroy来销毁他们。函数destroy接受一个指针,这个指针指向构造的对象。。
while(q!=p)
alloc.destroy(--q);
注意:只能对真正构造了的元素进行destroy操作
一旦destroy完成之后,就可以再次使用allocate方法,进行构造对象。
释放未初始化的内存,需要调用deallocate函数,此函数,将未初始化的内存,返回给系统
alloc.deallocate(p,n);
传递给deallocate的p是allocated返回的值,n为allocated传递进去的值。
拷贝和填充未初始化内存的算法
算法如下表:
例子如下:
auto p = alloc.allocate(vi.size() *2);
auto q = uninitialized_copy(vi.begin(),vi.end(),p);
uninitialized_fill_n(q.vi.size(),42);
本章完