前言
C++赋予了开发者对内存的精细掌控权。从栈的自动分配到堆的动态管理,理解其内存模型是编写高效、安全程序的核心。掌握这些机制,方能避免内存泄漏与悬空指针,真正释放这门语言的力量。
1. 内存分布
1.1 代码演示及存储位置分析
首先看一段包含了全局变量,静态变量,局部变量以及动态内存分配的演示代码:
cpp
int globalVar = 1;//全局变量,存储在数据段
static int staticGlobalVar = 1;//静态全局变量,存储在数据段
void Test()
{
static int staticVar = 1;//静态局部变量,存储在数据段
int localVar = 1;//局部变量,存储在栈上
int num1[10] = { 1, 2, 3, 4 };//局部整型数组,存储在栈上
char char2[] = "abcd";//局部字符数组,存储在栈上
const char* pChar3 = "abcd";//指针变量(const:具有常性),用于存储字符串常量的地址
//动态内存分配(堆上)
int* ptr1 = (int*)malloc(sizeof(int) * 4);
int* ptr2 = (int*)calloc(4, sizeof(int));
int* ptr3 = (int*)realloc(ptr2, sizeof(int) * 4);
free(ptr1);
free(ptr3);
}
1.2 内存的区域划分
内存主要划分为栈,堆,数据段(静态区),代码段(常量区),内存映射段(非本节重点,暂不讨论),各个区域的功能与变量的存储规则如下:
1.栈 :用于存储函数的局部变量,函数参数,返回地址 等。由编译器自动分配和释放,生命周期和函数的调用周期保持一致效率极高。内存地址从高向低分配(向下增长)。
2.堆 :用于程序运行时进行动态内存分配的区域 (如new/malloc)。由程序员手动申请和释放(或由垃圾回收机制管理),其生命周期由程序员控制,但管理不当容易导致内存泄漏。内存地址从低向高分配(向上增长)。
3.数据段:
- data段(初始化数据区) :存储已初始化的全局变量和静态变量(static)。
- bss段(未初始化数据区) :存储未初始化或初始化为0的全局变量和静态变量。程序加载时由操作系统初始化为零。
4.代码段:
- text段 :存储程序执行的机器指令(即代码本身),通常是只读的。
- 常量区 :通常位于代码段内,存储字符串常量 和
const修饰的全局/静态常量。只读。5.内存映射段 :内核将文件(如动态库)直接映射到此区域,也用于创建匿名映射以作为共享内存 或大块内存分配(如某些
malloc实现)。
基于上面讲述的规则,完成下面的选择题:
(选项:A. 栈 B. 堆 C. 数据段 D. 代码段)
-
globalVar(全局变量)→ C
-
staticGlobalVar(静态全局变量)→ C
-
staticVar(静态局部变量)→ C(静态变量无论是否在函数内,均存于数据段)
-
localVar(局部变量)→ A
-
num1(局部数组)→ A(数组是局部变量的一种,存储在栈上)
-
char2(局部字符数组)→ A(数组本身是局部变量,存于栈上)
-
*char2(数组内容)→ A(\"abcd\" 被拷贝到栈上的数组中,内容存于栈)
-
pChar3(指针变量)→ A(指针是局部变量,存于栈上)
-
*pChar3(指针指向的内容)→ D(指向字符串常量 \"abcd\",存于代码段)
-
ptr1(指针变量)→ A(指针是局部变量,存于栈上)
-
*ptr1(指针指向的内容)→ B(指向 malloc 分配的动态内存,存于堆上)
图解如下所示:

2. C语言中动态内存管理方式:malloc/calloc/realloc/free
在C语言中,进行动态内存管理主要通过这四个函数:malloc,calloc,realloc和free,接下来为大家介绍这四个函数的功能实现。
2.1 动态内存管理函数功能实现
(1) malloc:最基础的动态内存分配
-
函数原型 :
void* malloc(size_t size); -
功能 :从堆 上申请
size字节的连续内存,不初始化内存内容(内存中保留随机值)。 -
返回值 :成功返回指向内存的
void*指针,失败返回NULL。 -
用法示例:
cpp
// 申请4个int大小的内存(int占4字节,共16字节)
int* p = (int*)malloc(4 * sizeof(int));
if (p == NULL) { // 必须判空,防止内存分配失败
perror("malloc failed");
return 1;
}
// 使用内存(如赋值、访问)
p[0] = 10; p[1] = 20;
// 后续需手动释放(配合free)
(2) calloc:带初始化的动态内存分配
-
函数原型 :
void* calloc(size_t num, size_t size); -
功能 :从堆 上申请
num个size字节的连续内存(总大小num*size),并自动将所有字节初始化为0。 -
返回值 :成功返回指向内存的
void*指针,失败返回NULL。 -
用法示例:
cpp
// 申请4个int大小的内存(共16字节),并初始化为0
int* p = (int*)calloc(4, sizeof(int));
if (p == NULL) {
perror("calloc failed");
return 1;
}
// 此时p[0]~p[3]均为0,可直接使用
p[0] = 100; // 覆盖初始化的0
// 后续需手动释放(配合free)
(3) realloc:动态调整已分配内存的大小
-
函数原型 :
void* realloc(void* ptr, size_t new_size); -
功能 :调整之前通过
malloc/calloc/realloc分配 的内存块的大小(可扩大/缩小)。若原内存块后有足够空间,直接扩展;否则会新申请一块内存 ,将原内存内容拷贝过去,再释放原内存。 -
返回值 :成功返回指向新内存的
void*指针(可能与原ptr相同,也可能不同),失败返回NULL(原内存块仍有效,需注意判空)。 -
用法示例:
cpp
int* p = (int*)malloc(4 * sizeof(int)); // 初始申请4个int
if (p == NULL) { /* 错误处理 */ }
// 现在需要扩容为6个int
int* new_p = (int*)realloc(p, 6 * sizeof(int));
if (new_p == NULL) {
// realloc失败,原p仍有效,需处理(如释放p或继续使用)
free(p);
perror("realloc failed");
return 1;
}
p = new_p; // 更新指针为新内存地址
p[4] = 500; p[5] = 600; // 使用扩容后的内存
// 后续需手动释放(配合free)
(4) free:释放动态分配的内存
-
函数原型 :void free(void ptr);*
-
功能 :释放之前通过
malloc/calloc/realloc分配 的堆内存,将其归还给系统。若ptr为NULL,free无操作(安全)。 -
注意 :释放后,
ptr成为悬空指针 (指向已释放的内存),建议立即置为NULL,避免误用。 -
用法示例:
cpp
int* p = (int*)malloc(4 * sizeof(int));
if (p == NULL) { /* 错误处理 */ }
// 使用内存...
free(p); // 释放内存
p = NULL; // 置空,避免悬空指针
2.2 核心区别总结
|---------|-------|------|-----------------|
| 函数 | 初始化 | 内存调整 | 典型场景 |
| malloc | 不初始化 | 不支持 | 快速申请未初始化内存 |
| calloc | 初始化为0 | 不支持 | 申请需初始化的内存(数组清零) |
| realloc | 依赖原内存 | 支持 | 动态调整内存大小(扩容/缩容) |
| free | 无 | 无 | 释放动态分配的内存 |
2.3 常见面试题
- malloc/calloc/ralloc的区别?
2.malloc的实现原理?
3. C++内存管理方式
C语言内存管理方式在C++中可以继续使用,但有些地方就无能为力,而且使用起来比较麻烦,因此C++又提出了自己的内存管理方式:通过new和delete操作符进行动态内存管理。
3.1 new/delete操作内置类型
cpp
#include<iostream>
using namespace std;
int main()
{
//申请单个整型变量的内存空间
int* p1 = new int;
//申请10个连续的整型变量的内存空间
int* p2 = new int[10];
delete p1;//销毁单个整型变量的内存空间
delete[]p2;//销毁10个连续整型变量的内存空间
//申请空间+初始化
int* p3 = new int(0);
int* p4 = new int[10] {0};
int* p5 = new int[10] {1, 2, 3, 4, 5};
//销毁
delete p3;
delete[]p4;
delete[]p5;
return 0;
}
调试结果如下:


3.2 new/delete操作自定义类型
cpp
class A
{
public:
A(int a1 = 0)
:_a1(a1)
{
cout << "A(int a1 = 0)" << endl;
}
A(const A& aa)
:_a1(aa._a1)
{
cout << "A(const A& aa)" << endl;
}
A& operator=(const A& aa)
{
cout << "A& operator=(const A& aa)" << endl;
if (this != &aa)
{
_a1 = aa._a1;
}
return *this;
}
~A()
{
//delete _ptr;
cout << "~A()" << endl;
}
void Print()
{
cout << "A::Print->" << _a1 << endl;
}
A& operator++()
{
_a1 += 100;
return *this;
}
private:
int _a1 = 1;
};
int main()
{
A* p1 = new A;
A* p2 = new A(1);
delete p1;
delete p2;
return 0;
}
运行结果如下:

可以观察到,当用new和delete操作自定义类型时,会自动调用相应的构造函数和析构函数,内置类型是不存在这个操作的。知道这个特点之后,可以利用这一点直接手动构造一个链表,就会方便很多:
cpp
struct ListNode
{
int val;
ListNode* next;
ListNode(int x)
:val(x)
, next(nullptr)
{}
};
int main()
{
ListNode* n1 = new ListNode(1);
ListNode* n2 = new ListNode(1);
ListNode* n3 = new ListNode(1);
ListNode* n4 = new ListNode(1);
n1->next = n2;
n2->next = n3;
n3->next = n4;
return 0;
}
所以在很多C++的OJ题目中往往会包含相应的构造函数:

当然,在上面的类中,所展示的构造函数属于一个参数的默认构造函数,当构造函数是两个甚至两个以上参数的默认构造函数时,new/delete操作符的使用方法如下:
cpp
//两个参数的默认构造函数
class A
{
public:
A(int a1 = 0, int a2 = 0)
:_a1(a1)
, _a2(a2)
{
cout << "A(int a1 = 0, int a2 = 0)" << endl;
}
A(const A& aa)
:_a1(aa._a1)
{
cout << "A(const A& aa)" << endl;
}
A& operator=(const A& aa)
{
cout << "A& operator=(const A& aa)" << endl;
if (this != &aa)
{
_a1 = aa._a1;
}
return *this;
}
~A()
{
//delete _ptr;
cout << "~A()" << endl;
}
void Print()
{
cout << "A::Print->" << _a1 << endl;
}
A& operator++()
{
_a1 += 100;
return *this;
}
private:
int _a1 = 1;
int _a2 = 1;
};
int main()
{
A* p1 = new A(1);
A* p2 = new A(2,2);
A* p3 = new A[3];
return 0;
}
运行结果如下:

可以看到new几次就可以调用几次构造函数,当然这是建立在默认构造的函数的基础上的,如果构造函数不是默认构造函数(无参构造函数)时,比如半缺省参数的构造函数时,那么对连续自定义类型空间初始化时就必须要进行传参:
第一种写法:有名对象的拷贝构造
cpp
//不是默认构造函数
class A
{
public:
A(int a1, int a2 = 0)
:_a1(a1)
, _a2(a2)
{
cout << "A(int a1 = 0, int a2 = 0)" << endl;
}
A(const A& aa)
:_a1(aa._a1)
{
cout << "A(const A& aa)" << endl;
}
A& operator=(const A& aa)
{
cout << "A& operator=(const A& aa)" << endl;
if (this != &aa)
{
_a1 = aa._a1;
}
return *this;
}
~A()
{
//delete _ptr;
cout << "~A()" << endl;
}
void Print()
{
cout << "A::Print->" << _a1 << endl;
}
A& operator++()
{
_a1 += 100;
return *this;
}
private:
int _a1 = 1;
int _a2 = 1;
};
int main()
{
A aa1(1, 1);
A aa2(2, 2);
A aa3(3, 3);
A* p3 = new A[3]{aa1,aa2,aa3};
return 0;
}

从运行结果可以看出,这次调用的是拷贝构造。但这种方法并不好。
第二种:利用匿名对象,编译器会进行优化
cpp
int main()
{
A* p4 = new A[3]{ A(1,1),A(2,2),A(3,3) };
return 0;
}

第三种写法:利用多参数的隐式类型转换
cpp
int main()
{
A* p4 = new A[3]{ {1,1},{2,2},{3,3} };
return 0;
}

由此可以感受到默认构造函数的重要性!!!
3.3 注意事项
**1.**申请和释放单个元素的空间,使用new和delete操作符,申请和释放连续的空间时,使用new[]和delete[],需要搭配使用,如果不匹配会导致内存泄漏或程序崩溃(尤其是对于自定义类型)。
-
在申请自定义类型的空间时,new会自动调用构造函数,delete会自动调用析构函数,而malloc和free不会。
-
当使用new申请动态空间出现异常时,可以对程序进行捕获异常,写法如下,要学会这种写法:
cpp
int main()
{
try
{
// throw try/catch
void* p1 = new char[1024 * 1024 * 1024];
cout << p1 << endl;
void* p2 = new char[1024 * 1024 * 1024];
cout << p2 << endl;
void* p3 = new char[1024 * 1024 * 1024];
cout << p3 << endl;
}
catch (const exception& e)
{
cout << e.what() << endl;
}
return 0;
}
运行结果:

只要一行代码发生错误,就会直接跳转到catch行进行错误提醒的输出。