# Day 2

### Qt 概述 / 创建项目 / 第一个程序 / 信号槽 → 计算器

C++/Qt 方向实训 · 第 2 / 4 个讲授日

- 上午（08:30–11:00 讲 / 11:00–12:00 练）：Qt 概述 · 创建 Qt 项目 · 第一个 Qt 小程序
- 下午（13:30–16:00 讲 / 16:00–17:00 作业）：信号槽机制 → 项目案例：简易计算器

#### 开始前准备（一次性）

    tools/install_qt_host.sh
    cmake -S . -B build -G Ninja -DBUILD_QT_PART=ON \
          -DCMAKE_PREFIX_PATH=$PWD/.qt514/5.14.2/gcc_64 -DCMAKE_BUILD_TYPE=Debug
    cmake --build build

上午 · 讲授 08:30–11:00

## Qt 概述 / 创建项目 / 第一个程序 讲

### Qt 概述 讲

#### 核心概念

- Qt = 跨平台 C++ 应用框架：一份代码，Windows/Linux/macOS 原生编译；模块化组织—— 本课程用 `Core`（对象/容器/IO）、`Gui`、`Widgets`（桌面控件）。
- 版本纪律：本实训统一用隔离安装的 Qt 5.14.2（`.qt514/`），不碰系统 Qt——顶层 CMakeLists 有版本断言，连错版本直接 `FATAL_ERROR`。这是工程上 "锁依赖版本"的真实示范。

出处：仓库根 CMakeLists.txt（版本断言一节）

### 创建 Qt 项目（CMake 方式） 讲

    find_package(Qt5 5.14 REQUIRED COMPONENTS Core Gui Widgets)
    add_executable(minimal_qt_app minimal_qt_app.cpp)
    set_target_properties(minimal_qt_app PROPERTIES AUTOMOC ON)
    target_link_libraries(minimal_qt_app PRIVATE Qt5::Core Qt5::Gui Qt5::Widgets)

#### 核心概念

- 昨天的 `find_package` 概念今天落地：CMake 按 `CMAKE_PREFIX_PATH` 找到 Qt；`AUTOMOC` 自动跑 moc（元对象编译器）——它为 `Q_OBJECT` 类生成信号槽所需的代码，下一页就见到。

出处：p03/CMakeLists.txt、 p03/ch02/CMakeLists.txt

### 第一个 Qt 小程序 讲

#### 核心概念

- `QApplication` 是唯一的事件循环拥有者，`app.exec()` 阻塞进入 事件循环分发系统事件；`Q_OBJECT` 宏触发 moc 生成信号槽/属性系统所需的元对象代码。

#### 常见的坑

- 一个进程只能有一个 `QApplication`；忘记 `exec()` 窗口一闪而过；
- 用了信号槽却忘 `Q_OBJECT`：报 `undefined reference`—— 链接错误（昨天那页！），报错完全不提"缺宏"，知道这一条能省一小时。

出处：p03/ch02/minimal_qt_app.cpp

### 控件创建与对象树 讲

#### 核心概念

- QObject 对象树 = Qt 的所有权答案（接昨天"谁负责 delete"）：创建控件把 parent 传进去，parent 析构自动 delete 所有子对象——整棵树只需要管好根。

#### 常见的坑

- 忘记传 parent：对象没人管，真实内存泄漏；
- `move()` 手动定位的控件一旦套上布局会被布局接管覆盖。

出处：p03/ch03/button_creation.cpp

### ★ 栈对象与父子所有权打架 讲

    QPushButton btn("我建在栈上");   // 先声明 → 后析构
    QWidget window;                  // 后声明 → 先析构
    btn.setParent(&window);
    // 离开作用域：window 先析构 → 父子树 delete &btn（栈地址！）→ 崩溃

#### 核心概念

- 父子树会 delete 子对象 ⇒ 交给父子树管的对象必须建在堆上。一个对象只能有 一个所有者：给了 parent，就不能再由栈/智能指针/手动 delete 管。

#### 常见的坑

- 声明顺序反过来时这段代码"恰好不崩"——不要赌析构顺序的运气，规则只有一条。 现场演示：`tools/run_qt.sh qt_pitfall_stack_widget --crash`（真崩给你看）。

复现示例：examples/pitfall_stack_widget/ （README）

上午 · 课堂练习 11:00–12:00（工程师指导、答疑）

## 动手：跑通最小程序 练

#### 任务卡 1 · 跑通并改造最小程序 练

目标
  编译运行 `minimal_qt_app` 与 `button_creation`；把窗口标题和 按钮文字改成自己的学号+姓名（中文——如果乱码了，记住现象，下午有一页专讲）
