讲师中心 微信公众号
AI工具推荐 视频效率加速

如何编写一个遵循“三/五/零之法则”的C++类来管理内存

浅瑶吖_7898

浅瑶吖_7898

发布时间:2025-09-02 08:22:01

|

997人浏览过

|

来源于php中文网

原创

遵循“三/五/零法则”确保C++类正确管理资源:无需手动管理资源时遵循零法则,编译器自动生成默认函数;需管理资源时显式定义析构函数、拷贝构造、拷贝赋值、移动构造和移动赋值函数,防止内存泄漏、悬挂指针和双重释放;使用智能指针如std::unique_ptr和std::shared_ptr可自动管理资源,结合移动语义避免不必要的拷贝,提升性能。

如何编写一个遵循“三/五/零之法则”的c++类来管理内存

编写遵循“三/五/零法则”的 C++ 类,关键在于明确类的资源管理职责。如果类需要管理资源(如动态分配的内存),就需要遵循“三/五法则”;如果不需要,就遵循“零法则”。

解决方案

零法则(Rule of Zero):

最简单的情况。如果你的类不需要显式管理任何资源(例如,它只包含

int
、
double
、
std::string
等成员,这些成员本身负责它们的资源管理),那么你不需要定义析构函数、拷贝构造函数或拷贝赋值运算符。编译器会自动生成它们,并且通常是正确的。

#include <string>

class Person {
public:
    Person(std::string name, int age) : name_(name), age_(age) {}

private:
    std::string name_;
    int age_;
}; // 编译器会自动生成析构函数、拷贝构造函数和拷贝赋值运算符

三/五法则(Rule of Three/Five):

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

如果你的类需要管理资源,比如动态分配内存,那么你需要显式地定义析构函数、拷贝构造函数和拷贝赋值运算符(这是“三法则”)。 C++11 引入了移动构造函数和移动赋值运算符,因此“三法则”扩展为“五法则”。

考虑一个简单的字符串类:

#include <cstring> // 包含 strlen, strcpy, delete[]

class MyString {
public:
    // 构造函数
    MyString(const char* str = nullptr) {
        if (str) {
            size_ = std::strlen(str);
            data_ = new char[size_ + 1];
            std::strcpy(data_, str);
        } else {
            size_ = 0;
            data_ = new char[1];
            data_[0] = '\0';
        }
    }

    // 析构函数
    ~MyString() {
        delete[] data_;
    }

    // 拷贝构造函数
    MyString(const MyString& other) {
        size_ = other.size_;
        data_ = new char[size_ + 1];
        std::strcpy(data_, other.data_);
    }

    // 拷贝赋值运算符
    MyString& operator=(const MyString& other) {
        if (this != &other) { // 防止自赋值
            delete[] data_;   // 释放旧资源
            size_ = other.size_;
            data_ = new char[size_ + 1];
            std::strcpy(data_, other.data_);
        }
        return *this;
    }

    // 移动构造函数 (C++11)
    MyString(MyString&& other) noexcept : size_(other.size_), data_(other.data_) {
        other.size_ = 0;
        other.data_ = nullptr;
    }

    // 移动赋值运算符 (C++11)
    MyString& operator=(MyString&& other) noexcept {
        if (this != &other) {
            delete[] data_;
            size_ = other.size_;
            data_ = other.data_;
            other.size_ = 0;
            other.data_ = nullptr;
        }
        return *this;
    }

    const char* c_str() const { return data_; }

private:
    size_t size_;
    char* data_;
};

解释:

  • 构造函数: 分配内存并复制字符串。
  • 析构函数: 释放分配的内存。
  • 拷贝构造函数: 创建一个新对象,并从另一个对象复制数据(深拷贝)。
  • 拷贝赋值运算符: 避免自赋值,释放当前对象的内存,然后从另一个对象复制数据(深拷贝)。
  • 移动构造函数: “窃取”另一个对象的资源,并将另一个对象置于有效但未定义的状态。
    noexcept
    说明符表示这个操作不会抛出异常,这对于移动操作非常重要。
  • 移动赋值运算符: 类似于移动构造函数,但需要先释放当前对象的资源。

