Virtual_Destructor_IN_Cpp

C++ 中虚析构函数的使用

析构函数是为了在对象不被使用之后释放它的资源,虚函数是为了实现多态。那么把析构函数声明为 vitual 有什么作用呢?
直接的讲,C++ 中基类采用virtual虚析构函数是为了防止内存泄漏。具体地说,如果派生类中申请了内存空间,并在其析构函数中对这些内存空间进行释放。假设基类中采用的是非虚析构函数,当删除基类指针指向的派生类对象时就不会触发动态绑定,因而只会调用基类的析构函数,而不会调用派生类的析构函数。那么在这种情况下,派生类中申请的空间就得不到释放从而产生内存泄漏。所以,为了防止这种情况的发生,C++ 中基类的析构函数应采用virtual虚析构函数

参考文献:

https://blog.csdn.net/qq_34673519/article/details/101429799

CPP_11 Enum Class

C++ 11 枚举类 enum class

我们在 C++ 中常使用 enum 来给同一类别中的多个值命名,如:给颜色中的 0, 1, 2, 3, ... 值命名,可以用下面的写法:

1
2
3
4
5
6
7
enum Color {
Red,
Yellow,
Blue,
Gray,
...
};

C++C98 标准称 enum不限范围的枚举型别。因为 C++ 中的枚举量会泄露到包含这个枚举类型的作用域内,在这个作用域内就不能有其他实体取相同的名字。我们可以通过一段代码来演示这一现象:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#include <iostream>

enum Color {
RED,
YELLOW,
BLUE,
GRAY
};

auto GRAY = 10;

int main(void) {
std::cout << GRAY << std::endl;
return 0;
}

当我们编译时,会出现重定义错误:error: 'auto GRAY' redeclared as different kind of entity

为了解决这一问题,C++ 11 新标准提供了 enum Class 枚举类。对于上面的代码,我们再一次做出演示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#include <iostream>

enum class Color {
RED,
YELLOW,
BLUE,
GRAY
};

auto GRAY = 10;

int main(void) {
std::cout << static_cast<int>(Color::GRAY) << std::endl;
std::cout << GRAY << std::endl;
return 0;
}

此时,输出 310。可以看到在全局作用域的 GRAY 被赋值成了 10,而枚举类中的 GRAY 还是 3,且必须使用作用域限定符进行访问。 这里可以看到我使用了一个 static_cast<int> (Color::GRAY) 进行了一个强制类型转换,这是因为 enum 不支持隐式类型转换。如果想要进行转换,则必须使用 static_cast 进行强制类型转换。

CppGamingDEV_PVZ_BASE_EASYX

🎮 从零开始进行 C++ 游戏开发

[TOC]

游戏场景概念

  • 什么是场景?
    如果将要游戏程序比作是一场有玩家参与的盛大演出,那场景就是演出过程中的一幕。在不同的幕中,会有不同的剧本逻辑,也可能会有不同的角色登场,这些角色即游戏开发中常提到的 GameObject 的概念。
    不论是玩家、敌人还是子弹、道具等,这些从概念上讲都是 GameObject 的范畴。他们接受着不同的场景剧本的指挥,进行着不同逻辑的演出。
    了解了这些,我们就可以对程序的流程进行宏观的划分,游戏的主菜单是一个场景,玩家角色选择界面也是一个场景,游戏局内的逻辑也需要放置在一个单独的场景中。
    所以我们就可以定义一个 Scene 场景基类,主菜单角色选择局内游戏 作为新类分别继承 Scene 类。

游戏主循环框架概念

  • 什么是游戏主循环框架?
    游戏程序的主体是一个永不停歇的死循环。在每次循环中,我们读取玩家的操作信息,并根据这些操作,处理玩家数据更新,在最后的绘图阶段将游戏画面根据这些更新后的数据渲染出来。
    1
    2
    3
    4
    5
    6
    7
    初始化();
    while(true) {
    读取操作();
    处理数据();
    绘制画面();
    }
    施放资源;

