玩转C++内存管理:从新手到高手的必备指南

目录

引言

[一、C/C++ 内存分布](#一、C/C++ 内存分布)

1.内存分布

2.代码示例

3.详细解释

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

1.malloc

2.calloc

3.realloc

4.free

三、C++中的内存管理方式

[1.​new 操作符](#1.new 操作符)

[2.delete 操作符](#2.delete 操作符)

[3.new 和 delete 的优势](#3.new 和 delete 的优势)

[四、operator new 与 operator delete 函数](#四、operator new 与 operator delete 函数)

[1.operator new 的实现原理](#1.operator new 的实现原理)

[2.operator delete 的实现原理](#2.operator delete 的实现原理)

[3.operator new[] 与 operator delete[]](#3.operator new[] 与 operator delete[])

[五、new 和 delete 的实现原理](#五、new 和 delete 的实现原理)

[1. 内置类型的 new 和 delete 实现原理](#1. 内置类型的 new 和 delete 实现原理)

[2. 自定义类型的 new 和 delete 实现原理](#2. 自定义类型的 new 和 delete 实现原理)

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

[七、malloc/free 与 new/delete 的区别总结](#七、malloc/free 与 new/delete 的区别总结)


引言

C++中的内存管理是编写高效、可靠程序的关键所在。C++不仅继承了C语言的内存管理方式,还增加了面向对象的内存分配机制,使得内存管理既有灵活性,也更加复杂。学习内存管理不仅有助于提升程序效率,还有助于理解计算机的工作原理和资源分配策略。

一、C/C++ 内存分布

1.内存分布

在编译和执行C++程序时,内存划分为几个不同的区域,各个区域承担不同的任务。以下是C++内存的基本分布:

1.栈(Stack):

  • 用途:用于存储局部变量、数参数、返回地址等。

  • 特性:栈内存的分配和释放由系统自动管理,遵循LIFO(Last In, First Out)顺序。每当函数调用时,栈帧(Stack Frame)被压入栈中,函数返回时栈帧弹出。

  • 适用场景:适合小规模的临时变量和函数参数的存储。

2.堆(Heap):

  • 用途:用于程序运行时的动态内存分配。堆内存由程序员手动管理(申请和释放),例如通过new或malloc申请的内存。

  • 特性:堆内存是向上增长的(从低地址到高地址),且不同于栈,堆内存不会自动释放。程序员需要显式调用delete或free来释放内存,否则会导致内存泄漏。

  • 适用场景:适合需要动态分配或较大内存的数据结构,例如链表、树等。

3.数据段(Data Segment):

  • 用途:用于存储全局变量、静态变量等,程序开始时分配,程序结束时释放。

  • 特性:数据段分为已初始化的数据段和未初始化的数据段。已初始化的数据段(如定义为int globalVar = 1;的变量)在程序加载时会直接初始化为指定值;未初始化的数据段(如定义为int globalVar;的变量)则会被初始化为零。

  • 适用场景:适合存储在整个程序生命周期内都需保持的变量。

4.代码段(Text Segment):

  • 用途:存储程序的可执行代码,包括函数体、常量字符串等。

  • 特性:代码段通常是只读的,防止代码在运行时被意外修改,提高了程序的安全性。

  • 适用场景:适合存储程序中的可执行指令以及常量字符串等。

2.代码示例

cpp 复制代码
#include <cstdlib>  // 包含 malloc, calloc, realloc, free
#include <iostream>

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"; // 指针变量在栈中,指向的字符串常量"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);  // 释放堆上的内存
}

int main() {
    Test();
    return 0;
}

3.详细解释

  • globalVar 和 staticGlobalVar

    • 这两个变量分别是全局变量和静态全局变量,它们都存储在 已初始化数据段 中。已初始化数据段用于存储在程序启动时分配的全局变量和静态变量,并且在程序的整个生命周期内一直存在。也就是说,程序结束前,它们的值会一直保留。
  • staticVar

    • staticVar 是一个静态局部变量,虽然它是局部变量,但因为是 static 类型,它的存储位置与普通局部变量不同。staticVar 存储在 已初始化数据段 ,即使函数退出,变量也不会被释放,而是保留其值,直到程序结束。如果函数再次调用,staticVar 不会重新分配内存,而是继续使用上一次保留的值。
  • localVar 和 num1

    • localVar 是一个局部变量,num1 是一个局部数组,它们都存储在 中。栈用于存储局部变量、函数参数等,栈内存是随着函数的调用自动分配的,函数返回时栈上的内存会自动释放。局部变量的生命周期仅限于函数执行期间,函数返回后它们会被销毁。
  • char2

    • char2 是一个字符数组,存储在 中。编译器会将字符串 "abcd" 复制到栈中,这意味着 "abcd" 的内容实际上存在栈内存中。由于 char2[] 是局部变量,因此它的存储空间(包括字符串内容)在函数调用时分配,在函数返回时释放。
  • pChar3

    • pChar3 是一个指针变量,存储在 中。它指向一个字符串常量 "abcd",该字符串常量存储在 只读数据段(常量区)。常量区用于存储字符串字面量等只读数据,因此该字符串在程序的整个生命周期内都存在,且不能被修改。
  • ptr1、ptr2、ptr3

    • ptr1ptr2ptr3 这三个指针变量本身存储在 中,指向的内存则存储在 中。这些指针通过动态内存分配函数 malloccallocrealloc 分配了堆内存。堆内存用于程序运行时动态分配的数据,需要手动释放。如果这些堆内存未通过 free() 释放,则会导致内存泄漏。

    • ptr1 :通过 malloc 动态分配了内存,分配的内存位于堆中。

    • ptr2 :通过 calloc 动态分配了内存,分配的内存位于堆中,且会初始化为零。

    • ptr3 :通过 realloc 调整了 ptr2 的内存大小,新的内存分配在堆中。

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

详见前面博客:C语言动态内存管理

在C语言中,动态内存管理通过以下几个函数实现:

1.malloc

  • malloc(memory allocation) :分配指定大小的内存块,内存中的数据未被初始化。返回值是void*类型指针,需要手动强制转换为具体的类型。

    示例:

    cpp 复制代码
    int* ptr = (int*)malloc(sizeof(int) * 5);  // 申请5个int的内存

2.calloc

  • calloc(contiguous allocation) :分配一块连续的内存,并将所有字节初始化为零。返回值同样为void*类型。

示例:

cpp 复制代码
int* ptr = (int*)calloc(5, sizeof(int));  // 申请5个int的内存并初始化为0

3.realloc

  • realloc(reallocation) :用于调整已经分配的内存块大小。传入新大小后,realloc会尝试扩大或缩小原有的内存块,如果扩展失败,它会在新的位置申请内存并拷贝原内容。

    示例:

    cpp 复制代码
    int* newPtr = (int*)realloc(ptr, sizeof(int) * 10);  // 将原来的内存扩展到10个int

4.free

  • free :用于释放通过malloccallocrealloc申请的内存。内存释放后,不再受程序管理,避免内存泄漏。

    示例:

    cpp 复制代码
    free(ptr);  // 释放动态分配的内存

三、C++中的内存管理方式

C++继承了C语言的malloc、calloc、realloc和free,但提供了更加灵活的内存管理方式,即newdelete操作符:

1.​new 操作符

  • 用途:用于动态分配内存,并对基本类型或自定义对象进行初始化。

  • 特性:分配内存失败时,new会抛出异常std::bad_alloc,而不会像malloc返回NULL。

  • 语法:new 类型用于分配单个对象;new 类型[数量]用于分配数组。

示例:

cpp 复制代码
// 动态申请一个int类型的空间,未初始化,值未定义
    int* ptr1 = new int;

// 动态申请一个int类型的空间,并初始化为10
    int* ptr2 = new int(10);

// 动态申请3个int类型的连续空间,未初始化,值未定义
    int* ptr3 = new int[3];

2.delete 操作符

  • 用途:用于释放通过new分配的内存,避免内存泄漏。

  • 特性:delete用于释放单个对象,delete[]用于释放数组。

  • 语法:delete 指针用于释放单个对象;delete[] 指针用于释放数组。

注意:1. 申请和释放单个元素的空间,使用new和delete操作符
申请和释放连续的空间,使用 new[]和delete[],
2.new和delete要
匹配起来使用。

3.new 和 delete 的优势

new和delete不仅仅是分配和释放内存,还会自动调用构造函数和析构函数,非常适合面向对象编程中的自定义类型管理。

代码示例:newdelete****操作自定义类型

cpp 复制代码
#include <iostream>
#include <cstdlib>  // 包含 malloc 和 free

using namespace std;

class A {
public:
    // 构造函数
    A(int a = 0) : _a(a) {
        cout << "A() constructor called, object address: " << this << endl;
    }

    // 析构函数
    ~A() {
        cout << "~A() destructor called, object address: " << this << endl;
    }

private:
    int _a;  // 成员变量
};

int main() {
    // 使用 malloc 申请内存,但不会调用构造函数
    A* p1 = (A*)malloc(sizeof(A));  // 只分配内存,未调用构造函数
    A* p2 = new A(1);               // 分配内存并调用构造函数

    // 释放通过 malloc 申请的内存,不会调用析构函数
    free(p1);

    // 使用 delete 释放内存,会调用析构函数
    delete p2;

    // 内置类型的操作:malloc 和 new 对内置类型的行为几乎相同
    int* p3 = (int*)malloc(sizeof(int));  // 只分配内存,不会初始化
    int* p4 = new int;                    // 分配内存但未初始化
    free(p3);                             // 释放 malloc 分配的内存
    delete p4;                            // 释放 new 分配的内存

    // 动态申请数组
    A* p5 = (A*)malloc(sizeof(A) * 10);   // 只分配内存,未调用构造函数
    A* p6 = new A[10];                    // 分配内存并调用构造函数

    // 释放内存
    free(p5);      // 只释放内存,不调用析构函数
    delete[] p6;   // 释放数组并调用每个对象的析构函数

    return 0;
}

注意:在申请自定义类型的空间时,new会调用构造函数,delete会调用析构函数,而malloc与free不会。

四、operator new 与 operator delete 函数

在C++中, new和delete操作符用于动态内存管理,并且会在创建和销毁对象时自动调用构造函数和析构函数。然而 new和delete在底层是依赖于全局的 operator new 和operator delete 函数进行 实际的内存分配和释放操作。通过自定义这些函数,开发者可以控制内存的分配策略,尤其是在需要自定义内存管理(如内存池)时。

1.operator new 的实现原理

operator new 是C++的全局函数,它负责分配内存。默认情况下,它会调用 malloc 来分配指定大小的内存。如果内存分配失败,它会抛出 std::bad_alloc 异常。

代码示例:

cpp 复制代码
#include <new>   // 包含 bad_alloc
#include <cstdlib>  // 包含 malloc
#include <iostream>
using namespace std;

// operator new: 内存分配函数
void* operator new(size_t size) _THROW1(std::bad_alloc) {
    void* p;
    // 使用 malloc 分配内存,如果分配失败则尝试调用处理函数
    while ((p = malloc(size)) == 0) {
        // 如果处理函数返回 0,抛出 bad_alloc 异常
        if (_callnewh(size) == 0) {
            static const std::bad_alloc nomem;
            _RAISE(nomem);  // 抛出内存不足异常
        }
    }
    return p;  // 如果成功分配内存,返回指向该内存的指针
}

2.operator delete 的实现原理

operator deletenew 的对应函数,用于释放内存。它会调用 free 来释放由 new 分配的内存。和 new 类似,delete 也可以被用户自定义,以实现特定的内存管理需求。

代码示例:

cpp 复制代码
#include <cstdlib>  // 包含 free
#include <iostream>
using namespace std;

// operator delete: 内存释放函数
void operator delete(void* p) noexcept {
    // 如果传入的指针为空,则不执行释放操作
    if (p == NULL) return;

    // 通过 free 函数释放内存
    free(p);
}

3.operator new[]operator delete[]

与 operator new 和 operator delete 类似,C++ 中还提供了 operator new[] 和 operator delete[] 用于分配和释放数组。这些函数与单个对象的 operator newoperator delete 在功能上类似,只是针对的是数组。

代码示例:

cpp 复制代码
// operator new[]: 分配数组所需的内存
void* operator new[](size_t size) {
    cout << "Allocating array of size: " << size << endl;
    return malloc(size);  // 使用 malloc 分配内存
}

// operator delete[]: 释放数组的内存
void operator delete[](void* p) noexcept {
    cout << "Freeing array memory" << endl;
    free(p);  // 使用 free 释放内存
}

五、new 和 delete 的实现原理

1. 内置类型的 newdelete 实现原理

对于内置类型(例如 intdouble 等),newmallocdeletefree 的行为非常相似。它们之间的主要区别在于:

  1. 单个元素与数组new/delete 申请和释放单个对象,而 new[]/delete[] 申请和释放数组,即连续的多个元素。
  2. 异常处理
    • new 在内存分配失败时会抛出 std::bad_alloc 异常。
    • malloc 在分配失败时返回 NULL,所以使用 malloc 时需要手动检查返回值是否为空。
  3. 自动初始化
    • new 可以初始化内存。例如,new int(5) 会为新分配的 int 空间初始化为 5,而 malloc 只分配内存,不做初始化。

代码示例:

cpp 复制代码
#include <iostream>
#include <cstdlib>  // 包含 malloc 和 free
using namespace std;

int main() {
    // 使用 malloc 分配内存,不会初始化
    int* p1 = (int*)malloc(sizeof(int));

    // 使用 new 分配内存,并初始化为 10
    int* p2 = new int(10);

    // 释放内存,malloc 使用 free 释放
    free(p1);

    // 释放内存,new 使用 delete 释放
    delete p2;

    return 0;
}

总结:

  • 内置类型newmalloc 在申请内存的行为上相似,但 new 提供了异常处理机制,而 malloc 返回 NULL
  • delete/free 在释放内存的行为上基本一致,都是简单的释放操作。

2. 自定义类型的 newdelete 实现原理

对于自定义类型(例如类对象),newdelete 的行为比内置类型更复杂,因为它们不仅需要分配和释放内存,还必须调用构造函数和析构函数。这是 new/deletemalloc/free 的核心区别。

自定义类型 new 的工作流程:

  1. 调用 operator new 分配内存

    • operator new 通过 malloc 或其他内存分配函数为对象分配内存空间。
  2. 调用构造函数初始化对象

    • 在分配的内存上,new 调用对象的构造函数,完成对象的初始化。
    • 这一步确保了对象的成员变量得到正确的初始化。
      自定义类型 delete 的工作流程:
  3. 调用析构函数

    • delete 在释放对象之前,首先会调用对象的析构函数,用于释放对象中的资源(例如释放对象成员变量中动态分配的内存,关闭文件等)。
  4. 调用 operator delete 释放内存

    • 在调用完析构函数后,operator delete 函数被调用,使用 free 来释放该对象占用的内存空间。

代码示例:

cpp 复制代码
#include <iostream>
using namespace std;

class A {
public:
    // 构造函数
    A(int a = 0) : _a(a) {
        cout << "A() constructor called, value: " << _a << endl;
    }

    // 析构函数
    ~A() {
        cout << "~A() destructor called, value: " << _a << endl;
    }

private:
    int _a;  // 成员变量
};

int main() {
    // 使用 new 分配 A 类对象,调用构造函数
    A* p1 = new A(10);

    // 使用 delete 释放 A 类对象,调用析构函数
    delete p1;

    return 0;
}

总结:

  • newdelete:不仅分配和释放内存,还负责调用构造函数和析构函数,确保自定义类型对象的正确初始化和清理。
  • malloc/free:对于自定义类型来说,只会分配和释放内存,不会调用构造和析构函数,因此不适用于需要自动管理对象生命周期的情况。

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

placement-new是一种特殊的new语法,允许在指定的内存地址上构造对象。该特性在高效内存分配的场景(如内存池)中非常有用。

示例:

cpp 复制代码
#include <new> // 必须包含<new>头文件

class Example {
public:
    Example() { std::cout << "Example Constructor" << std::endl; }
    ~Example() { std::cout << "Example Destructor" << std::endl; }
};

int main() {
    char buffer[sizeof(Example)];      // 分配足够大的缓冲区
    Example* p = new(buffer) Example;  // 在缓冲区上构造对象

    p->~Example();  // 显式调用析构函数
    return 0;
}

七、malloc/free 与 new/delete 的区别总结

在 C++ 中,malloc/freenew/delete 都可以用于从堆上申请和释放内存。它们的共同点是都用于动态内存分配,并且需要用户手动释放内存。但它们之间有一些重要的区别:

  1. 函数 vs 操作符
  • malloc/free:这是 C 语言中的函数,用于分配和释放内存。

  • new/delete:这是 C++ 中的操作符,用于分配和释放内存,并且可以调用构造函数和析构函数。

  1. 内存初始化
  • malloc:只分配内存,不会对分配的内存进行初始化,内存中的数据是未定义的。

  • new :分配内存的同时可以初始化对象,尤其是对于自定义类型时,new 会调用构造函数对对象进行初始化。

  1. 内存大小计算
  • malloc :用户需要手动计算需要分配的内存大小并传递给 malloc,例如 malloc(sizeof(int))

  • new :用户不需要手动计算内存大小,new 会根据类型自动计算。例如 new int 自动分配 int 类型的空间。

  1. 返回类型
  • malloc :返回 void* 类型的指针,使用时必须进行强制类型转换,例如 (int*)malloc(sizeof(int))

  • new :返回具体类型的指针,不需要强制类型转换,例如 new int 返回 int* 类型的指针。

  1. 错误处理
  • malloc :内存分配失败时返回 NULL,因此需要在使用时手动检查返回值是否为 NULL

  • new :内存分配失败时会抛出 std::bad_alloc 异常,因此使用 new 时需要捕获异常。

  1. 构造函数与析构函数
  • malloc/free :只负责内存的分配与释放,不会调用构造函数和析构函数。因此,malloc/free 不能正确处理自定义类型的对象。

  • new/delete :在分配内存时会调用对象的构造函数完成初始化,在释放内存时会调用对象的析构函数完成资源的清理和释放。因此,new/delete 更适合管理自定义类型的对象。

掌握内存管理是编写高效C++程序的基础。通过熟悉栈、堆和各类动态内存管理方法,可以更好地理解C++底层机制,实现高效的内存管理。

相关推荐
Theodore_10221 小时前
4 设计模式原则之接口隔离原则
java·开发语言·设计模式·java-ee·接口隔离原则·javaee
‘’林花谢了春红‘’3 小时前
C++ list (链表)容器
c++·链表·list
----云烟----3 小时前
QT中QString类的各种使用
开发语言·qt
lsx2024064 小时前
SQL SELECT 语句:基础与进阶应用
开发语言
开心工作室_kaic4 小时前
ssm161基于web的资源共享平台的共享与开发+jsp(论文+源码)_kaic
java·开发语言·前端
向宇it4 小时前
【unity小技巧】unity 什么是反射?反射的作用?反射的使用场景?反射的缺点?常用的反射操作?反射常见示例
开发语言·游戏·unity·c#·游戏引擎
武子康4 小时前
Java-06 深入浅出 MyBatis - 一对一模型 SqlMapConfig 与 Mapper 详细讲解测试
java·开发语言·数据仓库·sql·mybatis·springboot·springcloud
转世成为计算机大神5 小时前
易考八股文之Java中的设计模式?
java·开发语言·设计模式
机器视觉知识推荐、就业指导5 小时前
C++设计模式:建造者模式(Builder) 房屋建造案例
c++
宅小海5 小时前
scala String
大数据·开发语言·scala