C++内存管理

内存管理

内存管理详解

内存分配方式

分配方式简介

在C++中,内存分为5个区:

  1. 栈:执行函数时,函数内部局部变量存储单元在栈上创建,结束时自动释放。效率很高,但存储容量有限。
  2. 堆:由 new 分配的内存块,编译器不会自动释放,需要应用程序对应的 delete 进行释放。如果没有释放,则程序运行结束后会由操作系统自动回收。
  3. 自由存储区:由 malloc 等分配的内存块,类似堆,由 free 结束自己的生命。
  4. 全局/静态存储区:存储全局变量和静态变量。
  5. 常量存储区:存放常量,不允许修改。
明确区分堆和栈
1
2
3
void f() {
int* p = new int[5];
}

在栈内存中存放了一个指向一块堆内存的指针 p。程序会先确定在堆中分配内存的大小,然后调用 operator new 分配内存,然后返回这块内存的首地址,放入栈中。

这里并没有释放内存,因此需要 delete[] p,告诉编译器删除的是数组,然后编译器根据Cookie信息去释放内存。

堆和栈的区别
  1. 管理方式:栈由编译器自动管理,无需手动控制;堆的释放工作由程序员控制,容易产生内存泄露。
  2. 空间大小:堆栈的内存都可以自己设定,堆内存一般不超过系统内存都是可以申请,栈内存个人测试默认值为8M。
  3. 碎片问题:对于堆而言,频繁的 new/delete 势必会造成内存空间的不连续,造成大量的碎片,使程序的效率降低;对于栈而言,则不会发生这样的情况,先进后出排列有序,以至于永远不可能有一个内存块从非栈顶位置弹出。
  4. 生长方向:对于堆而言,生长方向向上,向着内存地址增加的方向增长;对于栈而言,生长方向向下,向着内存地址减小的方向增长。
  5. 分配方式:堆都是动态分配的,没有静态分配的堆。栈有动态分配和静态分配两种,静态分配由编译器完成,比如局部变量的分配;动态分配由 alloca 函数进行分配,都由编译器进行释放。
  6. 分配效率:栈是机器系统提供的数据结构,计算机会在底层对栈提供支持,这就决定了栈的效率比较高。堆则是 C/C++ 函数库提供的,机制比较复杂,用到许多算法的实现内存的分配。显然,堆的效率比栈要低得多。

因此,更推荐尽量使用栈而不是堆。但是栈和堆相比并不灵活。如果需要分配大量内存空间,还是堆好一些。

控制 C++ 的内存分配

无论如何,一定要保守的使用内存分配。
一个防止堆破碎的通用方法是从不同固定大小的内存池中分配不同类型的对象。对每个类重载 newdelete 就提供了这样的控制。

重载全局的new和delete操作
1
2
3
4
5
6
7
8
9
void * operator new(size_t size) 
{
void *p = malloc(size);
return (p);
}
void operator delete(void *p);
{
free(p);
}

也可以对单个类的 new 和 delete 操作符重载。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class TestClass { 
public:
void * operator new(size_t size);
void operator delete(void *p);
// .. other members here ...
};
void *TestClass::operator new(size_t size)
{
void *p = malloc(size); // Replace this with alternative allocator
return (p);
}
void TestClass::operator delete(void *p)
{
free(p); // Replace this with alternative de-allocator
}
为单个类重载 new[] 和 delete[]

C++将对象数组的内存分配作为一个单独的操作,而不同于单个对象的内存分配。为了改变这种方式,同样需要重载 new[] 和 delete[]操作符。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
class TestClass { 
public:
void * operator new[ ](size_t size);
void operator delete[ ](void *p);
// .. other members here ..
};
void *TestClass::operator new[ ](size_t size)
{
void *p = malloc(size);
return (p);
}
void TestClass::operator delete[ ](void *p)
{
free(p);
}
int main(void)
{
TestClass *p = new TestClass[10];
// ... etc ...
delete[ ] p;
}

PS:对于多数 C++ 的实现,new[] 操作符中的个数参数是数组的大小加上额外的存储对象数目的一些字节。因此应该尽量避免分配对象数组,使内存分配策略简单。

