目录
2.2 malloc/free 和 calloc/realloc
8.3 C++17:aligned_new、std::byte
8.4 C++20:std::span、std::jthread
class 卑微码农:
def __init__(self):
self.技能 = ['能读懂十年前祖传代码', '擅长用Ctrl+C/V搭建世界', '信奉"能跑就别动"的玄学']
self.发量 = 100 # 初始发量
self.咖啡因耐受度 = '极限'
def 修Bug(self, bug):
try:
# 试图用玄学解决问题
if bug.严重程度 == '离谱':
print("这一定是环境问题!")
else:
print("让我看看是谁又没写注释...哦,是我自己。")
except Exception as e:
# 如果try块都救不了,那就...
print("重启一下试试?")
self.发量 -= 1 # 每解决一个bug,头发-1
# 实例化一个我
我 = 卑微码农()
引言
动态内存管理,说白了,就是在运行时分配和释放内存。C++不像Java那样自动帮你收拾,你得手动new/delete,或者用智能指针。学好这个,能让你的程序稳定高效。反之,就等着debug到吐血吧。我记得早年写一个网络服务器,用new分配缓冲区,结果忘delete,内存飙升,服务器重启好几次。学费交了不少,现在分享给你们。
第一部分:内存基础知识
先从基础说起,别觉得简单,很多新手在这里栽跟头。C++程序的内存分成几块:栈(stack)、堆(heap)、静态区(static/global)、代码区(text)。我们重点聊栈和堆,因为动态内存主要在堆上。
1.1 栈内存 vs 堆内存
栈是自动管理的,函数调用时分配,退出时释放。大小有限,通常几MB,速度快,因为是连续的。变量像int a = 5; 就在栈上。
堆是动态的,你想分配多少就多少(系统允许范围内),但得自己管理。堆上内存是全局的,函数退出也不释放,除非你delete。
为什么用堆?栈太小,存大数组或不确定大小的数据不行。比如读文件,你不知道文件多大,就得动态分配。
性能对比:栈分配O(1),堆涉及系统调用,慢点。但堆灵活。
我早年写嵌入式代码,栈溢出过好几次,因为递归太深。教训:大对象放堆。
1.2 静态内存 vs 动态内存
静态是编译时确定的,像全局变量static int x;。动态是运行时new出来的。
静态简单,但不灵活。动态能根据输入调整大小。
示例:静态数组 vs 动态数组。
#include <iostream>
using namespace std;
int main() {
// 静态数组,固定大小
int staticArr[5] = {1,2,3,4,5};
// 动态数组,大小运行时决定
int size = 0;
cin >> size;
int* dynamicArr = new int[size];
for(int i=0; i<size; ++i) {
dynamicArr[i] = i+1;
}
cout << "Dynamic array: ";
for(int i=0; i<size; ++i) {
cout << dynamicArr[i] << " ";
}
cout << endl;
delete[] dynamicArr; // 别忘释放
return 0;
}
输入5,输出1 2 3 4 5。静态数组不能cin size。
注意,new int[size]是C++风格,malloc也行,但new调用构造函数。
第二部分:传统动态内存分配
C++继承了C的malloc/free,但推荐new/delete,因为类型安全,还调用构造/析构。
2.1 new 和 delete 的基础用法
new分配内存并构造对象,delete释放并析构。
- 单对象:int* p = new int(5); delete p;
- 数组:int* arr = new int[10]; delete[] arr; // 注意[],不然只析构第一个
new抛bad_alloc异常如果失败,可用new(nothrow)返回nullptr。
示例:简单类对象。
#include <iostream>
using namespace std;
class MyClass {
public:
MyClass(int v) : value(v) { cout << "Constructed with " << v << endl; }
~MyClass() { cout << "Destructed" << endl; }
private:
int value;
};
int main() {
MyClass* obj = new MyClass(10);
// 用obj
delete obj;
MyClass* arr = new MyClass[3]{1,2,3}; // C++11初始化
delete[] arr;
return 0;
}
输出:Constructed with 10 Destructed Constructed with 1 Constructed with 2 Constructed with 3 Destructed (x3)
见没,new调用构造,delete调用析构。malloc就不行,得手动。
2.2 malloc/free 和 calloc/realloc
malloc(size)分配字节,返回void*,需强转。
free(ptr)释放。
calloc(num, size)分配并清零。
realloc(ptr, new_size)调整大小,复制数据。
为什么用new/delete? 类型安全,无需sizeof,自动构造/析构。
但在C代码或低级操作,用malloc。
示例:用malloc模拟动态数组。
#include <iostream>
#include <cstdlib> // malloc
using namespace std;
int main() {
int* ptr = (int*)malloc(5 * sizeof(int));
if(ptr == nullptr) {
cout << "Allocation failed" << endl;
return 1;
}
for(int i=0; i<5; ++i) {
ptr[i] = i*10;
}
cout << "Values: ";
for(int i=0; i<5; ++i) {
cout << ptr[i] << " ";
}
cout << endl;
// 调整大小
int* newPtr = (int*)realloc(ptr, 10 * sizeof(int));
if(newPtr) ptr = newPtr;
for(int i=5; i<10; ++i) {
ptr[i] = i*10;
}
cout << "After realloc: ";
for(int i=0; i<10; ++i) {
cout << ptr[i] << " ";
}
cout << endl;
free(ptr);
return 0;
}
输出:Values: 0 10 20 30 40 After realloc: 0 10 20 30 40 50 60 70 80 90
realloc方便,但可能移动内存,旧指针失效。
我用malloc在性能敏感的循环,因为new慢点(不过微乎其微)。
2.3 placement new
在指定地址构造对象,像new (addr) Type(args);
用于内存池或自定义分配。
示例:在缓冲区构造。
#include <iostream>
#include <new> // placement new
using namespace std;
int main() {
char buffer[sizeof(int) * 2]; // 静态缓冲
int* p1 = new (buffer) int(42); // 在buffer[0]构造
int* p2 = new (buffer + sizeof(int)) int(100);
cout << *p1 << " " << *p2 << endl;
p1->~int(); // 手动析构
p2->~int(); // 不delete,因为没分配
return 0;
}
输出:42 100
placement new不分配,只构造。用于优化。
第三部分:动态内存常见问题
内存管理坑多,漏了、碎了、悬了。
3.1 内存泄漏
分配了没释放,内存越来越少。
原因:忘delete,异常抛出前没释放,循环分配。
检测:用valgrind或Windows的CRT debug。
示例:泄漏演示。
void leakyFunction() {
int* p = new int[1000];
// 忘delete
}
int main() {
for(int i=0; i<10000; ++i) {
leakyFunction();
}
// 内存爆
return 0;
}
跑这个,内存飙升。修复:加delete[] p;
我项目中用RAII(资源获取即初始化)避免,像scope_guard。
3.2 悬垂指针和野指针
delete后指针还指向那里,用了UB(未定义行为)。
野指针:未初始化指针。
示例:
int main() {
int* p = new int(5);
delete p;
*p = 10; // 悬垂,崩溃或诡异
int* wild;
*wild = 20; // 野指针,危险
return 0;
}
修复:delete后设p=nullptr; 初始化指针。
3.3 内存碎片
频繁分配/释放,小块碎片多,大块分配失败。
外部碎片:空闲块散布。内部:分配多于请求。
解决:内存池,用大块预分配。
3.4 双重释放和无效释放
delete两次或delete栈指针,UB。
用nullptr检查。
第四部分:智能指针(C++11起)
智能指针是救星,自动管理。
4.1 unique_ptr:独占所有权
unique_ptr<T> up(new T); 自动delete。
不可拷贝,可move。
示例:
#include <iostream>
#include <memory> // unique_ptr
using namespace std;
class Demo {
public:
Demo() { cout << "Created" << endl; }
~Demo() { cout << "Destroyed" << endl; }
};
int main() {
{
unique_ptr<Demo> up(new Demo);
// 用up.get()取裸指针
} // 自动销毁
unique_ptr<int[]> arr(new int[5]);
for(int i=0; i<5; ++i) arr[i] = i;
// 自动delete[]
auto up2 = make_unique<Demo>(); // C++14,安全
return 0;
}
输出:Created Destroyed Created Destroyed
make_unique避免new异常时泄漏。
我用unique_ptr在函数返回动态对象。
4.2 shared_ptr:共享所有权
引用计数,计数0时delete。
shared_ptr<T> sp(new T);
可拷贝。
示例:
#include <iostream>
#include <memory>
using namespace std;
int main() {
shared_ptr<int> sp1 = make_shared<int>(42);
{
shared_ptr<int> sp2 = sp1; // 计数2
cout << *sp2 << " use_count: " << sp1.use_count() << endl;
} // 计数1
cout << "After scope: " << sp1.use_count() << endl;
return 0;
}
输出:42 use_count: 2 After scope: 1
make_shared高效,一次分配。
小心循环引用:A持B的shared,B持A的,永不释放。用weak_ptr。
4.3 weak_ptr:弱引用
不增计数,检查expired()。
示例:打破循环。
struct Node {
shared_ptr<Node> next;
weak_ptr<Node> prev; // 用weak避免循环
~Node() { cout << "Node destroyed" << endl; }
};
int main() {
auto n1 = make_shared<Node>();
auto n2 = make_shared<Node>();
n1->next = n2;
n2->prev = n1; // weak不增计
// 作用域结束,销毁
return 0;
}
输出:Node destroyed x2
weak_ptr.lock()得shared_ptr。
智能指针让我代码安全多了,早年手动delete到处是bug。
第五部分:自定义内存分配器
STL容器支持allocator,自定义分配。
5.1 allocator接口
需实现allocate/deallocate等。
示例:简单线性分配器。
#include <iostream>
#include <vector>
#include <memory>
using namespace std;
template<typename T>
class LinearAllocator {
public:
using value_type = T;
LinearAllocator(size_t size) : buffer(new char[size * sizeof(T)]), current(buffer), end(buffer + size * sizeof(T)) {}
~LinearAllocator() { delete[] buffer; }
T* allocate(size_t n) {
if(current + n * sizeof(T) > end) throw bad_alloc();
T* p = reinterpret_cast<T*>(current);
current += n * sizeof(T);
return p;
}
void deallocate(T*, size_t) {} // 不释放,线性
private:
char* buffer;
char* current;
char* end;
};
int main() {
LinearAllocator<int> alloc(10);
vector<int, LinearAllocator<int>> v(alloc);
for(int i=0; i<5; ++i) v.push_back(i);
for(int x : v) cout << x << " ";
cout << endl;
return 0;
}
输出:0 1 2 3 4
线性分配器快,但不释放。用于临时。
5.2 内存池
预分配块链表,分配从池取。
优点:减少碎片,快。
我用内存池在游戏渲染,分配小对象。
示例:简单池。
#include <iostream>
#include <vector>
using namespace std;
template<typename T, size_t BlockSize = 4096>
class MemoryPool {
public:
MemoryPool() : currentBlock(nullptr), currentSlot(nullptr), lastSlot(nullptr) {}
~MemoryPool() {
auto block = currentBlock;
while(block) {
auto prev = block->next;
delete[] reinterpret_cast<char*>(block);
block = prev;
}
}
T* allocate() {
if(currentSlot >= lastSlot) allocateBlock();
return reinterpret_cast<T*>(currentSlot++);
}
void deallocate(T* p) {} // 池不释放单个
private:
union Slot_ {
T element;
Slot_* next;
};
struct Block_ {
Block_* next;
char data[BlockSize * sizeof(Slot_)];
};
Block_* currentBlock;
char* currentSlot;
char* lastSlot;
void allocateBlock() {
auto newBlock = reinterpret_cast<Block_*>(new char[sizeof(Block_)]);
newBlock->next = currentBlock;
currentBlock = newBlock;
currentSlot = newBlock->data;
lastSlot = currentSlot + BlockSize * sizeof(Slot_);
}
};
int main() {
MemoryPool<int> pool;
vector<int*> ints;
for(int i=0; i<100; ++i) {
int* p = pool.allocate();
*p = i;
ints.push_back(p);
}
cout << "First few: ";
for(int i=0; i<5; ++i) {
cout << *ints[i] << " ";
}
cout << endl;
return 0;
}
这个池分配快,适合小对象频繁。
第六部分:内存优化技巧
6.1 避免不必要分配
用栈或静态如果可能。小对象用值语义。
6.2 对象池
类似内存池,但复用对象。
用于expensive构造的对象,如线程。
6.3 缓存对齐
分配时对齐缓存线(64字节),减少false sharing。
用aligned_alloc (C++17)。
示例:
#include <iostream>
#include <memory>
using namespace std;
int main() {
void* p = aligned_alloc(64, 128); // 64字节对齐
if(p) {
cout << "Aligned alloc success" << endl;
free(p);
}
return 0;
}
6.4 小对象优化(SOO)
如string在栈存小串。
自定义类可类似。
6.5 内存压缩
用bitfield或union节省。
第七部分:调试和工具
7.1 Valgrind
Linux神器,跑valgrind ./a.out 查泄漏、越界。
7.2 AddressSanitizer (ASan)
编译加-fsanitize=address,查越界、use-after-free。
我用ASan救过生产bug。
7.3 CRT debug (Windows)
_set_new_handler设置handler。
7.4 自定义operator new/delete
全局重载,日志分配。
示例:
#include <iostream>
#include <new>
using namespace std;
void* operator new(size_t size) {
cout << "Allocating " << size << " bytes" << endl;
return malloc(size);
}
void operator delete(void* p) noexcept {
cout << "Deallocating" << endl;
free(p);
}
int main() {
int* p = new int;
delete p;
return 0;
}
输出:Allocating 4 bytes Deallocating
用于追踪。
第八部分:C++标准演进
8.1 C++11:智能指针、make_shared
革命性,RAII普及。
8.2 C++14:make_unique
补齐unique。
8.3 C++17:aligned_new、std::byte
更好支持对齐。
8.4 C++20:std::span、std::jthread
span视图内存,jthread自动join。
未来C++23有更多,如std::mdspan。
我从C++98到20,感觉内存管理越来越友好。
常见错误与教训
- 忘delete[]数组,只delete单对象。
- new抛异常,没catch。
- 指针所有权不明,多个delete。
- 虚析构忘写,delete基类指针漏子类。
- 拷贝时深拷贝指针。
教训:多用智能指针,少手动。
结语
动态内存管理是C++的核心技能,掌握它,你的代码会更健壮。我这些年从手动new/delete到智能指针,再到自定义池,程序从崩溃到稳定。建议新手从基础练起,多用工具调试。欢迎评论你的经历!

1560

被折叠的 条评论
为什么被折叠?



