首页 > 系统相关 >【C++】动态内存(二)智能指针

【C++】动态内存(二)智能指针

时间:2024-08-15 16:54:21浏览次数:16  
标签:std int C++ 内存 动态内存 shared ptr 指针

由于new和delete会造成一定程度的内存泄漏问题,以及内存所有权不清晰,因此引入自动销毁相应内存空间的智能指针。
智能指针是抽象数据类型,本身具有析构函数,因此调用之后会自动调用析构函数,在析构函数中会自动调用delete来释放相应内存空间,因此不用手动显式的调用delete。

【C++】动态内存(二)智能指针

一、shared_ptr 基于引用计数的共享内存解决方案

1.基本用法:

std::shared_ptr<int> x(new int(4));

2.get()用法:
shared_ptr是一个类模板:std::shared_ptr,其中的模板参数是T。
get()用法返回的是一个T*。
所以shared_ptr<int>返回的是一个int*类型的指针。
其实意思是这样:shared_ptr其实并不是一个单纯的指针,他是一个类模板,里面包含了指向保存数据的内存的指针,也包含了指向引用计数的指针,还包含了很多方法(也就是函数),而get方法就是返回一个什么其他东西都没有的,裸指针,这个指针只是指向那个保存数据的内存地址。

3.reset()用法:
可以将已经赋值的shared_ptr指针的值改变为其他值。

std::shared_ptr<int> x(new int(4));
x.reset(new int(4));

也可将其赋值为nullptr,此时,下面两句是等价的。

x.reset();//x.reset((int*)nullptr);

4.指定内存回收逻辑
当我们想在shared_ptr销毁之前做一些其他事情的时候,就不能使用隐式的delete析构了,因为我们不知道她多会儿会调用该隐式析构,故而不知道何时该做我们想做的析构之前的其他事情,这时就需要显式的自己写一个函数去调用。
如下:

void fun2(int *ptr) {
	std::cout << "call deleter fun2" << std::endl;
	delete ptr;
}
int main()
{std::shared_ptr<int> ptr(new int(4),fun2);}

这样写,那么当shared_ptr的ptr指针的使用计数为0的时候,准备析构的时候,他不会调用默认的析构函数,而是会调用fun2函数,把自己这个地址传入给fun2函数,并且执行fun2中的代码之后由fun2中我们写的显示的析构来删除和销毁。
5.make_shared

std::shared_ptr<int> ptr(new int(3));
std::shared_ptr<int> x = std::make_shared<int>(3);

下面这句也可以简化为:

auto x = std::make_shared<int>(3);

上面三句的效果是一样的。但是推荐使用make_shared,而不推荐使用new。
原因:shared_ptr一共是需要维护两个指针的,一个指针指向保存数据的内存地址,一个指针是用来进行计数的,也就是引用计数,引用计数为0的时候就可以执行析构从而释放该空间了。
而使用new的时候,两个指针指向的地址可能会相隔很远,在计算机运行时偶尔会产生错误。
而使用make_shared的时候会有一个自带的优化,让shared_ptr的两个指针,即一个指向保存数据的内存地址的指针,一个指向引用计数的内存的指针,两个的内存分配的近一点,那么两个指针指的位置就会近一点,计算机相对不那么容易出现运行错误。
6.支持数组( C++17 支持 shared_ptr<T[]> ; C++20 支持make_shared 分配数组)
如下的代码是不合理的:

std::shared_ptr<int> x = new int[6];

因为shared_ptr的隐式析构函数是delete ptr,是销毁单独的指针的,但是上面的代码右边明显是构造了一个数组,而数组对应的销毁方式应该是delete []。所以上式是不可以的,是危险的。
解决方法也是有的,在c++17中支持了:

std::shared_ptr<int[]> x = new int[6];

c++17之后可以像左边一样这样写的时候,shared_ptr在默认析构的时候就会知道这是一个数组,而去调用delete []。,就没有危险了。

而c++20之后又支持了:

auto x = std::make_shared<int[5]>();

