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

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

目录

引言

一、C/C++ 内存分布

1.内存分布

2.代码示例

3.详细解释

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

1.malloc

2.calloc

3.realloc

4.free

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

1.new 操作符

2.delete 操作符

3.new 和 delete 的优势

四、operator new 与 operator delete 函数

1.operator new 的实现原理

2.operator delete 的实现原理

3.operator new[] 与 operator delete[]

五、new 和 delete 的实现原理

1. 内置类型的 new 和 delete 实现原理

2. 自定义类型的 new 和 delete 实现原理

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

七、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.代码示例

#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;
}

image.gif

3.详细解释

image.gif 编辑

  • 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*类型指针,需要手动强制转换为具体的类型。
    示例:
int* ptr = (int*)malloc(sizeof(int) * 5);  // 申请5个int的内存
  • image.gif

2.calloc

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

示例:

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

image.gif

3.realloc

  • realloc(reallocation)用于调整已经分配的内存块大小。传入新大小后,realloc会尝试扩大或缩小原有的内存块,如果扩展失败,它会在新的位置申请内存并拷贝原内容。
    示例:
int* newPtr = (int*)realloc(ptr, sizeof(int) * 10);  // 将原来的内存扩展到10个int
  • image.gif

4.free

  • free用于释放通过malloccallocrealloc申请的内存。内存释放后,不再受程序管理,避免内存泄漏。
    示例:
free(ptr);  // 释放动态分配的内存
  • image.gif

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

C++继承了C语言的malloccallocreallocfree,但提供了更加灵活的内存管理方式,即newdelete操作符

1.new 操作符

  • 用途:用于动态分配内存,并对基本类型或自定义对象进行初始化。
  • 特性:分配内存失败时,new会抛出异常std::bad_alloc,而不会像malloc返回NULL。
  • 语法:new 类型用于分配单个对象;new 类型[数量]用于分配数组。

示例:

// 动态申请一个int类型的空间,未初始化,值未定义
    int* ptr1 = new int;
// 动态申请一个int类型的空间,并初始化为10
    int* ptr2 = new int(10);
// 动态申请3个int类型的连续空间,未初始化,值未定义
    int* ptr3 = new int[3];

image.gif

2.delete 操作符

  • 用途:用于释放通过new分配的内存,避免内存泄漏。
  • 特性:delete用于释放单个对象,delete[]用于释放数组。
  • 语法:delete 指针用于释放单个对象;delete[] 指针用于释放数组。

注意:1.申请和释放单个元素的空间,使用new和delete操作符

            申请和释放连续的空间,使用 new[]和delete[],

         2.new和delete要匹配起来使用。

3.new 和 delete 的优势

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

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

#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;
}

image.gif

 注意:在申请自定义类型的空间时,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 异常。

代码示例:

#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;  // 如果成功分配内存,返回指向该内存的指针
}

image.gif

2.operator delete 的实现原理

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

代码示例:

#include <cstdlib>  // 包含 free
#include <iostream>
using namespace std;
// operator delete: 内存释放函数
void operator delete(void* p) noexcept {
    // 如果传入的指针为空,则不执行释放操作
    if (p == NULL) return;
    // 通过 free 函数释放内存
    free(p);
}

image.gif

3.operator new[]operator delete[]

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

代码示例:

// 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 释放内存
}

image.gif

五、new 和 delete 的实现原理

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

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

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

代码示例:

#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;
}

image.gif

总结:

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

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

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

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

  1. 调用 operator new 分配内存
  • operator new 通过 malloc 或其他内存分配函数为对象分配内存空间。
  1. 调用构造函数初始化对象
  • 在分配的内存上,new 调用对象的构造函数,完成对象的初始化。
  • 这一步确保了对象的成员变量得到正确的初始化。

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

  1. 调用析构函数
  • delete 在释放对象之前,首先会调用对象的析构函数,用于释放对象中的资源(例如释放对象成员变量中动态分配的内存,关闭文件等)。
  1. 调用 operator delete 释放内存
  • 在调用完析构函数后,operator delete 函数被调用,使用 free 来释放该对象占用的内存空间。

代码示例:

#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;
}

image.gif

image.gif 编辑

总结:

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

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

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

示例:

#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;
}

image.gif

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

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

1. 函数 vs 操作符

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

2. 内存初始化

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

3. 内存大小计算

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

4. 返回类型

  • malloc:返回 void* 类型的指针,使用时必须进行强制类型转换,例如 (int*)malloc(sizeof(int))
  • new:返回具体类型的指针,不需要强制类型转换,例如 new int 返回 int* 类型的指针。

