自学内容网 自学内容网

C++内存管理

目录

0.前言

1.C++内存布局

2.C语言动态内存管理

3.C++内存管理方式

3.1new/delete操作内置类型

3.2new/delete操作自定义类型

4.operator new和operator delete函数

5.new和delete的实现原理

5.1对内置类型

5.2对自定义类型

6.定位new表达式(placement-new)

6.1基本概念

6.2使用场景

6.3语法

6.4注意事项

7.常见概念辨析

7.1malloc/free和new/delete的区别

7.2内存泄漏

7.2.1内存泄漏的含义与危害

7.2.2内存泄漏的分类

7.2.3如何检测内存泄漏

7.2.4如何避免内存泄漏

8.结语


(图像由AI生成) 

0.前言

在前面的博客中,我们了解到C++作为一门面向对象的语言的核心特色之一——类与对象。除此之外,内存管理也是一个绝不能忽视的核心主题。良好的内存管理不仅可以提高程序的效率,还能避免许多由资源管理不当引起的问题。本博客旨在全面解析C++内存管理的各个方面,帮助读者构建坚实的理论基础,并学会实践中的关键技能。

1.C++内存布局

在C++中,程序的内存通常分为几个主要部分,各自承担着不同的功能和责任:

  1. 栈(Stack)

    • 用于自动存储局部变量和函数调用的管理(如返回地址和局部变量)。
    • 栈是有序的,先进后出的结构。当函数调用发生时,新的帧被推入栈中,函数返回时帧被弹出。
  2. 堆(Heap)

    • 用于动态内存分配,程序中使用newdeletemallocfree等操作进行管理。
    • 堆的管理更为复杂,涉及到内存的申请和释放,容易出现内存泄漏、碎片等问题。
  3. 内存映射段(Memory Mapped Regions)

    • 通常用于映射外部设备(如映射硬件设备状态)或文件(通过mmap等系统调用)。
  4. 数据段

    • 初始化数据段(.data):存储初始化的全局变量和静态变量。
    • 未初始化数据段(.bss):用于存储未初始化的全局变量和静态变量。
  5. 代码段(Text Segment)

    • 存储程序的机器语言指令。这部分内存通常是只读的,以防止程序代码被意外修改。

以下是一段C++代码示例,其中定义了不同类型的变量,并注释说明了它们各自的内存存储区域。

#include <iostream>
#include <string>
#include <vector>

int globalVar = 100;            // 存储在初始化数据段 (.data)
static int staticGlobalVar;     // 存储在未初始化数据段 (.bss)
const char* constText = "Hello World";  // 存储在代码段 (.text)

void exampleFunction() {
    int localVar = 5;          // 存储在栈 (stack)
    static int staticVar = 5;  // 存储在数据段 (.data 或 .bss,取决于是否被初始化)
    int* heapVar = new int(10); // 指针存储在栈上,指向的数据存储在堆 (heap)

    std::string str = "Hello"; // "Hello"存储在代码段,str对象本身存储在栈上
    std::vector<int> vec = {1, 2, 3, 4};  // vec对象本身存储在栈上,其数据部分存储在堆上
}

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

2.C语言动态内存管理

