Lesson 13:类的功能

用构造析构顺序、this、静态成员与运算符重载审计对象生命周期。

学习目标

  • 能解释构造函数、成员初始化顺序和析构函数如何包围对象有效生命周期
  • 能实现 this 与 static 成员的正确用法,区分每对象状态和类共享状态
  • 能分析运算符重载的操作数、结果与不变量,发现对称性、const 和资源所有权错误

来源、版次与标准边界

“Lesson 13:类的功能”以 SB Creative 出版社书页核定高桥麻奈著、第 5 版、2017 年 6 月 14 日发行、ISBN 978-4-7973-9259-3;出版社页面标 596 页。CiNii Books 书目与 16 课目录记录正文 xxiii+571 页,日本国会图书馆书目记录 571 页。页数口径存在前置页差异,因此本课程不拿页码充当目录证据。

“Lesson 13:类的功能”的中文说明、示例、交互、练习和答案均为独立教学重写。公开资料只确认 16 个 Lesson 标题;本页列出的细分概念是课程教学映射,不冒充原书逐级小节。技术规则参考 ISO C++ 标准入口复核,但 2017 年教材的 Visual Studio 2017 语境不会被静默升级为 C++23;现代写法只能明确标为迁移说明。

围绕“拥有资源的类怎样保证构造、复制、赋值和析构形成一致的所有权协议?”,本页要求保留“构造析构日志、资源地址、复制赋值策略、this 身份、静态计数和检测器结果。”。若故障“默认浅复制让两个对象析构时释放同一资源”无法在同一输入下制造首个分岔,应拒绝当前解释,而不是追加随机样例。

为什么对象不能先无效再慢慢补字段

如果调用者必须默认创建对象,再依次调用三个 setter 才能使用,那么这三个调用之间对象处于什么状态并不清楚。构造函数把建立首个有效状态变成对象创建协议:构造成功后,所有公开操作都可以依赖类不变量。

class Account {
public:
    Account(std::string owner, int balance)
        : owner_(std::move(owner)),
          balance_(balance >= 0 ? balance : 0) {}
 
private:
    std::string owner_;
    int balance_;
};

构造函数与类同名、没有普通返回类型。冒号后的成员初始化列表直接初始化成员,不是先默认构造再赋值;引用成员、const 成员和没有默认构造的成员尤其必须在这里初始化。

初始化顺序由成员声明顺序决定

即使初始化列表写成另一顺序,成员仍按类中声明顺序初始化。若 second_ 的初始化依赖 first_,却把声明顺序颠倒,就可能读取尚未初始化的成员。

class Range {
public:
    Range(int start, int length)
        : start_(start), end_(start_ + length) {}
 
private:
    int start_;
    int end_;
};

让初始化列表顺序与声明顺序一致,既符合真实执行,也减少编译警告和误读。构造函数体适合额外验证与动作,但成员进入函数体前已经完成初始化。

析构函数结束对象持有的责任

析构函数在对象生命周期结束时执行,用于释放类直接拥有的资源。名称是 ~ClassName,没有参数和返回类型。自动对象离开作用域时自动析构,动态对象在正确 delete 时析构。

class Trace {
public:
    explicit Trace(std::string name) : name_(std::move(name)) {
        std::cout << "enter " << name_ << '\n';
    }
    ~Trace() {
        std::cout << "leave " << name_ << '\n';
    }
 
private:
    std::string name_;
};

资源获取与释放绑定对象生命周期称为 RAII。若成员本身是 string、vector 或智能指针,它们已经自动管理资源,类通常不应手写 delete。越少直接拥有裸资源,复制、异常和提前 return 越容易正确。

this 指向当前对象而非类本身

普通成员函数调用时有隐含的 this 指针。它让函数区分当前实例,返回 *this 还能支持链式调用;但链式接口应返回有效对象引用,且不能超出对象生命周期。

class Counter {
public:
    Counter& add(int amount) {
        this->value_ += amount;
        return *this;
    }
 
private:
    int value_ = 0;
};

const 成员函数中的 this 指向 const 当前对象,因而不能修改普通成员。mutable 是少量缓存或同步场景的例外,不应拿来绕过 const 设计。

static 成员属于类的共享状态

普通数据成员每个对象一份,静态成员由类的所有对象共享;其中 static 成员函数没有当前对象,因此没有 this,只能直接访问 static 数据成员。

class Ticket {
public:
    Ticket() : id_(++issued_) {}
    int id() const { return id_; }
    static int issued() { return issued_; }
 
private:
    int id_;
    inline static int issued_ = 0;
};

共享计数在并发创建对象时需要同步,测试之间也会保留状态。不要把“很多方法都想用”当作 static 的理由;共享状态必须有明确进程范围、初始化和并发契约。

运算符重载把类型接入自然表达式

运算符重载为用户定义类型规定 + == << 等语法含义。它不是创建新优先级或改变操作数个数;重载后的语义应与运算符惯例一致,让读者能预测结果和副作用。

class Point {
public:
    Point(int x, int y) : x_(x), y_(y) {}
 
    Point& operator+=(const Point& other) {
        x_ += other.x_;
        y_ += other.y_;
        return *this;
    }
 
private:
    int x_;
    int y_;
};
 
Point operator+(Point left, const Point& right) {
    left += right;
    return left;
}

+= 修改左操作数并返回引用,+ 以副本计算新值而不修改原操作数。用复合赋值实现二元加法可以保持两者规则一致。

成员还是非成员取决于左操作数与对称性