在右边make_shared可以分配数组了,左边使用的是auto,其实翻译过来在编译器看来就是这样的:

std::shared_ptr<int[]> x= std::make_shared<int[5]>();

这个也是可以默认使用delete []去析构的,也是安全的,完备的。

7.注意: shared_ptr 管理的对象不要调用 delete 销毁
shared_ptr会自动销毁,一定不要定义了之后再手动的写一个显式的delete在后面,这样会造成多次销毁一个内存空间,会造成报错。

二、unique_ptr 独占内存的解决方案

unique_ptr不支持复制,但可以移动
shared_ptr是共享内存,是可以支持多个指针共享一块内存的,也就是支持如下的操作:

std::shared_ptr<int> x(new int(4));
std::shared_ptr<int> y(x);

第二句就是让y指向x所指向的地址,也就是两个指针指向同一块内存。

但是unique_ptr是独占内存的方式,因此不支持如上第二句的操作。
但是提供了另外一种操作:移动。如下:

std::unique_ptr<int> x(new int(3));
std::unique_ptr<int> y = std::move(x);

第二行的意思是,把原来x所指向的内存空间的地址赋值给y,让y指向这里,然后把x指针清空,赋值为nullptr。所以该操作之后仍然是一块内存空间仅由一个指针所指向,仍然是内存独占。
之后运行打印两个指针所指向的内存地址,可以看到x结果为0,y的结果是之前x所指向的地址。
由于有了复制操作,因此,unique_ptr也可以作为函数参数被传入或者传出,传入以及传出的过程都不是执行赋值的过程,因为这是不允许的,而会执行移动的过程。如下,是被允许的:

std::unique_ptr<int> fun3()
{
	std::unique_ptr<int> res(new int(3));
	return res;
}
int main()
{
	std::unique_ptr<int> x=fun();
}

其中main函数中的左右其实执行的就是移动操作来实现的把右边函数返回的指针所指的地址给到左边的指针,而不是使用的赋值操作。

三、weak_ptr 防止循环引用而引入的智能指针

四、动态内存的其他相关问题

1.使用allocator来分配内存

如果使用new和delete的话,分配内存以及在这块内存上构造对象是直接都会执行的,分配之后就直接赋值了。
而如果像单独的先进行分配内存,则要用到allocator。如下:

std::allocator<int> al;
int* ptr=al.allocate(3);

上面的语句指的是,先构造了一个allocator类模板的,以int数据类型为参数的对象,然后使用分配了可以存放三个int 的空间,把这个空间的首地址赋值给int类型的指针ptr。

如果想删除已分配的内存,则:

al.deallocate(ptrr, 3);

2.使用malloc/free来管理内存

melloc/free其实是c语言中用来分配内存的工具,c++中予以兼容。


```cpp

```c
#include <stdio.h>   
#include <stdlib.h> 
 
int main(void) 
{
    int *p1 = malloc(4*sizeof(int));  // 足以分配 4 个 int 的数组
    int *p2 = malloc(sizeof(int[4])); // 等价,直接命名数组类型
    int *p3 = malloc(4*sizeof *p3);   // 等价,免去重复类型名
 
    if(p1) {
        for(int n=0; n<4; ++n) // 置入数组
            p1[n] = n*n;
        for(int n=0; n<4; ++n) // 打印出来
            printf("p1[%d] == %d\n", n, p1[n]);
    }
 
    free(p1);
    free(p2);
    free(p3);
}

缺陷:不能分配对齐内存。

3.aligned_alloc 分配对齐内存

也是c语言中的语法,同样使用free释放已分配的内存。因为c语言标准中一开始只有malloc,但是发现malloc没有办法分配对齐内存。
所以引入了aligned_alloc来分配对齐内存。

但是不建议使用aligned_alloc和malloc/free,而建议使用allocator。 因为allocator中还有其他很多功能。

标签:std,int,C++,内存,动态内存,shared,ptr,指针
From: https://blog.csdn.net/Gorege__Hu/article/details/141135228

