彻底摘明白 C++ 的动态内存分配原理

简介: 大家好,我是V哥。C++的动态内存分配允许程序在运行时请求和释放内存,主要通过`new`/`delete`(用于对象)及`malloc`/`calloc`/`realloc`/`free`(继承自C语言)实现。`new`分配并初始化对象内存,`delete`释放并调用析构函数;而`malloc`等函数仅处理裸内存,不涉及构造与析构。掌握这些可有效管理内存,避免泄漏和悬空指针问题。智能指针如`std::unique_ptr`和`std::shared_ptr`能自动管理内存,确保异常安全。关注威哥爱编程,了解更多全栈开发技巧。先赞再看后评论,腰缠万贯财进门。

大家好,我是 V 哥。在C++中,动态内存分配允许程序在运行时请求和释放内存,这对于处理不确定大小的数据或者在程序执行过程中动态调整数据结构非常有用。C++主要通过newdelete运算符(用于对象)以及malloccallocreallocfree函数(继承自C语言)来实现动态内存分配,下面详细介绍它们的原理。先赞再看后评论,腰缠万贯财进门

1. newdelete运算符

原理概述

new运算符用于在堆上分配内存并调用对象的构造函数进行初始化,delete运算符用于释放new分配的内存并调用对象的析构函数。

详细步骤

new运算符
  1. 内存分配new运算符首先调用operator new函数来分配所需大小的内存块。operator new是一个全局函数,它通常会调用操作系统提供的内存分配函数(如malloc)来获取内存。
  2. 构造函数调用:如果分配内存成功,new运算符会调用对象的构造函数对分配的内存进行初始化。

示例代码:

#include <iostream>

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

int main() {
   
    // 使用new分配内存并调用构造函数
    MyClass* obj = new MyClass();
    // 使用delete释放内存并调用析构函数
    delete obj;
    return 0;
}
delete运算符
  1. 析构函数调用delete运算符首先调用对象的析构函数,用于清理对象占用的资源(如关闭文件、释放动态分配的子对象等)。
  2. 内存释放:调用operator delete函数释放之前分配的内存。operator delete通常会调用操作系统提供的内存释放函数(如free)。

数组的动态分配

使用new[]delete[]可以动态分配和释放数组。new[]会为数组中的每个元素调用构造函数,delete[]会为数组中的每个元素调用析构函数。

#include <iostream>

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

int main() {
   
    // 使用new[]分配数组内存并调用构造函数
    MyClass* arr = new MyClass[3];
    // 使用delete[]释放数组内存并调用析构函数
    delete[] arr;
    return 0;
}

2. malloccallocreallocfree函数

原理概述

这些函数是C语言标准库提供的动态内存分配函数,C++为了兼容C语言也支持这些函数。它们不涉及对象的构造和析构,只是简单地分配和释放内存。

详细介绍

malloc函数

malloc函数用于分配指定大小的内存块,返回一个指向该内存块的指针。如果分配失败,返回NULL

#include <iostream>
#include <cstdlib>

int main() {
   
    // 分配10个int类型的内存空间
    int* ptr = (int*)malloc(10 * sizeof(int));
    if (ptr != NULL) {
   
        // 使用内存
        for (int i = 0; i < 10; ++i) {
   
            ptr[i] = i;
        }
        // 释放内存
        free(ptr);
    }
    return 0;
}
calloc函数

calloc函数用于分配指定数量和大小的内存块,并将所有字节初始化为0。

#include <iostream>
#include <cstdlib>

int main() {
   
    // 分配10个int类型的内存空间并初始化为0
    int* ptr = (int*)calloc(10, sizeof(int));
    if (ptr != NULL) {
   
        // 释放内存
        free(ptr);
    }
    return 0;
}
realloc函数

realloc函数用于重新分配已经分配的内存块的大小。如果新的大小比原来的大,可能会在堆上移动内存块;如果新的大小比原来的小,会截断内存块。

#include <iostream>
#include <cstdlib>

int main() {
   
    // 分配10个int类型的内存空间
    int* ptr = (int*)malloc(10 * sizeof(int));
    if (ptr != NULL) {
   
        // 重新分配为20个int类型的内存空间
        int* newPtr = (int*)realloc(ptr, 20 * sizeof(int));
        if (newPtr != NULL) {
   
            ptr = newPtr;
            // 释放内存
            free(ptr);
        }
    }
    return 0;
}
free函数

free函数用于释放malloccallocrealloc分配的内存。

#include <iostream>
#include <cstdlib>

int main() {
   
    int* ptr = (int*)malloc(10 * sizeof(int));
    if (ptr != NULL) {
   
        // 释放内存
        free(ptr);
    }
    return 0;
}

内存管理的注意事项

  • 内存泄漏:如果使用newmalloc分配的内存没有使用deletefree释放,会导致内存泄漏,程序运行时会逐渐耗尽可用内存。
  • 悬空指针:释放内存后,指向该内存的指针成为悬空指针,继续使用悬空指针会导致未定义行为。
  • 不匹配使用new必须与delete配对使用,new[]必须与delete[]配对使用,malloccallocrealloc必须与free配对使用。

