跳转到内容
新建笔记

C++ 类与对象:访问控制、值语义和生命周期

类定义一种用户类型,对象是该类型的实例。阅读类代码时,应分别确认成员可见性、构造后的有效状态、对象寿命和资源所有权。本篇使用 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 对象模型。