c++ primer 第五版 笔记 第十二章

第二十章 动态内存

因翻译太耗时,现做笔记如下:

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管理动态内存存在三个常见的问题:

  1. 忘记delete内存。
  2. 使用已经释放的内存
  3. 同一块内存释放多次

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将不会被调用

为了能够正确的使用智能指针,请坚持一些基本规范:

  1. 不使用相同的内置指针初始化多个智能指针
  2. 不delete get()返回的指针
  3. 不使用get()初始化或reset另外一个智能指针
  4. 如果你使用了get()返回的指针,记住当最后一个对应的智能指针被销毁后,你的指针就会变为无效了
  5. 如果你使用了智能指针管理的资源不是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);

本章完

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值