如何避免动态内存分配导致的内存泄漏?

在C++中,动态内存分配如果管理不当很容易导致内存泄漏,即程序中已分配的内存不再被使用,但却没有被释放,随着程序的运行,可用内存会逐渐减少。以下是一些避免动态内存分配导致内存泄漏的方法:

1. 遵循配对原则

在使用动态内存分配时,要确保newdeletenew[]delete[]malloc/calloc/reallocfree正确配对使用。

示例代码

#include <iostream>
#include <cstdlib>

int main() {
   
    // 使用new分配单个对象
    int* singlePtr = new int(10);
    // 使用delete释放单个对象
    delete singlePtr;

    // 使用new[]分配数组
    int* arrayPtr = new int[5];
    // 使用delete[]释放数组
    delete[] arrayPtr;

    // 使用malloc分配内存
    char* charPtr = (char*)malloc(10 * sizeof(char));
    // 使用free释放内存
    free(charPtr);

    return 0;
}

2. 使用智能指针

智能指针是C++标准库提供的一种类模板,它可以自动管理动态分配的内存,当智能指针的生命周期结束时,会自动释放所指向的内存。

2.1 std::unique_ptr

std::unique_ptr是一种独占式智能指针,同一时间只能有一个std::unique_ptr指向同一个对象,当它离开作用域时,会自动释放所指向的内存。

示例代码

#include <iostream>
#include <memory>

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

int main() {
   
    // 创建std::unique_ptr
    std::unique_ptr<MyClass> ptr = std::make_unique<MyClass>();
    // 不需要手动释放内存,ptr离开作用域时会自动释放
    return 0;
}
2.2 std::shared_ptr

std::shared_ptr是一种共享式智能指针,多个std::shared_ptr可以指向同一个对象,使用引用计数来管理对象的生命周期,当引用计数为0时,会自动释放所指向的内存。

示例代码

#include <iostream>
#include <memory>

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

int main() {
   
    // 创建std::shared_ptr
    std::shared_ptr<MyClass> ptr1 = std::make_shared<MyClass>();
    std::shared_ptr<MyClass> ptr2 = ptr1;  // 引用计数加1
    // 当ptr1和ptr2都离开作用域时,引用计数变为0,自动释放内存
    return 0;
}

3. 异常安全

在使用动态内存分配时,要确保在发生异常的情况下也能正确释放内存。可以使用try-catch块来捕获异常,并在catch块中释放内存。但使用智能指针可以更方便地实现异常安全。

示例代码(使用智能指针实现异常安全)

#include <iostream>
#include <memory>
#include <stdexcept>

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

void func() {
   
    std::unique_ptr<MyClass> ptr = std::make_unique<MyClass>();
    // 可能会抛出异常的代码
    if (true) {
   
        throw std::runtime_error("An error occurred");
    }
    // 不需要手动释放内存,ptr离开作用域时会自动释放
}

int main() {
   
    try {
   
        func();
    } catch (const std::exception& e) {
   
        std::cout << "Exception caught: " << e.what() << std::endl;
    }
    return 0;
}

4. 封装动态内存管理

将动态内存管理封装在类中,通过类的构造函数分配内存,析构函数释放内存,遵循RAII(资源获取即初始化)原则。

示例代码

#include <iostream>

class MemoryWrapper {
   
private:
    int* data;
public:
    MemoryWrapper() {
   
        data = new int[10];
        std::cout << "Memory allocated" << std::endl;
    }
    ~MemoryWrapper() {
   
        delete[] data;
        std::cout << "Memory freed" << std::endl;
    }
};

int main() {
   
    MemoryWrapper wrapper;
    // 当wrapper离开作用域时,析构函数会自动释放内存
    return 0;
}

5. 代码审查和静态分析工具

定期进行代码审查,检查动态内存分配和释放的代码是否正确。同时,可以使用静态分析工具(如Clang Static Analyzer、Cppcheck等)来帮助检测潜在的内存泄漏问题。

最后

理解 C++的动态内存分配原理,以及掌握如何避免动态内存分配导致的内存泄漏?是在开发中非常关键的知识,这篇文章希望可以帮助到你,关注威哥爱编程,全栈开发就你行。