副标题1

为什么需要遵循“三/五/零法则”?不遵循会有什么问题?

不遵循“三/五/零法则”会导致各种资源管理问题,最常见的是内存泄漏和悬挂指针。

  • 内存泄漏: 如果你分配了内存但没有在析构函数中释放它,那么每次创建和销毁对象时都会泄漏内存。随着时间的推移,这会导致程序耗尽所有可用内存并崩溃。
  • 悬挂指针: 如果你复制了一个对象,但两个对象都指向同一块内存,那么当其中一个对象被销毁时,另一个对象将持有一个指向已释放内存的指针。访问这个指针会导致未定义的行为,通常是程序崩溃。
  • 双重释放: 如果你浅拷贝一个对象,然后销毁这两个对象,析构函数会被调用两次,试图释放同一块内存两次,这也会导致崩溃。
  • 性能问题: 默认的拷贝构造函数和拷贝赋值运算符可能会执行不必要的拷贝操作,尤其是在处理大型对象时。移动语义可以避免这些拷贝操作,提高性能。

副标题2

如何使用智能指针简化资源管理,并避免手动管理内存?

智能指针(如

std::unique_ptr
、
std::shared_ptr
和
std::weak_ptr
)可以自动管理动态分配的内存,从而避免手动管理内存的需要。它们是RAII(Resource Acquisition Is Initialization)原则的体现,即在对象构造时获取资源,在对象销毁时释放资源。

C++ 算法竞赛自动化测试数据生成与校验框架
C++ 算法竞赛自动化测试数据生成与校验框架

根据原题生成新题面、验证器及完整测试数据,自动套用 testlib 模板,用于用户要求生成测试数据时。

下载
  • std::unique_ptr
    : 用于独占所有权的场景。一个
    std::unique_ptr
    只能指向一个对象,并且当
    std::unique_ptr
    被销毁时,它指向的对象也会被销毁。

    #include <memory>
    
    class MyClass {
    public:
        MyClass() {
            data_ = new int[10];
        }
        ~MyClass() {
            delete[] data_;
        }
    private:
        int* data_;
    };
    
    int main() {
        std::unique_ptr<MyClass> ptr(new MyClass()); // 使用 unique_ptr 管理 MyClass 对象
        // 当 ptr 超出作用域时,MyClass 对象会被自动销毁,data_ 也会被释放
        return 0;
    }

    使用

    std::unique_ptr
    后,你不需要显式地定义析构函数、拷贝构造函数和拷贝赋值运算符,因为
    std::unique_ptr
    已经为你处理了资源管理。
    unique_ptr
    不支持拷贝,只支持移动,这符合独占所有权的语义。

  • std::shared_ptr
    : 用于共享所有权的场景。多个
    std::shared_ptr
    可以指向同一个对象,并且只有当所有
    std::shared_ptr
    都被销毁时,该对象才会被销毁。
    shared_ptr
    使用引用计数来跟踪有多少个指针指向同一个对象。

    #include <memory>
    
    int main() {
        std::shared_ptr<int> ptr1(new int(10));
        std::shared_ptr<int> ptr2 = ptr1; // ptr1 和 ptr2 共享同一个 int 对象
    
        // 当 ptr1 和 ptr2 都超出作用域时,int 对象才会被销毁
        return 0;
    }

    使用

    std::shared_ptr
    可以简化资源管理,但也需要注意循环引用的问题,循环引用会导致对象永远不会被销毁。可以使用
    std::weak_ptr
    来打破循环引用。

  • std::weak_ptr
    :
    std::weak_ptr
    是一种不拥有对象所有权的智能指针。 它指向由
    std::shared_ptr
    管理的对象,但不会增加引用计数。
    std::weak_ptr
    可以用来检测对象是否仍然存在。

副标题3

移动语义如何提升性能?何时应该使用移动语义?

