2.1 类与对象的构造与消亡


2.1 类与对象的构造与消亡

本节摘要:class 与 struct 在 C++ 里几乎等价,差别只在默认访问级别;访问修饰符是编译期闸门,一个字节也不改变内存布局。对象的诞生与消亡由构造函数链与析构函数链负责——构造自基类而成员逐层铺设,析构严格反序。本节顺带收编运算符重载:它让自定义类型用上加减乘除的语法,本质仍是普通函数调用。

你将学会

阅读完本节,你应当能够:

  1. 写出规范的类定义并推算其对象大小;
  2. 复述构造与析构的完整执行顺序;
  3. 实现拷贝构造与拷贝赋值并说明何时被调用;
  4. 用运算符重载给自定义类型加上自然的算术与流语法。

一、class 与 struct:一字之差

C++ 的 classstruct 唯一的区别是默认访问级别(class 私有、struct 公有)和默认继承方式。访问修饰符管的是"谁能用",不是"内存怎么摆":

class Account { public: // 编译期闸门:外部可见 Account(std::string owner, double balance) : owner_(std::move(owner)), balance_(balance) {} double balance() const { return balance_; } // const 成员函数:承诺只读 void deposit(double amt) { balance_ += amt; } private: // 仅类内部可见 std::string owner_; // 32 字节左右 具体依实现 double balance_; // 8 字节 }; static_assert(sizeof(std::string) > 0); // 占位说明 // sizeof(Account) ≈ sizeof(std::string) + 8(含对齐填充)

private 不加密、不搬运数据——对象布局与全 public 时一字不差,闸门只在编译器检查那一步生效。public 暴露接口,private 收藏状态,protected 留给继承体系内部使用,这就是封装在 C++ 里的全部物理实现。

初始化列表(构造函数冒号后的部分)不是语法装饰:成员在进入构造函数体之前就要完成初始化,列表正是"出生证明"的书写处;到了函数体里再赋值,等于"先默认构造再覆盖",白白多一轮工作。对 const 成员、引用成员,列表更是唯一选择——它们出生后不许改。列表的初始化顺序严格按成员声明顺序,不按列表书写顺序,把声明顺序与列表顺序保持一致是防错铁律。

二、生与死的完整链条

构造从最底层基类开始,逐层向上,每层再按声明顺序初始化成员;析构完全反序:

#include <iostream> #include <string> struct Engine { Engine() { std::cout << "引擎启动\n"; } ~Engine() { std::cout << "引擎熄火\n"; } }; class Car { public: Car() : engine_(), name_("test-car") { std::cout << "汽车出厂\n"; } ~Car() { std::cout << "汽车报废\n"; } private: Engine engine_; // 先构造 std::string name_; // 后构造 }; int main() { Car c; } // 块结束 自动析构:name_ 先死 engine_ 后死 // 输出: // 引擎启动 // 汽车出厂 // 汽车报废 // 引擎熄火

析构顺序是"构造顺序的倒放"——资源依赖决定了只能如此:后构造的可能依赖先构造的,回收时必须反着来。这条纪律在继承与容器里同样生效(第 2.2 节与第 4 章会再遇到它)。

**析构函数什么时候必须写?**当对象直接拥有裸资源(new 的内存、打开的文件句柄)时。经典的 Rule of Three:如果你需要手写析构函数、拷贝构造、拷贝赋值中的任何一个,多半三个都需要——因为"拥有裸资源"同时意味着"默认的逐成员拷贝会复制出两个指向同一资源的所有者",析构两次即双重释放:

class BadBuffer { public: BadBuffer() : data_(new int[100]) {} ~BadBuffer() { delete[] data_; } // 没写拷贝构造:默认版本逐成员拷贝 复制的是指针! private: int* data_; }; int main() { BadBuffer a; BadBuffer b = a; // 浅拷贝:两对象共享一块堆内存 } // 两次 delete[] 同一块内存 未定义行为

这个 13 行的程序浓缩了 C++ 半数的内存事故。完整的解法(Rule of Five)在第 3.2 节展开,这里先记结论:要么正确处理拷贝,要么禁止拷贝。C++11 的 BadBuffer(const BadBuffer&) = delete; 一行禁掉,简单粗暴但有效。

析构还有两个非写不可的场合:多态基类(第 2.3 节解释为何必须 virtual)、RAII 管理类(第 3 章主角)。

三、拷贝的两个时机与两种深浅

拷贝构造(用同类对象初始化新对象)与拷贝赋值(给已存在对象赋值)是两条路径, beginners 常混为一谈:

std::string a = "hello"; std::string b = a; // 拷贝构造:b 尚不存在 出生即复制品 std::string c; c = a; // 拷贝赋值:c 已存在 需先释放旧资源再复制

赋值比构造多一件事——处理旧状态。自赋值(a = a)是赋值运算符的经典考题:先 delete 再 copy 的写法遇到自赋值会把源数据销毁。标准防御是"拷贝并交换"或先判断 if (this == &other) return *this;。深拷贝(复制资源本身)与浅拷贝(复制指针值)的分野,本质仍是"谁拥有那块堆内存"——所有权思想第一次露头,第 3 章全面展开。

四、运算符重载:让类型像内建类型一样说话

运算符重载是语法糖,a + b 编译后就是函数调用。它不改变内存布局,改变的只是书写体验:

class Vector2 { public: Vector2(double x, double y) : x_(x), y_(y) {} Vector2 operator+(const Vector2& rhs) const { return Vector2(x_ + rhs.x_, y_ + rhs.y_); } Vector2& operator+=(const Vector2& rhs) { x_ += rhs.x_; y_ += rhs.y_; return *this; // 返回自身引用 支持 a += b += c } bool operator==(const Vector2& rhs) const { return x_ == rhs.x_ && y_ == rhs.y_; } double x() const { return x_; } double y() const { return y_; } private: double x_, y_; }; std::ostream& operator<<(std::ostream& os, const Vector2& v) { os << '(' << v.x() << ',' << v.y() << ')'; return os; // 返回流的引用 支持链式输出 }

几条工程纪律:算术运算符优先实现 += 再用其实现 +(在类外写 operator+,减少重复);流运算符必须写成非成员(左操作数是流对象);别过度重载——给矩阵重载 * 合理,给账户重载 + 表示存款就是灾难,语义对不上号的重载比不重载更难读。可重载的运算符几乎覆盖全部(算术、比较、下标、调用、转换),不可重载的只有 .::?: 等少数几员;&&|| 可重载但会失去短路语义,实践中不碰。

⚠️ 常见坑:构造函数里调用虚函数不会分发到派生类版本——此时派生类部分尚未构造,虚表指针还指着基类的表(2.3 节解释)。想"构造时多态",用两阶段构造或工厂函数。

本节要点回顾

  • 布局无门:public/private 只是编译期闸门,对象大小与排列不受影响;class 与 struct 仅默认级别不同。
  • 初始化列表即出生证明:顺序按成员声明;const 与引用成员只能在此初始化。
  • 生死链条:构造自基类向成员铺设,析构严格倒放;拥有裸资源就必须处理拷贝,否则双重释放。
  • 拷贝构造对赋值:一个管出生一个管改嫁,赋值要处理旧状态与自赋值。
  • 运算符重载是函数调用:语义匹配才重载,算子族优先实现复合赋值版本。

下一节看对象里套对象:继承如何把基类子对象拼进派生类,以及为什么基类指针总能指对位置。


作者与出处
原作者: 灏天文库
来源:灏天文库
整理: 灏天文库整理
由灏天文库平台收录,内容或由平台用户上传,仅供学习交流
发布者: 作者: 灏天文库 转发
评论区 (0)
U