目录
[1.1 内存区域划分](#1.1 内存区域划分)
[1.2 代码示例分析](#1.2 代码示例分析)
[2.1 malloc](#2.1 malloc)
[2.2 calloc](#2.2 calloc)
[2.3 realloc](#2.3 realloc)
[2.4 free](#2.4 free)
[3.1 new/delete操作内置类型](#3.1 new/delete操作内置类型)
[3.2 new/delete操作自定义类型](#3.2 new/delete操作自定义类型)
[四、operator new与operator delete函数](#四、operator new与operator delete函数)
[4.1 原理](#4.1 原理)
[4.2 代码示例](#4.2 代码示例)
[5.1 内置类型](#5.1 内置类型)
[5.2 自定义类型](#5.2 自定义类型)
[六、定位new表达式(placement - new)](#六、定位new表达式(placement - new))
[6.1 概念](#6.1 概念)
[6.2 使用格式](#6.2 使用格式)
[6.3 使用场景](#6.3 使用场景)
[7.1 内存泄漏概念](#7.1 内存泄漏概念)
[7.2 内存泄漏分类](#7.2 内存泄漏分类)
[7.3 如何检测内存泄漏](#7.3 如何检测内存泄漏)
[7.4 如何避免内存泄漏](#7.4 如何避免内存泄漏)
[8.1 malloc/calloc/realloc的区别](#8.1 malloc/calloc/realloc的区别)
[8.2 malloc的实现原理](#8.2 malloc的实现原理)
引言
在C和C++编程中,内存管理是一个至关重要的话题。合理的内存管理可以确保程序高效、稳定地运行,避免内存泄漏等问题。本文将深入探讨C/C++内存管理的各个方面,结合代码示例详细讲解,帮助大家更好地理解和掌握这一关键技能。
一、C/C++内存分布
1.1 内存区域划分
C/C++程序的内存通常分为以下几个区域:
**- 栈(Stack):**用于存储局部变量、函数参数等。栈向下增长,具有自动管理内存的特点,变量生命周期随函数结束而结束。例如:
cpp
cpp
void Test() {
int localVar = 1; // localVar存储在栈中
}
**- 堆(Heap):**用于动态内存分配,由程序员手动申请和释放。例如使用 malloc 、 calloc 、 realloc (C语言)或 new (C++)来分配内存,使用 free (C语言)或 delete (C++)来释放内存。
cpp
cpp
int* ptr = new int; // 在堆上分配一个int类型的空间
delete ptr; // 释放堆上分配的空间
**- 数据段(静态区):**存储全局变量和静态变量。全局变量和静态全局变量在程序启动时分配内存,程序结束时释放。
cpp
cpp
int globalVar = 1; // globalVar存储在数据段
static int staticGlobalVar = 1; // staticGlobalVar也存储在数据段
**- 代码段(常量区):**存放可执行代码和常量字符串等。常量字符串存储在这里,例如:
cpp
cpp
const char* pChar3 = "abcd"; // "abcd"存储在代码段
1.2 代码示例分析
下面通过一段代码来具体分析变量在内存中的分布:
cpp
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[2] = "abcd";
const char* pChar3 = "abcd";
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);
}
- globalVar 、 staticGlobalVar 、 staticVar 存储在数据段。
- localVar 、 num1 、 char2 、 pChar3 (指针本身)存储在栈中。
- *ptr1 、 *ptr2 、 *ptr3 指向的内存区域在堆上。
**要点:**理解不同类型变量在内存中的存储位置,对于分析程序的内存使用情况和排查问题很有帮助。
**易错点:**容易混淆局部变量和动态分配内存的生命周期和释放方式。
二、C语言中动态内存管理方式
2.1 malloc
malloc 函数用于在堆上分配指定大小的内存块,其原型为:
cpp
c
void* malloc(size_t size);
例如,分配一个 int 类型大小的内存空间:
cpp
c
int* ptr = (int*)malloc(sizeof(int));
if (ptr == NULL) {
// 处理内存分配失败的情况
perror("malloc");
return;
}
// 使用ptr
free(ptr); // 使用完后释放内存
2.2 calloc
calloc 函数用于分配指定数量和大小的内存块,并将其初始化为0,原型为:
cpp
c
void* calloc(size_t num, size_t size);
比如分配4个 int 类型的内存空间:
cpp
c
int* ptr = (int*)calloc(4, sizeof(int));
if (ptr == NULL) {
perror("calloc");
return;
}
// 使用ptr
free(ptr);
2.3 realloc
realloc 函数用于调整已分配内存块的大小,原型为:
cpp
c
void* realloc(void* ptr, size_t size);
例如,调整之前分配的内存块大小:
cpp
c
int* ptr = (int*)malloc(sizeof(int) * 4);
if (ptr == NULL) {
perror("malloc");
return;
}
int* newPtr = (int*)realloc(ptr, sizeof(int) * 8);
if (newPtr == NULL) {
perror("realloc");
free(ptr);
return;
}
ptr = newPtr; // 更新指针
// 使用ptr
free(ptr);
2.4 free
free 函数用于释放通过 malloc 、 calloc 、 realloc 分配的内存,原型为:
cpp
c
void free(void* ptr);
**要点:**使用这些函数时,要确保内存分配成功,并且及时释放不再使用的内存,避免内存泄漏。
易错点:
-
对 NULL 指针调用 free 虽然不会出错,但这是不必要的操作,还可能掩盖真正的问题。
-
多次释放同一块内存会导致程序崩溃。
三、C++内存管理方式
3.1 new/delete操作内置类型
在C++中,可以使用 new 和 delete 操作符进行动态内存管理。
- 申请单个 int 类型空间:
cpp
cpp
int* ptr4 = new int;
delete ptr4;
- 申请单个 int 类型空间并初始化为10:
cpp
cpp
int* ptr5 = new int(10);
delete ptr5;
- 申请10个 int 类型的空间:
cpp
cpp
int* ptr6 = new int[10];
delete[] ptr6;
3.2 new/delete操作自定义类型
对于自定义类型(如类), new 会调用构造函数, delete 会调用析构函数。
cpp
cpp
class A {
public:
A(int a = 0) : _a(a) {
std::cout << "A():" << this << std::endl;
}
~A() {
std::cout << "~A():" << this << std::endl;
}
private:
int _a;
};
int main() {
A* p1 = (A*)malloc(sizeof(A)); // 仅分配内存,未调用构造函数
A* p2 = new A(1); // 分配内存并调用构造函数
free(p1); // 未调用析构函数
delete p2; // 调用析构函数并释放内存
return 0;
}
**要点:**使用 new 和 delete 时要注意配对使用,申请和释放单个元素用 new 和 delete ,申请和释放数组用 new[] 和 delete[] 。
**易错点:**容易忘记使用 delete[] 释放动态数组,导致内存泄漏或程序错误。
四、operator new与operator delete函数
4.1 原理
new 和 delete 是用户进行动态内存申请和释放的操作符,而 operator new 和 operator delete 是系统提供的全局函数。 new 在底层调用 operator new 来申请空间, delete 在底层通过 operator delete 来释放空间。
operator new 实际通过 malloc 来申请空间,如果 malloc 申请空间成功则直接返回;申请空间失败,尝试执行空间不足应对措施,如果用户未设置该措施则继续申请,否则抛异常。
operator delete 最终是通过 free 来释放空间。
4.2 代码示例
cpp
cpp
// operator new的简单实现示意
void* operator new(size_t size) {
void* p = malloc(size);
if (p == NULL) {
// 处理内存不足情况,这里简单抛异常
throw std::bad_alloc();
}
return p;
}
// operator delete的简单实现示意
void operator delete(void* p) {
if (p != NULL) {
free(p);
}
}
**要点:**了解 operator new 和 operator delete 的原理,有助于在深入理解 new 和 delete 操作符的底层实现,在自定义内存管理机制时也很有帮助。
**易错点:**在重载 operator new 和 operator delete 时,要确保实现的正确性,避免引入新的内存问题。
五、new和delete的实现原理
5.1 内置类型
对于内置类型, new 和 malloc , delete 和 free 基本类似。不同的是, new/delete 申请和释放的是单个元素的空间, new[] 和 delete[] 申请的是连续空间,而且 new 在申请空间失败时会抛异常, malloc 会返回 NULL 。
5.2 自定义类型
- new 的原理:
-
调用 operator new 函数申请空间。
-
在申请的空间上执行构造函数,完成对象的构造。
- delete 的原理:
-
在空间上执行析构函数,完成对象中资源的清理工作。
-
调用 operator delete 函数释放对象的空间。
- new T[N] 的原理:
-
调用 operator new[] 函数,在 operator new[] 中实际调用 operator new 函数完成N个对象空间的申请。
-
在申请的空间上执行N次构造函数。
- delete[] 的原理:
-
在释放的对象空间上执行N次析构函数,完成N个对象中资源的清理。
-
调用 operator delete[] 释放空间,实际在 operator delete[] 中调用 operator delete 来释放空间。
**要点:**理解不同情况下 new 和 delete 的实现原理,有助于正确使用它们来管理内存。
**易错点:**在涉及自定义类型的动态内存管理时,要确保构造函数和析构函数的正确调用,以及内存的正确申请和释放。
六、定位new表达式(placement - new)
6.1 概念
定位 new 表达式是在已分配的原始内存空间中调用构造函数初始化一个对象。
6.2 使用格式
cpp
cpp
new (place_address) type或者new (place_address) type(initializer-list)
其中 place_address 必须是一个指针, initializer-list 是类型的初始化列表。
6.3 使用场景
通常配合内存池使用。因为内存池分配出的内存没有初始化,所以如果是自定义类型的对象,需要使用定位 new 表达式进行显式调用构造函数进行初始化。
cpp
cpp
class A {
public:
A(int a = 0) : _a(a) {
std::cout << "A():" << this << std::endl;
}
~A() {
std::cout << "~A():" << this << std::endl;
}
private:
int _a;
};
int main() {
char* buffer = new char[sizeof(A)];
A* p = new (buffer) A(1); // 使用定位new表达式在buffer指向的内存上构造A对象
p->~A(); // 手动调用析构函数
delete[] buffer;
return 0;
}
**要点:**掌握定位 new 表达式的使用,可以在特定场景下灵活地进行对象的构造和内存管理。
**易错点:**使用定位 new 表达式构造对象后,需要手动调用析构函数来清理资源,否则会导致内存泄漏或资源未释放问题。
七、内存泄漏相关问题
7.1 内存泄漏概念
内存泄漏不是指内存在物理上的消失,而是应用程序分配某段内存后,由于设计错误,失去了对该段内存的控制,从而造成了内存的浪费。
7.2 内存泄漏分类
**- 堆内存泄漏(Heap leak):**程序执行中通过 malloc 、 calloc 、 realloc 、 new 等从堆中分配的内存,用完后未通过相应的 free 或 delete 删除,导致这部分空间无法再被使用。
**- 系统资源泄漏:**程序使用系统分配的资源(如套接字、文件描述符、管道等),没有使用对应的函数释放掉,导致系统资源浪费,严重可致系统效能减少、执行不稳定。
7.3 如何检测内存泄漏
- 在Windows下,可以使用 _CrtDumpMemoryLeaks 函数进行简单检测,该函数只报出大概泄漏了多少个字节,没有其他更准确的位置信息。示例如下:
cpp
cpp
int main() {
int* p = new int[10];
_CrtDumpMemoryLeaks();
return 0;
}
- 在Linux下,可以使用 valgrind 等工具进行内存泄漏检测。
7.4 如何避免内存泄漏
- 养成良好的编码规范,申请的内存空间及时匹配释放。
- 采用RAII(Resource Acquisition Is Initialization)思想或智能指针来管理资源。
- 一些公司内部规范使用内部实现的私有内存管理库,自带内存泄漏检测功能。
**要点:**了解内存泄漏的概念、分类、检测和避免方法,有助于编写出更健壮、稳定的程序。
**易错点:**在复杂程序中,容易忽略对动态分配内存的释放,特别是在异常处理路径中。
八、常见面试题
8.1 malloc/calloc/realloc的区别
**- malloc :**分配指定大小的内存块,不初始化内存内容,返回指向分配内存起始地址的指针。
**- calloc :**分配指定数量和大小的内存块,并将其初始化为0,返回指向分配内存起始地址的指针。
**- realloc :**调整已分配内存块的大小,可能会移动内存块的位置,返回调整后内存块的指针。
8.2 malloc的实现原理
malloc 的实现原理较为复杂,常见的实现方式是通过维护一个空闲内存块链表,当调用 malloc 时,从链表中寻找合适大小的空闲块进行分配,如果没有合适大小的块,可能会进行内存的合并或向操作系统申请更多内存。在glibc中, malloc 的实现涉及到多个数据结构和算法来管理内存,如 arena 、 bin 等。
总结
C/C++内存管理是一个复杂而又关键的知识点,涵盖了内存分布、动态内存管理函数和操作符、内存泄漏等多个方面。通过深入理解和不断实践,才能熟练掌握内存管理技巧,编写出高效、稳定、健壮的程序。希望本文能对大家在C/C++内存管理的学习和实践中有所帮助。