0

0

C++如何在类中管理动态内存

P粉602998670

P粉602998670

发布时间:2025-09-14 14:08:01

|

570人浏览过

|

来源于php中文网

原创

C++类中管理动态内存不能依赖默认行为,因默认拷贝为浅拷贝,导致多对象共享同一内存,引发双重释放或悬空指针;需通过自定义析构函数、拷贝构造与赋值函数实现深拷贝,结合移动语义提升效率;现代C++推荐使用智能指针(如unique_ptr、shared_ptr)实现RAII,自动管理内存生命周期,遵循“零法则”,避免手动管理错误。

c++如何在类中管理动态内存

在C++类中管理动态内存,核心在于遵循“三/五/零法则”,即通过自定义析构函数、拷贝构造函数和拷贝赋值运算符来处理资源的生命周期,以避免诸如双重释放、内存泄漏等常见问题。现代C++更倾向于使用智能指针,将这些繁琐的手动管理工作交给标准库,从而实现“零法则”,大幅提升代码的健壮性和可维护性。

解决方案

说实话,C++里类对动态内存的管理,在我看来,就是对资源所有权和生命周期的一种精确控制。当一个类内部持有动态分配的资源(比如通过

new
分配的数组或对象),我们就不能简单地依赖编译器默认生成的成员函数。默认的拷贝构造和赋值操作只会进行“浅拷贝”,这意味着它们仅仅复制指针本身的值,而不是指针所指向的数据。结果就是,多个对象可能指向同一块内存,一旦其中一个对象被销毁,它会释放这块内存,而其他对象持有的指针就成了“悬空指针”,后续访问或再次释放就会导致程序崩溃。

要解决这个问题,我们必须手动实现“深拷贝”机制。这意味着在拷贝构造和赋值时,我们不仅要复制指针,更要为新对象分配一块独立的内存,并将原始对象的数据复制过去。同时,析构函数必须负责释放本对象所拥有的动态内存。

随着C++11的到来,移动语义的引入又为动态内存管理增添了新的维度。移动操作允许我们“窃取”临时对象或即将销毁对象所拥有的资源,而不是进行昂贵的深拷贝。这通过移动构造函数和移动赋值运算符实现,它们通常会将源对象的指针置空,从而避免了源对象析构时释放资源的风险。

立即学习C++免费学习笔记(深入)”;

最终,现代C++的趋势是尽可能地避免手动管理动态内存。智能指针(如

std::unique_ptr
std::shared_ptr
)的出现,让我们可以将动态内存的生命周期管理委托给这些RAII(Resource Acquisition Is Initialization)风格的包装器。这样一来,当智能指针对象超出作用域时,它会自动释放所管理的内存,极大地简化了代码,也减少了出错的可能。

为什么C++类中管理动态内存不能仅仅依赖默认行为?

这其实是个很经典的坑,很多初学者都会在这里摔跟头,我当年也不例外。关键点在于,C++编译器很“聪明”,但它的“聪明”是基于最普遍的场景。对于像

int
double
这样的基本类型,或者那些不包含动态内存的复杂类型,默认的拷贝和赋值行为(成员逐一拷贝)是完全没问题的。但一旦你的类成员中出现了裸指针(
T*
)指向动态分配的内存,问题就来了。

想象一下,你有一个类

MyArray
,它内部有一个
int* data
成员,指向一个动态分配的整数数组。

class MyArray {
public:
    int* data;
    size_t size;

    MyArray(size_t s) : size(s), data(new int[s]) {}
    // ... 缺少析构函数、拷贝构造、拷贝赋值
};

int main() {
    MyArray arr1(10);
    // 假设 arr1.data 指向地址 0x1000

    MyArray arr2 = arr1; // 默认拷贝构造
    // 此时 arr2.data 也指向 0x1000,和 arr1.data 指向同一块内存

    // ... arr1 和 arr2 使用各自的 data

    // 当 arr2 超出作用域,它的默认析构函数(如果存在)不会释放 data
    // 但如果 MyArray 有一个析构函数:~MyArray() { delete[] data; }
    // 那么 arr2 析构时会释放 0x1000
    // 接着 arr1 析构时,又会尝试释放 0x1000,这就是“双重释放”
    // 或者,如果 arr2 析构后,arr1 还在使用 0x1000,那就是“悬空指针”访问
}

你看,默认的拷贝操作只是简单地复制了

