当前位置: 首页 > news >正文

CPP 内存管理

目录

一、内存模型

二、接口函数

1、malloc 与 free

2、new 与 delete

三、智能指针

1、unique_ptr

(1)基本使用

(2)不能被复制,只能被移动

(3)自定义资源释放函数

2、shared_ptr

(1)基本使用

(2)循环引用

3、weak_ptr

4、内存占用

四、注意事项

1、拥抱 RALL

2、智能指针选择指南

3、谨慎使用 shared_ptr

4、自定义内存检测工具


一、内存模型

  • 栈是⼀种有限的内存区域,⽤于存储函数的局部变量、函数参数和函数调⽤信息的区域。函数的调⽤和返回通过栈来管理。栈上的变量⽣命周期与其所在函数的执⾏周期相同。栈上的内存分配和释放是⾃动的,速度较快。
  • 堆⽤于存储动态分配的内存的区域,由程序员⼿动分配和释放。使⽤ newdeletemallocfree 来进⾏堆内存的分配和释放。
  • 全局区存储全局变量和静态变量。⽣命周期是整个程序运⾏期间。在程序启动时分配,程序结束时释放。
  • 常量区也被称为只读区。存储常量数据,如字符串常量。
  • 代码区用于存储程序的代码。

二、接口函数

1、malloc 与 free

  • 只分配/释放原始内存,不调用构造函数/析构函数
  • 返回 void* 需手动类型转换
  • 分配失败时返回 NULL
  • 不感知对象生命周期
#include <cstdlib>
// 分配内存
void* malloc(size_t size);  // 分配未初始化的原始内存
// 释放内存
void free(void* ptr);// 示例:基础类型
int* p = (int*)malloc(sizeof(int)); 
*p = 10;
free(p);// 示例:数组
int* arr = (int*)malloc(5 * sizeof(int));
free(arr);

2、new 与 delete