相关文章
|
1月前
|
缓存 算法 程序员
C++STL底层原理:探秘标准模板库的内部机制
🌟蒋星熠Jaxonic带你深入STL底层:从容器内存管理到红黑树、哈希表,剖析迭代器、算法与分配器核心机制,揭秘C++标准库的高效设计哲学与性能优化实践。
C++STL底层原理:探秘标准模板库的内部机制
|
7月前
|
机器学习/深度学习 存储 算法
NoProp:无需反向传播,基于去噪原理的非全局梯度传播神经网络训练,可大幅降低内存消耗
反向传播算法虽是深度学习基石,但面临内存消耗大和并行扩展受限的问题。近期,牛津大学等机构提出NoProp方法,通过扩散模型概念,将训练重塑为分层去噪任务,无需全局前向或反向传播。NoProp包含三种变体(DT、CT、FM),具备低内存占用与高效训练优势,在CIFAR-10等数据集上达到与传统方法相当的性能。其层间解耦特性支持分布式并行训练,为无梯度深度学习提供了新方向。
244 1
NoProp:无需反向传播,基于去噪原理的非全局梯度传播神经网络训练,可大幅降低内存消耗
|
6月前
|
存储 缓存 Java
【高薪程序员必看】万字长文拆解Java并发编程!(5):深入理解JMM:Java内存模型的三大特性与volatile底层原理
JMM,Java Memory Model,Java内存模型,定义了主内存,工作内存,确保Java在不同平台上的正确运行主内存Main Memory:所有线程共享的内存区域,所有的变量都存储在主存中工作内存Working Memory:每个线程拥有自己的工作内存,用于保存变量的副本.线程执行过程中先将主内存中的变量读到工作内存中,对变量进行操作之后再将变量写入主内存,jvm概念说明主内存所有线程共享的内存区域,存储原始变量(堆内存中的对象实例和静态变量)工作内存。
217 0
|
4月前
|
安全 C语言 C++
比较C++的内存分配与管理方式new/delete与C语言中的malloc/realloc/calloc/free。
在实用性方面,C++的内存管理方式提供了面向对象的特性,它是处理构造和析构、需要类型安全和异常处理的首选方案。而C语言的内存管理函数适用于简单的内存分配,例如分配原始内存块或复杂性较低的数据结构,没有构造和析构的要求。当从C迁移到C++,或在C++中使用C代码时,了解两种内存管理方式的差异非常重要。
158 26
|
9月前
|
存储 程序员 编译器
玩转C++内存管理:从新手到高手的必备指南
C++中的内存管理是编写高效、可靠程序的关键所在。C++不仅继承了C语言的内存管理方式,还增加了面向对象的内存分配机制,使得内存管理既有灵活性,也更加复杂。学习内存管理不仅有助于提升程序效率,还有助于理解计算机的工作原理和资源分配策略。
|
5月前
|
C语言 C++
c与c++的内存管理
再比如还有这样的分组: 这种分组是最正确的给出内存四个分区名字:栈区、堆区、全局区(俗话也叫静态变量区)、代码区(也叫代码段)(代码段又分很多种,比如常量区)当然也会看到别的定义如:两者都正确,记那个都选,我选择的是第一个。再比如还有这样的分组: 这种分组是最正确的答案分别是 C C C A A A A A D A B。
85 1
|
11月前
|
存储 缓存 编译器
【硬核】C++11并发:内存模型和原子类型
本文从C++11并发编程中的关键概念——内存模型与原子类型入手,结合详尽的代码示例,抽丝剥茧地介绍了如何实现无锁化并发的性能优化。
456 68
|
8月前
|
存储 Linux C语言
C++/C的内存管理
本文主要讲解C++/C中的程序区域划分与内存管理方式。首先介绍程序区域,包括栈(存储局部变量等,向下增长)、堆(动态内存分配,向上分配)、数据段(存储静态和全局变量)及代码段(存放可执行代码)。接着探讨C++内存管理,new/delete操作符相比C语言的malloc/free更强大,支持对象构造与析构。还深入解析了new/delete的实现原理、定位new表达式以及二者与malloc/free的区别。最后附上一句鸡汤激励大家行动缓解焦虑。
|
10月前
|
存储 程序员 编译器
什么是内存泄漏?C++中如何检测和解决?
大家好,我是V哥。内存泄露是编程中的常见问题,可能导致程序崩溃。特别是在金三银四跳槽季,面试官常问此问题。本文将探讨内存泄露的定义、危害、检测方法及解决策略,帮助你掌握这一关键知识点。通过学习如何正确管理内存、使用智能指针和RAII原则,避免内存泄露,提升代码健壮性。同时,了解常见的内存泄露场景,如忘记释放内存、异常处理不当等,确保在面试中不被秒杀。最后,预祝大家新的一年工作顺利,涨薪多多!关注威哥爱编程,一起成为更好的程序员。
446 0
|
9月前
|
编译器 C++ 开发者
【C++篇】深度解析类与对象(下)
在上一篇博客中,我们学习了C++的基础类与对象概念,包括类的定义、对象的使用和构造函数的作用。在这一篇,我们将深入探讨C++类的一些重要特性,如构造函数的高级用法、类型转换、static成员、友元、内部类、匿名对象,以及对象拷贝优化等。这些内容可以帮助你更好地理解和应用面向对象编程的核心理念,提升代码的健壮性、灵活性和可维护性。

热门文章

最新文章