data
指针的值,并没有为
arr2
分配新的内存。结果就是
arr1.data
arr2.data
都指向了同一块堆内存。这就像你把一张房产证复印给了两个人,但房子只有一栋。当其中一个人“处理掉”了房子(释放了内存),另一个人手里的房产证就成了废纸,再拿去处理就会出大问题。这就是所谓的“浅拷贝”带来的“双重释放”和“悬空指针”问题,它们是程序崩溃和内存损坏的常见原因。

如何实现C++类中的深拷贝与移动语义?

要妥善管理类中的动态内存,我们就需要亲手操刀,实现那些编译器默认行为不符合我们需求的成员函数。这通常包括析构函数、拷贝构造函数、拷贝赋值运算符,以及C++11引入的移动构造函数和移动赋值运算符。

  1. 析构函数 (

    ~MyClass()
    ): 这是最基础的。当对象生命周期结束时,它负责释放由该对象拥有的动态内存。

    ~MyArray() {
        delete[] data; // 释放 data 指向的数组内存
        data = nullptr; // 良好的习惯,将指针置空
    }

    这里,我个人觉得,

    data = nullptr;
    这一步虽然不是严格必须,但对于调试和防止意外使用悬空指针来说,是个好习惯。

    e网企业2.0
    e网企业2.0

    一款适用于中小企业自助建站程序,是c#与xml技术相结合的产物,支持动态设定二级栏目,采用了开放式架构,建站模版自由添加。程序整合了(单一文本,新闻列表,图片列表 ,在线订单, 文件下载 , 留言板)六类插件,以所见即所得的方式,将烦锁的建站过程简化到三步,使用户可以轻松上手。 管理后台:manage.aspx 初始密码均为admin

    下载
  2. 拷贝构造函数 (

    MyClass(const MyClass& other)
    ): 当一个新对象通过另一个同类型对象初始化时被调用(例如
    MyArray arr2 = arr1;
    )。它必须为新对象分配独立的内存,并将源对象的数据复制过来。

    MyArray(const MyArray& other) : size(other.size) {
        if (size > 0) {
            data = new int[size];
            std::copy(other.data, other.data + size, data);
        } else {
            data = nullptr; // 处理空数组情况
        }
    }

    注意,这里我加了一个

    if (size > 0)
    判断,避免为零长度数组分配内存,虽然
    new int[0]
    是合法的,但这样处理更清晰。

  3. 拷贝赋值运算符 (

    MyClass& operator=(const MyClass& other)
    ): 当一个已存在的对象被另一个同类型对象赋值时被调用(例如
    arr2 = arr1;
    )。这里处理起来要稍微复杂一些,因为它涉及到一个已存在的对象,可能已经拥有资源。我们需要先释放旧资源,再分配新资源并复制数据,同时还要处理自我赋值的情况。

    MyArray& operator=(const MyArray& other) {
        if (this != &other) { // 防止自我赋值:arr1 = arr1;
            // 释放当前对象旧的资源
            delete[] data;
    
            // 分配新资源并拷贝数据
            size = other.size;
            if (size > 0) {
                data = new int[size];
                std::copy(other.data, other.data + size, data);
            } else {
                data = nullptr;
            }
        }
        return *this; // 返回当前对象的引用
    }

    自我赋值检查(

    if (this != &other)
    )是至关重要的,否则在
    arr1 = arr1;
    这种情况下,
    delete[] data;
    会提前释放掉
    arr1
    自己的数据,导致后续拷贝操作出错。

  4. 移动构造函数 (

    MyClass(MyClass&& other) noexcept
    ): C++11引入,用于从右值(通常是临时对象或即将销毁的对象)“窃取”资源。这比深拷贝效率高得多,因为它避免了内存分配和数据复制。
    noexcept
    是强烈建议的,表示此操作不会抛出异常。

    MyArray(MyArray&& other) noexcept
        : data(other.data), size(other.size) { // 直接接管资源
        other.data = nullptr; // 将源对象的指针置空
        other.size = 0;       // 将源对象的大小置零
    }

    这里,我们只是简单地将源对象的指针和大小“偷”过来,然后将源对象置于一个有效的、可析构的状态(指针置空,大小为零)。

  5. 移动赋值运算符 (

    MyClass& operator=(MyClass&& other) noexcept
    ): 同样用于从右值移动资源到已存在的对象。它也需要处理自我赋值和释放旧资源。

    MyArray& operator=(MyArray&& other) noexcept {
        if (this != &other) { // 防止自我赋值
            delete[] data; // 释放当前对象旧的资源
    
            // 移动资源
            data = other.data;
            size = other.size;
    
            // 将源对象置空
            other.data = nullptr;
            other.size = 0;
        }
        return *this;
    }

    通过这“五大金刚”,我们才能确保类在处理动态内存时行为正确、高效。这工作量看起来不小,也容易出错,这也是为什么现代C++更推崇智能指针的原因。

