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

内存管理(智能指针,内存对齐,野指针,悬空指针)

📌 1. 野指针 (Wild Pointer)

什么是野指针?

野指针指的是未初始化的指针变量。它指向的内存地址是随机的、未知的。

产生原因

cpp

int* ptr; // 野指针!未初始化,指向随机地址
*ptr = 10; // 危险!可能破坏系统内存char* str; // 同样是野指针
std::cout << *str; // 未定义行为

危险后果

  1. ** segmentation fault**:访问受保护的内存区域

  2. 数据损坏:意外修改了其他程序或系统的数据

  3. 难以调试:错误表现随机,难以重现

解决方法

总是初始化指针

cpp

int* ptr = nullptr; // 初始化为空指针
int* ptr2 = new int(10); // 初始化为有效内存
int* ptr3 = &someVariable; // 指向已有变量

📌 2. 悬空指针 (Dangling Pointer)

什么是悬空指针?

悬空指针指的是指针指向的内存已被释放,但指针本身仍然保存着原来的地址。

产生原因

情况1:释放后未置空

cpp

int* ptr = new int(10);
delete ptr; // 内存已释放
// 现在ptr是悬空指针!
*ptr = 20; // 危险!访问已释放内存
情况2:局部变量返回

cpp

int* createArray() {int arr[5] = {1, 2, 3, 4, 5};return arr; // 返回局部数组的指针
} // arr的内存被释放int* danglingPtr = createArray(); // 悬空指针!
std::cout << danglingPtr[0]; // 未定义行为
情况3:多个指针指向同一内存

cpp

int* ptr1 = new int(100);
int* ptr2 = ptr1; // 两个指针指向同一内存delete ptr1; // 释放内存
// 现在ptr1和ptr2都是悬空指针!
std::cout << *ptr2; // 危险!

危险后果

  1. ** use-after-free**:使用已释放的内存

  2. 数据损坏:可能覆盖新分配的内存数据

  3. 安全漏洞:可能被利用进行攻击

解决方法

方法1:释放后立即置空

cpp

int* ptr = new int(10);
delete ptr;
ptr = nullptr; // 重要!释放后立即置空if (ptr != nullptr) { // 安全检查*ptr = 20;
}
方法2:使用智能指针(推荐)

cpp

#include <memory>
std::shared_ptr<int> ptr1 = std::make_shared<int>(10);
std::shared_ptr<int> ptr2 = ptr1; // 共享所有权// 不需要手动delete,自动管理生命周期
// 当所有shared_ptr都超出作用域时,内存自动释放
方法3:避免返回局部变量地址

cpp

// 错误
int* createArray() {int arr[5] = {1, 2, 3, 4, 5};return arr; // 不要这样做!
}// 正确:动态分配
int* createArray() {int* arr = new int[5]{1, 2, 3, 4, 5};return arr; // 调用者需要负责delete[]
}// 更正确:使用vector
std::vector<int> createArray() {return {1, 2, 3, 4, 5}; // 安全!
}

📌 一、什么是内存对齐?

内存对齐指的是数据在内存中的存储地址必须是某个值(通常是2、4、8、16等2的幂次方)的整数倍。

简单例子

cpp

struct Example {char a;      // 1字节int b;       // 4字节  short c;     // 2字节
};

没有对齐时(假设从地址0开始):

text

地址: 0   1   2   3   4   5   6   7   8   9
数据: [a][b][b][b][b][c][c][ ][ ][ ]
总大小: 7字节?但实际上...

实际对齐后(在64位系统 typical alignment):

text

地址: 0   1   2   3   4   5   6   7   8   9   10  11
数据: [a][ ][ ][ ][b][b][b][b][c][c][ ][ ]
总大小: 12字节!(因为有填充字节)

📌 二、为什么需要内存对齐?

1. 硬件要求(最主要的原因)

现代CPU不是以字节为单位访问内存,而是以字长(word size)为单位(通常为4字节或8字节)。

不对齐访问的代价

cpp

// 假设int需要4字节对齐,但存储在地址0x3
int* ptr = (int*)0x3; 
int value = *ptr; // CPU需要2次内存访问!

