一,项目学习介绍
1,简单理解

2,阶段分布

3,验收目标

4,计划安排

二,磁盘故障排查(这是我遇到的问题)
现象
• VMware 报错:Exception 0xc0000006 (disk error while paging)。
• Ubuntu 启动后进入 emergency mode。
• 出现 /bin/bash: No such file or directory。
• fsck 检查发现块计数、inode 位图和孤立文件错误。
原因
虚拟机存放在外置 E 盘。异常关机、连接中断或写入未完成,导致三层存储出现异常:
-
Windows 的 E 盘文件系统异常。
-
VMware 的 VMDK 虚拟磁盘需要修复。
-
Ubuntu 的 /dev/sda2 ext4 文件系统损坏。
/bin/bash 实际没有被删除,只是根文件系统异常,启动时无法正常访问。目前没有证据表明物理硬盘已经损坏。
解决方案
-
先把虚拟机文件完整备份到 D 盘,并校验文件一致性。
-
使用 chkdsk E: /f /x 修复 E 盘文件系统。
-
使用 VMware 工具修复主 VMDK 文件。
-
从 Ubuntu ISO 进入临时系统。
-
确认系统分区为 /dev/sda2,执行:
Bash
启用自动换行
复制
sudo fsck -f -y /dev/sda2
-
再检查一次,确认没有新错误。
-
挂载分区并确认 /mnt/bin/bash 存在。
-
卸载分区、断开 ISO,然后重新启动原系统。
目前 Ubuntu 已恢复正常启动,D 盘备份暂时保留,不要删除
三,第一周汇报
本周学习结果
- 已在 Ubuntu 桌面环境完成 LAN Device Monitor 最小版本,并通过编译和运行测试。
- 有效输入能够加入表格,空输入能够弹出警告且不会添加数据。
- 已能说明按钮点击后的调用链,以及 QObject 父子关系、事件循环和输入校验的基本原因。
掌握到什么程度
|--------|--------|--------------------------|
| 层级 | 能力 | 判断标准 |
| 第 1 级 | 看懂 | 看到代码能说出它大致在做什么。 |
| 第 2 级 | 解释 | 能说明为什么这样写,并能使用专业术语表达。 |
| 第 3 级 | 应用 | 能写出关键结构,修改小功能,并根据错误定位问题。 |
| 第 4 级 | 独立 | 不看完整答案完成四个文件、构建、测试和排错。 |
第一周目标 稳定达到第 2 级,并开始进入第 3 级。不要求死记全部 include,但必须能解释核心因果关系,并能找到对应代码。
使用方法
- 先只读类比,用自己的话复述生活场景。
- 再读专业表达,把生活说法换成 Qt 和 C++ 术语。
- 遮住解释,只看代码,说出每行为什么存在。
- 回答闯关题时必须说明原因,不只回答对错或结果。参考答案放在文档最后。
一 第一周十二关路线
|--------|-----------------------|--------------------------|
| 关卡 | 知识点 | 通过标准 |
| 1 | 工程结构 | 说清四个文件和构建流程 |
| 2 | MainWindow 继承 QWidget | 说清窗口能力来自哪里 |
| 3 | 成员变量和初始化列表 | 区分声明与初始化 |
| 4 | QObject 父子关系 | 区分指针作用域与对象生命周期 |
| 5 | 信号与槽 | 说清 connect 四个组成部分 |
| 6 | 函数声明与 Q_OBJECT | 理解 MainWindow 作用域和 moc |
| 7 | QString 读取输入 | 理解 text trimmed 和 const |
| 8 | 输入校验 | 理解 isEmpty 逻辑与 return |
| 9 | 布局管理 | 区分 addWidget 与 addLayout |
| 10 | 表格和界面反馈 | 完成插入 更新 清空和焦点 |
| 11 | 事件循环和调用链 | 解释程序为何持续响应 |
| 12 | 构建和验收 | 完成 qmake make 运行和测试 |
二 十二个知识点
第 1 关 工程结构
类比理解 把项目看成一套小房子。.pro 是施工清单,main.cpp 是大门和总电源,mainwindow.h 是房屋结构图,mainwindow.cpp 是具体施工和房间功能。
|------------------------|--------|---------------------------------------|
| 文件 | 类比 | 专业职责 |
| lan_device_monitor.pro | 施工清单 | 声明 Qt 模块、源文件、头文件和目标程序名。 |
| main.cpp | 入口和总电源 | 创建 QApplication 和 MainWindow,并启动事件循环。 |
| mainwindow.h | 结构图 | 声明窗口类、成员变量、构造函数和槽函数。 |
| mainwindow.cpp | 具体施工 | 创建控件、设置布局、建立连接并实现 addDevice。 |
专业表达 qmake 读取 .pro 文件并生成 Makefile;make 按照 Makefile 调用编译器和链接器。Qt 的信号槽和 Q_OBJECT 还需要 moc 生成辅助代码。

