堆与栈
进程地址空间如下:
数据段和BSS段
int data[] = {1,2}; // 数据段
int big_data[10000] = {};// BSS
int main(){
int A[] = {1,2,3}; // 栈空间
}
注意数据段/BSS段比栈空间大,但是更慢
栈和堆内存一览:
栈 | 堆 | |
---|---|---|
内存组织 | 连续的(先进后出) | 一次分配内的内存连续,分配间分段 |
最大大小 | 很小 | 整个系统的内存 |
如果超出 | 程序会崩溃 | 异常或空指针 |
分配 | 编译时 | 运行时 |
局部性 | 高 | 低 |
线程视角 | 每个线程都有自己的栈 | 线程间共享 |
栈内存:
一个局部变量要么在栈内存要么在cpu寄存器中
int x = 3; // 不在栈上,在数据段
struct A {
int k; // 取决于A的实例在哪
};
int main(){
int y = 3; // 栈
char z[] = "abc"; // 栈
A a; // 栈
void* ptr = malloc(4); // 变量“ptr”在栈上
}
栈内存的组织可以获得更高的性能,但是它也是有限的。
存储在栈上的数据类型:
- 局部变量:局部作用域的变量
- 函数参数:从调用者传入函数的数据
- 返回地址:从函数传入调用者的数据
- 编译器临时变量:编译器特定的指令
- 中断上下文
每个存在栈上的对象在作用域外不再有效,例如
int* f(){
int array[3] = {1,2,3};
return array;
}
int* ptr = f();
cout<<ptr[0]; // 非法
void g(bool x){
const char* str = "abc";
if(x){
char xyz[] = "xyz";
str = xyz;
}
cout << str; // 如果x为真,为非法访问
}
堆内存:
new/new[] 和 delete/delete[]是c++关键字,可以执行动态内存分配以及释放,并且运行时对象创建和析构。而malloc和free是c函数,并且它们只能分配和释放内存块。
new和delete的优点:
- 使用语言关键字,更加安全
- 返回类型:new会返回精确的数据类型,而malloc只会返回void*
- 失败:new会抛出异常,而malloc会返回一个NULL指针
- 分配的size:使用new关键字,编译器会计算所需要的字节数;而malloc需要手动计算
- 初始化:new可以在分配时进行初始化
- 多态:含有虚函数的对象必须使用new分配来初始化虚表指针
一些例子如下:
// 分配单个元素
int* value = new int;
// 分配N个元素
int* array = new int[N];
// 分配N个结构
MyStruct* array = new MyStruct[N];
//分配并且0初始化
int* array = new int[N]();
// 释放单个元素
int* value = new int;
delete value;
// 释放N个元素
int* value = new int[N];
delete[] value;
注意一些基本规则:
- 每个通过malloc分配的对象必须通过free释放
- 每个通过new分配的对象必须通过delete释放
- 每个通过new[]分配的对象必须通过delete[]释放
- malloc,new,new[]在成功例子不会返回空指针,除非0-size的分配
- free,delete和delete[]应在在空指针上不会产生错误
混合使用new,new[],malloc可能会出现UB
2维内存分配:
// 在栈上很简单,编译期知道维度
int A[3][4];
// 运行时才知道维度,old version
int** A = new int*[3];
for(int i = 0;i<3;i++)
A[i] = new int[4];
for(int i = 0;i<3;i++)
delete[] A[i];
delete[] A;
// C++ 11
auto A = new int[3][4];
int n = 3;
auto B = new int[n][4];
// auto C = new int[n][n]; // error
delete[] A;
非分配放置(non-allocating placement)
(ptr) type允许显式地确定对象的位置
// 栈内存
char buffer[8];
int* x = new (buffer) int;
short* y =new (x+1) short[2];
// 不需要释放x和y
// 堆内存
unsigned* buffer2 = new unsigned[2];
double* z = new (buffer2) double;
delete[] buffer2; // ok
// delete[] z; // ok,but bad
注意非平凡对象的放置分配需要在运行时显式调用对象的析构函数,因为不能检测到何时对象超出作用域。
struct A {
~A() {cout<< "des";}
};
char buffer[10];
auto x = new (buffer) A();
// delete x; // error
x->~A();
非抛出分配(non-throwing allocation)
new操作允许非抛出分配,通过传入std::nothrow对象。它返回NULL指针而不是抛出std::bad_alloc异常,如果内存分配失败了
int* array = new (std::nothrow) int[very_large_size];
std::nothrow不意味着分配内存的对象不能自己抛出异常
struct A {
A() {throw std::runtime_error{};}
};
A* array = new (std::nothrow) A; // throw std::runtime_error{};
内存泄漏
程序不再使用的堆内存的实体却仍然占据着内存空间
问题:
- 非法的内存访问 -> 段错误/错误的结果
- 额外的内存消耗
int main(){
int* array = new int[10];
array = nullptr; // 内存泄漏
}
程序请求os分配的内存,os分配的内存是以虚拟页为粒度的,例如linux上为4KB。这意味着越界访问并不总是导致段错误,最坏的情况是带有UB的执行过程。
int* x = new int;
int num_iters = 4096 / sizeof(int);
for(int i = 0;i<num_iters;i++)
x[i] = 1;// ok