CPU需要:

  1. 读取地址0x0-0x3的4字节

  2. 读取地址0x4-0x7的4字节

  3. 拼接出需要的4字节数据

2. 性能优化

对齐的内存访问只需要1次内存操作,而不是2次或更多。

性能对比

访问类型CPU操作次数性能影响
对齐访问1次⚡ 最快
不对齐访问2次🐢 慢2倍
严重不对齐多次🚫 极慢

3. 平台兼容性

某些架构(如ARM、SPARC)根本不允许未对齐的内存访问,会导致硬件异常。

cpp

// 在ARM架构上可能直接崩溃!
int* misaligned_ptr = (int*)(char_buffer + 1);
int value = *misaligned_ptr; // 硬件异常!

📌 三、对齐规则和示例

基本对齐规则

每个数据类型都有自然的对齐要求:

  • char:1字节对齐

  • short:2字节对齐

  • int:4字节对齐

  • float:4字节对齐

  • double:8字节对齐

  • 指针:4字节(32位)或8字节(64位)对齐

结构体对齐示例

cpp

struct MyStruct {char a;      // 1字节,偏移0// 3字节填充(因为int需要4字节对齐)int b;       // 4字节,偏移4  short c;     // 2字节,偏移8// 2字节填充(使整体大小为最大成员的整数倍)
}; // 总大小:12字节

内存布局

text

偏移: 0   1   2   3   4   5   6   7   8   9   10  11
数据: [a][pad][pad][pad][b][b][b][b][c][c][pad][pad]

📌 四、如何控制内存对齐?

1. 编译器指令(通用)

cpp

// 强制4字节对齐
struct alignas(4) MyStruct {char a;int b;short c;
}; // 大小:8字节(而不是12字节)// 或者使用pragma(编译器特定)
#pragma pack(push, 1) // 强制1字节对齐(无填充)
struct TightPacked {char a;int b;  short c;
}; // 大小:7字节
#pragma pack(pop) // 恢复默认对齐

2. C++11 alignas 关键字

cpp

#include <iostream>struct alignas(16) AlignedStruct {int a;double b;char c;
};int main() {std::cout << "Alignment: " << alignof(AlignedStruct) << std::endl;std::cout << "Size: " << sizeof(AlignedStruct) << std::endl;return 0;
}

3. 动态内存对齐

cpp

#include <cstdlib>
#include <iostream>// C11/C++17 的动态对齐分配
void* aligned_memory = std::aligned_alloc(64, 1024); // 64字节对齐,分配1KB
// 使用...
std::free(aligned_memory);

在 C++ 中,智能指针是一种封装了原始指针的类模板,用于自动管理动态内存,避免内存泄漏。它们通过 RAII(资源获取即初始化)机制,在离开作用域时自动释放所指向的内存。C++ 标准库提供了三种主要的智能指针:unique_ptrshared_ptr 和 weak_ptr,它们各自有不同的特性和用途。

1. unique_ptr:独占所有权的智能指针

  • 特性:同一时间内,只能有一个 unique_ptr 指向某块内存,即所有权是独占的。
  • 行为
    • 不允许拷贝(copy),但允许移动(move),即所有权可以转移。
    • 当 unique_ptr 离开作用域或被销毁时,会自动释放所指向的内存。
  • 适用场景
    • 管理单个对象的动态内存,且不需要共享所有权。
    • 作为函数的返回值或参数(通过移动语义传递)。
  • 示例

    cpp

    运行

    #include <memory>
    int main() {std::unique_ptr<int> ptr1(new int(10));  // 独占指向10的内存// std::unique_ptr<int> ptr2 = ptr1;  // 错误:不能拷贝std::unique_ptr<int> ptr2 = std::move(ptr1);  // 正确:转移所有权(ptr1变为空)return 0;
    }  // ptr2离开作用域,自动释放内存
    

