3、C++基础回顾与Qt扩展:C++核心语法回顾、Qt对C++的扩展(信号与槽、元对象系统)、Qt容器与算法
说实话,很多同学学Qt学得痛苦,根源不在Qt本身,而是C++基础没打牢。我见过太多人,信号与槽写得很溜,但一问到虚函数、智能指针就懵了。嗯,这其实很正常——Qt对C++做了很多封装,让你感觉不到底层细节,但一旦遇到复杂问题,基础不牢的代价就来了。
这一章,咱们不搞「从零学C++」那套。我挑几个最核心、最常用的点,结合我踩过的坑,帮你快速回顾。同时看看Qt到底在C++之上「加」了哪些好东西。
3.1 C++核心语法:你真正需要掌握的
我个人习惯,把C++基础分成三块:内存管理、面向对象、模板与STL。咱们一个一个来。
3.1.1 内存管理:别再new了
我记得刚入行那会儿,写代码全是 new 和 delete。后来项目越做越大,内存泄漏查得我头皮发麻。现在我的原则很简单:能用栈就不用堆,能用智能指针就不用裸指针。
核心原则:
- 局部对象:直接定义在栈上,生命周期自动管理
- 需要动态分配:用
std::unique_ptr或std::shared_ptr - Qt对象:利用Qt的父子对象机制,自动析构
// 不推荐
MyClass* obj = new MyClass();
// ... 忘了 delete
// 推荐(C++11+)
auto obj = std::make_unique<MyClass>();
// Qt风格
MyClass* obj = new MyClass(parent); // parent 负责析构
我的经验:在Qt项目中,尽量用Qt的父子对象机制。它比智能指针更轻量,而且与Qt的事件循环完美配合。但如果你在写纯C++算法库,那就用 std::unique_ptr。
3.1.2 面向对象:多态是灵魂
面向对象三大特性——封装、继承、多态。封装和继承大家都会,但多态很多人用不好。说白了,多态就是「父类指针调用子类方法」。Qt里到处都是这个模式:你写一个 QWidget 的子类,然后把它当作 QWidget* 传给布局管理器。
class Base {
public:
virtual void show() { std::cout << "Base\n"; }
};
class Derived : public Base {
public:
void show() override { std::cout << "Derived\n"; }
};
void print(Base* b) {
b->show(); // 多态:实际调用 Derived::show()
}
我曾经踩过的坑:忘记在析构函数前加 virtual。结果子类对象被父类指针删除时,只调了父类的析构函数,子类资源没释放。嗯,内存泄漏就这么来的。
3.1.3 模板与STL:写通用代码的利器
模板这东西,初看觉得麻烦,用多了就真香。Qt的容器和算法大量使用了模板。你想想看,QList<int> 和 QList<QString> 写法几乎一样,这就是模板的功劳。
template <typename T>
T max(T a, T b) {
return (a > b) ? a : b;
}
// 使用
int m1 = max(3, 5);
double m2 = max(3.14, 2.71);
STL里的 std::vector、std::map 也是高频使用的。但注意,在Qt项目中,我更推荐用Qt自己的容器(后面会讲)。
3.2 Qt对C++的扩展:信号与槽、元对象系统
Qt最牛的地方,就是它用C++实现了一套「元对象系统」。说白了,就是让C++有了反射、动态属性、信号与槽这些能力。没有这套系统,Qt就是个普通的GUI库。
3.2.1 信号与槽:Qt的灵魂
信号与槽,本质上是观察者模式的一种实现。一个对象发出信号,另一个对象的槽函数被调用。关键是:信号和槽不需要知道对方的存在。这大大降低了耦合度。
// 定义
class MyButton : public QPushButton {
Q_OBJECT
public:
// ...
signals:
void clicked(); // 信号:只声明,不实现
public slots:
void onButtonClicked(); // 槽:普通成员函数
};
// 连接
connect(button, &QPushButton::clicked, this, &MyWidget::onButtonClicked);
为什么信号与槽这么重要?
- 类型安全:编译期检查参数类型
- 松耦合:发送者和接收者互不依赖
- 线程安全:可以跨线程传递信号
我个人习惯,在写自定义组件时,一定会把「状态变化」设计成信号。比如一个温度计控件,温度变了就发 temperatureChanged(double) 信号。这样其他模块想监听温度变化,直接连这个信号就行,完全不用改温度计的代码。
3.2.2 元对象系统:Qt的「黑魔法」
元对象系统,说白了就是Qt在编译时给类加了一些「元信息」。比如类名、父类、属性列表、信号列表等。这些信息在运行时可以查询和操作。这就是反射。
要使用元对象系统,你的类必须满足三个条件:
- 继承自
QObject - 在类声明开头写
Q_OBJECT宏 - 用
moc(元对象编译器)处理头文件
class MyClass : public QObject {
Q_OBJECT
Q_PROPERTY(int value READ value WRITE setValue NOTIFY valueChanged)
public:
int value() const;
void setValue(int v);
signals:
void valueChanged(int);
};
有了元对象系统,你可以做很多「常规C++做不到」的事:
- 运行时获取类名:
obj->metaObject()->className() - 动态调用方法:
QMetaObject::invokeMethod() - 属性系统:
obj->setProperty("value", 42)
我的经验:元对象系统虽然强大,但不要滥用。我见过有人把所有业务逻辑都塞进 QObject 子类里,结果编译慢得要死。记住:只有需要信号与槽、属性系统、运行时反射的类,才继承 QObject。
3.3 Qt容器与算法:比STL更贴心
Qt提供了一套自己的容器类,比如 QList、QMap、QHash 等。它们和STL容器功能类似,但有几个明显的优势:
- 隐式共享(写时复制):拷贝容器时,只复制指针。只有真正修改内容时,才进行深拷贝。性能更好。
- 与Qt信号与槽无缝集成:可以直接在信号中传递容器。
- 更友好的API:比如
QList::append()比std::vector::push_back()更直观。 - 支持Java风格迭代器:
foreach宏(虽然C++11后不推荐了)。
// Qt容器
QList<int> list;
list << 1 << 2 << 3; // 流式操作
list.append(4);
QMap<QString, int> map;
map["apple"] = 5;
map.insert("banana", 3);
// 遍历
for (auto key : map.keys()) {
qDebug() << key << map[key];
}
Qt还提供了很多实用的算法函数,比如 qSort()、qFind()、qCopy() 等。不过C++11之后,STL的算法已经很强大了,我个人现在更倾向于用STL算法,因为更通用。
注意:Qt容器和STL容器可以互相转换,但要注意性能。比如 QList 转 std::vector 会触发深拷贝。我建议:在Qt项目内部,统一用Qt容器;在需要与第三方库交互时,再转成STL容器。
3.4 知识体系总览
下面这张图,帮你理清本章的核心脉络。你可以把它当作一个「知识地图」,随时回来查阅。
3.5 本章小结
这一章内容不少,但核心就三件事:
- C++基础:内存管理、多态、模板——这些是写任何C++程序都绕不开的。别偷懒,一定要掌握。
- Qt扩展:信号与槽、元对象系统——这是Qt的「独门绝技」,也是你从「会用Qt」到「懂Qt」的分水岭。
- Qt容器与算法:比STL更贴心,但也要注意使用场景。我的建议是:Qt项目内用Qt容器,跨项目用STL。
嗯,基础就回顾到这里。下一章咱们开始动手写真正的Qt项目。到时候你会发现,今天讲的这些东西,全都会用上。