C++现代实践中,智能指针如何简化类内动态内存管理?

说真的,自从智能指针普及开来,我个人在写C++代码时,已经很少直接使用裸指针来管理类内部的动态内存了。智能指针简直就是动态内存管理领域的“救星”,它彻底改变了我们处理资源生命周期的方式,让“三/五法则”在很多情况下变得不再必要,这也就是所谓的“零法则”。

核心思想是RAII(Resource Acquisition Is Initialization,资源获取即初始化)。智能指针在构造时获取资源(动态内存),在析构时自动释放资源。这意味着,一旦你把动态内存的管理权交给了智能指针,你就几乎不用再担心内存泄漏、双重释放或者悬空指针的问题了。

举个例子,如果我们的

MyArray
类使用
std::unique_ptr
来管理其内部的动态数组:

#include  // 包含智能指针头文件
#include  // 用于 std::copy

class MyArraySmart {
public:
    std::unique_ptr data; // 使用 unique_ptr 管理动态数组
    size_t size;

    // 构造函数:分配内存并初始化 unique_ptr
    MyArraySmart(size_t s) : size(s) {
        if (size > 0) {
            data = std::make_unique(size); // 使用 make_unique 分配内存
        }
        // else data 保持 nullptr,unique_ptr 默认构造就是空的
    }

    // 拷贝构造函数:unique_ptr 不支持拷贝,需要手动深拷贝
    MyArraySmart(const MyArraySmart& other) : size(other.size) {
        if (size > 0) {
            data = std::make_unique(size);
            std::copy(other.data.get(), other.data.get() + size, data.get());
        }
    }

    // 拷贝赋值运算符:类似拷贝构造,手动深拷贝
    MyArraySmart& operator=(const MyArraySmart& other) {
        if (this != &other) {
            // unique_ptr 会自动释放旧资源,我们只需要重新分配和拷贝
            size = other.size;
            if (size > 0) {
                data = std::make_unique(size);
                std::copy(other.data.get(), other.data.get() + size, data.get());
            } else {
                data.reset(); // 释放并置空
            }
        }
        return *this;
    }

    // 移动构造函数和移动赋值运算符:unique_ptr 支持移动语义,默认生成就够了
    // MyArraySmart(MyArraySmart&&) = default;
    // MyArraySmart& operator=(MyArraySmart&&) = default;

    // 析构函数:unique_ptr 会自动释放内存,无需手动编写
    // ~MyArraySmart() = default;
};

可以看到,即使使用了

unique_ptr
,如果类需要拷贝语义,我们仍然需要手动实现拷贝构造和拷贝赋值。这是因为
std::unique_ptr
是独占所有权的,它不能被拷贝,只能被移动。但即便如此,我们至少不用再手动调用
delete[]
了,这已经是一个巨大的进步。

那么,什么时候用

std::unique_ptr
,什么时候用
std::shared_ptr
呢?

  • std::unique_ptr
    :当资源是独占的,只有一个所有者时使用。它提供了严格的所有权语义,效率很高,没有引用计数的开销。如果你确定一个对象只会被一个类实例拥有,并且在那个实例销毁时资源也应该被释放,那么
    unique_ptr
    是首选。
  • std::shared_ptr
    :当资源需要被多个对象共享所有权时使用。它通过引用计数来管理资源的生命周期,只有当最后一个
    shared_ptr
    对象被销毁时,资源才会被释放。它的开销比
    unique_unique_ptr
    稍大,因为它需要维护引用计数。比如,如果你有一个配置对象,可能被多个服务模块引用,那么
    shared_ptr
    就非常合适。

在我看来,现代C++编程,尽可能地拥抱智能指针是一种最佳实践。它能让你的代码更简洁、更安全,也能让你把更多精力放在业务逻辑上,而不是繁琐的内存管理细节。当然,理解裸指针管理动态内存的原理依然重要,毕竟智能指针也是基于这些原理构建的,而且总有一些特殊场景需要我们直接与底层内存打交道。但对于日常开发,智能指针绝对是首选。

热门AI工具

更多
DeepSeek
DeepSeek

幻方量化公司旗下的开源大模型平台

豆包大模型
豆包大模型

字节跳动自主研发的一系列大型语言模型