2. shared_ptr:共享所有权的智能指针

  • 特性:允许多个 shared_ptr 指向同一块内存,通过引用计数跟踪所有者数量。
  • 行为
    • 当一个 shared_ptr 被拷贝时,引用计数加 1;当被销毁时,引用计数减 1。
    • 当引用计数减为 0 时,自动释放所指向的内存。
  • 适用场景
    • 需要多个对象共享同一资源的所有权(例如:树结构中父节点和子节点互相引用)。
  • 注意
    • 避免循环引用(如两个 shared_ptr 互相指向对方),会导致引用计数无法归零,内存泄漏。此时需配合 weak_ptr 解决。
  • 示例

    cpp

    运行

    #include <memory>
    int main() {std::shared_ptr<int> ptr1(new int(20));std::shared_ptr<int> ptr2 = ptr1;  // 引用计数变为2{std::shared_ptr<int> ptr3 = ptr1;  // 引用计数变为3}  // ptr3销毁,引用计数变为2return 0;
    }  // ptr1和ptr2销毁,引用计数变为0,内存释放
    

3. weak_ptr:弱引用的智能指针

  • 特性:一种 “弱引用”,不拥有所指向内存的所有权,也不影响引用计数。
  • 行为
    • 必须从 shared_ptr 转换而来,无法直接管理内存。
    • 可以通过 lock() 方法获取一个 shared_ptr(若内存未释放),否则返回空。
  • 适用场景
    • 解决 shared_ptr 的循环引用问题。
    • 需访问某资源,但不希望延长其生命周期(例如:缓存、观察者模式)。
  • 示例

    cpp

    运行

    #include <memory>
    struct Node {std::weak_ptr<Node> parent;  // 用weak_ptr避免循环引用// std::shared_ptr<Node> parent;  // 若用shared_ptr,会导致循环引用
    };int main() {std::shared_ptr<Node> child(new Node());std::shared_ptr<Node> parent(new Node());child->parent = parent;  // weak_ptr不增加引用计数return 0;
    }  // 引用计数正常归零,内存释放
    

三者的核心区别总结

智能指针所有权拷贝 / 移动引用计数主要用途
unique_ptr独占禁止拷贝,允许移动管理单个对象,不共享所有权
shared_ptr共享允许拷贝和移动多对象共享同一资源
weak_ptr无所有权允许拷贝和移动不影响解决循环引用,弱引用资源

通过合理使用这三种智能指针,可以大幅减少手动管理内存的错误,使 C++ 代码更安全、更易维护。

在 C++ 中,循环引用(Circular Reference) 是使用shared_ptr时容易出现的问题,它会导致内存泄漏。这一问题的根源在于shared_ptr的引用计数机制与循环依赖的结合,使得引用计数无法归零,最终导致动态内存无法释放。

什么是循环引用?

当两个或多个shared_ptr互相持有对方的引用,形成一个 “闭环” 时,就会发生循环引用。此时,每个shared_ptr的引用计数都无法减到 0,导致它们指向的内存永远不会被释放,造成内存泄漏。

举个具体例子

假设我们有两个类AB,它们互相用shared_ptr引用对方:

cpp

运行

