Appearance
面向对象与多态
struct 与 class
在 C++ 中二者都可以拥有数据成员、成员函数、构造/析构函数、模板、继承和访问控制。关键默认值不同:
| 特性 | struct | class |
|---|---|---|
| 成员默认访问权限 | public | private |
| 默认继承方式 | public | private |
工程上常用 struct 表示公开、轻量的数据聚合,用 class 表示要隐藏不变量和提供行为的抽象;这只是约定,不是语言强制。
面向对象三大特性
- 封装:把数据与操作封装在类型中,隐藏实现细节,通过受控接口维护不变量;
- 继承:派生类复用/扩展基类接口和实现,表达“is-a”关系;不应只为代码复用而滥用;
- 多态:同一接口可按对象实际类型表现不同,实现可扩展替换。
重载、重写与隐藏
- 重载(overload):同一作用域内同名函数,参数列表不同;编译器在编译期选择调用目标。仅返回类型不同不能构成重载。
- 重写(override):派生类以兼容签名实现基类的
virtual函数,运行期通过基类指针/引用分派。应总是写override,让编译器检查。 - 名称隐藏(name hiding):派生类声明同名函数会隐藏基类所有同名重载,即使参数不同;可使用
using Base::func;恢复重载集合。
cpp
class Shape {
public:
virtual double area() const = 0;
virtual ~Shape() = default;
};
class Circle final : public Shape {
public:
explicit Circle(double r) : r_(r) {}
double area() const override { return 3.14159 * r_ * r_; }
private:
double r_;
};静态与动态多态
- 静态(编译期)多态:函数/运算符重载、模板、CRTP 等,调用目标编译期确定,容易内联,通常零运行期开销。
- 动态(运行期)多态:虚函数、接口/抽象基类;通过基类指针或引用,在运行期依对象动态类型分派。
虚函数、纯虚函数与虚表
- 虚函数可以有默认实现,派生类可选择是否重写;
- 纯虚函数写作
virtual void f() = 0;,使类成为抽象类,不能直接实例化;纯虚函数也可以在类外提供定义,但派生类若要变为具体类仍须完成覆盖; - 抽象基类应有虚析构函数,保证经基类指针销毁派生对象时析构链完整。
大多数 C++ ABI 会用隐藏的 vptr 指向虚函数表(vtable)实现动态分派:表中存放对应虚函数的入口,派生类覆盖后使用自己的入口。这是主流实现方式,不是 C++ 标准规定的对象布局;多重继承、虚继承时布局和调整更复杂。
动态分派有一次间接调用和潜在优化限制的成本,通常很小,但不应在极端热点中不经评估地滥用。
构造函数与析构函数
构造函数
构造函数在对象创建时初始化基类和成员,名称与类相同、没有返回类型。常见形式:
- 默认构造函数;
- 带参构造函数;
- 拷贝构造函数;
- 移动构造函数;
- 委托构造函数;
initializer_list构造函数;explicit转换构造函数(避免不期望的隐式转换);constexpr构造函数(允许满足条件时常量求值)。
“初始化列表构造函数”“常量构造函数”不是与上述并列的标准构造函数分类。成员应优先在成员初始化列表中初始化,尤其是 const 成员、引用成员和无默认构造函数的成员。
构造函数能否使用虚函数?
构造函数本身不能声明为虚函数。 构造函数的职责正是创建指定类型的对象,调用哪一个构造函数必须在编译期由 new Derived、局部变量声明等方式确定,因此没有“通过基类接口按实际对象类型动态分派构造函数”的语义。也不要把原因简单归结为“对象还没有内存”或“还没有虚表”:对象存储会先被取得,而 vptr/vtable 是常见 ABI 实现细节,不是语言标准保证。
构造函数内部可以在语法上调用虚函数,但要非常谨慎。 在基类构造函数执行期间,派生类子对象尚未构造完成;为了避免访问未初始化的派生状态,虚调用只会分派到当前正在构造的类的最终覆盖版本,而不会调用更派生类的覆盖:
cpp
#include <iostream>
class Base {
public:
Base() { describe(); } // 输出 Base,而非 Derived
virtual void describe() const { std::cout << "Base\\n"; }
virtual ~Base() = default;
};
class Derived : public Base {
public:
Derived() : name_("ready") {}
void describe() const override { std::cout << name_ << "\\n"; }
private:
std::string name_;
};
// 构造 Derived 时,先执行 Base(),此时 name_ 尚未初始化,
// 因此 Base() 中的 describe() 调用 Base::describe()。在 Derived 自己的构造函数体中调用该虚函数时,会分派到 Derived 的实现,但若它还有更派生类,仍不会分派到更派生类。即便如此,也应确保函数只依赖已经初始化的状态;工程上通常避免在构造函数中调用可被重写的虚函数,改用私有/非虚初始化辅助函数,或在对象完整构造后由工厂/显式初始化步骤调用扩展钩子。
若在构造或析构过程中调用纯虚函数,且该调用会落到纯虚函数作为最终覆盖版本,行为未定义(许多运行库会报 pure virtual method called)。因此不要让构造/析构路径依赖纯虚函数。
析构过程规则相反但原因相同:派生类已先析构,基类析构函数中的虚调用只分派到基类版本,不能依赖派生类资源。
析构函数
析构函数写作 ~ClassName(),对象结束生命周期时执行清理。它可以且经常应在多态基类中声明为 virtual。若基类析构非虚,却经由 Base* 删除 Derived 对象,行为未定义,派生资源可能无法正确释放。