派生类构造时,先构造基类子对象,再构造自身成员,最后执行派生类构造函数的函数体。初始化列表决定“用什么参数构造”,实际先后顺序由类的声明决定。Qt 的 parent 只是构造参数之一,还承担对象树和窗口层级的含义。
本文使用 C++17、Qt 6 Widgets;完整示例可放入一个目录,保存为 main.cpp 和 CMakeLists.txt 后构建。
基类是否需要写进初始化列表
跳转到“基类是否需要写进初始化列表”如果基类没有可访问、未删除的默认构造函数,派生类必须在初始化列表中指定能调用的基类构造函数。例如只有 Base(int) 时,需要写 : Base(baseValue)。不能等进入派生类函数体后再给基类“补做一次构造”。
默认构造函数包括可以不提供实参就调用的构造函数,并不要求声明中完全没有形参。因此,形如 QWidget(QWidget *parent = nullptr, Qt::WindowFlags f = Qt::WindowFlags()) 的构造函数,也可以不传实参。
| 情况 | 初始化方式 | 需要注意 |
|---|---|---|
基类只有 Base(int) | 写 Base(value) | 省略后会尝试默认初始化基类,但找不到可用构造函数 |
| 基类有可用默认构造函数 | 可以省略基类项 | 是否显式写 Base() 是表达意图的选择,不是一条通用强制规则 |
派生类有 double derivedValue | 写 derivedValue(value) 或提供成员默认初始化 | 只初始化基类不会顺便初始化这个 double |
Qt 控件接受可选 parent | 通常写 QWidget(parent) | 省略时仍能默认构造,但会丢掉调用者传入的父对象关系 |
原例有一个构造函数只写了 Base(baseValue),没有初始化 derivedValue。对于该例中自动存储期对象的普通 double 成员,这不是自动得到 0.0;应在读取前完成明确初始化。下面把这两个成员都放进初始化列表。
顺序以声明为准
跳转到“顺序以声明为准”普通非委托构造中,顺序依次为:最派生类负责的虚基类、按基类声明顺序排列的直接基类、按成员声明顺序排列的成员、构造函数体。把初始化列表的文字顺序倒过来,不会改变实际构造顺序,反而容易隐藏依赖错误。C++ 初始化顺序
完整程序中的 ConstructionOrder 记录 base、first、second、derived-body,用来观察基类和成员的先后关系。成员 first_、second_ 的初始化列表也按声明顺序书写。
#include <QApplication>#include <QLabel>#include <QVBoxLayout>#include <QWidget>
#include <iostream>#include <string>#include <vector>
class Base {public: explicit Base(int value) : baseValue(value) {} virtual ~Base() = default; int baseValue;};
class Derived : public Base {public: Derived(int baseValue, double value) : Base(baseValue), derivedValue(value) {} double derivedValue;};
class DefaultBase {public: DefaultBase() : baseValue(0) {} int baseValue;};
class DefaultDerived : public DefaultBase {public: explicit DefaultDerived(double value) : derivedValue(value) {} double derivedValue;};
class TraceBase {public: explicit TraceBase(std::vector<std::string>& log) { log.push_back("base"); }};
class MemberProbe {public: MemberProbe(std::vector<std::string>& log, const char* name) { log.emplace_back(name); }};
class ConstructionOrder : public TraceBase {public: explicit ConstructionOrder(std::vector<std::string>& log) : TraceBase(log), first_(log, "first"), second_(log, "second") { log.push_back("derived-body"); }private: MemberProbe first_; MemberProbe second_;};
class MyWidget : public QWidget {public: explicit MyWidget(QWidget* parent = nullptr) : QWidget(parent) { auto* layout = new QVBoxLayout(this); layout->addWidget(new QLabel("MyWidget forwards its parent", this)); }};
int main(int argc, char* argv[]) { QApplication app(argc, argv); Derived value(7, 2.5); DefaultDerived defaultValue(3.5); std::cout << value.baseValue << ' ' << value.derivedValue << '\n'; std::cout << defaultValue.baseValue << ' ' << defaultValue.derivedValue << '\n';
std::vector<std::string> log; ConstructionOrder order(log); for (const auto& entry : log) { std::cout << entry << '\n'; }
QWidget window; auto* layout = new QVBoxLayout(&window); auto* child = new MyWidget(&window); layout->addWidget(child); window.setWindowTitle("Constructor and parent example"); window.resize(380, 120); window.show(); return app.exec();}cmake_minimum_required(VERSION 3.16)project(widget_constructors LANGUAGES CXX)set(CMAKE_CXX_STANDARD 17)set(CMAKE_CXX_STANDARD_REQUIRED ON)set(CMAKE_CXX_EXTENSIONS OFF)find_package(Qt6 REQUIRED COMPONENTS Widgets)add_executable(widget-example main.cpp)target_link_libraries(widget-example PRIVATE Qt6::Widgets)控制台先打印 7 2.5 和 0 3.5,随后依次打印 base、first、second、derived-body;窗口中显示一个 MyWidget 子控件。DefaultDerived 没写 DefaultBase(),仍会调用其默认构造函数。
Qt 的 parent 既是约定,也是对象关系
跳转到“Qt 的 parent 既是约定,也是对象关系”explicit MyWidget(QWidget *parent = nullptr) 允许无参创建顶层控件,也允许传入父控件。这里显式写 QWidget(parent),目的是转交父对象,不能据此推断所有 Qt 基类都强制要求实参。QWidget 构造函数
父 QObject 销毁时会删除仍属于它的子对象。示例中 window 是栈对象,而 child 和它的标签、布局由对象树管理,不需要再手动逐个删除。栈上的父子对象尤其要留意构造与析构顺序:不要把一个会较晚析构的栈对象,挂到一个会较早析构的父对象下面。对象树与所有权
单纯继承 QWidget、转交 parent 和使用已有信号,不要求每个类都写 Q_OBJECT。当类需要自己的信号、元对象属性等功能时,再按 Qt 的元对象机制添加宏并配置 moc。示例中的 MyWidget 没有新增这些功能。
通过 Base* 删除实际为 Derived 的对象时,示例的虚析构函数保证派生类析构也能参与;这与构造参数传递是两件事。构造期间的虚函数分派又有特殊规则,见不同类型的内容区。多个控件的组织方式见继承与组合。