动态内存与智能指针
文章目录
1.动态内存与智能指针
静态内存:存储局部static对象,类static对象,以及定义在任何函数之外的对象,使用时创建,程序结束时销毁。
栈内存:保存在函数内的非static对象,包括函数形参,函数内局部变量。
堆内存:动态分配的内存,在程序运行时分配的对象,动态对象的生存期由程序来控制。
2.智能指针
- 常规指针:new 和 delete 需要显式调用,因此如果调用不及时容易出现内存泄漏(等以后出一篇容易造成内存泄露的举例及解决)。
- 智能指针:shared_ptr:允许多个指针指向同一个对象。 unique_ptr:独占指向的对象。weak_ptr:弱引用
2.1 shared_ptr类
智能指针使用方式与普通指针一样。
shared_ptr<string> p1; if (p1 && p1->empty()) { //如果p1不为空,是一个空string,则赋予其一个新值 *p1 = "hi"; }
make_shared函数
在动态内存中分配一个对象并且初始化它,返回指向此对象的shared_ptr。
在make_shared后面加上一个<>给出指定类型:类似于emplace成员,在make_shared需要用参数来构造指定的对象,否则进行值初始化。
//指向一个值为42的int的shared_ptr shared_ptr<int> p1 = make_shared<int>(42); shared_ptr<string> p2 = make_shared<string>(10, '*');//10个* shared_ptr<int> p3 = make_shared<int>(); //值初始化,0 //使用auto auto p4 = make_shared<int>(5); //5
shared_ptr的拷贝与赋值
每个shared_ptr都会记录有多少个相同的对象。
p和q只想相同的对象,此对象有两个引用者。
shared_ptr<int> p = make_shared<int>(10); auto q(p);
引用计数:每个shared_ptr都有一个关联的计数器。
拷贝,初始化,作为函数参数,函数返回,计数器都会递增。
赋予新值,被销毁,局部shared_ptr离开作用域,计数器就会递减。
当计数器的计数为0时,就会自动释放他所管理的内存空间。
shared_ptr<int> q = make_shared<int>(5); shared_ptr<int> p = make_shared<int>(10); auto q=p; //q赋值为p,p的计数器递增,q的计数器递减;q计数器为0,即5被释放。
shared_ptr的析构函数用来管理它所指向对象的引用,并且管理计数器,计数器为0,则析构函数会自动销毁对象,释放内存。
作为函数返回
返回一个shared_ptr对象:
shared_ptr<int> factory(int arg) { //....... return make_shared<int>(arg); }
p在局部变量中创建,并没有返回,离开函数作用域时,p销毁,自动释放内存:
void factory(int arg) { //....... shared_ptr<int> p=make_shared<int>(arg); }
下面返回了一个p的拷贝,拷贝会使得计数器递增,p被销毁,但是并没有释放内存:
shared_ptr<int> factory(int arg) { //....... shared_ptr<int> p=make_shared<int>(arg); return p; }
要确保在无用之处及时释放shared_ptr对象,例如在容器中,unqie重排元素,之后的元素便不需要,则需要使用erase删除无用的元素。
2.2 直接管理内存
new分配与初始化对象
采用直接初始化或者默认初始化,也可以使用列表初始化。
int* p=new int int* p=new int(10); string* s=new string("woaini"); vector<string>* ps=new vector<string>{"abc","dbg","dad","polkd"};
使用auto推断想要分配的对象类型:只能有单一的初始化器。
auto x = new auto("abc"); //const char* 类型
动态分配const对象:
const int* a = new const int(10); const string* b = new const string("abc"); auto c = new const string("5");
分配失败的情况:
int* p1 = new int; //分配失败,new抛出bad_alloc int* p2 = new (nothrow) int; //分配失败,new返回一个空指针
delete释放
delete通常不会分辨指向的是一个静态还是动态分配的对象,也不会指出是否释放成功。
delete可以释放const对象:
const int* p = new const int(10); delete p;
由内置指针管理的动态内存在被显式释放前一直都会存在。
void factory(int arg) { //....... shared_ptr<int> p=make_shared<int>(arg); delete p; //显式释放 }
shared_ptr<int> factory(int arg) { //....... shared_ptr<int> p=make_shared<int>(arg); return p; } delete p; //记得在后面释放内存
ps:1. 忘记delete内存
2. 使用已经释放掉的对象 3. 同一块内存释放两次
delete之后重置指针值
const int* p = new const int(10); delete p; p = nullptr; //重置指针为空
delete的局限与bug:
const int* p = new const int(10); auto q = p; //q和p被绑定到同一个对象 delete p; //释放p,q也被释放 p = nullptr; //只把p设置为空指针,但是q仍然保存着已经释放了的动态内存地址
智能指针与普通指针的不同:
int* q = new int(42), * p = new int(100); p = q; //普通指针,进行赋值后,指向新的内存空间,但是原来的内存会一直保留,造成内存泄露 auto q2 = make_shared<int>(42), p2 = make_shared<int>(100); p2 = q2; //智能指针,进行赋值后,会自动递减计数器,释放原有内存
2.3 shared_ptr和new综合使用
使用普通指针初始化智能指针: 智能指针的构造函数时explicit的,不支持隐私,所以不能使用拷贝初始化的形式,只能使用直接初始化的形式。
shared_ptr<int> p1=new int(10); //错误 shared_ptr<int> p1{new int(10)}; //正确,只能使用直接初始化的形式
包括在函数的返回值中:必须显式的创建shared_ptr< int >,来转换为智能指针。
构建智能指针shared_ptr时,要使用make_shared来创建,因此分配的对象就能直接与shared_ptr绑定,避免了无意间将一块内存绑定到多个独立创建的shared_ptr之上。
普通指针与智能指针传递给函数的注意事项:
void process(shared_ptr<int> ptr) { //这是一个函数体 //.... } //正确方式: shared_ptr<int> p(new int(400)); process(p); int i = *p; //错误方式: int* x(new int(42)); //危险:x不是智能指针 process(x); //错误,不能将int* 转换为shared_ptr process(shared_ptr<int>(x)); //合法,但是内存会被释放 int j = *x; //未定义,x是一个空悬指针
临时的shared_ptr传递给函数的情况: 当这个临时的shared_ptr在函数内执行完毕后,被销毁,计数器递减,为0,会释放其所指向的内存空间;此时再让一个j去访问一个已经释放了的空间是不合法的。
get函数
返回一个内置指针,指向智能指针所管理的对象。
作用:我们要向不能使用智能指针的代码传递一个内置指针。
使用get返回的指针不能delete释放:会使得原智能指针也被delete一次,智能指针成为空悬指针,然后其销毁时,会再次释放一次,造成delete了两次!!!
只有确保代码不会被delete时,才能使用get。 并且永远不要用get初始化另一个智能指针或者为智能指针赋值。
shared_ptr<int> p(new int(100)); int* q = p.get(); // p 和 q共享一个内存的使用权 //shared_ptr<int> s = p.get(); //错误 delete q; //释放q,导致p也被释放 int foo = *p; return 0; //在最后p释放时,已经指向了无效的区域,发生崩溃
reset操作
使用reset来将一个新的指针赋予一个shared_ptr。
shared_ptr<int> p(new int(100)); p.reset(new int(50)); //指向新的内存空间
reset会更新引用计数,会释放原来的内存空间。
与unique配合使用,来控制多个shared_ptr共享的对象。
在改变底层对象之前,先检查是否有当前对象的唯一控制权,如果不是,则创建一份新的拷贝,让其单独到一个新的内存空间里去:
shared_ptr<int> p(new int(100)); auto q(p); //p q 共享控制权 if (!p.unique()) //不是唯一控制的 { p.reset(new int(*p)); //更新内存空间位置 } *p += 10; //在改变底层对象
2.4 智能指针和异常
智能指针在程序结束或者是异常终止,都能将其释放。但是内置指针如果在delete之前被终止,则内存空间不会被释放。
使用自己的释放操作:制作删除器:完成对shared_ptr中保存的指针进行释放的操作(相当于析构函数的操作)。
ps:
- 不适用相同的内置指针值初始化多个智能指针
- 不delete 或者 get返回的指针
- 不适用get初始化或reset另一个智能指针
- 使用get返回的指针,在对应的最后一个智能指针销毁后,你的内置指针就无效了
- 使用智能指针管理的资源,不是new分配的内存,要传递给他一个删除器
2.5 unique_ptr
unique_ptr:拥有它所指向的对象,某个时刻,只能有一个unique_ptr指向一个给定的对象。
初始化必须采用直接初始化的方式。
unique_ptr<int> p(new int(100));
unique_ptr拥有其对象, 因此不能普通共享。
unique_ptr<int> p{ new int(100) }; unique_ptr<int> q{ p }; //不支持拷贝 unique_ptr<int> c; c = p; //不支持赋值
可以借助reset和release完成共享:
- release:释放其指向的对象,将本身置为空,返回其原来所指的对象。
- reset(q):另其指向q对象。
unique_ptr<int> p{ new int(100) }; unique_ptr<int> q(p.release()); //转移给q unique_ptr<int> c; c.reset(q.release());
可以让一个指针指向release的返回值
auto p=q.release()
从函数返回unique_ptr:
unique_ptr<int> Clone1(int p) //返回临时对象 { return unique_ptr<int>(new int(p)); //从函数返回unique_ptr指针 } unique_ptr<int> Clone2(int p) //返回局部对象的拷贝 { unique_ptr<int> ret(new int(p)); return ret; }
传递删除器:
unique_ptr<objT,delT> p(new objT,func);
2.6 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的引用计数,wp的对象随时都有可能被shared_ptr释放。
shared_ptr使用reset置为空,释放内存空间,weak_ptr与shared_ptr绑定,也跟着一起释放。
- lock函数:weak_ptr的函数,如果expired为空,返回空weak_ptr;否则返回指向其对象的shared_ptr
- expired函数:若use_count为空,返回true;否则返回false;
- use_count函数:返回与shared_ptr共享的数量。
auto p = make_shared<int>(42); weak_ptr<int> wp(p); p.reset(); // wp.use_count()==1 -> wp.expired()==false -> wp.lock() 返回其指针对象,解引用得到值 cout << *(wp.lock()) << endl; //出错,wp为空悬指针
3.动态数组
ps:使用标准库容器而不是动态分配的数组,使用容器更简单,更不容易出现内存管理错误并且可以有更好的性能。
动态数组不是数组类型!!!
new和数组
new分配多个内存空间
int* p = new int[50]; //分配50个int的空间,不必是常量
简便的起别名方式:
typedef int arrT[50]; //起别名,arrT表示一个50个int的数组类型 int* p = new arrT; //分配50个元素的数组
ps:动态数组无法使用begin()和end(),因此也无法使用基于范围for语句,因为分配的内存并不是数组类型
初始化
采用值初始化,()为空则采用默认初始化,可以使用列表初始化,如果给出的元素个数少于分配的数量,则剩下的采用默认值初始化的方式。
int* p = new int[10] {1, 2, 3, 4, 5};
释放动态数组
采用[]的形式,必须要对应[],指明了这是一个数组的释放。
int* p=new int[10]; delete p;
ps:数组中的元素按逆序销毁,即从后往前销毁。
智能指针和动态数组
-
使用unique_ptr的动态数组
指向一个包含10个int的数组:
unique_ptr<int[]> p(new int[10]); p.release(); //调用释放其内存,相当于delete[] p
unique_ptr指向一个数组,就不能使用->和 . 运算符(成员访问运算符),但是可以使用[]下标运算符访问元素:
for (size_t i=0;i!=10;++i) { cout<<p[i]<<" "; }
-
使用shared_ptr的动态数组
必须提供自己定义的删除器:
shared_ptr<int[]> q(new int[20], [](int* p) {delete[] p; }); q.reset(); //使用lamdba释放数组,使用delete[]
shared_ptr的动态数组不支持下标,也不支持指针的算术运算
for (size_t i=0;i!=10;++i) { *(q.get()+i) = i; //转换为内置指针,进行内置指针的解引用与递增 }
allocator类
引入:
new的局限性:将内存分配和对象构造组合在一起。
delete的局限性:将对象析构和内存释放组合在一起。
- 我们希望将内存分配和构造对象分离,只有在真正需要时才会执行对象的创建操作。
allocator类
提供了将内存分配和对象构造分离开的方法。
它分配的内存是原始的,未构造的。
allocator<string> alloc; //可以分配string的allocator对象 auto const p = alloc.allocate(n); //可以分配n个未初始化的string
construct成员函数
接受一个指针和零个或多个额外参数,在给定位置构造一个元素,
allocator<string> alloc; //可以分配string的allocator对象 auto p = alloc.allocate(10);//可以分配n个未初始化的string auto q = p; alloc.construct(q++, 10, 'c'); alloc.construct(q++); alloc.construct(q++, "hi"); cout << *p << endl;
destroy函数
对每个构造的元素调用destroy来销毁他们,接受一个指针,对指向的对象执行析构函数。
while (q != p) { alloc.destroy(--q); //释放我们真正构造的string对象 }
deallocate函数
大下参数必须与分配时提供的大小参数一样。
alloc.deallocate(p,10); //释放内存
拷贝和填充未初始化内存的算法
- uninitialized_copy(b,e,b2);
- uninitialized_copy_n(b,n,b2);
- uninitialized_fill(b,e,t);
- uninitialized_fill_n(b,n,t);
vector<int> a{ 1,2,3,4,5,6,7,8,9 }; allocator<int> alloc; //创建一个比容器大两倍的初始内存 auto p = alloc.allocate(a.size() * 2); //返回得到的q指向最后一个构造元素之后的位置 auto q = uninitialized_copy(a.cbegin(), a.cend(), p); //填充42 uninitialized_fill_n(q, a.size(), 42);
完整操作:
allocator<string> alloc; string s; auto str = alloc.allocate(10); auto p = str; while (cin>>s && p != str + 10) { alloc.construct(p++, s); } while (p != str) { alloc.destroy(--p); } alloc.deallocate(str, 10);
本文来自博客园,作者:hugeYlh,转载请注明原文链接:https://www.cnblogs.com/helloylh/p/17209734.html
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 从 HTTP 原因短语缺失研究 HTTP/2 和 HTTP/3 的设计差异
· AI与.NET技术实操系列:向量存储与相似性搜索在 .NET 中的实现
· 基于Microsoft.Extensions.AI核心库实现RAG应用
· Linux系列:如何用heaptrack跟踪.NET程序的非托管内存泄露
· 开发者必知的日志记录最佳实践
· TypeScript + Deepseek 打造卜卦网站:技术与玄学的结合
· Manus的开源复刻OpenManus初探
· 写一个简单的SQL生成工具
· AI 智能体引爆开源社区「GitHub 热点速览」
· C#/.NET/.NET Core技术前沿周刊 | 第 29 期(2025年3.1-3.9)