通义千问
通义千问

阿里巴巴推出的全能AI助手

腾讯元宝
腾讯元宝

腾讯混元平台推出的AI助手

文心一言
文心一言

文心一言是百度开发的AI聊天机器人,通过对话可以生成各种形式的内容。

讯飞写作
讯飞写作

基于讯飞星火大模型的AI写作工具,可以快速生成新闻稿件、品宣文案、工作总结、心得体会等各种文文稿

即梦AI
即梦AI

一站式AI创作平台,免费AI图片和视频生成。

ChatGPT
ChatGPT

最最强大的AI聊天机器人程序,ChatGPT不单是聊天机器人,还能进行撰写邮件、视频脚本、文案、翻译、代码等任务。

相关专题

更多
resource是什么文件
resource是什么文件

Resource文件是一种特殊类型的文件,它通常用于存储应用程序或操作系统中的各种资源信息。它们在应用程序开发中起着关键作用,并在跨平台开发和国际化方面提供支持。本专题为大家提供相关的文章、下载、课程内容,供大家免费下载体验。

158

2023.12.20

java基础知识汇总
java基础知识汇总

java基础知识有Java的历史和特点、Java的开发环境、Java的基本数据类型、变量和常量、运算符和表达式、控制语句、数组和字符串等等知识点。想要知道更多关于java基础知识的朋友,请阅读本专题下面的的有关文章,欢迎大家来php中文网学习。

1503

2023.10.24

Go语言中的运算符有哪些
Go语言中的运算符有哪些

Go语言中的运算符有:1、加法运算符;2、减法运算符;3、乘法运算符;4、除法运算符;5、取余运算符;6、比较运算符;7、位运算符;8、按位与运算符;9、按位或运算符;10、按位异或运算符等等。本专题为大家提供相关的文章、下载、课程内容,供大家免费下载体验。

233

2024.02.23

php三元运算符用法
php三元运算符用法

本专题整合了php三元运算符相关教程,阅读专题下面的文章了解更多详细内容。

87

2025.10.17

if什么意思
if什么意思

if的意思是“如果”的条件。它是一个用于引导条件语句的关键词,用于根据特定条件的真假情况来执行不同的代码块。本专题提供if什么意思的相关文章,供大家免费阅读。

781

2023.08.22

c语言const用法
c语言const用法

const是关键字,可以用于声明常量、函数参数中的const修饰符、const修饰函数返回值、const修饰指针。详细介绍:1、声明常量,const关键字可用于声明常量,常量的值在程序运行期间不可修改,常量可以是基本数据类型,如整数、浮点数、字符等,也可是自定义的数据类型;2、函数参数中的const修饰符,const关键字可用于函数的参数中,表示该参数在函数内部不可修改等等。

531

2023.09.20

string转int
string转int

在编程中,我们经常会遇到需要将字符串(str)转换为整数(int)的情况。这可能是因为我们需要对字符串进行数值计算,或者需要将用户输入的字符串转换为整数进行处理。php中文网给大家带来了相关的教程以及文章,欢迎大家前来学习阅读。

483

2023.08.02

int占多少字节
int占多少字节

int占4个字节,意味着一个int变量可以存储范围在-2,147,483,648到2,147,483,647之间的整数值,在某些情况下也可能是2个字节或8个字节,int是一种常用的数据类型,用于表示整数,需要根据具体情况选择合适的数据类型,以确保程序的正确性和性能。本专题为大家提供相关的文章、下载、课程内容,供大家免费下载体验。

545

2024.08.29

2026赚钱平台入口大全
2026赚钱平台入口大全

2026年最新赚钱平台入口汇总,涵盖任务众包、内容创作、电商运营、技能变现等多类正规渠道,助你轻松开启副业增收之路。阅读专题下面的文章了解更多详细内容。

33

2026.01.31

热门下载

更多
网站特效
/
网站源码
/
网站素材
/
前端模板

精品课程

更多
相关推荐
/
热门推荐
/
最新课程
Go 教程
Go 教程

共32课时 | 4.4万人学习

Go语言实战之 GraphQL
Go语言实战之 GraphQL

共10课时 | 0.8万人学习

关于我们 免责申明 举报中心 意见反馈 讲师合作 广告合作 最新更新
php中文网:公益在线php培训,帮助PHP学习者快速成长!
关注服务号 技术交流群
PHP中文网订阅号
每天精选资源文章推送

Copyright 2014-2026 https://www.php.cn/ All Rights Reserved | php.cn | 湘ICP备2023035733号