C++ / a working model

09 / 80   ·   C++11   ·   约 7 分钟

class 与 struct:默认访问不同,能力相同

先记住这句话

C++ 的 class 和 struct 都定义类类型,都支持构造、析构、继承、虚函数与模板。语言差异主要是默认成员访问和默认继承访问;用 struct 表达简单数据、用 class 维护不变量,是设计惯例而不是功能限制。

本篇内容
  1. 两项默认值
  2. 对象能力没有分档
  3. 用约定帮助维护不变量
  4. 运行示例
  5. 动手练习

两项默认值

struct 中未写访问说明符的成员默认 public,class 中默认 private。继承同样如此:struct D : B 默认 public 继承,class D : B 默认 private 继承。显式写出 public、protected 或 private 后,关键字造成的这一默认差异就被覆盖。

默认继承取决于派生类使用的关键字,而不是基类使用 class 还是 struct。示例让两个派生类使用同一个基类,再通过类型特征检查从派生指针到基类指针是否能进行公开隐式转换,避免只背成员默认值而漏掉继承规则。

对象能力没有分档

struct 可以有私有数据、构造函数、析构函数、虚函数和复杂业务逻辑;class 也可以只有公开字段。不能因为看到 struct 就断言它没有虚表、没有构造成本、能按字节复制或能直接传给 C。那些性质取决于具体成员、继承和特殊成员函数。

是否为聚合、标准布局类型、平凡可复制类型,是三组不同的规则,并且某些细节随标准版本变化。设计序列化或底层接口时应检查所需要的具体性质,而不是把关键字当作内存布局或 ABI 的证明。

用约定帮助维护不变量

简单坐标、返回值组合等允许各字段独立赋值的数据,通常用 struct 直观表达。银行余额、资源句柄等需要保持字段间关系的类型,通常用 class 将状态设为 private,并通过构造和成员函数集中验证修改。

这是一种沟通设计意图的约定,而不是要求把所有字段都机械变为私有再添加等价 getter/setter。选择接口时应问:哪些状态必须始终合法,谁负责确保合法?访问控制能限制使用者写法,但不会自动验证数值范围,也不是内存安全隔离边界。

容易答错的地方

  • 把 class 改为 struct 而遗漏显式访问说明符,会同时改变成员可见性和未注明的继承访问。
  • struct 不保证兼容 C 布局;引用成员、虚函数、继承及库类型成员都需要单独审查。

运行一个例子

最低标准 C++11 · 完整程序 · 下载 .cpp

#include <cassert>
#include <iostream>
#include <type_traits>

struct Base { int value = 7; };
struct PublicChild : Base {};
class PrivateChild : Base {};

class Box {
    int value_;
public:
    explicit Box(int value) : value_(value) {}
    int value() const { return value_; }
};

int main() {
    static_assert(std::is_convertible<PublicChild*, Base*>::value, "public base");
    static_assert(!std::is_convertible<PrivateChild*, Base*>::value, "private base");
    PublicChild child;
    Box box(9);
    assert(child.value == 7);
    assert(box.value() == 9);
    std::cout << child.value << ' ' << box.value() << '\n';
}

在本地编译

g++ -std=c++11 -Wall -Wextra -Wpedantic -pthread basics-class-struct.cpp -o example && ./example

预期结果

7 9

CHECK YOUR UNDERSTANDING

合上答案,试着解释。

把 class Child : Base 改成 struct Child : Base,却要求保持原有访问策略,应怎样改写?

查看参考答案

写成 struct Child : private Base,并在原来默认私有的成员区域前写 private:。这样显式保留原来的默认继承和成员访问;原有 public: 或 protected: 区段继续保持原样。仅仅更换关键字不是保持行为的机械重命名。

继续查证

标准草案链接会随工作草案更新;本文版本标记对应示例最低要求,不表示草案中的所有新规则都适用于旧标准。

回到目录