C++ 头文件

  • 为什么需要头文件卫士?
    如果不使用头文件卫士,可能会导致出现重定义的问题。例如,我们有一个头文件 A.h ,有一个头文件 B.h,头文件 B.h 中使用了 A.h 的内容。当我们在主函数里面使用了 A.hB.h 的时候,使用 #include 时会将头文件中的内容全部复制#include 的部分。这样的话如果不加头文件卫士,就会导致重定义的问题,如下:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    // A.h
    int a;

    // B.h
    #include "A.h"
    int b;

    // main.cpp
    #include "A.h" // int a;
    #include "B.h" // int a;
    // int b;
    // 出现重定义错误

    // 错误信息如下
    In file included from B.h:1,
    from main.cpp:2:
    A.h:1:5: error: redefinition of 'int a'
    1 | int a;
    | ^
    In file included from main.cpp:1:
    A.h:1:5: note: 'int a' previously declared here
    1 | int a;
    | ^
    MSVC 编译器中,头文件卫士如下:
    1
    #pragma once
    在其他的一些编译器中,常写为这样:
    1
    2
    3
    4
    #ifndef __HEADER_H__
    #define __HEADER_H__

    #endif

场景管理器

游戏程序是一个巨大的死循环,也是一个巨大的状态机。不同的游戏场景代表着不同的状态,管理着这些状态的“状态机”,在游戏开发中有一个特殊的名字——场景管理器。

Better QT

[TOC]

🍞 Better QT

🍦 1. Qt 的一些常用技巧

1.1 快捷键

  • 快捷键 Ctrl + Tab 可以切换文件;
  • 快捷键 Alt + ENTER 弹出代码生成提示,可以快速提示错误修改方案,类似于 IDEA 的 Alt + ENTER
  • 快捷键 Alt + 鼠标 同时输入;
  • 快捷键 Ctrl + R 运行程序;
  • 快捷键 Ctrl + M 创建书签(Bookmark),或者直接在某行代码前右键添加书签;
  • 快捷键 Ctrl + ENTER 在当前行下方插入空行;
  • 快捷键 Ctrl + Shift + ENTER 在当前行下方插入空行;
  • 快捷键 Ctrl + I 代码对齐;
  • 快捷键 Ctrl + ; 格式化代码;
  • 快捷键 Shift + Delete 剪切当前行,可以当删除用;
  • 快捷键 Ctrl + Shift + R 局部变量统一修改;
  • 快捷键 Ctrl + Shift + V 复制历史;
  • 用键盘模拟鼠标操作:
    功能键 方向键 备注
    Ctrl Shift Alt 左/右 上/下 Home/End 方向键具有移动光标的作用
    × × × 字符 字符 行首/行尾 -
    × × 单词 滚动条 文件头/尾 -
    × 单词 移动 行首/行尾 Shift具有选中文本的作用
    × - 向上/下复制选中部分 - -
  • 快捷键 F1 查看帮助、文档
  • 快捷键 F2 快速到变量或者函数间切换
  • 快捷键 F4 快速在.cpp文件和.h文件间切换
  • 快捷键 Ctrl + Shift + U 查找所有使用该符号的地方
  • 快捷键 Ctrl + K 打开定位器
  • 快捷键 Ctrl + L 跳转到某一行
  • 快捷键 Ctrl + [Shift] + F 查找/替换当前文件[项目]当前选中的内容
  • 快捷键 [Shift] + F3 查找下[上]一个
  • 快捷键 Ctrl + B 编译工程
  • 快捷键 Ctrl + R 运行工程
  • 快捷键 F5 调试运行
  • 快捷键 Ctrl + Shift + F5 重启调试
  • 快捷键 F9 设置和取消断点
  • 快捷键 F10 单步跳过
  • 快捷键 F11 单步进入

