类定义一种用户类型,对象是该类型的实例。阅读类代码时,应分别确认成员可见性、构造后的有效状态、对象寿命和资源所有权。本篇使用 C++17,把原先混合了多种特性却不能编译的大类拆成可验证的例子。
声明类与访问成员
跳转到“声明类与访问成员”
class 默认的成员访问级别和基类继承方式都是 private;struct 的两个默认值都是 public。构造函数、成员函数和数据成员都服从访问控制,并不存在“未标注的类方法默认公开”这一例外。访问控制
| 访问级别 | 类自身成员/友元 | 派生类成员 | 普通外部代码 |
|---|---|---|---|
public | 可访问 | 可访问 | 可访问 |
protected | 可访问 | 按保护访问规则访问 | 不可直接访问 |
private | 可访问 | 不可直接访问 | 不可直接访问 |
友元是获准访问私有成员的特定函数或类,不自动成为成员,也不会向后继承或传递友元资格。派生类对 protected 的访问还受对象表达式类型限制,不能据此访问任意基类对象的受保护成员。
#include <cassert>#include <string>#include <utility>
class Sample {public: struct Range { int minimum; int maximum; }; // 嵌套类型也以分号结束
explicit Sample(std::string name, int value = 0) : name_(std::move(name)), value_(value) {} int value() const { return value_; } void setValue(int value) { value_ = value; } const std::string& name() const { return name_; } static int scale(int value) { return value * 2; } friend bool sameValue(const Sample& a, const Sample& b) { return a.value_ == b.value_; }private: std::string name_; int value_;};
int main() { Sample a("input", 4); Sample b("output"); b.setValue(Sample::scale(2)); Sample::Range range{0, 10}; assert(a.name() == "input" && sameValue(a, b)); assert(range.minimum <= a.value() && a.value() <= range.maximum);}类也可以定义在函数内部,称为局部类;它受局部类的额外限制,但“函数里不能定义类”并不正确。局部类
#include <cassert>int main() { class Local { public: int value() const { return 5; } }; Local object; assert(object.value() == 5);}构造、析构与值语义
跳转到“构造、析构与值语义”构造函数建立对象状态;析构函数在对象寿命结束时执行清理。构造成员的顺序取决于成员声明顺序,不是初始化列表的排列。构造函数没有返回类型,析构函数写成 ~ClassName();在本文 C++17 的同一类中不能重复定义多个析构函数。
管理字符串和动态序列时优先使用 std::string、std::vector 等资源对象,从而直接使用编译器生成的复制和移动操作,称为 Rule of Zero。这样也避免原例中“先删除原内存、再分配新内存”在分配抛异常时留下悬空指针的问题。
#include <cassert>#include <string>#include <utility>#include <vector>
struct Record { std::string name; std::vector<int> values;};
int main() { Record original{"sensor", {1, 2, 3}}; Record copied = original; // 复制构造 copied.values[0] = 9; assert(original.values[0] == 1); // 元素各自保存 Record assigned; assigned = original; // 复制赋值:目标已存在 assert(assigned.values == original.values); Record moved = std::move(copied); // 移动构造 assert(moved.values[0] == 9); // copied 仍可销毁或重新赋值,不假定其移动后的字符串/容器内容。 copied = Record{"new", {4}}; assert(copied.values[0] == 4);}自行管理资源时再考虑自定义析构、复制和移动,并明确深拷贝、禁止复制或转移所有权的选择。含 unique_ptr 的类默认不可复制;含 shared_ptr 的类复制后通常共享其指向的对象,不是复制该对象本身。
构造初始化细节见 成员初始化与 Qt parent;虚析构、抽象类与覆盖规则分别见 继承和多态、抽象类、纯虚函数与析构。不要把这些特性都塞入一个缺少定义的“万能类”。
const 成员函数与 static 成员
跳转到“const 成员函数与 static 成员”const 成员函数限制经 this 修改当前对象的普通非静态数据成员,可被常量对象调用;它不是“这个函数不改变任何状态”的承诺。mutable 成员、静态数据以及指针所指向的外部可变对象仍需分别分析。
静态成员函数没有 this,需要显式对象才能访问非静态成员;它可以由构造函数或析构函数调用。C++17 的 inline static 数据成员可直接在类中定义,不需要再写一份类外定义。
#include <cassert>
class Counter {public: explicit Counter(int& external) : target_(&external) { ++alive_; } ~Counter() { --alive_; } Counter(const Counter&) = delete; Counter& operator=(const Counter&) = delete; int get() const { ++reads_; return value_; } void set(int value) { value_ = value; } void updateExternal(int value) const { *target_ = value; } static int alive() { return alive_; }private: int value_ = 0; int* target_; mutable int reads_ = 0; inline static int alive_ = 0;};
int main() { int external = 1; { Counter object(external); object.set(7); const Counter& view = object; assert(view.get() == 7 && Counter::alive() == 1); view.updateExternal(8); assert(external == 8); } assert(Counter::alive() == 0);}const 也可以参与成员函数重载,例如同名的 data() 与 data() const 分别向可变对象和常量对象提供适当访问。返回成员的引用或指针会借用当前对象,其寿命不能超过对象本身。
继承、对象大小与 Qt 对象
跳转到“继承、对象大小与 Qt 对象”私有继承会改变基类公开/保护成员向外暴露的权限,但派生类自己的成员仍可按规则访问基类的公开/保护成员;基类私有成员仍不能直接访问。虚函数的常见实现使用虚表和对象内的指针,不过 C++ 标准不规定“每个虚函数占一个指针大小”或固定对象布局,不能据此直接推算 sizeof。
原 YOLOWINDOW 问答需区分三个类型:
// 概念片段:要求 YoloWindow 是公开继承 QWidget 的完整类型。YoloWindow yolo; // 一个对象YoloWindow* window = &yolo; // 指向对象的指针QObject* object = &yolo; // 合法的派生类指针向基类指针转换不能把对象 yolo 当成要求 QObject* 的实参,但 &yolo 可以进行公开继承链上的指针转换。栈对象也可以有 Qt parent,关键是销毁顺序:先构造父对象、再构造栈子对象,子对象先析构并从父对象中移除,才符合安全顺序。不要让父对象提前删除一个还会自动析构的栈子对象。
QObject 本身禁止复制,QObject 派生对象通常通过指针交互;Qt parent 所有权还应与 C++ 对象寿命保持一致。完整例子及常用控件继承关系见 Qt 对象模型。