相关文章

  • 在C/C++中嵌入Lua代码及使用VS Code调试
     Lua在设计之初就是为了嵌入到应用程序中,为这些应用程序提供灵活的扩展和定制功能。Lua的核心是用C语言编写的,所以Lua脚本可以很容易地与C/C++代码进行交互,通过Lua脚本,用户可以在不修改原有C/C++代码的基础上,实现功能的扩展和定制。 在C/C++程序中可以使用Lua来编写一些需......
  • C++标准库 algorithm 堆操作 heap
    算法库-堆操作基本操作make_heap()(1)从一个元素范围创建出一个最大堆(2)将区间内的元素转化为heap.--传比较器push_heap()对heap增加一个元素.将一个元素加入到一个最大堆pop_heap()对heap取出下一个元素.从最大堆中移除最大元素sort_heap()对heap转化为一......
  • C++标准库 iomanip 输入输出操纵符 Manipulator
    输入/输出操纵符输入输出操纵符是C++中用于控制输入输出流格式的一组特殊函数或对象。它们通常用于格式化输出,例如设置宽度、精度、对齐方式等,而不涉及数据的实际读写。功能概述:输入输出操纵符能够控制输出的外观,比如调整对齐方式、设置输出的宽度和精度、控制换行等。使用......
  • 【计算机二级C++】题目与C++知识自检
    @目录公共基础知识计算机基础数据库数据结构树链表排序队列栈C++const与static指针函数重载构造与析构多态、继承、权限数据类型输入输出流模板公共基础知识计算机基础计算机完成一条指令所花费的时间称为指令周期顺序程序不具有并发性下列叙述中正确的是CA.算法的......
  • C/C++ 动态分配:malloc()和free()所涉及的空指针和强制类型转换、与new和delete的对比
    1、动态分配的内涵所谓动态内存分配,是指在程序运行时根据需要分配和释放内存,而不是在编译时确定内存需求。动态分配包括两方面的内涵:在堆上分配内存。对于linux的虚拟内存,可以分成以下5段:文本段、数据段(分初始化和未初始化数据段)、堆和栈。不使用动态分配定义一个变量,这个变......
  • C语言指针详解-上
    C语言指针详解-上前言1.指针的基本概念1.1指针是什么1.2指针的声明与初始化1.3取地址符`&`和解引用符`*``&`运算符用于**获取变量的地址**`*`运算符用于访问指针指向的值2.指针的类型常见数据类型的指针指针与数组、字符串数组指针结构体指针函数指针二级指针void指......
  • c#和c++数据交互二
    1:新建c++模板,生成类型动态库2:c++里类型点击查看代码classA{public: inta; };typedefstruct{ intup; intdown; charinfos[4]; //LPWSTRinfos;}info,*LPinfo;3:c++声明动态库方法点击查看代码#defineCVAPIextern"C"__declspec(dll......
  • C++:命名空间与输入输出
    目录前言一、命名空间1.1namespace的价值1.2namespace的定义1.3命名空间的使用二、C++输入&输出前言   C++是一种面向对象的计算机程序设计语言,‌它扩展了C语言的功能,‌并引入了面向对象编程的概念,‌如类、‌继承和多态等,C++是以C语言为基础进行了拓展与创新,C......
  • c#和C++数据交互 一CLR篇
    1:配置VC++目录:包含目录:头文件的所在路径,#include时用“”链接器:输入:附加依赖项如果是clr模板,一定要用lib文件,不然会报链接错误2:先建立一个简单的dll模板,实现加法运算点击查看代码NativeCalculate.hclass__declspec(dllexport)NativeCalculate{public: ......
  • C++ 编译过程
    源码——>预处理器.i文件——>编译器.s文件——>汇编器.o文件——>链接器——>执行程序 预处理器:读取代码里#开头的命令,并把他插入到程序文本里,生产已.i为后缀名的文件编译器:把.i 文件生成汇编文件 .s汇编器:把汇编文件生成二进制文件.a 链接器:链......