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

目录

引言

一、C/C++内存分布

[1.1 内存区域划分](#1.1 内存区域划分)

[1.2 代码示例分析](#1.2 代码示例分析)

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

[2.1 malloc](#2.1 malloc)

[2.2 calloc](#2.2 calloc)

[2.3 realloc](#2.3 realloc)

[2.4 free](#2.4 free)

三、C++内存管理方式

[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 代码示例)

五、new和delete的实现原理

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

相关推荐
巨可爱熊2 小时前
高并发内存池(定长内存池基础)
linux·运维·服务器·c++·算法
码农新猿类5 小时前
服务器本地搭建
linux·网络·c++
GOTXX5 小时前
【Qt】Qt Creator开发基础:项目创建、界面解析与核心概念入门
开发语言·数据库·c++·qt·图形渲染·图形化界面·qt新手入门
徐行1106 小时前
C++核心机制-this 指针传递与内存布局分析
开发语言·c++
序属秋秋秋6 小时前
算法基础_数据结构【单链表 + 双链表 + 栈 + 队列 + 单调栈 + 单调队列】
c语言·数据结构·c++·算法
mldl_7 小时前
(个人题解)第十六届蓝桥杯大赛软件赛省赛C/C++ 研究生组
c语言·c++·蓝桥杯
一个小白17 小时前
C++ 用红黑树封装map/set
java·数据库·c++
Lenyiin7 小时前
《 C++ 点滴漫谈: 三十三 》当函数成为参数:解密 C++ 回调函数的全部姿势
c++·回调函数·lenyiin
埜玊8 小时前
C++之 多继承
c++
1024熙9 小时前
【C++】——lambda表达式
开发语言·数据结构·c++·算法·lambda表达式