验收标准
  `QT_QPA_PLATFORM=offscreen tools/run_qt.sh minimal_qt_app` 退出码 0； 有 X 环境下能看到自己改的中文标题正常显示
基础知识 · 参考
  p03/ch02/minimal_qt_app.cpp · p03/ch03/button_creation.cpp

下午 · 讲授 13:30–16:00

## 信号槽机制 → 计算器案例 讲

### 信号槽：内置 / 自定义 / lambda 讲

#### 核心概念

- 观察者模式的 Qt 实现：发送者只 `emit`，不知道谁在听——对象间彻底解耦；
- 新语法 `&Class::signal` 编译期类型检查；旧字符串语法 `SIGNAL()/SLOT()` 只在运行时检查；连接方式 Direct/Queued/Auto（Day5 跨线程时回收）。

#### 常见的坑

- 旧语法写错不报编译错误，只是运行时静默连接失败；
- 重载信号不加 `QOverload` 消歧义直接编译报错。

出处：p03/ch04/builtin_signal_slot.cpp · custom_signal_slot.cpp · lambda_signal_slot.cpp

### ★ lambda 槽的生命周期与第五参数 讲

    connect(sender, &Sender::sig, [obj]{ obj->f(); });        // ✗ obj 销毁后仍触发
    connect(sender, &Sender::sig, obj, [obj]{ obj->f(); });   // ✓ 第五参数=context

#### 核心概念

- 第五参数（context object）决定连接跟谁的生命周期挂钩：传了 context，对象析构时 Qt 自动断开连接。规则：lambda 捕获了某个 QObject，就把它作为第五参数传进去。

#### 常见的坑

- 不传 context：接收方销毁后 lambda 仍被调用，捕获的指针就是悬空指针——UB 可能崩、 也可能"打印出看似正常的旧值"，后者更危险（开发机正常、演示现场炸）。

复现示例：examples/pitfall_lambda_capture/ （README， `--dangle` 看真实悬空访问）

### ★ 重复 connect：点一次按钮弹两次框 讲

#### 核心概念

- `connect` 每调一次新增一条连接，Qt 不去重——同一对信号槽连两次， 槽就被调两次。修复：connect 收拢到构造函数只做一次（首选）；或 `Qt::UniqueConnection`（只对成员函数指针有效，lambda 防不了）。

#### 常见的坑

- connect 写在会被多次执行的路径上（每次打开对话框都跑一遍的"初始化"函数）—— 现象是"点一次弹两次"，第一反应查按钮，根源在连接次数。

复现示例：examples/pitfall_double_connect/ （README）

### ★ QString 与中文编码 讲

#### 核心概念

- `QString` 内部是 UTF-16，和 `std::string`（字节串）是两种东西， 互转必须指明编码：`toUtf8()`/`fromUtf8()`；中文字面量建议 `QStringLiteral("中文")` + 源码文件保存为 UTF-8。

#### 常见的坑

- 源码不是 UTF-8、或用 `QString(char*)` 隐式转换中文字面量 → 界面乱码。 上午改学号姓名乱码的同学：根源就在这页。Day4 读写文件时编码问题还会再来一次。

### 案例 · 简易计算器（布局脚手架：先用后学） 讲

    auto* grid = new QGridLayout(window);         // 布局原理明天下午讲透
    grid->addWidget(btn7, 0, 0);  grid->addWidget(btn8, 0, 1);   // 今天先会用：
    grid->addWidget(btn9, 0, 2);  grid->addWidget(btnDiv, 0, 3); // 行、列坐标摆键盘

#### 组装思路

- 数字/运算符按钮（上午的 `button_creation`）+ 点击信号槽（刚学的三种写法） + `QGridLayout` 脚手架排键盘 + `QLabel`/`QLineEdit` 显示结果。
- 项目 = 已学知识点的组装——这是 Day5 综合案例和 Day6–10 项目的预演。

下午 · 课堂作业 16:00–17:00（工程师答疑）

## 作业：完成计算器 练

#### 任务卡 2 · 简易计算器 练

目标
  组合按钮 + 信号槽 + QGridLayout 脚手架，做出能加减乘除的最小计算器
验收标准
  至少支持两个数 + 一种运算符的计算并正确显示结果；界面不重叠、按钮可点击； connect 全部用新语法且写在构造函数里（今天的坑卡现学现用）
基础知识 · 参考
  p03/ch03/button_creation.cpp · p03/ch04/custom_signal_slot.cpp · p03/ch08/custom_widget.cpp（布局参考）

## 当日产出验证

#### 验收

现场演示计算器完成至少一次正确运算（`QT_QPA_PLATFORM=offscreen tools/run_qt.sh <target>` 或有 X 环境直接运行）；中文界面文字无乱码。

返回目录 · ← 上一天 · 下一天 →