闯关题 1 分别说明四个文件的职责,并解释为什么通常先运行 qmake,再运行 make。
第 2 关 MainWindow 继承 QWidget
类比理解 QWidget 像一间具备墙、门和尺寸的基础房间。MainWindow 在这个基础上增加输入框、按钮和表格,因此它仍然是一种 QWidget。
专业表达 MainWindow 通过 public 继承成为 QWidget 的派生类,所以能够使用 QWidget 提供的 setWindowTitle、resize、show 等成员函数。
cpp
class MainWindow : public QWidget
{
Q_OBJECT
// ...
};
MainWindow::MainWindow(QWidget *parent)
: QWidget(parent)
{
setWindowTitle("LAN Device Monitor");
resize(640, 360);
}
闯关题 2 MainWindow 没有自己定义 setWindowTitle,为什么仍然能调用它?构造函数为什么要写 QWidget(parent)?
第 3 关 成员变量和初始化列表
类比理解 类中的成员声明像先在资产清单上预留栏目;构造函数初始化列表才是在对象出生时,把真实控件放进对应栏目。两者不是重复操作。
专业表达 QLineEdit *nameEdit 声明了一个指针成员。nameEdit(new QLineEdit(this)) 在进入构造函数体之前初始化该成员,使它指向新创建的 QLineEdit 对象。成员变量会随着 MainWindow 对象继续存在。
cpp
// mainwindow.h 中声明成员
QLineEdit *nameEdit;
// mainwindow.cpp 的构造函数初始化列表中初始化成员
MainWindow::MainWindow(QWidget *parent)
: QWidget(parent),
nameEdit(new QLineEdit(this))
{
}
闯关题 3 头文件中的 QLineEdit *nameEdit 和初始化列表中的 nameEdit(new QLineEdit(this)) 分别做什么?为什么不是声明两次?
第 4 关 QObject 父子关系
类比理解 指针变量像写着房间地址的纸条,控件对象才是真正的家具。纸条离开作用域不等于家具消失;如果家具登记在 MainWindow 名下,MainWindow 销毁时会统一处理它。
专业表达 new QLineEdit(this) 把 MainWindow 设置为输入框的 QObject 父对象。父对象销毁时会自动销毁子对象。局部指针变量的作用域和堆上对象的生命周期是两个问题。
|---------------|----------------------|------------------------|
| 对象或变量 | 作用 | 何时结束 |
| 局部指针变量 | 保存对象地址 | 离开当前函数作用域时消失 |
| 成员指针 nameEdit | 由 MainWindow 保存输入框地址 | 随 MainWindow 对象结束 |
| QLineEdit 对象 | 真正的输入框控件 | 父对象 MainWindow 销毁时自动销毁 |
闯关题 4 构造函数结束后,成员指针 nameEdit 和它指向的输入框对象是否还存在?如果把指针写成构造函数里的局部变量,情况有什么不同?
第 5 关 信号与槽
类比理解 按钮像门铃,clicked 信号是铃声,connect 是提前接好的线路,addDevice 是接到铃声后执行登记工作的接待员。接线路时不会立即登记,真正按铃后才执行。
专业表达 QObject::connect 把发送者的信号连接到接收者的槽函数。函数指针写法能让编译器检查信号和槽的类型。
cpp
connect(addButton, &QPushButton::clicked,
this, &MainWindow::addDevice);
|----------|-----------------------|------------------|
| 组成部分 | 本项目中的对象 | 含义 |
| 发送者 | addButton | 发出通知的按钮对象 |
| 信号 | QPushButton::clicked | 按钮被点击的通知 |
| 接收者 | this | 当前 MainWindow 对象 |
| 槽函数 | MainWindow::addDevice | 收到通知后执行的成员函数 |
闯关题 5 指出 connect 的四个组成部分,并解释为什么写这行代码时 addDevice 不会立即执行。
第 6 关 函数声明与 Q_OBJECT
类比理解 头文件里的函数声明像菜单上写着可以提供某项服务;源文件里的函数定义才是后厨真正完成服务的过程。MainWindow:: 相当于标明这项服务属于哪个部门。
专业表达 void addDevice(); 是成员函数声明;void MainWindow::addDevice() 是类外定义。MainWindow:: 是作用域限定符。Q_OBJECT 启用 Qt 元对象系统,qmake 会安排 moc 处理该类。
cpp
// mainwindow.h
private slots:
void addDevice();
// mainwindow.cpp
void MainWindow::addDevice()
{
// 函数实现
}
闯关题 6 声明和定义有什么区别?MainWindow:: 表示什么?加入 Q_OBJECT 后为什么需要重新运行 qmake?
第 7 关 QString 读取输入
类比理解 先从表单抄下用户填写的文字,再把首尾无意义的空格擦掉,最后把结果封存,避免后面的代码意外修改它。
专业表达 QLineEdit::text() 返回 QString;QString::trimmed() 删除首尾空白,不删除中间空格;const 表示变量初始化后不再修改。
cpp
const QString name =
nameEdit->text().trimmed();
const QString ip =
ipEdit->text().trimmed();
闯关题 7 如果输入是两个前导空格加 Ubuntu 三个空格加 PC 再加两个尾随空格,trimmed 后会留下什么?const 在这里防止什么?
第 8 关 输入校验
类比理解 登记前先检查名称和地址是否齐全。任意一项缺失,就提示用户并结束本次登记,不能继续把不完整数据写进表格。
专业表达 isEmpty() 检查 QString 内容是否为空;逻辑或运算符 || 表示任意一个条件成立,整个条件就成立;return 立即结束本次 addDevice 调用。
cpp
if (name.isEmpty() || ip.isEmpty())
{
QMessageBox::warning(
this,
"Missing Information",
"Please enter a device name and IP address.");
return;
}
闯关题 8 名称为空、IP 有内容时会发生什么?为什么表格不会增加行,但程序仍然能够继续操作?
第 9 关 布局管理
类比理解 控件是要摆放的家具,布局是负责排列家具的管理员。窗口变大或变小时,管理员会重新分配位置和空间。
专业表达 addWidget 把控件加入布局,addLayout 把一个子布局嵌套进另一个布局。布局负责控件的尺寸、间距、边距和对齐,减少手工坐标计算。
cpp
QFormLayout *formLayout = new QFormLayout;
formLayout->addRow("Device Name:", nameEdit);
formLayout->addRow("IP Address:", ipEdit);
QVBoxLayout *mainLayout = new QVBoxLayout(this);
mainLayout->addLayout(formLayout);
mainLayout->addWidget(addButton);
mainLayout->addWidget(deviceTable);
mainLayout->addWidget(statusLabel);
闯关题 9 addLayout 和 addWidget 分别加入什么?为什么使用布局后不需要为每个控件写固定坐标?
第 10 关 表格和界面反馈
类比理解 设备表像登记簿。rowCount 找到下一条空白记录的位置,insertRow 插入新行,setItem 按列填写内容。登记完成后,状态栏给出反馈,并把输入区恢复为下一次登记的状态。
专业表达 QTableWidget 使用从 0 开始的行列索引。当前行数可作为末尾新行的索引。setItem 为指定单元格设置 QTableWidgetItem。clear 只清除输入框文字,不删除控件对象;setFocus 把键盘焦点放回名称输入框。
cpp
const int row = deviceTable->rowCount();
deviceTable->insertRow(row);
deviceTable->setItem(row, 0, new QTableWidgetItem(name));
deviceTable->setItem(row, 1, new QTableWidgetItem(ip));
deviceTable->setItem(row, 2, new QTableWidgetItem("Unknown"));
deviceTable->setItem(row, 3, new QTableWidgetItem("Never"));
statusLabel->setText(QString("Added %1.").arg(name));
nameEdit->clear();
ipEdit->clear();
nameEdit->setFocus();
闯关题 10 表格已经有 2 行时,rowCount 返回多少,新数据插入到哪一行?clear 以后 nameEdit 对象是否还存在?
第 11 关 事件循环和完整调用链
类比理解 GUI 主线程像一直值班的前台。它不断接收点击、键盘和重绘任务。一次任务处理完就回到值班状态;如果前台被一个很长的循环占住,其他事件只能排队,窗口看起来就卡住了。
专业表达 app.exec() 启动 Qt 事件循环。事件循环在 GUI 线程中分发输入、定时器和重绘事件。槽函数执行 return 后控制权回到事件循环,不会退出整个应用。长时间阻塞 GUI 线程会让事件无法及时分发。