#include <memory>class B;  // 前向声明class A {
public:std::shared_ptr<B> b_ptr;  // A持有B的shared_ptr~A() { std::cout << "A被销毁" << std::endl; }
};class B {
public:std::shared_ptr<A> a_ptr;  // B持有A的shared_ptr~B() { std::cout << "B被销毁" << std::endl; }
};int main() {{std::shared_ptr<A> a(new A());std::shared_ptr<B> b(new B());// 形成循环引用:a持有b,b持有aa->b_ptr = b;b->a_ptr = a;}  // 离开作用域,预期A和B被销毁return 0;
}

运行结果:程序不会输出 “A 被销毁” 和 “B 被销毁”,说明AB的内存没有被释放,发生了内存泄漏。

为什么会发生内存泄漏?

我们一步步分析引用计数的变化:

  1. 创建a(指向 A 对象)时,A 的引用计数为 1。
  2. 创建b(指向 B 对象)时,B 的引用计数为 1。
  3. a->b_ptr = b:B 的引用计数变为 2(ba->b_ptr共同引用)。
  4. b->a_ptr = a:A 的引用计数变为 2(ab->a_ptr共同引用)。
  5. 离开作用域时,ab被销毁:
    • a销毁:A 的引用计数从 2 减为 1(剩余b->a_ptr的引用)。
    • b销毁:B 的引用计数从 2 减为 1(剩余a->b_ptr的引用)。
  6. 此时,A 和 B 的引用计数都为 1,永远不会再减为 0,它们的内存永远不会被释放。

如何解决循环引用?

使用weak_ptr可以打破循环引用。weak_ptr是一种 “弱引用”,它持有对对象的引用,但不增加引用计数,因此不会影响对象的生命周期。

修改上面的例子,将其中一个shared_ptr改为weak_ptr

cpp

运行

#include <memory>
#include <iostream>class B;class A {
public:std::shared_ptr<B> b_ptr;  // 仍用shared_ptr~A() { std::cout << "A被销毁" << std::endl; }
};class B {
public:std::weak_ptr<A> a_ptr;  // 改为weak_ptr,不增加引用计数~B() { std::cout << "B被销毁" << std::endl; }
};int main() {{std::shared_ptr<A> a(new A());std::shared_ptr<B> b(new B());a->b_ptr = b;  // B的引用计数变为2b->a_ptr = a;  // A的引用计数仍为1(weak_ptr不增加计数)}  // 离开作用域return 0;
}

运行结果:输出 “A 被销毁” 和 “B 被销毁”,内存正常释放。

原因分析

  1. b->a_ptrweak_ptr,赋值时 A 的引用计数仍为 1(不增加)。
  2. 离开作用域时,a销毁:A 的引用计数从 1 减为 0 → A 被释放。
  3. A 释放后,其成员b_ptr(指向 B)被销毁:B 的引用计数从 2 减为 1。
  4. 随后b销毁:B 的引用计数从 1 减为 0 → B 被释放。

总结

  • 循环引用shared_ptr的常见陷阱,由互相引用的shared_ptr形成闭环导致。
  • 核心问题:引用计数无法归零,内存无法释放。
  • 解决方案:在循环引用的一方使用weak_ptr,打破计数闭环。
  • weak_ptr的适用场景:需要引用对象,但不希望影响其生命周期(如解决循环引用、缓存等)。

通过合理搭配shared_ptrweak_ptr,可以既享受共享所有权的便利,又避免循环引用带来的内存泄漏。

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

相关文章:

  • Java中Integer转String
  • 为什么企业需要项目管理
  • 安卓编程 之 线性布局
  • 树莓派4B 安装中文输入法
  • AtCoder Beginner Contest 421
  • Mysql 学习day 2 深入理解Mysql索引底层数据结构
  • 【开题答辩全过程】以 基于WEB的茶文化科普系统的设计与实现为例,包含答辩的问题和答案
  • 用简单仿真链路产生 WiFi CSI(不依赖专用工具箱,matlab实现)
  • 面试tips--MyBatis--<where> where 1=1 的区别
  • 如何查看Linux系统中文件夹或文件的大小
  • 【LeetCode - 每日1题】有效的数独
  • SQLSugar 快速入门:从基础到实战查询与使用指南
  • MySQL 在 CentOS 上的安装与配置文件路径详解
  • 【系列06】端侧AI:构建与部署高效的本地化AI模型 第5章:模型剪枝(Pruning)
  • 【LeetCode - 每日1题】鲜花游戏
  • 深度学习:洞察发展趋势,展望未来蓝图
  • Verilog 硬件描述语言自学——重温数电之典型组合逻辑电路
  • 深度学习通用流程
  • 用更少的数据识别更多情绪:低资源语言中的语音情绪识别新方法
  • nestjs连接oracle
  • 大模型备案、算法备案补贴政策汇总【广东地区】
  • SNMPv3开发--snmptrapd
  • CNB远程部署和EdgeOne Pages
  • More Effective C++ 条款18:分期摊还预期的计算成本(Amortize the Cost of Expected Computations)
  • 数据库的CURD
  • Shell 秘典(卷三)——循环运转玄章 与 case 分脉断诀精要
  • C语言类型转换踩坑解决过程
  • Java高并发架构核心技术有哪些?
  • 安装Redis
  • compute:古老的计算之道