📌 1. 野指针 (Wild Pointer)
什么是野指针?
野指针指的是未初始化的指针变量。它指向的内存地址是随机的、未知的。
产生原因
cpp
int* ptr; // 野指针!未初始化,指向随机地址 *ptr = 10; // 危险!可能破坏系统内存char* str; // 同样是野指针 std::cout << *str; // 未定义行为
危险后果
** segmentation fault**:访问受保护的内存区域
数据损坏:意外修改了其他程序或系统的数据
难以调试:错误表现随机,难以重现
解决方法
总是初始化指针:
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; // 危险!
危险后果
** use-after-free**:使用已释放的内存
数据损坏:可能覆盖新分配的内存数据
安全漏洞:可能被利用进行攻击
解决方法
方法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需要:
读取地址0x0-0x3的4字节
读取地址0x4-0x7的4字节
拼接出需要的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_ptr
、shared_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,导致它们指向的内存永远不会被释放,造成内存泄漏。
举个具体例子
假设我们有两个类A
和B
,它们互相用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 被销毁”,说明A
和B
的内存没有被释放,发生了内存泄漏。
为什么会发生内存泄漏?
我们一步步分析引用计数的变化:
- 创建
a
(指向 A 对象)时,A 的引用计数为 1。 - 创建
b
(指向 B 对象)时,B 的引用计数为 1。 a->b_ptr = b
:B 的引用计数变为 2(b
和a->b_ptr
共同引用)。b->a_ptr = a
:A 的引用计数变为 2(a
和b->a_ptr
共同引用)。- 离开作用域时,
a
和b
被销毁:a
销毁:A 的引用计数从 2 减为 1(剩余b->a_ptr
的引用)。b
销毁:B 的引用计数从 2 减为 1(剩余a->b_ptr
的引用)。
- 此时,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 被销毁”,内存正常释放。
原因分析:
b->a_ptr
是weak_ptr
,赋值时 A 的引用计数仍为 1(不增加)。- 离开作用域时,
a
销毁:A 的引用计数从 1 减为 0 → A 被释放。 - A 释放后,其成员
b_ptr
(指向 B)被销毁:B 的引用计数从 2 减为 1。 - 随后
b
销毁:B 的引用计数从 1 减为 0 → B 被释放。
总结
- 循环引用是
shared_ptr
的常见陷阱,由互相引用的shared_ptr
形成闭环导致。 - 核心问题:引用计数无法归零,内存无法释放。
- 解决方案:在循环引用的一方使用
weak_ptr
,打破计数闭环。 weak_ptr
的适用场景:需要引用对象,但不希望影响其生命周期(如解决循环引用、缓存等)。
通过合理搭配shared_ptr
和weak_ptr
,可以既享受共享所有权的便利,又避免循环引用带来的内存泄漏。