1.2 Creator 片段

片段简单理解一下就是已经写好的一些模式化的代码,用户可以使用内置片段或者根据自己的需要自定义片段。

  1. 自带片段示例
    Qt Quick Part
  2. 自定义片段
    一个用户的自定义片段需要以下几个内容:
    $$片段 = 一级标题 + 二级标题 + 片段文本$$
    需要通过:编辑(Edit)→首选项(Preferences)→文本编辑器(Text Editor)→片段(Snippets)进行设置
    Qt Custom Snippets
    比如我要添加一个自定义片段 note,用来表示文件注释,可以选择 GroupC++,然后选择 Add,添加指定的内容:
    Add Custom Snippets

🍦 2. Qt 代码/文件解释

Qt的源代码和文件解释

2.1 Qt 代码

  • hellocosbrowser.h

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    #ifndef HELLOCOSBROWSER_H
    #define HELLOCOSBROWSER_H

    #include <QWidget>
    #include <QMessageBox>

    QT_BEGIN_NAMESPACE
    namespace Ui { class HelloCOSBrowser; }
    QT_END_NAMESPACE

    class HelloCOSBrowser : public QWidget // QWidget 是所有应用程序窗口的基类
    {
    Q_OBJECT // Qt的宏, 支持 Qt 的特性, 如信号与槽、对象树、元对象等

    public:
    // 这里 HelloCOSBrowser 指定父窗口指针为 nullptr, 则它会作为一个独立的窗口进行展示, 否则则会作为父窗口的一个控件
    // 关于这个父窗口指针, 一个很典型的应用就是 微信
    // 当我们创建新窗口的时候, 如果不指定父窗口, 就会弹出一个独立的新窗口, 即电脑任务栏的图标会多出来一个
    // 如果指定了父窗口, 则不会创建一个独立的窗口, 即电脑任务栏处的图标不会增加
    HelloCOSBrowser(QWidget *parent = nullptr);
    ~HelloCOSBrowser();

    private slots:
    void showDialog();

    private:
    Ui::HelloCOSBrowser *ui;
    };
    #endif // HELLOCOSBROWSER_H

2.2 Qt 工程文件解释

文件列表

文件名称 描述
pro 文件 该文件是 Qt 的项目文件,qmake工具可以根据此文件生成 Makefile
pro.user 文件 该文件包含和用户相关的项目信息(用户不需要关注此文件)
ui 文件 Qt 的设计师界面文件
.cpp 文件 C++ 源文件
.h 文件 C++ 头文件

🍦 3. MOC编译器

MOC(Meta-Object Compiler)编译器
C++ 编译器本身不支持 Qt 的某些机制,Qt 希望对 C++ 代码进行自动扩展,这里就需要用到宏(例如:Q_Object)和继承。
此外为了方便用户使用,希望用户无感知,可以将这一操作直接集成到框架中。

3.1 Qt 编译过程

1
2
3
4
5
预编译 -> 编译 -> 汇编 -> 链接 -> 目标

+-------------------------+

拓展代码 -> MOC编译器 -> 新CPP代码

通过上述方式,实现 Qt 的某些特性。我们可以发现,当我们写完代码进行编译后,会产生一个 debug 文件夹,此时我们进入该文件夹,会看到一些元对象编译器编译的文件,如 moc_xxxx.cppmoc_xxx.h 等文件。

3.2 MOC 的使用方法

  1. MOC 编译工具由 Qt 框架自动调用
  2. 扫描 C++ 头文件,寻找 Q_OBJECT
  3. 生成拓展 C++ 代码,再进行预编译
  4. 程序员在使用时,需要继承 QObject 类或者是 QObject 子类,并且包含 Q_OBJECT 宏。

🍦 4. Qt应用程序开发

4.1 Qt Designer 设计师界面使用

