《C++ Primer》学习笔记(第十二章)——动态内存

本文详细介绍了C++中动态内存的直接管理和智能指针管理,包括new/delete操作、shared_ptr、unique_ptr、weak_ptr的使用及allocator类的高级应用。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

动态内存

之前一篇文章(这篇文章)讲到过,c++内存分为:堆区,栈区,静态区/全局区,代码区和文字常量区五个区。c++通过动态分配的内存位于堆区,堆区的内存需要程序员自动释放,如果没有进行内存释放,就有可能导致内存泄漏。

一、直接管理内存

1、c++通过运算符new在动态内存中分配空间并且返回一个指向该对象的指针;delete接受一个动态对象指针并销毁该对象释放内存。动态分配的对象可以默认初始化,也可以进行值初始化,对于内置类型来说,进行默认初始化其值是未定义的,如:

int* p1=new int;  //默认初始化,不确定的值
int* p2=new int();  //值初始化为0
int* p3=new int(10);//值初始化为10

2、也可以动态分配一个const对象,和一般cosnt对象一样,动态分配的const对象必须初始化,如:

const int* p=new const int(10);

3、r如果内存耗尽,我们不能通过new运算符分配要求的内存空间,这时使用new运算符就会抛出一个类型为bad_alloc的异常。如果当使用new运算符不能分配内存时,我们不希望抛出异常而是返回一个空指针,在new后面加上nothrow,如:

int *p=new int(10); //当内存不够时,抛出类型为bad_alloc 的异常。
int *p=new (nothrow) int(10);//当内存不够时,返回一个空指针而不是抛出异常

4、使用new分配得到的动态内存,必须使用delete来手动释放内存(智能指针除外),如果分配的是动态数组,则使用delete[]。使用delete时,需要注意一下几点:
①、delete必须作用的指针必须是指向动态分配的对象或者该指针为空指针
②、不能对指向同一个内存地址的多个指针释放多次
③、被delete后的指针称为空悬指针,虽然此时该指针无效了,但是该指针仍然可能保存有动态分配的内存地址,因此使用空悬指针会产生不确定的错误,所以在delete指针后,最好将指针设为空指针(nullptr)。

总结以上几点,可以知道使用new和delete直接管理动态内存容易出现以下三个问题:
①、忘记delete内存,导致内存泄漏;
②、对同一块内存释放多次,比如delete两个指向相同内存地址的指针;
③、使用已经delete后的指针可能产生错误,最好将delete后的指针置为空指针。

由于直接管理动态内存比较麻烦而且容易出错。为了更好的管理动态内存,c++新标准库提供了两种智能指针来管理动态对象:shared_ptr允许多个指针指向同一个对象;unique_ptr则独占所指向的对象。另外还有一个weak_ptr的弱指针,可以指向shared_ptr所管理的对象。

二、shared_ptr指针
1、shared_ptr类为模板类,使用shared_ptr指针时需要指定对象类型。如:

shared_ptr<int> p1;//默认初始化的智能指针为空指针

2、最安全分配和使用动态内存的方法是调用名为:make_shared的标准库函数,make_shared函数动态分配一个对象,并且返回指向该对象的shared_ptr,如:

shared_ptr<int> p1=make_shared<int>(); // 值初始化为0
shared_ptr<int> p2=make_shared<int>(10);  //初始化为10
shared_ptr<string> p3=make_shared<string>("HELLO");

3、可以使用指向动态内存的普通指针(注意是指向动态内存)来初始化一个shared_ptr智能指针,但是接受指针参数的智能指针构造函数是exolicit的,因此只能直接初始化,如:

int* p = new int(10);  //指向动态内存的普通指针
shared_ptr<int> sp = p;  //错误,不能进行隐式转换,explicit
shared_ptr<int> sp(p);  //正确,直接初始化,调用智能指针的构造函数

但是注意,如果我们将shared_ptr绑定到一个普通指针上,就将内存管理交给了智能指针,那么智能指针可能会释放掉内存,因此我们不应该再使用原来的普通指针访问原来的对象了,如:

void func(int* p)
{
	shared_ptr<int> sp(p);  //临时shared_ptr智能指针,
}

int main()
{
	int* p = new int(10);  //指向动态分配对象的普通指针
	func(p);
	cout << *p << endl;  //输出为定义
    return 0;
}

如上述代码所示:当我们将指针p传入到函数func时,p指针初始化了一个shared_ptr指针sp,func函数结束时sp指针就会释放内存,此时p所指向的内存也就被释放了,后续在使用p访问就可能发生错误。

4、同样我们可以使用get函数从shared_ptr智能指针中得到一普通的内置指针,该指针指向与智能指针指向相同的对象。注意我们不能对该内置指针进行delete操作,也不能使用get得到指针初始化另一个智能指针,如:

5、可以对shared_ptr指针进行拷贝和赋值,这样可以使得多个指针指向同一个对象,如:

shared_ptr<int> p1 = make_shared<int>(10);
shared_ptr<int> p2(p1);  //拷贝
shared_ptr<int> p3; //空指针
p3 = p1;  //赋值,此时p1、p2、p3三个指针都指向同一个 对象。