常见的内存错误

  1. 内存分配未成功,却使用了它。
    • 如果是用 malloc 或 new 来申请内存,应该用 if(p==NULL) 或 if(p!=NULL)进行防错。
  2. 内存分配虽然成功,但未初始化就引用它。
    • 无论用何种方式创建数组,都要赋初值,即便是零值。
  3. 内存分配成功并且已经初始化,但操作越界。
    • 多发生在下标“多1”或“少1”。
  4. 忘记了释放内存,造成内存泄漏。
    • 动态内存的申请与释放必须配对,程序中 mallocfree 的使用次数一定要相同,否则肯定有错误(new/delete同理)。
  5. 释放了内存却继续使用它。
    • 程序过于复杂,难以分清是否释放了内存,此时应当重新设计数据结构,解决混乱。
    • return语句写错,不要返回指向“栈内存”的指针或引用,因为该内存在函数体结束时被自动销毁。
    • free或delete释放内存后,没有将指针设为NULL,导致“野指针”。

指针与数组对比

数组要么在静态存储区被创建(如全局数组),要么在栈上被创建。数组名对应着(而不是指向)一块内存,其地址和容量在生命周期内保持不变,只有数组的内容可以改变。

指针可以随时指向任何类型的内存块,它的特征是“可变”的,所以常用指针来操作动态内存,指针远比数组灵活,但也更危险。

修改数组内容
1
2
3
4
5
6
7
char a[] = “hello”; 
a[0] = ‘X’;
cout << a << endl;
// 可以拆分为 char s[] = "world"; char *p = str;
char *p = “world”; // 注意 p 指向常量字符串
p[0] = ‘X’; // 编译器不能发现该错误,但运行时产生[Bus error]
cout << p << endl;
内容复制与比较
1
2
3
4
5
6
7
8
9
10
11
12
// 数组… 
char a[] = "hello";
char b[10];
strcpy(b, a); // 不能用 b = a;
if(strcmp(b, a) == 0) // 不能用 if (b == a)

// 指针…
// 语句 p = a 并不能把 a 的内容复制给指针 p,而是把 a 的地址赋给了 p。
int len = strlen(a);
char *p = (char *)malloc(sizeof(char)*(len+1));
strcpy(p, a); // 不要用 p = a;
if(strcmp(p, a) == 0) // 不要用 if (p == a)
计算内存容量
1
2
3
4
5
char a[] = "hello world"; 
char *p = a;
cout<< sizeof(a) << endl; // 12 字节
// sizeof(p) 等价于 sizeof(char *),没有办法的值指针所指的内存容量,除非申请内存时就记住它
cout<< sizeof(p) << endl; // 4 字节

注意当数组作为函数的参数进行传递时,该数组自动退化为同类型的指针。

1
2
3
4
5
void Func(char a[100]) 
{
// 不论数组 a 的容量是多少,sizeof(a)始终等于 sizeof(char *)。
cout<< sizeof(a) << endl; // 4 字节而不是 100 字节
}

指针参数如何传递内存

1
2
3
4
5
6
7
8
9
10
void GetMemory(char *p, int num) 
{
p = (char *) malloc(sizeof(char) * num);
}
void Test(void)
{
char *str = NULL;
GetMemory(str, 100); // str 仍然为 NULL
strcpy(str, "hello"); // 运行错误
}

如果函数的参数是一个指针,不要指望用该指针去申请动态内存。编译器总是要为函数的每个参数制作临时副本,指针参数 p 的副本是 _p,编译器使 _p = p。如果函数体内的程序修改了_p 的内容,就导致参数 p 的内容作相应的修改。这就是指针可以用作输出参数的原因。_p 申请了新的内存,只是把 _p 所指的内存地址改变了,但是 p 丝毫未变。所以函数 GetMemory 并不能输出任何东西。事实上,每执行一次 GetMemory 就会泄露一块内存,因为没有用 free 释放内存。

如果非得要用指针参数去申请内存,那么应该改用“指向指针的指针”:

1
2
3
4
5
6
7
8
9
10
11
12
void GetMemory2(char **p, int num) 
{
*p = (char *)malloc(sizeof(char) * num);
}
void Test2(void)
{
char *str = NULL;
GetMemory2(&str, 100); // 注意参数是 &str,而不是 str
strcpy(str, "hello");
cout<< str << endl;
free(str);
}