Qt Designer
① Qt 控件编辑模式
② Qt 信号与槽编辑模式
③ Qt 伙伴关系编辑模式
④ Qt Tab 顺序编辑模式:可以设置按下 Tab 键的高亮顺序

4.2 Qt 核心——信号与槽

信号与槽的基本概念
Qt Signals and Slots

  • Qt 中的信号和槽是支持多对多的,即一个信号可以对应多个槽,一个槽可以由多个信号触发。
  • Qt 中的信号无需实现,可以由函数(普通函数或者槽函数)通过 emit 关键字发送信号传递参数。
4.2.1 Qt中如何定义信号
  1. 继承 QObject 类或其派生类,同时包含 Q_OBJECT
  2. 使用关键字 signals 声明函数信号函数,不需要具体实现信号函数
  3. 使用 emit 关键字发送信号
4.2.2 Qt中如何定义槽函数
  1. 必须包含 Q_OBJECT
  2. 使用关键字 [public/protected/private] slots 声明函数
  3. 需要具体实现声明的槽函数
4.2.3 Qt中如何连接信号与槽(三种写法)
  1. SIGNAL/SLOT 宏写法:
    QObject::connect(this, SIGNAL(...), this, SLOT(...));
  2. 函数指针写法:
    QObject::connect(this, &SignalFunction, this, &SlotFunction)
  3. lambda 表达式写法:
    QObject::connect(this, &SignalFunction, this, [=]() { qDebug() << "..."; })

三种写法的比较:

连接信号与槽 函数指针
编译 运行 编译 运行
参数类型 完全相同
隐式转换 向上 ×
向下 ×
不可以隐式转换 × × ×
参数个数 信号=槽
信号>槽
信号<槽 × × ×
  • 这样看来好像宏写法相对于函数指针的写法来说,可能会带来一些问题,因为有时候宏写法通过编译后,在运行阶段可能会出现一些问题;而函数指针写法可以在出现这一问题之前(编译阶段)提前发现这一问题,使得程序无法通过编译。
  • 但是事实上,宏写法还是存在一定的好处,当信号函数出现重载时,使用函数指针时,无法直接进行连接(会产生报错),只能使用类型转换来进行函数指针类型的转换,如static_cast<void QSpinBox::*)(int)>
  • 一般情况下,推荐使用函数指针方式连接信号与槽。
  • 当面对信号与槽函数有重载的情况时,推荐使用宏方式连接。
  • 对于短小的槽函数的调用且功能不被复用时,推荐使用 lambda 方式连接。
4.2.4 其他连接信号与槽的方式
  • 使用 Qt Designer 连接信号与槽
    Qt Designer Connect
  • 使用”转到槽”方式
  • 信号与槽自动绑定
    使用 void on_<对象名>_<信号名>(信号参数); 时可以不使用 connect 进行连接,但是当对象名、信号名或参数发生变化时,连接将会失效,且编译不会有错误提示。

4.3 Qt 窗口

4.3.1 窗口的类型

顶层窗口、次级窗口(父、子窗口)
Qt Windows
在该图中,①可以称为顶层窗口(父窗口),②可以称为次级窗口(子窗口)。窗口中的某些按钮、输入框…等就是控件。

4.3.2 设置窗口标志

在 Qt 中可以使用 setWindowFlags() 来设置窗口标志

  1. 设置窗口无标题栏
    Window With No Title

    1
    this->setWindowFlags(Qt::CustomizeWindowHint);
  2. 设置窗口无边框
    Window With No Frame

    1
    setWindowFlags(Qt::FramelessWindowHint);
  3. 设置窗口置顶

    1
    setWindowFlags(Qt::WindowStaysOnTopHint);

    如果按照上述方式依次设置窗口标志,我们会发现当设置第 $3$ 步时前面两步的操作都失效了,这是因为设置窗口置顶时,会覆盖前面的设置。我们可以使用“或”符连接这些标志,解决这一问题:

    1
    setWindowFlags(Qt::CustomizeWindowHint | Qt::FramelessWindowHint | Qt::WindowStaysOnTopHint);

