目录
- 一、概述
- 1.1 背景介绍:从UI到逻辑
- 1.2 学习模式:Qt控制台应用
- 二、C++语法快速入门
- 2.1 变量、数据类型与注释
- 2.2 函数与代码封装
- 2.3 循环与容器:批量处理
- 三、面向对象编程:封装数据与行为
- 四、Qt的核心扩展:信号与槽通信
- 五、总结与展望
一、概述
1.1 背景介绍:从UI到逻辑
在上一篇文章中,我们成功搭建了开发环境,并为“AI螺丝瑕疵检测系统”创建了一个专业、美观的项目骨架。一个软件的UI(用户界面)是它的“面孔”,负责与用户交互;而其C++后端则是它的“大脑”和“骨骼”,负责处理数据、执行复杂逻辑并与硬件通信。
本篇文章的核心任务,就是构建这个强大的“大脑”和“骨骼”。我们将深入C++的世界,但会聚焦于在Qt开发中最常用、最核心的部分。通过本篇的学习,读者将掌握创建后端逻辑、组织代码以及实现对象间通信的基本功,为后续集成真正的视觉算法和硬件交互打下坚实的语言基础。
1.2 学习模式:Qt控制台应用
为了让读者能专注于C++语法本身,暂时不受QML图形界面的干扰,本章的所有示例都将以**Qt控制台应用程序(Qt Console Application)**的形式呈现。这是一种没有图形界面的、纯粹在命令行输出信息的程序,是学习和验证后端逻辑的绝佳工具。
二、C++语法快速入门
本节面向C++初学者,旨在通过可直接运行的、独立的控制台程序,快速介绍后续章节将会频繁使用的基本语法元素。
2.1 变量、数据类型与注释
【例2-1】 产品信息存储与显示。
变量是程序中用于存储数据的基本单元,可以将其看作是内存中一个带标签的盒子。数据类型则定义了这个“盒子”能装什么类型的东西(如整数、小数、字符串等)以及能对它进行哪些操作。在C++中,定义任何变量都必须首先声明其数据类型。注释用于解释代码,不会被编译器执行。
(1) 基本数据类型: int
(整数), double
(双精度浮点数), bool
(布尔值, true或false)。
(2) Qt常用数据类型: QString
(字符串)。在Qt开发中, 推荐使用QString
而非C++标准库的std::string
,因为它提供了更丰富的功能和更好的Unicode支持。
(3) 注释: 单行注释以//
开始,多行注释以/*
开始,以*/
结束。
使用 Qt Creator 创建一个Qt Console Application程序Demo_2_1_Variables
。项目创建完成后,打开main.cpp
,修改代码如下:
#include <QCoreApplication>
#include <QDebug> // 包含Qt的调试输出头文件
#include <QString> // 包含Qt的字符串类头文件int main(int argc, char *argv[])
{QCoreApplication a(argc, argv);// --- 变量定义与初始化 ---int screwCount = 100;double diameter = 5.02;bool isQualified = true;QString batchNumber = "SN20250715-B";// --- 使用qDebug()进行控制台输出 ---qDebug() << "开始进行产品信息检查...";qDebug() << "螺丝数量:" << screwCount << "个";qDebug() << "产品直径:" << diameter << "mm";qDebug() << "质量状态:" << isQualified;qDebug() << "生产批号:" << batchNumber;qDebug() << "检查完毕。";return a.exec();
}
单击 Qt Creator 左下角的绿色“运行”按钮。在下方的“应用程序输出”窗口,可以看到如下结果:
开始进行产品信息检查...
螺丝数量: 100 个
产品直径: 5.02 mm
质量状态: true
生产批号: "SN20250715-B"
检查完毕。
关键代码分析:
(1) #include <QDebug>
: 要使用qDebug()
,必须包含此头文件。
(2) #include <QString>
: QString
是Qt中处理文本的专用类,功能强大,完美支持Unicode(包括中文),是Qt开发的首选字符串类型。
(3) 变量定义: 数据类型 variableName = initialValue;
是C++定义变量的基本格式。变量名通常使用小驼峰命名法(camelCase)。
(4) qDebug() << ...
: 这是qDebug
的链式调用语法。它会自动在每个<<
分隔的变量之间加上空格,并在最后输出一个换行符,非常便捷。
2.2 函数与代码封装
【例2-2】 封装尺寸检查逻辑。
函数是一段被命名、可重复使用的代码块,用于执行一个特定的任务。随着程序逻辑变得复杂,将不同功能的代码组织成独立的函数,可以极大地提高程序的可读性、可维护性和代码的复用性。一个函数通常有输入(参数)和输出(返回值)。
使用 Qt Creator 创建一个Qt Console Application程序Demo_2_2_Functions
。项目创建完成后,打开main.cpp
,修改代码如下:
#include <QCoreApplication>
#include <QDebug>// --- 函数定义 ---
// 这个函数接收一个double类型的参数(parameter),没有返回值(void)
// 它的作用是封装了完整的尺寸检查逻辑
void checkScrewDiameter(double diameterToTest)
{qDebug() << "--- 开始检查直径:" << diameterToTest << "mm ---";const double standardDiameter = 5.0;const double tolerance = 0.05;if (diameterToTest > standardDiameter + tolerance) {qDebug() << "判定结果: 不合格,直径过大。";} else if (diameterToTest < standardDiameter - tolerance) {qDebug() << "判定结果: 不合格,直径过小。";} else {qDebug() << "判定结果: 合格,尺寸在公差范围内。";}qDebug() << "--- 检查结束 ---" << Qt::endl; // Qt::endl显式地换行
}int main(int argc, char *argv[])
{QCoreApplication a(argc, argv);// --- 函数调用 ---// 通过函数名和传递相应的参数来执行函数中的代码checkScrewDiameter(5.08); // 调用函数,检查一个过大的螺丝checkScrewDiameter(4.93); // 调用函数,检查一个过小的螺丝checkScrewDiameter(5.01); // 调用函数,检查一个合格的螺丝return a.exec();
}
单击 Qt Creator 左下角的绿色“运行”按钮,可以看到如下结果:
--- 开始检查直径: 5.08 mm ---
判定结果: 不合格,直径过大。
--- 检查结束 ------ 开始检查直径: 4.93 mm ---
判定结果: 不合格,直径过小。
--- 检查结束 ------ 开始检查直径: 5.01 mm ---
判定结果: 合格,尺寸在公差范围内。
--- 检查结束 ---
关键代码分析:
(1) 函数定义: “返回值类型 函数名(参数类型1 参数名1, ...)
”是定义函数的基本形式。void
是一种特殊的返回类型,表示该函数执行完毕后不返回任何值。
(2) 函数参数: double diameterToTest
是函数的形式参数(形参),它是在函数定义时声明的,作为函数内部的一个局部变量来接收外部传入的数据。
(3) 函数调用: checkScrewDiameter(5.08)
是在main
函数中调用已定义的函数。5.08
是实际参数(实参),在调用发生时,它的值会被复制一份并传递给函数中的形参diameterToTest
。
(4) const
关键字: const double standardDiameter = 5.0;
const
表示standardDiameter
是一个常量,其值在程序运行期间不能被修改。对于程序中固定不变的数值(如标准、公差),使用常量是一种推荐的编程实践,可以防止意外修改并提高代码的可读性。
2.3 循环与容器:批量处理
【例2-3】 批量质检一批产品。
循环是一种控制结构,它允许一段代码根据设定的条件重复执行多次。容器则是一种特殊的对象,专门用于存储和管理一组数据集合。当需要对一个集合中的所有元素执行相同操作时(例如,检查流水线上的一整批产品),将循环与容器结合使用,可以实现高效的自动化处理。
使用 Qt Creator 创建一个Qt Console Application程序Demo_2_3_LoopsAndContainers
。项目创建完成后,打开main.cpp
,修改代码如下:
#include <QCoreApplication>
#include <QDebug>
#include <QVector> // 包含QVector容器的头文件int main(int argc, char *argv[])
{QCoreApplication a(argc, argv);// 创建一个QVector容器,用于存放一批待检测的螺丝直径QVector<double> diameterBatch;diameterBatch.append(5.01); // 使用append()方法向容器末尾添加元素diameterBatch.append(4.98);diameterBatch.append(5.09); // 这个不合格diameterBatch.append(4.92); // 这个也不合格diameterBatch.append(5.04);qDebug() << "流水线来了一批产品,共" << diameterBatch.size() << "个,开始质检...";int qualifiedCount = 0;// 使用for循环遍历QVector中的每一个元素for (int i = 0; i < diameterBatch.size(); ++i) {double currentDiameter = diameterBatch[i]; // 使用索引[i]访问容器中的元素bool isCurrentQualified = (currentDiameter >= 4.95 && currentDiameter <= 5.05);if (isCurrentQualified) {qDebug() << "第" << i + 1 << "个螺丝,直径" << currentDiameter << "mm, 合格。";qualifiedCount++;} else {qDebug() << "第" << i + 1 << "个螺丝,直径" << currentDiameter << "mm, 不合格!";}}qDebug() << "质检完成。合格数量:" << qualifiedCount;return a.exec();
}
单击 Qt Creator 左下角的绿色“运行”按钮,可以看到如下结果:
流水线来了一批产品,共 5 个,开始质检...
第 1 个螺丝,直径 5.01 mm, 合格。
第 2 个螺丝,直径 4.98 mm, 合格。
第 3 个螺丝,直径 5.09 mm, 不合格!
第 4 个螺丝,直径 4.92 mm, 不合格!
第 5 个螺丝,直径 5.04 mm, 合格。
质检完成。合格数量: 3
关键代码分析:
(1) #include <QVector>
: QVector
是Qt提供的一个模板容器类,功能类似于一个可以动态调整大小的数组。要使用它,必须包含此头文件。
(2) QVector<double>
: 定义了一个专门用于存放double
类型数据的QVector
容器。尖括号< >
中的类型指定了容器内所有元素的类型。
(3) append()
: 这是QVector
的成员函数,用于在容器的末尾追加一个新元素。
(4) size()
: 这是QVector
的成员函数,用于获取容器中当前存储的元素总数。
(5) for循环结构: for(int i = 0; i < diameterBatch.size(); ++i)
是最经典的for循环结构。它包含三个部分,由分号隔开:
- 初始化:
int i = 0
,创建一个循环计数器i
并赋初值为0。 - 循环条件:
i < diameterBatch.size()
,每次循环开始前检查此条件,若为真则执行循环体,否则退出循环。 - 迭代表达式:
++i
,每次循环体执行完毕后执行此操作,使计数器加1。
(6) 元素访问:diameterBatch[i]
,通过方括号和索引i
(从0开始)来访问QVector
容器中的具体元素。
(7)&&
运算符:(currentDiameter >= 4.95 && currentDiameter <= 5.05)
中的&&
是逻辑“与”运算符,它表示其两侧的条件必须同时为真,整个表达式的结果才为真。
三、面向对象编程:封装数据与行为
当程序逻辑变得复杂时,使用零散的变量和函数来管理会变得混乱。面向对象编程(Object-Oriented Programming, OOP)提供了一种更高级的组织方式,其核心就是类(Class)。
【核心概念:数据与行为的统一体】
类是创建对象的“蓝图”。它将描述某个事物的数据(称为属性或成员变量)和能对这些数据进行的操作(称为方法或成员函数)封装(Encapsulation)在一起。通过类,可以创建出具体的对象(Object),每个对象都是一个独立、完整的实体,极大地提高了代码的结构化程度。
【例2-4】 创建一个螺丝计数器类。
1. 创建项目与类文件
- 新建一个名为
Demo_2_4_Classes
的Qt控制台项目。 - 在Qt Creator中,右键点击项目名称,选择
添加新文件...
->C++
->C++ Class
,类名输入SimpleCounter
,基类保持默认的无
,完成向导。Qt Creator会自动创建simplecounter.h
和simplecounter.cpp
。
2. 编写代码 (simplecounter.h)
#ifndef SIMPLECOUNTER_H
#define SIMPLECOUNTER_H// 类的声明
class SimpleCounter
{
public:// 构造函数:在创建对象时自动调用,用于初始化SimpleCounter();// 公有(public)成员函数: 外部代码可以直接调用void increment(); // 计数加一void clear(); // 计数清零int getCount() const; // 获取当前计数值private:// 私有(private)成员变量: 外部代码无法直接访问,实现了封装和保护int m_count;
};#endif // SIMPLECOUNTER_H
3. 编写代码 (simplecounter.cpp)
#include "simplecounter.h"// 构造函数的实现
SimpleCounter::SimpleCounter()
{// 在这里进行初始化工作m_count = 0;
}// 成员函数的实现
void SimpleCounter::increment()
{m_count++;
}void SimpleCounter::clear()
{m_count = 0;
}// const关键字表示这个函数不会修改类的任何成员变量
int SimpleCounter::getCount() const
{return m_count;
}
4. 编写代码 (main.cpp)
#include <QCoreApplication>
#include <QDebug>
#include "simplecounter.h" // 包含我们自己定义的类的头文件int main(int argc, char *argv[])
{QCoreApplication a(argc, argv);qDebug() << "创建两个计数器: 合格品计数器和次品计数器";// 从SimpleCounter类创建两个具体的对象SimpleCounter qualifiedCounter;SimpleCounter defectiveCounter;// 模拟生产过程qDebug() << "生产了3个合格品...";qualifiedCounter.increment();qualifiedCounter.increment();qualifiedCounter.increment();qDebug() << "生产了1个次品...";defectiveCounter.increment();// 通过公有方法获取内部的计数值并打印qDebug() << "当前合格品数量:" << qualifiedCounter.getCount();qDebug() << "当前次品数量:" << defectiveCounter.getCount();return a.exec();
}
5. 运行结果
创建两个计数器: 合格品计数器和次品计数器
生产了3个合格品...
生产了1个次品...
当前合格品数量: 3
当前次品数量: 1
关键代码分析:
(1) 头文件(.h)与源文件(.cpp): 在C++中,类的声明(它有什么)通常放在头文件中,而成员函数的具体实现(它怎么做)放在源文件中。这是一种代码组织规范。
(2) public:
与 private:
: 这是访问说明符。public
部分是类的对外接口,任何代码都可以访问。private
部分是类的内部实现,只有该类的成员函数才能访问,外部代码无法直接触及。这体现了封装的思想,保护了内部数据的安全性。
(3) 构造函数: SimpleCounter()
是一个特殊的成员函数,它没有返回类型,且函数名与类名完全相同。在创建类的对象时(如SimpleCounter qualifiedCounter;
),构造函数会被自动调用,是执行初始化操作的最佳位置。
(4) SimpleCounter::
: ::
是作用域解析运算符。在.cpp
文件中,它用于指明一个函数实现是属于哪个类的。
四、Qt的核心扩展:信号与槽通信
【核心概念:Qt的灵魂机制】
在图形界面程序中,一个对象的状态改变(如按钮被点击)常常需要通知其他多个对象。如果使用直接函数调用的方式,对象之间会产生紧密的依赖关系(耦合),不利于程序的维护和扩展。
为此,Qt引入了其最核心、最强大的创新之一:信号与槽(Signals and Slots)。
- 信号(Signal): 当对象内部状态发生改变时,由该对象发射(emit)出去的一种“广播”或“通知”。它只负责发出通知,不关心谁会收到。
- 槽(Slot): 一个普通的成员函数,用于接收并处理某个信号。
通过connect
函数,可以将一个对象的信号与另一个(或同一个)对象的槽连接起来。当信号被发射时,所有与之连接的槽函数就会被自动调用。这种机制使得信号的发送方和接收方可以互不知道对方的存在,实现了完美的解耦。
要让一个类支持信号与槽,必须:
- 公有继承自
QObject
。 - 在类声明的私有区顶部添加
Q_OBJECT
宏。
【例2-5】 工作者完成任务后通知管理者。
1. 创建项目与类文件
- 新建一个名为
Demo_2_5_SignalsAndSlots
的Qt控制台项目。 - 分别添加
Worker
和Manager
两个C++类,注意它们的基类都要选择QObject
。
2. 编写代码 (worker.h & worker.cpp)
// worker.h
#ifndef WORKER_H
#define WORKER_H
#include <QObject>
#include <QString>class Worker : public QObject
{Q_OBJECT // 必须添加此宏以支持信号与槽
public:explicit Worker(QObject *parent = nullptr);void doWork(); // 模拟执行一个任务
signals:// 信号只需要声明,无需实现。在signals关键字下声明void workFinished(const QString &result);
};
#endif // WORKER_H// worker.cpp
#include "worker.h"
#include <QDebug>
#include <QThread>Worker::Worker(QObject *parent) : QObject(parent) {}
void Worker::doWork()
{qDebug() << "[工作者] 开始执行耗时任务...";QThread::sleep(2); // 模拟耗时2秒QString result = "所有螺丝检测完毕";qDebug() << "[工作者] 任务完成,准备发射信号...";emit workFinished(result); // 使用emit关键字发射信号
}
3. 编写代码 (manager.h & manager.cpp)
// manager.h
#ifndef MANAGER_H
#define MANAGER_H
#include <QObject>
#include <QString>class Manager : public QObject
{Q_OBJECT
public:explicit Manager(QObject *parent = nullptr);
public slots:// 槽函数声明在 public slots: 区域void onWorkFinished(const QString &result);
};
#endif // MANAGER_H// manager.cpp
#include "manager.h"
#include <QDebug>Manager::Manager(QObject *parent) : QObject(parent) {}void Manager::onWorkFinished(const QString &result)
{qDebug() << "[管理者] 收到信号,槽函数被调用!";qDebug() << " > 结果:" << result;
}
4. 编写代码 (main.cpp)
#include <QCoreApplication>
#include "worker.h"
#include "manager.h"
#include <QDebug>int main(int argc, char *argv[])
{QCoreApplication a(argc, argv);Worker worker;Manager manager;// --- 信号与槽的连接 ---QObject::connect(&worker, &Worker::workFinished,&manager, &Manager::onWorkFinished);qDebug() << "[主程序] 命令工作者开始工作。";worker.doWork(); // 调用此函数将最终导致信号被发射return a.exec();
}
5. 运行结果
[主程序] 命令工作者开始工作。
[工作者] 开始执行耗时任务...
[工作者] 任务完成,准备发射信号...
[管理者] 收到信号,槽函数被调用!> 结果: "所有螺丝检测完毕"
关键代码分析:
(1) QObject
与Q_OBJECT
: Worker
和Manager
都继承自QObject
,并在类声明中包含了Q_OBJECT
宏,这是使用信号槽的前提。Qt的MOC(元对象编译器)会预处理这个宏,生成支持信号槽所需的额外代码。
(2) signals:
: 用于声明信号函数。信号函数没有函数体,其返回值类型通常是void
。
(3) emit
: 这是一个空宏,在代码中用于标记一个信号的发射。当执行到emit workFinished(...)
时,Qt的元对象系统会接管,并调用所有与workFinished
信号连接的槽。
(4) public slots:
: 用于声明槽函数。槽函数的本质是一个普通的成员函数,可以有自己的参数和实现。
(5) QObject::connect(...)
: 这是建立连接的核心函数。其最常用的形式是:
connect(信号发送者地址, &发送者类::信号名, 信号接收者地址, &接收者类::槽函数名);
这种写法在编译时就会进行类型检查,如果信号和槽的参数不匹配,或者函数名写错,编译器会直接报错,非常安全。
五、总结与展望
在本篇文章中,我们不仅复习了C++的基础语法,更深入探讨了面向对象编程的核心——类与对象,以及Qt框架的灵魂——信号与槽机制。这些是构建任何复杂、可维护的Qt应用程序都不可或缺的知识。
现在,我们已经具备了用C++构建结构化、可交互的后端逻辑的能力。在下一篇文章**【《使用Qt Quick从零构建AI螺丝瑕疵检测系统》——3. QML入门:像搭积木一样构建UI】**中,我们将把目光转向用户界面,开始探索QML的精彩世界,学习如何为我们的C++后端打造一个现代化的“面孔”。