跳转到内容
新建笔记

Qt 控件构造:基类初始化、成员顺序与父对象

派生类构造时,先构造基类子对象,再构造自身成员,最后执行派生类构造函数的函数体。初始化列表决定“用什么参数构造”,实际先后顺序由类的声明决定。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_ 的初始化列表也按声明顺序书写。

main.cpp
#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();
}
CMakeLists.txt
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 的对象时,示例的虚析构函数保证派生类析构也能参与;这与构造参数传递是两件事。构造期间的虚函数分派又有特殊规则,见不同类型的内容区。多个控件的组织方式见继承与组合。