5. 错误处理

  • malloc:内存分配失败时返回 NULL,因此需要在使用时手动检查返回值是否为 NULL
  • new:内存分配失败时会抛出 std::bad_alloc 异常,因此使用 new 时需要捕获异常。

6. 构造函数与析构函数

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

image.gif 编辑

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

相关文章
|
3月前
|
存储 缓存 编译器
【硬核】C++11并发:内存模型和原子类型
本文从C++11并发编程中的关键概念——内存模型与原子类型入手,结合详尽的代码示例,抽丝剥茧地介绍了如何实现无锁化并发的性能优化。
213 68
|
26天前
|
安全 C语言 C++
彻底摘明白 C++ 的动态内存分配原理
大家好,我是V哥。C++的动态内存分配允许程序在运行时请求和释放内存,主要通过`new`/`delete`(用于对象)及`malloc`/`calloc`/`realloc`/`free`(继承自C语言)实现。`new`分配并初始化对象内存,`delete`释放并调用析构函数;而`malloc`等函数仅处理裸内存,不涉及构造与析构。掌握这些可有效管理内存,避免泄漏和悬空指针问题。智能指针如`std::unique_ptr`和`std::shared_ptr`能自动管理内存,确保异常安全。关注威哥爱编程,了解更多全栈开发技巧。 先赞再看后评论,腰缠万贯财进门。
110 0
|
2月前
|
存储 程序员 编译器
什么是内存泄漏?C++中如何检测和解决?
大家好,我是V哥。内存泄露是编程中的常见问题,可能导致程序崩溃。特别是在金三银四跳槽季,面试官常问此问题。本文将探讨内存泄露的定义、危害、检测方法及解决策略,帮助你掌握这一关键知识点。通过学习如何正确管理内存、使用智能指针和RAII原则,避免内存泄露,提升代码健壮性。同时,了解常见的内存泄露场景,如忘记释放内存、异常处理不当等,确保在面试中不被秒杀。最后,预祝大家新的一年工作顺利,涨薪多多!关注威哥爱编程,一起成为更好的程序员。
|
4月前
|
存储 缓存 C语言
【c++】动态内存管理
本文介绍了C++中动态内存管理的新方式——`new`和`delete`操作符,详细探讨了它们的使用方法及与C语言中`malloc`/`free`的区别。文章首先回顾了C语言中的动态内存管理,接着通过代码实例展示了`new`和`delete`的基本用法,包括对内置类型和自定义类型的动态内存分配与释放。此外,文章还深入解析了`operator new`和`operator delete`的底层实现,以及定位new表达式的应用,最后总结了`malloc`/`free`与`new`/`delete`的主要差异。
96 3
|
4月前
|
存储 编译器 Linux
【c++】类和对象(上)(类的定义格式、访问限定符、类域、类的实例化、对象的内存大小、this指针)
本文介绍了C++中的类和对象,包括类的概念、定义格式、访问限定符、类域、对象的创建及内存大小、以及this指针。通过示例代码详细解释了类的定义、成员函数和成员变量的作用,以及如何使用访问限定符控制成员的访问权限。此外,还讨论了对象的内存分配规则和this指针的使用场景,帮助读者深入理解面向对象编程的核心概念。
296 4
|
5月前
|
存储 程序员 编译器
简述 C、C++程序编译的内存分配情况
在C和C++程序编译过程中,内存被划分为几个区域进行分配:代码区存储常量和执行指令;全局/静态变量区存放全局变量及静态变量;栈区管理函数参数、局部变量等;堆区则用于动态分配内存,由程序员控制释放,共同支撑着程序运行时的数据存储与处理需求。
312 22
|
5月前
|
程序员 C++ 容器
在 C++中,realloc 函数返回 NULL 时,需要手动释放原来的内存吗?
在 C++ 中,当 realloc 函数返回 NULL 时,表示内存重新分配失败,但原内存块仍然有效,因此需要手动释放原来的内存,以避免内存泄漏。
|
5月前
|
存储 C语言 C++
【C++打怪之路Lv6】-- 内存管理
【C++打怪之路Lv6】-- 内存管理
74 0
【C++打怪之路Lv6】-- 内存管理
|
5月前
|
存储 C语言 C++
【C/C++内存管理】——我与C++的不解之缘(六)
【C/C++内存管理】——我与C++的不解之缘(六)
|
5月前
|
程序员 C语言 C++
C++入门5——C/C++动态内存管理(new与delete)
C++入门5——C/C++动态内存管理(new与delete)
126 1

热门文章

最新文章