在C语言中,动态内存管理是通过一组标准库函数实现的,主要包括 malloc, calloc, realloc, 和 free。这些函数提供了在运行时从堆区分配和释放内存的能力,使得程序能够根据需要动态调整内存使用量。以下是这些函数的主要特性和用途:

  1. malloc (Memory Allocation)

    • 功能:分配指定大小的内存块。
    • 用法void* malloc(size_t size);
    • 返回值:成功时返回指向分配的内存的指针,失败时返回NULL
    • 特点:分配的内存不会被初始化,可能含有垃圾数据。
  2. calloc (Contiguous Allocation)

    • 功能:分配指定数量、指定大小的内存,并自动初始化所有位为零。
    • 用法void* calloc(size_t num, size_t size);
    • 返回值:成功时返回指向分配的内存的指针,失败时返回NULL
    • 特点:适合于需要分配多个相同大小对象的情况,且需要内存内容清零。
  3. realloc (Re-Allocation)

    • 功能:重新分配先前通过 malloccalloc 分配的内存块的大小。
    • 用法void* realloc(void* ptr, size_t newSize);
    • 返回值:成功时返回指向新内存的指针(可能与原来的指针不同),失败时返回NULL且原内存块不受影响。
    • 特点:当需要扩大或缩小已分配内存的大小时使用,避免了额外的复制操作和提高内存使用效率。
  4. free

    • 功能:释放之前通过 malloc, calloc, 或 realloc 分配的内存块。
    • 用法void free(void* ptr);
    • 特点:释放的内存块之后可以被再次分配;如果传入NULL指针,free函数无效果。

这一部分我们在之前的博客中已经详细介绍过。详见C语言——动态内存管理-CSDN博客

3.C++内存管理方式

C++提供了一套相对于C更高级的动态内存管理工具,主要体现在newdelete操作符的使用上。这些操作符不仅封装了内存的分配和释放,还处理了对象的构造和析构,提供了更为安全和便捷的内存管理方式。

3.1new/delete操作内置类型

对于内置类型,如intdoublechar等,newdelete主要用于分配和释放内存,同时保持类型安全和初始化的控制。以下是如何使用newdelete以及它们的特性:

  1. 使用new分配内置类型

    • 语法TypeName* ptr = new TypeName;
    • 功能:分配足够的内存以存储TypeName类型的一个对象,并返回指向该内存的指针。
    • 初始化:对于内置类型,new会默认初始化内存(如果是内置的基本数据类型,如intdouble,其值未定义,除非另行指定)。
    • 例子int* p = new int(10); 分配一个int类型的内存,并初始化为10。
  2. 使用delete释放内置类型

    • 语法delete ptr;
    • 功能:释放new操作符分配的内存,并将指针ptr所指向的对象所占用的内存返回给堆。
    • 注意:使用delete释放内存后,指针变量本身并未清空或删除,它依然存在。适当的做法是将指针设置为nullptr,避免悬挂指针。

下面是一段示例代码:

#include <iostream>

int main() {
    // 分配单个整数
    int* p = new int(10); // 分配并初始化
    std::cout << "Value: " << *p << std::endl;  // 输出: Value: 10

    // 释放内存
    delete p;
    p = nullptr; // 避免悬挂指针

    // 分配数组
    int* array = new int[5] {1, 2, 3, 4, 5};  // 分配并初始化一个整数数组
    for (int i = 0; i < 5; ++i) {
        std::cout << array[i] << " ";  // 输出数组内容
    }
    std::cout << std::endl;

    // 释放数组
    delete[] array;
    array = nullptr; // 避免悬挂指针

    return 0;
}

注意事项

  • 类型安全newdelete提供了类型安全,确保分配的内存与对象类型匹配。
  • 内存泄漏:必须确保每次new操作都对应一个delete操作,否则会导致内存泄漏。
  • 数组注意事项:分配数组时使用new[],释放时使用delete[]。不遵循这一规则可能导致资源泄露或程序崩溃。

3.2new/delete操作自定义类型

对于C++中的自定义类型,如类和结构体,使用newdelete不仅涉及内存的分配和释放,还包括对象的构造和析构。这使得newdelete在处理复杂对象时显得更为强大和灵活。

使用new分配自定义类型

当使用new为自定义类型分配内存时,new操作符会先分配足够的内存来存储对象,然后在分配的内存上调用对象的构造函数来初始化对象。

  • 语法TypeName* ptr = new TypeName(arguments);
  • 例子:为自定义的类MyClass分配内存,并调用其构造函数。
class MyClass {
public:
    int a;
    MyClass(int x) : a(x) {
        std::cout << "MyClass constructed with value " << a << std::endl;
    }
    ~MyClass() {
        std::cout << "MyClass destroyed" << std::endl;
    }
};