4.4 窗口坐标系与几何布局

4.4.1 窗口的坐标系
Window Coordinate System
4.4.2 窗口的几何布局
Window Layout

4.5 添加图标

4.5.1 为窗口添加图标
  1. 准备图标文件
  2. 调用 setWindowIcon 方法
4.5.2 为应用程序添加图标(一般使用这种方式)
  1. 准备图标文件 logo.ico
  2. 修改 pro 工程文件 RC_ICONS = <Path>
  3. 通过此种方式修改图标,可执行程序 .exe 的图标会修改,且不需要额外单独设置窗口图标。

4.6 部署产品的三种方式

  1. 手动部署(不常用,比较繁琐)
    进入 .exe 文件所在的文件夹(debug目录),双击运行 .exe 文件,会提示缺少的文件(包括dll动态库、plugin插件等),然后找到对应的文件移动到 .exe 文件的同级目录下即可,如下:
    EXE File Dictionary
    如果配置了环境变量则大概率不会出现报错提示缺少库的问题,那么这种方式就会失效。

  2. 使用 windeployqt 部署
    ① 查找 windeployqt.exe 程序
    ② 将 windeployqt.exe 加入环境变量
    ③ 再命令行界面执行命令 windeployqt.exe <exe_file_dir> 完成操作
    WIN DEPLOY QT
    WIN DEPLOY QT
    WIN DEPLOY QT

  3. 使用creator 部署
    ① 项目导航窗口→运行→部署→添加自定义部署
    CREATOR DEPLOY
    CREATOR DEPLOY
    CREATOR DEPLOY
    ② 输入 windeployqt.exe 程序及对应的命令行参数
    CREATOR DEPLOY
    CREATOR DEPLOY
    ③ 执行部署命令
    CREATOR DEPLOY
    CREATOR DEPLOY OK

🍦 5. Qt 常用控件

官方文档:Qt 6.7

一个不错的 Qt 中文文档:Qt 中文文档 5.15.1 版本

5.1 QLabel 标签控件

QLabel 的本质其实就是显示数据。其可以显示文本数据、图片数据。

了解了这些我们再来看 QLabel 的一些常用属性:

属性 说明
text: QString 文本内容:纯文本
openExternalLinks:bool 文本内容:超链接
textFormat : Qt::TextFormat 文本内容:不同类型的文本,如富文本等
alignment: Qt::Alignment 文本格式:对齐方式
indent: int 文本格式:缩进
margin : int 文本格式:边距
wordWrap: bool 文本格式:换行
pixmap: QPixmap 图片内容:显示图片
hasSelectedText: const bool 方法:文本是否被选中
scaledContents: bool 方法:是否缩放内容
selectedText: const QString 方法:获取选中的内容
textInteractionFlags: Qt::TextInteractionFlags 方法:指定标签应如何与用户输入交互,若它显示文本

5.2 QLineEdit 单行输入框控件

QLineEdit 的本质是用于不确定的输入,如用户的手机号、用户的密码。这样就给了用户一定的自由,但是我们同时需要制定一系列的规则,以校验用户的输入。例如用户输入手机号,我们需要制定一个长度为 11 位的规则,并且输入字符中不包含字母等,以方便开发人员进行校验。当然我们也可以配合一些其他操作来优化用户体验,如:清空(如:快速清空内容)、提示(如:提示输入格式)、记忆(如:记忆之前的输入)等。

了解了这些我们可以看一下 QLineEdit 的一些常用属性(不完全,具体还是需要看文档):

属性 说明
clearButtonEnabled: bool 清空文本框内容
该属性保存行编辑不为空时是否显示清除按钮。
placeholderText: QString 占位符文本,可以用于提示输入内容。
inputMask: QString 掩码