闯关题 11 为什么 addDevice 中的 return 不会关闭程序?为什么在槽函数里执行很长的循环会让窗口卡住?
第 12 关 构建和功能验收
类比理解 qmake 先根据项目清单生成施工规则,make 再按规则完成编译和链接,最后运行生成的程序进行验收。规则改变时,需要重新生成规则。
专业表达 修改 .pro、加入新的源文件或引入需要 moc 处理的 Q_OBJECT 类后,应重新运行 qmake。make 只按现有 Makefile 构建,不会替代 qmake 的项目分析工作。
cpp
qmake lan_device_monitor.promake -j4./lan_device_monitor
|--------|---------------------------|---------------------------------------|
| 测试 | 输入 | 预期结果 |
| 空输入 | 名称或 IP 为空 | 弹出 Missing Information,不增加表格行,程序继续运行。 |
| 有效输入 | Ubuntu PC 和 192.168.10.10 | 新增四列数据,状态变为 Added Ubuntu PC.,输入框清空。 |
闯关题 12 如果新增了 Q_OBJECT 类,却只运行 make,没有重新运行 qmake,问题可能出在哪里?完整的构建和验收顺序是什么?
三 完整代码练习
先完成前面的知识点,再练习完整代码。第一遍可以对照输入;第二遍只看文件结构和功能要求;第三遍重点练习 connect、输入校验、表格插入和事件循环。注释解释本周新增或容易混淆的内容。
lan_device_monitor pro
cpp
QT += widgets
CONFIG += c++11
TEMPLATE = app
TARGET = lan_device_monitor
SOURCES += main.cpp \
mainwindow.cpp
HEADERS += mainwindow.h
main cpp
cpp
#include <QApplication>
#include "mainwindow.h"
int main(int argc, char *argv[])
{
// QApplication 管理 GUI 应用资源和事件循环。
QApplication app(argc, argv);
// MainWindow 是顶层窗口对象。
MainWindow window;
window.show();
// 启动事件循环,程序在这里持续等待界面事件。
return app.exec();
}
mainwindow h
cpp
#ifndef MAINWINDOW_H
#define MAINWINDOW_H
#include <QWidget>
class QLabel;
class QLineEdit;
class QPushButton;
class QTableWidget;
class MainWindow : public QWidget
{
// 启用 Qt 元对象系统,qmake 会安排 moc 处理。
Q_OBJECT
public:
explicit MainWindow(QWidget *parent = nullptr);
private slots:
// 声明按钮点击后执行的槽函数。
void addDevice();
private:
// 成员指针保存各个控件对象的地址。
QLineEdit *nameEdit;
QLineEdit *ipEdit;
QPushButton *addButton;
QTableWidget *deviceTable;
QLabel *statusLabel;
};
#endif
mainwindow cpp
cpp
#include "mainwindow.h"
#include <QAbstractItemView>
#include <QFormLayout>
#include <QHeaderView>
#include <QLabel>
#include <QLineEdit>
#include <QMessageBox>
#include <QPushButton>
#include <QString>
#include <QStringList>
#include <QTableWidget>
#include <QTableWidgetItem>
#include <QVBoxLayout>
MainWindow::MainWindow(QWidget *parent)
: QWidget(parent),
// 初始化列表在构造函数体执行前初始化成员。
// this 使这些控件成为 MainWindow 的子对象。
nameEdit(new QLineEdit(this)),
ipEdit(new QLineEdit(this)),
addButton(new QPushButton("Add Device", this)),
deviceTable(new QTableWidget(0, 4, this)),
statusLabel(new QLabel("Ready.", this))
{
setWindowTitle("LAN Device Monitor");
resize(640, 360);
nameEdit->setPlaceholderText("Ubuntu PC");
ipEdit->setPlaceholderText("192.168.10.10");
QStringList headers;
headers << "Name"
<< "IP Address"
<< "Status"
<< "Last Check";
deviceTable->setHorizontalHeaderLabels(headers);
deviceTable->horizontalHeader()->setSectionResizeMode(
QHeaderView::Stretch);
deviceTable->setSelectionBehavior(
QAbstractItemView::SelectRows);
deviceTable->setEditTriggers(
QAbstractItemView::NoEditTriggers);
// 表单布局管理两组标签和输入框。
QFormLayout *formLayout = new QFormLayout;
formLayout->addRow("Device Name:", nameEdit);
formLayout->addRow("IP Address:", ipEdit);
// 主布局从上到下排列子布局、按钮、表格和状态文字。
QVBoxLayout *mainLayout = new QVBoxLayout(this);
mainLayout->addLayout(formLayout);
mainLayout->addWidget(addButton);
mainLayout->addWidget(deviceTable);
mainLayout->addWidget(statusLabel);
// 这里只建立连接,不会立即调用 addDevice。
connect(addButton, &QPushButton::clicked,
this, &MainWindow::addDevice);
}
void MainWindow::addDevice()
{
// 读取文字并删除首尾空白,const 防止后续误改。
const QString name =
nameEdit->text().trimmed();
const QString ip =
ipEdit->text().trimmed();
// 任意一项为空时提示并结束本次槽函数调用。
if (name.isEmpty() || ip.isEmpty())
{
QMessageBox::warning(
this,
"Missing Information",
"Please enter a device name and IP address.");
return;
}
// rowCount 返回当前行数,可直接作为末尾新行的索引。
const int row = deviceTable->rowCount();
deviceTable->insertRow(row);
deviceTable->setItem(
row, 0, new QTableWidgetItem(name));
deviceTable->setItem(
row, 1, new QTableWidgetItem(ip));
deviceTable->setItem(
row, 2, new QTableWidgetItem("Unknown"));
deviceTable->setItem(
row, 3, new QTableWidgetItem("Never"));
statusLabel->setText(
QString("Added %1.").arg(name));
// clear 只清除文字,不删除输入框对象。
nameEdit->clear();
ipEdit->clear();
nameEdit->setFocus();
}
编译和运行
cpp
qmake lan_device_monitor.promake -j4./lan_device_monitor
四 综合自测
答题要求 每题至少包含条件、执行过程和结果。能说出结果但说不出原因,说明还需要回到对应知识点复习。
1. 不看文档,写出函数指针形式的 connect,并逐项指出发送者、信号、接收者和槽函数。
2. 说明构造函数结束后 nameEdit 成员指针、输入框对象和一个假设的局部指针变量分别会发生什么。
3. 从点击 Add Device 开始,完整描述程序回到事件循环之前的执行顺序。
4. 预测名称为空、IP 为 192.168.10.10 时的界面结果,并解释为什么不会新增表格行。
5. 预测输入 Ubuntu PC 和 192.168.10.10 后,表格、状态文字、输入框和焦点分别发生什么。
6. 解释修改 .pro 或加入 Q_OBJECT 类后为什么要重新运行 qmake。
7. 把状态文字改为 Device added: Ubuntu PC。指出只需要修改哪一行,以及 QString::arg 如何替换占位符。
8. 关闭完整代码,只根据四个文件的职责重新建立工程。编译报错时先判断属于 C++ 语法、Qt 类型、moc 还是构建文件问题。
实践验收清单
|---------|---------------------------|
| 检查项 | 通过标准 |
| 工程 | 四个文件存在,文件名和 .pro 中的记录一致。 |
| 构建 | qmake 和 make 成功,程序可以启动。 |
| 空输入 | 弹出英文警告,不增加数据,窗口继续响应。 |
| 有效输入 | 表格新增四列数据,状态正确,输入框清空并恢复焦点。 |
| 口头解释 | 能解释完整调用链和每个关键步骤的原因。 |
| 小修改 | 能独立修改一处界面文字并重新构建验证。 |
五 闯关题参考答案
第 1 题 四个文件分别负责项目配置、程序入口、类结构声明和窗口行为实现。qmake 先把 .pro 转换成 Makefile,make 再按 Makefile 编译和链接。
第 2 题 MainWindow public 继承 QWidget,因此拥有 QWidget 的窗口能力。QWidget(parent) 初始化继承来的 QWidget 部分,并把 parent 交给父类。
第 3 题 QLineEdit *nameEdit 在类中声明成员;初始化列表在对象构造时让该成员指向新建的 QLineEdit。声明说明成员存在,初始化给成员初始值。
第 4 题 成员指针 nameEdit 和输入框对象都会随着 MainWindow 继续存在。局部指针变量会在构造函数结束时消失,但如果它指向的 QObject 已把 MainWindow 设为父对象,对象仍由父对象管理。
第 5 题 发送者是 addButton,信号是 QPushButton::clicked,接收者是 this,槽函数是 MainWindow::addDevice。connect 只保存连接关系,点击发生后才触发槽函数。
第 6 题 声明只告诉编译器函数的名称和类型,定义提供函数体。MainWindow:: 表示该函数属于 MainWindow。Q_OBJECT 需要 moc 生成元对象代码,因此应让 qmake 更新构建规则。
第 7 题 trimmed 删除首尾空格,结果仍是 Ubuntu 三个空格 PC,因为中间空格不会被删除。const 防止 name 和 ip 在后续代码中被重新赋值。
第 8 题 名称为空会使 if 条件成立,程序显示 Missing Information,然后 return 结束本次 addDevice 调用。插入表格的代码不会执行,但 Qt 事件循环仍在运行。
第 9 题 addLayout 加入子布局,addWidget 加入控件。布局根据窗口大小自动计算控件的几何位置、尺寸、间距和边距。
第 10 题 已有 2 行时 rowCount 返回 2,新数据插入索引为 2 的第三行。clear 只清除文字,nameEdit 控件对象仍然存在。
第 11 题 return 只结束 addDevice,随后控制权回到 app.exec 运行的事件循环。长循环持续占用 GUI 线程,使点击和重绘事件无法及时分发,因此窗口看起来卡住。
第 12 题 新增 Q_OBJECT 类后只运行 make,旧 Makefile 可能没有正确的 moc 规则或生成文件。正确顺序是 qmake、make、运行程序,再测试空输入和有效输入。
六 第一周最终复述模板
本项目使用 Qt Widgets 和 C++ 实现局域网设备登记界面。MainWindow 继承 QWidget,输入框、按钮、表格和状态标签作为它的子对象,由 QObject 父子关系管理生命周期。QPushButton 的 clicked 信号通过函数指针形式的 connect 连接到 MainWindow::addDevice 槽函数。槽函数读取并校验输入,验证失败时显示警告并结束本次调用,验证成功时在 QTableWidget 末尾插入一行,再更新状态、清空输入并恢复焦点。app.exec() 运行事件循环,使 GUI 线程持续接收点击、输入和重绘事件。
完成标准 合上文档后,能够用自己的话说出上面这段内容,并能在代码中指出每一句对应的位置。