int main() {
    MyClass* myObject = new MyClass(10);  // 调用构造函数,输出构造信息
}

使用delete释放自定义类型

对于使用new创建的自定义类型的对象,使用delete来释放这些对象时,delete操作符会首先调用对象的析构函数,然后释放对象所占用的内存。

  • 语法delete ptr;
  • 行为:首先调用ptr指向对象的析构函数,然后释放内存。
  • 例子:释放上面创建的MyClass对象,并触发析构函数。
int main() {
    MyClass* myObject = new MyClass(10);
    delete myObject;  // 调用析构函数,输出析构信息
    myObject = nullptr; // 避免悬挂指针
}

注意事项

  • 构造和析构newdelete自动处理对象的构造和析构,这是与内置类型最大的区别。
  • 异常安全:如果在构造过程中发生异常,已经分配的内存会被自动释放,防止内存泄漏。
  • 资源管理delete确保释放对象所持有的任何资源,例如,如果对象包含动态分配的内存或文件句柄,它们也应在析构函数中被释放。

4.operator new和operator delete函数

在C++中,operator newoperator delete提供了一种机制,允许程序员自定义对象的内存分配和释放过程。这两个操作符函数可以被重载,以适应特定的内存管理需求,如实现内存池、跟踪内存使用情况或增加特殊的内存分配逻辑。

基本概念

operator newoperator delete是全局函数,也可以为特定类进行重载。这些函数不仅仅是简单地分配和释放内存,它们在C++的内存管理中扮演了构造和析构对象之外的角色。

1. operator new

operator new函数负责分配足够的内存以容纳特定类型的对象。它是new表达式的一部分,当你使用new关键字时,C++编译器会调用operator new来分配内存,然后在分配的内存上调用构造函数来构造对象。

  • 原型

    void* operator new(size_t size);

  • 参数

    • size:需要分配的内存大小,通常由编译器根据对象类型自动提供。
  • 返回值

    • 返回一个指向分配的内存块的指针。如果无法分配内存,标准的operator new会抛出一个std::bad_alloc异常,除非使用了nothrow版本。
  • 例子:重载一个类的operator new

class MyClass {
public:
    static void* operator new(size_t size) {
        std::cout << "Custom new operator for MyClass. Size: " << size << std::endl;
        return ::operator new(size);
    }
};

2. operator delete

operator delete负责释放由operator new分配的内存,并在对象生命周期结束后由delete表达式调用。在对象的析构函数执行后,operator delete被调用来处理内存的释放。

  • 原型

    void operator delete(void* pointer);
  • 参数

    • pointer:指向要释放内存的指针。
  • 功能

    • 释放之前通过operator new分配的内存。如果指针是nullptr,则operator delete不执行任何操作。
  • 例子:重载一个类的operator delete

class MyClass {
public:
    static void operator delete(void* pointer) {
        std::cout << "Custom delete operator for MyClass" << std::endl;
        ::operator delete(pointer);
    }
};

注意事项

  • 匹配使用:为类重载operator new时,也应该重载operator delete。这样可以确保内存分配和释放的一致性。
  • 异常安全:如果在构造对象后但在返回指针之前发生异常,应当在operator new内部正确处理,以避免内存泄漏。

5.new和delete的实现原理

在C++中,newdelete不仅是关键字,它们背后还有复杂的实现机制,负责管理内存的分配和释放。理解这些实现细节可以帮助开发者写出更高效和更稳定的程序。

5.1对内置类型

对于内置类型(如intdoublechar等),newdelete的实现比较直接,主要涉及内存分配和释放,不涉及复杂的对象构造和析构。

new的实现原理

  1. 内存分配new首先调用operator new函数来分配内存。这是一个全局函数,其标准实现通常调用底层的内存管理API(如C的malloc函数)来获取足够的内存。

  2. 初始化:对于内置类型,标准的new通常不进行任何初始化,除非显式提供了初始化值(例如new int(42))。如果没有提供初始化值,则分配的内存中的内容是未定义的。