那么shapre_ptr智能指针是如何实现管理动态内存的呢?
实际上shapre_ptr采用了我们之前进到过的引用计数的方法,在之前进到行为像指针的类时,也采用了引用计数的办法。我们可以认为每一个shapre_ptr都有一个关联的计数器用来记录指向同一个对象的智能指针的数量,当每拷贝一个shared_ptr时,计数器加会加一,单销毁一个shared_ptr时,计数器减一,当计数器为0时,shared_ptr类的析构函数就会执行内存释放的操作,因此不需要我们手动进行delete。引用计数个工作原理可以参考之前的文章(第十三章
我们可以调用use_count函数查看智能指针中计数器的值,如:

shared_ptr<int> p1 = make_shared<int>(10);
shared_ptr<int> p2(p1);
shared_ptr<int> p3 = p1;
cout << p1.use_count() << endl;  //输出为3,表示有3个智能指针指向同一个对象

三、unique_ptr
1、shared_ptr指针可以使多个指针共享同一个对象,而unique_ptr指针则与之相反,某一时刻只能有一个unique_ptr指向同一个对象。unique_ptr没有shared_ptr中的make_shared函数,与shared_ptr类似,unique_ptr只能绑定到new返回来的指针上,并且必须采用直接初始化,如:

unique_ptr<int> p1(new int(10));
//
int* q = new int(100);
unique_ptr<int> p2(q);//将q初始化p2后,,不能对q进行delete操作,内存管理权交给了p2;

2、由于unique_ptr允许同一时刻只能有一个unique_pre指向同一个对象,因此unique_ptr类不支持拷贝和赋值操作。(实际上在unique_ptr类内部将拷贝和赋值构造函数定义为=delete,即删除的),但是有一个例外就是,我们可以拷贝或者赋值一个将要销毁的unique_ptr,最常见的栗子就是从函数返回一个unique_ptr,如:

unique_ptr<int> p1(new int(10));
unique_ptr<int> p2=p1;//错误,不能赋值和拷贝

unique<int> func(int n){
return unque_ptr<int>(new int(n));  //正确,可以拷贝一个将要销毁的unique_ptr
};

3、虽然unique_ptr不能拷贝会在赋值,但是通过调用函数release或者reseat可以将指针的所有权从一个(非const)unique_ptr转到另一个unique_ptr。如:

unique_ptr<int> p1(new int(10));
unique_ptr<int> p2(p1.release());  //将指针P1的所有权交给p2,此时不能再使用p1

四、weak_ptr
1、weak_ptr指向shared_ptr管理的对象,创建weak_ptr时要用shared_ptr来初始化。将一个weak_ptr绑定到一个shared_ptr上,不会改变shared_ptr的计数器。如:

shared_ptr<int> p1 = make_shared<int>(10);  //p1的计数器为1
weak_ptr<int> wp(p1);  //将p1初始化wp,此时p1的计数器仍然为1

2、由于使用shared_ptr初始化一个weak_ptr不改变shared_ptr的计数器,因此weak_ptr所指向的对象可能被释放掉,因此不能直接使用weak_ptr访问对象,而是通过调用lock函数返回一个shared_ptr指针来间接访问对象,此时计数器会加1,如:

shared_ptr<int> p1 = make_shared<int>(10);  
weak_ptr<int> wp(p1);
cout << p1.use_count() << endl;  //输出为1
shared_ptr<int> p2 = wp.lock();  //p1的计数器加一
cout<< p1.use_count() << endl;  //输出为2
cout << *p2 << endl;  //输出为10

五、allocator类
使用new运算符通常是将内存分配和对象构造组合在一起,同样使用delete通常是把对象析构和内存释放组合在一起。如果我们希望将内存分配和对象构造分离、将对象析构和内存释放分离,就可以使用allocator类。allocator类允许我们先分配一块大的内存,然后可以在该内存内按需构造函数,allocator分配的内存是原始的,未构造的。智能指针类与allocator类都放在memory头文件中。

1、allocator类是一个模板类。由于allocator类将内存申请和对象构造分离。将对象析构和内存释放分离,因此当我们使用allocator类时,通常需要进行以下四步操作:

①、内存申请,使用函数allocate():

allocator<string> alloc;  //定义一个allocator类对象
string* s = alloc.allocate(10);  //调用allocate函数申请内存但不不进行对象构造,返回起始位置指针

②、对象构造,使用函数construct():

string* q= s;
alloc.construct(q++, "IG");  //调用construct函数构造对象,该函数需要传入一个指针和一个用于构造的参数
alloc.construct(q++, "RNG");
alloc.construct(q++, "EDG");
alloc.construct(q++, "FPX");
alloc.construct(q++, "WE");

③、对象析构,调用destroy()函数,注意我们只能对构造的元素进行destroy:

while (q != s)
alloc.destroy(--q);

④内存释放,调用deallocate()函数:

alloc.deallocate(s, 10);  //函数第一个参数为分配内存的首地址,第二个参数为元素个数且必须与allocate函数中的参数一致。

看一下完整代码(中间插入了打印的代码):

#include<memory>
//其他头文件

int main()
{
	allocator<string> alloc;  //定义allocator类对象
	string* s = alloc.allocate(10);  //内存申请
	string* q= s;  //为了保持申请内存的首地址s,我们重新定义了一个指针
	alloc.construct(q++, "IG");   //构造5个对象
	alloc.construct(q++, "RNG");
	alloc.construct(q++, "EDG");
	alloc.construct(q++, "FPX");
	alloc.construct(q++, "WE");
	string* p = q;
	while (p != s)   
		cout << *(--p) << endl;  ////访问构造的对象
	while (q != s)
		alloc.destroy(--q);   //对象析构
	alloc.deallocate(s, 10);  //内存释放
    return 0;
}
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值