关键特征

  • 自动计算内存大小(无需 sizeof
  • 自动调用构造函数/析构函数
  • 类型安全(无需类型转换)
  • 分配失败抛出 std::bad_alloc 异常
  • 支持重载(类级别或全局)
/*单个对象
*/
// 分配 + 构造
auto* ptr = new Type(args...); 
// 析构 + 释放
delete ptr;/*对象数组
*/
auto* arr = new Type[N];  // 调用 N 次默认构造函数
delete[] arr;             // 调用 N 次析构函数

三、智能指针

1、unique_ptr

(1)基本使用

   std::unique_ptr对其持有的堆内存具有唯一拥有权,引用计数永远是 1。std::unique_ptr对象销毁时会释放其持有的堆内存。

        可以使用以下方式初始化一个std::unique_ptr对象:

#include <memory>
//初始化方式 1
std::unique_ptr<int> sp1(new int(123));//初始化方式 2
std::unique_ptr<int> sp2;
sp2.reset(new int(123));//初始化方式 3,强烈推荐
std::unique_ptr<int> sp3 = std::make_unique<int>(123);
std::unique_ptr<int[]> uptr_arr = std::make_unique<int[]>(10); // 10 个 int 的数组
uptr_arr[0] = 42; // 像普通数组一样使用

        应该使用更安全的方式(std::make_unique)创建unique_ptr,《Effective Modern C++》中有如下解释。

        让很多人对 C++11 规范吐槽的地方之一是,C++11 新增了 std::make_shared() 方法创建一个 std::shared_ptr 对象,却没有提供相应的 std::make_unique() 方法创建一个 std::unique_ptr 对象,这个方法直到 C++14 才被添加进来。当然,在 C++11 中你很容易实现出一个这样的方法:

template<typename T, typename... Ts>
std::unique_ptr<T> make_unique(Ts&& ...params)
{return std::unique_ptr<T>(new T(std::forward<Ts>(params)...));
}

   std::unique_ptr离开其作用域时会自动释放持有的内存。当然你也可以调用 reset()立即销毁持有的内存,也可以分配新对象。

// 1. 自动释放:当 uptr 离开其作用域时
{auto uptr = std::make_unique<MyClass>();
} // 此处 ~MyClass() 被自动调用// 2. 手动释放并置空:.release() 放弃所有权,返回原始指针(你需要负责管理这个指针的生命周期)
MyClass* raw_ptr = uptr.release();
delete raw_ptr; // 现在必须手动删除// 3. 手动释放并销毁:.reset() 立即销毁其管理的对象,并将自身置空
uptr.reset(); // 删除对象,uptr = nullptr
uptr.reset(new MyClass()); // 删除旧对象,接管新对象

(2)不能被复制,只能被移动

        鉴于 std::auto_ptr 的前车之鉴,std::unique_ptr 禁止复制语义,为了达到这个效果,std::unique_ptr 类的拷贝构造函数和赋值运算符(operator =)被标记为 =delete

template <class T>
class unique_ptr
{//省略其他代码...//拷贝构造函数和赋值运算符被标记为deleteunique_ptr(const unique_ptr&) = delete;unique_ptr& operator=(const unique_ptr&) = delete;
};

由于无法复制,所有权的传递必须通过 std::move

std::unique_ptr<MyClass> uptr_src = std::make_unique<MyClass>();
// std::unique_ptr<MyClass> uptr_dst = uptr_src; // 错误!编译失败,无法复制
std::unique_ptr<MyClass> uptr_dst = std::move(uptr_src); // 正确:所有权转移// 此时 uptr_src 变为 nullptr,不再拥有任何对象
if (uptr_src == nullptr) {std::cout << "uptr_src is now empty" << std::endl;
}
// uptr_dst 现在拥有对象
uptr_dst->doSomething();

(3)自定义资源释放函数

        默认情况下,智能指针对象在析构时只会释放其持有的堆内存(自动调用 delete 或者 delete[]),但是假设这块堆内存代表的对象还对应一种需要回收的资源(如操作系统的套接字句柄、文件句柄等),我们可以通过自定义智能指针的资源释放函数。

        假设现在有一个 socket 类,对应着操作系统的套接字句柄,在回收时需要关闭该对象,我们可以如下自定义智能指针对象的资源析构函数,这里以 std::unique_ptr 为例:

#include <iostream>
#include <memory>class Socket
{
public:Socket(){}~Socket(){}//关闭资源句柄void close(){}
};int main()
{auto deletor = [](Socket* pSocket) {//关闭句柄pSocket->close();//TODO: 你甚至可以在这里打印一行日志...delete pSocket;};std::unique_ptr<Socket, void(*)(Socket * pSocket)> spSocket(new Socket(), deletor);return 0;
}

自定义 std::unique_ptr 的资源释放函数其规则是:

std::unique_ptr<T, DeletorFuncPtr>

其中T是你要释放的对象类型,DeletorFuncPtr 是一个自定义函数指针。上述代码 33 行表示 DeletorFuncPtr 有点复杂,我们可以使用 decltype(deletor) 让编译器自己推导deletor的类型,因此可以将 33 行代码修改为:

std::unique_ptr<Socket, decltype(deletor)> spSocket(new Socket(), deletor);

2、shared_ptr

(1)基本使用

        多个 shared_ptr 可以指向同一个对象,并通过引用计数机制来管理内存。每当一个新的 shared_ptr 指向该对象时,引用计数加 1;每当一个 shared_ptr 被销毁或重置时,引用计数减 1。当引用计数变为 0 时,对象被自动删除。

        多个线程之间,递增和减少资源的引用计数是安全的。(注意:这不意味着多个线程同时操作 std::shared_ptr 引用的对象是安全的)。std::shared_ptr 提供了一个 use_count() 方法来获取当前持有资源的引用计数。除了上面描述的,std::shared_ptr 用法和 std::unique_ptr 基本相同。

        下面是一个使用 std::shared_ptr 的示例:

std::shared_ptr<MyClass> sptr = std::make_shared<MyClass>();
std::cout << "Use count: " << sptr.use_count() << std::endl; // 输出当前引用计数 (1)
if (sptr.unique()) { // 等价于 use_count() == 1std::cout << "This is the only owner" << std::endl;
}// 使用 std::make_shared 去初始化一个 std::shared_ptr 对象
std::shared_ptr<MyClass> sptr2(sptr);sptr.reset(); // 当前 shared_ptr 放弃所有权,引用计数-1。如果变为 0 则删除对象。sptr 本身变为 nullptr
sptr.reset(new MyClass()); // 放弃旧对象的所有权,接管新对象// 获取原始指针(谨慎使用!不要手动删除它!)
MyClass* raw_ptr = sptr.get();

(2)循环引用

造成资源无法释放,此时应将其中一个指针(通常是反向指针 prev)改为 weak_ptr

struct Node {std::shared_ptr<Node> next;std::shared_ptr<Node> prev;// ~Node() { cout << "Node destroyed" << endl; }
};auto node1 = std::make_shared<Node>();
auto node2 = std::make_shared<Node>();
node1->next = node2; // node2 引用计数 = 2
node2->prev = node1; // node1 引用计数 = 2// 当离开作用域时:
// node2 引用计数从2 -> 1 (因为 node1->next 还指着它)
// node1 引用计数从2 -> 1 (因为 node2->prev 还指着它)
// 引用计数无法归零,内存泄漏!

3、weak_ptr

   std::weak_ptr 是一个不控制资源生命周期的智能指针,它指向一个由 shared_ptr 管理的对象,但不增加其引用计数。它的存在不会阻止所指向对象的销毁。它主要用于解决 shared_ptr 的循环引用问题。它的构造和析构不会引起引用计数的增加或减少。

   std::weak_ptr 必须从一个 shared_ptr 构造或赋值。

auto shared = std::make_shared<MyClass>();
std::weak_ptr<MyClass> weak = shared; // 引用计数仍为 1

        因为 weak_ptr 不拥有对象,所以不能直接使用 ->* 操作符。必须先将它转换为一个 shared_ptr

// 方法一:使用 .lock()
std::shared_ptr<MyClass> temp = weak.lock();
if (temp) { // 检查转换是否成功(对象是否还存在)temp->doSomething(); // 安全地使用对象// 此时引用计数至少为 2 (temp 和原来的 shared)
} else {std::cout << "Object has been destroyed" << std::endl;
}// 方法二:使用构造函数(如果对象已失效则抛出 std::bad_weak_ptr 异常)
try {std::shared_ptr<MyClass> temp(weak);temp->doSomething();
} catch (const std::bad_weak_ptr&) {std::cout << "Bad weak pointer!" << std::endl;
}

        还支持查看对象是否已经被销毁。

// 检查观察的对象是否已被销毁
if (weak.expired()) { // 等价于 use_count() == 0std::cout << "The object is gone" << std::endl;
}// 查看当前引用计数(注意:可能已经过时)
std::cout << "Use count (maybe stale): " << weak.use_count() << std::endl;

4、内存占用

        一个std::unique_ptr对象大小与裸指针大小相同(即sizeof(std::unique_ptr<T>) == sizeof(void*))。

        而std::shared_ptr的大小是std::unique_ptr的一倍。

#include <iostream>
#include <memory>
#include <string>int main()
{std::shared_ptr<int> sp0;std::shared_ptr<std::string> sp1;sp1.reset(new std::string());std::unique_ptr<int> sp2;std::weak_ptr<int> sp3;std::cout << "sp0 size: " << sizeof(sp0) << std::endl;std::cout << "sp1 size: " << sizeof(sp1) << std::endl;std::cout << "sp2 size: " << sizeof(sp2) << std::endl;std::cout << "sp3 size: " << sizeof(sp3) << std::endl;return 0;
}

        在32位机器上,std::unique_ptr占 4 字节,std::shared_ptrstd::weak_ptr占 8 字节;

        在64位机器上,std_unique_ptr占 8 字节,std::shared_ptrstd::weak_ptr占 16 字节。也就是说,std_unique_ptr的大小总是和原始指针大小一样,std::shared_ptrstd::weak_ptr大小是原始指针的一倍。

四、注意事项

1、拥抱 RALL

        RAII (Resource Acquisition Is Initialization) 是 C++ 管理的核心思想:将资源(如内存)的生命周期与对象的生命周期绑定。对象构造时获取资源,析构时自动释放资源。智能指针是 RAII 理念用于内存管理的完美实现。在现代 C++ 中,你应该几乎永远不需要直接使用 newdelete

        强烈建议使用 std::make_uniquestd::make_shared 来创建智能指针,而不是直接用 new。它们更安全(防止异常导致泄漏)、更高效(尤其是 make_shared)。

        彻底拥抱现代 C++ 的内存管理哲学。将 new/delete 视为遗留特性,仅在极其特殊的情况下(例如,需要与某些只接受原始指针的 C 库交互)才使用它们,并且要将其封装在 RAII 类中立即管理。你的代码安全性和可维护性将得到质的提升。

2、智能指针选择指南

特性

std::unique_ptr

std::shared_ptr

std::weak_ptr

所有权

独占

共享

无(弱引用)

复制

不允许(只能移动)

允许

允许

性能开销

几乎为零(与原始指针无异)

较高(引用计数,原子操作)

较高(同 shared_ptr)

主要用途

默认选择,单一明确所有者

共享所有权,多个所有者

打破循环引用,观察

如何创建

make_unique<T>()

make_shared<T>()

shared_ptr构造

  • 默认首选 unique_ptr:除非明确需要共享所有权,否则都应使用它。它最轻量、最安全。
  • 谨慎使用 shared_ptr:仅在确实需要多个所有者共享同一对象生命周期时使用。要警惕循环引用。
  • 使用 weak_ptr 作为 shared_ptr 的补充:用于解决循环引用或作为观察者。
  • 坚持使用 make_uniquemake_shared,它们提供了更强的异常安全保证,并且 make_shared 通常效率更高(单次分配同时存储对象和控制块)。

3、谨慎使用 shared_ptr

   shared_ptr的引用计数问题不好排查,非必要不使用shared_ptr,项目尽量模块化,模块之间不涉及复杂的内存管理,内存管理在框架做好。

        上层业务代码一般不会出现任何内存管理,业务代码尽量不写裸指针代码,一切都底层包装好。尽量避免多线程内存管理,推荐消息传递,或者任务划分,比如A线程创建buffer, B线程处理,C线程释放,不推荐复杂对象shared_ptr嵌套。

4、自定义内存检测工具

        一个复杂的 c/c++ 程序线上运行过程中内存泄露没有释放,如何检测?可能的检测办法是自己实现一套内存检测库。这个库的具体原理和常见内存检测差不多,都是勾住new malloc free delete等操作,拦截后记录

        不同的是,这个库是动态的,也就是说,程序在运行过程中,你可以实时查看当前c/c++ new malloc的信息,申请内存代码位置+大小,当你感觉你的程序内存泄露有没有释放,你可以及时查看程序内存申请信息,也许就会发现,某些内存应该释放但还没有释放,以此定位问题。

        这个库如何应用?它可以在程序里开辟一个线程创建一个httpserver,打开网页就可以实时查看程序的内存对象信息。

http://www.dtcms.com/a/458163.html

相关文章:

  • 专做网页的网站设计网站大全湖南岚鸿网站大全
  • 小公司网站怎么建一级水蜜桃
  • Java25 新特性介绍
  • 珠海做网站找哪家好在线网站推荐几个
  • 倍增:64位整除法
  • 钓鱼网站开发系列教程2013电子商务网站建设
  • Python协程详解:从并发编程基础到高性能服务器开发
  • 以太网数据包协议字段全解析(进阶补充篇)
  • 北京手机网站建设公司哪家好目前较好的crm系统
  • githup网站建设广州工程建设网站
  • 【C++实战(80)】解锁C++大数据处理密码:复盘、调优与实战突破
  • 做一网站需要多少钱博客社区类网站模板下载
  • 【Git】 远程操作 与 标签管理
  • 新品速递 | 亚信电子发布 AX58101 EtherCAT 子设备控制器
  • 山西手机版建站系统哪家好电信宽带360元一年
  • Spring Boot JSON匹配测试
  • 9.MySQL索引
  • Java--多线程基础知识(四)
  • 实现接口文档与测试脚本的实时同步
  • 用vis做的简单网站汉语言专业简历制作说明
  • 如何查看网站开发语言.net core 网站开发
  • 第5章:聊天记忆(Chat Memory)—让 AI 记住上下文
  • RAG创新方案支REFRAG
  • 高通收购Arduino,加速开发者获取领先的边缘计算与AI技术
  • 住房和城市建设厅网站wordpress本地网站怎么访问
  • mongo 适应场景
  • 沧浪企业建设网站价格win8导航网站模板
  • 实战篇:智能选配合理之轨——工业远心镜头选型终极攻略
  • 深入理解队列(Queue):从原理到实践的完整指南
  • 网站开发企业组织结构集团有限公司