int* p = new int;  // 内存未初始化
int* q = new int(10);  // 内存初始化为10

delete的实现原理

  1. 内存释放delete关键字对应地调用operator delete函数来释放内存。这个函数通常会调用底层的内存释放API(如C的free函数),将之前由operator new分配的内存返回给系统。

  2. 资源管理:对于内置类型,由于没有复杂的构造和析构过程,delete的操作相对简单。主要任务是确保内存被正确释放,避免内存泄漏。

delete p;  // 释放p指向的内存
p = nullptr;  // 避免产生悬挂指针

示例代码

这里是一个简单的例子,演示了如何使用newdelete操作内置类型的内存:

#include <iostream>

int main() {
    // 使用new分配内存
    int* p = new int(10);  // 分配并初始化为10
    std::cout << "Value: " << *p << std::endl;  // 输出: Value: 10

    // 使用delete释放内存
    delete p;
    p = nullptr; // 推荐做法,避免悬挂指针

    return 0;
}

在这个示例中,new分配了一个整数的内存并进行了初始化,而delete则释放了这块内存并清空了指针。这种简单的用法隐藏了newdelete背后的复杂实现,但对于内置类型来说,这种直接的内存操作是足够的。这样的实现确保了内存管理的基本安全性和效率。

5.2对自定义类型

对于自定义类型,如类或结构体,newdelete不仅涉及内存分配和释放,还包括对象的构造和析构。这增加了实现的复杂性,但同时提供了更多控制和灵活性,使得内存管理能够与对象的生命周期紧密结合。

new的实现原理

new用于自定义类型时,其工作流程大致如下:

  1. 内存分配:与内置类型类似,new首先调用operator new函数来分配足够的内存。这通常通过调用底层的内存管理系统(如malloc)来完成。

  2. 对象构造:在分配好的内存上,new接着调用对象的构造函数来初始化对象。这个步骤是自定义类型和内置类型操作的主要区别。构造函数的调用确保了对象的数据成员根据定义进行初始化,并执行任何其他必要的设置。

例如,给定一个类Person,使用new创建其对象的过程会涉及到以下操作:

class Person {
public:
    std::string name;
    int age;

    Person(const std::string& n, int a) : name(n), age(a) {
        std::cout << "Person created: " << name << ", " << age << std::endl;
    }

    ~Person() {
        std::cout << "Person destroyed: " << name << std::endl;
    }
};

int main() {
    Person* p = new Person("John", 30); // 分配内存并构造对象
}

在这个例子中,new不仅分配了内存,还调用了Person的构造函数来初始化对象的nameage字段。

delete的实现原理

使用delete释放自定义类型对象时,操作包括:

  1. 对象析构delete首先调用对象的析构函数。析构函数负责进行清理工作,如释放对象可能持有的资源(比如动态分配的内存、文件句柄等)。

  2. 内存释放:析构函数执行完毕后,delete调用operator delete函数释放对象占用的内存。这通常涉及到调用底层的内存释放函数(如free)。

在前面的Person类示例中,删除p指向的对象的过程会调用Person的析构函数,并输出析构信息,然后释放内存:

int main() {
    Person* p = new Person("John", 30);
    delete p; // 调用析构函数,然后释放内存
}

注意事项

  • 异常安全:在构造过程中如果发生异常,已分配的内存需要被正确释放以防内存泄漏。C++的异常处理机制确保了在构造函数抛出异常时,分配给对象的内存会被自动释放。
  • 配对使用:每个通过new分配的对象都必须通过相应的delete来释放,确保每个对象的生命周期得到正确管理。
  • 复杂对象:对于包含其他资源的对象,如指向其他动态分配内存的指针,必须在析构函数中正确处理这些资源的释放。

6.定位new表达式(placement-new)

