资讯动态

Qt Core模块深度解析:元对象系统、事件循环与多线程实战

发布时间:2026/10/13 8:50:56 来源:尧图企业网站定制
1. 为什么每个Qt开发者都该吃透Core模块刚接触Qt那会儿我和很多人一样觉得QtCore就是个“基础库”——不就是QString、QObject、信号槽那点东西吗直到有一次项目里需要做一个跨线程的数据处理管道我随手用了QThread加moveToThread结果程序跑起来偶尔崩在QObject::killTimer上排查了整整两天才发现是事件循环和对象归属没理清楚。那次之后我才真正意识到QtCore不是“基础”它是整个Qt框架的地基地基没打牢上面盖什么楼都晃。这篇文章想聊的就是QtCore这个模块。它到底包含什么、为什么这样设计、哪些坑是新手必踩的、哪些技巧是老手才知道的。不管你是刚学Qt的学生还是已经用Qt做过几个项目但总觉得“知其然不知其所以然”的开发者我都建议你把Core模块从头到尾捋一遍。它不像QtWidgets那样能直接看到界面效果也不像QtNetwork那样有明确的业务场景但它是所有其他模块的依赖——QWidget继承自QObjectQNetworkReply的信号槽机制来自Core连QML引擎的底层对象模型也是Core提供的。可以说Core模块的掌握程度直接决定了你写Qt代码的上限。我见过太多人写Qt代码信号槽连上了就完事从不关心连接类型QString和std::string混着用编码问题出了才抓瞎容器类随手选一个QList从不考虑底层数据结构。这些问题在Demo里不会暴露一旦项目规模上去、线程多起来、数据量变大就会集中爆发。所以这篇文章我会从设计思路讲到具体实现从常用类讲到冷门但好用的工具再把我自己踩过的坑和排查经验整理出来希望能帮你少走几年弯路。2. Core模块的整体设计与核心思路拆解2.1 元对象系统Qt区别于其他C框架的根本要理解QtCore首先得理解元对象系统Meta-Object System。这是Qt最核心的设计也是它和标准C最大的区别。标准C是静态类型语言编译期确定一切没有运行时类型信息RTTI的完整支持更没有反射机制。Qt通过mocMeta-Object Compiler在编译前扫描头文件为每个带Q_OBJECT宏的类生成额外的C代码从而实现了信号槽、运行时类型识别、动态属性、国际化翻译等功能。这个设计的精妙之处在于它没有修改C语言本身而是用预处理器加代码生成的方式在标准C之上搭了一层“元层”。你写的class MyObject : public QObject经过moc处理后会多出一个staticMetaObject静态成员和一个qt_metacall虚函数。staticMetaObject里存了类名、父类信息、信号槽索引、属性表等元数据qt_metacall则负责在运行时根据索引分发调用。为什么这么设计因为C的虚函数表只能支持编译期确定的函数签名而信号槽需要在运行时根据字符串或索引动态连接。moc生成的代码相当于给每个QObject子类建了一张“通讯录”信号发出时查表找到所有连接的槽然后逐个调用。这套机制的开销主要在连接建立阶段一旦连接完成后续的信号触发就是一次数组遍历加函数指针调用效率相当可观。注意Q_OBJECT宏必须放在类声明的private区域默认就是private且该类必须直接或间接继承自QObject。如果你忘了加这个宏信号槽连接会在运行时失败而且编译器不会报错只会输出一条警告。我见过有人排查了半天发现就是漏了这个宏。2.2 值语义与隐式共享QString和容器的设计哲学QtCore里的很多类都采用了隐式共享Implicit Sharing技术也叫写时复制Copy-On-Write。QString、QByteArray、QList、QVector、QMap等都是这样。这意味着当你复制一个QString时底层并不会立即复制字符数据而是增加引用计数两个对象共享同一块内存。只有当你修改其中一个时才会真正分配新内存并复制数据。这个设计的好处很明显函数传参时按值传递大字符串不会产生昂贵的拷贝开销返回值优化也更容易实现。但代价是每次修改都要检查引用计数而且多线程环境下需要原子操作来保证计数安全。Qt的实现是在数据块头部存一个QAtomicInt作为引用计数复制时原子递增析构时原子递减减到零才释放内存。这里有个容易踩的坑迭代器失效。因为隐式共享的存在当你通过非const迭代器访问容器时Qt会强制“detach”分离也就是确保当前对象独占数据。如果你在循环里同时持有多个迭代器detach可能导致其他迭代器失效。比如QListint list {1, 2, 3, 4, 5}; QListint::iterator it1 list.begin(); QListint::iterator it2 list.begin() 2; *it1 10; // 这里触发detachit2可能失效正确的做法是使用const迭代器constBegin()/constEnd()来遍历只读场景或者用基于范围的for循环。如果确实需要修改尽量在修改前完成所有迭代器的获取或者改用索引访问。2.3 事件循环Qt程序的“心脏”QCoreApplication的exec()方法启动的事件循环是Qt程序运转的核心。所有异步操作——网络响应、定时器触发、用户输入、信号槽的队列连接——都依赖事件循环来分发。理解事件循环的工作机制是写出不卡顿、不崩溃的Qt程序的前提。事件循环本质上是一个无限循环不断从事件队列中取出事件分发给对应的QObject处理。QEventLoop::exec()内部会调用平台相关的事件等待函数如Linux上的poll或epollWindows上的MsgWaitForMultipleObjects在没有事件时阻塞等待有事件时唤醒处理。这种设计避免了忙等待CPU占用率很低。但事件循环不是万能的。如果你在槽函数里执行了一个耗时操作比如读取大文件、复杂计算事件循环就被阻塞了界面会卡死定时器不触发网络响应也处理不了。这时候就需要用QTimer::singleShot(0, ...)把后续操作放到下一次事件循环迭代或者用QThread/QtConcurrent把耗时任务挪到后台线程。提示QCoreApplication::processEvents()可以临时处理一下事件队列但不要滥用。它会导致重入问题比如用户在槽函数执行期间又触发了一个事件可能造成状态不一致。我一般只在进度条更新这种场景下谨慎使用。3. 核心类深度解析与实操要点3.1 QObject一切对象的基类QObject是Qt对象模型的根基。它提供了对象树管理、信号槽、事件处理、定时器、线程归属等核心功能。每个QObject都有一个可选的父对象当父对象析构时所有子对象会被自动删除。这个机制极大简化了内存管理——你只需要在堆上new一个对象并指定父对象就不需要手动delete了。但对象树也有陷阱。栈上创建的对象不能设置父对象因为父对象析构时会尝试delete子对象而栈对象的内存不是new出来的会导致未定义行为。我见过有人写QObject parent; QTimer timer(parent); // 错误timer在栈上parent析构时会delete它正确的做法是QTimer *timer new QTimer(parent);或者不设父对象、让timer和parent的生命周期各自独立。另一个关键点是线程归属。每个QObject都有一个thread()返回其所属线程。默认情况下对象属于创建它的线程。跨线程的信号槽连接如果是Qt::AutoConnection默认会根据发送者和接收者的线程关系自动选择直连或队列连接。但如果你手动moveToThread必须确保对象没有父对象因为父对象和子对象必须在同一线程。QThread *workerThread new QThread; Worker *worker new Worker; // 不能有父对象 worker-moveToThread(workerThread); connect(workerThread, QThread::started, worker, Worker::doWork); connect(worker, Worker::finished, workerThread, QThread::quit); connect(workerThread, QThread::finished, worker, QObject::deleteLater); workerThread-start();这段代码是Qt多线程的经典模式。注意deleteLater()——它会在当前事件循环迭代结束后删除对象比直接delete安全因为直接删除可能发生在对象正在处理事件的时候。3.2 信号槽的三种连接类型与选择策略信号槽是Qt最著名的特性但很多人只用了默认的Qt::AutoConnection对其他连接类型一知半解。实际上连接类型决定了槽函数在哪个线程执行、是同步还是异步选错了会导致死锁、崩溃或性能问题。连接类型行为适用场景Qt::DirectConnection槽函数在信号发出线程立即执行同一线程内或需要同步返回结果Qt::QueuedConnection槽函数在接收者线程的事件循环中执行跨线程通信避免竞态Qt::BlockingQueuedConnection同队列连接但发送线程阻塞直到槽执行完跨线程需要同步结果但极易死锁Qt::AutoConnection同线程用直连跨线程用队列默认大多数场景Qt::BlockingQueuedConnection是最危险的。如果发送者和接收者在同一线程它会直接死锁——发送线程阻塞等待槽执行但槽需要发送线程的事件循环来分发而事件循环已经被阻塞了。我建议除非你非常清楚自己在做什么否则不要用这个类型。跨线程通信时还有一个常见错误在队列连接中传递自定义类型。队列连接会把参数拷贝到事件对象中所以自定义类型必须用qRegisterMetaType()注册否则运行时会报“Cannot queue arguments of type”错误。struct MyData { int id; QString name; }; Q_DECLARE_METATYPE(MyData) // 在main函数或初始化时注册 qRegisterMetaTypeMyData(MyData);Q_DECLARE_METATYPE让类型可以被QVariant存储qRegisterMetaType让类型可以被队列连接使用。两个都需要缺一不可。3.3 QString的编码陷阱与高效用法QString内部使用UTF-16存储每个字符占2字节BMP范围内。这和std::string的UTF-8或本地编码完全不同转换时容易出问题。最常见的坑是std::string s 中文; QString qs QString::fromStdString(s); // 可能乱码fromStdString默认按UTF-8解码但如果你的std::string是GBK编码Windows中文环境常见就会乱码。正确的做法是明确指定编码QString qs QString::fromLocal8Bit(s.c_str()); // 按本地编码 // 或者 QString qs QString::fromUtf8(s.c_str()); // 按UTF-8反过来QString转std::string时toStdString()返回UTF-8编码但如果你需要本地编码得用toLocal8Bit()。性能方面QString的字符串拼接要避免频繁的操作。虽然隐式共享减少了拷贝但每次仍然可能触发重新分配。大量拼接时用QStringBuilder或QStringList::join()更高效// 低效 QString result; for (const auto item : items) { result item ,; } // 高效 QStringList parts; for (const auto item : items) { parts item; } QString result parts.join(,);QStringBuilder通过表达式模板延迟求值把多次拼接合并成一次内存分配在循环拼接场景下性能提升明显。使用时需要包含QStringBuilder并确保QT_USE_QSTRINGBUILDER已定义。3.4 容器类的选择QList、QVector、QMap到底用哪个Qt提供了多种容器类选择哪个取决于你的数据结构和访问模式。很多人习惯性地用QList但其实QList在Qt 5和Qt 6中的实现完全不同。Qt 5的QList内部是一个void*数组每个元素如果小于等于指针大小就直接存值否则存指针。这导致QListLargeStruct会有额外的堆分配开销。Qt 6把QList和QVector统一了QList就是QVector的别名底层是连续数组。容器底层结构随机访问头部插入尾部插入查找QList/QVector连续数组O(1)O(n)均摊O(1)O(n)QLinkedList双向链表O(n)O(1)O(1)O(n)QMap红黑树O(log n)O(log n)O(log n)O(log n)QHash哈希表均摊O(1)均摊O(1)均摊O(1)均摊O(1)选择原则很简单需要随机访问用QList需要频繁中间插入删除用QLinkedList需要有序键值对用QMap需要最快查找用QHash。QHash的键必须提供qHash()函数自定义类型需要重载。实操心得QMap的键是有序的遍历时按升序输出QHash的遍历顺序是不确定的。如果你需要稳定的遍历顺序用QMap如果只关心查找速度用QHash。另外QHash在数据量小的时候几十个元素可能比QMap慢因为哈希计算和冲突处理有固定开销。4. 实操过程与核心环节实现4.1 从零搭建一个基于QObject的观察者模式假设我们要实现一个简单的数据模型当数据变化时通知多个视图更新。用Qt的信号槽可以很优雅地实现class DataModel : public QObject { Q_OBJECT public: explicit DataModel(QObject *parent nullptr) : QObject(parent) {} int value() const { return m_value; } public slots: void setValue(int newValue) { if (m_value newValue) return; m_value newValue; emit valueChanged(m_value); } signals: void valueChanged(int newValue); private: int m_value 0; }; class DataView : public QObject { Q_OBJECT public: explicit DataView(const QString name, QObject *parent nullptr) : QObject(parent), m_name(name) {} public slots: void onValueChanged(int value) { qDebug() m_name received: value; } private: QString m_name; }; // 使用 DataModel model; DataView view1(View1); DataView view2(View2); QObject::connect(model, DataModel::valueChanged, view1, DataView::onValueChanged); QObject::connect(model, DataModel::valueChanged, view2, DataView::onValueChanged); model.setValue(42); // 两个视图都会收到通知这个模式的关键在于DataModel不需要知道有哪些视图只需要发出信号视图也不需要知道数据来自哪里只需要连接信号。这种解耦让代码易于扩展——新增一个视图只需要多一行connect不需要修改DataModel。4.2 用QTimer实现一个精准的定时任务调度器QTimer是QtCore里最常用的类之一但很多人只用了最简单的start(interval)。实际上QTimer的精度受事件循环影响如果事件队列繁忙定时器可能延迟触发。对于需要更高精度的场景可以设置定时器类型QTimer *timer new QTimer(this); timer-setTimerType(Qt::PreciseTimer); // 尽量精确 // Qt::CoarseTimer 是默认值允许5%误差更省电 // Qt::VeryCoarseTimer 允许500ms误差适合后台任务 timer-setInterval(100); // 100ms connect(timer, QTimer::timeout, this, MyClass::onTimeout); timer-start();Qt::PreciseTimer会尽量保证毫秒级精度但代价是更频繁地唤醒CPU。在移动设备上Qt::CoarseTimer更合适因为它允许系统合并定时器唤醒省电。另一个技巧是用QTimer::singleShot做延迟执行比手动创建定时器再删除更简洁QTimer::singleShot(1000, this, [this]() { // 1秒后执行this作为上下文对象如果this被销毁则不会执行 doSomething(); });传入this作为上下文对象很重要——如果this在延迟期间被销毁lambda不会执行避免了悬空指针。4.3 文件与目录操作QFile、QDir、QFileInfo的配合使用QtCore提供了一套跨平台的文件系统API。QFile负责读写QDir负责目录遍历QFileInfo负责获取文件元信息。三者配合可以完成大多数文件操作。读取文本文件的推荐方式QFile file(data.txt); if (!file.open(QIODevice::ReadOnly | QIODevice::Text)) { qWarning() Cannot open file: file.errorString(); return; } QTextStream in(file); in.setCodec(UTF-8); // Qt 5需要Qt 6默认UTF-8 while (!in.atEnd()) { QString line in.readLine(); processLine(line); } file.close();注意QIODevice::Text标志在Windows上会把\r\n转换为\n在Linux上无影响。如果你需要保留原始换行符不要加这个标志。遍历目录时QDirIterator比QDir::entryList()更高效因为它是流式遍历不需要一次性把所有文件名加载到内存QDirIterator it(/path/to/dir, QDir::Files | QDir::NoDotAndDotDot, QDirIterator::Subdirectories); while (it.hasNext()) { QString filePath it.next(); QFileInfo info(filePath); qDebug() info.fileName() info.size() info.lastModified(); }QDirIterator::Subdirectories标志会递归遍历子目录。如果目录很大建议加上过滤器减少不必要的遍历。4.4 用QSettings做跨平台配置持久化QSettings是QtCore里做配置存储的利器。它自动处理平台差异Windows上写注册表Linux上写~/.config下的ini文件macOS上写plist。你只需要用统一的API读写键值对QSettings settings(MyCompany, MyApp); // 写入 settings.setValue(window/width, 800); settings.setValue(window/height, 600); settings.setValue(user/name, Alice); // 读取 int width settings.value(window/width, 640).toInt(); QString name settings.value(user/name, Guest).toString(); // 同步到磁盘通常不需要手动调用析构时自动同步 settings.sync();QSettings支持分组用beginGroup/endGroup、数组用beginWriteArray/beginReadArray还支持INI格式的显式指定QSettings settings(config.ini, QSettings::IniFormat);注意QSettings的键名是大小写敏感的但Windows注册表不区分大小写。如果你在Windows上写了Window/Width读的时候用window/width也能读到但在Linux上就不行。所以建议统一使用小写键名避免跨平台问题。5. 常见问题与排查技巧实录5.1 信号槽连接失败为什么我的槽没被调用这是新手最常见的问题。信号槽连接失败通常有以下几个原因现象可能原因排查方法编译通过但槽不执行忘了加Q_OBJECT宏检查类声明重新运行qmake/cmake连接返回false信号或槽签名不匹配用connect的返回值判断检查参数类型跨线程槽不执行接收者线程没有事件循环确认线程调用了exec()队列连接报错自定义类型未注册调用qRegisterMetaType槽执行了但参数不对连接类型选错检查是否用了DirectConnection跨线程最隐蔽的是Q_OBJECT宏遗漏。moc不会报错编译器也不会报错但运行时connect会输出“QObject::connect: No such signal”之类的警告。如果你在控制台看不到警告可以安装消息处理器qInstallMessageHandler([](QtMsgType type, const QMessageLogContext context, const QString msg) { QByteArray localMsg msg.toLocal8Bit(); fprintf(stderr, %s\n, localMsg.constData()); });这样所有Qt警告都会输出到stderr方便排查。5.2 程序崩溃在QObject析构对象树与线程的纠葛我遇到过好几次程序在退出时崩溃堆栈显示在QObject::~QObject()里。这类问题通常和对象树、线程有关。常见场景子对象在父对象析构后被访问父对象析构时删除了子对象但其他地方还持有子对象的指针。跨线程删除对象在A线程delete了属于B线程的对象导致B线程的事件循环访问已释放内存。deleteLater后立即退出事件循环deleteLater依赖事件循环如果事件循环已经退出对象不会被删除造成内存泄漏。排查这类问题我一般用QObject::dumpObjectTree()打印对象树看看父子关系是否符合预期。对于线程问题用QObject::thread()确认对象归属确保delete操作在对象所属线程执行。实操心得如果必须在跨线程场景下删除对象用QMetaObject::invokeMethod(obj, deleteLater, Qt::QueuedConnection)让删除操作在对象所属线程的事件循环中执行。这比直接delete安全得多。5.3 事件循环阻塞导致的界面卡死界面卡死是Qt程序最常见的性能问题。根本原因是在主线程执行了耗时操作阻塞了事件循环。排查方法很简单在耗时操作前后打时间戳看看哪个操作超过了50ms。超过50ms的操作就应该考虑移到后台线程。但移到后台线程不是万能的。如果后台线程需要更新界面必须通过信号槽切回主线程// 在worker线程中 emit resultReady(data); // 队列连接自动切回主线程 // 在主线程中 connect(worker, Worker::resultReady, this, MainWindow::updateUI);updateUI会在主线程执行因为MainWindow属于主线程队列连接会自动把调用投递到主线程的事件循环。另一个容易忽略的点是QPainter的绘制。如果在paintEvent里做了复杂计算同样会阻塞事件循环。建议把计算结果缓存起来paintEvent只负责绘制。5.4 内存泄漏排查从QObject对象树到智能指针Qt的对象树机制减少了内存泄漏但不是万能的。没有父对象的QObject需要手动管理或者用智能指针。QScopedPointer和QSharedPointer是Qt提供的智能指针用法和std::unique_ptr/std::shared_ptr类似但能和Qt的对象树机制配合。排查内存泄漏我一般用ValgrindLinux或Dr. MemoryWindows。但Qt的信号槽连接也会持有对象引用如果连接没有断开接收者对象不会被释放。QObject::disconnect可以手动断开但更推荐在接收者析构时自动断开——Qt会自动处理只要接收者是QObject。// 旧式连接接收者不是QObject时不会自动断开 connect(sender, SIGNAL(valueChanged(int)), this, SLOT(onValueChanged(int))); // 新式连接编译期检查接收者析构时自动断开 connect(sender, Sender::valueChanged, this, Receiver::onValueChanged);新式连接函数指针语法比旧式SIGNAL/SLOT宏更安全因为编译期就能检查签名匹配而且接收者析构时自动断开连接。我强烈建议所有新代码都用新式连接。6. 进阶技巧与性能优化6.1 用QElapsedTimer做精准性能测量QElapsedTimer是QtCore里做性能测量的工具比QTime更精准因为它使用单调时钟不受系统时间调整影响QElapsedTimer timer; timer.start(); // 执行耗时操作 doHeavyWork(); qint64 elapsed timer.elapsed(); // 毫秒 qDebug() Elapsed: elapsed ms;QElapsedTimer的精度取决于平台通常在微秒级。如果需要纳秒级精度可以用QElapsedTimer::nsecsElapsed()。6.2 QVariant的高级用法自定义类型与类型转换QVariant是Qt的“万能类型”可以存储任何已注册的类型。它在属性系统、数据库、模型视图里广泛使用。自定义类型要存入QVariant需要Q_DECLARE_METATYPEstruct Point { int x, y; }; Q_DECLARE_METATYPE(Point) Point p{10, 20}; QVariant var QVariant::fromValue(p); Point p2 var.valuePoint();QVariant还支持类型转换比如toString()、toInt()等。但转换失败时返回默认值不会报错。如果你需要知道转换是否成功用canConvert()先判断QVariant var(123); if (var.canConvertint()) { int value var.toInt(); }6.3 用QRegularExpression替代QRegExpQt 5引入了QRegularExpression基于PCRE引擎性能比旧的QRegExp好很多而且语法更标准。Qt 6已经移除了QRegExp所以新代码应该直接用QRegularExpressionQRegularExpression re((\\d{4})-(\\d{2})-(\\d{2})); QRegularExpressionMatch match re.match(2024-01-15); if (match.hasMatch()) { QString year match.captured(1); QString month match.captured(2); QString day match.captured(3); }QRegularExpression支持命名捕获组、前瞻断言、Unicode属性等高级特性功能比QRegExp强大得多。性能方面QRegularExpression的匹配速度通常快2-5倍复杂正则的差距更大。6.4 多线程编程QThread与QtConcurrent的选择QtCore提供了两种多线程方式QThread和QtConcurrent。QThread更底层适合需要精细控制线程生命周期的场景QtConcurrent更高层适合把单个函数放到线程池执行。QtConcurrent::run是最简单的多线程方式QFutureint future QtConcurrent::run([]() { // 在后台线程执行 return heavyComputation(); }); // 等待结果 future.waitForFinished(); int result future.result();QtConcurrent底层使用QThreadPool默认线程数是CPU核心数。你可以用QThreadPool::globalInstance()-setMaxThreadCount()调整。QThread适合需要长期运行的线程比如网络接收循环、串口读取等。但QThread的子类化继承QThread并重写run()通常不是最佳实践更推荐用moveToThread把工作对象移到线程中这样工作对象的槽函数会在新线程执行而QThread对象本身仍在原线程。提示QThread::run()默认调用exec()启动事件循环。如果你重写了run()但没有调用exec()线程中的事件循环不会启动队列连接的槽不会执行。这是很多初学者困惑的地方。7. 我踩过的那些坑与经验总结聊了这么多技术细节最后分享几个我实际踩过的坑都是文档里不会写的。第一个坑是QString::arg()的链式调用。arg()是按顺序替换占位符的但如果你混用了%1和%2而且参数类型不同可能会得到意外结果QString s %1 %2; s s.arg(hello).arg(42); // hello 42 s s.arg(42).arg(hello); // 42 hello更隐蔽的是如果第一个参数本身包含%1会被后续的arg替换QString s %1 %2; s s.arg(%2).arg(world); // %2 world 还是 world world // 结果是 world world因为第一个arg把%1替换成了%2 // 第二个arg又把%2替换成了world避免这个问题的方法是使用QString::arg的多参数版本或者用QStringBuilder的%操作符。第二个坑是QFile的readAll()在超大文件上会耗尽内存。readAll()一次性把整个文件读入内存如果文件有几个GB程序直接OOM。正确的做法是分块读取QFile file(large.bin); file.open(QIODevice::ReadOnly); const qint64 chunkSize 1024 * 1024; // 1MB while (!file.atEnd()) { QByteArray chunk file.read(chunkSize); processChunk(chunk); }第三个坑是QTimer在对象析构后仍然触发。如果QTimer的父对象被销毁定时器也会被销毁不会触发。但如果定时器没有父对象而且你在槽函数里访问了已销毁的对象就会崩溃。解决方案是用QPointer或者确保定时器的生命周期不超过其使用者。第四个坑是QHash的迭代器在插入时失效。QHash在插入新元素时可能触发rehash导致所有迭代器失效。如果你需要在遍历时插入先用QList收集要插入的键值对遍历完再统一插入。这些坑的共同点是它们不会在编译期报错也不会在简单Demo里暴露只有在特定条件下才会触发。所以我的建议是写Qt代码时多想想“如果这个对象被销毁了会怎样”“如果这个操作在另一个线程执行会怎样”“如果数据量放大100倍会怎样”。这种防御性思维比记住多少API都重要。QtCore模块的内容远不止这些还有QDateTime、QUrl、QJsonDocument、QXmlStreamReader、QProcess、QLibrary等等。但核心的设计思想和常用的类就是上面这些。把元对象系统、事件循环、隐式共享、线程模型这四个基石理解透了其他类都是触类旁通。我在实际项目里越来越觉得Qt的API设计有一种一致性——一旦你理解了它的设计哲学学新类就是看文档的事不需要再从头摸索。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