比较好的方法是 传指针的引用

1
2
3
4
5
6
7
8
9
10
11
12
void GetMemory(char *&p, int num)
{
p = (char *)malloc(sizeof(char) * num);
}
void Test(void)
{
char *str = NULL;
GetMemory(str, 100);
strcpy(str, "hello");
cout << str << endl;
free(str);
}

可以用函数返回值来传递动态内存。这种方法更加简单:
1
2
3
4
5
6
7
8
9
10
11
12
13
char *GetMemory3(int num) 
{
char *p = (char *)malloc(sizeof(char) * num);
return p;
}
void Test3(void)
{
char *str = NULL;
str = GetMemory3(100);
strcpy(str, "hello");
cout<< str << endl;
free(str);
}

在使用返回值时,千万别返回 指向“栈内存” 的指针、引用,因为该内存在函数结束时 自动消亡 了,返回的指针是个野指针了。
1
2
3
4
5
6
7
8
9
10
11
char *GetString()
{
char p[] = "hello world"; //数组内容存储在栈区,函数结束时,会释放掉
return p;
}
void Test(void)
{
char *str = NULL;
str = GetString(); //因为非配的内存早已释放掉,此时的str是个野指针,内容是垃圾
cout << str << endl;
}

在函数中不定义数组,定义指针:
1
2
3
4
5
6
7
8
9
10
11
char *GetString()
{
char *p = "hello world"; //数组内容存储在静态区,函数结束时,不会释放掉
return p;
}
void Test(void)
{
char *str = NULL;
str = GetString();
cout << str << endl;
}

杜绝“野指针”

“野指针”不是 NULL 指针,是指向“垃圾”内存的指针。“野指针”是很危险的,if 语句对它不起作用。出现的原因有:

  1. 指针变量没有被初始化。任何指针变量刚被创建时不会自动成为 NULL 指针,它的缺省值是随机的。所以,指针变量在创建的同时应当被初始化,要么将指针设置为 NULL,要么让它指向合法的内存。
  2. 指针 p 被 free 或者 delete 之后,没有置为 NULL,让人误以为 p 是个合法的指针。
  3. 指针操作超越了变量的作用域范围。

有了 malloc/free 为什么还要 new/delete?

malloc 与 free 是 C++/C 语言的标准库函数,new/delete 是 C++ 的运算符。它们都可用于申请动态内存和释放内存。
对于非内部数据类型的对象而言,光用 malloc/free 无法满足动态对象的要求。对象在创建的同时要自动执行构造函数,对象在消亡之前要自动执行析构函数。由于 malloc/free 是库函数而不是运算符,不在编译器控制权限之内,不能够把执行构造函数和析构函数的任务强加于 malloc/free
而C++程序经常要调用 C 函数,而 C 程序只能用 malloc/free 管理动态内存,因此不能只用 new/delete

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
class Obj 
{
public :
Obj(void){ cout << “Initialization” << endl; }
~Obj(void){ cout << “Destroy” << endl; }
void Initialize(void){ cout << “Initialization” << endl; }
void Destroy(void){ cout << “Destroy” << endl; }
};
void UseMallocFree(void)
{
Obj *a = (obj *)malloc(sizeof(obj)); // 申请动态内存
a->Initialize(); // 初始化
//…
a->Destroy(); // 清除工作
free(a); // 释放内存
}
void UseNewDelete(void)
{
Obj *a = new Obj; // 申请动态内存并且初始化
//…
delete a; // 清除并且释放内存
}

内存耗尽怎么办?

如果在申请动态内存时找不到足够大的内存块,mallocnew 将返回 NULL 指针,表示内存申请失败。
处理“内存耗尽”问题,一般可以通过判断指针是否为 NULL,是的话用 returnexit(1) 终止整个程序的运行,也可以自己为 new 和 molloc 设置异常处理函数。
如果一个函数内有多处需要申请动态内存,那么应该用 exit(1) 及时终止程序。

作者

Benboby

发布于

2020-11-02

更新于

2021-02-09

许可协议

Your browser is out-of-date!

Update your browser to view this website correctly.&npsb;Update my browser now

×