移动语义通过“窃取”另一个对象的资源来避免不必要的拷贝操作,从而提升性能。 它特别适用于以下场景:

  • 返回大型对象: 当函数返回一个大型对象时,移动语义可以避免拷贝构造函数的调用。

    MyString createString() {
        MyString str("Hello, world!");
        return str; // str 会被移动到函数调用者
    }
  • 临时对象: 当使用临时对象时,移动语义可以避免拷贝构造函数和拷贝赋值运算符的调用。

    MyString str1 = MyString("Hello");
    MyString str2 = std::move(str1); // 将 str1 移动到 str2

    std::move
    实际上并没有移动任何东西,它只是将一个左值转换为右值引用,使得移动构造函数或移动赋值运算符可以被调用。
    str1
    在移动后处于有效但未定义的状态,不应该再使用它,除非重新赋值。

  • 容器操作: 当向容器中插入元素时,移动语义可以避免拷贝操作。例如,

    std::vector::push_back
    有一个重载版本,接受右值引用,允许移动元素而不是拷贝元素。

    std::vector<MyString> vec;
    MyString str("Hello");
    vec.push_back(std::move(str)); // 将 str 移动到 vec 中

总而言之,遵循“三/五/零法则”是编写健壮、高效的 C++ 代码的关键。 使用智能指针可以简化资源管理,而移动语义可以避免不必要的拷贝操作,提升性能。在设计类时,仔细考虑类的资源管理职责,并选择合适的资源管理策略。

热门AI工具

更多
UpDream
UpDream Hot

一款AI视频创作工具,主要用于哔哩哔哩推出的自研AI视频创作工具,适合需要提升相关任务效率的用户。

讯飞绘文

讯飞绘文是一款由科大讯飞推出的一站式 AIGC 内容运营平台。

豆包大模型

豆包大模型是一款由字节跳动推出的企业级大语言模型服务平台。

DeepSeek

DeepSeek是一款面向对话、写作、编程和推理场景的AI大模型工具。

WorkBuddy

一款AI办公效率工具,主要用于腾讯云推出的AI原生桌面智能体工作台,适合需要提升相关任务效率的用户。

SkildArt
SkildArt Hot

SkildArt是一款AI文本写作工具,一站式 AI 视觉创作平台。

火山引擎

火山引擎是一款面向企业的云计算与AI服务平台。

音述AI
音述AI Hot

一款AI音频处理工具,主要用于音述AI是一个以“用声音述说故事”为核心的 AI 音乐创作与声音分享社区,适合需要提升相关任务效率的用户。

VibeKnow
VibeKnow Hot

一款AI视频创作工具,主要用于全球首个AI知识视频创作平台,文档、文章、网页,一键生成视频,适合需要提升相关任务效率的用户。

相关专题

更多
string转int
string转int

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

5279

2023.08.02

resource是什么文件
resource是什么文件

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

320

2023.12.20

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

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

5804

2023.10.24

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

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

2404

2024.02.23

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

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

1612

2025.10.17

js 字符串转数组
js 字符串转数组

js字符串转数组的方法:1、使用“split()”方法;2、使用“Array.from()”方法;3、使用for循环遍历;4、使用“Array.split()”方法。本专题为大家提供js字符串转数组的相关的文章、下载、课程内容,供大家免费下载体验。

1558

2023.08.03

js截取字符串的方法
js截取字符串的方法

js截取字符串的方法有substring()方法、substr()方法、slice()方法、split()方法和slice()方法。本专题为大家提供字符串相关的文章、下载、课程内容,供大家免费下载体验。

2264

2023.09.04

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

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

5804

2023.10.24

Buffalo框架数据库开发全教程
Buffalo框架数据库开发全教程

本专题围绕Buffalo框架数据库开发,讲解database.yml多环境配置、soda与fizz迁移生成回滚、模型结构体标签、增删改查与条件查询、一对多与多对多关联、数据校验、回调钩子、事务处理及原生SQL执行能力。

120

2026.09.23

热门下载

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

精品课程

更多
相关推荐
/
热门推荐
/
最新课程
dev.java 官方:Learn Java
dev.java 官方:Learn Java

共0课时 | 0人学习

Java JDBC数据库连接官方教程
Java JDBC数据库连接官方教程

共0课时 | 0人学习

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

Copyright 2014-2026 https://www.php.cn/ All Rights Reserved | php.cn