赋值、下标、调用与箭头等运算符必须是成员;对称的二元算术和比较常做非成员,使左右两侧转换机会一致。输入输出运算符左侧是流,也通常做非成员。

std::ostream& operator<<(std::ostream& out, const Point& point);

返回流引用支持连续输出。运算符不应做读者意料外的网络访问、全局注册或修改右操作数;若操作不具自然运算语义,命名函数更清楚。

复制资源拥有者必须定义一致策略

若类直接保存 new 返回的裸指针并在析构中 delete,编译器生成的默认复制只复制地址,两个对象析构时会重复释放。此时必须实现正确复制、禁止复制,或改用值成员/智能指针表达所有权。

class Buffer {
public:
    Buffer(const Buffer&) = delete;
    Buffer& operator=(const Buffer&) = delete;
};

本章不展开完整复制控制,但要建立检查:一旦类有自定义析构或直接资源所有权,就必须审计复制构造和赋值。最好的简化通常是让标准库成员自动完成资源管理。

先预测生命周期与操作结果

先预测构造、复制/禁止复制、运算、离开作用域的调用顺序;再验证 a+b 是否保持 a/b 不变、a+=b 是否只修改 a、static 计数是否按对象创建增长。观察应对应具体契约,而不是只打印“调用了函数”。

三步审计类的高级功能

分步1 / 3

第一步:包围有效生命周期

列出成员声明顺序、构造前置条件与析构责任,确保构造一完成不变量成立,任何退出路径都会释放直接拥有资源。

正式节点与章专属证据

  • 构造函数:在“Lesson 13:类的功能”中核对输入、状态变化、失败模式和可复现证据;第 1 个节点必须能回到“每项资源恰有一个负责释放的所有者,复制或移动后所有对象仍处于有效状态。”。
  • 析构函数:在“Lesson 13:类的功能”中核对输入、状态变化、失败模式和可复现证据;第 2 个节点必须能回到“每项资源恰有一个负责释放的所有者,复制或移动后所有对象仍处于有效状态。”。
  • this 指针:在“Lesson 13:类的功能”中核对输入、状态变化、失败模式和可复现证据;第 3 个节点必须能回到“每项资源恰有一个负责释放的所有者,复制或移动后所有对象仍处于有效状态。”。
  • 静态成员:在“Lesson 13:类的功能”中核对输入、状态变化、失败模式和可复现证据;第 4 个节点必须能回到“每项资源恰有一个负责释放的所有者,复制或移动后所有对象仍处于有效状态。”。
  • 运算符重载:在“Lesson 13:类的功能”中核对输入、状态变化、失败模式和可复现证据;第 5 个节点必须能回到“每项资源恰有一个负责释放的所有者,复制或移动后所有对象仍处于有效状态。”。

先用输入合同检查本页正式节点,再在相同初值下逐步比较正常和失败轨迹,最后只启用“默认浅复制让两个对象析构时释放同一资源”完成反例与复位。三个交互都必须能独立重置,且重置后再次满足“每项资源恰有一个负责释放的所有者,复制或移动后所有对象仍处于有效状态。”。

输入与状态合同

Lesson 13:类的功能

拥有资源的类怎样保证构造、复制、赋值和析构形成一致的所有权协议?

必须先声明

为构造函数声明输入类型、有效范围、对象生命周期和失败策略。

可复核证据

保存Lesson 13:类的功能的原始输入、初值与第一条可检查诊断。

正式节点:构造函数、析构函数、this 指针、静态成员、运算符重载

编译与运行轨迹

同一输入下比较正常与失败路径

  1. 01构造获得资源
  2. 02按明确策略复制或禁止复制
  3. 03维护各对象不变量
  4. 04每项资源只释放一次

不变量:每项资源恰有一个负责释放的所有者,复制或移动后所有对象仍处于有效状态。

故障定位与复位

一次只破坏一个前提

基线可继续:每项资源恰有一个负责释放的所有者,复制或移动后所有对象仍处于有效状态。

小结

  • 构造函数建立首个合法状态,成员按声明顺序初始化,初始化列表直接构造成员
  • 析构函数结束对象直接拥有的资源责任,优先让标准库成员实现 RAII
  • this 指向当前对象,const 成员限制通过当前对象修改;static 成员属于类共享状态且无 this
  • 运算符重载应保持惯用副作用、返回类型与对称性,不能发明违背直觉的隐藏行为
  • 类若直接拥有裸资源或自定义析构,必须同时审计复制与赋值;值成员通常能消除手工控制

练习

  1. 问题 1:判断顺序。 初始化列表顺序与成员声明顺序冲突时,谁决定真实初始化顺序?
  1. 问题 2:区分状态。 普通成员、this 与 static 成员之间是什么关系?
  1. 问题 3:审计加法。 为什么 += 常修改左值并返回引用,而 + 返回新值且不修改两侧?

名词解释

名词解释

本章出现的专业名词,用大白话再讲一遍。

构造函数
创建对象时建立首个合法状态的特殊成员函数。
成员初始化列表
在构造函数体前直接初始化成员的初始化器集合。
析构函数
对象生命周期结束时释放其直接拥有资源的特殊成员函数。
static 成员
属于类整体、由所有对象共享的成员。
运算符重载
为用户定义类型提供已有运算符语义的函数机制。

资料与写作方式声明

本章以高桥麻奈 2017 年 C++ 入门教材第 5 版权威目录界定学习范围,并结合正文列出的技术资料独立重写;不宣称复现原书正文,也不沿用原作表述。

原作版权归作者与出版社所有;本站原创教学结构与表述仅供学习交流。

讨论

评论区加载中…