C/C++内存管理:从基础到进阶

目录

引言

一、C/C++内存分布

1.1 内存区域划分

1.2 代码示例分析

二、C语言中动态内存管理方式

2.1 malloc

2.2 calloc

2.3 realloc

2.4 free

三、C++内存管理方式

3.1 new/delete操作内置类型

3.2 new/delete操作自定义类型

四、operator new与operator delete函数

4.1 原理

4.2 代码示例

五、new和delete的实现原理

5.1 内置类型

5.2 自定义类型

六、定位new表达式(placement - new)

6.1 概念

6.2 使用格式

6.3 使用场景

七、内存泄漏相关问题

7.1 内存泄漏概念

7.2 内存泄漏分类

7.3 如何检测内存泄漏

7.4 如何避免内存泄漏

八、常见面试题

8.1 malloc/calloc/realloc的区别

8.2 malloc的实现原理

总结


引言

在C和C++编程中,内存管理是一个至关重要的话题。合理的内存管理可以确保程序高效、稳定地运行,避免内存泄漏等问题。本文将深入探讨C/C++内存管理的各个方面,结合代码示例详细讲解,帮助大家更好地理解和掌握这一关键技能。

一、C/C++内存分布

1.1 内存区域划分

C/C++程序的内存通常分为以下几个区域:

- 栈(Stack):用于存储局部变量、函数参数等。栈向下增长,具有自动管理内存的特点,变量生命周期随函数结束而结束。例如:

cpp

void Test() {

    int localVar = 1; // localVar存储在栈中

}

- 堆(Heap):用于动态内存分配,由程序员手动申请和释放。例如使用 malloc 、 calloc 、 realloc (C语言)或 new (C++)来分配内存,使用 free (C语言)或 delete (C++)来释放内存。

cpp

int* ptr = new int; // 在堆上分配一个int类型的空间

delete ptr; // 释放堆上分配的空间

- 数据段(静态区):存储全局变量和静态变量。全局变量和静态全局变量在程序启动时分配内存,程序结束时释放。

cpp

int globalVar = 1; // globalVar存储在数据段

static int staticGlobalVar = 1; // staticGlobalVar也存储在数据段

- 代码段(常量区):存放可执行代码和常量字符串等。常量字符串存储在这里,例如:

cpp

const char* pChar3 = "abcd"; // "abcd"存储在代码段

1.2 代码示例分析

下面通过一段代码来具体分析变量在内存中的分布:

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 函数用于在堆上分配指定大小的内存块,其原型为:

c

void* malloc(size_t size);

例如,分配一个 int 类型大小的内存空间:

c

int* ptr = (int*)malloc(sizeof(int));

if (ptr == NULL) {

    // 处理内存分配失败的情况

    perror("malloc");

    return;

}

// 使用ptr

free(ptr); // 使用完后释放内存

2.2 calloc

calloc 函数用于分配指定数量和大小的内存块,并将其初始化为0,原型为:

c

void* calloc(size_t num, size_t size);

比如分配4个 int 类型的内存空间:

c

int* ptr = (int*)calloc(4, sizeof(int));

if (ptr == NULL) {

    perror("calloc");

    return;

}

// 使用ptr

free(ptr);

2.3 realloc

realloc 函数用于调整已分配内存块的大小,原型为:

c

void* realloc(void* ptr, size_t size);

例如,调整之前分配的内存块大小:

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 分配的内存,原型为:

c

void free(void* ptr);

要点:使用这些函数时,要确保内存分配成功,并且及时释放不再使用的内存,避免内存泄漏。

易错点:

- 对 NULL 指针调用 free 虽然不会出错,但这是不必要的操作,还可能掩盖真正的问题。

- 多次释放同一块内存会导致程序崩溃。

三、C++内存管理方式

3.1 new/delete操作内置类型

在C++中,可以使用 new 和 delete 操作符进行动态内存管理。

- 申请单个 int 类型空间:

cpp

int* ptr4 = new int;

delete ptr4;

- 申请单个 int 类型空间并初始化为10:

cpp

int* ptr5 = new int(10);

delete ptr5;

- 申请10个 int 类型的空间:

cpp

int* ptr6 = new int[10];

delete[] ptr6;

3.2 new/delete操作自定义类型

对于自定义类型(如类), new 会调用构造函数, delete 会调用析构函数。

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

// 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

new (place_address) type或者new (place_address) type(initializer-list)

其中 place_address 必须是一个指针, initializer-list 是类型的初始化列表。

6.3 使用场景

通常配合内存池使用。因为内存池分配出的内存没有初始化,所以如果是自定义类型的对象,需要使用定位 new 表达式进行显式调用构造函数进行初始化。

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

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++内存管理的学习和实践中有所帮助。