定位new表达式,通常称为“placement new”,是C++中一种特殊的内存管理技术,它允许在已经分配的内存上构造对象。这种方式主要用于需要对内存使用进行精细控制的场合,如在特定的内存位置创建对象,或重用已经分配的内存以避免额外的内存分配开销。

6.1基本概念

定位new的基本语法和普通的new类似,但它需要一个额外的参数——一个指向已经分配的内存的指针。这允许定位new在这块预先存在的内存上直接构造对象,而不是像普通的new那样首先分配内存。

6.2使用场景

  • 内存池:定位new可以在预先分配的内存池中构造对象,提高内存分配效率和减少碎片化。
  • 缓冲区重用:在同一块内存区域多次构造、析构不同类型的对象,避免反复的内存分配和释放。
  • 硬件指定位置:在特定物理内存地址构造对象,常用于嵌入式系统或操作系统的底层编程。

6.3语法

#include <new>  // 必须包含头文件 <new>

void* place = ::operator new(sizeof(YourClass)); // 分配内存
YourClass* p = new(place) YourClass(arguments);  // 在place指向的地址构造对象

这里,place是一个指针,指向已分配的内存,YourClass(arguments)是构造函数的参数。

示例代码

以下示例演示了如何使用定位new:

#include <iostream>
#include <new>  // For std::nothrow and placement new

class Widget {
public:
    Widget() { std::cout << "Widget constructed\n"; }
    ~Widget() { std::cout << "Widget destroyed\n"; }
};

int main() {
    // 分配足够的内存
    void* buffer = ::operator new(sizeof(Widget));

    try {
        // 在buffer指定的内存上构造Widget对象
        Widget* w = new(buffer) Widget;

        // 使用对象...
        
        // 手动调用析构函数
        w->~Widget();
    } catch (...) {
        ::operator delete(buffer);
        throw;  // 重新抛出异常
    }

    // 释放内存
    ::operator delete(buffer);
    return 0;
}

在这个例子中,Widget类的对象被构造在通过::operator new手动分配的内存上。使用完毕后,需要手动调用析构函数来销毁对象,并且手动释放内存。

6.4注意事项

  • 内存管理:使用定位new时,必须自行管理内存的分配和释放,这增加了错误的风险。
  • 析构函数:定位new不会自动调用析构函数,因此需要手动调用析构函数来正确管理对象的生命周期。
  • 异常处理:构造函数中如果发生异常,需要适当处理已分配的内存,避免泄漏。

7.常见概念辨析

7.1malloc/free和new/delete的区别

在C++程序设计中,malloc/freenew/delete是两组用于内存管理的函数和操作符。虽然它们的基本目的相同——分配和释放内存——但它们之间存在几个重要的区别,这些区别影响了它们在实际编程中的使用和功能。

  1. 构造和析构

    • new/delete:不仅负责内存的分配和释放,还自动调用对象的构造函数和析构函数。这确保了对象的成员被正确初始化和清理。
    • malloc/free:仅仅分配和释放内存,不调用任何构造或析构函数。使用malloc分配内存后,内存中的内容是未初始化的。
  2. 类型安全

    • new/delete:是类型安全的,返回的是类型指定的指针,不需要类型转换。
    • malloc/freemalloc返回一个void*类型的指针,需要显式转换为需要的类型。
  3. 异常处理

    • new:默认情况下,如果内存分配失败,new会抛出std::bad_alloc异常。
    • malloc:分配内存失败时返回NULL,不抛出异常。这要求程序员必须检查返回值以确定内存分配是否成功。
  4. 内存分配方式

    • new:可以重载new操作符来改变对象的分配行为。
    • malloc:是一个库函数,不能被重载,行为不变。
  5. 便利性和功能

    • new/delete:可以更加方便地用于构造和析构类的对象,支持重载以提供自定义行为。
    • malloc/free:更适合用于分配一块原始内存区域,如分配一个大的字节数组。

示例代码

下面的代码清晰地展示了new/deletemalloc/free的使用差异:

#include <iostream>

class Widget {
public:
    Widget() { std::cout << "Widget constructed\n"; }
    ~Widget() { std::cout << "Widget destroyed\n"; }
};

int main() {
    // 使用new/delete
    Widget* w = new Widget;
    delete w;

    // 使用malloc/free
    Widget* w2 = (Widget*)malloc(sizeof(Widget)); // 只分配内存,不调用构造函数
    w2->Widget::Widget(); // 需要显式调用构造函数
    w2->~Widget(); // 需要显式调用析构函数
    free(w2);
}

在这个示例中,使用newdelete自动调用了Widget类的构造函数和析构函数。而使用mallocfree时,必须显式调用构造函数和析构函数,这增加了代码的复杂度并且容易出错。

7.2内存泄漏

内存泄漏是程序设计中常见的一种问题,特别是在使用手动内存管理的语言如C和C++中。内存泄漏指的是程序未能释放不再使用的内存,导致有效内存逐渐减少,最终可能影响程序性能或导致程序崩溃。

7.2.1内存泄漏的含义与危害

含义

  • 内存泄漏发生在已分配的内存不再被任何部分的程序代码引用,但由于某些原因未被释放或无法释放,从而造成系统内存的浪费。

危害

  • 性能下降:内存泄漏会逐渐消耗系统的内存资源,导致可用内存减少,影响系统性能。
  • 系统稳定性:长时间的内存泄漏可能导致内存不足,引发程序或系统崩溃。
  • 资源浪费:内存资源被无效占用,不能被其他程序或同一程序的其他部分利用。

7.2.2内存泄漏的分类

1. 显式泄漏

  • 发生在明确的动态内存分配和释放之间。例如,使用newmalloc分配的内存没有相应的deletefree来释放。

2. 隐式泄漏

  • 指内存仍被程序的活动部分所引用,但实际上已无用处,无法被再次利用。这通常涉及到数据结构错误或逻辑错误。

3. 资源泄漏

  • 涉及到非内存资源如文件句柄、数据库连接等,这些资源未被适时释放,虽不直接消耗物理内存,但也会导致资源耗尽。

7.2.3如何检测内存泄漏

1. 代码审查

  • 人工检查代码,寻找没有配对的new/deletemalloc/free

2. 运行时工具

  • 使用专门的工具如 ValgrindDr. MemoryVisual Leak Detector 等来监测运行时的内存分配和释放。
  • 开发环境和编译器内置工具,如Visual Studio的内存检测工具。

3. 日志和监控

  • 在代码中加入日志记录内存分配和释放的活动,帮助追踪可能的泄漏点。

7.2.4如何避免内存泄漏

1. 使用智能指针

  • 在C++中使用std::unique_ptrstd::shared_ptr等智能指针自动管理内存。智能指针在销毁时自动释放其指向的内存。我们将在后续的博客中对智能指针做详细介绍。

2. 资源获取即初始化 (RAII)

  • 将资源的生命周期绑定到对象的生命周期,确保通过对象的构造和析构自动管理资源。

3. 定期代码审查和重构

  • 定期进行代码审查,消除潜在的内存泄漏风险点。使用更安全的编程实践和模式。

4. 测试和验证

  • 实施严格的测试流程,包括单元测试、集成测试和系统测试,使用专门的工具定期检查内存泄漏。

8.结语

在本博客中,我们全面探讨了C++中的内存管理机制,从基本的内存布局到高级的内存操作技术,包括newdelete的使用、内存泄漏的问题及其防治措施。通过深入了解这些概念,开发者可以更有效地控制程序的资源使用,提高程序的性能和稳定性。希望本文的内容能帮助你避免常见的内存管理错误,编写出更加健壮和高效的代码。


原文地址:https://blog.csdn.net/wxk2227814847/article/details/137970639

免责声明:本站文章内容转载自网络资源,如本站内容侵犯了原著者的合法权益,可联系本站删除。更多内容请关注自学内容网(zxcms.com)!