0%

QApplication 类 详解

QApplication 类是 Qt 应用程序的核心类之一,用于管理应用程序的控制流和主要设置。它负责处理应用程序的初始化、事件循环、窗口管理、以及应用全局的设置。通常,一个 GUI 应用程序中只能有一个 QApplication 实例。

1. 类定义

1
class QApplication : public QGuiApplication

QApplication 继承自 QGuiApplication,并且通过其进一步继承了 QCoreApplicationQApplication 是 GUI 程序的基础,而 QGuiApplication 适用于不需要窗口但依然有 GUI 功能的程序(如 OpenGL 渲染等)。QCoreApplication 则用于没有 GUI 的控制台程序。

2. 创建 QApplication 对象

在大多数情况下,QApplication 是程序的第一个创建的对象,并且程序的主要控制权交给了它:

1
2
3
4
5
6
7
8
9
10
11
#include <QApplication>
#include <QPushButton>

int main(int argc, char *argv[]) {
QApplication app(argc, argv);

QPushButton button("Hello, Qt!");
button.show();

return app.exec();
}

在这个例子中:

  • QApplication app(argc, argv); 初始化了应用程序对象。
  • app.exec(); 进入应用程序的事件循环,处理用户输入和其他事件。

3. 主要功能和成员函数

QApplication 提供了许多全局设置和管理功能:

1. 事件循环

  • int exec(): 启动事件循环。应用程序在进入这个循环后开始运行,直到调用 quit() 或窗口关闭。
  • void exit(int returnCode = 0): 退出事件循环。

2. 全局设置

  • void setStyle(const QString &style): 设置应用程序的 GUI 样式,如 “Fusion”、”Windows”、”Macintosh” 等。
  • QStyle *style(): 返回当前使用的样式。

3. 应用程序信息

  • void setApplicationName(const QString &name): 设置应用程序的名称。
  • QString applicationName(): 获取应用程序的名称。
  • void setApplicationVersion(const QString &version): 设置应用程序版本。
  • QString applicationVersion(): 获取应用程序版本。

4. 图标和主题

  • void setWindowIcon(const QIcon &icon): 设置应用程序的全局图标,这个图标会出现在应用窗口的标题栏、任务栏以及系统托盘中。
  • QIcon windowIcon(): 获取应用程序的图标。

5. 剪贴板

  • QClipboard *clipboard(): 返回系统的剪贴板对象,可以用来复制和粘贴文本、图片等数据。

6. 应用程序事件处理

  • bool notify(QObject *receiver, QEvent *event): 事件通知处理函数,通常不需要重写,但可以在特殊情况下进行自定义事件处理。
  • void installEventFilter(QObject *filterObj): 安装事件过滤器,用于拦截和处理特定事件。

7. 颜色与字体

  • void setPalette(const QPalette &palette): 设置全局调色板,影响整个应用程序的颜色风格。
  • QFont font(): 获取当前应用程序的全局字体。
  • void setFont(const QFont &font): 设置应用程序的全局字体。

4. 注意事项

  • QApplication 是一个 GUI 应用的核心,因此所有 GUI 应用必须创建 QApplication 对象。
  • 一个应用程序中只能有一个 QApplication 实例。如果尝试创建多个,会导致程序异常。
  • QApplication 必须在创建任何其他 Qt 对象之前创建。

5. 常见的使用场景

  • 应用程序启动和事件管理: QApplication 的主要职责是启动并维持事件循环,这是 GUI 程序处理用户输入、界面更新的基础。
  • 全局设置: QApplication 允许你为整个应用程序设置默认的字体、颜色、样式等。
  • 跨平台支持: Qt 中的 QApplication 处理了许多跨平台细节,使得开发者可以在不同平台上使用一致的 API。

6. 示例代码

以下是一个更完整的示例,展示了如何使用 QApplication 设置全局样式和图标:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
#include <QApplication>
#include <QPushButton>
#include <QIcon>

int main(int argc, char *argv[]) {
QApplication app(argc, argv);

// 设置应用程序信息
app.setApplicationName("My Qt Application");
app.setApplicationVersion("1.0.0");

// 设置应用程序图标
app.setWindowIcon(QIcon(":/resources/myicon.png"));

// 创建一个按钮并显示
QPushButton button("Click Me");
button.setFont(QFont("Arial", 18)); // 设置字体
button.show();

return app.exec();
}

7. 继承与扩展

在一些复杂的应用中,可以通过继承 QApplication 来扩展其功能。例如,可以重写 notify() 函数来捕获所有事件:

1
2
3
4
5
6
7
8
9
class MyApplication : public QApplication {
public:
MyApplication(int &argc, char **argv) : QApplication(argc, argv) {}

bool notify(QObject *receiver, QEvent *event) override {
// 在此进行自定义事件处理
return QApplication::notify(receiver, event);
}
};

8. 总结

QApplication 是 Qt GUI 应用程序的基础,它负责管理事件循环、全局设置、样式、系统剪贴板等。理解并熟练使用 QApplication 是开发 Qt GUI 应用程序的重要步骤。

Qt QByteArray 类 详解

QByteArray 是 Qt 框架中用于处理字节数据的类。它类似于 C++ 标准库中的 std::string,但专门设计用来处理原始字节数据。QByteArray 提供了许多功能,包括存储和操作字节数据、支持多种编码、以及对比和查找等操作。以下是对 QByteArray 类的详细解解:

1. 基本功能

  • 定义和初始化
    QByteArray 可以用多种方式初始化,包括从 C 风格字符串、Qt 字符串(QString)、或者通过构造函数。

    1
    2
    3
    QByteArray byteArray1; // 空的字节数组
    QByteArray byteArray2("Hello, world!"); // 从 C 风格字符串初始化
    QByteArray byteArray3(QString("Hello, world!").toUtf8()); // 从 QString 初始化
  • 存储字节数据
    QByteArray 用于存储字节数据,可以方便地进行操作。

    1
    QByteArray byteArray("Data");

2. 基本操作

  • 追加和插入
    可以使用 append()insert() 方法将数据追加到字节数组的末尾或在指定位置插入。

    1
    2
    byteArray.append(" more data");
    byteArray.insert(4, " insert");
  • 移除和替换
    remove() 方法用于删除字节数据,而 replace() 用于替换指定范围的字节数据。

    1
    2
    byteArray.remove(4, 6); // 从位置4开始,删除6个字节
    byteArray.replace("insert", "replace");
  • 清空和检查
    clear() 方法可以清空字节数组,isEmpty() 方法用于检查字节数组是否为空。

    1
    2
    byteArray.clear();
    bool isEmpty = byteArray.isEmpty();

3. 编码和解码

  • 与 QString 互转
    QByteArray 可以方便地与 QString 转换,使用 toStdString() 可以转换为 std::string

    1
    2
    3
    QString str = "Hello, world!";
    QByteArray byteArray = str.toUtf8();
    QString backToStr = QString::fromUtf8(byteArray);
  • 编码和解码
    QByteArray 支持多种编码格式,如 UTF-8、Latin1 等。

    1
    2
    QByteArray utf8Array = "UTF-8 data";
    QByteArray latin1Array = utf8Array.toLatin1();

4. 查找和比较

  • 查找子串
    使用 indexOf() 方法查找子字节串的位置,contains() 方法检查是否包含某个子字节串。

    1
    2
    int pos = byteArray.indexOf("data");
    bool contains = byteArray.contains("data");
  • 比较字节数组
    使用 compare() 方法比较两个字节数组。

    1
    2
    3
    QByteArray byteArray1("abc");
    QByteArray byteArray2("abc");
    int result = QByteArray::compare(byteArray1, byteArray2); // 0 表示相等

5. 转换和操作

  • **转换为 std::string**:
    可以通过 toStdString() 方法将 QByteArray 转换为标准 C++ 字符串。

    1
    std::string stdString = byteArray.toStdString();
  • 操作字节数据
    QByteArray 提供了类似于 C++ 数组的操作,例如直接访问字节。

    1
    char firstByte = byteArray[0];

6. 文件操作

  • 读写文件
    QByteArray 可用于处理文件内容,可以与 QFile 类一起使用来读取和写入字节数据。

    1
    2
    3
    4
    5
    6
    QFile file("example.dat");
    if (file.open(QIODevice::ReadWrite)) {
    QByteArray data = file.readAll();
    file.write("New data");
    file.close();
    }

示例代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
#include <QByteArray>
#include <QString>
#include <QDebug>

int main() {
QByteArray byteArray("Hello, world!");

// Append data
byteArray.append(" Welcome to Qt.");

// Replace part of the string
byteArray.replace("world", "Qt");

// Convert to QString
QString str = QString::fromUtf8(byteArray);

// Print the result
qDebug() << str; // Output: Hello, Qt! Welcome to Qt.

return 0;
}

QByteArray 是处理字节数据时非常有用的类,特别是在涉及到编码转换、数据传输和文件操作时。它提供了灵活且高效的字节数据处理功能。

Qt QMutex 类 详解

QMutex 是 Qt 框架中的一个类,用于实现线程同步。它提供了一种机制来控制对共享资源的访问,以避免多个线程同时访问同一资源而导致的竞态条件。QMutex 类是 Qt 的核心线程库的一部分,用于确保在多线程环境中的数据一致性和避免数据冲突。以下是对 QMutex 类的详细解释:

1. 基本概念

  • 定义
    QMutex 是一个互斥锁,用于在多线程程序中保护共享数据。它确保在任何时刻只有一个线程可以访问被保护的资源。

  • 使用场景

    • 保护共享数据结构(如变量、对象)不被多个线程同时修改。
    • 在并发环境中避免数据不一致性和竞态条件。

2. 基本操作

  • 构造和析构
    QMutex 的构造函数创建一个互斥锁实例,析构函数释放该互斥锁。

    1
    QMutex mutex; // 默认构造函数
  • 加锁和解锁

    • 加锁lock() 方法用于加锁,如果互斥锁已被其他线程占用,则调用线程将会被阻塞,直到互斥锁变为可用。
    • 解锁unlock() 方法用于释放互斥锁,使其他线程可以访问受保护的资源。
    1
    2
    3
    mutex.lock(); // 加锁
    // 访问共享资源
    mutex.unlock(); // 解锁
  • 自动锁
    使用 QMutexLocker 类可以自动管理互斥锁的加锁和解锁,避免因异常或遗漏导致的死锁问题。

    1
    2
    3
    QMutexLocker locker(&mutex);
    // 访问共享资源
    // locker 的析构函数会自动解锁

3. 互斥锁的类型

  • 递归互斥锁
    QMutex 可以是递归的,允许同一个线程多次锁定同一个互斥锁而不会导致死锁。使用 QMutex::Recursive 类型构造递归互斥锁。

    1
    QMutex recursiveMutex(QMutex::Recursive);
  • 非递归互斥锁
    默认构造的 QMutex 是非递归的,要求在一个线程中加锁后,必须在同一线程中解锁,否则会导致死锁。

    1
    QMutex nonRecursiveMutex; // 默认为非递归

4. 静态方法

  • **QMutex::tryLock()**:
    尝试加锁而不阻塞,如果互斥锁已被其他线程占用,它会立即返回 false

    1
    2
    3
    4
    5
    6
    if (mutex.tryLock()) {
    // 成功加锁
    mutex.unlock();
    } else {
    // 未能加锁
    }
  • **QMutex::lock()QMutex::unlock()**:
    这些方法分别用于加锁和解锁,不同于 tryLock(),它们会阻塞直到成功加锁。

5. 性能考虑

  • 锁的开销
    使用互斥锁会有一定的性能开销,尤其是在高频率锁定和解锁的情况下。合理设计锁的粒度和使用自动锁可以减少这种开销。

  • 避免死锁
    使用 QMutexLocker 可以帮助避免由于忘记解锁或异常导致的死锁问题。

示例代码

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
#include <QMutex>
#include <QMutexLocker>
#include <QThread>
#include <QDebug>

QMutex mutex;

void threadFunction() {
QMutexLocker locker(&mutex); // 自动加锁
qDebug() << "Thread is running";
// 访问共享资源
// locker 的析构函数会自动解锁
}

int main() {
QThread thread1(threadFunction);
QThread thread2(threadFunction);

thread1.start();
thread2.start();

thread1.wait();
thread2.wait();

return 0;
}

总结

QMutex 类在 Qt 的多线程编程中扮演了重要角色,通过提供互斥锁机制来保护共享资源的访问,确保线程安全。合理使用 QMutexQMutexLocker 可以帮助你管理多线程环境中的资源,避免竞态条件和数据不一致性问题。

QMainWindow 类 详解

QMainWindow 是 Qt 框架中用于创建主窗口的类。它提供了一个标准的窗口框架,通常用于构建图形用户界面(GUI)应用程序的主窗口。QMainWindow 提供了功能齐全的窗口部件,如菜单栏、工具栏、状态栏等,以及一个中央区域用于显示主要内容。

1. 类定义

1
class QMainWindow : public QWidget

QMainWindow 继承自 QWidget,因此它也是一个 QWidget,具有所有 QWidget 的特性。

2. 主要功能

QMainWindow 提供了一些标准的窗口元素和布局管理功能:

  • 菜单栏 (QMenuBar): 用于显示应用程序的菜单项。
  • 工具栏 (QToolBar): 用于提供快捷工具按钮。
  • 状态栏 (QStatusBar): 用于显示状态信息。
  • 中央区域 (QWidget): 用于显示主内容区域,可以是任何其他的 QWidget。

3. 关键成员函数

1. 设置和获取中央窗口部件

  • void setCentralWidget(QWidget *widget): 设置主窗口的中央部件。
  • QWidget *centralWidget() const: 获取当前的中央部件。

2. 菜单栏管理

  • QMenuBar *menuBar() const: 获取菜单栏对象。如果需要在窗口中添加菜单项,可以使用此函数。

3. 工具栏管理

  • QToolBar *addToolBar(const QString &title): 添加工具栏到主窗口。
  • QToolBar *toolBar(const QString &title) const: 根据标题获取工具栏对象。

4. 状态栏管理

  • QStatusBar *statusBar() const: 获取状态栏对象,用于在窗口底部显示状态信息。
  • void setStatusBar(QStatusBar *statusBar): 设置自定义状态栏。

5. 菜单和工具栏操作

  • void removeToolBar(QToolBar *toolbar): 从主窗口中移除工具栏。
  • void removeToolBarBreak(QToolBar *toolbar): 移除工具栏之间的分隔符。

4. 示例代码

以下是一个简单的使用 QMainWindow 的示例,展示了如何创建一个带有菜单栏、工具栏和状态栏的主窗口:

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
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
#include <QApplication>
#include <QMainWindow>
#include <QMenuBar>
#include <QToolBar>
#include <QStatusBar>
#include <QPushButton>
#include <QWidget>

int main(int argc, char *argv[]) {
QApplication app(argc, argv);

QMainWindow mainWindow;
mainWindow.setWindowTitle("QMainWindow Example");

// 创建并设置中央部件
QWidget *centralWidget = new QWidget;
QPushButton *button = new QPushButton("Click Me");
QVBoxLayout *layout = new QVBoxLayout;
layout->addWidget(button);
centralWidget->setLayout(layout);
mainWindow.setCentralWidget(centralWidget);

// 创建菜单栏
QMenuBar *menuBar = mainWindow.menuBar();
QMenu *fileMenu = menuBar->addMenu("File");
fileMenu->addAction("Open");
fileMenu->addAction("Save");
fileMenu->addSeparator();
fileMenu->addAction("Exit");

// 创建工具栏
QToolBar *toolBar = mainWindow.addToolBar("Main Toolbar");
toolBar->addAction("Open");
toolBar->addAction("Save");

// 创建状态栏
QStatusBar *statusBar = new QStatusBar;
mainWindow.setStatusBar(statusBar);
statusBar->showMessage("Ready");

// 显示主窗口
mainWindow.resize(800, 600);
mainWindow.show();

return app.exec();
}

5. 继承和扩展

QMainWindow 可以被继承和扩展,以添加自定义功能或修改现有行为。例如,你可以创建一个自定义的主窗口类,添加额外的工具栏或菜单项:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
#include <QMainWindow>
#include <QAction>
#include <QMenuBar>
#include <QToolBar>
#include <QStatusBar>

class MyMainWindow : public QMainWindow {
Q_OBJECT
public:
MyMainWindow() {
// 自定义菜单
QMenu *fileMenu = menuBar()->addMenu("File");
QAction *exitAction = fileMenu->addAction("Exit");
connect(exitAction, &QAction::triggered, this, &QMainWindow::close);

// 自定义工具栏
QToolBar *toolbar = addToolBar("My Toolbar");
toolbar->addAction("My Action");

// 自定义状态栏
statusBar()->showMessage("Welcome to My Application");
}
};

6. 注意事项

  • 中央部件QMainWindow 只允许设置一个中央部件(centralWidget)。如果你需要在中央区域放置多个部件,可以使用布局管理器将它们组织起来。
  • 工具栏和菜单:工具栏和菜单可以动态添加或移除,但要确保在主窗口显示之前设置好它们。
  • 状态栏:状态栏通常用于显示应用程序的状态信息,可以显示多条信息或者使用复杂的状态显示组件。

7. 总结

QMainWindow 是一个功能丰富的主窗口类,提供了标准的窗口元素,如菜单栏、工具栏和状态栏。它是构建 Qt GUI 应用程序的基础组件之一,并提供了丰富的接口来管理和定制窗口的各个部分。通过继承和扩展 QMainWindow,你可以创建符合需求的复杂主窗口。

Qt Ui::MainWindow 类 详解

在 Qt 应用程序中,Ui::MainWindow 类通常是由 Qt Designer 生成的用于管理用户界面元素的类。它是 Qt UI 系统的重要组成部分,主要用于连接 UI 界面设计与应用程序逻辑。通常在使用 Qt Designer 设计主窗口时,生成的 .ui 文件会通过 uic 工具转换为 C++ 代码,其中包含一个 Ui::MainWindow 类。

1. Ui::MainWindow 类的作用

Ui::MainWindow 是自动生成的类,它负责管理和初始化设计时创建的 UI 元素。这个类是由 Qt Designer 创建的 .ui 文件转换而来的,通常位于生成的 ui_mainwindow.h 文件中。它主要负责以下内容:

  • 创建和布局窗口控件。
  • 初始化控件的属性和默认状态。
  • 提供接口用于在代码中访问这些控件。

2. Ui::MainWindow 的使用方式

当你使用 Qt Designer 创建一个主窗口并将其保存为 mainwindow.ui 时,Qt 会自动生成一个 ui_mainwindow.h 文件,其中包含 Ui::MainWindow 类的定义。这个类可以通过在自定义的 MainWindow 类中包含 ui_mainwindow.h 文件来使用。

示例代码:

假设我们在 Qt Designer 中创建了一个主窗口,并保存为 mainwindow.ui。生成的 ui_mainwindow.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
// ui_mainwindow.h(自动生成的文件)
namespace Ui {
class MainWindow {
public:
QWidget *centralWidget;
QMenuBar *menuBar;
QStatusBar *statusBar;
QPushButton *myButton;

void setupUi(QMainWindow *MainWindow) {
if (MainWindow->objectName().isEmpty())
MainWindow->setObjectName(QString::fromUtf8("MainWindow"));
MainWindow->resize(400, 300);
centralWidget = new QWidget(MainWindow);
menuBar = new QMenuBar(MainWindow);
statusBar = new QStatusBar(MainWindow);
myButton = new QPushButton(centralWidget);
myButton->setText("Click Me");

MainWindow->setCentralWidget(centralWidget);
MainWindow->setMenuBar(menuBar);
MainWindow->setStatusBar(statusBar);
}
};
}

在自定义主窗口类中的使用:

通常,我们会在自定义的主窗口类中使用 Ui::MainWindow 以便将设计的 UI 和应用逻辑结合在一起:

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
#include <QMainWindow>
#include "ui_mainwindow.h" // 包含自动生成的 UI 类

class MainWindow : public QMainWindow {
Q_OBJECT

public:
explicit MainWindow(QWidget *parent = nullptr)
: QMainWindow(parent), ui(new Ui::MainWindow) {
ui->setupUi(this); // 初始化 UI
connect(ui->myButton, &QPushButton::clicked, this, &MainWindow::onButtonClicked);
}

~MainWindow() {
delete ui;
}

private slots:
void onButtonClicked() {
// 处理按钮点击事件
}

private:
Ui::MainWindow *ui;
};

3. setupUi() 方法

setupUi() 方法是 Ui::MainWindow 类中的核心方法。它负责初始化窗口控件并设置其布局、属性、信号槽连接等。通常在主窗口类的构造函数中调用该方法。这个方法需要一个 QMainWindow 对象作为参数,以便将控件添加到主窗口中。

4. 访问 UI 元素

通过 Ui::MainWindow 类中的指针,可以轻松访问和操作设计时创建的控件。例如,使用 ui->myButton 可以访问按钮并设置它的文本或连接信号槽。

1
2
ui->myButton->setText("New Text");
connect(ui->myButton, &QPushButton::clicked, this, &MainWindow::onButtonClicked);

5. Ui::MainWindowQMainWindow 的关系

Ui::MainWindow 是一个辅助类,实际的窗口功能仍然由继承自 QMainWindow 的自定义类实现。QMainWindow 提供了主窗口的基本框架和功能(如菜单栏、状态栏、工具栏),而 Ui::MainWindow 主要负责管理控件和布局。

6. 自定义控件与扩展 UI

如果你需要在设计时添加自定义控件或进行更多的 UI 扩展,可以在 Qt Designer 中通过提升控件的方式实现。此外,你也可以手动在 MainWindow 类的构造函数中添加新的控件,并将其与现有的 UI 进行整合。

7. 典型的工作流程

  • 使用 Qt Designer 设计主窗口,并保存为 .ui 文件。
  • 通过 Qt 的构建工具(如 qmake 或 CMake)自动生成对应的 ui_mainwindow.h 文件。
  • 在自定义的主窗口类中包含生成的 ui_mainwindow.h 文件,并使用 Ui::MainWindow 类来管理 UI 元素。
  • 在应用程序逻辑中,使用 ui-> 前缀来访问控件,并编写对应的事件处理代码。

总结

Ui::MainWindow 是一个自动生成的类,它负责将 Qt Designer 设计的 UI 界面与代码逻辑连接起来。通过调用 setupUi(),开发者可以方便地初始化和使用设计时创建的控件,从而简化了 UI 编程的流程。

QThread 类 详解

QThread 是 Qt 中用于实现多线程的类。它提供了一个平台无关的、面向对象的线程接口,使得在 GUI 应用程序中处理耗时操作时可以保持界面的响应性。在 Qt 中,QThread 是线程管理的基础类,但 Qt 推荐的使用方式与传统的 C++ 线程管理(如 std::thread)有所不同。

1. QThread 的基本概念

  • 线程与事件循环QThread 继承自 QObject,因此它具有信号和槽机制,并且可以在子线程中运行一个事件循环。事件循环允许子线程接收信号并执行槽函数,这在 GUI 编程中非常有用。

  • 工作者线程模型:在 Qt 中,推荐的多线程编程方式是将一个对象的工作移到另一个线程中,而不是直接继承 QThread。这种方法更符合 Qt 的对象模型,也更易于管理信号与槽的连接。

2. QThread 的使用方式

QThread 可以通过多种方式使用,主要包括以下两种:

  1. 直接继承 QThread 类(传统方式)
  2. 工作者线程模型(推荐方式)

2.1 直接继承 QThread

在这种方式中,你需要继承 QThread 并重写其 run() 方法,run() 方法是线程开始执行的入口点。你可以在这里编写需要在线程中运行的任务。

示例代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
#include <QThread>
#include <QDebug>

class MyThread : public QThread {
Q_OBJECT

protected:
void run() override {
for (int i = 0; i < 5; ++i) {
qDebug() << "Running in thread:" << QThread::currentThread();
QThread::sleep(1); // 模拟耗时操作
}
}
};

int main() {
MyThread thread;
thread.start(); // 开始线程
thread.wait(); // 等待线程结束

return 0;
}

缺点:这种方法虽然直观,但不推荐使用,因为它与 Qt 的信号槽机制不太兼容,容易导致线程中的对象生命周期管理问题。

2.2 工作者线程模型(推荐方式)

工作者线程模型是将一个 QObject 派生类(工作对象)移动到一个新线程中,然后在新线程中执行它的任务。这种方式更安全且与 Qt 的事件系统无缝集成。

步骤如下:

  1. 创建一个工作对象,继承自 QObject,并定义需要在线程中运行的任务。
  2. 创建一个 QThread 对象。
  3. 使用 moveToThread() 将工作对象移动到新线程。
  4. 通过信号槽机制启动任务。

示例代码:

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
30
31
32
33
34
#include <QCoreApplication>
#include <QThread>
#include <QDebug>

class Worker : public QObject {
Q_OBJECT

public slots:
void doWork() {
for (int i = 0; i < 5; ++i) {
qDebug() << "Working in thread:" << QThread::currentThread();
QThread::sleep(1); // 模拟耗时操作
}
}
};

int main(int argc, char *argv[]) {
QCoreApplication a(argc, argv);

Worker worker;
QThread thread;

// 将工作对象移动到子线程
worker.moveToThread(&thread);

// 在子线程中启动工作
QObject::connect(&thread, &QThread::started, &worker, &Worker::doWork);
QObject::connect(&thread, &QThread::finished, &worker, &QObject::deleteLater);
QObject::connect(&thread, &QThread::finished, &thread, &QObject::deleteLater);

thread.start(); // 启动线程

return a.exec();
}

优点:这种方式使得工作对象可以与信号槽机制结合得更好,QThread 仅负责线程管理,实际的任务执行由工作对象处理。

3. 信号与槽的线程安全性

  • 在 Qt 中,不同线程中的信号与槽可以跨线程连接。当信号和槽位于不同线程时,Qt 会自动将信号的发送和槽的调用封装为异步事件,通过事件循环来处理。这意味着跨线程的信号槽连接是线程安全的。

  • 线程间的信号槽连接默认是异步的(Qt::QueuedConnection),即信号被发送时不会立即调用槽函数,而是将其加入事件队列,等待事件循环调度。你也可以显式指定连接类型,如 Qt::DirectConnection,来使得信号和槽在同一线程中同步执行。

4. 线程中的事件循环

QThread 中的事件循环使得线程可以处理信号、定时器等事件。默认情况下,QThread::run() 方法启动的线程没有事件循环,必须手动调用 exec() 以启动事件循环。

示例:

1
2
3
4
5
6
7
8
class MyThread : public QThread {
Q_OBJECT

protected:
void run() override {
exec(); // 启动事件循环
}
};

5. 线程生命周期管理

  • 启动线程:使用 start() 启动线程。
  • 停止线程:调用 quit() 退出事件循环,然后使用 wait() 等待线程结束。
  • 线程结束后自动清理:可以通过连接 QThread::finished 信号到 QObject::deleteLater 来自动清理线程对象。

6. 常见问题与注意事项

  • UI 操作必须在主线程:Qt 的 GUI 元素必须在主线程中操作,如果在子线程中直接访问 GUI 会导致崩溃。
  • 对象的生命周期管理:在使用 moveToThread() 时,要确保对象在其所属线程中被创建和销毁,以避免线程间的对象访问问题。
  • 避免阻塞主线程:长时间的计算或 IO 操作应放到子线程中,以保持主线程(UI 线程)的响应性。

7. 常见用例

  • 在后台执行耗时任务,如文件读写、网络请求、数据处理等。
  • 使用定时器在子线程中定期执行任务。
  • 在子线程中处理异步操作并通过信号槽通知主线程结果。

总结

QThread 是 Qt 中实现多线程编程的重要工具。尽管可以通过继承 QThread 来实现自定义线程,但推荐的方式是使用工作者线程模型,将任务放到一个独立的 QObject 中并移动到线程执行。这种方式更符合 Qt 的设计理念,并且更易于管理复杂的信号槽连接与对象生命周期。

Qt QTimer 类 详解

QTimer 是 Qt 框架中的一个用于定时和计时的类。它提供了一种非常方便的方式来设置定时器,并在定时器超时时执行指定的操作。QTimer 在 Qt 的事件驱动模型中非常重要,尤其适合在需要周期性或延迟执行操作的场景中使用,例如动画、定时任务、用户界面刷新等。

1. QTimer 的基本功能

  • 定时器类型

    • 单次定时器:定时器触发一次后就自动停止。
    • 循环定时器:定时器以固定的时间间隔循环触发,直到手动停止。
  • 信号与槽机制
    QTimer 依赖于信号和槽机制,定时器超时时会发出 timeout() 信号,应用程序可以连接到这个信号并执行特定的槽函数。

2. QTimer 的常用方法

  • **start(int msec)**:启动定时器,参数 msec 是以毫秒为单位的间隔时间。
  • **stop()**:停止定时器。如果定时器正在运行,它会被停止,且不会再触发。
  • **setInterval(int msec)**:设置定时器的间隔时间(单位:毫秒)。
  • **setSingleShot(bool singleShot)**:设置定时器是否为单次触发。如果设置为 true,定时器在超时后会自动停止。
  • **isActive()**:检查定时器是否正在运行。
  • **remainingTime()**:返回定时器剩余的时间(单位:毫秒)。如果定时器已超时或停止,则返回 -1。

3. QTimer 的使用方式

QTimer 可以有两种常见的使用方式:

  1. 直接使用 QTimer 静态方法
  2. 创建 QTimer 对象,并将其与槽函数连接

3.1 直接使用静态方法

  • **QTimer::singleShot(int msec, const QObject *receiver, const char *member)**:
    这是一个静态方法,适合用于只需要延迟执行一次的操作。它会在指定时间后发出信号并调用连接的槽函数。

    1
    QTimer::singleShot(2000, this, SLOT(doSomething())); // 2秒后调用槽函数 doSomething()

3.2 创建 QTimer 对象

你可以创建一个 QTimer 对象并手动控制它的启动、停止和触发。

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
#include <QTimer>
#include <QDebug>

class MyObject : public QObject {
Q_OBJECT

public:
MyObject() {
// 创建定时器
timer = new QTimer(this);

// 连接定时器的超时信号到槽函数
connect(timer, &QTimer::timeout, this, &MyObject::onTimeout);

// 设置定时器为循环模式,每隔1秒触发一次
timer->start(1000); // 1000毫秒 = 1秒
}

private slots:
void onTimeout() {
qDebug() << "Timer triggered!";
}

private:
QTimer *timer;
};

4. 单次定时器与循环定时器

  • 单次定时器:在 QTimer 中可以通过设置 setSingleShot(true) 或使用 QTimer::singleShot() 来实现单次定时器。

    1
    2
    3
    QTimer *timer = new QTimer(this);
    timer->setSingleShot(true); // 设置为单次定时器
    timer->start(2000); // 2秒后触发
  • 循环定时器:默认情况下,QTimer 是循环定时器,即每隔指定的时间间隔触发一次。

    1
    2
    QTimer *timer = new QTimer(this);
    timer->start(1000); // 每隔1秒触发

5. 定时器的精度

  • QTimer 是基于 Qt 事件循环的,因此它的精度受限于系统的事件调度机制。在处理复杂的 UI 或繁重任务时,定时器的精度可能受到影响。通常情况下,QTimer 可以提供毫秒级的精度,但并不适用于需要严格实时性的场景。

6. 定时器的线程安全性

  • QTimer 必须在其所属的线程中使用。如果你在多线程环境中使用定时器,确保定时器与其所在的线程一致。可以使用 QTimerQThread 的组合来在子线程中处理定时任务。

7. 常见用例

  • 动画刷新:通过定时器定期更新 UI 元素的状态。
  • 定时任务:在应用程序中定时执行某些任务,例如自动保存、定时更新数据等。
  • 延迟操作:在特定时间后执行某一操作,如提示信息延迟消失。

8. QTimer 示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
#include <QApplication>
#include <QMainWindow>
#include <QTimer>
#include <QLabel>

int main(int argc, char *argv[]) {
QApplication a(argc, argv);

QMainWindow mainWindow;
QLabel *label = new QLabel("Hello, Qt!", &mainWindow);
mainWindow.setCentralWidget(label);

QTimer *timer = new QTimer(&mainWindow);
QObject::connect(timer, &QTimer::timeout, [&]() {
label->setText("Timer triggered!");
});
timer->start(2000); // 2秒后触发

mainWindow.show();
return a.exec();
}

总结

QTimer 是一个强大且灵活的定时工具,在 Qt 开发中广泛用于管理定时任务。它与 Qt 的信号和槽机制紧密集成,使得处理异步操作和事件驱动编程变得更加容易。无论是周期性操作还是一次性延迟操作,QTimer 都能够提供理想的解决方案。

CSV文件是什么

CSV(Comma-Separated Values,逗号分隔值)文件是一种用于存储表格数据的简单文本文件格式。CSV文件每行通常代表表格中的一行,每行的数据字段由逗号分隔。它被广泛用于数据导入导出,因为其结构简单,几乎所有的表格处理软件(如Excel、Google Sheets)和数据库系统都支持CSV格式。

CSV 文件的特点

  1. 结构简单:文件由纯文本组成,行表示记录,逗号分隔每行中的字段。
  2. 兼容性好:可以被几乎所有的电子表格软件和数据库读取和导出。
  3. 便于编辑:可以直接用文本编辑器查看和编辑。

CSV 文件格式示例

假设有一个简单的学生信息表,包含 “姓名” 和 “年龄” 两个字段。CSV 文件内容可能如下:

1
2
3
4
姓名,年龄
张三,25
李四,30
王五,28

每行表示一条记录,张三25 之间用逗号分隔,表示姓名和年龄。

CSV 的应用场景

  • 数据导入和导出:CSV 常用于在不同软件之间传递数据,例如在数据库和表格软件之间导出或导入数据。
  • 简单的数据存储:CSV 文件常用于保存简单的结构化数据。

注意事项

  • 分隔符:虽然默认使用逗号,但在某些区域(如欧洲)可能会使用分号作为分隔符。
  • 转义字符:如果字段内容包含逗号,需用双引号包裹该字段。例如:"张三, 李四",25
  • 不支持复杂格式:CSV 文件不支持单元格格式、颜色、图表等,只能存储纯文本数据。

引言

  • 一定学会Qt的基本使用,能够设计和开发常用的人机交互界面。

第一章 致读者

1.1 本书结构

  • 纯粹的入门教材通常会这样组织其内容–所有概念都先回介绍再应用,因此必须从第一页开始顺序阅读。与之相反,纯粹的参考手册则可以从任何地方开始查阅,因为每个主题的描述都简明扼要,辅以指向相关主题的引用。
  • 本书包含以下四个部分
    • 第一部分:第一章是本书的导引,会介绍一点C++的背景知识。第2-5章对C++语言及其标准库进行简要介绍
    • 第二部分:第6-15章介绍C++的内置类型和基本特性以及如何用他们构造程序
    • 第三部分:第16-29章介绍C++的抽象机制及如何用这些机制编写面向对象和泛型编程
    • 第四部分:第30-44章概述标准库并讨论一些兼容性问题。

1.1.1 引言

  • 接下来几章将要简要介绍C++程序设计语言及其标准库的主要概念和特性
    • 第二章:基础知识。介绍C++的内存模型,计算模型和错误处理模型
    • 第三章:抽象机制。介绍用来支持数据抽象,面向对象编程以及泛型编程的语言特性
    • 第四章:容器与算法。介绍标准库提供的字符串,简单I/O,容器和算法等特性
    • 第五章:并发和实用功能。概述与资源管理,并发,数字计算,正则表达式及其他一些方面相关的标准库工具

1.1.2 基本特性

  • C++支持传统的C语言编程风格,第二部分重点介绍支持C编程风格的C++子集,包括类型,对象,作用域和存储的基本概念。
    • 第六章:类型与声明。基础类型,命令,作用域,初始化,简单类型推断,对象生命周期和类型别名
    • 第七章:指针,数组与引用
    • 第八章:结构,联合与枚举
    • 第九章:语句。声明语句,选择语句,迭代语句,goto语句和注释语句
    • 第十一章:选择适当的操作。逻辑运算符,条件表达式,递增和递减,自由空间,{}列表,lambda表达式和显式类型转换
    • 第十二章:函数。函数声明和定义,inline函数,constexpr函数,实参传递,重载函数,前置和后置条件,函数的指针和宏
    • 第十三章:异常处理。错误处理风格,异常保证,资源管理,强制不变量,throw和catch,一个vector的实现
    • 第十四章:名字空间。namespace,模块化和接口,使用名字空间组织代码。
    • 第十五章:源文件与程序。分离编译,链接,使用头文件及程序启动和结束

1.1.3 抽象机制

  • 第三部分介绍的C++特性用来支持不同形式的抽象,包括面向对象编程和泛型编程。所有章节可以粗略分为三组:类,类继承和模板
    • 第十六章:类。用户自定义类型,也就是类的概念,是所有C++抽象机制的基础
    • 第十七章:构造,清理,拷贝和移动。展示了程序员如何定义类对象创建和初始化操作的含义。此外,拷贝,移动和析构的含义同样可由程序员来定义
    • 第十八章:运算符重载。介绍了为用户自定义类型指定运算符含义的规则,重点介绍常用的算术和逻辑运算符
    • 第十九章:特殊运算符。讨论用户自定义的非算术运算符的使用
    • 第二十章:派生类。介绍构建类层次的基本语言特性及其基本使用方法。我们可以实现接口(抽象类)与其实现(派生类)的完全分离,两者间的联系通过虚函数提供
    • 第二十一章:类层次。讨论有效的使用类层次的方法。
    • 第二十二章:运行时类型信息。介绍如何使用存储在对象中的数据实现在类层次中导航。我们可以使用dynamic_cast查询一个基类对象是否是作为派生类对象定义的
    • 第二十三章:模板。介绍隐藏在模板及其使用方法之下的基本原理
    • 第二十四章:泛型程序设计。介绍设计泛型程序所需的基本技术
    • 第二十五章:特例化。介绍特例化技术,即如何利用给定的一组模板参数,从模板生成类和函数
    • 第二十六章:实例化。主要介绍名字绑定规则
    • 第二十七章:模板和类层次。介绍模板层次和类层次如何结合使用
    • 第二十八章:元编程。介绍如何用模板生成程序
    • 第二十九章:一个矩阵设计。

1.1.4 标准库

  • 实际上这一部分可以当作标准库组件的用户手册来使用
    • 第三十章:标准库概览。给出标准库的概览,列出标准库头文件,并介绍语言支持和程序诊断方面的支持
    • 第三十一章:STL容器。介绍迭代器,容器和算法框架中的容器
    • 第三十二章:STL算法。介绍STL中的算法
    • 第三十三章:STL迭代器。介绍STL中的迭代器和其他工具
    • 第三十四章:内存和资源。介绍内存和资源管理相关的工具组件
    • 第三十五章:工具。介绍一些重要性稍低的工具组件
    • 第三十六章:字符串。介绍标准库string
    • 第三十七章:正则表达式
    • 第三十八章:I/O流。介绍标准库I/O流,包括格式化和非格式化输入输出,错误处理以及缓冲
    • 第三十九章:区域设置。
    • 第四十章:数值计算。
    • 第四十一章:并发。介绍C++基本内存模型和C++所提供的支持无锁并发编程的工具
    • 第四十二章:线程和任务。介绍支持线程和锁风格并发编程的类和支持基于任务并发编程模式的类
    • 第四十三章:C标准库。介绍纳入C++标准库的C标准库特性
    • 第四十四章:兼容性。

1.2 C++的设计

  • 程序设计语言的目的就是帮助我们用代码来表达思想。因此,一种程序设计语言要完成两个相关的任务:为程序员提供一个工具,用来指明需要由计算机执行什么动作;为程序员提供一组概念,用于思考能做些什么。

  • C++的设计理念是同时提供:

    • 将内置操作和内置类型直接映射到硬件,从而提供高效的内存利用和高效的底层操作。
    • 灵活且低开小的抽象机制,使得用户自定义类型无论是符号表达,使用范围还是性能都能与内置类型相当。
  • 系统程序设计(system programming)的含义是编写直接使用硬件资源的,严重受限于资源的代码,或是编写的代码与这类代码联系紧密。特别是软件基础设施的实现(例如设备驱动程序,通信协议栈,虚拟机,操作系统,业务支持系统,编程环境以及基础库)大部分都属于系统程序设计。

1.2.1 程序设计风格

  • 我们可以简单描述软件设计和编程的基本理念:

    • 用代码直接表达想法
    • 无关的想法应独立表达
    • 用代码直接描述想法之间的关联
    • 可以自用的组合用代码表达想法,但仅在这种组合有意义时
    • 简单的想法应简单表达
  • C++语言特性直接支持四种程序设计风格

    • 过程式程序设计
    • 数据抽象
    • 面向对象程序设计
    • 泛型程序设计
  • 但是,重点不在于对单个程序设计风格的支持,而在于有效的组合它们。

  • 我理想中的语言特性应该能优雅的组合使用,来支持连续统一的程序设计风格和各种各样的程序设计技术

    • 过程式程序设计:这种风格专注于处理和设计恰当的数据结构。支持这种风格也是C语言的设计目标。C++对这种风格的支持体现为内置类型,运算符,语句,函数,struct和union等特性。
    • 数据抽象:这种风格专注于接口的设计以及一般实现细节的隐藏和特殊的表示方式。C++支持具体类和抽象类。一些语言特性可直接用来定义具有私有实现细节,构造函数和析构函数以及相关操作的类。而抽象类则为完全的数据隐藏提供了直接支持。
    • 面向对象程序设计:这种风格专注于类层次的设计,实现和使用。除了允许定义类框架之外,C++还提供了各种各样的特性来支持类框架中的导航以及简化由已有的类来定义新的类。类层次提供了运行时多态和封装机制。
    • 泛型程序设计:这种风格专注于通用算法的设计,实现和使用。在这里,通用的含义是,一个算法可以设计成能处理多种类型,只要这些类型满足算法对其实参的要求即可。C++支持泛型编程的主要特性是模板,模板提供了运行时参数多态。
  • 上述这些设计和编程风格的强大在于它们的综合,每种风格都对综合启动了重要作用,而这种综合实际上就是C++。因此,只关注一种风格是错误的,除非你只编写一些玩具程序,否则只关注一种风格会导致开发工作的浪费,产生非最优的程序

1.2.2 类型检查

  • 静态类型和编译时类型检查的概念对高效使用C++是极为重要的。静态类型的使用是可表达性,可维护性和新能的关键。

1.2.3 C兼容性

  • C++从C语言发展而来,它保留了C的特性作为子集。

1.2.4 语言,库和系统

  • C++的基本内置类型,运算符和语句都是计算机硬件能直接处理的:数字,字符和地址。C++没有内置的高级数据类型,也没有高级操作原语。

1.3 学习C++

  • 语言特性的存在是为了支持各种程序设计风格和技术。因此,语言的学习应该更关注掌握其固有的,内在的风格,而不是试图了解每个语言特性的所有细节。
  • 请记住学习C++细节知识的真正目的是:在良好设计所提供的语境中,有能力组合使用语言特性和库特性来支持好的程序设计风格。
  • 学习C++最重要的是重视基本概念(例如类型安全,资源管理和不变式)和程序设计技术(例如使用限定作用域的对象进行资源管理以及在算法中使用迭代器),还要注意不要迷失在语言技术性细节中。
  • 学习一门程序设计语言的目的是成为一个更好的程序员,即,能更高效的设计和实现新系统,维护旧系统。为此,领悟编程和设计技术比了解所有细节重要得多。

1.3.1 用C++编程

  • C++程序设计的主要理念与大多数高级语言编程一样:用代码直接表达从设计而来的概念。

1.4 C++的历史

1.4.5 C++的用途

  • C++有大量的支持库和工具集,例如
    • Boost 可移植基础库
    • POCO 网站开发库
    • QT 跨平台应用开发库
    • wxWidgets 跨平台图形用户界面库
    • WebKit 网页浏览器布局引擎库
    • CGAL 计算几何库
    • QuickFix 金融信息交换库
    • OpenCV 实时图像处理库
    • Root 高能物理库

1.5 建议

  • 每一章都有建议,给出该章节内容相关的一些具体建议。这些建议都是一些经验法则,而非不变的定律。
  • 对于初学者,下面列出了一些来自C++的设计,学习和历史这几节的建议
    • 用代码直接表达想法(概念),例如,表达为一个函数,一个类或是一个枚举
    • 编写代码应以优雅且高效为目标
    • 不要过度抽象
    • 设计应关注提供优雅且高效的抽象,可能的情况下以库的形式呈现
    • 用代码直接表达想法之间的关联,例如,通过参数化或类层次
    • 无关的想法应用独立的代码表达,例如,壁面类之间的相互依赖
    • 令资源是显式的(将它们表示为类对象)
    • 简单的想法应该简单表达
    • 使用库,特别是标准库,不要试图从头开始构建所有东西
    • 使用类型丰富的程序设计风格
    • 如果数据具有不变量,封装它
  • 总之:编写好程序需要智慧,风格和耐心。你不可能第一次就成功,要不断尝试。

第二章 C++概览:基础知识

2.1 引言

2.2 基础概念

  • C++是一种编译型语言。顾名思义,想要运行一段C++程序,需要首先用编译器把源代码转换为对象文件,然后再用链接器把这些对象组合生成可执行程序。一个C++程序通常包含许多源代码文件,通常称为源文件。
  • 一个可执行程序适用于一种特定的硬件/系统组合,是不可移植的。当我们谈论C++程序的可移植性时,通常是指源代码的可移植性。也就是说,同一份源代码可以在不同系统上成功编译并运行。
  • ISO的C++标准定义了两种实体
    • 核心语言功能,例如内置类型和循环
    • 标准库组件,例如容器和I/O操作
  • C++是一种静态类型语言,这意味着编译器在处理任何实体(例如对象,值,名称和表达式)时,都必须清楚它的类型。对象的类型决定了能在该对象上执行哪些操作。

2.2.1 Hello world

  • 在每个C++程序中有且只有一个名为main()的全局函数,在执行一个程序时首先执行该函数。如果main()返回一个int值,则这个值将作为程序给系统的返回值。如果main()没有返回任何值,则系统也将收到一个表示程序完成的值。这个值:基于Linux/Unix的环境通常会用到,而基于windows的环境一般不会用到
  • 基本上所有可执行代码都要放在函数中,并且被main()直接或间接的调用

2.2.2 类型,变量和算术运算

  • 每个名字和每个表达式都有一个类型,类型决定所能执行的操作。

  • 声明(declaration)是一条语句,负责为程序引入一个新的名字,并指定该命名实体的类型

    • 类型(type) 定义一组可能的值以及一组(对象上的)操作
    • 对象(object) 是存放某类型值的内存空间
    • 值(value) 是一组二进制位,具体的含义由类型决定
    • 变量(variable) 是一个命名的对象
  • C++提供了好几种表示初始化的符号

    • 符号 = 是一种比较传统的形式,最早被C语言使用
    • 花括号内的一组初始化器列表。最好在C++中使用更通用的{}列表形式。抛开其他因素不谈,使用初始化器列表的形式至少可以确保不会发生某些可能导致信息丢失的类型转换。
  • 我们可以使用 = 的初始化形式与auto配合,因为在此过程中不存在可能引发错误的类型转换。

  • 当我们没有明显的理由需要显式指定数据类型时,一般使用auto。在这里,明显的理由包括

    • 该定义位于一个较大的作用域中,我们希望代码的读者清楚的直到其类型
    • 我们希望明确规定某个变量的范围和精度(例如希望使用double而非float)

2.2.3 常量

  • C++支持如下两种不变性概念
    • const:大致意思是,我承诺不改变这个值。主要用于说明接口,这样在把变量传入函数时就不必担心变量会在函数内被改变了。编译器负责确认并执行const的承诺
    • constexpr:大致意思是,在编译时求值。主要用于说明常量,作用是允许将数据内置于只读内存中以及提升性能。

2.3 用户自定义类型

  • 我们把可以通过基本类型,const修饰符和声明运算符构造出来的类型称为内置类型(built-in type)
  • 我们把利用C++的抽象机制构建的新类型称为用户自定义类型(user-defined types)

第三章 C++概览:抽象机制

3.2 类

  • C++最核心的语言特性就是类。类是一种用户自定义的数据类型,用于在程序代码中表示某种概念。
  • 我们只考虑对三种重要的类的基本支持
    • 具体类
    • 抽象类
    • 类层次中的类

3.2.1 具体类型

  • 具体类的基本思想是他们的行为 就像内置类型一样。

  • 析构函数的命名规则是一个求补运算符后接类的名字,从含义上来说它是构造函数的补充。

3.5 建议

  • 直接用代码表达你的想法
  • 在代码中直接定义类来表示应用中的概念
  • 用具体类表示那些简单的概念或性能关键的组件
  • 避免裸的new和delete操作
  • 用资源句柄和RAII管理资源
  • 当接口和实现需要完全分离时使用抽象类作为接口
  • 用类层次表示具有固有的层次关系的概念
  • 在设计类层次时,注意区分实现继承和接口继承
  • 控制好对象的构造,拷贝,移动和析构操作
  • 以值的方式返回容器(依赖于移动操作以提高效率)
  • 注意强资源安全,也就是说,不要泄露任何你认为是资源的东西
  • 使用函数模板表示通用的算法

第四章 C++概览:容器与算法

4.1 标准库

  • 本书介绍的标准库设施,在任何一个完整的C++实现中都是必备的部分。

4.1.1 标准库概述

  • 标准库提供的设施可以分为以下几类
    • 运行时语言支持,例如对资源分配和运行时类型信息的支持
    • C标准库
    • 字符串和I/O流
    • 一个包含容器和算法的框架,人们习惯上称这个框架为标准模板库(STL)
    • 对数值计算的支持
    • 对正则表达式匹配的支持
    • 对并发程序设计的支持,包括thread和lock机制
    • 一系列工具,它们用于支持模板元编程,STL-风格的泛型程序设计和通用程序设计
    • 用于资源管理的智能指针和垃圾收集器接口
    • 特殊容器
  • 本质上来说,C++标准库提供了最常用的基本数据结构以及运行在之上的基础算法。

4.6 建议

  • 没必要推倒重来,直接使用标准库是最好的选择
  • 除非万不得已,大多数时候先考虑使用标准库,在考虑别的库
  • 标准库绝非万能
  • 一定要了解各种标准库容器的设计思想和优缺点
  • 优先选用vector作为你的容器类型
  • 如果你拿不准会不会越界,记得使用带边界检查的容器
  • 用push_back()或者back_inserter()给容器添加元素

第五章 C++概览:并发与实用功能

5.2 资源管理

  • 所有程序都包含一项关键人物:管理资源。所谓资源是指程序中符合先获取后释放规律的东西,比如内存,锁,套接字,线程句柄和文件句柄等。

5.2.1 unique_ptr与shared_ptr

  • 标准库提供了两种智能指针来管理自由存储上的对象
    • unique_ptr对应所有权唯一的情况
    • shared_ptr对应所有权共享的情况
  • 这些智能指针最基本的作用是防止由于编程疏忽而造成的内存泄漏。

5.3 并发

  • 并发,也就是多个任务同时执行,被广泛用于提高吞吐率(用多个处理器共同完成单个运算)和提高响应速度(允许程序的一部分在等待响应时,另一部分继续执行)
  • 标准库并发设施重点提供系统级并发机制,而不是直接提供复杂的高层并发模型。基于标准库并发设施可以构建出这类高层并发模型,并以库的形式提供。
  • 标准库直接支持在单一地址空间内并发执行多个线程。为了实现这一目的,C++提供了一个适合的内存模型和一套原子操作。

5.3.1 任务和thread

  • 我们称那些可以与其他计算并行执行的计算为任务(task)。线程是任务在程序中的系统级表示。
  • 若要启动一个与其他任务并发执行的任务,我们可以构造一个std::thread并将任务作为它的实参。这里的任务是以函数或函数对象的形式出现的。
  • 一个程序的所有线程共享单一地址空间。在这一点上线程与进程不同,进程间通常不直接共享数据。由于共享单一地址空间,因此线程间可通过共享对象互相通信。通常通过锁或其他防止数据竞争的机制来控制线程间通信。

5.3.4 等待事件

  • 有时候thread需要等待某种外部事件,比如另一个thread完成了任务或是已经过去了一段时间。最简单的事件就是时间流逝。
  • 通过外部事件实现线程间通信的基本方式是使用condition_variable,它定义在中。condition_variable提供了一种机制,允许一个thread等待另一个thread。特别的是,它允许一个thread等待某个条件(condition,通常称为一个事件,event)发生,这种条件通常是其他thread完成工作产生的结果。

5.3.5 任务通信

  • 标准库提供了一些特性,允许程序员在抽象的任务层(工作并发执行)进行操作,而不是在底层的线程和锁的层次直接进行操作。
    • future和promise用来从一个独立线程上创建出的任务返回结果
    • packaged_task是帮助启动任务以及连接返回结果的机制
    • async()以非常类似调用函数的方式启动一个任务

5.7 建议

  • 用资源句柄管理资源(RAII)
  • 用unique_ptr访问多态类型的对象
  • 用shared_ptr访问共享的对象
  • 用类型安全的机制处理并发
  • 最好避免共享数据
  • 不要为了所谓效率,而不经思考,不经测试的选择使用共享数据
  • 从并发执行任务的角度进行设计,而不是直接从thread角度思考
  • 一个库是否有用,与它的规模和复杂程度无关
  • 别轻易抱怨程序的效率低下,记得用事实说话
  • 编写代码时可以显式的令其利用某些类型的属性
  • 利用正则表达式简化模式匹配任务
  • 进行数值计算时优先选择使用库而非语言本身
  • 用numeric_limits访问数值类型的属性

第六章 类型与声明

6.1 ISO C++标准

  • 在C++标准之下,很多重要的功能都是依赖于实现的(implementation-defined)。这意味着对于语言的某个概念来说,每个实现版本都必须为之设定恰当的,定义良好的语法行为,同时详细记录行为规范。

6.1.1 实现

  • C++的一个具体实现可以有两种形式:宿主式(hosted)和独立式(freestanding)。
  • 在宿主式实现中包含了C++标准和本书描述的所有标准库功能;独立式实现包含的标准库功能可能会少一些。

6.2 类型

  • C++程序中的每个名字(标识符)都对应一种数据类型。该类型决定了这个名字(即该名字代表的实体)能执行哪些运算以及如何执行这些运算。

6.2.1

  • C++包含了一套基本类型(fundamental type),这些类型对应计算机最基本的存储单元并且展现了如何利用这些单元存储数据。

    • 布尔值类型
    • 字符类型
    • 整数类型
    • 浮点数类型
    • void类型,用以表示类型信息缺失
    • 基于上述类型,我们可以用声明符构造出更多类型
      • 指针类型
      • 数组类型
      • 引用类型
    • 除此之外,用户还能自定义类型
      • 数据结构和类
      • 枚举类型,用以表示特定值的集合
  • 其中,布尔值,字符和整数统称为整型(integral type),整型和浮点型进一步统称为算术类型(arithmetic type )。

  • 我们把枚举类型和类称为用户自定义类型(user-defined),因为用户必须先定义它们,然后才能使用;这一点显然与基本类型无须声明可以直接使用的方式不同。

  • 与之相反,我们把基本类型,指针和引用统称为内置类型(built-in type)。

6.2.2 布尔值

  • 一个布尔变量(bool)的取值或是true或者是false,布尔变量常用于表示逻辑运算的结果。
  • 根据定义,当我们把布尔值换成整数时,true转为1,而false转为0.反之,整数值也能在需要的时候隐式的转换成布尔值,其中非0整数值对应true,而0对应false。

6.2.9 对齐

  • 对象首先应该有足够的空间存放对应的变量,但这还不够。在一些及其的体系结构中,存放变量的字节必须保持一种良好的对齐方式(alignment),以便硬件在访问数据资源时足够高效(在极端情况下一次性访问所有数据)
  • 例如,4字节的int应该按字(4字节)的边界排列,而8字节的double有时也应该按字(8字节)的边界排列。
  • alignof()运算符返回实参表达式的对齐情况。

6.3 声明

  • 在C++程序中要想使用某个名字(标识符),必须先对其进行声明。换句话说,我们必须指定它的类型以便编译器指导这个名字对应的是何种实体。
  • 大多数声明(declaration)同时也是定义(definition)。我们可以把定义看成是一种特殊的声明,它提供了在程序中使用该实体所需的一切信息。尤其是当实体需要内存空间来存储某些信息时,定义语句把所需的内存预留了出来。

6.3.1 声明的结构

  • 我们可以认为一条声明语句(依次)包含5个部分:
    • 可选的前置修饰符。static,virtual
    • 基本类型。 const int
    • 可选的声明符。p[7]
    • 可选的后缀函数修饰符。const noexcept
    • 可选的初始化器或函数体。{return x;}
  • 修饰符,是指声明语句中最开始的关键字,例如virtual,extern,constexpr等。修饰符的作用是指定所声明对象的某些非类型属性。
  • 声明符由一个名字和一些可选的声明运算符组成。最常用的声明运算符包括
    • 前缀 * 指针
    • 前缀 *const 常量指针
    • 前缀 & 左值引用
    • 前缀 && 右值引用
    • 前缀 auto 函数(使用后置返回类型)
    • 后缀 [] 数组
    • 后缀 () 函数
    • 后缀 -> 从函数返回

6.3.2 声明多个名字

  • C++允许在同一条声明语句中声明多个名字,其中包含逗号隔开的多个声明符即可。
  • 读者千万要注意,在声明语句中,运算符只作用于紧邻的一个名字,对于后续的其他名字是无效的

6.3.4 作用域

  • 声明语句为作用域引入了一个新名字,换句话说,某个名字只能在程序文本的某个特定区域使用。

    • 局部作用域(local scope):函数,lambda表达式中声明的名字称为局部名字。局部名字的作用域从声明处开始,到声明语句所在的快结束为之。其中块(block)是指用一对{}包围的代码片段。对于函数和lambda表达式最外层的块来说,参数名字是其中的局部名字
    • 类作用域(class scope): 如果某个类位于任意函数,类和枚举类或其他名字空间的外部,则定义在该类中的名字称为成员名字或类成员名字。类成员名字的作用域从类声明的{开始,到类声明的结束为止
    • 名字空间作用域(namespace scope):
    • 全局作用域(global scope):
    • 语句作用域(statement scope):
    • 函数作用域(function scope):
  • 在块内声明的名字能隐藏外层快及全局作用域中的同名声明。换句话说,一个已有的名字能在块内被重新定义以指向另外一个实体。退出块后,该名字恢复原来的含义。

  • 我们可以使用作用域解析运算符::访问被隐藏了的全局名字。

6.3.5 初始化

  • 顾名思义,初始化器就是对象在初始状态下被赋予的值。初始化器又四种可能的形式

    • X a1{v};
    • X a2 = {v};
    • X a3 = v;
    • X a4(v)
  • 在这些形式中,只有第一种不受任何限制,在所有场景中都能使用。

  • 建议读者使用{}。使用{}的初始化称为列表初始化(list initialization),它能防止窄化转换。这句话的意思是

    • 如果一种整型存不下另一种整型的值,则后者不会被转换成前者。例如,允许char到int的类型转换,但是不允许int到char的类型转换
    • 如果一种浮点型存不下另一种浮点型的值,则后者不会被转换成前者。例如,允许float到double的类型转换,但是不允许double到float的类型转换
    • 浮点型的值不能转换成整型值
    • 整型值不能转换成浮点型的值
  • 当我们使用auto关键字从初始化器推断变量的类型时,没必要采用列表初始化的方式。而且如果初始化器是{}列表,则推断得到的数据类型肯定不是我们想要的结果。

  • 因此,当使用auto的时候应该选择=的初始化形式。

  • 空初始化器列表{}指定使用默认值进行初始化。大多数数据类型都有默认值

    • 对于整数类型来说,默认值是数字0的某种适当形式。
    • 指针的默认值是nullptr
    • 用户自定义类型的默认值由该类型的构造函数决定。

6.3.6 推断类型:auto和decltype()

  • C++语言提供了两种从表达式中推断数据类型的机制
    • auto根据对象的初始化器推断对象的数据类型,可能是变量,const或者constexpr的类型
    • decltype(expr)推断的对象不是一个简单的初始化器,有可能是函数的返回类型或者类成员的类型。
  • 这里所谓的推断其实非常简单:auto和decltype()只是简单报告一个编译器已知的表达式的类型。
6.3.6.1 auto类型修饰符
  • 表达式的类型越难读懂,越难书写,auto就越有用
  • 在较小的作用域中,建议程序员优先选择使用auto
  • 请注意,表达式的类型永远不会是引用类型,因为表达式会隐式的执行解引用操作。

6.3.6.3 decltype()修饰符

  • 当有一个合适的初始化器的时候可以使用auto。但是很多时候我们既想推断得到类型,又不想在此过程中定义一个初始化的变量,此时,我们应该使用声明类型修饰符decltype(expr)。其中,推断所得的结果是expr的声明类型。这种用法在泛型编程中很有效。

6.4 对象和值

  • 对象(object)是指一块连续存储区域,左值(lvalue)是指对象的一条表达式。
  • 左值的字面意思是:能用在赋值运算符左侧的东西,但其实不是所有左值都能用在赋值运算符的左侧,左值也有可能指示某个常量。未被声明成const的左值称为可修改的左值。

6.4.1 左值和右值

  • 为了补充和挖山左值的含义,我们相应的定义了右值(rvalue)。简单来说,右值是指不能作为左值的值,比如像函数返回值一样的临时值
  • 在实际编程过程中,考虑左值和右值就足够了。一条表达式要么是左值,要么是右值,不可能两者都是。

6.4.2 对象的声明周期

  • 对象的声明周期(lifetime)从对象的构造函数完成的那一刻开始,直到析构函数执行为止。对于那些没有声明构造函数的类型(比如int),我们可以认为它们拥有默认的构造函数和析构函数,并且这两个函数不执行任何实际操作。

  • 我们从生命周期的角度把对象划分成以下类别

    • 自动对象(automatic):除非程序员特别说明,否则在函数中声明的对象在其定义处被创建,当超出作用域范围时被销毁。这样的对象被称为自动对象。在大多数实现中,自动对象被分配在栈空间上。每调用一次函数,获取新的栈帧(stack frame)以存放它的自动对象。
    • 静态对象(static):在全局作用域或名字空间作用域中声明的对象以及在函数或类中声明的static成员只被创建并初始化一次,并且直到程序结束之前都活着。这样的对象被称为静态对象。静态对象在程序的整个执行周期内地址唯一。在多线程环境中,静态对象可能会造成某些意料之外的问题。因为所有线程都共享对象,所以必须为其加锁以避免数据竞争。
    • 自由存储对象(free store):用new和delete直接控制其声明周期的对象
    • 临时对象(temporary):比如计算的中间结果或者用于存放const实参引用的值的对象。临时对象的生命周期由其用法决定。
    • 线程局部对象(thread-local):或者说声明为thread_local的对象,这样的对象随着线程的创建而创建,随着线程的销毁而销毁。
  • 其中,静态和自动被称为存储类(store class)

  • 数组元素和非静态类成员的生命周期由它们所属的对象决定。

6.6 建议

  • 尽量避免不确定的和未定义的行为
  • 如果某些代码必须依赖于具体实现,记得把它们与程序的其他部分分离开来
  • 对于字符对应的数字值不要乱作假定
  • 以0开头的整数是八进制
  • 不要使用魔法常量
  • 注意带符号类型和无符号类型之间的转换
  • 在一条声明语句中只声明一个名字
  • 常用的,局部的名字尽量短;不常用的,非局部的名字可以长一些
  • 对象的名字应该尽量反应对象的含义而非类型
  • 坚持一种统一的命名风格
  • 避免使用全大写的名字
  • 作用域宜小不宜大
  • 最好不要在一个作用域以及它的外层作用域中使用相同的名字
  • 使用指定类型声明时最好用{}初始化器语法
  • 使用auto声明时最好用=语法
  • 避免使用未初始化的变量
  • 当内置类型被用来表示一个可变值时,不妨给它起个能反应其含义的别名。
  • 用别名作为类型的同义词,用枚举和类定义新类型。

第七章 指针,数组与引用

7.1 引言

  • 我们能通过名字使用对象。在C++中,对象位于内存的某个地址中,如果我们知道对象的地址和类型,就能访问它。
  • 在C++语言中存放及使用内存地址是通过指针和引用完成的。

7.2 指针

  • 对于类型T来说,T*是表示 指向T的指针 的类型。换句话说,T*类型的变量能存放T类型对象的地址。

  • 对指针的一个基本操作是解引用(dereferencing),即引用指针所指的对象。这个操作也称为间接取值(indirection)。解引用运算符是个前置一元运算符,对应的符号是*。

  • 指针的具体实现应该与运行程序的机器的寻址机制同步。大多数机器支持逐字节访问内存,其他机器则需要从字中抽取字节。很少有机器能直接寻址到一个二进制位。因此,能独立分配且用内置指针指向的最小对象是char类型的对象。

  • 有一点读者注意:bool占用的内存空间至少和char一样多。如果想把更小的值存的更紧密,可以使用位逻辑操作,结构中的位域或者bitset

  • 符号*在用作类型名的后缀时表示 指向 的含义。如果我们想表示指向数组的指针或者指向函数的指针,需要使用稍微复杂的一些方式。

    1
    2
    3
    4
    5
    int* pi;  // 指向int的指针
    char** ppc; // 指向字符指针的指针
    int* ap[15]; // ap是一个数组,包含15个指向int的指针
    int (*fp)(char*); // 指向函数的指针,该函数接受一个char*实参,返回一个int
    int* f(char*); // 该函数接受一个char*实参,返回一个指向int的指针。

7.2.1 void *

  • 在某些偏向底层的代码中,我们偶尔需要在不知道对象确切类型的情况下,仅通过对象在内存中的地址存储或传递对象。此时,我们会用到void*,其含义是 指向未知类型对象的指针。
  • 除了函数指针和指向类成员的指针,指向其他任意类型对象的指针都能被赋给一个void*类型的变量。
  • void*最主要的用途是当我们无法假定对象的类型时,向函数传递指向该对象的指针;它还用于从函数返回未知类型的对象。要想使用这样的对象,必须先进行显式类型转换。

7.2.2 nullptr

  • 字面值常量nullptr表示空指针,即不指向任何对象的指针。我们可以把nullptr赋给其他任意指针类型,但是不能赋给其他内置类型。
  • 使用nullptr的好处很多,首先它的可读性更强,其次当一组重载函数既可以接受指针也可以接受整数时,用nullptr能够避免语义混淆。

7.3 数组

  • 假设有类型T,T[size]的含义是包含size个T类型元素的数组。元素的索引值范围是0到size-1。
  • 以0作为终止符的char数组是应用最广泛的一种数组。这是C语言存储字符串的基本方式,因此我们常把0作为终止符的char数组称为C风格字符串。

7.3.1 数组的初始化器

  • 我们能用值的列表初始化一个数组,例如:
    1
    2
    int v1[] = {1, 2, 3, 4};
    char v2[] = {'a', 'b', 'c', 'd'};
  • 如果声明数组的时候没有指定它的大小但是给出了初始化器列表,则编译器会根据列表包含的元素数量自动计算数组的大小。
  • 如果我们指定了数组的大小,但是提供的初始化器列表元素数量过多,则程序会发生错误。
  • 如果初始化器提供的元素数量不足,则系统自动把剩余的元素赋值为0
  • C++没有为数组提供内置的拷贝操作。不允许用一个数组初始化另一个数组,即使两个数组的类型完全一样也不行,因为数组不支持赋值操作。同样,不允许以传值方式传递数组。
  • 如果你想给一组对象赋值,可以使用vector,array或者valarray
  • 我们可以用字符串字面量常量初始化字符的数组。

7.4 数组中的指针

  • 在C++语言中,指针与数组密切相关。数组名可以看成是指向数组首元素的指针。

7.4.3 传递数组

  • 不能以值传递的方式直接把数组传给函数,我们通常传递的是数组首元素的指针。

7.5 指针与const

  • C++提供了两种与常量有关的概念
    • constexpr:编译时求值
    • const:在当前作用域内,值不发生改变
  • 基本上,constexpr的作用是指示或确保在编译时求值,而const的主要任务是规定接口的不可修改性。
  • 很多对象的值一旦初始化就不会再改动
    • 使用符号化常量的代码比直接使用字面值常量的代码更易维护
    • 我们经常通过指针读取数据,但是很少通过指针写入数组
    • 绝大多数函数的参数只负责读取数据,很少写入数据。
  • 为了表达已经初始化就不可修改的特性,我们可以再对象的定义中加上const关键字。
  • 一旦我们把某物声明成const,就确保它的值在其作用域内不会发生改变
  • 使用const会改变一种类型。所谓改变不是说改变了常量的分配方式,而是限制了它的使用方式。

7.6 指针与所有权

  • 资源必须先分配后释放。指针是最常用的资源句柄。

7.7 引用

  • 引用作为对象的别名存放的也是对象的机器地址。与指针相比,引用不会带来额外的开销。引用和指针的区别主要包括
    • 访问引用与访问对象本身从语法形式上看是一样的
    • 引用所引的永远是一开始初始化的那个对象
    • 不存在空引用。我们可以认为应用一定对应着某个对象。
  • 引用实际上是对象的别名。引用最重要的用途是作为函数的实参或返回值,此外,它也被用于重载运算符。
  • 为了体现左值/右值以及const/非const的区别,存在三种形式的引用
    • 左值引用(lvalue reference): 引用那些我们希望改变值的对象
    • const引用(const reference): 引用那些我们不希望改变值的对象,比如常量
    • 右值引用(rvalue reference): 所引对象的值在我们使用之后就无需保留了,例如临时变量。
  • 这三种形式统称为引用,其中前两种形式都是左值引用。

7.7.1 左值引用

  • 在类型名字中,符号X&的意思是 X的引用:它常用于表示左值的引用,因此称为左值引用。

7.8 建议

  • 使用指针时越简单直接越好
  • 不要对指针执行稀奇古怪的算术运算
  • 注意不要越界访问数组,尤其不要再数组之外的区域写入内容
  • 不要使用多维数组,用合适的容器代替它
  • 用nullptr代替0和NULL
  • 与内置的C风格数组相比,优先使用容器
  • 优先选用string,而不是以0结尾的char数组
  • 如果字符串字面值常量中包含太多反斜线,则使用原始字符串
  • const引用比普通引用更适合作为函数的实参
  • 只要当需要转发和移动时才使用右值引用
  • 让表示所有权的指针位于句柄类的内部
  • 再底层代码之外尽量不要使用void*
  • 用const指针和const引用表示接口中不允许修改的部分

第八章 结构,联合与枚举

8.1 引言

  • 用户自定义类型是能否有效使用C++的关键。
  • 三种用户自定义类型的初级形式
    • struct,结构是由任意类型元素(即成员)构成的序列
    • union,是一种struct,同一时刻只保存一个元素的值
    • enum,枚举是包含一组命名常量(称为枚举值)的类型
    • enum class(限定作用域的枚举类型)是一种enum,枚举值位于枚举类型的作用域内,不存在向其他类型的隐式类型转换。

8.2 结构

  • 数组是相同类型元素的集合。相反,struct是任意类型元素的集合。
  • 结构类型的对象可以被赋值,作为实参传入函数,或者作为函数的结果返回。
  • 默认情况下,比较运算符(==, !=)等一些似是而非的操作并不适用于结构类型。当然,用户有权自定义这些运算符。

8.2.1 struct的布局

  • 在struct的对象中,成员按照声明的顺序依次存放。
  • 在内存中为成员分配空间时,顺序与声明结构的时候保持一致。
  • 然而,一个struct对象的大小不一定恰好等于它所有元素大小的累积之和。因为很多机器要求一些特定类型的对象沿着系统结构设定的边界分配空间,以便机器能高效的处理这些对象。例如,整数通常沿着字的边界分配空间。
  • 在这类机器上,我们说对象对齐(aligned)得很好。
  • 通常情况下,我们应该从可读性的角度出发设计结构成员的顺序。只有当需要优化程序的性能时,才按照成员的大小排序。

8.2.2 struct的名字

  • 类型名字只要一出现就能马上使用了,无需等到该类型的声明全部完成。例如
    1
    2
    3
    4
    5
    struct Link 
    {
    Link* previous;
    Link* successor;
    };
  • 但是,只有等到struct的声明全部完成,才能声明它的对象。例如
    1
    2
    3
    4
    struct No_good 
    {
    No_good member; // 错误:递归定义
    };
  • 因为编译器无法确定No_good的大小,所以程序会报错。要想让两个或更多struct互相引用,必须提前声明好struct的名字。例如
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    struct List;
    struct Link
    {
    Link* pre;
    Link* suc;
    List* member_of;
    int data;
    };

    struct Link
    {
    Link* head;
    };
  • 如果没有一开始声明List,则在稍后声明Link时使用List*类型的指针将造成错误。
  • 我们可以在真正定义一个struct类型之前就使用它的名字,只要在此过程中不使用成员的名字和结构的大小就行了。然而,直到struct的声明完成之前,它都是一个不完整的类型。

8.2.3 结构与类

  • struct是一种class,它的成员默认是public的。struct可以包含成员函数,尤其是构造函数。
  • 如果只是想按照默认的顺序初始化结构的成员,则不需要专门定义一个构造函数。
  • 但是如果你需要改变实参的顺序,检验实参的有效性,修改实参或者建立不变式,则应该编写一个专门的构造函数。

8.2.4 结构与数组

  • 很自然的,我们可以构建struct的数组,也可以让struct包含数组。

8.2.5 类型等价

  • 对于两个struct来说,即使它们的成员相同,它们本身仍是不同的类型。例如
    1
    2
    struct S1{int a;};
    struct S2{int a;};
  • S1和S2是两种类型,因此:
    1
    2
    S1 x;
    S2 y = x; // 错误:类型不匹配
  • struct本身的类型与其成员的类型不能混为一谈。例如
    1
    2
    S1 x;
    int i = x; // 错误:类型不匹配
  • 在程序中,每个struct只能有唯一的定义。

8.2.6 普通旧数据

  • 有时候,我们只想把对象当成普通旧数据(内存中连续字节序列)而不愿考虑那些高级语义概念,比如运行时多态,用户自定义的拷贝语义等。这么做的主要动机是在硬件条件允许的范围内尽可能高效的移动对象。

  • 例如,要执行拷贝含有100个元素的数组的任务,调用100次拷贝构造函数显然不像直接调用std::memcpy()有效率,毕竟后者只需要使用一个块移动指令即可。

  • POD(普通旧数据)是指能被 仅当作数据 处理的对象,程序员无需顾及类布局的复杂性以及用户自定义的构造,拷贝和移动语义。

  • 我们如果想把某个对象仅当作数据处理,则要求该对象必须满足下述条件

    • 不具有复杂的布局
    • 不具有非标准拷贝语义
    • 含有一个最普通的默认构造函数。

8.2.7 域

  • 看起来用一整个字节(一个char或者一个bool)表示一个二元变量(比如on/off开关)有些浪费,但是char已经是C++中能独立分配和寻址的最小对象了。我们也可以把这些微小的变量组织在一起作为struct的域(field)。域也称为位域(bit-field)
  • 我们只要指定成员所占的位数,就能把它定义成域了。

8.3 联合

  • union是一种特殊的struct,它的所有成员都分配在同一个地址空间上。因此,一个union实际占用的空间大小与其最大的成员一样。自然的,在同一时刻union只能保存一个成员的值。
  • 语言本身并不负责追踪和管理union到底存的是哪种值,这是程序员的责任。
  • 使用union的目的无非是让数据更紧密,从而提高程序的性能。然而,大多数程序即使用了union也不会提高太多;同时,使用union的代码更容易出错。因此,我认为union是一种被过度使用的语言特性,最好不要出现在你的程序中

8.3.1 联合与类

  • 从技术上说,union是一种特殊的struct,而struct是一种特殊的class。然而,很多提供给类的功能和联合无关,因此对union施加了一些限制
    • union不能含有虚函数
    • union不能含有引用类型的成员
    • union不能含有基类
    • 在union的所有成员中,最多只能有一个成员包含类初始化器
    • union不能被用作其他类的基类。
  • 这些约束规则有效的阻止了很多错误的发生,同时简化了union的实现过程。后面一点非常重要,因为union的主要作用是优化代码的性能,所以我们肯定不希望再使用union的过程中引入隐形的代价。

8.4 枚举

  • 枚举(enumeration)类型用于存放用户指定的一组整数值。枚举类型的每种取值各自对应一个名字,我们把这些值叫做枚举值(enumerator)
  • 枚举类型分为两种
    • enum class,它的枚举值名字位于enum的局部作用域内,枚举值不会隐式的转换成其他类型
    • 普通的enum,它的枚举值名字与枚举类型本身位于同一个作用域中,枚举值隐式的转换成整数
  • 通常情况下,建议程序员使用enum class。

8.4.1 enum class

  • enum class是一种限定了作用域的强类型枚举
  • 枚举常用一些整数类型表示,每个枚举值是一个整数。我们把用于表示某个枚举的类型称为它的基础类型(underlying type)。基础类型必须是一种带符号或无符号的整数类型,默认是int。我们可以显式的指定。
  • 默认情况下,枚举值从0开始,依次递增。
  • C++允许先声明一个enum class,稍后再给出它的定义
  • 一个整数类型的值可以显式的转换成枚举类型。如果这个值属于枚举的基础类型的取值范围,则转换是有效的;否则,如果超出了合理的表示范围,则转换的结果是未定义的。
  • 对enum class执行的sizeof的结果是对其基础类型执行sizeof的结果。如果没有显式指定基础类型,则枚举类型的尺寸等于sizeof(int)

8.4.3 未命名的enum

  • 一个普通的enum可以是未命名的。例如
    1
    enum {arrow_up = 1, arrow_down, arrow_sideways};
  • 如果我们需要的只是一组整型常量,而不是用于声明变量的类型,则可以使用未命名的enum。

8.5 建议

  • 如果想紧凑的存储数据,则把结构中尺寸较大的成员布局在较小的成员之前
  • 用位域表示由硬件决定的数据布局
  • 不要天真的认为仅靠把几个值打包在一个字节中就能轻易的优化内存
  • 用union减少内存空间的使用,不要将它用于类型转换
  • 用枚举类型表示一组命名的常量

第九章 语句

9.1 引言

  • C++提供了一组即符合传统又灵活易用的语句。
  • 一个声明就是一条语句,表达式的末尾加上一个分号也是一条语句。
  • 与表达式不同,语句本身没有值。语句的主要作用是指定执行的顺序。

9.2 语句概述

  • 分号本身也是一条语句,即空语句(empty statement)
  • 花括号{} 括起来的一个可能为空的语句序列称为块(block)或者复合语句(compound statement)。块中声明的名字的作用域到块的末尾就结束了。
  • 声明(declaration),是一条语句,没有赋值语句或过程调用语句;赋值和函数调用不是语句,它们是表达式。
  • for初始化语句(for-init-statement)要么是声明,要么是一条表达式语句,它们都以分号结束
  • for初始化声明(for-init-declaration)必须是一个未初始化变量的声明
  • try语句块(try-block)的作用是处理异常。

9.3 声明作为语句

  • 一个声明就是一条语句。除非变量被声明成static,否则在控制线程传递给当前声明语句的同时执行初始化器。
  • 允许把声明当成一条语句使用的目的是尽量减少由未初始化变量造成的程序错误,并且让代码的局部性更好。在绝大多数情况下,如果没有为变量找到一个合适的值,暂时不要声明它。

9.4 选择语句

  • if语句和switch语句都需要首先检测一个值
  • 条件(condition)可能是一个表达式,也可能是一个声明。

9.4.1 if语句

  • 一个名字只能在声明它的作用域中使用。在if语句中,一个分支声明的名字不能在另一个分支中直接使用。
  • if语句的一个分支不能仅有一条声明语句,没有别的语句。例如
    1
    2
    3
    4
    5
    void f1(int i)
    {
    if (i)
    int x = i + 2; // 错误:if语句分支的声明
    }

9.4.2 switch语句

  • switch语句在一组候选项(case标签)中进行选择。case标签中出现的表达式必须是整型和枚举类型的常量表达式。在同一个switch语句中,一个值最多被case标签使用一次。
  • switch语句可以用一组if语句等价的替换。
  • 谨记switch语句的每一个分支都应该有一条结束语句,否则程序将会继续执行下一个分支的内容
  • 有一种好的解决办法:在那些我们确使希望继续执行下一个分支的地方加上注释,指明程序的意图。
  • 要想结束一个分支,最常用的是break语句,有时候也可以用return语句。
  • C++允许在switch语句的块内声明变量,但是不能不初始化。如果我们确使需要switch语句中使用变量,最好把该变量的声明和使用限定在一个块中。

9.4.3 条件中的声明

  • 要想避免不小心误用变量,最好的变法是把变量的作用域限定在一个较小的范围内。

9.5 循环语句

  • 循环语句能表示成for,while和do的形式

9.5.1 范围for语句

  • 最简单的循环是范围for语句,它使得程序员可以依次访问指定范围内的每个元素
  • for(int x : v)读作 对于范围v中的每个元素x。程序从头到尾依次访问v的全部元素。
  • 命名元素的变量的作用域是整个for语句。冒号之后的表达式必须是一个序列,换句话说,如果我们对它的调用v.begin()和v.end()或者begin(v)和end(v),得到的应该是迭代器。

9.5.2 for语句

  • 如果循环不符合 引入一个循环变量,检验条件,更新循环变量 的模式,则它更适合用while语句表示。

9.5.3 while语句

  • while语句重复执行它的受控语句直到条件部分变成false
  • 与for语句相比,while语句更适合处理以下两种情况
    • 一是没有一个明显的循环变量
    • 二是程序员觉得把负责更新循环变量的语句置于循环体内更自然。

9.5.4 do语句

  • do语句与while非常相似,唯一的区别就是do语句的条件位于循环体之后。

9.5.5 退出循环

  • break语句负责跳出最近的外层switch语句
  • 当我们需要中途离开循环体的时候,可以使用break语句。通常情况下,应该让完整退出的条件位于while语句和for语句的条件部分,只要这么做不会违背循环本身的逻辑就行。

9.6 goto语句

1
2
goto 标识符;
标识符: 语句
  • 标签的作用域是标签所处的函数。这意味着你能用goto从块的范围跳进跳出,唯一的限制是不能跳过初始化器或者跳入到异常处理程序中。
  • 在一般的代码中,goto可以用来跳出嵌套的循环或者switch(语句),这是他为数不多的有意义的用法之一。

9.7 注释与缩进

  • 如果语言本身能说清楚某件事,那就不要放在注释中,而应该让语句来完成。
  • 好注释负责指明一段代码应该实现什么功能(代码的意图),而代码本身负责完成该功能(完成的方式)。最好的方式是,注释的语言应该保持在一个较高层次的抽象水平上,这样便于人们理解而无需纠结过多技术细节。
  • 关于注释,我的习惯是
    • 在针对每个源文件的注释中指明:该文件中的声明有何共同点,对应的参考手册条目,程序员以及维护该文件所需的其他信息
    • 为每个类,模板和名字空间分别编写注释
    • 为每个非平凡的函数分别编写注释并指明:函数的目的,用到的算法,以及该函数对其应用环境所做的某些设定。
    • 为全局和名字空间内的每个变量及常量分别编写注释
    • 为某些不太明显或不可移植的代码编写注释
    • 其他情况,则几乎不需要注释了。

9.8 建议

  • 直到有了合适的初始值再声明变量
  • 如果可能的话,优先选用switch语句而非if语句。
  • 如果可能的话,优先选用范围for语句而非普通的for语句
  • 当没有明显的循环变量时,优先选用while语句而非for语句
  • 避免使用do语句
  • 避免使用goto语句
  • 注释应该简短直接
  • 代码能说清楚的事情就别放在注释中
  • 注释应该表明程序的意图
  • 坚持一种缩进风格,不要轻易改变

第十一章 选择适当的操作

11.1 其他运算符

  • 逻辑运算符,位逻辑运算符,条件表达式,递增递减运算符

11.1.1 逻辑运算符

  • 逻辑运算符 &&, ||, !接受算术类型以及指针类型的运算对象,将其转换为bool类型,最后返回一个bool类型的结果。

11.1.2 位逻辑运算符

  • 位逻辑运算符 &, |, ^, ~, >> , << 作用于整型对象,即 char, short, int ,long, long long 以及对应的unsigned版本,以及 bool, wchar_t, char16_t, char32_t等类型

11.1.3 条件表达式

  • 某些if语句可以改写成条件表达式(conditional-expression),例如
    1
    2
    3
    4
    if (a <= b)
    max = b;
    else
    max = a;
  • 这段代码可以更加直观的表示为
    1
    max = (a <= b) ? b : a;
  • 其中,条件部分的括号并非必须,但是加上后能使代码更易读
  • 条件表达式能用在常量表达式中,这一点非常重要
  • 此外,throw表达式也能作为条件表达式的一个分支。

11.1.4 递增与递减

  • ++x的值是x的新值(即,x递增之后的值)。例如,y = ++x 等价于 y = (x = x + 1)
  • 与之相反,x++的值是x的旧值。例如,y = x++ 等价于 y = (t = x, x = x + 1, t),其中,t是一个与x类型相同的变量

11.2 自由存储

  • 命名对象的生命周期由其作用域决定。然而,某些情况下我们希望对象与创建它的语句所在的作用域独立开来。例如,很多时候我们在函数内部创建了对象,并且希望在函数返回后仍能使用这些对象。
  • 运算符new负责创建这样的对象,运算符delete则负责销毁它们。new分配的对象 位于自由存储之上,或者说在堆上 ,在动态内存中

11.2.1 内存管理

  • 自由存储的问题主要包括

    • 对象泄露(leaked object): 使用new,但是忘了用delete释放掉分配的对象
    • 提前释放(premature deletion): 在尚有其他指针指向该对象并且后续仍会使用该对象的情况下过早的delete
    • 重复释放(double deletion): 同一对象被释放两次,两次调用对象的析构函数。
  • 重复释放的问题在于资源管理器通常无法追踪资源的所有者

  • 在一个规模较大的程序中要想确保准确释放掉分配的每一个对象(只释放一次且确保释放点正确)实在太难了。

  • 有两种方法可以避免上述问题,我建议程序员使用这两种方法代替裸new和delete

    • 除非万不得已不要把对象放在自由存储上,优先选用作用域内的变量
    • 当你在自由存储上构建对象时,把它的指针放在一个管理器对象(manager object,有时也称为句柄)中,此类对象通常含有一个析构函数,可以确保释放资源。尽可能让这个管理器对象作为作用域内的变量出现。很多习惯于使用自由存储的场合其实都可以用移动语义代替,只要从函数中返回一个表示大对象的管理器对象就可以了
  • 关于new和delete,我的经验是应该尽量确保没有裸new,即,令new位于构造函数或类似的函数中,delete位于析构函数中,由它们提供内存管理的策略。此外,new常用作资源句柄的实参。

11.2.2 数组

  • new还能用来创建对象的数组
  • 普通的delete用于删除单个对象,delete[]负责删除数组。

11.2.3 获取内存空间

  • 自由存储运算符new, delete, new[], delete[]的实现位于 头文件中

11.2.4 nothrow new

  • 有的程序不允许出现异常,此时,我们可以是使用nothrow版本的new和delete

11.3 列表

  • {}列表构建的是某种类型的对象,因此其中包含的元素数量和类型都必须符合构建该类型对象的要求。

11.4 lambda表达式

  • lambda表达式(lambda expression),有时也称为lambda函数。它是定义和使用匿名函数对象的一种简便的方式。在图形界面中,这样的操作常被称为回调(callback)

11.4.1 实现模型

  • lambda的主体部分变为了operator()()的函数体。因为lambda并不返回值,所以operator()()是void。默认情况下,operator()()是const,因此在lambda体内部无法修改捕获的变量,这也是目前为止最常见的情况。如果你希望在lambda的内部修改其状态,则应该把它声明为mutable。当然,此时对应的operator()()就不能声明为const了。
  • 我们把由lambda生成的类的对象称为闭包对象(closure object,或者简称为闭包)

11.4.3 捕获

  • lambda的主要用途是封装一部分代码以便于将其用作参数。

11.5 显式类型转换

  • C++提供了多种显式类型转换的操作:
    • 构造,使用{}符号提供对新值类型安全的构造
    • 命名的转换,提供不同等级的类型转换
      • const_cast,对某些声明为const的对象获得写入的权力
      • static_cast,反转一个定义良好的隐式类型转换
      • reinterpret_cast,改变位模式的含义
      • dynamic_cast,动态的检查类层次关系
    • C风格的转换,提供命名的类型转换或组合。
    • 函数化符号,提供C风格转换的另一种形式。

11.6 建议

  • 与后置++运算符相比,建议优先使用前置++运算符
  • 使用资源句柄避免泄露,提前删除和重复删除
  • 除非万不得已,否则不要把对象放在自由存储上;优先使用作用域内的变量
  • 避免使用裸new和裸delete
  • 使用RAII
  • 如果需要对操作添加注释,则应该选用命名的函数对象而非lambda
  • 如果操作具有一定的通用性,则应该选用命名的函数对象而非lambda
  • lambda应该尽量简短。
  • 处于可维护性和正确性的考虑,通过引用的方式捕获一定要慎之再慎
  • 让编译器推断lambda的返回类型
  • 用T{e}构造值
  • 避免显式类型转换
  • 当不得不使用显式类型转换时,尽量使用命名的转换
  • 对于数字类型之间的转换,考虑使用运行时检查的强制类型转换,例如narrow_cast<>

第十二章 函数

12.1 函数声明

  • 在C++程序中要想做点什么事,最好的办法是调用一个函数来完成它。定义函数的过程就是描述某项操作应该如何执行的过程。我们必须首先声明一个函数,然后才能调用它。
  • 函数声明负责指定函数的名字,返回值的类型以及调用该函数所需要的参数数量和类型。
  • 函数的类型即包括返回类型也包括参数的类型。对于类成员函数来说,类的名字也是函数类型的一部分。

12.1.1 为什么使用函数

  • 函数的一项重要作用,即,把一个复杂的运算分解为若干有意义的片段,然后分别为它们命名。
  • 我们希望代码是易于理解的,因为这是实现可维护性的第一步。
  • 关于函数的一条最基本的建议是应该令其规模较小,以便于我们一眼就能知悉该函数的全部内容。对于大多数程序员来说,函数的规模最好控制在大约40行内。

12.1.2 函数声明的组成要件

  • 函数声明除了指定函数的名字,一组参数以及函数的返回类型外,还可以包含多种限定符和修饰符。我们将其总结如下
    • 函数的名字,必选
    • 参数列表,可以为空,必选
    • 返回类型,可以是void,可以是前置或后置形式,必选
    • inline,表示一种愿望:通过内联函数体实现函数调用
    • constexpr,表示当给定常量表达式作为实参时,应该可以在编译时对函数求值
    • noexcept,表示该函数不允许抛出异常
    • 链接说明,例如static
    • [[noreturn]],表示函数不会用常规的调用/返回机制返回结果
  • 此外,成员函数还能被限定为
    • virtual,表示该函数可以被派生类覆盖
    • override,表示该函数必须覆盖基类中的一个虚函数
    • final,表示该函数不能被类覆盖
    • static,表示该函数不与某一特定的对象关联
    • const,表示该函数不能修改其对象的内容。
  • 函数声明成下面的形式
    1
    2
    3
    struct S {
    [[noreturn]] virtual inline auto f(const unsigned long int *const) -> void const noexcept;
    };

12.1.3 函数定义

  • 如果函数会在程序中调用,那么它必须在某处定义(只定义一次)。函数定义是特殊的函数声明,它给出了函数体的内容
  • 函数的定义以及全部声明必须对应同一类型。不过,为了与C语言兼容,我们会自动忽略参数类型的顶层const。例如,下面两条声明语句对应的是同一个函数
    1
    2
    void f(int);    // 类型是void(int)
    void f(const int); // 类型是void(int)
  • 函数的参数名字不属于函数类型的一部分,不同的声明语句中参数的名字无需保持一致。
  • 对于一条非定义的声明语句来说,为参数命名的好处是可以简化代码文档,但是我们不一定非得这么做。相反,我们通常通过不命名某个参数来表示该参数未在函数定义中使用。
    1
    2
    3
    4
    void search(table* t, const char* key, const char*)
    {
    // 未用到第三个参数
    }
  • 一般来说,未命名的参数有助于简化代码并提升代码的可扩展性。此时,尽管某些参数未被使用,但是为其预留位置可以确保函数的调用者不会受到未来函数变动的影响。
  • 除了函数以外,我们还能调用其他一些东西,它们遵循函数的大多数规则
    • 构造函数,constructor,严格来说不是函数,因为它没有返回值,可以初始化基类和成员,我们无法得到其地址
    • 析构函数,destructor,不能被重载,我们无法得到其地址
    • 函数对象,function object,不是函数,不能被重载,但是其operator()是函数
    • lambda表达式,lambda expression,是定义函数对象的一种简写形式。

12.1.4 返回值

  • 每个函数声明都包含函数的返回类型,除了构造函数和类型转换函数。

  • 传统上,在C和C++中,返回类型位于函数声明语句一开始的地方。然而,我们也可以在函数声明中把返回类型写在参数列表之后。例如

    1
    2
    std::string to_string(int a); // 前置返回类型
    auto to_string(int a) -> std::string; // 后置返回类型
  • 也就是说,前置的auto关键字表示函数的返回类型放在参数列表之后。后置返回类型由符号 -> 引导

  • 后置返回类型的必要性源于函数模板声明,因为其返回类型是依赖于参数的。例如

    1
    2
    template <class T, class U>
    auto product(const std::vector<T>& x, const std::vector<U>& y) -> decltype(x * y);
  • 如果函数调用了它自身,我们称之递归(recursive)

  • 函数可以包含多条return语句

  • 与参数传递的语义类似,函数返回值的语义也与拷贝初始化的语义一致。return语句初始化一个返回类型的变量。编译器检查返回表达式的类型是否与函数的返回类型吻合,并在必要时执行标准的或者用户自定义的类型转换。

12.1.7 [[noreturn]]函数

  • 形如[[…]]的概念被称为属性(attribute),属性可以置于C++语法的任何位置。通常情况下,属性描述了位于他前面的语法实体的性质,这些性质依赖于实现。
  • 此外,属性也能出现在声明语句开始的位置。C++只包含两个标准属性,[[noreturn]]和[[carries_dependency]]
  • 把[[noreturn]]放在函数声明语句的开始位置表示我们不希望该函数返回任何结果

12.1.8 局部变量

  • 定义在函数内部的名字通常称为局部名字(local name)。当线程执行到局部变量或者常量的定义出时,它们将被初始化。除非我们把变量声明为static,否则函数的每次调用都会拥有该变量的一份拷贝。
  • 相反,如果我们把局部变量声明为static,则在函数的所有调用中都将使用唯一的一份静态分配的对象,该对象在线程第一次到达它的定义处时被初始化。
  • static局部变量有一个非常重要的作用,即,它可以在函数的多次调用间维护一份公共信息而无须使用全局变量。如果使用了全局变量,则有可能会被其他不相干的函数访问甚至干扰。
  • 除非你进入了一个递归调用的函数或者发生了死锁,通常情况下,static局部变量的初始化不会导致数据竞争。也就是说,C++实现必须用某种无锁机制确保局部static变量的初始化被正确执行。递归的初始化一个局部static变量将产生未定义的结果。
  • static局部变量有助于避免非局部变量间的顺序依赖。

12.2 参数传递

  • 当程序调用一个函数时(使用后缀(),称为调用运算符 call operator或者应用运算符 application operator),我们为该函数的形参(formal arguments, 即 parameters)申请内存空间,并用实参(actual argument)初始化对应的形参。参数传递的语义与初始化的语义一致(严格地说是拷贝初始化)。
  • 编译器负责检查实参的类型是否与对应的形参类型吻合,并在非必要的时候执行标准类型转换或用户自定义的类型转换。除非形参是引用,其他情况下传入函数的是实参的副本。

12.2.1 引用参数

  • 引用传递的准确描述应该是左值引用传递,原因是函数不能接受一个右值引用作为它的参数
  • 该如何选择参数的传递方式?
    • 对于小对象使用值传递的方式
    • 对于你无需修改的大对象使用const引用传递
    • 如果需要返回计算结果,最好使用return而非通过参数修改对象
    • 使用右值引用实现移动和转发
    • 如果找不到合适的对象,则传递指针
    • 除非万不得已,否则不要使用引用传递
  • 在最后一条经验追责中,除非万不得已 是基于: 当我们需要修改对象的值时,传递指针比使用引用更容易表达清楚程序的原意

12.2.2 数组参数

  • 当数组作为函数的参数时,实际传入的是指向该数组首元素的指针
  • 数组类型的参数与指针类型的参数等价。

12.2.3 列表参数

  • 一个由{}限定的列表可以作为下述形参的实参
    • 类型std::initializer_list,其中列表的值能隐式的转换成T
    • 能用列表中的值初始化的类型
    • T类型数组的引用,其中列表的值能隐式的转换成T

12.2.4 数量未定的参数

  • 对于某些函数来说,很难明确指定调用时期望的参数数量和类型。要实现这样的接口,我们有三种选择
    • 使用可变模板:它允许我们以类型安全的方式处理任意类型,任意数量的参数。只要写一个小的模板元程序来解释参数列表的正确含义并采取适当的操作就可以了
    • 使用initializer_list作为参数类型。它允许我们以类型安全的方式处理某种类型的,任意数量的参数,在大多数上下文中,这种元素类型相同的参数列表是最常见和最重要的情形
    • 用省略号(…)结束参数列表,表示可能有更多的参数。它允许我们通过使用中的宏处理任意类型的,任意数量的参数。这种方案并非类型安全的,并且很难用于复杂的用户自定义类型。

12.2.5 默认参数

  • 一个通用的函数所需的参数通常比处理简单情况所需的参数要多。
  • 默认参数在函数声明时执行类型检查,在调用函数时求值。
  • 在同一个作用域的一系列声明语句中,默认参数不能重复或者改变。

12.3 重载函数

  • 大多数情况下我们应该给不同的函数起不一样的名字。但如果不同函数是在不同类型的对象上执行相同概念的任务,则给他们起同一个名字是更好的选择。为不同数据类型的同一种操作起相同的名字称为重载(overloading)
  • 对于编译器来说,同名函数唯一的共同点就是名字相同。
  • 模板为定义成组的重载函数提供了一种系统的方法

12.3.1 自动重载解析

  • 重载解析与函数声明的次序无关

12.3.2 重载与返回类型

  • 在重载解析过程中不考虑函数的返回类型,这样可以确保对运算符或者函数调用的解析独立于上下文

12.3.3 重载与作用域

  • 重载发生在一组重载函数集的成员内部,也就是说,重载函数应该位于同一个作用域内。不同的非名字空间作用域中的函数不会重载。
  • 基类和派生类提供的作用域不同,因此默认情况下基类函数和派生类函数不会发生重载
  • 如果我们希望实现跨类作用域或者名字空间作用域的重载,应该使用using声明或者using指示。

12.4 前置与后置条件

  • 我们把函数调用时应该遵循的约定称为前置条件(precondition),把函数返回时应该遵循的约定称为后置条件(postcondition)

12.5 函数指针

  • 与数据对象类似,由函数体生成的代码也置于某块内存区域中,因此它也有自己的地址。
  • 出于某些考虑,有的与机器体系结构有关,有的与系统设计有关,我们不允许函数指针修改所指的代码。
  • 程序员只能对函数做两种操作:调用它或者获取它的地址。通过获取函数地址得到的指针能被用来调用该函数。
  • 对于直接调用函数和通过指针调用函数这两种情况来说,参数传递的规则是一样的。
  • 函数指针为算法的参数化提供了一种途径。

12.6 宏

  • 宏在C语言中非常重要,但在C++中的作用就小得多了。关于宏的最重要的原则是:除非万不得已,否则不要使用宏。

12.6.1 条件编译

12.6.2 预定义宏

  • 编译器预定义了一些宏
    • __cplusplus: 在C++编译器中有定义(C语言编译器没有)。在C++11程序中它的值是201103L
    • DATA: “yyyy:mm:dd”格式的日期。
    • TIME: “hh:mm:ss”格式的时间
    • FILE: 当前源文件的名字
    • LINE: 当前源代码的代码行数
    • FUNC: 是一个由具体实现定义的C风格字符串,表示当前函数的名字
    • STDC_HOSTED: 如果当前实现是宿主式的,则为1;否则为0
    • STDC: 在C语言编译器中有定义(C++编译器中没有)

12.6.3 编译指令

  • 如果可能,尽量避免使用#pragma

12.7 建议

  • 把有用的操作打包在一起构成函数,然后认真起个名字
  • 一个函数应该对应逻辑上的一个操作
  • 让函数尽量短
  • 不要返回指向局部变量的指针或者引用
  • 如果函数必须在编译时求值,把他声明成constexpr
  • 如果函数无法返回结果,把他设置为 [[noreturn]]
  • 对小对象使用传值的方式
  • 如果你想传递无需修改的大值,使用传const引用的方式
  • 尽量通过return值返回结果,不要通过参数修改对象
  • 用右值引用实现移动和转发
  • 如果找不到合适的对象,可以传入指针
  • 除非万不得已,否则不要传递非const引用
  • const的用处广泛,程序元应该多用
  • 我们认为char或者const char参数指向的是C风格字符串
  • 避免把数组当成指针传递
  • 用initializer_list传递元素类型相同但是元素数量未知的列表
  • 避免使用数量未知的参数(…)
  • 当几个函数完成的功能在概念上一致,仅仅是处理的类型有区别时,使用重载。
  • 在整数类型上重载时,提供一些函数以消除二义性
  • 为你的函数指定前置条件和后置条件
  • 与函数指针相比,优先使用函数对象和虚函数
  • 不要使用宏
  • 如果必须使用宏,一定要用很多大写字母组成宏的名字,尽管这样的名字看起来会很丑陋

第十三章 异常处理

13.1 错误处理

  • 本章介绍如何用异常进行错误处理。我们必须基于一定的策略综合运用各项语言机制才能高效的处理错误。
  • 本章的内容主要分为两个部分
    • 一是异常安全保障(exception-safety guarantee),它是程序从运行时错误中快速恢复的关键;
    • 另一个是使用构造函数和析构函数进行资源管理的资源获取即初始化(Resource Acquisition Is Initialization, RAII)技术。
  • 因为异常安全保障和资源获取即初始化都依赖于不变式的规范,所以本章也会介绍一些关于强制断言的内容。

13.1.1 异常

  • 异常(exception)的概念可以帮助我们将信息从检测到错误的地方传递到处理该错误的地方。如果函数无法处理某个问题,则抛出异常,并且寄希望于函数的调用者能直接或者间接的处理该问题。函数如果希望处理某个问题,可以捕获(catch)相应的异常。
    • 主调组件如果想处理某些失败的情形,可以把这些异常置于try块的catch从句中
    • 被调组件如果无法完成既定的任务,可以用throw表达式抛出一个异常来说明这一情况。

13.1.2 传统的错误处理

  • 当函数检测到某个无法局部处理的问题并且必须向函数的调用者报告时,除了使用异常机制处理该错误,其他几种传统的处理方式都有各自的不足
    • 终止程序,这是一种非常极端的处理方式
    • 返回错误值。
    • 返回合法值,而程序却处于错误状态。问题是主调函数可能没有意识到程序已经处于错误状态了。例如,许多标准C语言库函数设置一个非局部变量error来表示错误。在应用并发机制时,用非局部变量记录错误状态的做法不太奏效
    • 调用错误处理函数。

13.1.3 渐进决策

  • 在异常处理模式中,对于未处理的错误(未捕获的异常)的最终响应是终止程序。

13.1.4 另一种视角看异常

  • C++的异常处理机制主要用于处理那些无法在局部范围内解决的问题。

13.7 建议

  • 在设计初期尽早确定异常处理策略
  • 当无法完成既定任务时抛出异常
  • 用异常机制处理错误
  • 不要试图捕获每个函数的每个异常
  • 抛出异常前先释放局部资源
  • 尽量减少使用try块
  • 不要让析构函数抛出异常
  • 把普通代码和异常处理代码分离开来

第十四章 名字空间

14.1 组合问题

  • 任何实际问题都是由若干独立部分组成的。函数和类提供了相对细粒度的关注点分离,而库,源文件和编译单元则提供了粗粒度的分离。
  • 逻辑上最理想的方式是模块化,即独立的事物保持分离,只允许通过良好定义的接口访问模块。
  • C++并不是通过单一语言特性来支持模块的概念,也并不存在模块这种语法构造。取而代之,C++通过其他语言特性(函数,类和名字空间)的组合和源码的组织来表达模块化。

14.2 名字空间

  • 名字空间(namespace)的概念用来直接表示本属一体的一组特性,例如库代码。
  • 一个名字空间应该表达某种逻辑结构:一个名字空间中的声明应该一起提供一些特性,使得用户看来它们是一个整体,而且能反应一组共同的设计策略。
  • 实际上,在一个名字空间中声明的实体是作为名字空间的成员被引用的。
  • 从名字空间外引用成员的其他方法包括using声明,using指示和参数依赖查找。

14.2.1 显式限定

  • 我们可以在名字空间的定义中声明一个成员,稍后用 名字空间名::成员名 的语法定义它
  • 一个名字空间形成一个作用域,通常的作用域规则也适用于名字空间。因此,名字空间是一个非常基础,非常简单的概念。程序规模越大,用名字空间表达程序的逻辑划分就越有用。全局作用域也是一个名字空间,可以显式的用 :: 来引用。例如
    1
    2
    3
    4
    5
    6
    7
    8
    int f();  // 全局函数

    int g()
    {
    int f; // 局部变量:屏蔽了全局函数
    f(); // 错误:不能调用一个整型变量
    ::f(); // 正确:调用全局函数
    }
  • 类也是名字空间

14.2.2 using声明

  • using声明将一个代用名引入了作用域,最好尽量保持代用名的局部性以避免混淆
  • 当用于一个重载的名字时,using声明会应用于其所有重载版本。例如
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    namespace N{
    void f(int);
    void f(string);
    };

    void g()
    {
    using N::f;
    f(789); // N::f(int)
    f("Bruce") // N::f(string)
    }

14.2.3 using指示

  • 在一个函数中,可以安全的使用using指示以方便符号表示,但是对全局using指示必须小心谨慎,因为过度使用using指示会导致名字冲突,而避免名字冲突恰恰是引入名字空间的目的。

14.2.5 名字空间是开放的

  • 名字空间是开放的:即,你可以从多个分离的名字空间声明中向一个名字空间添加名字。例如
    1
    2
    3
    4
    5
    6
    7
    8
    9
    namespace A 
    {
    int f();
    }

    namespace A
    {
    int g();
    }
  • 这样,名字空间的成员就不需要连续放置在单一的文件中。

14.5 建议

  • 用名字空间表达逻辑结构
  • 将除main()之外的所有非局部名字都置于名字空间中
  • 设计一个名字空间,以便能方便的使用它避免意外访问到不相关的名字空间
  • 不要为名字空间起非常短的名字
  • 如必要,使用名字空间别名为长名字空间提供简写
  • 不要给你的名字空间的使用者增加太多符号表示上的负担
  • 为接口和实现使用分离的名字空间
  • 当定义名字空间成员时使用 Namespace::member 表示方式
  • 用inline名字空间支持版本控制
  • 将using指示用于代码转换,用于基础库以及用于局部作用域
  • 不要将using指示放在头文件中。

第十五章

15.1 分离编译

  • 任何实际程序都由很多逻辑上分离的部分组成。为了更好的管理这些组成部分,我们可以将程序表示为一组源码文件,其中每个文件包含一个或多个逻辑组件。
  • 我们的任务是为程序设计一个文件集合,使得能以一种一致,易理解和灵活的方式表示这些逻辑组件。特别是,我们以接口(例如函数声明)与实现(例如函数定义)的完全分离为目标。
  • 当用户将一个源文件(source file)提交给编译器后,首先对文件进行预处理,即,处理宏以及将#include指令指定的头文件包含进来。预处理的结果称为编译单元(translation unit)。编译单元是编译器真正处理的内容,也是C++语言规则所描述的内容。
  • 链接器是将分离编译的多个部分绑定在一起的程序。编译器有时也被称为加载器(loader)。链接可以在程序开始运行前全部完成,但也可以在程序运行中将新代码添加进来–动态链接。
  • 程序的源文件组织通常称为其物理结构(physical structure)。程序的逻辑结构和物理结构不必相同。

15.2 链接

  • 除非已显式声明为局部名字,否则函数名,类名,模板名,变量名,名字空间名,枚举名以及枚举值名的使用必须跨所有编译单元保持一致。
  • 程序员必须保证每个名字空间,类,函数等必须在其出现的每个编译单元中都正确声明,且对应相同实体的声明都是一致的。
  • 对象在程序中只能定义一次,它可以声明很多次,但类型必须完全一致。
  • 如果全局作用域中或名字空间中的变量定义不带初始值,则该变量会使用默认初始值。非static局部变量或创建在自由存储上的对象则不会使用默认初始值。
  • 在类体外,实体必须先声明后使用。
  • 如果一个名字在其定义处之外的编译单元中也可以使用,我们称其具有外部链接(external linkage)。
  • 如果一个名字只能在其定义所在的编译单元中被引用,我们称其具有内部链接(internal linkage)
  • 在名字空间作用域,包括全局作用域中使用关键字static表示 不能再其他源文件中访问,即内部链接。
  • 关键字const按时默认内部链接。
  • 链接器看不到的名字,例如局部变量名,被称为无链接。
  • 默认情况下,名字空间中的const对象,constexpr对象,类型别名以及任何声明为static的实体都具有内部链接。
  • 为确保一致性,应该将别名,const对象,constexpr对象和inline函数放置在头文件中
  • 我们可以通过显式声明为一个const对象赋予外部链接。

15.2.1 文件内名字

  • 我们一般最好避免使用全局变量,因为这会引起维护问题。将变量放在名字空间中会有些帮助,但仍可能引起数据竞争
  • 如果必须使用全局变量,至少应限制它们只在单一源文件中使用,有两种方法实现这种限制
    • 将声明放在无名名字空间中
    • 声明实体时使用static
  • 使用无名名字空间可以令名字成为编译单元的局部名字。无名名字空间的效果非常像内部链接。

15.2.2 头文件

  • 同一个对象,函数,类等所有声明都要保持类型一致。因此,提交给编译器并随后链接在一起的源码必须保持一致。实现不同编译单元声明一致性的一种不完美但是很简单的方法是:在包含可执行代码或数据定义的源文件中 #include 包含接口信息的头文件
  • #include 机制是一种文本处理方式–将源程序片段收集起来形成单一的编译单元(文件)
  • 建议将简单常量定义放在头文件中,但不将集合定义放在头文件中,其原因是C++实现很难避免多个编译单元中重复的集合定义。
  • 使用 #include 时过分卖弄聪明是不明智的。我的建议是
    • 只 #include 头文件。不要#include包含变量定义和非inline函数的普通源码
    • 只 #include 完整的声明和定义
    • 只在全局作用域,链接说明块及名字空间定义中 #include 头文件
    • 将所有#include放在其他代码之前,以尽量减少无意造成的依赖关系
    • 避免使用宏技巧
    • 尽量减少在头文件中使用非局部的名字

15.2.3 单一定义规则

  • 每个给定类,枚举和模板等在程序中只能定义一次

15.2.4 标准库头文件

  • 标准库特性是通过一组标准库头文件提供的。

15.2.5 链接非C++代码

  • 因为C和C++关系紧密,extern “C” 指示特别有用。需要注意的是,extern “C”中的C表示的是链接规范而非语言。extern “C” 通常用于将函数链接到恰好符合C实现规范的Fortran和汇编程序。

15.4 程序

  • 一组分离编译的单元经由链接器组合就形成了程序。其中用到的每个函数,对象,类型等都必须是唯一定义的。一个程序必须恰好包含一个名为main()的函数。通过调用全局函数main()开始执行程序的主要计算任务,从main()返回后程序就终止了。main()的返回类型是int,所有C++实现都支持下面两个版本的main():
    1
    2
    int main() {}
    int main(int argc, char* argv[]) {}
  • main()返回的int作为程序执行的结果被传递给调用main()的系统,非零返回值表示发生了一个错误。

15.4.1 非局部变量初始化

  • 原则上,定义在任何函数之外的变量(即,全局变量,名字空间变量以及类static变量)在main()被调用前初始化。

15.4.3 程序终止

  • 程序终止的方式有很多种
    • 从main()返回
    • 调用exit()
    • 调用abort()
    • 抛出一个未捕获的异常
    • 违反noexcept
    • 调用quick_exit()
  • 如果使用标准库函数exit()终止一个程序,则会调用已构造的静态对象的析构函数。但是,如果程序是使用标准库函数abort()终止的,析构函数就不会被调用。
  • 注意,这意味着exit()不会立即终止程序。在一个析构函数中调用exit()会导致无限递归。
  • 函数exit(),abort(),quick_exit(),atexit(),at_quick_exit()都是在中声明的。

15.5 建议

  • 用头文件表达接口,强调逻辑结构
  • 在实现函数的源文件中 #include 声明函数的头文件
  • 不要在不同编译单元中定义同名但含义相近却不完全一致的全局实体
  • 不要再头文件中定义非内联函数
  • 只在全局作用域和名字空间中使用 #include
  • 只 #include 完整的声明
  • 使用包含保护
  • 在名字空间中 #include C头文件以避免全局名字
  • 令头文件自包含
  • 区分用户接口和实现者接口
  • 区分一般用户接口和专家用户接口
  • 若代码是用作非C++程序的一部分,则应避免需要运行时初始化的非局部对象。

第三部分 抽象机制

  • 这一部分介绍定义和使用新类型的C++特性,主要介绍通常称为面向对象程序设计和泛型程序设计的技术。

第十六章 类

16.1 引言

  • C++类是创建新类型的工具,创建出的新类型可以像内置类型一样方便的使用。而且,派生类和模板允许程序员表达类之间的关系并利用这种关系。
  • 一个类型就是一个概念,一个思想,一个观念等等的具体表示
  • 类是用户自定义类型。如果一个概念没有与之直接对应的内置类型,我们就定义一个新类型来表示它。
  • 定义新类型的基本思想是将实现的细节与正确使用它的必要属性分离。这种分离的最佳表达方式是:通过一个专用接口引导数据结构及其内部辅助例程的使用。

16.2 类基础

  • 下面是类的简要概括
    • 一个类就是一个用户自定义类型
    • 一个类由一组成员构成。最常见的成员类别是数据成员和成员函数
    • 成员函数可定义初始化,拷贝,移动和清理等语义
    • 对对象使用 . 访问成员,对指针使用 -> 访问成员
    • 可以为类定义运算符
    • 一个类就是一个包含其成员的名字空间
    • public成员提供类的接口,private成员提供实现细节
    • struct是成员默认为public的class

16.2.1 成员函数

  • 声明于类定义内的函数称为成员函数(member function),对恰当类型的特定变量使用结构成员访问语法才能调用这种函数。
  • 由于不同结构可能有同名成员函数,在定义成员函数时必须指定结构名
  • 在成员函数中,不必显式引用对象即可使用成员的名字.在此情况下,名字所引用的是调用函数的对象的成员.

16.2.2 默认拷贝

  • 默认情况下,对象是可拷贝的.特别是,一个类对象可以用同类的另一个对象的副本来进行初始化.例如:
    1
    2
    Data d1 = my_birthday;
    Date d2{my_birthday};
  • 默认情况下,一个类对象的副本是对每个成员逐个拷贝得到的.
  • 类似的,类对象默认也可以通过赋值操作拷贝.
  • 再重复一边,默认的拷贝语义是逐成员复制的.如果对于类这不是正确的选择,用户可以定义一个恰当的赋值运算符

16.2.3 访问控制

  • 标签public将类的主体分为两部分.
    • 第一部分中的名字是私有的(private),它们只能被成员函数使用.
    • 第二部分是公有的(public),构成类对象的公共接口.
  • struct就是一个成员默认为公有的class,成员函数的声明和使用是一样的.
  • 但是,非成员函数禁止使用私有成员.

16.2.4 class和struct

  • 下面的语法结构
    • class X {…};
  • 称为类定义(class definition),它定义了一个名为X的类型.由于历史原因,类定义常常被称为类声明.
  • 如果我认为一个类是简单数据结构,更喜欢使用struct.
  • 如果我认为一个类是 具有不变式的真正类型,会使用class.
  • C++并不要求在类定义中首先声明数据.实际上,将数据成员放在最后以强调提供公共用户接口的函数(位置在前)通常是很有意义的.例如
    1
    2
    3
    4
    5
    6
    7
    8
    class Date3 
    {
    public:
    Date3(int dd, int mm, int yy);
    void add_year(int n);
    private:
    int d, m, y;
    }

16.2.5 构造函数

  • 构造函数的本质是构造一个给定类型的值.构造函数的显著特征是与类具有相同的名字.
  • 如果一个类有一个构造函数,其所有对象都会通过调用构造函数完成初始化.如果构造函数需要参数,在初始化时就要提供这些参数.
  • 由于构造函数定义了类的初始化方式,因此我们可以使用{}初始化记法
  • 我建议优先使用{}记法而不是(),因为前者明确表明了要做什么(初始化),从而避免了某些潜在错误.而且可以一致的使用.
  • 通过提供多个构造函数,可以为某类型的对象提供多种不同的初始化方法.
  • 构造函数的重载规则与普通函数相同.只要构造函数的参数类型明显不同,编译器就能选择正确的版本使用.
  • 注意,通过确保对象的正确初始化,构造函数极大的简化了成员函数的实现.有了构造函数,其他成员函数就不再需要处理未初始化数据的情况.

16.2.6 explicit构造函数

  • 默认情况下,用单一参数调用一个构造函数,其行为类似于从参数类型到类自身类型的转换.
  • 我们可以指定构造函数不能用作隐式类型转换.如果构造函数的声明带有关键字explicit,则它只能用于初始化和显示类型转换.
  • 用=进行初始化可看作拷贝初始化(copy initialization).一般来说,初始化器的副本会被放入待初始化的对象.
  • 省略=会将初始化变为显式初始化.显式初始化也称为直接初始化(direct initialization)
  • 默认情况下,应该将单参数的构造函数声明为explicit.
  • 如果一个构造函数声明为explicit且定义在类外,则在定义中不能重复explicit
    1
    2
    3
    4
    5
    6
    7
    8
    9
    class Date 
    {
    int d, m, y;
    public:
    explicit Date(int dd);
    };

    Date::Date(int dd) {...} // 正确
    explicit Date(int dd) {...} // 错误
  • 大多数explicit起很重要作用的构造函数都接受单一参数.但是,explicit也可用于无参或多个参数的构造函数.
  • 列表初始化也存在直接初始化和拷贝初始化的区别.

16.2.7 类内初始化器

  • 当使用多个构造函数时,成员初始化可以是重复的。

16.2.8 类内函数定义

  • 如果一个函数不仅在类中声明,还在类中定义,那么它就被当作内联函数处理,即很少修改且频繁使用的小函数适合类内定义。
  • 类成员可以访问同类的其他成员,而不管成员是在哪里定义的。即函数和数据成员的声明是不依赖于顺序的。

16.2.13 成员类型

  • 类型和类型别名也可以作为类的成员。
  • 成员类可以引用其所属类的类型和static成员。当给定所属类的一个对象时,只能引用非static成员。

16.3 具体类

  • C++直接支持一部分抽象作为其内置类型。但是,大多数抽象并不直接支持 。
  • 如果一个类的表示是其定义的一部分,我们就称它是具体的(concrete,或者称它是一个具体类)。这将与抽象类区分开来,后者为多种实现提供一个公共接口。在定义中明确类的表示方式令我们能:
    • 将对象置于栈,静态分配的内存以及其他对象中
    • 拷贝和移动对象
    • 直接引用具名对象

16.3.2 辅助函数

  • 一般而言,一个类都会有一些无须定义在类内的关联函数,因为它们不需要直接访问类的表示。

16.3.3 重载运算符

  • 添加一些函数使得用户自定义类型能使用人们习惯的符号通常是很有用的
  • 注意,赋值和拷贝初始化是默认提供的。

16.3.4 具体类的重要性

  • 具体类的使用就像内置类型一样。具体类型也称为值类型(value type),使用它们编程称为面向值的程序设计。
  • 一个具体类型的目标是高效的做好一件相对简单的事情,为用户提供修改其行为的特性通常不是其目标。特别是,展现运行时多态行为也不是其意图。

16.4 建议

  • 将概念表示为类
  • 将类的接口与实现分离
  • 仅当数据真的仅仅是数据且数据成员不存在有意义的不变式时才使用共有数据(struct)
  • 定义构造函数来处理对象初始化
  • 默认将单参数构造函数声明为explicit
  • 将不修改其对象状态的成员函数声明为const
  • 具体类型是最简单的类。只要适用,就应该优先选择具体类型而不是更复杂的类或普通数据结构。
  • 仅当函数需要直接访问类的表示时才将其实现为成员函数。
  • 使用名字空间建立类与其辅助函数间的显示关联
  • 将不修改对象值的成员函数定义为const成员函数。
  • 若一个函数需要访问类的表示,但并不需要用某个具体对象来调用,建议将其实现为static成员函数

第十七章 构造,清理,拷贝和移动

17.1 引言

  • 本章主要介绍与对象的生命周期有关的技术:
    • 我们如何创建对象
    • 如何拷贝对象
    • 如果移动对象
    • 在对象销毁时如何进行清理工作
  • 移动和拷贝的区别在于,拷贝操作后两个对象具有相同的值,而移动操作后移动源不一定具有原始值。如果源对象在操作后不再使用,我们就可以使用移动操作。
  • 一个对象在6中情况下会被拷贝或移动
    • 作为赋值操作的源
    • 作为一个对象初始化器
    • 作为一个函数实参
    • 作为一个函数返回值
    • 作为一个异常
  • 在所有这些情况下,都会应用拷贝或移动构造函数
  • 除了初始化具名对象和自由存储上的对象,构造函数还用来初始化临时对象。

17.2 构造函数和析构函数

  • 我们可以通过定义一个构造函数来指出一个类的对象应如何初始化。与构造函数对应,我们还可以定义一个析构函数来确保对象销毁时进行恰当的清理操作。

17.2.1 构造函数和不变式

  • 与类同名的成员称为构造函数。构造函数的声明指出其参数列表,但未指出返回类型。
  • 构造函数的任务是初始化该类的一个对象。一般而言,初始化操作必须建立一个类不变式,所谓不变式就是当成员函数(从类外)被调用时必须保持的某些东西
  • 为什么应该定义一个不变式呢?这是为了
    • 聚焦于类的设计工作上
    • 理清类的行为
    • 简化成员函数的定义
    • 理清类的资源管理
    • 简化类的文档
  • 通常,设计不变式最终会节省我们的总工作量

17.2.2 析构函数和资源

  • 构造函数初始化对象。换句话说,它创建供成员函数进行操作的环境。
  • 这种基于构造函数/析构函数的资源管理风格被称为资源获取即初始化或简称RAII
  • 一对匹配的构造函数/系统函数是C++中实现可变大小对象的常用机制。

17.2.3 基类和成员析构函数

  • 构造函数和析构函数可以很好的与类层次配合。构造函数会自顶向下的创建一个类对象
    • 首先,构造函数调用其基类的构造函数
    • 然后,它调用成员的构造函数
    • 最后,它执行自身的函数体。
  • 析构函数则按相反顺序拆除一个对象。
    • 首先,析构函数执行自身的函数体
    • 然后,它调用其成员的析构函数
    • 最后,它调用其基类的析构函数。
  • 特别是,一个virtual基类必须在任何可能使用它的基类之前构造。

17.2.4 调用构造函数和析构函数

  • 当对象退出作用域或被delete释放时,析构函数会被隐式调用。显式调用析构通常是不必要的,而且会导致严重的错误。

17.3 类对象初始化

  • 本节讨论如何初始化一个类的对象,分使用构造函数和不适用构造函数两种情况讨论

17.3.1 不适用构造函数进行初始化

  • 我们不能为内置类型定义构造函数,但能用一个恰当类型的值初始化内置类型对象。例如
    1
    2
    int a{1};
    char* p{nullptr};
  • 类似的,我们可以用下列方法初始化一个无构造函数的类的对象
    • 逐成员初始化
    • 拷贝初始化
    • 默认初始化,不用初始化器或空初始化列表

17.3.2 使用构造函数进行初始化

  • 当逐成员拷贝不能满足需求时,我们可以定义构造函数来初始化对象。特别是,构造函数常用来建立类的不变式并获取必要的资源。
  • 注意,当定义了一个接受参数的构造函数后,默认构造函数就不存在了
  • 我使用{}语法来明确表示正在进行初始化,而不仅仅是在赋值,调用函数或是声明函数.只要是在构造对象的地方,我们都可以用{}初始化语法为构造函数提供参数.
  • 处于这个原因,{}初始化有时也称为通用(universal)初始化:
    • 这种语法可以在任何地方,
    • 而且,{}初始化还是一致的:无论你在哪里使用语法{v}将类型X的对象初始化为值v,都会创建相同的值
  • 与{}相反,=和()初始化语法不是通用的.
  • 注意,{}初始化器语法不允许窄化转换.这是我们更倾向于使用{}风格而不是()或=的另一个原因.

17.3.2.1 用构造函数进行初始化

  • 使用 ()语法, 可以请求在初始化过程中使用一个构造函数.即,对一个类,你可以保证用构造函数进行初始化而不会进行{}语法也提供的逐成员初始化或初始化器列表初始化.
  • {}初始化的一致使用自C++11起才成为现实

17.3.3 默认构造函数

  • 无参的构造函数被称为默认构造函数.
  • 如果构造对象时未指定参数或提供了一个空初始化器列表,则会调用默认构造函数.
  • 内置类型被认为具有默认构造函数和拷贝构造函数.但是,对于内置类型的未初始化的非static变量,其默认构造函数不会被调用.内置整数类型的默认值为0,浮点类型的默认值为0.0,指针类型的默认值为nullptr
  • 引用和const必须被初始化.

17.3.4 初始化器列表构造函数

  • 接受单一std::initializer_list参数的构造函数被称为初始化器列表构造函数.
  • 一个初始化器列表构造函数使用一个{}列表作为其初始化值来构造对象.
  • 标准库容器都有初始化器列表构造函数,初始化器列表赋值运算符等成员.
  • 我们想要使用接受一个{}列表进行初始化的机制,就要定义一个接受std::initializer_list类型参数的函数,通常是一个构造函数.

17.3.4.1 initializer_list构造消除歧义

  • 如果一个类已有多个构造函数,则编译器会使用常规的重载解析规则根据给定参数选择一个正确的构造函数.当选择构造函数时,默认构造函数和初始化器列表构造函数优先.具体规则如下
    • 如果默认构造函数或初始化器列表构造函数都匹配,优先选择默认构造函数
    • 如果一个初始化器列表构造函数和一个普通构造函数都匹配,优先选择列表初始化器构造函数.

17.3.4.2 使用initializer_list

  • 可以将接受一个initializer_list参数的函数作为一个序列来访问,即,通过成员函数begin(), end()和size()访问.
  • 不幸的是,initializer_list不提供下标操作
  • initializer_list是以传值方式传递的.这是重载解析规则所要求的,而且不会带来额外开销,因为一个initializer_list对象只是一个小句柄,通常是两个字大小,指向一个元素类型为T的数组
  • initializer_list的元素是不可变的,不要考虑修改它们的值

17.3.4.3 直接和拷贝初始化

  • {}初始化也存在直接初始化和拷贝初始化的区别.对一个容器来说,这意味着这种区别对容器自身及其中的元素都有作用:
    • 容器的初始化器列表构造函数可以是explicit,也可以不是
    • 初始化器列表的元素类型的构造函数可以是explicit,也可以不是.

17.4 成员和基类初始化

  • 构造函数可以建立不变式并获取资源.一般而言,构造函数是通过初始化类成员和基类来完成这些工作的.

17.4.1 成员初始化

  • 在构造函数的定义中,通过成员初始化器列表给出成员的构造函数的参数.例如
    1
    2
    3
    4
    5
    Club::Club(const string& n, Data fd)
    : name{n}, members{}, officers{}, founder{fd}
    {

    }
  • 成员初始化器列表以一个冒号开始,后面的成员初始化器用逗号间隔.
  • 类自身的构造函数在其函数体执行之前会先调用成员的构造函数.
  • 成员的构造函数按成员在类中声明的顺序调用,而不是按成员在初始化器中列表中出现的顺序.
  • 为了避免混淆,最好按成员的声明顺序指明初始化器.
  • 一个构造函数可以初始化其类的成员和基类,但不会初始化其成员或基类的成员或基类.

17.4.2 基类初始化器

  • 派生类的基类的初始化方式与非数据成员相同.即,如果基类要求一个初始化器,我们就必须在构造函数中提供相应的基类初始化器.
  • 与成员初始化类似,基类按声明顺序进行初始化,建议按此顺序指定基类的初始化器.基类的初始化在成员之前,销毁在成员之后.

17.4.3 委托构造函数

  • 如果你希望两个构造函数做相同的操作,可以重复代码,也可以定义一个 init()函数 来执行两者相同的操作.
  • 一种替代方法是用一个构造函数定义另一个
    1
    2
    3
    4
    5
    6
    7
    8
    class X 
    {
    int a;
    public:
    X(int x) {if (0 < x && x <= max) a = x; else throw Bad_X(x);}
    X() : X(24){}
    X(string s):X(to<int>(s)){}
    }
  • 即,使用一个成员风格的初始化器,但用的是类自身的名字(也是构造函数名),它会调用另一个构造函数,作为这个构造过程的一部分.这样的构造函数称为委托构造函数(delegating constructor,有时也称为转发构造函数, forwarding constructor)

17.4.4 类内初始化器

  • 我们可以在类声明中为非static数据成员指定初始化器.例如
    1
    2
    3
    4
    5
    6
    class A 
    {
    public:
    int a {77};
    int b = 88;
    };
  • 处于语法分析和名字查找相关的很隐蔽的技术原因,{}和=语法能用于类内成员初始化器,但是 ()语法就不行
  • 一个类内初始化器可以使用它的位置所在作用域中的所有名字.

17.4.5 static成员初始化

  • 一个static类成员是静态分配的,而不是每个类对象的一部分.一般来说,static成员声明充当类外定义的声明.例如
    1
    2
    3
    4
    5
    class Node 
    {
    static int node_count; // 声明
    };
    int Node::node_count = 0; // 定义
  • 但是,在少数简单的特殊情况下,在类内声明中初始化static成员也是可能的.条件是 static成员必须是整型或枚举类型的const,或字面值类型的constexpr,且初始化器必须是一个常量表达式.例如
    1
    2
    3
    4
    5
    6
    7
    8
    9
    class Curious 
    {
    public:
    static const int c1 = 7; // 正确
    static int c2 = 11; // 错误:非const
    const int c3 = 13; // 正确,但非static
    static const int c4 = sqrt(9); // 错误:类内初始化器不是常量
    static const float c5 = 7.0; // 错误:类内初始化成员不是整型,应该使用constexpr而非const
    };
  • 当且仅当你使用一个已初始化成员的方式要求它像对象一样在内存中存储时,该成员必须在某处定义.初始化器不能重复;
  • 成员常量的主要用途是为类声明中其他地方用到的常量提供符号名称.

17.5 拷贝和移动

  • 当我们需要从a到b传输一个值的时候,通常有两种逻辑上不同的方法
    • 拷贝(copy)是 x = y 的常规含义:即,结果是x和y的值都等于赋值前y的值
    • 移动(move)将x变为y的旧值,y变为某种移出状态(moved-from state).我们最感兴趣的情况–容器,移出状态就是 空
  • 一般来说,移动操作不能抛出异常,而拷贝操作则可以.因为拷贝可能需要获取资源,移动操作通常比拷贝操作更高效.
  • 为了避免乏味的重复性工作,拷贝和移动操作都有默认定义

17.5.1 拷贝

  • 类X的拷贝操作有两种

    • 拷贝构造函数: X(const X&)
    • 拷贝赋值运算符: X& operator=(const X&)
  • 拷贝构造函数与拷贝赋值运算符的区别在于前者初始化一片未初始化的内存,而后者必须正确处理目标对象已构造并可能拥有资源的情况.

  • 从拷贝的目的来看,一个基类就是一个成员:为了拷贝派生类的一个对象,你必须拷贝其基类

17.5.2 移动

  • 移动赋值背后的思想是将左值的处理与右值的处理分离:拷贝赋值操作和拷贝构造函数接受左值,而移动赋值操作和移动构造函数则接受右值。对于return值,采用移动构造函数。

17.6 生成默认操作

  • 编写拷贝操作,析构函数这样的常规操作会很乏味也容易出错,因此需要时编译器可为我们生成这些操作。默认情况下,编译器会为一个类生成
    • 一个默认构造函数:X()
    • 一个拷贝构造函数: X(const X&)
    • 一个拷贝赋值运算符:X& operator=(const X&)
    • 一个移动构造函数:X(X&&)
    • 一个移动赋值运算符: X& operator=(X&&)
    • 一个析构函数:~X()

17.6.1 显式声明默认操作

  • 使用 =default 总是比你自己实现默认语义要好。

17.6.2 默认操作

  • 每个生成的操作的默认含义,像编译器生成它们所用的实现方法一样,就是对类的每个基类和非static数据成员应用此操作。即,逐成员拷贝,逐成员默认构造等等。

17.6.4 使用delete删除的函数

  • 我们可以删除一个函数,即,我们可以声明一个函数不存在,从而令(隐式或显式)使用它的尝试成为错误。
  • 这种机制最明显的应用是消除其他默认函数。例如,防止拷贝基类是很常见的,因为这种拷贝容易导致切片。

17.7 建议

  • 应该构造函数,赋值操作以及析构函数设计为一组匹配的曹祖
  • 使用构造函数为类建立不变式
  • 如果一个构造函数获取了资源,那么这个类就需要一个析构函数释放该资源
  • 如果一个类有虚函数,它就需要一个虚析构函数
  • 如果一个类没有构造函数,它可以进行逐成员初始化
  • 优先选择使用{}初始化而不是=和()初始化
  • 当且仅当类对象有 自然的 默认值时才为类定义默认构造函数
  • 如果一个类是容器,为它定义一个初始化列表构造函数
  • 按声明顺序初始化成员和基类
  • 如果一个类有一个引用成员,它可能需要拷贝操作
  • 在构造函数中优先选择成员初始化而不是赋值操作
  • 使用类内初始化器来提供默认值
  • 如果一个类是一个资源句柄,它可能需要拷贝和移动操作
  • 当编写一个拷贝构造函数时,小心拷贝每个需要拷贝的元素
  • 一个拷贝操作应该提供等价性和独立性
  • 小心纠缠的数据结构
  • 优先选择移动语义和写前拷贝而不是浅拷贝
  • 如果一个类被用作基类,防止切片现象
  • 如果一个类需要一个拷贝操作或一个析构函数,它可能需要一个构造函数,一个析构函数,一个拷贝赋值操作以及一个拷贝构造函数
  • 如果一个类有一个指针成员,它可能需要一个析构函数和非默认拷贝操作

第十八章 运算符重载

18.1 引言

  • 运算符重载最常用于数字类型,但是用户自定义运算符的用处绝不仅仅局限于数字类型。

18.5 建议

  • 定义运算符时应该尽量模仿传统用法
  • 如果默认的拷贝操作对于某种类型不适用,应该重新定义或者干脆禁用
  • 对于较大的运算对象,选用const引用类型
  • 对于较大的返回结果,选择移动构造函数
  • 对于需要访问类的表示部分的操作,优先将其定义为成员函数
  • 反之,对于无须访问类的表示部分的操作,优先将其定义为非成员函数
  • 用名字空间把辅助函数和它们的类结合在一起
  • 把对称的运算符定义成非成员函数
  • 用用户自定义的字面值常量模仿传统用法
  • 不要轻易为数据成员提供 set() 和 get() 函数,除非从语义上确使需要它们
  • 谨慎使用隐式类型转换
  • 避免使用丢失部分信息的类型转换
  • 对于同一种类型转换,切勿把它同时定义成构造函数以及类型转换运算符。

第十九章 特殊运算符

19.2 特殊运算符

  • 下列运算符
    • [] () -> ++ – new delete
    • +, <, ~等传统的一元或者二元运算符相比有其特殊之处,主要是从这些运算符在代码中的使用到程序员给出的定义的映射与传统运算符有轻微的差别。

19.2.1 取下标

  • 我们可以用operator[]函数为类对象的下标赋予某种新的含义。operator[]函数的第二个参数可以是任意类型的,因此,它常被用于定义vector,关联数组等类型
  • operator必须是非static成员函数

19.2.2 函数调用

  • 函数调用可以看成是一个二元运算,它的左侧运算对象是expression,右侧运算对象是expression-list。调用运算符()可以像其他运算符一样被重载。
  • 运算符() 最直接也是最重要的目标是为某些行为类似函数的对象提供函数调用语法。其中,行为模式与函数类似的对象称为类函数对象(function-like object)或者简称为函数对象。
  • 函数调用运算符通常是模板

19.2.3 解引用

  • 解引用运算符 -> 可以定义成一个一元后置运算符
  • 重载->的主要目的是创建 智能指针,即,行为与指针类似的对象

19.2.4 递增和递减

  • 在C++的所有运算符中,递增运算符和递减运算符是最特别的,因为它们即可以作为前置运算符,也可以作为后置运算符。
  • 前置递增运算符返回对象的引用,后置递增预算符返回一个新创建的对象。

19.2.5 分配和释放

  • 运算符new通过调用operator new()分配内存。相应的,运算符delete通过调用operator delete()释放内存。

19.2.6 用户自定义字面值常量

  • C++为内置数据类型提供了字面值常量
    1
    2
    3
    4
    5
    6
    7
    123;  // int
    1.2; // double
    1.2F; // float
    'a'; // char
    1ULL; // unsigned long long
    0xD0; // 十六进制 unsigned
    "as"; // C风格字符串
  • 我们也能为用户自定义类型提供字面值常量,或者更新内置类型字面值常量的形式。例如
    1
    2
    3
    4
    5
    "Hi"s // 字符串,并非以0结尾的字符数组
    1.2i // 虚数
    1010101111001100b // 二进制数
    123s // 秒数
    123.56km // 注意此处并非miles
  • 上述的用户自定义字面值常量是通过字面值常量运算符定义的,这类运算符负责把带后缀的字面值常量映射到目标类型。字面值常量运算符的名字由operator””加上后缀组成

19.4 友元

  • 一条普通的成员函数声明语句在逻辑上包含相互独立的三层含义
    • 该函数有权访问类的私有成员
    • 该函数位于类的作用域之内
    • 我们必须用一个含有this指针的对象调用该函数
  • 通过把成员函数声明成static的,我们可以令它只具有前两层含义。
  • 通过把非成员函数声明成friend的,我们可以令它只具有第一层含义。换句话说,一个friend函数可以像成员函数一样访问类的实现,但是在其他层面上与类是完全独立的。
  • 通常情况下,我们可以选择把类设计为成员(嵌套的类)或者非成员的友元

19.4.1 发现友元

  • 友元必须在类的外层作用域中提前声明,或者定义在直接外层非类作用域中。
  • 友元函数应该显式的声明在外层作用域中,或者接收一个数据类型为该类或者其派生类的参数;否则我们无法调用该友元函数。

19.4.2 友元与成员

  • 到底应该何时使用友元函数,何时把操作定义为成员函数呢?
  • 首先,我们应该让有权访问类的表示的函数数量尽可能少,并且确保所选的访问函数准确无误。

19.5 建议

  • 用operator执行取下标以及通过单个值查询等操作
  • 用operator()()执行函数调用,取下标以及通过多个值查询等操作
  • 用operator->()解引用 智能指针
  • 前置++优于后置++
  • 除非万不得已,否则不要定义全局operator new(), operator delete()
  • 为特定类或者类层次体系定义成员函数operator new() 和 operator delete(),用他们分配和释放内存空间
  • 用用户自定义的字面值常量模仿人们习惯的语法表示
  • 在大多数应用场合,建议使用标准库string而非你自己的版本
  • 如果需要使用非成员函数访问类的表示,比如改进写法,或者同时访问两个类的表示,把它声明成类的友元。
  • 当需要访问类的实现时,优先选用成员函数而非友元函数

第二十章 派生类

20.1 引言

  • C++从Simula借鉴了类和类层次的思想。而且,C++还借鉴了一个重要的设计思想:类应该用来建模程序员和应用程序世界中的思想。
  • C++提供了派生类的概念及相关的语言机制来表达层次关系,即,表达类之间的共性。
  • C++语言特性支持从已有类构建新的类
    • 实现继承(implementation inheritance): 通过共享基类所提供的特性来减少实现工作量
    • 接口继承(interface inheritance): 通过一个公共基类提供的接口允许不同派生类互换使用
  • 接口继承常被称为运行时多态(run-time polymorphism, 或者动态多态, dynamic polymorphism)。
  • 相反,模板所提供的类的通用性和继承无关,常被称为编译时多态(compile-tile polymorphism, 或静态多态, static polymorphism)

20.2 派生类

  • 派生关系通常可以图示为从派生类到其基类的一个箭头,表示派生类引用其基类。
  • 我们常常称一个派生类继承了来自基类的属性,因此这种关系也称为继承(inheritance)。有时,基类也称为超类(superclass),派生类称为子类(subclass)
  • 但是,派生类对象中的数据是其基类对象数据的超集。一个派生类通常比基类保存更多数据,提供更多函数,从这一点来说它比基类更大,绝不会更小。
  • 派生类概念的一种流行且高效的实现是将派生类对象表示为基类对象,再加上那些专属于派生类的信息放在末尾。
  • 派生一个类没有任何内存额外开销,所需内存就是成员所需空间。
  • 换句话说,若通过指针和引用进行操作,派生类对象可以当作其基类对象处理,反过则不能。
  • 将一个类用作基类等价于定义一个该类的(无名)对象。因此,类必须定义后才能用作基类。
    1
    2
    3
    4
    class Employee; // 只是声明,不是定义
    class Manager : public Employee { // 错误:Employee未定义
    ...
    };

20.2.1 成员函数

  • 派生类的成员可以使用基类的公有和保护成员,就好像它们声明派生类中一样。但是派生类不能访问基类的私有成员。
  • 通常,对派生类而言最干净的解决方案是只使用其基类的公有成员。

20.2.2 构造函数和析构函数

  • 构造函数和析构函数照例是必不可少的
    • 对象自底向上构造(基类先于成员,成员先于派生类),自顶向下销毁(派生类先于成员,成员先于基类)
    • 每个类都可以初始化其成员和基类(但不能直接初始化其基类的成员或基类的基类)
    • 类层次中析构函数通常应该是virtual的
    • 类层次中类的拷贝构造函数须小心使用,以壁面切片现象
    • 虚函数调用的解析,dynamic_cast,以及构造函数或析构函数中的typeid()反映了构造和析构的阶段(而不是尚未构造完成的对象的类型)

20.3 类层次

  • 一个派生类自身也可以作为其他类的基类。例如
    1
    2
    3
    class Employee {/*...*/};
    class Manager : public Employee {/*...*/};
    class Director : public Manager {/*...*/};
  • 我们习惯称这样一组相关的类为类层次(class hierarchy)。这种层次结构大多数情况下是一棵树,但也可能是更一般的图结构

20.3.1 类型域

  • 为了使派生类不至于成为仅仅是一种方便的声明简写方式,我们必须解决一个问题:给定一个Base*类型的指针,它指向的对象的真正派生类型是什么?
  • C++提供了四种基本解决方法
    • 保证指针只能指向单一类型的对象
    • 在基类中放置一个类型域,供函数查看
    • 使用dynamic_cast
    • 使用虚函数
  • 除非使用final,否则方法1依赖于所使用类型的很多知识。

20.3.2 虚函数

  • 虚函数机制允许程序员在基类中声明函数,然后在每个派生类中重新定义这些函数,从而解决了类型域方法的固有问题。编译器和链接器会保证对象和施用于对象之上的函数之间的正确关联。

  • 关键字virtual指出print()作为这个类自身定义的print()函数及其派生类中定义的print()函数的接口。

  • 为了允许一个虚函数声明能作为派生类中定义的函数的接口,派生类中函数的参数类型必须与基类中声明的参数类型完全一致,返回类型也只允许细微改变。虚成员函数有时也称为方法(method)

  • 首次声明虚函数的类必须定义它(除非虚函数被声明为纯虚函数)

  • 即使没有派生类,也可以使用虚函数,而一个派生类如果不需要自有版本的虚函数,可以不定义它。当派生一个类时,如需要某个函数,定义恰当版本即可。

  • 如果派生类中一个函数的名字和参数类型与基类中的一个虚函数完全相同,则称它覆盖(override)了虚函数的基类版本。此外,我们也可以用一个派生类层次更深的返回类型覆盖基类中的虚函数。

  • 除了我们显式说明调用虚函数的哪个版本(例如Employee::print())之外,覆盖版本会作为最恰当的选择应用于调用它的对象。无论用哪个基类访问对象,虚函数调用机制都会保证我们总是得到相同的函数。

  • 无论真正使用的确切Employee类型是什么,都能令Employee的函数表现出正确的行为,这称为多态性(polymorphism)。具有虚函数的类型称为多态类型(polymorphic type)或者说是 运行时多态类型(run-time polymorphic type)

  • 在C++中为了获得运行时多态行为,必须调用virtual成员函数,对象必须通过指针或引用进行访问。当直接操作一个对象时(而不是通过指针或引用),编译器了解其确切类型,从而就不需要运行时多态了。

20.3.3 显式限定

  • 使用作用域解析运算符::调用函数能保证不适用virtual机制

20.3.4 覆盖控制

  • 特定的控制机制
    • virtual: 函数可能被覆盖
    • =0:函数必须是virtual的,且必须被覆盖
    • override: 函数要覆盖基类中的一个虚函数
    • final: 函数不能被覆盖

20.3.5 using基类成员

  • 函数重载不会跨作用域

20.3.6 返回类型放松

  • 覆盖函数的类型必须与它所覆盖的虚函数的类型完全一致,C++对这一规则提供了一种放松机制。即,如果原返回类型为B,则覆盖函数的返回类型可以为D,只要B是D的一个公有基类即可。类似的,返回类型B&可以放松为D&。这一规则有时称为协变返回规则(covariant return)

20.4 抽象类

  • 具有一个或多个纯虚函数的类称为抽象类(abstract class)。我们无法创建抽象类的对象
  • 抽象类就是要作为通过指针和引用访问的对象的接口(为保持多态行为)。因此,对一个抽象类来说,定义一个虚析构函数通常很重要。由于抽象类提供的接口不能用来创建对象,因此抽象类通常没有构造函数。
  • 抽象类只能用作其他类的接口。抽象类提供接口,但不暴露实现细节。
  • 抽象类所支持的设计风格称为接口继承(interface inheritance),它与实现继承(implementation inheritance)相对,后者是由带状态或定义了成员函数的基类所支撑的。两种风格组合使用是可能的。

20.5 访问控制

  • 一个类成员可以是 private, protected或public的
    • 如果它是private的,仅可被所属类的成员函数和友元函数所使用
    • 如果它是protected的,仅可被所属类的成员函数和友元函数以及派生类的成员函数和友元函数所使用
    • 如果它是public的,可被任何函数所使用
  • 这反映了函数按类访问权限可分为三类:
    • 实现类的函数(其友元和成员)
    • 实现派生类的函数(派生类的友元和成岩)
    • 以及其他函数

20.5.1 protected成员

  • 当设计一个类层次时,有时我们提供的函数是供派生类的实现者而非普通用户所用的。

  • 例如,我们可能为派生类实现者提供一个高效的,不进行检查的访问函数,为其他人提供一个安全的进行检查的访问函数。我们可以通过将不检查的版本声明为protected来达到这一目的。

  • 在类中,成员默认是private的,而这通常是更好的选择。以我的经验,总是有其他代替方法,从而无须将派生类要用到的大量数据都放到一个公共基类中。

20.5.2 访问基类

  • 类似成员,基类也可以声明为private,protected或public。例如
    1
    2
    3
    class X: public B {/*...*/};
    class Y: protected B {/*...*/};
    class Z: private B {/*...*/};
  • 不同的访问说明符满足不同设计需求
    • public派生令派生类称为基类的一个子类型。例如,X是一种B。这是最常见的派生形式
    • private基类最有用的情形就是当我们定义一个类时将其接口限定为基类,从而可提供更强的保障。
    • protected基类在类层次中很有用,其中进一步的派生是常态。类似private派生,protected派生也用于表示实现细节。

20.7 建议

  • 避免使用类型域
  • 通过指针和引用访问多态对象
  • 使用抽象类,以便聚焦于清晰接口的设计应该提供什么
  • 在大型类层次中用override显式说明覆盖
  • 谨慎使用final
  • 使用抽象类说明接口
  • 使用抽象类保持实现细节和接口分离
  • 如果一个类有虚函数,那么它也应该有一个虚析构函数
  • 抽象类通常不需要构造函数
  • 优先选择private成员用于类的细节实现
  • 优先选择public成员用于接口
  • 仅在确实需要时才使用protected成员,且务必小心使用
  • 不要将数据成员声明为protected

第二十一章 类层次

21.4 建议

  • 为了避免忘记delete用new创建的对象,建议使用unique_ptr或者shared_ptr
  • 不要在作为接口的基类中防止数据成员
  • 用抽象类表示接口
  • 为抽象基类定义一个虚析构函数确保其正确的清理资源
  • 在规模较大的类层次中用override显式的覆盖
  • 用抽象类支持接口继承
  • 用含有数据成员的基类支持实现继承
  • 用普通的多重继承表示特征的组合
  • 用多重继承把实现和接口分离开来
  • 用虚基类表示层次中一部分类公有的内容

第二十二章 运行时类型信息

22.1 引言

  • 一般来说,类是从基类的框架中构造出来的。这种类框架(class lattice)通常被称为类层次。
  • 我们在设计类时,会努力令使用者不必过分操心一个类是如何由其他类组合出来的。特别是,虚调用机制保证了:当我们对一个对象调用函数f()时,对类层次中任何提供了可调用的f()声明的类,以及定义了f()的类,都会调用此函数。
  • 本章将介绍如何在仅有基类提供的接口的情况下获得全部对象信息。

22.2 类层次导航

  • 在运行时使用类型信息通常被称为 运行时类型信息,简写为RTTI(Run-Time Type Information)
  • 从基类到派生类的转换通常称为向下转换(downcast),因为我们画继承树的习惯是从根向下画。类似的,从派生类到基类的转换称为向上转换(upcast)。而从基类到兄弟类的转换,则称为交叉转换(crosscast)

22.2.1 dynamic_cast

  • 运算符dynamic_cast接受两个运算对象: 被 <和>包围的一个类型和被(和)包围的一个指针或引用
  • dynamic_cast要求给定的指针或应用指向一个多态类型,以便进行向下或向上转换。

22.2.3 static_cast和dynamic_cast

  • dynamic_cast可以从一个多态虚基类转换到一个派生类或是一个兄弟类。
  • static_cast则不行,因为它不检查要转换的对象。

22.7 建议

  • 使用虚函数确保无论用什么接口访问对象都执行相同的操作
  • 如果在类层次中导航不可避免,使用dynamic_cast
  • 使用dynamic_cast进行类型安全的显式类层次导航
  • 使用dynamic_cast转换引用类型,当无法转换到所需类时,会被认为是一个错误
  • 使用dynamic_cast转换指针类型,当无法转换到所需类时,会被认为是一个错误
  • 用双重分发或访客模式基于两个动态类型的操作
  • 在构造和重构过程中不要调用虚函数
  • 使用typeid实现扩展的类型信息
  • 使用typeid查询对象的类型,但不要用它查询对象的接口
  • 优选虚函数而不是基于typeid或dynamic_cast的重复的switch语句

网络日记002

引言

  • 现在是周四,本周工作还差用户手册未完成。本周主要完成了参数配置文件功能和贴合流程优化。现在有些空闲下来,在考虑未来的职业方向。

当前现状

  • 浏览Boss直聘看到的一些求职关键词

    • 良好的3D数学基础
    • 熟练掌握线性代数
    • ROS/ROS2
    • 笔试
    • 机器人软件,包括应用软件,人机界面,驱动程序
    • Qt在windows或linux环境下进行应用软件开发
    • 熟悉面向对象基本思想,了解常用设计模式
  • 有一个疑问:找工作面试和实际工作技能是一样的吗?面试会有面试技巧。

简介

  • Qt开发过程中遇到的问题及解决方案

VS2022 打开ui文件自动退出问题

  • 选择“扩展->QT VS Tools->Options”,对话框中左侧选择“Qt->General”,右侧选择“Qt Designer->Run in detached window”选项值设为true 即可解决。

VS2022 cmake 加载ui文件无法生成头文件问题

  • 选择 “项目->使用cmake调试器配置” 选项重新配置项目
  • 重启VS

qt5 字符编码问题

Qt中MainWindow界面最大化按钮是灰色的

  • 在Qt Designer中将maximumSize的值设置为16777215x16777215即可使窗口打开时最大化按钮可用。

简介

  • 基于Qt5的桌面应用软件开发常见技巧

Qt 设置标签的背景颜色和透明度

在 Qt 中,可以通过多种方式设置 QLabel 的背景颜色和透明度。以下是具体方法:


1. 使用样式表 (setStyleSheet)

设置背景颜色

通过 setStyleSheet,可以使用 CSS 样式为 QLabel 设置背景颜色。

示例:

1
2
QLabel *label = new QLabel("Hello, Qt!");
label->setStyleSheet("background-color: lightblue; color: black;");

效果:

  • 标签背景为浅蓝色,文字颜色为黑色。

设置背景透明度

使用 rgbahsla 颜色值可以设置背景透明度。

示例:

1
2
QLabel *label = new QLabel("Transparent Background");
label->setStyleSheet("background-color: rgba(0, 0, 255, 0.5); color: white;");

效果:

  • 背景颜色为半透明蓝色,文字颜色为白色。

2. 使用 QPalette 设置背景颜色

QPalette 是 Qt 中管理控件颜色的类,适用于简单背景颜色的设置。

示例:

1
2
3
4
5
QLabel *label = new QLabel("Hello, Palette!");
QPalette palette = label->palette();
palette.setColor(QPalette::Window, QColor(0, 0, 255, 127)); // 设置半透明蓝色
label->setAutoFillBackground(true); // 启用背景填充
label->setPalette(palette);

效果:

  • 背景为半透明蓝色。

3. 自定义绘制 (paintEvent)

对于高度定制的背景绘制,可以重写 paintEvent 方法。

示例:

1
2
3
4
5
6
7
8
9
10
class TransparentLabel : public QLabel {
protected:
void paintEvent(QPaintEvent *event) override {
QPainter painter(this);
painter.setBrush(QBrush(QColor(0, 0, 255, 127))); // 半透明蓝色
painter.setPen(Qt::NoPen);
painter.drawRect(this->rect());
QLabel::paintEvent(event); // 绘制文字
}
};

效果:

  • 标签背景为半透明蓝色,支持自定义绘制逻辑。

4. 背景和文字透明独立处理

如果需要设置背景透明,同时保持文字完全不透明,可以使用伪类或层叠元素技术:

伪类方法

通过 ::before 添加一个透明背景层:

1
2
3
4
5
QLabel *label = new QLabel("Text with Transparent Background");
label->setStyleSheet(
"QLabel { color: black; } "
"QLabel::before { content: ''; position: absolute; top: 0; left: 0; width: 100%; height: 100%; background-color: rgba(0, 0, 255, 0.5); z-index: -1; }"
);

完整示例

以下示例演示如何结合 QLabel 和样式表设置透明背景及文本颜色:

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
#include <QApplication>
#include <QLabel>
#include <QVBoxLayout>
#include <QWidget>

int main(int argc, char *argv[]) {
QApplication app(argc, argv);

QWidget window;
window.setWindowTitle("QLabel Background Example");
window.resize(400, 200);

QLabel *label1 = new QLabel("Opaque Background");
label1->setStyleSheet("background-color: lightblue; color: black;");

QLabel *label2 = new QLabel("Transparent Background");
label2->setStyleSheet("background-color: rgba(255, 0, 0, 0.5); color: white;");

QVBoxLayout *layout = new QVBoxLayout(&window);
layout->addWidget(label1);
layout->addWidget(label2);

window.show();
return app.exec();
}

运行效果:

  • 第一行是背景不透明的标签。
  • 第二行是带有透明背景的标签。

总结

方法 适用场景 优点
setStyleSheet 简单背景颜色和透明度设置 易于实现,语法类似 CSS
QPalette 设置单一颜色的背景 使用标准 API,易于理解
paintEvent 高度自定义背景绘制 灵活控制绘制行为
伪类或叠加元素 背景和文字透明度独立设置 背景透明但文字保持清晰

根据实际需求选择适合的方法,即可轻松实现 QLabel 的背景颜色和透明度设置!

设置子窗口显示在父窗口的位置 (绝对坐标)

  • 需求:需要在父窗体弹出消息提示框,弹出的位置是相对于父窗体的位置
  • 方法
    • 获取父窗口相对于屏幕的坐标,即绝对坐标
    • 计算子窗口的坐标
    • 移动窗口
  • 示例如下
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    void NewMainWindow::MessageAlertButtonClicked()
    {
    QLabel* label_ptr = new QLabel("this is a message alert...", this);
    label_ptr->setWindowFlags(Qt::Tool | Qt::FramelessWindowHint | Qt::WindowStaysOnTopHint);
    label_ptr->setStyleSheet("background-color: green; color: white; padding: 10px; border-radius: 5px;");
    label_ptr->setAlignment(Qt::AlignHCenter);
    label_ptr->setFixedSize(200, 50);

    QPoint global_pos = mapToGlobal(QPoint(0, 0));
    label_ptr->move(global_pos.x() + width() / 2 - label_ptr->width() / 2, global_pos.y() + 50);
    label_ptr->show();

    // 设置定时器 3 秒后关闭窗口
    QTimer::singleShot(3000, label_ptr, &QLabel::deleteLater);
    }

建立一个最简单的窗口

1
2
3
4
5
6
7
8
9
10
#include <QtWidgets>

int main(int argc,char *argv[]) //主函数入口,编译器将会从这里开始启动程序
{
QApplication a(argc,argv); //启动Qt的应用程序,相当于初始化Qt的框架
QWidget w; //QWidget类是所有用户界面对象的基类
w.resize(400,300); //设置界面宽为400像素,高为300像素
w.show(); //展示界面
return a.exec(); //程序在a的事件循环里执行
}

网路日记001

引言

今天是周六,在图书馆学习qt,但是收获很少且效率很低。目前我对于职业方向很迷茫,我需要解决这个问题,但不能只在脑海中思考,还要落在实处。所以,我开始写网络日记,试图让思考的过程有迹可循。网络日记并不是一天一篇,而是隔几天总结自己的现状和未来的思考。

当前现状

在一家做机器人的公司工作,岗位是机器人软件研发工程师,方向是机器人应用模块开发。当前参加的两个项目为碰钉机器人和装板机器人,两者都为移动地盘+机械臂+末端工具,项目架构分为通信模块,机器人模块,任务模块,视觉模块。我负责任务模块。

当前开发环境在windows平台,用到的开发工具有 VSCode,Visual Studio 2022,CLion。用到的第三方库有qt,spdlog。

当前困难

代码开发工作较少且难度较低,更多的是现场调试。与视觉没有关系,与底层没有关系,纯纯业务开发。

这样的情况产生的问题有:

  • 代码提升有限
  • 开发环境和技术栈并不通用,不便于跳槽

简介

  • svn项目管理工具学习笔记

SVN是什么

SVN,全称为Subversion,是一种开源的版本控制系统(Version Control System,简称VCS)。它用于管理和跟踪文件的版本,特别是在多个开发人员协作时对代码的管理。SVN 允许团队成员对项目中的文件进行修改、查看历史版本、合并更改,并在需要时还原到以前的版本。

SVN 的主要功能包括:

  1. 版本控制:跟踪文件的每一次修改,记录历史版本。
  2. 分支和标签:可以为代码创建分支,以便并行开发不同的功能,标签用于标记特定版本。
  3. 并行开发:多人可以同时对相同文件进行修改,SVN会处理合并冲突。
  4. 回滚:可以将文件恢复到先前的版本。
  5. 集中式存储库:SVN 使用的是一个中央的存储库,所有的代码和历史记录都保存在这个中央服务器上,用户通过客户端与服务器进行交互。

SVN 在早期非常流行,特别是在 Git 等分布式版本控制系统普及之前。现在,许多开发团队已经转向 Git,但 SVN 依然在一些企业和项目中使用。

SVN版本控制系统 详解

SVN(Subversion)是一种集中式版本控制系统,用于管理项目中文件和目录的变更。其目的是帮助开发者更高效地协作,同时保证项目文件的完整性和历史记录。下面是 SVN 的详细介绍,包括其工作原理、架构、核心概念及主要功能。

1. SVN 的工作原理

SVN 采用集中式版本控制模型,即所有的项目文件都存储在一个中央存储库(Repository)中。开发人员通过客户端从存储库中获取文件的副本,并在本地工作。修改完成后,用户可以将更改提交回中央存储库。

SVN 的工作流程通常如下:

  1. Checkout:开发者从中央存储库拉取项目的当前版本到本地进行修改。
  2. Update:开发者在提交之前,通常会先更新本地的代码库,获取其他开发者提交的最新修改。
  3. Commit:修改完成后,开发者将更改提交到中央存储库,产生一个新的版本。
  4. Merge:如果有冲突,SVN 会帮助开发者合并不同的修改。

2. SVN 的架构

SVN 的架构分为两个主要部分:

  • SVN 服务器:存储所有的文件和它们的历史版本,用户通过网络连接到 SVN 服务器进行协作。服务器可以部署在本地或远程。
  • SVN 客户端:用户通过客户端与服务器交互,可以从服务器拉取文件、提交修改、查看历史等。常见的 SVN 客户端包括 TortoiseSVN、命令行客户端等。

3. SVN 的核心概念

  • Repository(存储库):存放项目文件的中央位置,存储文件的当前版本及其历史版本。
  • Working Copy(工作副本):用户从存储库拉取的本地副本,用户可以在本地对其进行修改。
  • Revision(修订版):每一次对存储库的修改都会生成一个新的修订版本,修订版用递增的数字标识。
  • Trunk(主干):项目的主要开发线,通常用于存放稳定或开发中的代码。
  • Branch(分支):从主干或其他分支创建的独立开发线,常用于实现新的功能或修复 bug。
  • Tag(标签):用于标记某个特殊的修订版本,通常用于发布版本。
  • Merge(合并):将不同分支的修改合并到一起,通常在多条开发线并行工作时使用。

4. SVN 的主要功能

4.1 版本控制

  • SVN 可以记录每个文件的修改历史,允许用户查看每次更改的内容以及是谁进行的修改。
  • 可以通过版本号恢复到以前的版本,回滚文件状态。

4.2 并行开发

  • 分支和合并:通过创建分支,团队可以并行开发多个功能,避免冲突。开发完成后可以合并到主干。
  • 冲突检测和解决:SVN 能检测出多个开发者修改同一文件的不同部分,并提供冲突解决工具。

4.3 锁定机制

  • 为避免多用户同时编辑同一文件导致冲突,SVN 提供文件锁定机制,允许用户锁定文件,使得其他用户暂时无法编辑该文件,适用于无法自动合并的文件,如二进制文件。

4.4 访问控制和安全

  • SVN 服务器允许管理员为不同用户设置不同的访问权限,可以控制某些用户只读或读写某些分支或文件。
  • 支持 HTTP(S) 等加密协议,确保数据传输的安全性。

5. SVN 的优势与局限

优势:

  • 简单易用:SVN 的命令和概念相对简单,适合小到中型团队使用。
  • 集中式管理:由于所有代码和历史都集中存储,便于备份、管理和访问控制。
  • 完善的分支管理:支持创建分支和标签,便于并行开发和版本发布管理。
  • 适用多种文件类型:不仅适用于代码,还可以管理文档、图片等文件。

局限:

  • 集中式存储模型的瓶颈:所有开发人员都需要访问中央服务器,容易产生网络延迟问题,尤其是团队分布全球时。
  • 不适合离线工作:与 Git 等分布式版本控制系统相比,SVN 在离线情况下的操作较为不便,用户必须连接到服务器才能提交更改。
  • 性能问题:在处理大量文件或复杂项目时,SVN 的性能可能不如分布式版本控制系统。

6. SVN 和 Git 的对比

  • 集中式 vs 分布式:SVN 是集中式版本控制系统,而 Git 是分布式版本控制系统。在 Git 中,每个开发者都有完整的项目历史记录,SVN 则将所有历史保存在中央服务器。
  • 分支管理:SVN 的分支管理较为笨重,创建和切换分支可能涉及较多步骤,而 Git 由于其轻量级分支机制,使得分支操作非常快速。
  • 提交机制:SVN 的提交必须连接到中央服务器,而 Git 支持本地提交,允许开发者离线工作,并在连接服务器后再推送更改。

7. 典型使用场景

  • 传统企业:SVN 仍然在一些企业软件项目中使用,特别是在需要严格控制权限和管理较大代码库时。
  • 文档和文件管理:因为 SVN 处理二进制文件相对较好,且支持锁定文件,它常用于需要管理大量文档和文件的项目。

8. 常用命令

  • svn checkout URL:从存储库下载项目到本地工作副本。
  • svn update:更新工作副本中的文件,获取最新的更改。
  • svn commit -m "message":提交本地更改到存储库。
  • svn add FILE:将新文件或目录添加到版本控制中。
  • svn delete FILE:从版本控制中删除文件。
  • svn status:查看当前工作副本的修改状态。
  • svn log:查看存储库的提交历史。
  • svn merge BRANCH_URL:将某个分支的更改合并到当前分支。

总结

SVN 是一个强大且稳定的版本控制系统,适合需要集中管理、精确控制和清晰历史记录的项目。尽管如今 Git 更受欢迎,SVN 在某些特定领域和企业环境中仍然广泛使用。如果你的团队已经习惯了集中式版本控制系统的工作流程,SVN 依然是一个可靠的选择。

svn 基本项目管理流程

SVN(Subversion)是一种版本控制系统,用于管理项目中的代码和文件的版本变化。在使用SVN进行项目管理时,通常有一套基本的流程,可以帮助团队更高效地协作和管理代码。以下是一个常见的 SVN 项目管理流程:

1. 创建项目版本库

  • 在服务器上初始化一个 SVN 仓库,用于存储项目的所有版本历史记录。
  • 创建基础目录结构,一般包括:
    • trunk:主开发线,放置稳定的代码。
    • branches:分支,用于开发新功能或修复 Bug 的不同版本。
    • tags:标签,标记重要的发布版本,如 v1.0v2.0

2. 工作副本 Checkout

  • 开发者将项目从 SVN 仓库中检出(checkout)到本地机器。检出操作会复制项目的所有文件和目录到本地。
  • 命令示例:
    1
    svn checkout https://svn.example.com/repo/project/trunk

3. 日常开发流程

  1. 更新 (Update)

    • 在开始修改代码前,开发者应通过 svn update 命令同步最新版本的代码,以确保本地工作副本是最新的。
    • 命令示例:
      1
      svn update
  2. 修改 (Modify)

    • 在本地修改代码或其他文件。SVN 会跟踪这些修改,但这些更改只保存在开发者的本地副本中,直到提交(commit)到仓库。
  3. 检查状态 (Status)

    • 使用 svn status 查看哪些文件被修改、删除或新增。
    • 命令示例:
      1
      svn status
  4. 添加/删除文件 (Add/Delete Files)

    • 如果有新的文件需要加入版本控制,使用 svn add 命令。
    • 如果需要删除文件,使用 svn delete 命令。
    • 命令示例:
      1
      2
      svn add newfile.txt
      svn delete oldfile.txt
  5. 解决冲突 (Conflict Resolution)

    • 如果在更新代码时发生冲突,SVN 会提示冲突文件,开发者需要手动解决冲突,然后标记为已解决:
      1
      svn resolved conflictedfile.txt

4. 提交更改 (Commit)

  • 完成本地修改后,开发者需要将修改提交到 SVN 仓库,确保其他团队成员可以看到并使用最新版本的代码。
  • 提交时需要编写清晰的提交说明,描述更改内容。
  • 命令示例:
    1
    svn commit -m "Fix bug in login module"

5. 分支管理 (Branch Management)

  • 创建分支:为了开发新功能或修复特定问题,可以从主干(trunk)创建一个新的分支。
    1
    svn copy https://svn.example.com/repo/project/trunk https://svn.example.com/repo/project/branches/feature-branch -m "Creating a new feature branch"
  • 合并分支:在分支上的开发完成后,可以将分支的修改合并回主干。
    1
    svn merge https://svn.example.com/repo/project/branches/feature-branch

6. 打标签 (Tagging)

  • 在项目达到某个稳定点时,可以创建一个标签,方便将来的回滚或参考。标签一般是不可修改的。
  • 命令示例:
    1
    svn copy https://svn.example.com/repo/project/trunk https://svn.example.com/repo/project/tags/release-1.0 -m "Tagging version 1.0"

7. 查看历史记录 (Log)

  • 使用 svn log 查看项目的提交历史,了解之前的更改和版本信息。
  • 命令示例:
    1
    svn log

8. 回滚版本 (Revert)

  • 如果发现某个版本存在问题,可以回滚到之前的某个版本。
  • 命令示例:
    1
    2
    svn revert somefile.txt
    svn merge -r HEAD:1234 https://svn.example.com/repo/project/trunk

总结

SVN 的基本流程包括从版本库检出项目、更新本地副本、修改文件、解决冲突、提交修改、分支管理、打标签以及查看历史记录。这一流程有助于确保团队协作开发时代码的版本控制,避免冲突并方便回滚和历史查看。

简介

  • C++程序设计语言 第四部分 标准库相关笔记

第三十章 标准库概览

30.1 引言

  • 标准库是一个组件集合,在ISO C++标准中定义,在所有实现中都以一致的形式(和性能)提供。出于可移植性和长期维护的考虑,强烈推荐在合适的地方尽量使用表混库。一般而言,不要尝试重新发明轮子。

30.1.1 标准库设施

  • 标准库是所有C++实现都必须提供的,以便每个程序员都能依靠它来编写程序。C++标准库提供

    • 语言特性的支持,例如内存管理,范围for语句和运行时类型信息
    • 具体C++实现所定义的一些语言相关的信息,如最大float值
    • 单纯用语言难以高效实现的基本操作,例如 is_polymorphic, is_scalar 和 is_nothrow_constructible
    • 底层(无锁)并发编程设施
    • 基于线程的并发编程的支持
    • 基于任务的并发的基本支持,例如future和async()
    • 大多数程序员难以实现最优且可移植版本的函数,例如 uninitialized_fill()和memmove()
    • 无用内存回收(垃圾收集)的基本支持,例如declare_reachable()
    • 程序员编写可移植代码所需的复杂基础组件,例如list,map,sort和IO流
    • 用于标准库自身扩展的框架,例如允许用户为自定义类型提供与内置类型相似的I/O操作的规范和基础组件以及标准模板库STL
  • 标准库的设计目标之一是成为其他库的公共基础。特别是,组合使用标准库特性可以起到三方面的支撑作用

    • 可移植性的基础
    • 一组紧凑且高效的组件,可以作为构造性能敏感的库和应用的基础
    • 一组实现库内交互的组件。

30.2 头文件

  • 标准库组件都定义在命名空间std中,以一组头文件的形式提供。头文件构成了标准库最主要的部分,因此,列出头文件可以给出标准库的一个概貌。

  • 以字母c开头的标准库头文件对应C标准库中的头文件。每个C标准库头文件<x.h>都定义了一些同时位于全局命名空间和命名空间std中的内容,且有一个定义相同内容的对应头文件。理想情况下,头文件中的名字不会污染全局命名空间,但不幸的是(归咎于管理多语言,多操作系统环境的复杂性)大多数实际情况下会发生污染。

  • 容器

    • 可变大小一维数组
    • 双端队列
    • 单向链表
    • 双向链表
    • 关联数组
    • 集合
    • 哈希关联数组
    • 哈希集合
    • 队列
    • 固定大小一维数组
    • bool数组
  • 关联容器multimap和multiset分别声明在中,priority_queue声明在

  • 通用工具

    • 运算符和值对
    • 元组
    • 类型萃取
    • 将type_info用作一个关键字或哈希码
    • 函数对象
    • 资源管理指针
    • 限定作用域的分配器
    • 编译时有理数算术运算
    • 时间工具
    • C风格日期和时间工具
    • 迭代器及其支持
  • 迭代器机制令标准库算法具有通用性

  • 算法

    • 泛型算法
    • bseach(), qsort()
  • 一个典型的泛型算法能应用于任何类型的元素构成的序列。C标准库函数bsearch()和qsort()只能用于内置数组,且元素类型不能有用户自定义的拷贝构造函数和析构函数

  • 诊断

    • 异常类
    • 标准异常
    • 断言宏
    • C风格错误才处理
    • 系统错误支持
  • 字符串和字符

    • T的字符串
    • 字符分类
    • 宽字符分类
    • C风格字符串函数
    • C风格宽字符字符串函数
    • C风格分配函数
    • C风格多字节字符串
    • 正则表达式匹配
  • 头文件声明了strlen(),strcpy()等一族函数。头文件声明了atof()和atoi(),可将C风格字符串转换为数值。

  • 输入/输出

    • I/O组件的前置声明
    • 标准iostream对象和操作
    • iostream基类
    • 流缓冲
    • 输入流模板
    • 输出流模板
    • 操纵符
    • 字符串流
    • 字符分类函数
    • 文件流
    • printf() I/O函数族
    • 宽字符printf()风格I/O函数
  • 操纵符是操作流状态的对象

  • 本地化

    • 表示文化差异
    • 文化差异C风格表示
    • 代码转换
  • locale对日期输出格式,货币表示符号和字符串校勘等在不同语言和文化中有差异的内容进行本地化

  • 语言支持

    • 数值限制
    • 数值标量限制C风格宏
    • 浮点数限制C风格宏
    • 标准整数类型名
    • 动态内存管理
    • 运行时类型识别支持
    • 异常处理支持
    • initializer_list
    • C标准库语言支持
    • 可变长函数参数列表
    • C风格栈展开
    • 程序终止
    • 系统时钟
    • C风格信号处理
  • 头文件定义了sizeof()返回的类型size_t,指针减法和数组下标返回的类型ptrdiff_t以及声名狼藉的NULL宏

  • C风格栈展开(使用<csetjmp中的setjmp和longjmp>)与析构函数和异常处理不兼容,因此最好避免使用。

  • 数值

    • 复数及其运算
    • 数值向量及其运算
    • 推广的数值运算
    • 标准数学函数
    • C风格随机数
    • 随机数发生器
  • 由于历史原因,abs()和div()不像其他数学函数那样在中,而是在

  • 并发

    • 原子类型及其操作
    • 等待动作
    • 异步任务
    • 互斥类
    • 线程
  • C标准库的一些组件与C++程序员有着不同程度的关联,C++标准库提供了对这些组件的访问机制。

  • C兼容性

    • 公共整数类型的别名
    • C的bool类型
    • 浮点数环境
    • C的对齐机制
    • C的泛型数学函数:

30.3 语言支持

  • 语言支持是标准库中很小但至关重要的部分,是程序正常运行所必需的,因为语言特性依赖于这些组件。

  • 标准库支持的语言特性

    • new和delete
    • typeid()和type_info
    • 范围for
    • initializer_list

30.3.1 initializer_list 支持

  • 一个 {} 列表会依据规则转换为一个 std::initializer_list 类型的对象。

  • 不幸的是, initializer_list并未提供下标运算符。如果你希望用 [] 而不是 *,可对指针使用下标

    1
    2
    3
    4
    5
    6
    7
    8
    void f(initializer_list<int> lst)
    {
    const int* p = lst.begin();
    for (int i = 0; i < lst.size(); i++>)
    {
    std::cerr << p[i] << "\n";
    }
    }
  • initializer_list自然也可用于范围for语句

    1
    2
    3
    4
    5
    6
    7
    void f2(initializer_list<int> lst)
    {
    for (auto x : lst)
    {
    std::cerr << x << "\n";
    }
    }

30.3.2 范围for支持

  • 一条范围for语句会借助迭代器映射为一条for语句。
  • 中,标准库提供了std::begin()和std::end()两个函数,可用于内置数组及任何提供了begin()和end()成员的类型。
  • 所有标准库容器和字符串都支持使用范围for的迭代;容器适配器(例如stack和priority_queue)则不支持。容器的头文件(例如)会包含,因此用户很少需要自己直接包含它。

30.4 错误处理

  • 标准库包含的组件已有将近40年的开发历程。因此,它们处理错误的风格和方法并不统一

    • C风格库函数大多数通过设置errno来指示发生了错误
    • 很多对元素序列进行操作的算法返回一个尾后迭代器来指示 未找到 或 失败
    • I/O流库要依赖于每个流中的一个状态来反映错误,并可能(根据用户需要)通过抛出异常来指示错误。
    • 一些标准库组件,例如vector,string和bitset通过抛出异常来指示错误。
  • 标准库的设计目标之一是所有组件都遵守基本保证:即,即使抛出了异常,也不会有资源(例如内存)泄露,且不会有标准库类的不变式被破坏的情况出现。

30.4.1 异常

  • 一些标准库组件通过抛出异常来报告错误
  • 标准库异常
    • bitset: 抛出invalid_argument, out_of_range,overflow_error
    • iostream: 如果允许异常的话,抛出ios_base::failure
    • regex: 抛出regex_error
    • string: 抛出length_error,out_of_range
    • vector: 抛出out_of_range
    • new T: 如果不能为一个T分配内存,抛出bad_alloc
    • dynamic_cast(r): 如果不能将引用r转换为一个T,抛出bad_cast
    • typeid(): 如果不能获得一个type_info,抛出bad_typeid
    • thread: 抛出system_error
    • call_once(): 抛出system_error
    • mutex: 抛出system_error
    • condition_variable: 抛出system_error
    • async(): 抛出system_error
    • packaged_task: 抛出system_error
    • future和promise: 抛出system_error
  • 任何直接或间接使用这些组件的代码都可能遇到这些异常。
  • 除非你确认使用组件的方式不会令它们抛出异常,否则坚持在某处(例如main())捕获标准库异常类层次的某个根类(例如exception)和任何异常(…)是一个很好的编程习惯。

30.4.1.1 标准库exception类层次

  • 不要抛出int,C风格字符串等内置类型,而应该抛出专门表示异常的类型的对象。

30.4.1.2 异常传播

  • 中提供了一些组件,令异常传播对程序员可见
  • 异常传播
    • exception_ptr: 非特定的异常指针类型
    • ep = current_exception() ep是一个exception_ptr,指向当前异常,若当前无活动异常则不指向任何异常;不抛出异常
    • rethrow_exception(ep): 重新抛出ep指向的异常;ep包含的不能是空指针nullptr;无返回值
    • ep = make_exception_ptr(e): ep是指向exception e的exception_ptr;不抛出异常。
  • 一个exception_ptr可以指向任何异常,不局限于exception类层次中的异常。可将exception_ptr看作一种智能指针(类似shared_ptr)–只要一个exception_ptr还指向其异常,那么这个异常就会保持活跃。这样,我们就可以通过exception_ptr将一个异常从捕获它的函数中传递出来,并在其他地方重新抛出。即,exception_ptr可用来实现在捕获线程之外的其他线程中重抛出异常,这是promise和future所需要的。对一个exception_ptr使用rethrow_exception(在不同线程中)不会引起数据竞争。
  • 异常不能从noexcept函数中传播出去.

30.4.1.3 terminate()

  • 中,标准库提供了处理意外异常的组件
  • terminate
    • h = get_terminate(): h为当前终止处理程序;不抛出异常
    • h2 = set_terminate(): 当前终止处理程序被设定为h;h2为旧终止处理程序;不抛出异常
    • terminate(): 终止程序;无返回值;不抛出异常。
  • 除了极特殊的情况下使用set_terminate()和terminate()之外,其他情况应避免使用这些函数

30.4.2 断言

  • 标准库提供了断言机制
  • 断言
    • static_assert(e, s) 在编译时对e求职;若!e为假则将s作为编译器错误信息输出
    • assert(e): 若宏NOBUG未定义,则在运行时对e进行求职,若!e为假,向cerr输出一个消息并调用abort();若定义了NOBUG,则什么也不做。
    • assert()是一个宏,定义在中,assert()生成什么错误信息由C++具体实现自己决定,但应该包含源文件名(FILE)和assert()所在的源码行号(LINE)
    • 断言常常用于产品级代码而非教材的小例子中(它也本应如此)
    • 函数名(func)也可能包含在消息中。

30.4.3 system_error

  • 中,标准库提供了一个能从操作系统和底层系统组件报告错误的框架。

30.4.3.1 错误码

  • 当一个错误以错误码的形式从程序底层浮上来时,我们必须处理这个错误或将错误码转换为一个异常。

30.4.3.2 错误类别

30.4.3.3 system_error异常

  • system_error报告的错误都源自标准库中处理操作系统的部分。它传递一个error_code,并可传递一个错误消息字符串。
  • 自然的,system_error可用于标准库之外的程序。它传递一个系统相关的error_code,而不是一个可移植的error_condition

30.4.3.4 可移植的错误状态

  • 可移植错误码(error_condition)的表现形式与系统相关的error_code几乎相同。总体思路是每个系统有一组特有的(“原生的”)错误码,可映射到潜在可移植的错误码,这样对于需要跨平台编程的程序员(通常是编写库的程序员)来说会更加方便。

30.5 建议

  • 使用标准库组件保持可移植性
  • 使用标准库组件尽量减少维护成本
  • 将标准库组件作为更广泛和更专门化的库的基础
  • 使用标准库组件作为灵活,广泛使用的软件的模型
  • 标准库组件定义在命名空间std中,都是在标准库头文件中定义的
  • 每个C标准库头文件X.h都有其C++标准库对应的版本
  • 必须#include相应的头文件才能使用标准库组件
  • 为了对内置数组使用范围for,需要 #include
  • 优选基于异常的错误处理而非返回错误码方式的错误处理
  • 始终要捕获 exception&(对标准库和语言支持的异常)和…(对意料之外的异常)
  • 标准库exception层次可以(但不是必须支持)用于用户自定义异常
  • 如果发生严重错误,调用terminate()
  • 大量使用static_assert()和assert()
  • 不要假定assert()总是会被求值

第三十一章 STL容器

31.1 引言

  • STL包含标准库中的迭代器,容器,算法和函数对象几个部分。

31.2 容器概览

  • 一个容器保存着一个对象序列。容器可分类为

    • 顺序容器提供对元素(半开)序列的访问
    • 关联容器提供基于关键字的关联查询
  • 此外,标准库还提供了一些保存元素的对象类型,它们并未提供顺序容器或关联容器的全部功能

    • 容器适配器提供对底层容器的特殊访问
    • 拟容器保存元素序列,提供容器的大部分但非全部功能
  • STL容器都是资源句柄,都定了拷贝和移动操作。所有容器操作都提供了基本保证,确保与基于异常的错误处理机制能够正确协同工作。

  • 顺序容器

    • vector<T, A> 空间连续分配的T类型元素序列,默认选择容器
    • list<T, A> T类型元素双向链表,当需要插入/删除元素但不移动已有元素时选择它
    • forward_list<T, A> T类型元素单向链表,很短的或空序列的理想选择
    • deque<T, A> T类型元素双端队列,向量和链表的混合,对大多数应用而言,都比向量和链表其中之一要慢。
  • 这些容器都定义在,中。顺序容器为元素连续分配内存(例如vector)或将元素组织为链表(例如forward_list),元素的类型是容器的成员value_type(或者是上表中的T)。deque(发音为 deck)采用链表和连续存储的混合方式。

  • 除非你有充足的理由,否则应该优选vector而不是其他顺序容器。注意,vector提供了添加,删除元素的操作,这些操作都允许vector按需增长或收缩。对于包含少量元素的序列而言,vector是一种完美的支持列表操作的数据结构。

  • 当在一个vector中插入,删除元素时,其他元素可能会移动。与之相反,链表或关联容器中的元素则不会因为插入新元素或删除其他元素而移动。

  • forward_list(单向链表)是一种专为空链表和极短链表优化过的数据结构。一个空forward_list只占用一个内存字。在实际应用中,有相当多的情况链表是空的(还有很多情况链表是非常短)

  • 有序关联容器

    • map<K,V,C,A> 从K到V的有序映射,一个(K,V)对序列
    • multimap<K,V,C,A> 从K到V的有序映射,允许重复关键字
    • set<K,C,A> K的有序集合
    • multiset<K,C,A> K的有序集合,允许重复关键字
  • 这些容器通常用平衡二叉树(通常是红黑树)实现

  • 关键字(K)的默认序标准是 std::less

  • 类似顺序容器,模板参数A是分配器,容器用它来分配和释放内存。对映射,A的默认值是 std::allocator<std::pair<const K,T>>,对集合,A的默认值是std::allocator

  • 无序关联容器

    • unordered_map<K,V,H,E,A> 从K到V 的无序映射
    • unordered_multimap<K,V,H,E,A> 从K到V 的无序映射,允许关键字重复
    • unordered_set<K,H,E,A> K的无序集合
    • unordered_multiset<K,H,E,A> 从K的无序集合,允许关键字重复
  • 这些容器都是采用溢出链表法的哈希表实现。关键字类型K的默认哈希函数类型H为 std::hash。关键字类型K的默认相等判断函数类型E为 std::equal_to;相等性判定函数用来判断哈希值相同的两个对象是否相等

  • 容器适配器是一类特殊容器,它们为其他容器提供了特殊的接口

    • priority_queue<T,C,Cmp> T的优先队列,Cmp是优先级函数类型
    • queue<T,C> T的队列,支持push()和pop()操作
    • stack<T,C> T的栈,支持push()和pop()操作
  • 一个priority_queue的默认优先级函数Cmp为std::less.queue的默认容器类型C为 std::deque,stack和priority_queue的默认容器类型C为std::vector

  • 某些数据类型具有标准容器所应有的大部分特性,但又非全部。我们有时称这些数据类型为 拟容器。

    • T[N] 固定大小的内置数组;N个连续存储的类型为T的元素;没有size()或其他成员函数
    • array<T, N> 固定大小的数组,N个连续存储的类型为T的元素,类似内置数组,但解决了大部分问题。
    • basic_string<C, Tr, A> 一个连续分配空间的类型为C的字符序列,支持文本处理操作,例如连接(+, +=);basic_string通常都经过了优化,短字符串无须使用自由存储空间。
    • string basic_string
    • u16string basic_string
    • u32string basic_string
    • wstring basic_string
    • valarray 数值向量,支持向量运算,但有一些限制,这些限制是为了鼓励高性能实现;只在做大量向量运算时使用
    • bitset N个二进制位的集合,支持集合操作,例如&和|
    • vector vector的特例化版本,紧凑保存二进制位
  • 对basic_string,A是其分配器,Tr是字符萃取

  • 如果可以选择的话,应该有限选择vector,string或array这样的容器,而不是内置数组。内置数组有两个问题–数组到指针的隐式类型转换和必须要记住大小,它们都是错误的主要来源。

  • 还应该优先选择标准字符串,而不是其他字符串或C风格字符串。C风格字符串的指针语义意味着笨拙的符号表示和程序员的额外工作,它也是主要错误来源之一(例如内存泄漏)

31.2.1

  • C++标准并未给标准容器规定特定的表示形式,而是指明了容器接口和一些复杂性要求。实现者会选择适当的(通常也是巧妙优化过的)实现方法来满足一般要求和常见用途。除了处理元素所需的一些内容之外,这类句柄还持有一个分配器。

  • 对于一个vector,其元素的数据结构很可能是一个数组。vector会保存指向一个元素组的指针,还会保存元素数目和向量容量(已分配的和尚未使用的位置数)或等价的一些信息

  • list很可能表示为一个指向元素的链接序列以及元素数目

  • forward_list很可能表示为一个指向元素的链接序列

  • map很可能实现为一颗平衡树,树节点指向(键,值)对

  • unorder_map很可能实现为一个哈希表

  • string的实现可能为:短string的字符保存在string句柄内,而长string的元素则保存在自由存储空间中的连续区域(类似vector的元素)。类似vector,一个string也会预留空闲空间,以便扩张时不必频繁的重新分配空间

  • 类似内置数组,array就是一个简单的元素序列,无句柄。这意味着一个局部array不会使用任何自由存储空间(除非它本身实在自由存储空间中分配的),而且一个类的array成员也不会悄悄带来任何自由存储空间操作。

31.2.2 对元素的要求

  • 若想作为一个容器的元素,对象类型必须允许容器拷贝,移动以及交换元素。如果容器使用拷贝构造函数或拷贝赋值操作拷贝一个元素,拷贝的结果必须是一个等价的对象。这大致意味着任何对象值相等性检测都必须得到副本和原值相等的结论。换句话说,元素拷贝必须能像int的普通拷贝一样正常工作。

  • 类似的,移动构造函数和移动赋值操作也必须具有常规定义和常规移动语义。此外,元素类型还必须允许按常规语义交换元素。如果一个类型定义了拷贝或移动操作,则标准库swap()就能正常工作。

  • 对元素类型的要求和细节散布在C++标准中,很难阅读,但基本上,如果一个类型具有常规的拷贝或移动操作,容器就能保存该类型元素。只要满足容器元素的基本要求和算法的特定要求(例如元素有序),很多基本算法,例如copy(),find()和sort()都能正常运行。

  • 当无法拷贝对象时,一个替换方案是将对象指针而不是对象本身保存在容器中。最典型的例子就是多态类型。例如,我们使用vector<unique_prt>或vector<Shape*>,而不是vector来保证多态性行为

31.2.2.1 比较操作
  • 关联容器要求其元素能够排序,很多可以应用于容器的操作也有此要求。默认情况下,< 运算符被用来定义序。如果 < 不合适,程序员必须提供一个替代操作。
  • 排序标准必须定义一个严格弱序(strict weak ordering)。形式化地描述,即小于和相等关系(如果定义了的话)都必须是传递的。

31.3 操作概览

  • 常量,表示操作花费的时间不依赖于容器中的元素数目;常量时间(constant time)的另一种常见表示方式是O(1)。O(n)表示操作花费的时间与元素数目成正比
  • 所有容器的size()操作都是常量时间的。

31.3.1 成员类型

  • 每个容器都定义了如下一组成员类型
    • value_type 元素类型
    • allocator_type 内存管理类型
    • size_type 容器下标,元素数目等无符号类型
    • difference_type 迭代器差异的带符号类型
    • iterator 行为类似value_type*
    • const_iterator 行为类似 const_value_type*
    • reverse_iterator 行为类似 value_type*
    • const_reverse_iterator 行为类似 const_value_type*
    • reference const_value_type&
    • const_reference 行为类似value_type*
    • pointer 行为类似 value_type*
    • const_pointer 行为类似 const_value_type*
    • kep_type 关键字类型;仅关联容器具有
    • mapped_type 映射值类型;仅关联容器具有
    • key_compare 比较标准类型;仅有序容器具有
    • hasher 哈希函数类型: 仅无序容器具有
    • key_euqal 等价性检验函数类型;仅无需容器具有
    • local_iterator 桶迭代器类型;仅无需容器具有
    • const_local_iterator 桶迭代器类型;仅无需容器具有
  • 每个容器和 拟容器 都提供了上表中大多数成员类型,但是不会提供无意义的类型

31.3.2 构造函数,析构函数和赋值操作

  • 赋值操作并不拷贝或移动分配器,目标容器获得了一组新的元素,但会保留其旧分配器,新元素(如果有的话)的空间也是用此分配器分配的。
  • 谨记,一个构造函数或是一次元素拷贝可能会抛出异常,来指出它无法完成这个任务
  • 紧急,对大小初始化器使用 (),而对其他所有初始化器都是用 {}
  • 容器通常都很大,因此我们几乎总是以引用方式传递容器实参。但是,由于容器是资源句柄,我们可以高效地以返回值的方式返回容器(隐含使用移动操作)。类似地,当我们不想用别名的时候,可以用移动方式传递容器实参。

31.3.3 大小和容量

  • 大小是指容器中的元素数目;容量是指在重新分配更多内存之前容器能够保存的元素数目。
  • 在改变大小或容量时,元素可能会被移动到新的存储位置。这意味着指向元素的迭代器(以及指针和引用)可能会失效(即,指向旧元素的位置)
  • 指向关联容器(例如map)元素的迭代器只有当所指元素从容器中删除(erase())时才会失效。与之相反,指向顺序容器(例如vector)元素的迭代器当元素重新分配空间(例如resize(), reverse()或push_back())或所指元素在容器中移动(例如在前一个位置进行erase()或insert())时也会失效。

31.3.4 迭代器

  • 容器可以看作按容器迭代器定义的顺序或相反的顺序排列的元素序列。对一个关联容器,元素的序由容器比较标准(默认为 < )决定
  • 元素遍历的最常见形式是从头至尾遍历一个容器。最简单的遍历方法是使用范围for语句,它隐含的使用了begin()和end()
  • 当我们需要了解一个元素在容器中的位置或需要同时引用多个元素时,就要直接使用迭代器。在这些情况下,auto很有用,它能帮助尽量简化代码并减少输入错误。

31.3.5 元素访问

31.3.6 栈操作

  • 标准vector,deque和list(不包括forward_list和关联容器)提供了高效的元素序列尾部操作

  • 栈操作

    • c.push_back(x) 将x添加到c的尾元素之后(使用拷贝或移动)
    • c.pop_back() 删除c的尾元素
    • c.emplace_back(args) 用args构造一个对象,将它添加到c的尾元素之后
  • c.push_back(x)将x移动或拷贝入x,这会将c的大小增加1。如果内存耗尽或x的拷贝构造函数抛出一个异常,c.push_back(x)会失败。push_back()失败不会对容器造成任何影响,因为标准库操作都提供了强保证。

31.3.7 列表操作

31.3.8 其他操作

  • 容器可以比较和交换
  • swap()操作既交换元素也交换分配器

31.4 容器

31.4.1 vector

  • STL的vector是默认容器–除非你有充分理由,否则应该使用它。如果你希望使用链表或内置数组替代vector,应慎重考虑后再做决定。
31.4.1.1 vector和增长
  • 使用大小(元素数目)和容量(不重新分配空间的前提下可容纳的元素数目)零push_back()操作时的向量增长相当高效:不会在添加每个元素时都分配内存,而是在超出容量时才进行一次重新分配。C++标准并未指出超出容量时向量的增长幅度,但很多C++实现都是增加大小的一半。
  • 容量的概念令指向vector元素的迭代器只有在真正发生重分配时才会失效。

31.3.1.2 vector和嵌套

  • 与其他数据结构相比,vector(以及类似的连续存储元素的数据结构)有三个主要优势
    • vector的元素是紧凑存储的:所有元素都不存在额外的内存开销。类型为vector的vec的内存消耗大致为 sizeof(vector) + vec.size() * sizeof(x)。其中sizeof(vector) 大约为12个字节,对大向量而言是微不足道的
    • vector的遍历非常快。为访问下一个元素,我们不必利用指针间接寻址,而且对类vector结构上的连续访问,现代计算机都进行了优化。这使得vector元素的线性扫描(就像find()和copy()所做的)接近最优
    • vector支持简单且高效的随机访问。这使得vector上的很多算法(例如sort()和binary_search())非常高效

31.3.1.3 vector和数组

  • vector是一种资源句柄,这是允许改变大小和实现高效移动语义的原因。但是这一特点偶尔也会变为缺点–尤其是与不依赖于元素和句柄分离存储的数据结构(例如内置数组和array)相比。将元素序列保存在栈中或另一个对象中可能会带来性能上的优势。

31.3.1.4 vector和string

  • vector是可改变大小,连续存储的char序列,string也是如此。那么我们应该如何在两者之间进行选择呢
  • vector是一种保存值的通用机制,并不对保存的值之间的关系做任何假设。对一个vector而言,字符串Hello,World只不过是一个13个char类型的元素的序列而已。与之相反,string的设计目的就是保存字符序列,它认为字符间的关系是非常重要的。因此,我们很少会对string中的字符进行排序,因为这会破坏字符串的含义。某些string操作反映了这一点(例如c_str(), >> 和 find() C风格字符串以0结束)。string的实现也反映了对其使用方式的假设。

31.4.2 链表

  • STL提供了两种链表类型

    • list:双向链表
    • forward_list:单向链表
  • list为元素插入和删除操作进行了优化。当你向一个list插入元素或是从一个list删除元素时,list中其他元素的位置不会受到影响。特别的是,指向其他元素的迭代器也不会受到影响。

  • 默认情况下,list的元素都独立分配内存空间,而且要保存指向前驱和后继的指针。与vector相比,每个list元素占用更多内存空间(通常每个元素至少多4个字),遍历(迭代)操作也要慢得多,因为需要通过指针进行间接寻址而不是简单的连续访问。

  • forward_list是单向链表。你可以将其看作一种为空链表或很短的链表专门优化的数据结构,对这类链表的操作通常是从头开始遍历。

31.4.3 关联容器

  • 关联容器支持基于关键字的查找。它有两个变体
    • 有序关联容器(ordered associative container)基于一个序标准(默认是小于比较操作 <)进行查找。这类容器用平衡二叉树实现,通常是红黑树
    • 无序关联容器(unordered associative container)基于一个哈希函数进行查找。这类容器用哈希表实现,采用溢出链表策略。
  • 两类容器都支持
    • map: {键,值}对序列
    • set: 不带值的map(或者你可以说关键字就是值)
  • 最后,映射和集合,无论是有序还是无序的,都有两个变体
    • 普通映射或集合:每个关键字只有唯一一项
    • 多重映射或集合:每个关键字可对应多项。
  • 一个关联容器的名字指出了它在三维空间{集合|映射,普通|无序,普通|多重}中的位置。

31.4.3.1 有序关联容器

  • 实际上,[]并不仅仅是insert()的简写形式,它所做的要更多一些。m[k]的结果等价于 (*(m.insert(make_pair(k,v{})).first)).second,其中V是映射类型。insert(make_pair())这种描述方式相当冗长,我们可以用emplace()取而代之:dictionary.emplace(“sea cow”, “extinct”);

  • 关联容器中元素的关键字是不可变的。因此,我们不能改变set中的值。我们甚至不能改变不参与比较的元素的成员。如果需要修改元素,应使用map。不要尝试修改关键字:假如你成功了,就意味着查找元素的底层机制会崩溃。

31.4.3.2 无序关联容器

  • 无序关联容器都是用哈希表实现的。对简单应用而言,无序关联容器与有序容器的差别不大,因为关联容器共享大部分操作。
  • unordered_map的遍历顺序取决于插入顺序,哈希函数和装载因子。特别是,元素的遍历顺序并不保证与其插入顺序一致。

31.4.3.3 构造unordered_map

31.4.3.4 哈希和相等判定函数

  • 用户可以自定义哈希函数,定义方式有多种,不同的技术可满足不同的需求。

31.4.3.5 装载因子和桶

  • 无序容器实现的重要部分对程序员是可见的。我们说具有相同哈希值的关键字 落在同一个桶中。程序员也可以获取并设置哈希表的大小。
  • 无序关联容器的装载因子(load factor)定义为已用空间的比例。例如,若capacity()为100个元素,size()为30,则load_factor()为0.3
  • 桶接口的一个用途是允许对哈希函数进行实验: 一个糟糕的哈希函数会导致某些关键字值的bucket_count()异常大,即,很多关键字被映射为相同的哈希值。

31.5 容器适配器

  • 容器适配器(container adaptor)为容器提供不同的(通常是受限的)的接口。容器适配器的设计用法就是仅通过其特殊接口使用。特别是,STL容器适配器不提供直接访问其底层容器的方式,也不提供迭代器或下标操作。
  • 从一个容器创建容器适配器的技术是一种通用的按用户需求非侵入式适配类接口的技术。

31.5.1 stack

  • stack是一个容器接口,容器类型是作为模板实参传递给它的。stack通过接口屏蔽了其底层容器上的非栈操作,接口采用常规命名: top(), push()和pop()
  • 此外,stack还提供了常用的比较运算符(==, <等)和非成员函数swap()
  • 默认情况下,stack用deque保存其元素,但任何提供back(),push_back()和pop_back()操作的序列都可使用。例如
    1
    2
    stack<char> s1;   // 使用deque<char>保存元素
    stack<int, vector<int>> s2; // 使用vector<int>保存元素
  • vector通常比deque更快,使用内存也更少
  • stack对底层容器使用push_back()来添加元素。因此,只要机器还有可用内存供容器申请,stack就不会溢出。
  • 默认情况下,stack使用其底层容器的分配器。如果这不够,有几个构造函数可以指定分配器。

31.5.2 queue

  • queue定义在中,它是一个容器接口,允许在back()中插入元素,在front()中提取元素

31.5.3 priority_queue

  • priority_queue是一种队列,其中每个元素都被赋予一个优先级,用来控制元素被top()获取的顺序。priority_queue的声明非常像queue,只是多了处理一个比较对象的代码和一组从序列进行初始化的构造函数。
  • 默认情况下,priority_queue简单的用 < 运算符比较元素,用top()返回优先级最高的元素

31.6 建议

  • 一个STL容器定义一个序列
  • 将vector作为默认容器使用
  • insert()和push_back()这样的插入操作在vector上通常比在list上更高效
  • 将forward_list用于通常为空的序列
  • 当设计性能时,不要盲目信任你的直觉,而要进行测试
  • 不要盲目信任渐进复杂性度量;某些序列很短而单一操作的代价差异可能很大
  • STL容器都是资源句柄
  • map通常实现为红黑树
  • unordered_map是哈希表
  • STL容器的元素类型必须提供拷贝和移动操作
  • 如果你希望保持多态行为,使用指针或智能指针的容器
  • 比较操作应该实现一个严格弱序
  • 以传引用方式传递容器参数,以传值方式返回容器
  • 对一个容器,用()初始化器初始化大小,用{}初始化器语法初始化元素列表
  • 用范围for循环或首位迭代器对容器进行简单遍历
  • 如果不需要修改容器元素,使用const迭代器
  • 当使用迭代器时,用auto避免冗长易错的输入
  • 用reserve()壁面指向容器元素的指针和迭代器失效
  • 未经测试不要假定reserve()会有性能收益
  • 使用容器上的push_back()或resize(),而不是数组上的realloc()
  • vector和deque改变大小后,不要继续使用其上的迭代器
  • 在需要时使用reserve()令性能可预测
  • 不要假定[]会进行范围检查
  • 当需要保证进行范围检查时使用at()
  • 用emplace()方便符号表示
  • 优选紧凑连续存储的数据结构
  • 用emplace()避免提前初始化元素
  • 遍历list的代价相对较高
  • list一般会有每个元素四个字的额外内存开销
  • 有序容器序列由其比较对象(默认为 <)定义
  • 无序容器(哈希容器)序列并无可预测的序
  • 如果你需要在大量数据中快速查找元素,使用无序容器
  • 对五自然序的元素类型,使用无序容器
  • 如果需要按顺序遍历元素,使用有序关联容器
  • 用实验检查你的哈希函数是否可以接受
  • 用异或操作组合标准哈希函数得到的哈希函数通常有很好的性能
  • 0.7通常是一个合理的装载因子
  • 你可以为容器提供其他接口
  • STL适配器不提供对其底层容器的直接访问

第三十二章 STL算法

32.1 引言

  • STL包含标准库中的迭代器,容器,算法和函数对象几个部分

32.2 算法

  • 中定义了大约80个标准算法。

  • 很多算法都遵循一种常规表示方式: 返回序列的末尾来表示 未找到。

  • 无论是标准库算法还是用户自己设计的算法,都很重要

    • 每个算法命名一个特定操作,描述其接口,并指定其语义
    • 每个算法都可能广泛使用并被很多程序员熟知
  • 如果你发现你写的一段代码有若干看起来没什么关联的循环,局部变量,或是有很复杂的控制结构,那么就应该考虑是否可以简化代码,将某些部分改写为具有描述性的名字以及良好定义的目的,接口和依赖关系的函数/算法。

32.2.1 序列

  • 标准库算法的理想目标是为可优化实现的某些东西提供最通用最灵活的接口。
  • 注意,无论一个STL算法返回什么,它都不会是实参的容器。传递给STL算法的实参是迭代器,算法完全不了解迭代器所指向的数据结构。
  • 迭代器的存在主要是为了将算法从它所处理的数据结构上分离开来,反之亦然。

32.8 建议

  • STL算法操作一个或多个序列
  • 一个输入序列是一个半开区间,由一对迭代器定义
  • 进行搜索时,算法通常返回输入序列的末尾位置表示 未找到
  • 优选精心说明的算法而非 随意代码
  • 当你要编写一个循环时,思考它是否可以表达为一个通用算法
  • 确保一对迭代器实参确使指定了一个序列
  • 当迭代器对风格显得冗长时,引入容器/范围版本的算法
  • 用谓词和其他函数对象赋予标准算法更宽泛的含义
  • 谓词不能修改其实参
  • 指针上默认的==和 < 极少能满足标准算法的需求
  • 了解你使用的算法的时间复杂性,但要记住复杂性评价只是对性能的粗略引导
  • 只在对一个任务没有更专用的算法时使用 for_each() 和 transform()
  • 算法并不直接向其实参序列添加元素或从其中删除元素
  • 如果不得不处理未初始化的对象,考虑 uninitialized_* 系列算法
  • STL算法基于其排序比较操作实现相等性比较
  • 注意,排序和搜索C风格的字符串要求用户提供一个字符串比较操作。

第三十三章 STL迭代器

33.1 引言

  • 迭代器是标准库算法和所操作的数据间的粘合剂。反过来,也可以说迭代器机制是为了最小化算法与所操作的数据结构间的依赖性

33.1.1 迭代器模型

  • 与指针类似,迭代器提供了简介访问的操作(例如解引用操作 *)和移动到新元素的操作(例如,++操作移动到下一个元素)。一对迭代器定义一个半开区间 [begin:end),即所谓序列(sequence)
    • 即,begin指向序列的首元素,end指向序列的尾元素之后的位置。永远也不要从 *end 读取数据,也不要向它写入数据。
    • 注意,空序列满足 begin == end;即,对任意迭代器p都有 [p:p)是空序列
  • 为了 读取一个序列,算法通常接受一对表示半开区间[begin:end)的迭代器(b, e),并使用++编译序列直至到达末尾

33.1.2 迭代器类别

  • 标准库提供了五种迭代器

    • 输入迭代器(input iterator): 利用输入迭代器,我们可以用++向前遍历序列并用*(反复)读取每个元素。我们可以用==和!=比较输入迭代器。istream提供了这种迭代器
    • 输出迭代器(output iterator): 利用输出迭代器,我们可以用++向前遍历序列并用*每次写入一个元素。ostream提供了这种迭代器
    • 前向迭代器(forward iterator): 利用前向迭代器,我们可以反复使用++向前遍历序列并用*读写元素(除非元素是const的)。如果一个前向迭代器指向一个类对象我们可以用->访问其成员。我们可以用==和!=比较前向迭代器。forward_list提供了这种迭代器
    • 双向迭代器(bidirection iterator): 利用双向迭代器,我们可以向前(用++)和向后(用–)遍历序列并用*(反复)读写元素(除非元素是const的)。如果一个双向迭代器指向一个类对象,我们可以用->访问其成员。我们可以用==和!=比较双向迭代器。list,map和set提供了这种迭代器
    • 随机访问迭代器(random-access iterator): 对一个随机访问迭代器,我们可以用[]进行下标操作,用+加上一个整数,以及用-减去一个整数。我们可以将指向同一个序列的两个随机访问迭代器相减来获得他们的距离。我们可以用==,!=,<=, >和>=比较双向迭代器。vector提供了这种迭代器
  • 这些迭代器类别是概念而非类,因此这个层次并非用继承实现的类层次。如果你希望用迭代器类别做一些更进阶的事情,可(直接或间接)使用iterator_traits。

33.1.3 迭代器萃取

  • 迭代器标签的本质是类型,用来基于迭代器类型选择算法。
  • 关键思路是:为了获得迭代器的属性,你应该访问其 iterator_traits而不是迭代器本身

33.1.4 迭代器操作

33.2 迭代器适配器

  • 中,标准库提供了适配器,能从一个给定的迭代器类型生成有用的相关迭代器类型
    • reverse_iterator 反向遍历
    • back_insert_iterator 在尾部插入
    • front_insert_iterator 在头部插入
    • insert_iterator 在任意位置插入
    • move_iterator 移动而不是拷贝
    • raw_storage_iterator 写入未初始化的存储空间

33.4 函数对象

  • 很多标准库算法接受函数对象(或函数)参数,来控制其工作方式。常见的函数对象包括比较标准,谓词(返回bool的函数)和算术运算。在中,标准库提供了若干常用函数对象。

33.5 函数适配器

  • 函数适配器接受一个函数参数,返回一个可用来调用该函数的函数对象。
    • g = bind(f, args)
      • g(args2)等价于f(args3)是通过用args2中的实参替换args中对应的占位符(例如 _1, _2, _3)得到的
    • g = mem_fn(f) 若p是一个指针,则g(p, args)表示p->f(args),否则g(p, args)表示p.mf(args); args是一个实参列表
    • g = not1(f) g(x)表示!f(x)
    • g = not2(f) g(x, y) 表示 !f(x, y)

33.5.3 function

  • 我们可以直接使用bind(),以及用它来初始化auto变量。从这个角度看,bind()很像是一个lambda
  • 标准库function是一种类型,它可以保存你能调用运算符()调用的任何对象。即,一个function类型对象就是一个函数对象
  • 显然,function对回调,将操作作为参数传递等机制非常有用

33.6 建议

  • 一个输入序列由一对迭代器定义
  • 一个输出序列由单一迭代器定义;程序员应负责避免溢出
  • 对任意迭代器p,[p:p)是一个空序列
  • 使用序列列尾表示 未找到
  • 将迭代器理解为更通用,通常行为也更好的指针
  • 使用迭代器类型,例如 list::iterator,而不是指向容器中元素的指针
  • 用iterator_traits获取迭代器的相关信息
  • 可以用iterator_traits实现编译时分发
  • 用iterator_traits实现基于迭代器类别选择最优算法
  • 用base()从reverse_iterator提取iterator
  • 可以使用插入迭代器向容器添加元素
  • move_iterator可用来将拷贝操作变为移动操作
  • 确认你的容器可用范围for语句遍历
  • 用bind()创建函数和函数对象的变体
  • 注意bind()会提前解引用;如果你希望推迟解引用,使用ref()
  • 可使用mem_fn()或lambda将p->f(a)调用规范转换为f(p, a)
  • 如果你需要一个可以保存各种可调用对象的变量,使用function

第三十四章 内存和资源

34.1 引言

  • STL时标准库中高度结构化的,通用的数据管理和操作组件。本章介绍更为专用的以及处理裸内存的(与处理强类型对象相对)组件

34.2 拟容器

  • 标准库中有一些容器不能很好的纳入STL框架,例如内置数组,array和string。我有时将他们称为拟容器。

    • T[N] 固定大小的内置数组,连续存储的N个类型为T的元素,隐式转换为T*
    • array<T,N> 固定大小的数组,连续存储的N个类型为T的元素,类似内置数组,但解决了大部分问题
    • bitset 固定大小的N个二进制的序列
    • vector vector的特例化版本,紧凑保存二进制位序列
    • pair<T, U> 两个元素,类型为T和U
    • tuple<T…> 任意数目任意类型的元素的序列
    • basic_string 类型为C的字符的序列,提供了字符串操作
    • valarray 类型为T的数值的数组,提供了数值运算
  • 为什么标准库会提供这么多容器?这是为了满足很多常见但又有差异(通常也有重叠)的需求。如果标准库不提供这些容器,很多人将不得不自己实现。例如

    • pair和tuple是异构的,所有其他容器都是同构的(元素都是相同类型)
    • array, vector和tuple连续保存元素;forward_list和map是链式结构
    • bitset和vector保存二进制位,通过代理对象访问这些二进制位;所有其他标准库容器都可以保存不同类型并直接访问元素
    • basic_string要求其元素位某种字符类型,它提供了字符串操作,例如连接操作和区域敏感操作,valarray要求其元素为数值类型,并提供数值运算。

34.2.1 array

  • array定义在中,是固定大小的给定类型的元素的序列,元素数目在编译时指定。因此,连同其元素可以在栈中,对象内或静态存储中分配空间。

  • array在哪个作用域中定义,元素就会在其中分配。理解array的最好方式是将其视为固定大小的内置数组,但不会隐式的,出乎意料地转换为指针类型,且提供了一些便利的函数

  • array中不保存任何管理信息(例如大小)。这意味着移动一个array比拷贝它更为高效(除非array的元素是资源句柄,且定义了高效的移动操作)。array没有构造函数或分配器(因为它不直接分配任何东西)

  • array的元素数目和下标值是unsigned类型(size_t)

  • 元素数目必须是一个常量表达式

  • 如果需要可变元素数目,应该使用vector。另一方面,由于array的元素数目在编译时即知,array的size()是一个constexpr函数

  • 如果需要,可以将一个array作为指针显式的传递给一个C风格的函数

  • vector是如此灵活,我们为什么要使用array呢?

    • 原因是array虽不如vector灵活,但更简单。少数情况下,直接访问分配在栈中的元素较之在自由存储空间中分配元素,然后通过vector(句柄)间接访问它们,最后将它们释放,会有巨大的性能优势。
    • 当然另一方面,栈是一个有限的资源(特别是在一些嵌入式系统中),而栈溢出是非常糟糕的
  • 如果我们可以使用内置数组,又为什么要使用array呢?

    • array了解自己的大小,因此很容易使用标准库算法,而且可以拷贝(用=或初始化)。
    • 但是选择array的主要原因是,它使我不必为糟糕的指针转换头疼。

34.2.2 bitset

  • 一个bitset就是一个包含N个二进制位的数组,它定义在中。它与vector的不同之处是大小固定,与set的不同之处是二进制位用整数索引而不是关联值,与vector和set的共同差异是提供了操作二进制位的操作。

  • 用内置指针是不可能寻址一个二进制位的。因此,bitset提供了一种位引用(代理)类型。对那些由于某种原因不适合用内置指针寻址的对象,通常这种技术是很有用的解决方案

  • bitset设计中的一个关键思想是:能放入单个机器字中的bitset可优化实现。其接口反映了这一思想。

34.2.3 vector

  • vector定义在中,它是vector的一个特例化版本,提供了二进制(bool值)的紧凑存储
  • 显然,vector与bitset很相似,与bitset不同而与vector相似的是,vector具有分配器,也能改变大小。
  • 类似vector,vector中索引更大的元素保存在高地址。这与bitset的内存布局完全相反。而且标准库也提供将整数和字符串直接转换为vector的操作。

34.2.4 元组

  • 标准库提供了两种将任意类型的值组成单一对象的方法
    • pair保存两个值
    • tuple保存零个或多个值
  • 如果我们预先知道恰好有两个值,pair就很有用处了。而tuple用于我们必须处理任意多个值的情况

34.3 资源管理指针

  • 一个指针指向一个对象(或不指向任何东西)。但是,指针并不能指出谁(如果有的话)拥有对象。即,仅仅查看指针,我们得不到任何关于”谁应(或是如何,或是是否)删除对象”的信息。
  • 中,我们可以找到表达所有权的智能指针
    • unique_ptr: 表示互斥的所有权
    • shared_ptr: 表示共享的所有权
    • weak_ptr: 可打破循环共享数据结构中的回路

34.3.1 unique_ptr

  • unique_ptr提供了一种严格的所有权语义

    • 一个unique_ptr拥有一个对象,它保存一个指针(指向该对象)。即,unique_ptr有责任用所保护的指针销毁所指向的对象(如果有的话)
    • unique_ptr不能拷贝(没有拷贝构造函数和拷贝赋值函数),但是可以移动
    • unique_ptr保存一个指针,当它自身被销毁时,使用关联的释放器(如果有的话)释放所指向的对象(如果有的话)
  • unique_ptr的用途包括

    • 为动态分配的内存提供异常安全
    • 将动态分配内存的所有权传递给函数
    • 从函数返回动态分配的内存
    • 在容器中保存指针
  • unique_ptr不提供拷贝构造函数和拷贝赋值运算符。

34.3.2 shared_ptr

  • shared_ptr表示共享所有权。当两段代码需要访问同一个数据,但两者都没有独享所有权(负责销毁对象)时,可以使用shared_ptr。shared_ptr是一种计数指针,当计数变为零时释放所指向的对象。

  • 我们可以将共享指针理解为包含两个指针的结构:一个指针指向对象,另一个指针指向计数器。

  • 释放器(deleter)用来在计数器变为零时释放共享对象。默认释放器通常是delete(它会调用对象的析构函数(如果存在的话),并释放自由存储空间)

  • 当可以选择时

    • 优先选择unique_ptr而不是shared_ptr
    • 优先选择普通限域对象而不是在堆中分配空间,由unique_ptr管理所有权的对象

34.3.3 weak_ptr

  • weak_ptr指向一个shared_ptr所管理的对象。为了访问对象,可使用成员函数lock()将weak_ptr转换为shared_ptr。weak_ptr允许访问他人拥有的对象
    • (仅)当对象存在时你才需要访问他
    • 对象可能在任何时间被(其他人)释放
    • 在对象最后一次被使用后必须调用其析构函数(通常释放非内存资源)

34.4 分配器

  • STL容器和string都是资源句柄,获取和释放内存来保存其元素。为此,它们使用分配器(allocator)。分配器的基本目的是为给定类型提供内存资源以及提供在内存不再需要时将其归还的地方。
  • 基本的分配器函数有
    1
    2
    p = a.allocate(n);  // 为n个类型为T的对象获取空间
    a.deallocate(p, n); // 释放p所指的保存n个类型为T的对象的空间

34.4.1 默认分配器

  • 所有标准库容器都(默认)使用默认分配器,它用new分配空间,用delete释放空间

34.5 垃圾收集接口

  • 垃圾收集(自动回收无引用的内存区域)有时被认为是万能灵药,但它并不是。特别是,垃圾收集器可能无法避免并非纯内存的资源的泄露,例如文件句柄,线程句柄以及锁。
  • 我将垃圾收集看作下列常见的防泄漏技术都已用尽时的最后一种方便的手段
    • 只要可能,应使用具有正确语义的资源句柄来防止应用程序中的资源泄露。标准库提供了string, vector, unordered_map, thread, lock_guard以及其他很多资源句柄。移动语义允许从函数高效返回这类对象
    • 使用unique_ptr保存这样的对象:不隐式管理其所拥有资源(例如指针),需要免受不成熟释放机制之害或是需要特别关注分配方式(释放器)、
    • 使用shared_ptr保存需要共享所有权的对象。

34.6 未初始化内存

  • 大多数情况下,最好避免使用未初始化的内存。这样做可以简化编程,消除很多错误。
  • 除了标准的allocator,头文件还提供了fill*系列函数用于处理未初始化内存。

34.7 建议

  • 当你需要一个具有constexpr大小的序列时,使用array
  • 优先选择array而不是内置数组
  • 当你需要N个二进制位而N又不到一定是整数类型的位宽时,使用bitset
  • 避免使用vector
  • 当使用pair时,考虑使用make_pair()进行类型推断
  • 当使用tuple时,考虑使用make_tuple()进行类型推断
  • 使用unique_ptr表示互斥所有权
  • 使用shared_ptr表示共享所有权
  • 尽量不适用weak_ptr
  • (仅)当由于逻辑上或性能上的原因,常用的new/delete语义不能满足需求时才使用分配器
  • 优先选择有特定语义的资源句柄而不是智能指针
  • 优先选择unique_ptr而不是shared_ptr
  • 优先选择智能指针而不是垃圾收集
  • 为通用资源的管理提供一致,完整的策略
  • 在大量使用指针的程序中处理泄露问题,垃圾收集是非常有用的
  • 垃圾收集是可选的
  • 不要伪装指针(即使你不使用垃圾收集)
  • 如果你使用垃圾收集,使用declare_no_pointers()令垃圾收集器忽略不可能包含指针的数据
  • 不要随机使用未初始化内存,除非你确使必须这么做。

第三十五章 工具

35.1 引言

  • 标准库提供了很多应用广泛的工具组件,但它们很难归到某类主要组件中。

35.2 时间

  • 中,标准库提供了处理时间段和时间点的组件。

  • 我们通常希望对某事计时或做某些依赖于时间的事情。例如,标准库互斥量和锁提供了让thread等待一段时间(duration)或等待到给定时刻(time_point)的选项。

  • 如果你希望获得当前的time_point,可以对3种时钟之一调用now(): system_clock, steady_clock和high_resolution_clock

  • 时钟返回一个time_point, 一个duration就是相同时钟的两个time_point间的距离。

  • 时间组件的设计目的之一是支持在系统深层中的高效使用;它们不提供社交日历便利维护这类组件。实际上,时间组件源自高能物理的迫切需求。

35.2.1 duration

  • 中,标准库提供了类型duration来表示两个时间点(time_point)间的距离

35.2.2 time_point

  • 中,标准库提供了类型time_point,用来表示给定纪元的一个时间点,用给定的clock度量
  • 一个纪元(epoch)就是由给定clock确定的一个时间范围,用duration来衡量,从duration::zero()开始

35.2.3 时钟

  • time_point和duration值归根结底是从硬件时钟获得的。
  • 系统提供了3个命名的时钟
    • system_clock 系统实时时钟;可以重置系统时钟(向前或向后跳)来匹配内部时钟
    • steady_clock 时间稳定推移的时钟,即时间不会回退且时钟周期的间隔是常量
    • high_resolution_clock 一个系统上具有最短时间增量的时钟

35.3 编译时有理数运算

  • 中定义了类ratio,提供了编译时有理数运算。标准库用ratio提供时间段和时间点的编译时表示
  • 其基本思想是将一个有理数的分子和分母编码为(值)模板实参。分母必须非零

35.4 类型函数

  • 中,标准库提供了类型函数,用来确定类型的属性(类型萃取)以及从已有类型生成新类型(类型生成器)

35.4.1 类型萃取

  • 中,标准库提供了多种类型函数,允许程序员确定一个类型或一对类型的属性。它们的名字大多是自解释的。主类型谓词(primary type predicate)检测类型的基本属性
  • 类型萃取返回一个布尔值。为了访问此值,可使用后缀::value

35.4.2 类型生成器

  • 中,标准库提供了从一个给定类型实参生成另一个类型的类型函数。
  • 一个类型转换器返回一个类型。为了访问这个类型,可以使用后缀::type

35.5 其他工具

35.5.1 move()和forward()

  • move()进行简单的右值转换。我们用move()告知编译器:此对象在上下文中不再被使用,因此其值可被移动,留下一个空对象
  • forward()的典型用法是将一个实参从一个函数 完美转发 到另一个函数。
  • 当希望用一个移动操作 窃取 一个对象的表达形式时,使用move();当希望转发一个对象时,使用forward()
  • 因此,forward(x)总是安全的,而move(x)标记x将被销毁,因此要小心使用。调用move(x)之后x唯一安全的用法就是析构或者是赋值的目的。

35.5.2 swap()

  • 中,标准库提供了一个通用的swap()和一个针对内置数组的特例化的版本
  • swap()不能用来交换右值

35.5.3 关系运算符

  • 中,标准库提供了任意类型的关系运算符,它们定义在子命名空间rel_ops中

35.5.4 比较和哈希type_info

  • 中,标准库提供了比较和哈希type_index的组件。一个type_index是从一个type_info创建的,专门用于这种比较和哈希。

35.6 建议

  • 组件,如steady_clock, duration和time_point进行计时
  • 优先使用组件而不是组件
  • 用duration_cast获得已知单位的时间段
  • 用system_clock::now()获得当前时间
  • 可以在编译时查询类型的属性
  • 仅当obj的值不再使用时使用move(obj)
  • 用forward()进行转发。

第三十六章 字符串

36.1 引言

  • 中,标准库提供了字符分类操作,在中提供了字符串相关操作,在中提供了正则表达式匹配组件,在中提供了C风格字符串支持。
  • 不同字符集的处理,编码和区域习惯将在第三十九章介绍

36.2 字符分类

  • 标准库提供了一些分类函数,帮助用户操纵字符串(及其他字符序列),还提供了一些指出字符类型属性的萃取,帮助实现字符串上的操作。

36.2.1 分类函数

  • 中,标准库提供了从基本运行字符集中分类字符的函数

    • isspace(c)
    • isalpha(c)
    • isdigit(c)
    • isxdigit(c)
    • isupper(c)
    • islower(c)
    • isalnum(c)
    • iscntrl(c)
    • ispunct(c)
    • isprint(c)
    • isgraph(c)
  • 此外,标准库提供了两种去除大小写区别的有用函数

    • toupper(c)
    • tolower(c)
  • 这些字符分类函数很有用,原因之一是字符分类其实比看起来要麻烦很多。

    • 例如,一个初学者可能会这样编写代码: if (‘a’ < ch && ch < ‘z’>) // 一个小写字母
    • 下面的写法更简洁,而且可能更高效: if (islower(ch)) // 一个小写字母
  • 更重要的是,在编码空间中,字母并不保证是连续编码的,所以第一段代码可能会出错。

36.2.2 字符萃取

  • 一个字符类型的属性由其char_traits定义。一个char_traits是以下模板的特例化版本
    • template struct char_traits {};
  • 所有char_traits都定义在命名空间std中,头文件中给出了标准char_traits。通用char_traits本身是没有属性的;只有特定字符类型的特例化char_traits才有属性

36.3 字符串

  • 中,标准库提供了通用字符串模板basic_string
  • 元素(字符串)是连续存储的,这样底层输入操作可以安全的将basic_string的字符序列作为源或目的。
  • basic_string提供了强保证:若一个basic_string操作抛出了异常,则字符串保持不变。
  • 与容器类似,basic_string的设计目的不是为了用作基类,而且它提供了移动语义,因此能高效地以传值方式由函数返回。

36.3.1 string和C风格字符串

  • C风格字符串与string的根本区别是,string是具有常规语义的真正类型,而C风格字符串则是一些有用的函数支撑的一组规范。

36.3.2 构造函数

  • basic_string提供了各式各样令人眼花缭乱的构造函数。

  • 最常用也是最简单的

    • string s0;
    • string s1 {“As simple as that”};
    • string s2 {s1};
  • 不要尝试用一个nullptr初始化一个string。最好情况下,你会得到一个糟糕的运行时错误,而最坏情况下,你会得到难以理解的未定义行为。

  • 值string::npos表示一个超出string长度的位置,通常用来表示 string 尾

36.3.3 基本操作

  • basic_string提供了比较操作,大小和容量控制操作以及访问操作
  • 用at()进行越界访问会抛出std::out_of_range。若+=(), push_back()或+会令size()超过max_size(),则抛出std::length_error

36.3.4 字符串I/O

  • 我们可以用 << 输出basic_string,以及用 >> 读取输入保存到basic_string中。若输入操作会令size()超过max_size(),则抛出std::length_error
  • getline()会从输入流中删除结束符(默认为 ‘\n\ ),但不将其存入字符串中。这简化了对输入的逐行处理。

36.3.7 find系列函数

  • 标准库提供了各种各样令人眼花缭乱的子串查找操作。照例,find()从s.begin()开始向后搜索,rfind()从s.end()开始向前搜索。find()函数用string::npos(非位置)来表示 未找到
  • find_*_of()系列函数与find()和rfind()的不同指出在于它查找单个字符而非一个字符序列

36.4 建议

  • 使用字符分类而非手工编写的代码检查字符范围
  • 如果实现类字符串的抽象,使用character_traits实现字符上的操作
  • 可用basic_string创建任意字符类型的字符串
  • 将string用作变量和成员而非基类
  • 优先选择string操作而非C风格字符串函数
  • 以传值方式返回string(依赖移动语义)
  • 将string::npos表示 string 剩余部分
  • 不要将nullptr传递给接受C风格字符串的string函数
  • string可以按需增长和收缩
  • 当需要进行范围检查时,使用at()而非迭代器或[]
  • 当需要优化速度时,使用迭代器或[]而非at()
  • 如果使用string,应在程序某些地方捕获length_error和out_of_range
  • 仅在必要时,用c_str()生成string的C风格字符串表示
  • string输入是类型敏感的且不会溢出
  • 优先选择string_stream或通用的值抽取函数而非直接使用str*系列数值转换函数
  • 使用find()操作在string中定位元素(而不是自己编写循环)
  • 直接或间接使用substr()读取字符串,用replace()写入子串

第三十八章 I/O流

38.1 引言

  • I/O流库提供了文本和数值的输入输出功能,这种输入输出是带缓冲的,可以是格式化的,也可以是未格式化的。
  • I/O流工具定义在等头文件中
    • ostream将有类型的对象转换为字符流(字节流)
    • istream将字符流(字节流)转换为有类型的对象

38.2 I/O流层次

  • 关键的类是basic_ios,其中定义了大多数实现和很多操作。

38.2.1 文件流

  • 中,标准库提供了读写文件的流
    • ifstream 用于从文件读取数据
    • ofstream 用于向文件写入数据
    • fstream 用于读写文件

38.2.2 字符串流

  • 中,标准库提供了读写string的流
    • istringstream 用于从string读取数据
    • ostringstream 用于向string写入数据
    • stringstream 用于读写string
  • 字符串流不提供拷贝操作。如果你希望两个名字指向同一个字符串流,可使用引用或指针

38.3 错误处理

  • 一个iostream在某个时刻会处于四种状态之一,这些状态的定义来自于的basic_ios中
    • good() 前一个iostream操作成功
    • eof() 到达输入尾(文件尾)
    • fail() 发生了出乎意料的事情(例如,读取数据却得到一个‘x’)
    • bad() 发生了处于医疗的严重事情(例如,磁盘读错误)
  • 如果一个流不在good()状态,对它的任何操作都没有效果,即相当于空操作。

38.7 建议

  • 若用户自定义类型的值存在有意义的文本表示,可为其定义 << 和 >>
  • 用cout进行正常输入,用cerr输出错误
  • 标准库提供了普通字符和宽字符的iostream,你可以为任意类型的字符定义iostream
  • 标准库为标准I/O流,文件和string定义了标准iostream
  • 不要尝试拷贝一个文件流
  • 二进制I/O依赖于系统
  • 在使用文件流之前记得检查它是否已关联到文件
  • 优先选择ifstream和ofstream,而非通用的fstream
  • 用stringfstream进行内存中的格式化
  • 用异常机制捕获稀少的bad() I/O错误
  • 用流状态fail处理潜在的可恢复的I/O错误
  • 为了定义新的 << 和 >> 你无需修改istream或ostream
  • 当实现iostream原语操作时,使用sentry
  • 优先选择格式化输入而非未格式化的,底层的输入
  • 读取输入存入string不会导致溢出
  • 当使用get(), getline() 和 read() 时,要小心结束标准
  • 默认忽略空白符

  • 优先选择操纵夫而非状态标志来控制I/O
  • 如果你希望混合C风格I/O和iostream-I/O,使用sync_with_stdio(true)
  • 使用sync_with_stdio(false)优化iostream
  • 连接流来实现交互式I/O
  • 用imbue()来令iostream反应locale的文化差异
  • width()说明只应用于紧接着的下一个I/O操作
  • precision()说明应用于后续所有浮点输出操作
  • 浮点格式说明(例如scientific)应用于后续所有浮点输出操作
  • 为使用接受参数的标准操纵符,要 #include
  • 你几乎不会需要flush()
  • 除非可能有审美趣味上的原因,否则不要使用 endl
  • 如果iostream格式化变得令人厌烦,编写你自己的操纵符
  • 通过定义一个简单的函数对象,你可以实现三元运算符的效果(和效率)

第四十章 数值计算

40.1 引言

  • 当复杂数据结构是计算过程中的必要部分时,C++的优势就变得很重要了。C++因而被广泛用于科学计算,工程计算,金融计算以及其他包含复杂数值计算的任务,这催生了这类计算的语言特性和技术。
  • 本章介绍标准库中支持数值计算的部分。我不打算讲授数值方法。数值计算本身就是一个吸引人的话题。学习数值计算需要一门好的数值方法课程或者至少有一本好的教材—而不是仅靠一本编程语言手册和导引就能完成的。

40.2 数值限制

  • 为了处理数值相关的有趣事情,我们通常需要了解一些内置数值类型的一般特性的知识。为了让程序员能最充分的利用硬件,这些特性都是依赖于具体C++实现,而不是由语言本身规定的。
  • 例如
    • 最大的int有多大?
    • 最小的正float是什么?
    • 将一个double赋予一个float时,是舍入还是截断?
    • 一个char有多少位?
  • 这类问题的答案由 numeric_limits 的特例化版本提供,它定义在
  • 每个特例化版本提供其实参类型的相关信息。因此,通用numberic_limits模板就是一组常量和constexpr函数的标志句柄。真正的信息保存在特例化版本中。

40.2.1 数值标量限制C风格宏

  • C++ 从C继承了描述整数属性的宏,它们定义在

40.3 标准数学函数

  • 中我们可以找到通常被称为标准数学函数(Standard Mathematical Function)的组件

40.7 随机数发生器

  • 中,标准库定义了生成(伪)随机数的特性。这种随机数是按照数学公式生成的值的序列,而不是无法猜测(真随机)的数,后者可以从物理过程中获得,例如放射性衰变或太阳辐射。

  • 标准库提供了四种随机数相关的实体

    • 均匀随机数发生器(uniform random number generator)是一个返回无符号整数值的函数对象,值域中每个可能值(理想情况下)被返回的概率相等
    • 随机数引擎(random number engine, 简称引擎)是一个均匀随机数发生器,可用默认状态创建,或者用一个seed确定的状态创建
    • 随机数引擎适配器(random number engine adaptor, 简称适配器)是一个随机数引擎,它接受某个其他随机数引擎生成的值,并应用算法将这些值转换为另一个具有不同随机特性的值的序列。
    • 随机数分布(random number distribution, 简称分布)是一个函数对象,其返回值的分布服从一个关联的数学概率密度函数或一个关联的离散概率函数
  • 符合用户习惯的更简单的描述是,一个随机数发生器就是一个引擎加一个分布。引擎生成一个均匀分布的值的序列,分布再将这些值转换为要求的形状(分布)。即,如果你从随机数发生器接受了大量数值并绘制它们,就会得到一个描述它们分布的相当平滑的图形。

  • 大多数请款下,大多数程序员只需要一个给定范围内假肚腩的均匀分布整数序列或浮点数序列。

40.7.4 C风格随机数

  • 和<stdlib.h>中,标准库提供了一些简单的特性来生成随机数

    1
    2
    3
    4
    #define RAND_MAX implementation_defined /*最大可能整数*/

    int rand(); // 0和RAND_MAX间的伪随机数
    void srand(unsigned int i); // 将随机数发生器的种子设置为i
  • 调用srand(s)用种子(seed)s(作为参数提供)爱是一个新的随机数序列。为了方便调试,一个给定种子生成固定的序列通常很重要。但是,我们通常希望用一个新种子开始程序的每次运行。

第四十一章 并发

41.1 引言

  • 并发,即多个任务同时执行,广泛用于提高吞吐率(使用多个处理器完成单个运算)或提高响应能力(当程序的一部分等待响应时允许另一部分继续执行)

  • 如果一项活动可能与其他活动并发执行,我们就称之为任务(task)。线程(thread)是执行任务的计算机特性在系统层面的表示。一个标准库thread可执行一个任务。一个线程可与其他线程共享地址空间。即,在单一地址空间中的所有线程能访问相同的内存位置。而并发系统程序员所面临的重要挑战之一就是,确保多线程并发访问内存的方式是合理的。

  • 标准库对并发的支持包括

    • 内存模型(memory model): 这是对内存并发访问的一组保证,主要是确保简单的普通访问能按人们的朴素的预期工作
    • 对无锁编程(programming without locks)的支持: 这是一些避免数据竞争的细粒度底层机制
    • 一个线程(thread)库: 这是一组支持传统线程–锁风格的系统级并发编程的组件,例如thread, condition_variable, mutex
    • 一个任务(task)支持库: 这是一些支持任务级并发编程的特性: future, promise, packaged_task, async()
  • 这些主题是按照从最基础,最底层到最高层的顺序排列的。内存模型是所有编程风格所共用的。为提高程序员开发效率,尽量减少错误,应在尽可能高的层次上编程。例如,应该优先选择future而不是mutex实现信息交换;除非是简单的计数器,否则应该优选mutex而不是atomic;诸如此类,尽量将复杂任务留给标准库实现者。

  • 在C++标准库的语境中,一个锁(lock)就是一个mutex(互斥量)以及任何构建于mutex之上的抽象,用来提供对资源的互斥访问或同步多个并发任务的进度。

  • 进程(process)即运行于独立地址空间,通过进程间通信机制进行交互的线程,并不在本书介绍范围之内。

  • 类似的,我们可以以函数对象(例如lambda)的形式定义任务并将它们传递给线程,而无需进行类型转换或担心类型违规。

41.2 内存模型

  • C++实现大多标准库组件的形式提供对并发机制的支持。这些组件依赖于一组称为内存模型(memory model)的语言保证。内存模型是计算机设计师和编译器实现者之间关于计算机硬件最佳表示方式的讨论结果。
  • 为了理解所涉及的问题,请记住一个简单事实:对内存中对象的操作永远不直接处理内存中的对象,而是将对象加载到处理器的寄存器中,在哪里修改,然后再写回内存。更糟糕的是,对象通常首先从主存加载到缓存中,然后再加载到寄存器。

41.2.1 内存位置

  • C++内存模型保证两个更新和访问不同内存位置的线程可以互不影响的执行。这恰是我们的朴素期望。防止我们遇到现代硬件有时很奇怪和微妙的行为是编译器的任务。编译器和硬件如何写作来实现这一目的应该由编译器负责。我们编程所用的机器实际上是由硬件和非常底层的(由编译器生成的)软件组合提供的。

41.2.2 指令重排

  • 为提高性能,编译器,优化器以及硬件都可能重排指令顺序。

41.2.3 内存序

  • 术语内存序(memory ordering)用来描述一个线程从内存访问一个值时会看到什么。最简单的内存序称为顺序一致性(sequentially consistent)。再一个顺序一致性内存模型中,每个线程看到的是相同的操作执行效果,此顺序就像是所有指令都在单一线程中顺序执行一样。
  • 线程仍可重排指令,但对其他线程可以观察变量的每个时间点,时间点前执行的指令集合(因而)和观察家到的内存位置的值必须是明确定义的且对所有线程都一致。
  • 观察值从而强制内存位置的一个一致性视图的操作被称为原子操作(atomic operation)

41.3 原子性

  • 所谓无锁编程,就是一组来编写不显示使用锁的并发程序的技术。程序员转而依靠原语操作(由硬件直接支持)来避免小对象(通常是单一字或双字)的数据竞争。不必忍受数据竞争的原语操作通常被称为原子操作(atomic operation),可用来实现高层并发机制,例如锁,线程和无锁数据结构。
  • 除了简单的原子计数器这一明显例外,无锁编程通常很复杂,最好留给专家使用

41.3.1 atomic类型

  • 原子类型(atomic type)是atomic模板的特例化版本。原子类型的对象上的操作是原子的(atomic)。即,操作由单一线程执行,不会受到其它线程干扰

41.3.2 标志和栅栏

  • 除了支持原子类型之外,标准库还提供了两种更底层的同步特性: 原子标志和栅栏。它们的主要用途是实现最底层的原子特性,例如自旋锁和原子类型。这两个特性是仅有的每个C++实现都保证支持的无锁机制。
  • 基本上没有程序员需要使用标志或栅栏。其使用者通常是和硬件设计师紧密合作的人。

41.4 volatile

  • 说明符volatile用来指出一个对象可被线程控制范围之外的东西修改
  • volatile说明符主要是告知编译器不要优化掉明显冗余的读写操作
  • 除非是直接处理硬件的底层代码中,否则不要使用volatile
  • 不要假定volatile在内存模型中有特殊含义,它确使没有。与某些新语言不同,在C++中volatile并非一种同步机制。为了进行同步,应该使用atomic,mutex或condition_variable

41.5

  • 用并发提高响应能力或吞吐率
  • 只要代价可接受,应在尽可能高的抽象层次上编程
  • 优先选择packaged_task和future,而不是直接使用thread和mutex
  • 除非是实现简单计数器,否则优先选择mutex和condition_variable,而不是直接使用atomic
  • 尽量避免显式共享数据
  • 将进程视为线程的代替
  • 标准库并发特性是类型安全的
  • 内存模型是为了省去程序员从机器体系结构思考计算机的麻烦
  • 内存模型令内存行为大致如我们的朴素预取
  • 不同线程访问一个struct的不同位域可能互相干扰
  • 避免数据竞争
  • 原子类型和操作可实现无锁编程
  • 无锁编程对避免死锁和确保每个线程持续前进是很重要的
  • 将无锁编程留给专家
  • 将放松内存模型留给专家
  • volatile告知编译器一个对象的值可以被程序之外的东西改变
  • C++的volatile不是一种同步机制

第四十二章 线程和任务

42.1 引言

42.2 线程

  • thread是计算的概念在计算机硬件层面的抽象。C++标准库thread的设计目标是与操作系统线程形成一对一映射。

  • 所有thread工作于同一个地址空间中。如果你希望硬件能防止数据竞争,则应该使用进程。thread间不共享栈,因此局部变量不会产生数据竞争问题,除非你不小心将一个局部变量的指针传递给其他thread。

  • 如果一个thread不能继续前进(例如遇到了一个其他thread所拥有的mutex),我们称它处于阻塞(blocked)或睡眠(asleep)状态

  • 一个thread表示一个系统资源,一个系统线程,甚至可能有专用硬件,因此thread可以移动但是不能拷贝。

42.2.1 身份

  • 每个执行线程都有唯一标识符,用thread::id类型的值表示。如果一个thread不表示一个执行线程,则其id为默认的id{}。一个thread的id可以通过调用get_id()获得。

42.2.2 构造

  • thread的构造函数接受一个要执行的任务,以及该任务要求的参数。参数的数量和类型必须与任务所要求的参数列表匹配。

  • thread构造完毕之后,一旦运行时系统能获取它运行所需的资源,它就开始执行任务。你可以认为这个过程是 立即的。并不存在单独的启动thread操作。

  • 如果你希望构建一组任务,将它们链接在一起,你应该将任务构造为函数对象,然后,在他们就绪之后启动thread

  • 将任务从一个thread移动到另一个thread并不影响其执行,thread的移动只是改变thread指向的是什么。

42.2.3 析构

  • 显然,thread的析构函数销毁thread对象。为了防止发生系统线程的生命期长于其thread的意外情况,thread析构函数调用terminate()结束程序(若thread是joinable()的,即get_id() != id())

42.2.4 join()

  • t.join() 告诉当前thread在t结束之前不要继续前进

42.2.5 detach()

  • 注意,thread提供了移动赋值操作和移动构造函数。这令thread可以迁移出它创建时所在的作用域,从而常常可作为detach()的替代方案。我们可以将thread迁移到程序的主模块,通过unique_ptr或shared_ptr访问它们,或者将它们放置于一个容器中(例如vector),免得失去与它们的联系。

  • 如果你必须detach()一个thread,请确保它没有引用其作用域中的变量

  • 我们必须使用detach()才能让一个thread离开其作用域;除非有非常好的理由,否则不要这么做,即使需要使用detach(),也应首先仔细思考thread的任务可能做什么,然后再使用。

42.2.6 名字空间this_thread

  • 对当前thread的操作定义再名字空间this_thread中
    • x = get_id() x为当前thread的id;不抛出异常
    • yield() 给调度器机会运行另一个thread;不抛出异常
    • sleep_until(tp) 令当前thread进行睡眠状态,直到time_point tp
    • sleep_for(d) 令当前thread进行睡眠状态,持续duration d
  • 在所有主要C++实现中thread都是可抢占的;即,C++实现可以从一个任务切换到另一个任务,以确保所有thread都以一个合理的速度前进。

42.2.7 杀死thread

  • thread漏掉了一个重要操作,没有一种简单的标准方法告知一个正在运行的thread对其任务已经失去了兴趣,因此请它停止运行并释放所有资源。此操作(在不同语言和系统中被称为杀死,取消和终止)的缺席有各种历史原因和技术原因。
  • 如需要,应用程序员可以编写自己的杀线程操作。例如,很多任务包含一个请求循环。在此情况下,发送一条请自杀消息给一个thread即可令其释放所有资源并结束。如果没有请求循环,线程可以周期性的检查一个需要变量来判断用户是否还需要本线程的结果。

42.2.8 thread_local 数据

  • 如其名,一个thread_local变量是一个thread专有的对象,其他thread不能访问,除非其拥有者将指向它的指针提供给了其他线程。

  • 我们说一个thread_local具有线程存储存续时间(thread storage duration)。每个thread对thread_local变量都有自己的拷贝。thread_local在首次使用前初始化。如果已构造,会在thread退出时销毁。

  • thread_local存储的一个重要用途是供thread显示缓存互斥访问数据。

  • 一般而言,非局部内存是并发编程的一个难题,因为确定数据是否共享通常不那么简单,因而可能成为数据竞争之源

  • 名字空间变量,局部static和类static成员都可以声明为thread_local。

42.3 避免数据竞争

  • 避免数据竞争的最好方法是不共享数据。将感兴趣的数据保存在局部变量中,保存在不与其他线程共享的自由存储中,或是保持在thread_local内存中。不要将这类数据的指针传递给其他thread。当另一个thread需要处理这类数据时(例如并行排序),传递数据特定片段的指针并确保在任务结束之前不触碰此数据片段。
  • 这些简单规则背后的思想是避免并发数据访问,因此程序不需要锁机制且能达到最高效率。在不能应用这些规则的场合,例如有大量数据需要共享的场合,可使用某种形式的锁机制
    • 互斥量(mutex):互斥量就是一个用来表示某个资源互斥访问权限的对象。为访问资源,先获取互斥量,然后访问数据,最后释放互斥量
    • 条件变量(condition variable): 一个thread用条件变量等待另一个thread或计时器生成的事件。
  • 严格来说,条件变量不能防止数据竞争,而是帮我们避免引入可能引起数据竞争的共享数据。

42.3.1 互斥量

  • mutex对象用来表示资源的互斥访问。因此,它可用来防止数据竞争以及同步多个thread对共享数据的访问。

42.3.2 多重锁

  • 为执行某个任务获取多个资源的需求非常常见。不幸的是,获取两个锁就可能产生死锁。

42.3.3 call_once()

  • 我们通常希望初始化对象时不会产生数据竞争。谓词,类型once_flag和函数call_once()提供了一种高效且简单的底层工具。
  • 可以将call_once()理解为这样一种方法,它简单的修改并发前代码,这些代码依赖于已初始化的static数据。

42.3.4 条件变量

  • 我们用条件变量管理thread间的通信。一个thread可等待(阻塞)在一个condition_variable上,直至发生某个事件,例如到达一个特定时刻或者另一个thread完成。

42.4 基于任务的并发

  • 本节介绍如何指定一种简单的任务:一种根据给定参数完成一项工作,生成一个结果的任务

42.4.1 future和promise

  • 任务间的通信由一对future和promise处理。任务将其结果放入一个promise,需要此结果的任务则从对应的future提取结果。

42.4.2 promise

  • 一个promise就是一个共享状态的句柄。它是一个任务可用来存放其结果的地方,供其他任务通过future提取。

42.4.3 packaged_task

  • packaged_task保存了一个任务和一个future/promise对

42.4.4 future

  • future就是共享状态的句柄,它是任务提取由promise存放的结果的地方

42.4.5 shared_future

  • future的结果值只能被读一次,因为读取时它就被移动了。因此,如果你希望反复读取结果值,或是可能有多个读者读取结果,就必须拷贝它,然后读取副本。这正是shared_future所做的。每个可用的shared_future都是通过直接或间接的从具有相同结果类型的future中移出值来进行初始化的。

42.5 建议

  • thread是系统线程的类型安全的接口
  • 不要销毁正在运行的thread
  • 用join()等待thread结束
  • 除非不得已,否则不要detach()一个thread
  • 用lock_guard或unique_lock管理互斥量
  • 用lock()获取多重锁
  • 用condition_variable管理thread间通信
  • 从并发执行任务的角度思考,而非直接从thread角度思考
  • 重视间接性
  • 用promise返回结果,从future获取结果
  • 不要对一个promise两次执行set_value(), set_exception()
  • 用packaged_task管理任务抛出的异常以及安排返回值
  • 用packaged_task和future表达对外部服务的请求以及等待其应用
  • 不要从一个future两次使用get()
  • 用async()启动简单任务
  • 选择好的并发粒度很困难:依赖实验和测量做出选择
  • 尽量将并发隐藏在并行算法接口之后
  • 并行算法在语义上可能与解决统一问题的穿行解决方案不同
  • 有时,穿行解决方案比并行版本简单且快速。

第四十三章 C标准库

43.1 引言

  • C标准库经过很小改动后已被纳入C++标准库中。

43.2 文件

  • C I/O系统是基于文件的。一个文件(FILE *)可以指向一个外存文件或一个标准输入输出流: stdin, stdout, stderr

43.3 printf()系列函数

  • 最流行的C标准库函数是输出函数。但是,我倾向于使用iostream库,因为它是类型安全且可扩展的。

43.4 C风格字符串

  • 一个C风格字符串就是一个零结尾的char数组。定义于中的一组函数提供了对这种字符串表示方法的支持。

43.5 内存

  • 操纵内存的函数通过void*指针(const void *用于只读内存)对裸内存(类型未知)进行操作。
  • 注意,malloc()等函数并不调用构造函数,free()也不会调用析构函数。不要对具有构造函数和析构函数的类型使用这些函数。而且memset()也不应该用于具有构造函数的任何类型。

43.6 日期和时间

  • 中,可以找到一些日期和时间相关的类型和函数

43.8 建议

  • 如果担心资源泄露,使用fstream而不是fopen()/fclose()
  • 处于类型安全和扩展性的考虑,优先选择而不是
  • 据对不要使用gets()或者scanf(“%s”, s)
  • 处于资源管理易用性和简单性考虑,使用而不是
  • 只对裸内存使用C内存管理例程,如memcpy()
  • 优先选择vector而不是malloc()和realloc()
  • C标准库不了解构造函数和析构函数
  • 优先选择而不是进行计时
  • 考虑到灵活性,易用性和性能,优先选择sort()而不是qsort()
  • 不要使用exit(),应该选择抛出异常
  • 不要使用longjmp(),应该选择抛出异常

第四十四章 兼容性

44.1 引言

  • 本章的目的是
    • 给出C++11新特性的简明列表
    • 介绍会给程序员的带来难题的差异
    • 指出解决问题的方法

44.2 C++11扩展

44.2.1 语言特性

  • 研究语言特性列表着实让人眼花缭乱。但要记住,语言特性并不是孤立使用的。特别是,大多数C++11新特性如果脱离了其他特性构成的框架就毫无意义。
  • 下面特性列表的顺序大致就是在本书中第一次出现的顺序
    • 使用{}列表进行一致且通用的初始化
    • 从初始化器进行类型推断:auto
    • 避免窄化转换
    • 推广的且有保证的常量表达式:constexpr
    • 范围for语句
    • 空指针关键字: nullptr
    • 限域且强类型的枚举:enum class
    • 编译时断言:static_assert
    • {}列表到std::initializer_list的语言映射
    • 右值引用,允许移动语义
    • 以>>结束的嵌套模板参数
    • lambda
    • 可变参数模板
    • 类型和模板别名
    • Unicode字符
    • long long整数类型
    • 对齐控制: alignas, alignof
    • 在声明中将表达式的类型用作类型的能力:decltype
    • 裸字符串字面值常量
    • 推广的POD
    • 推广的union
    • 局部类作为模板实参
    • 尾置语法和两种标准属性: [[carries_dependency]]和[[noreturn]]
    • 阻止异常传播: noexcept说明符
    • 检测表达式抛出异常的可能性:noexcept运算符
    • inline名字空间
    • 委托构造函数
    • 类内成员初始化器
    • 默认控制:defult和delete
    • 显式转换运算符
    • 用户自定义字面值常量
    • template实例化更为显式的控制:extern template
    • 函数模板的默认模板实参
    • 继承构造函数
    • 覆盖控制:override和final
    • 更简单,更通用的SFINAE规则
    • 内存模型
    • 线程局部存储:thread_local

44.2.2 标准库组件

44.4 建议

  • 在使用新特性编写产品级代码前,应先尝试编写小规模程序来测试它是否符合标准以及你所使用的C++实现是否满足性能要求
  • 学习C++时应使用你能获得的最新的,最完整的标准C++实现
  • C和C++的公共子集不是学习C++的最佳起点
  • 优先选择标准特性而不是非标准特性
  • 避免使用throw说明这样的启用特性
  • 避免使用C风格类型转换
  • 隐式int已被弃用,因此应显式说明每个函数,变量,const等的类型
  • 在将C程序转换为C++程序时,首先确保一致使用函数声明(原型)和标准头文件
  • 在将C程序转换为C++程序时,需将与C++关键字同名的变量改名
  • 处于可以执行和类型安全的考虑,如果必须使用C,应该用C和C++的公共子集编写代码
  • 在将C程序转换为C++程序时,应将malloc()的返回结果转换为正确类型或改用new
  • 当从malloc()和free()转换为new 和delete时,考虑使用vector,push_back()和reserve()而不是realloc()
  • 在将C程序转换为C++程序时,记住C++中没有从int到枚举类型的隐式类型转换,如果需要,应该使用显式类型转换
  • 名字空间std中定义的特性都是定义于一个文件名无后缀的头文件中
  • 包含以便使用std::string
  • 每个标准C头文件<X.h>都将名字置于全局名字空间中,对应的C++头文件将名字置于名字空间std中
  • 声明C函数时使用extern “C”

简介

  • Qt 中常见的数据类型及其笔记

Qt QAtomicInt类型 详解

QAtomicInt 是 Qt 中提供的一种用于原子操作的整数类型。原子操作指的是在多线程环境中执行的操作,它们在 CPU 层级上是不可分割的,确保操作在执行时不会被中断,从而避免线程竞争问题。

QAtomicInt 概述

QAtomicInt 是一个提供了基本整型(int)的原子操作封装的类。它通常用于实现线程安全的计数器或其他需要原子性递增、递减操作的场景。

特性

  • 原子性:所有的操作都在底层以原子方式执行,避免了线程安全问题。
  • 跨平台支持:Qt 提供的原子操作在不同的平台上具有一致性,可以在 Windows、Linux、macOS 等平台上使用。

主要方法

以下是 QAtomicInt 常用的方法:

  1. 构造函数

    • QAtomicInt():默认构造一个原子整数,并将其初始化为 0。
    • QAtomicInt(int value):使用指定的值初始化原子整数。
  2. 读操作

    • int loadAcquire() const:获取当前值,确保所有读操作在此操作之前完成。
    • int loadRelaxed() const:获取当前值,但不强制同步内存。
  3. 写操作

    • void storeRelease(int newValue):设置新值,确保所有写操作在此操作之后完成。
    • void storeRelaxed(int newValue):设置新值,但不强制同步内存。
  4. 增减操作

    • bool ref():将值递增 1。如果结果是非零值,返回 true,否则返回 false
    • bool deref():将值递减 1。如果结果是非零值,返回 true,否则返回 false
    • int fetchAndAddAcquire(int value):在获取值后再增加指定的值。
    • int fetchAndAddRelaxed(int value):在不强制同步内存的情况下增加指定的值。
    • int fetchAndStoreAcquire(int newValue):获取当前值并将其设置为 newValue
    • int fetchAndStoreRelaxed(int newValue):在不强制同步内存的情况下设置新值。

使用场景

  • 线程计数器:在多线程应用中,可以使用 QAtomicInt 来实现一个线程安全的计数器。
  • 资源管理:可以用于引用计数的实现,确保在多线程环境下资源能够正确地分配和释放。

示例代码

以下是一个使用 QAtomicInt 的简单例子:

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
#include <QAtomicInt>
#include <QDebug>
#include <QThread>

QAtomicInt counter(0);

void workerFunction() {
for (int i = 0; i < 1000; ++i) {
counter.ref(); // 线程安全地增加计数器
}
}

int main() {
QThread t1(workerFunction);
QThread t2(workerFunction);

t1.start();
t2.start();

t1.wait();
t2.wait();

qDebug() << "Final counter value:" << counter.loadAcquire(); // 输出最终计数值
return 0;
}

在这个例子中,我们创建了两个线程,每个线程都递增 counter 1000 次。由于使用了 QAtomicInt,计数操作是线程安全的,因此最终的计数值是 2000。

注意事项

  • QAtomicInt 适合用于简单的整数原子操作。如果需要更复杂的原子操作,可以考虑使用 QAtomicInteger<T>QMutexQSemaphore 等同步原语。

QAtomicInt 提供了一种简单而高效的方式来处理多线程环境中的整数操作,确保数据的一致性和线程安全性。

Qt QString 详解

QString 是 Qt 框架中用于表示和操作文本字符串的类。它是一个 Unicode 字符串类,能够很好地处理多语言字符集和复杂文本操作。QString 提供了丰富的功能用于创建、修改、查询和转换字符串。以下是 QString 的详细介绍,包括常用功能和示例代码。

1. QString 基本概念

QString 是一个 Unicode 字符串类,支持宽字符集。Qt 使用 UTF-16 编码存储字符串。相比 C++ 的 std::stringQString 更加适合处理包含多语言和特殊字符的文本。

2. QString 的构造方法

QString 提供了多种构造函数,可以从各种数据类型构建字符串。

1
2
3
4
5
QString str1;                        // 空字符串
QString str2("Hello, Qt!"); // 从字符串字面值创建
QString str3(QLatin1String("Hello")); // 从 QLatin1String 创建
QString str4(str2); // 拷贝构造函数
QString str5 = QString::number(42); // 从数字创建

3. 常用方法

3.1. 字符串拼接

QString 提供了多种方式来拼接字符串。

  • 使用 + 运算符拼接:

    1
    2
    3
    QString str1 = "Hello";
    QString str2 = "World";
    QString result = str1 + " " + str2; // "Hello World"
  • 使用 append() 方法拼接:

    1
    2
    QString str = "Hello";
    str.append(" World"); // "Hello World"

3.2. 字符串长度和访问

  • 获取字符串长度:

    1
    2
    QString str = "Hello";
    int length = str.length(); // 5
  • 访问单个字符:

    1
    QChar ch = str.at(1);  // 'e'

3.3. 查找和替换

QString 提供了多种查找和替换子字符串的函数:

  • 查找子字符串:

    1
    2
    QString str = "Hello, Qt!";
    int index = str.indexOf("Qt"); // 返回子字符串首次出现的索引,结果是 7
  • 替换子字符串:

    1
    2
    QString str = "Hello, World!";
    str.replace("World", "Qt"); // 结果是 "Hello, Qt!"

3.4. 大小写转换

  • 转换为大写或小写:
    1
    2
    3
    QString str = "Hello";
    QString upperStr = str.toUpper(); // "HELLO"
    QString lowerStr = str.toLower(); // "hello"

3.5. 截取字符串

  • 使用 mid() 截取部分字符串:

    1
    2
    QString str = "Hello, Qt!";
    QString substr = str.mid(7, 2); // 结果是 "Qt"
  • 使用 left()right()

    1
    2
    QString leftStr = str.left(5);    // "Hello"
    QString rightStr = str.right(3); // "Qt!"

3.6. 字符串分割

  • 使用 split() 按照分隔符拆分字符串:
    1
    2
    QString str = "apple,banana,grape";
    QStringList list = str.split(","); // 拆分为 ["apple", "banana", "grape"]

3.7. 转换为其他数据类型

  • 转换为整数、浮点数等:

    1
    2
    3
    4
    5
    QString str = "123";
    int num = str.toInt(); // 123

    QString floatStr = "123.45";
    double num2 = floatStr.toDouble(); // 123.45
  • 转换为字节数组:

    1
    2
    QString str = "Hello";
    QByteArray byteArray = str.toUtf8(); // UTF-8 编码的字节数组

4. 静态方法

4.1. 创建字符串

  • 使用 QString::number() 方法从数值创建字符串:

    1
    2
    QString str = QString::number(42);      // "42"
    QString str2 = QString::number(3.1415); // "3.1415"
  • 使用 arg() 格式化字符串:

    1
    QString str = QString("The value is %1").arg(42);  // "The value is 42"

5. 比较字符串

  • 使用 compare() 方法比较两个字符串:

    1
    2
    3
    QString str1 = "apple";
    QString str2 = "orange";
    int result = QString::compare(str1, str2); // < 0,因为 "apple" 在字典序中小于 "orange"
  • 也可以使用 ==!= 比较字符串:

    1
    2
    3
    if (str1 == str2) {
    // 字符串相等
    }

6. 字符串的编码处理

QString 内部使用 UTF-16 存储字符,但可以轻松转换为其他编码格式,例如 UTF-8、Latin1 等。

1
2
3
QString str = "你好,世界";
QByteArray utf8String = str.toUtf8(); // 转换为 UTF-8 字节数组
QByteArray latin1String = str.toLatin1(); // 转换为 Latin1 编码字节数组

7. 性能优化

QString 使用了引用计数(copy-on-write)的机制来优化性能,避免不必要的拷贝操作。当多个 QString 对象引用同一数据时,它们共享数据;只有当其中一个发生修改时,才会执行深拷贝。

8. 使用 QStringQByteArray 的转换

QByteArray 是字节数组类,而 QString 是 Unicode 字符串类,它们可以互相转换。

1
2
3
4
QString str = "Hello";
QByteArray byteArray = str.toUtf8(); // 将 QString 转换为 QByteArray

QString str2 = QString::fromUtf8(byteArray); // 将 QByteArray 转换回 QString

9. 处理空字符串

  • isEmpty()isNull() 用于判断字符串是否为空或为 null
    1
    2
    3
    QString str;
    bool empty = str.isEmpty(); // true,因为是空字符串
    bool null = str.isNull(); // true,因为没有分配任何值

10. 总结

QString 是一个功能强大的类,能够处理各种复杂的字符串操作。它的 Unicode 支持使其在处理多语言字符时特别有效,且提供了高效的性能优化措施。

Qt QVector 详解

QVector 是 Qt 框架中用于存储相同类型元素的动态数组类,类似于 C++ 标准库的 std::vector。它提供了许多方便的接口来进行动态分配、访问和操作数据。下面是对 QVector 的详解。

1. 基本概念

QVector<T> 是一个模板类,T 是其中元素的类型。QVector 可以根据需要动态调整数组的大小,支持类似数组的下标访问方式,并且性能表现与 std::vector 类似。

1
QVector<int> intVector;  // 创建一个存储整数的 QVector

2. 常用操作

2.1 初始化

QVector 支持多种初始化方式,包括默认构造、指定大小的构造、和使用初始值的构造等。

1
2
3
QVector<int> vec1;  // 默认构造,空向量
QVector<int> vec2(5); // 构造一个包含5个默认初始化元素的向量
QVector<int> vec3(5, 10); // 构造一个包含5个元素,每个元素初始化为10

2.2 访问元素

可以通过下标访问或者使用 at() 函数。

1
2
int first = vec3[0];  // 使用下标访问
int second = vec3.at(1); // 使用 at() 方法访问

2.3 添加元素

可以通过 append()push_back() 方法向向量末尾添加元素。insert() 方法可以在特定位置插入元素。

1
2
3
vec1.append(3);      // 向尾部添加元素
vec1.push_back(5); // 与 append 类似,添加到末尾
vec1.insert(1, 4); // 在索引 1 处插入元素

2.4 删除元素

可以使用 remove(), removeAt(), removeFirst(), removeLast() 等函数来删除元素。

1
2
3
vec1.removeAt(0);    // 删除索引为 0 的元素
vec1.removeFirst(); // 删除第一个元素
vec1.removeLast(); // 删除最后一个元素

2.5 查找和判断

QVector 提供 contains() 来判断是否包含某元素,indexOf()lastIndexOf() 用于查找某个元素的索引。

1
2
bool hasValue = vec1.contains(5);  // 判断是否包含 5
int index = vec1.indexOf(5); // 查找 5 的位置

2.6 大小和容量

QVector 动态调整大小,可以通过 size() 获取当前元素的数量,通过 capacity() 获取预分配的内存容量。可以通过 resize() 改变 QVector 的大小。

1
2
3
int size = vec1.size();       // 获取当前元素数量
int capacity = vec1.capacity(); // 获取当前容量
vec1.resize(10); // 改变大小为10,可能会填充新元素

3. 迭代

支持 C++ 范围循环(range-based for loop)和迭代器。

1
2
3
4
5
6
7
8
for (int value : vec1) {
qDebug() << value;
}

QVector<int>::iterator it;
for (it = vec1.begin(); it != vec1.end(); ++it) {
qDebug() << *it;
}

4. 性能

  • 内存管理QVector 会动态调整内存,避免频繁的内存分配。通常,capacity()size() 大,以减少内存重新分配的次数。
  • 浅拷贝优化(Copy-on-Write)QVector 使用 Qt 的隐式共享机制,也称为浅拷贝。当一个 QVector 被复制时,实际上并不会立即进行数据复制,直到修改操作发生,这提高了性能。

5. 与其他容器的互操作

QVector 提供了与其他容器(如 QList, QSet)的互操作能力,可以方便地将 QVector 转换为其他容器类型。

1
QList<int> list = vec1.toList();  // 将 QVector 转为 QList

6. 常用函数总结

  • append(T value) / push_back(T value):在末尾添加元素。
  • at(int i):返回指定索引处的元素。
  • size():返回当前元素数量。
  • capacity():返回当前预分配的内存大小。
  • clear():清空所有元素。
  • contains(T value):判断是否包含某个元素。
  • indexOf(T value):返回某个元素的索引。
  • removeAt(int i):删除指定位置的元素。
  • resize(int size):调整大小。

7. QVector 使用示例

下面是一个简单的示例,演示了 QVector 的基本用法。

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
30
#include <QVector>
#include <QDebug>

int main() {
QVector<int> vec;

// 添加元素
vec.append(1);
vec.append(2);
vec.append(3);

// 访问元素
qDebug() << "First element:" << vec.at(0);

// 遍历元素
for (int i = 0; i < vec.size(); ++i) {
qDebug() << "Element at index" << i << ":" << vec[i];
}

// 查找和判断
if (vec.contains(2)) {
qDebug() << "Vector contains 2";
}

// 删除元素
vec.removeAt(1);
qDebug() << "After removing index 1:" << vec;

return 0;
}

总结

QVector 是 Qt 提供的强大而灵活的动态数组容器,适用于需要高效存储和访问大量相同类型数据的场景。在日常使用中,可以充分利用其丰富的 API 和高性能特性来简化开发流程。

简介

  • Qt常见技巧

C++ std::vector与QVector互相转换

在开发过程中,C++ 标准库的 std::vector 和 Qt 的 QVector 是两种常用的动态数组容器。在某些情况下,需要将这两种容器互相转换。下面是具体的转换方法:

1. 从 std::vector 转换为 QVector

可以使用 QVector 的构造函数或 QVectoroperator= 重载来完成。

使用构造函数

1
2
3
4
5
#include <vector>
#include <QVector>

std::vector<int> stdVector = {1, 2, 3, 4, 5};
QVector<int> qVector(stdVector.begin(), stdVector.end());

使用赋值运算符

1
2
3
4
5
6
#include <vector>
#include <QVector>

std::vector<int> stdVector = {1, 2, 3, 4, 5};
QVector<int> qVector;
qVector = QVector<int>::fromStdVector(stdVector);

2. 从 QVector 转换为 std::vector

可以使用 QVectortoStdVector() 方法。

1
2
3
4
5
#include <vector>
#include <QVector>

QVector<int> qVector = {1, 2, 3, 4, 5};
std::vector<int> stdVector = qVector.toStdVector();

注意事项

  1. 类型兼容性
    确保 std::vectorQVector 中存储的类型一致,例如两者都是 intdouble 等。

  2. 效率问题
    转换的效率取决于容器的大小。两种容器在底层内存布局上可能存在差异,因此需要完整的元素复制。

  3. 扩展用法
    如果存储的元素是复杂的类对象,确保拷贝构造函数或赋值运算符的正确性。

通过上述方法,可以轻松实现 std::vectorQVector 之间的互相转换,满足 Qt 与标准库协同工作的需求。

C++ Qt std::string传给QString中文乱码

好的!如果你的 std::string 使用的是其他编码(比如 GBK),你需要先将它转换为 UTF-8。可以使用 QTextCodec 来完成这个转换。以下是一个例子:

1
2
3
4
5
6
7
8
9
10
#include <QString>
#include <QTextCodec>
#include <string>

std::string str = "你的字符串"; // 假设这里是 GBK 编码的字符串

// 将 std::string 转换为 QString
QTextCodec *codec = QTextCodec::codecForName("GBK"); // 使用 GBK 编码
QString qstr = codec->toUnicode(str.c_str());

这样就可以将 std::string 中的 GBK 编码字符串正确转换为 QString 了。如果你知道你的字符串使用的是哪种编码,只需替换 "GBK" 为相应的编码名称。还有什么其他问题吗?

Qt5 QWidget类对象主动关闭窗口

在 Qt5 中,可以让一个 QWidget 类对象主动关闭窗口。一般来说,可以调用 close() 方法来关闭窗口。close()QWidget 类的一个公共成员函数,用于请求关闭窗口。这个方法会发送一个 closeEvent 事件,从而触发窗口的关闭。

如果想要让窗口在特定条件下自动关闭,可以在窗口的逻辑中调用 close() 方法。例如,我们可以在按钮点击事件中或者条件满足时让窗口关闭。

示例代码

以下代码演示了一个自定义的 MyWidget 类,包含一个按钮,点击该按钮后窗口会自动关闭。

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
30
31
32
33
34
35
36
37
38
39
40
#include <QApplication>
#include <QWidget>
#include <QPushButton>
#include <QVBoxLayout>

class MyWidget : public QWidget {
Q_OBJECT

public:
MyWidget(QWidget *parent = nullptr) : QWidget(parent) {
// 设置窗口标题
setWindowTitle("Qt5 主动关闭窗口示例");

// 创建按钮
QPushButton *closeButton = new QPushButton("关闭窗口");

// 布局
QVBoxLayout *layout = new QVBoxLayout(this);
layout->addWidget(closeButton);
setLayout(layout);

// 连接按钮点击信号到关闭窗口的槽
connect(closeButton, &QPushButton::clicked, this, &MyWidget::closeWindow);
}

private slots:
void closeWindow() {
// 主动关闭窗口
this->close();
}
};

int main(int argc, char *argv[]) {
QApplication app(argc, argv);

MyWidget window;
window.show();

return app.exec();
}

代码说明

  1. MyWidget:继承自 QWidget,并创建了一个按钮用于关闭窗口。

    • closeButton:点击按钮后会触发 closeWindow() 槽函数。
  2. closeWindow() 槽函数:调用 this->close() 来请求关闭当前窗口。

  3. close() 函数QWidget 类的内置函数,调用后会关闭窗口并触发 closeEvent

Qt5 保存数据到excel表格文件

在Qt5中将数据保存为Excel表格文件,可以使用以下几种方法:

1. 使用 QtXlsxWriter 库

QtXlsxWriter 是一个专为 Qt 设计的库,能够直接处理 .xlsx 文件格式。它可以帮助您轻松地创建、编辑、保存 Excel 表格。

安装 QtXlsxWriter

  1. 克隆库:
    1
    git clone https://github.com/dbzhang800/QtXlsxWriter.git
  2. 在项目中添加库:
    将库文件夹中的 src 目录直接添加到您的项目中,并在 .pro 文件中包含:
    1
    include(./QtXlsxWriter/src/xlsx/qtxlsx.pri)

使用示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
#include <QCoreApplication>
#include "xlsxdocument.h" // QtXlsxWriter 头文件

int main(int argc, char *argv[]) {
QCoreApplication a(argc, argv);

QXlsx::Document xlsx;
xlsx.write("A1", "姓名"); // 写入表头
xlsx.write("B1", "年龄");

xlsx.write("A2", "张三");
xlsx.write("B2", 25);

xlsx.write("A3", "李四");
xlsx.write("B3", 30);

xlsx.saveAs("output.xlsx"); // 保存文件

return a.exec();
}

2. 使用 QAxObject(仅适用于 Windows)

如果在 Windows 系统中使用 Qt,可以通过 QAxObject 调用 COM 接口直接控制 Microsoft Excel 应用程序。这种方法适合于有 Excel 安装的 Windows 环境。

使用示例

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
30
31
32
33
#include <QCoreApplication>
#include <QAxObject>

int main(int argc, char *argv[]) {
QCoreApplication a(argc, argv);

QAxObject *excel = new QAxObject("Excel.Application"); // 打开Excel应用
excel->setProperty("Visible", false); // 设置为不可见
QAxObject *workbooks = excel->querySubObject("Workbooks");
QAxObject *workbook = workbooks->querySubObject("Add"); // 新建工作簿

QAxObject *sheet = workbook->querySubObject("Sheets(int)", 1); // 获取第一个工作表
sheet->setProperty("Name", "Sheet1");

// 填写数据
sheet->querySubObject("Cells(int,int)", 1, 1)->setProperty("Value", "姓名");
sheet->querySubObject("Cells(int,int)", 1, 2)->setProperty("Value", "年龄");

sheet->querySubObject("Cells(int,int)", 2, 1)->setProperty("Value", "张三");
sheet->querySubObject("Cells(int,int)", 2, 2)->setProperty("Value", 25);

sheet->querySubObject("Cells(int,int)", 3, 1)->setProperty("Value", "李四");
sheet->querySubObject("Cells(int,int)", 3, 2)->setProperty("Value", 30);

// 保存文件并关闭
workbook->dynamicCall("SaveAs(const QString&)", QDir::currentPath() + "/output.xlsx");
workbook->dynamicCall("Close()");
excel->dynamicCall("Quit()");

delete excel;

return a.exec();
}

3. 使用 CSV 文件格式

如果您不要求严格的 Excel 格式(.xlsx/.xls),可以将数据保存为 CSV 文件。这种方式简单并且跨平台支持。

使用示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
#include <QCoreApplication>
#include <QFile>
#include <QTextStream>

int main(int argc, char *argv[]) {
QCoreApplication a(argc, argv);

QFile file("output.csv");
if (file.open(QIODevice::WriteOnly | QIODevice::Text)) {
QTextStream out(&file);

out << "姓名,年龄\n";
out << "张三,25\n";
out << "李四,30\n";

file.close();
}

return a.exec();
}

方法总结

  • QtXlsxWriter:适合跨平台、直接生成 .xlsx 文件。
  • QAxObject:适合 Windows 系统,适用于复杂的 Excel 操作。
  • CSV 文件:适用于简单的数据保存,跨平台,但不支持 Excel 的高级功能(如格式化)。

Qt QVector和C风格数组互相转换

在 Qt 中,QVector 和 C 风格数组(如 int[])之间的互相转换是常见的需求。下面介绍如何在这两者之间进行转换。

1. C 风格数组转 QVector

要将一个 C 风格数组转换为 QVector,可以使用 QVector 的构造函数或者通过 append() 方法将数组中的元素逐个添加到 QVector 中。

方法 1:使用 QVector 构造函数

如果你有一个 C 风格数组,并且知道它的大小,可以直接通过 QVector 的构造函数将其转换为 QVector

1
2
3
4
int arr[] = {1, 2, 3, 4, 5};
int size = sizeof(arr) / sizeof(arr[0]);

QVector<int> vec(arr, arr + size);
  • arr + size 表示数组的结束位置。
  • QVector 构造函数接受两个迭代器或指针,用于定义要添加到向量的元素范围。

方法 2:使用 QVector::append()QVector::push_back()

你可以逐个将数组元素添加到 QVector 中:

1
2
3
4
5
6
7
int arr[] = {1, 2, 3, 4, 5};
int size = sizeof(arr) / sizeof(arr[0]);

QVector<int> vec;
for (int i = 0; i < size; ++i) {
vec.append(arr[i]);
}

2. QVector 转 C 风格数组

要将 QVector 转换为 C 风格数组,可以通过使用 QVector 提供的 data() 函数。data() 函数返回一个指向 QVector 内部存储区的指针,这个指针就是一个普通的 C 风格数组。

方法 1:通过 QVector::data()

1
2
3
4
5
6
7
QVector<int> vec = {1, 2, 3, 4, 5};
int* arr = vec.data(); // 获取 C 风格数组的指针

// 可以像操作普通数组一样使用 arr
for (int i = 0; i < vec.size(); ++i) {
qDebug() << arr[i];
}

注意QVector::data() 返回的指针指向 QVector 内部的数据区域,因此不能对该指针进行 delete[] 操作。它仅仅是一个数据访问的快捷方式,内存管理仍然由 QVector 负责。

方法 2:手动复制数据到一个新的 C 风格数组

如果你希望独立于 QVector 的生命周期使用 C 风格数组,可以手动分配内存并复制数据。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
QVector<int> vec = {1, 2, 3, 4, 5};
int size = vec.size();
int* arr = new int[size];

// 将 QVector 的数据复制到 C 风格数组
for (int i = 0; i < size; ++i) {
arr[i] = vec[i];
}

// 使用 arr...
for (int i = 0; i < size; ++i) {
qDebug() << arr[i];
}

// 记得释放手动分配的内存
delete[] arr;

3. 注意事项

  • QVector::data() 返回的指针与 QVector 共享内存,因此在 QVector 被修改或销毁后,这个指针可能会失效。
  • 当使用 new 分配的 C 风格数组时,必须手动释放内存,避免内存泄漏。

总结

  • **C 风格数组转 QVector**:可以使用构造函数或逐个添加元素。
  • QVector 转 C 风格数组:可以使用 data() 获得指向内部数组的指针,或者手动分配新数组并复制数据。

这两个容器之间的转换非常灵活,可以根据具体需求选择合适的转换方式。

QVector和std::vector的区别和联系

QVectorstd::vector 是 Qt 和 C++ 标准库中常用的动态数组容器,它们在功能上有许多相似之处,但在设计和使用上也有一些区别。下面是它们的区别和联系的详细说明。

1. 共同点

  • 动态数组:两者都是动态数组容器,支持按需自动扩展容量,并且提供随机访问功能(通过下标访问元素)。
  • 元素连续存储:无论是 QVector 还是 std::vector,它们都保证其元素在内存中是连续存储的。这使得它们能够很好地支持基于指针的算法,并与低级 C 风格数组兼容。
  • 模板类:两者都是模板类,可以存储任何类型的对象,只要该类型满足拷贝或移动要求。
  • 时间复杂度:两者在大多数操作上的时间复杂度是相同的,诸如:
    • 访问元素:O(1)
    • 添加/删除元素(尾部):均摊 O(1)
    • 插入/删除元素(中间):O(n)

2. 主要区别

2.1 库和平台依赖

  • **QVector**:是 Qt 框架的一部分,必须依赖 Qt 环境才能使用。QVector 提供了一些与 Qt 生态系统紧密集成的特性,适合在 Qt 应用中使用。
  • **std::vector**:属于 C++ 标准库的一部分,不依赖任何第三方库,适用于标准 C++ 开发。std::vector 通常更适合在不依赖 Qt 的纯 C++ 项目中使用。

2.2 浅拷贝(隐式共享)机制

  • **QVector**:支持隐式共享(copy-on-write,COW)。这意味着当你复制一个 QVector 时,Qt 实际上并不会立即复制其内部数据,而是与源对象共享数据,直到有一方修改容器中的内容时才会真正复制数据。这种设计在处理大量数据时可以节省内存和提高性能。

    1
    2
    3
    QVector<int> vec1 = {1, 2, 3};
    QVector<int> vec2 = vec1; // 浅拷贝,vec1 和 vec2 共享内存
    vec2[0] = 10; // 修改时才会进行深拷贝
  • **std::vector**:不支持隐式共享。当你复制一个 std::vector 时,会立即进行深拷贝,将所有元素复制到新的容器中。每次复制都需要消耗额外的内存和时间。

    1
    2
    std::vector<int> vec1 = {1, 2, 3};
    std::vector<int> vec2 = vec1; // 深拷贝,vec1 和 vec2 拥有独立的数据

2.3 性能和线程安全

  • **QVector**:由于 Qt 使用了隐式共享机制,某些操作(如拷贝和赋值)在表面上比 std::vector 更高效。但这也带来了一些额外的复杂性,尤其是在多线程环境下。如果多个线程共享一个 QVector,并且有写操作,必须确保数据的一致性,否则会引发线程安全问题。

  • **std::vector**:没有隐式共享,因此不需要在多线程环境下担心这种问题。只要保证不同线程不同时修改同一个 std::vector 实例,线程安全性可以更容易控制。

2.4 API 和接口差异

  • **QVector**:作为 Qt 的一部分,提供了一些与 Qt 生态系统紧密集成的接口和功能。例如,QVectortoList() 方法可以方便地将它转换为 QList。此外,QVector 支持 qDebug() 输出、QDataStream 序列化等 Qt 特有的特性。

    1
    2
    QVector<int> vec = {1, 2, 3};
    QList<int> list = vec.toList(); // 转换为 QList
  • **std::vector**:遵循标准 C++ API,没有与 Qt 特定的接口。例如,std::vector 没有类似的转换功能,但是它与标准库容器之间可以通过迭代器进行高效互操作。std::vector 还支持标准算法库 (<algorithm>) 中的许多算法,如 std::sort, std::find 等。

2.5 内存分配与管理

  • **QVector**:Qt 框架对内存分配做了许多优化,尤其是在管理动态对象的生命周期和复制时。QVector 的隐式共享机制减少了不必要的深拷贝,同时内存管理方式与 Qt 的其他容器(如 QList)保持一致。

  • **std::vector**:标准库的 std::vector 使用的是 C++ 标准库的内存分配器,通常为 std::allocator。开发者可以通过自定义内存分配器来精细控制内存分配行为。

2.6 反射和元对象系统

  • **QVector**:由于 Qt 支持元对象系统(Meta-Object System),QVector 可以与 Qt 的信号和槽、反射等机制配合使用,支持特定类型的序列化、GUI 绑定等。

  • **std::vector**:不支持 Qt 的元对象系统或信号槽机制。因此,如果你在使用 Qt 的信号槽机制或需要动态类型检查、反射等特性时,QVector 是更合适的选择。

3. 适用场景

  • **使用 QVector**:

    • 项目中广泛使用 Qt 框架。
    • 需要与 Qt 容器(如 QList, QSet)互操作,或依赖于 Qt 的元对象系统和信号槽机制。
    • 需要隐式共享以优化内存使用和提高性能(尤其是数据量较大时)。
  • **使用 std::vector**:

    • 项目不依赖 Qt,纯 C++ 开发。
    • 需要兼容 C++ 标准库的其他容器和算法。
    • 希望避免隐式共享带来的复杂性,尤其是在多线程环境中。

总结

特性 QVector std::vector
库依赖 Qt 框架 C++ 标准库
隐式共享 支持(浅拷贝,写时复制) 不支持(深拷贝)
线程安全 需要注意隐式共享导致的安全问题 非共享,线程安全性更容易控制
内存分配 使用 Qt 的内存管理 使用标准库分配器
与 Qt 集成 强(支持信号槽、Qt 容器转换等)
性能优化 提供浅拷贝优化 无隐式共享,复制开销较大
适用场景 Qt 项目,数据量大 纯 C++ 项目,多线程安全

两者的选择通常取决于项目的上下文以及你是否需要 Qt 提供的隐式共享和其他特性。如果你正在开发 Qt 应用程序,QVector 通常是更好的选择;如果是标准 C++ 项目,std::vector 更为适合。

简介

  • windows环境下C++编程遇到的函数

windows C++ WSAStartup()函数 详解

WSAStartup() 是 Windows Sockets API(也称为 Winsock)中用于初始化 Windows Sockets 库的函数。该函数在使用任何其他 Windows Sockets 函数之前必须调用,用于设置程序对网络通信的支持。

函数原型

1
2
3
4
int WSAStartup(
WORD wVersionRequested, // 请求的 Winsock 版本
LPWSADATA lpWSAData // 指向 WSADATA 结构的指针,用于接收系统信息
);
  • 参数

    • wVersionRequested:指定应用程序请求的 Winsock 版本。该参数由高字节和低字节组成,例如,MAKEWORD(2, 2) 表示请求 Winsock 2.2 版本。
    • lpWSAData:指向一个 WSADATA 结构的指针,用于接收有关 Windows Sockets 实现的详细信息。
  • 返回值

    • 如果函数调用成功,返回值为零 (0)。
    • 如果函数调用失败,返回一个非零的错误代码。常见的错误代码包括 WSASYSNOTREADY(底层网络子系统不可用)和 WSAVERNOTSUPPORTED(请求的 Winsock 版本不受支持)。

使用说明

  1. 版本管理

    • 在调用 WSAStartup() 时,必须指定应用程序希望使用的 Winsock 版本。最常用的是 2.2 版本 (MAKEWORD(2, 2)),因为它支持大多数现代网络应用程序的需求。
    • 如果系统支持请求的版本,WSAStartup() 会返回该版本的详细信息。如果系统不支持请求的版本,则返回较低的版本信息,或者函数调用失败。
  2. WSADATA 结构

    • lpWSAData 参数指向的 WSADATA 结构用于接收 Winsock 的相关信息。该结构包含了 Winsock 版本、最大套接字数等重要信息。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    typedef struct WSAData {
    WORD wVersion; // Winsock 版本
    WORD wHighVersion; // 最高支持的 Winsock 版本
    char szDescription[WSADESCRIPTION_LEN + 1]; // 实现描述
    char szSystemStatus[WSASYSSTATUS_LEN + 1]; // 系统状态
    unsigned short iMaxSockets; // 支持的最大套接字数
    unsigned short iMaxUdpDg; // 最大 UDP 数据报长度
    char FAR* lpVendorInfo; // 供应商特定信息
    } WSADATA, *LPWSADATA;
  3. 清理

    • 当应用程序不再需要使用 Windows Sockets API 时,应该调用 WSACleanup() 函数来卸载 Winsock 库并释放相关资源。
  4. 错误处理

    • 如果 WSAStartup() 返回非零值,表明初始化失败,应用程序应检查返回值并通过 WSAGetLastError() 函数获取详细的错误信息。

使用示例

以下是一个简单的示例,展示如何正确使用 WSAStartup() 函数来初始化 Winsock 库:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
#include <winsock2.h>
#include <iostream>

int main() {
WSADATA wsaData;

// 初始化 Winsock 2.2 版本
int result = WSAStartup(MAKEWORD(2, 2), &wsaData);
if (result != 0) {
std::cerr << "WSAStartup failed with error: " << result << std::endl;
return 1;
}

std::cout << "Winsock initialized successfully." << std::endl;
std::cout << "Winsock version: " << LOBYTE(wsaData.wVersion) << "." << HIBYTE(wsaData.wVersion) << std::endl;
std::cout << "Description: " << wsaData.szDescription << std::endl;

// 在此处可以编写网络通信相关代码...

// 清理 Winsock
WSACleanup();
return 0;
}

在这个示例中:

  • WSAStartup() 函数用于初始化 Winsock 库,请求使用 Winsock 2.2 版本。
  • 如果初始化成功,程序会输出 Winsock 的版本信息和描述。
  • 当程序不再需要使用网络功能时,调用 WSACleanup() 函数来清理 Winsock 资源。

WSAStartup() 是任何使用 Windows Sockets API 进行网络编程的应用程序中的必备步骤。初始化成功后,您就可以使用其他 Winsock 函数来执行各种网络操作,例如创建套接字、连接到服务器、发送和接收数据等。

windows C++ GetCurrentThreadId()函数 详解

GetCurrentThreadId() 是 Windows API 中的一个函数,用于获取当前线程的唯一标识符(线程 ID)。在多线程编程中,每个线程都有一个唯一的 ID,可以通过该函数获取,便于在线程间进行标识和管理。

函数原型

1
DWORD GetCurrentThreadId(void);
  • 参数

    • void:该函数不接受任何参数。
  • 返回值

    • 返回当前调用线程的 DWORD 类型的线程 ID。线程 ID 是一个系统分配的数字,用于唯一标识当前线程。

使用说明

  1. 线程 ID 的唯一性

    • 每个线程在其生命周期内都有一个唯一的线程 ID。当线程终止时,该 ID 可能会被系统回收并分配给新的线程。
  2. 典型用法

    • GetCurrentThreadId() 常用于调试、日志记录、线程间通信或同步等场景中。例如,可以使用线程 ID 来标记日志消息,便于区分不同线程的输出。
    • 线程 ID 也可以用于将线程与某些特定的资源(如窗口、数据结构)关联起来。
  3. 与其他 API 的关系

    • 线程 ID 与线程句柄不同,线程句柄通过 CreateThreadOpenThread 等函数获取,而线程 ID 则是一个直接标识线程的数字。
    • 通过 OpenThread() 函数可以将线程 ID 转换为线程句柄,便于进行更复杂的线程操作。
  4. 注意事项

    • 线程 ID 是系统分配的,不应直接作为关键数据或资源的唯一标识,因为它们在特定条件下可能被重复使用。

使用示例

以下是一个简单的示例,演示如何使用 GetCurrentThreadId() 获取并打印当前线程的 ID。

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
#include <windows.h>
#include <iostream>

// 线程函数
DWORD WINAPI ThreadProc(LPVOID lpParam) {
DWORD threadId = GetCurrentThreadId();
std::cout << "Thread ID: " << threadId << " is running." << std::endl;
Sleep(2000); // 模拟工作
std::cout << "Thread ID: " << threadId << " is exiting." << std::endl;
return 0;
}

int main() {
// 创建两个线程
HANDLE hThread1 = CreateThread(NULL, 0, ThreadProc, NULL, 0, NULL);
HANDLE hThread2 = CreateThread(NULL, 0, ThreadProc, NULL, 0, NULL);

// 等待线程完成
WaitForSingleObject(hThread1, INFINITE);
WaitForSingleObject(hThread2, INFINITE);

// 关闭线程句柄
CloseHandle(hThread1);
CloseHandle(hThread2);

return 0;
}

在这个示例中:

  • ThreadProc 是线程的执行函数,每个线程在启动后都会执行该函数。
  • 每个线程都会调用 GetCurrentThreadId() 来获取并打印其自身的线程 ID。
  • 主线程创建了两个子线程,并等待它们完成执行。
  • 通过线程 ID,您可以在日志或调试信息中区分不同线程的行为。

GetCurrentThreadId() 是多线程编程中的一个基本工具,能够帮助开发者识别和管理不同的线程。

windows C++ WaitForSingleObject()函数 详解

WaitForSingleObject() 是 Windows API 中用于同步操作的函数。它用于使调用线程等待一个内核对象(如线程、进程、信号量、事件等)变为有信号状态,或者等待超时。该函数经常用于多线程编程中,确保线程之间的协调与同步。

函数原型

1
2
3
4
DWORD WaitForSingleObject(
HANDLE hHandle, // 内核对象的句柄
DWORD dwMilliseconds // 等待的时间(毫秒)
);
  • 参数

    • hHandle:要等待的内核对象的句柄。这个句柄可以是由 CreateEventCreateMutexCreateSemaphoreCreateThread 等函数返回的句柄。
    • dwMilliseconds:指定等待的时间,单位为毫秒。可以是以下值之一:
      • INFINITE:表示无限等待,直到对象变为有信号状态。
      • 非零值:指定最大等待时间(毫秒)。如果在指定时间内对象没有变为有信号状态,函数会返回 WAIT_TIMEOUT
      • 0:表示立即返回,不等待。如果对象已经是有信号状态,函数立即返回;否则,函数立即返回 WAIT_TIMEOUT
  • 返回值

    • WAIT_OBJECT_0 (0x00000000L):指定的对象变为有信号状态。
    • WAIT_TIMEOUT (0x00000102L):等待超时,指定的对象未变为有信号状态。
    • WAIT_ABANDONED (0x00000080L):等待的对象是一个互斥体对象,且上一个拥有该互斥体的线程在没有释放互斥体的情况下终止。表示互斥体已被“放弃”。
    • WAIT_FAILED:函数调用失败。可以通过 GetLastError() 获取错误代码。

使用说明

  1. 同步操作

    • WaitForSingleObject() 主要用于线程同步,确保某个线程在执行某些操作之前等待另一个线程或进程完成其工作。
  2. 常见应用场景

    • 等待线程或进程终止:通过等待线程或进程的句柄,确保主线程在子线程或子进程完成后再继续执行。
    • 事件同步:通过等待事件对象,控制多个线程的执行顺序。
    • 互斥体和信号量:通过等待这些对象,控制对共享资源的访问。
  3. 注意事项

    • 如果使用 INFINITE 作为等待时间,线程将无限期地等待,直到对象变为有信号状态,这可能导致线程挂起,无法继续执行。
    • 对于互斥体,使用 WAIT_ABANDONED 返回值表示该互斥体对象被上一个线程错误地放弃,此时程序应小心处理共享资源的状态。

使用示例

下面是一个使用 WaitForSingleObject() 的简单示例,演示如何等待一个线程完成执行。

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
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
#include <windows.h>
#include <iostream>

// 线程函数
DWORD WINAPI ThreadProc(LPVOID lpParam) {
std::cout << "Thread is running..." << std::endl;
Sleep(3000); // 模拟线程工作 3 秒钟
std::cout << "Thread is exiting..." << std::endl;
return 0;
}

int main() {
// 创建线程
HANDLE hThread = CreateThread(
NULL, // 默认安全属性
0, // 默认堆栈大小
ThreadProc, // 线程函数
NULL, // 线程函数的参数
0, // 默认创建标志
NULL // 不接收线程 ID
);

if (hThread == NULL) {
std::cerr << "Failed to create thread. Error: " << GetLastError() << std::endl;
return 1;
}

// 等待线程完成
DWORD dwResult = WaitForSingleObject(hThread, INFINITE);
switch (dwResult) {
case WAIT_OBJECT_0:
std::cout << "Thread has terminated." << std::endl;
break;
case WAIT_TIMEOUT:
std::cerr << "Wait timed out." << std::endl;
break;
case WAIT_FAILED:
std::cerr << "Wait failed. Error: " << GetLastError() << std::endl;
break;
}

// 关闭线程句柄
CloseHandle(hThread);

return 0;
}

在这个示例中:

  • CreateThread() 函数创建了一个新线程,执行 ThreadProc 函数。
  • 主线程使用 WaitForSingleObject() 来等待子线程完成执行。因为等待时间设置为 INFINITE,主线程会一直等待,直到子线程终止。
  • 通过 dwResult 检查 WaitForSingleObject() 的返回值,决定接下来的操作。

WaitForSingleObject() 是多线程编程中非常重要的一个工具,能有效管理线程间的执行顺序和资源访问控制。

windows C++ GetLastError()函数 详解

GetLastError() 是 Windows API 中用于获取调用失败的函数返回的错误代码的函数。许多 Windows API 函数在执行失败时,不会直接返回错误信息,而是通过设置一个内部的线程局部变量来记录错误代码。调用 GetLastError() 函数可以检索到这个错误代码,用于诊断和处理错误情况。

函数原型

1
DWORD GetLastError(void);
  • 返回值
    • 返回一个 DWORD 类型的错误代码。这个错误代码是一个整数值,对应特定的错误类型。

使用说明

  1. 线程局部存储

    • GetLastError() 返回的错误代码与调用线程是关联的,即每个线程都有自己的错误代码存储区。因此,如果多线程程序中某个线程调用 GetLastError(),它获取到的错误代码仅适用于该线程的上下文。
  2. 使用场景

    • 当调用 Windows API 函数时,如果函数返回了一个失败的状态(例如返回 NULLINVALID_HANDLE_VALUE),通常应该紧接着调用 GetLastError() 来获取具体的错误代码。这有助于诊断失败的原因并采取相应措施。
  3. FormatMessage() 配合使用

    • 错误代码本身是一个数字,通常难以直接理解。可以使用 FormatMessage() 函数将错误代码转换为可读的错误消息字符串。
  4. 清除错误代码

    • GetLastError() 只会返回最近一次失败的函数调用的错误代码。对于成功的函数调用,错误代码不会被清除。因此,在进行新的操作之前,如果想确保没有残留的错误代码,可以先调用 SetLastError(0) 清除错误状态。

常见错误代码

以下是一些常见的错误代码及其含义:

  • ERROR_SUCCESS (0):操作成功。
  • ERROR_FILE_NOT_FOUND (2):系统找不到指定的文件。
  • ERROR_ACCESS_DENIED (5):拒绝访问。
  • ERROR_INVALID_HANDLE (6):句柄无效。
  • ERROR_NOT_ENOUGH_MEMORY (8):内存不足,无法完成此操作。
  • ERROR_INVALID_PARAMETER (87):参数错误。
  • ERROR_INSUFFICIENT_BUFFER (122):缓冲区大小不足。

完整的错误代码列表可以在微软文档中找到。

使用示例

以下是一个简单的示例,演示如何使用 GetLastError() 获取错误代码并显示相应的错误消息:

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
30
31
32
33
34
35
36
37
38
39
40
41
42
#include <windows.h>
#include <iostream>

int main() {
// 尝试打开一个不存在的文件
HANDLE hFile = CreateFile(
L"nonexistent_file.txt", // 文件名
GENERIC_READ, // 访问模式
0, // 共享模式
NULL, // 安全属性
OPEN_EXISTING, // 如何创建
FILE_ATTRIBUTE_NORMAL, // 文件属性
NULL // 模板文件句柄
);

if (hFile == INVALID_HANDLE_VALUE) {
DWORD dwError = GetLastError(); // 获取错误代码
std::cerr << "Failed to open file. Error code: " << dwError << std::endl;

// 使用 FormatMessage 获取错误信息
LPVOID lpMsgBuf;
FormatMessage(
FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_FROM_SYSTEM | FORMAT_MESSAGE_IGNORE_INSERTS,
NULL,
dwError,
MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT),
(LPWSTR)&lpMsgBuf,
0,
NULL
);

std::wcerr << "Error message: " << (LPWSTR)lpMsgBuf << std::endl;

// 释放 FormatMessage 分配的缓冲区
LocalFree(lpMsgBuf);
} else {
std::cout << "File opened successfully." << std::endl;
CloseHandle(hFile); // 关闭文件句柄
}

return 0;
}

在这个示例中:

  • 尝试打开一个不存在的文件 nonexistent_file.txt
  • CreateFile() 调用失败并返回 INVALID_HANDLE_VALUE,表示操作失败。
  • 使用 GetLastError() 获取具体的错误代码,并使用 FormatMessage() 将错误代码转换为可读的错误消息。

这个函数对于调试和错误处理非常重要,可以帮助开发者准确定位问题所在。

windows C++ ClearCommError()函数 详解

ClearCommError() 是 Windows API 中用于处理通信端口(如串口)错误的函数。它用于获取通信设备的错误信息,并可以清除通信设备的错误状态。这个函数通常用于串口通信程序中,用来检查和处理通信异常情况。

函数原型

1
2
3
4
5
BOOL ClearCommError(
HANDLE hFile, // 通信设备句柄
LPDWORD lpErrors, // 指向一个变量,该变量接收设备错误信息
LPCOMSTAT lpStat // 指向 COMSTAT 结构,该结构接收通信状态信息
);
  • 参数

    • hFile:通信设备的句柄。通常通过 CreateFile() 函数获取,用于表示串口设备(如 COM1, COM2)。
    • lpErrors:指向一个 DWORD 变量的指针,用于接收通信设备的错误状态信息。这个参数可以为 NULL,如果不需要获取错误信息。
    • lpStat:指向一个 COMSTAT 结构的指针,该结构接收设备的通信状态信息。这个参数可以为 NULL,如果不需要获取通信状态信息。
  • 返回值

    • 如果函数调用成功,返回值为非零值 (TRUE)。
    • 如果函数调用失败,返回值为零 (FALSE)。可以通过调用 GetLastError() 函数获取详细的错误信息。

错误状态

lpErrors 参数接收的错误状态是一个或多个以下值的组合:

  • CE_BREAK:接收到中断信号。
  • CE_FRAME:硬件检测到帧错误。
  • CE_OVERRUN:输入缓冲区溢出。数据丢失。
  • CE_RXOVER:输入缓冲区溢出,字符被丢弃。
  • CE_RXPARITY:接收到的字符有奇偶校验错误。
  • CE_TXFULL:应用程序试图传输字符时,输出缓冲区已满。

COMSTAT 结构

lpStat 参数指向的 COMSTAT 结构,用于获取通信设备的状态信息。该结构包括如下成员:

1
2
3
4
5
6
7
8
9
10
11
12
typedef struct _COMSTAT {
DWORD fCtsHold : 1; // CTS (Clear To Send) 信号被保持
DWORD fDsrHold : 1; // DSR (Data Set Ready) 信号被保持
DWORD fRlsdHold : 1; // RLSD (Receive Line Signal Detect) 信号被保持
DWORD fXoffHold : 1; // XOFF 被保持
DWORD fXoffSent : 1; // 已发送 XOFF
DWORD fEof : 1; // 已接收到 EOF
DWORD fTxim : 1; // 传输缓冲区被空中断
DWORD fReserved : 25; // 保留
DWORD cbInQue; // 输入缓冲区中的字节数
DWORD cbOutQue; // 输出缓冲区中的字节数
} COMSTAT, *LPCOMSTAT;

使用示例

下面是一个使用 ClearCommError() 函数的简单示例:

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
30
31
32
33
34
35
36
37
#include <windows.h>
#include <iostream>

int main() {
// 打开串口 (COM1)
HANDLE hComm = CreateFile(
L"COM1", // 设备名
GENERIC_READ | GENERIC_WRITE, // 访问模式
0, // 共享模式
NULL, // 安全属性
OPEN_EXISTING, // 打开已存在的设备
0, // 文件属性
NULL // 模板文件句柄
);

if (hComm == INVALID_HANDLE_VALUE) {
std::cerr << "Failed to open COM1. Error: " << GetLastError() << std::endl;
return 1;
}

// 检查并清除错误
DWORD dwErrors;
COMSTAT comStat;
if (ClearCommError(hComm, &dwErrors, &comStat)) {
if (dwErrors != 0) {
std::cerr << "Communication error occurred: " << dwErrors << std::endl;
} else {
std::cout << "No errors. Bytes in queue: " << comStat.cbInQue << std::endl;
}
} else {
std::cerr << "Failed to clear communication error. Error: " << GetLastError() << std::endl;
}

// 关闭串口
CloseHandle(hComm);
return 0;
}

在这个示例中:

  • CreateFile() 用于打开串口设备 COM1
  • ClearCommError() 用于检查通信错误,并获取当前通信状态。
  • 如果有错误发生,dwErrors 变量将包含具体的错误代码。
  • comStat 结构提供有关输入和输出缓冲区状态的信息。

这个函数在串口通信程序中非常有用,可以帮助开发者处理通信中的异常情况,如数据丢失、缓冲区溢出等问题。

windows C++ CloseHandle()函数 详解

CloseHandle() 是 Windows API 中用于关闭内核对象句柄的函数。它是 Windows 操作系统中资源管理的一部分,用于释放进程中占用的系统资源。

函数原型

1
BOOL CloseHandle(HANDLE hObject);
  • 参数

    • hObject:需要关闭的句柄。这个句柄可以是打开的文件、线程、进程、信号量、文件映射对象、互斥体等内核对象。
  • 返回值

    • 如果函数调用成功,返回值为非零值 (TRUE)。
    • 如果函数调用失败,返回值为零 (FALSE)。可以通过调用 GetLastError() 函数获取详细的错误信息。

使用说明

  1. 资源管理

    • 在 Windows 操作系统中,许多资源(如文件、进程、线程等)都是通过句柄来管理的。每当你创建或打开这些资源时,系统都会分配一个句柄。当不再需要这些资源时,必须调用 CloseHandle() 来释放句柄,否则会导致资源泄漏。
  2. 句柄类型

    • CloseHandle() 可以用于关闭多种类型的句柄,例如文件句柄、线程句柄、进程句柄、互斥体句柄、事件对象句柄等。需要确保关闭正确的句柄类型,以避免程序异常。
  3. 多次调用

    • 对同一个句柄多次调用 CloseHandle() 是错误的行为。这将导致未定义的行为,可能会引发程序崩溃或其他严重的错误。因此,调用 CloseHandle() 后,不应再使用这个句柄。
  4. 系统资源的自动释放

    • 当进程终止时,系统会自动关闭该进程中所有打开的句柄。但依赖于系统自动关闭句柄通常不是一个好的实践,程序应该显式地调用 CloseHandle() 来关闭不再需要的句柄。

示例代码

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
30
#include <windows.h>
#include <iostream>

int main() {
// 打开一个文件
HANDLE hFile = CreateFile(
L"example.txt", // 文件名
GENERIC_READ, // 访问模式
0, // 共享模式
NULL, // 安全属性
OPEN_EXISTING, // 如何创建
FILE_ATTRIBUTE_NORMAL, // 文件属性
NULL); // 模板文件句柄

if (hFile == INVALID_HANDLE_VALUE) {
std::cerr << "Failed to open file. Error: " << GetLastError() << std::endl;
return 1;
}

// 执行文件操作...

// 关闭文件句柄
if (CloseHandle(hFile)) {
std::cout << "File handle closed successfully." << std::endl;
} else {
std::cerr << "Failed to close file handle. Error: " << GetLastError() << std::endl;
}

return 0;
}

在这个示例中,CreateFile() 函数用于打开一个文件,并返回一个文件句柄。然后,使用 CloseHandle() 函数关闭这个文件句柄,释放相关资源。

windows C++ PurgeComm()函数 详解

PurgeComm 函数是 Windows API 中用于清除串口通信设备的输入或输出缓冲区的函数。它可以有效地清除缓冲区中的数据以及挂起的输入或输出请求,确保串口通信处于已知状态。这在处理通信错误或重置串口设备时非常有用。

函数原型

1
2
3
4
BOOL PurgeComm(
HANDLE hFile,
DWORD dwFlags
);

参数详解

  1. hFile

    • 类型:HANDLE
    • 描述:这是一个串口设备的句柄,通常由 CreateFile 函数获得,代表一个打开的串口通信端口(如 "COM1")。
  2. dwFlags

    • 类型:DWORD

    • 描述:指定要清除的缓冲区或挂起的操作的标志。可以是以下值的组合:

    • PURGE_RXABORT (0x0002): 终止所有挂起的读取操作。未完成的读取操作将失败。

    • PURGE_RXCLEAR (0x0008): 清除接收缓冲区中的数据。

    • PURGE_TXABORT (0x0001): 终止所有挂起的写入操作。未完成的写入操作将失败。

    • PURGE_TXCLEAR (0x0004): 清除发送缓冲区中的数据。

    这些标志可以通过按位或 (|) 组合使用,例如 PURGE_RXABORT | PURGE_TXCLEAR

返回值

  • 成功:如果函数执行成功,返回 TRUE,表示缓冲区已被成功清除。
  • 失败:如果函数执行失败,返回 FALSE,可以通过调用 GetLastError() 来获取更多错误信息。

使用示例

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
30
31
32
33
34
35
#include <windows.h>
#include <iostream>

int main() {
// 打开串口
HANDLE hSerial = CreateFile(
"COM1", // 串口名称
GENERIC_READ | GENERIC_WRITE, // 读写权限
0, // 独占访问
NULL, // 默认安全属性
OPEN_EXISTING, // 打开现有串口
0, // 属性标志
NULL // 不使用模板文件
);

if (hSerial == INVALID_HANDLE_VALUE) {
std::cerr << "Failed to open COM port. Error: " << GetLastError() << std::endl;
return 1;
}

// 清除接收缓冲区和终止所有挂起的读取操作
if (!PurgeComm(hSerial, PURGE_RXCLEAR | PURGE_RXABORT)) {
std::cerr << "Failed to purge COM port. Error: " << GetLastError() << std::endl;
CloseHandle(hSerial);
return 1;
}

std::cout << "COM port purged successfully." << std::endl;

// 进行其他串口通信操作...

// 关闭串口
CloseHandle(hSerial);
return 0;
}

解释示例中 PurgeComm 的使用

在上面的例子中,我们首先打开了 COM1 串口。接着,我们使用 PurgeComm 函数清除了接收缓冲区 (PURGE_RXCLEAR) 并终止了所有挂起的读取操作 (PURGE_RXABORT)。这有助于在进行进一步的串口操作之前,确保没有未处理的旧数据或挂起的操作。

典型用法场景

  • 处理通信错误:在检测到通信错误后,可以使用 PurgeComm 清除串口缓冲区,以便重新开始通信。
  • 重置串口状态:当需要重置串口状态时,可以清除所有挂起的操作和缓冲区内容,确保通信的稳定性。
  • 同步操作:当程序需要与设备重新同步时,可以通过清除接收缓冲区来忽略不完整或意外的输入。

注意事项

  • 挂起操作的影响:使用 PURGE_RXABORTPURGE_TXABORT 标志会导致挂起的读取或写入操作失败,并返回错误。使用这些标志时需要确保程序能够正确处理这些失败的操作。
  • 数据丢失:清除缓冲区(使用 PURGE_RXCLEARPURGE_TXCLEAR)会导致缓冲区中的数据丢失。因此,调用 PurgeComm 函数之前应确保缓冲区中的数据已被处理或不再需要。

常见错误

  • **ERROR_INVALID_HANDLE**:无效的句柄,可能是因为串口未成功打开或句柄已关闭。
  • **ERROR_IO_PENDING**:有未完成的 I/O 操作。这通常表示在尝试清除缓冲区时,有未处理完的操作。

PurgeComm 是串口通信中一个重要的维护工具,特别是在需要处理错误、重置通信状态或确保系统处于已知状态时。通过正确使用该函数,可以提高串口通信的稳定性和可靠性。

windows C++ SetCommTimeouts()函数 详解

SetCommTimeouts 函数是 Windows API 中用于设置串口通信设备的超时时间的函数。它允许你定义串口设备在读取和写入操作时的超时行为,这是确保串口通信可靠性的重要一步。

函数原型

1
2
3
4
BOOL SetCommTimeouts(
HANDLE hFile,
LPCOMMTIMEOUTS lpCommTimeouts
);

参数详解

  1. hFile

    • 类型:HANDLE
    • 描述:这是一个串口设备的句柄,通常由 CreateFile 函数获得,代表一个打开的串口通信端口(如 "COM1")。
  2. lpCommTimeouts

    • 类型:LPCOMMTIMEOUTS
    • 描述:指向 COMMTIMEOUTS 结构的指针,该结构包含了设备输入输出操作的超时设置。

返回值

  • 成功:如果函数执行成功,返回 TRUE,表示串口设备的超时设置已被成功应用。
  • 失败:如果函数执行失败,返回 FALSE,可以通过调用 GetLastError() 来获取更多错误信息。

COMMTIMEOUTS 结构体

COMMTIMEOUTS 结构体定义了串口设备读写操作的超时设置。结构体定义如下:

1
2
3
4
5
6
7
typedef struct _COMMTIMEOUTS {
DWORD ReadIntervalTimeout; // 读取字符间隔超时(毫秒)
DWORD ReadTotalTimeoutMultiplier; // 总读取超时乘子
DWORD ReadTotalTimeoutConstant; // 总读取超时常量(毫秒)
DWORD WriteTotalTimeoutMultiplier; // 总写入超时乘子
DWORD WriteTotalTimeoutConstant; // 总写入超时常量(毫秒)
} COMMTIMEOUTS, *LPCOMMTIMEOUTS;

结构体字段详解

  1. ReadIntervalTimeout

    • 描述:指定两次字符读取之间的最大间隔时间。如果超出此时间,读取操作将完成。以毫秒为单位。
    • 特殊值:
      • MAXDWORD:表示非零值的超时时间无效,系统返回立即可用的数据,而不等待进一步的数据输入。
  2. ReadTotalTimeoutMultiplier

    • 描述:指定读取操作的超时乘子。实际的超时为乘子乘以读取的字符数。
  3. ReadTotalTimeoutConstant

    • 描述:指定读取操作的总超时常量。该值加上 ReadTotalTimeoutMultiplier 的结果为总读取超时时间。
  4. WriteTotalTimeoutMultiplier

    • 描述:指定写入操作的超时乘子。实际的超时为乘子乘以写入的字符数。
  5. WriteTotalTimeoutConstant

    • 描述:指定写入操作的总超时常量。该值加上 WriteTotalTimeoutMultiplier 的结果为总写入超时时间。

使用示例

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
30
31
32
33
34
35
36
37
38
39
40
41
42
#include <windows.h>
#include <iostream>

int main() {
// 打开串口
HANDLE hSerial = CreateFile(
"COM1", // 串口名称
GENERIC_READ | GENERIC_WRITE, // 读写权限
0, // 独占访问
NULL, // 默认安全属性
OPEN_EXISTING, // 打开现有串口
0, // 属性标志
NULL // 不使用模板文件
);

if (hSerial == INVALID_HANDLE_VALUE) {
std::cerr << "Failed to open COM port. Error: " << GetLastError() << std::endl;
return 1;
}

// 设置串口超时参数
COMMTIMEOUTS timeouts = { 0 };
timeouts.ReadIntervalTimeout = 50; // 50ms 的字符间隔超时
timeouts.ReadTotalTimeoutMultiplier = 10; // 每个字符的读取时间为 10ms
timeouts.ReadTotalTimeoutConstant = 100; // 总读取操作的附加时间为 100ms
timeouts.WriteTotalTimeoutMultiplier = 10; // 每个字符的写入时间为 10ms
timeouts.WriteTotalTimeoutConstant = 100; // 总写入操作的附加时间为 100ms

if (!SetCommTimeouts(hSerial, &timeouts)) {
std::cerr << "Failed to set COM port timeouts. Error: " << GetLastError() << std::endl;
CloseHandle(hSerial);
return 1;
}

std::cout << "COM port timeouts configured successfully." << std::endl;

// 进行其他串口通信操作...

// 关闭串口
CloseHandle(hSerial);
return 0;
}

解释示例中超时设置的逻辑

  • 读取超时

    • ReadIntervalTimeout = 50:如果两次字符读取之间的间隔超过 50 毫秒,读取操作将结束。
    • ReadTotalTimeoutMultiplier = 10:对于每个要读取的字符,设置 10 毫秒的超时。
    • ReadTotalTimeoutConstant = 100:总读取超时常量为 100 毫秒。

    例如,如果要读取 5 个字符,总读取超时时间为:(5 * 10) + 100 = 150 毫秒。

  • 写入超时

    • WriteTotalTimeoutMultiplier = 10:对于每个要写入的字符,设置 10 毫秒的超时。
    • WriteTotalTimeoutConstant = 100:总写入超时常量为 100 毫秒。

    例如,如果要写入 5 个字符,总写入超时时间为:(5 * 10) + 100 = 150 毫秒。

注意事项

  • 超时的适应性:设置的超时应根据实际应用的需要进行调整。如果超时设置得过短,可能会导致读取或写入操作过早地结束;而如果超时设置得过长,则可能会导致应用程序响应迟缓。
  • 特殊情况:如果串口通信中需要实时处理(如工业控制),则超时设置要特别小心,确保在通信故障时系统能够快速响应。

常见错误

  • **ERROR_INVALID_HANDLE**:无效的句柄,可能是因为串口未成功打开或句柄已关闭。
  • **ERROR_INVALID_PARAMETER**:传递给 SetCommTimeouts 的参数无效,可能是 COMMTIMEOUTS 结构体中的字段值不合理。

SetCommTimeouts 函数是配置串口通信设备超时的关键函数,通过合理设置,可以确保串口通信的有效性和可靠性,避免因超时问题导致的通信失败。

windows C++ SetCommState()函数 详解

SetCommState 函数是 Windows API 中用于设置串口设备通信参数的一个函数。它可以修改串口设备的配置,如波特率、数据位、停止位和奇偶校验等。这对于串口通信非常重要,因为需要确保串口设备的设置与通信双方的要求一致。

函数原型

1
2
3
4
BOOL SetCommState(
HANDLE hFile,
LPDCB lpDCB
);

参数详解

  1. hFile

    • 类型:HANDLE
    • 描述:这是一个串口设备的句柄,通常由 CreateFile 函数获得,代表一个打开的串口通信端口(如 "COM1")。
  2. lpDCB

    • 类型:LPDCB
    • 描述:指向 DCB(Device Control Block)结构的指针,该结构包含了串口设备的通信设置。通过 SetCommState 函数,你可以将这些设置应用到串口设备上。

返回值

  • 成功:如果函数执行成功,返回 TRUE,表示串口设备的配置已被成功修改。
  • 失败:如果函数执行失败,返回 FALSE,可以通过调用 GetLastError() 来获取更多错误信息。

DCB 结构体

DCB 结构体保存了串口设备的详细设置,如波特率、数据位、停止位、奇偶校验等。该结构体的定义如下:

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
30
typedef struct _DCB {
DWORD DCBlength; // DCB结构体大小
DWORD BaudRate; // 波特率
DWORD fBinary : 1; // 二进制模式,必须为 TRUE
DWORD fParity : 1; // 启用奇偶校验
DWORD fOutxCtsFlow : 1;// CTS(清除发送)流控制
DWORD fOutxDsrFlow : 1;// DSR(数据设置就绪)流控制
DWORD fDtrControl : 2; // DTR(数据终端就绪)流控制
DWORD fDsrSensitivity : 1; // DSR敏感性
DWORD fTXContinueOnXoff : 1; // 在接收到XOFF时继续发送
DWORD fOutX : 1; // 启用XON/XOFF发送控制
DWORD fInX : 1; // 启用XON/XOFF接收控制
DWORD fErrorChar : 1; // 启用错误字符替换
DWORD fNull : 1; // 启用空字节丢弃
DWORD fRtsControl : 2; // RTS(请求发送)流控制
DWORD fAbortOnError : 1; // 发生错误时中止所有读写操作
DWORD fDummy2 : 17; // 保留
WORD wReserved; // 保留
WORD XonLim; // 传输XON字符之前输入缓冲区中最少的字节数
WORD XoffLim; // 传输XOFF字符之前输入缓冲区中最多的字节数
BYTE ByteSize; // 数据位数(4-8)
BYTE Parity; // 奇偶校验设置(0-4 = 无,奇,偶,标记,空格)
BYTE StopBits; // 停止位数(0,1,2 = 1位,1.5位,2位)
char XonChar; // XON字符
char XoffChar; // XOFF字符
char ErrorChar; // 错误字符(如果fErrorChar为TRUE)
char EofChar; // 文件结束字符
char EvtChar; // 事件字符
WORD wReserved1; // 保留
} DCB, *LPDCB;

使用示例

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
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
#include <windows.h>
#include <iostream>

int main() {
// 打开串口
HANDLE hSerial = CreateFile(
"COM1", // 串口名称
GENERIC_READ | GENERIC_WRITE, // 读写权限
0, // 独占访问
NULL, // 默认安全属性
OPEN_EXISTING, // 打开现有串口
0, // 属性标志
NULL // 不使用模板文件
);

if (hSerial == INVALID_HANDLE_VALUE) {
std::cerr << "Failed to open COM port. Error: " << GetLastError() << std::endl;
return 1;
}

// 获取当前串口状态
DCB dcbSerialParams = { 0 };
dcbSerialParams.DCBlength = sizeof(dcbSerialParams);

if (!GetCommState(hSerial, &dcbSerialParams)) {
std::cerr << "Failed to get COM port state. Error: " << GetLastError() << std::endl;
CloseHandle(hSerial);
return 1;
}

// 配置串口参数
dcbSerialParams.BaudRate = CBR_9600; // 设置波特率为9600
dcbSerialParams.ByteSize = 8; // 设置数据位为8
dcbSerialParams.StopBits = ONESTOPBIT; // 设置停止位为1
dcbSerialParams.Parity = NOPARITY; // 设置无奇偶校验

// 设置串口状态
if (!SetCommState(hSerial, &dcbSerialParams)) {
std::cerr << "Failed to set COM port state. Error: " << GetLastError() << std::endl;
CloseHandle(hSerial);
return 1;
}

std::cout << "COM port configured successfully." << std::endl;

// 进行其他串口通信操作...

// 关闭串口
CloseHandle(hSerial);
return 0;
}

主要字段详解

  • **BaudRate**:设置串口的波特率(例如 CBR_9600 表示 9600 bps)。
  • **ByteSize**:设置每个数据包的数据位数,可以是 4 到 8 位。
  • **Parity**:设置奇偶校验位,常用值包括 NOPARITY (0),ODDPARITY (1),EVENPARITY (2)。
  • **StopBits**:设置停止位数,常用值为 ONESTOPBIT (0),ONE5STOPBITS (1),TWOSTOPBITS (2)。
  • **fBinary**:必须设置为 TRUE,表示串口以二进制模式工作。
  • **fParity**:是否启用奇偶校验。

注意事项

  • 结构体初始化:在调用 SetCommState 之前,确保 DCB 结构体的所有字段都已正确设置,特别是 DCBlength 字段应被设置为 sizeof(DCB)
  • 获取和设置状态:通常在调用 SetCommState 之前,先使用 GetCommState 获取当前串口配置,然后对 DCB 结构体进行修改,并再调用 SetCommState 进行设置。
  • 波特率一致性:确保通信双方使用相同的波特率和其他通信参数,否则会导致通信失败或数据错误。

常见错误

  • **ERROR_INVALID_HANDLE**:无效的句柄,可能是因为串口未成功打开或句柄已关闭。
  • **ERROR_BAD_COMMAND**:请求的操作不能被串口设备执行,可能是由于串口不支持特定的配置。
  • **ERROR_INVALID_PARAMETER**:传递给 SetCommState 的参数无效,可能是 DCB 结构体中的字段值不合理。

SetCommState 是配置串口通信的核心函数,它允许你设置各种串口通信参数,以确保串口设备按照期望的方式工作。

windows C++ GetCommState()函数 详解

GetCommState 函数是 Windows API 中用于获取串口通信设备当前配置的一个函数。它可以获取串口设备的通信参数,包括波特率、数据位、停止位和奇偶校验设置等。

函数原型

1
2
3
4
BOOL GetCommState(
HANDLE hFile,
LPDCB lpDCB
);

参数详解

  1. hFile

    • 类型:HANDLE
    • 描述:这是一个串口设备的句柄,通常由 CreateFile 函数获得,代表一个打开的串口通信端口(如 "COM1")。
  2. lpDCB

    • 类型:LPDCB
    • 描述:指向 DCB 结构的指针,该结构用于存储串口设备的当前配置。DCB 结构保存了串口的详细设置,包括波特率、数据位、停止位、奇偶校验等。

返回值

  • 成功:如果函数执行成功,返回 TRUE,并且 lpDCB 指向的结构体被填充为当前的串口配置。
  • 失败:如果函数执行失败,返回 FALSE,可以通过调用 GetLastError() 来获取更多错误信息。

DCB 结构体

DCB(Device Control Block)结构体包含了串口设备的配置信息。结构体定义如下:

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
30
typedef struct _DCB {
DWORD DCBlength; // DCB结构体大小
DWORD BaudRate; // 波特率
DWORD fBinary : 1; // 二进制模式,必须为 TRUE
DWORD fParity : 1; // 启用奇偶校验
DWORD fOutxCtsFlow : 1;// CTS(清除发送)流控制
DWORD fOutxDsrFlow : 1;// DSR(数据设置就绪)流控制
DWORD fDtrControl : 2; // DTR(数据终端就绪)流控制
DWORD fDsrSensitivity : 1; // DSR敏感性
DWORD fTXContinueOnXoff : 1; // 在接收到XOFF时继续发送
DWORD fOutX : 1; // 启用XON/XOFF发送控制
DWORD fInX : 1; // 启用XON/XOFF接收控制
DWORD fErrorChar : 1; // 启用错误字符替换
DWORD fNull : 1; // 启用空字节丢弃
DWORD fRtsControl : 2; // RTS(请求发送)流控制
DWORD fAbortOnError : 1; // 发生错误时中止所有读写操作
DWORD fDummy2 : 17; // 保留
WORD wReserved; // 保留
WORD XonLim; // 传输XON字符之前输入缓冲区中最少的字节数
WORD XoffLim; // 传输XOFF字符之前输入缓冲区中最多的字节数
BYTE ByteSize; // 数据位数(4-8)
BYTE Parity; // 奇偶校验设置(0-4 = 无,奇,偶,标记,空格)
BYTE StopBits; // 停止位数(0,1,2 = 1位,1.5位,2位)
char XonChar; // XON字符
char XoffChar; // XOFF字符
char ErrorChar; // 错误字符(如果fErrorChar为TRUE)
char EofChar; // 文件结束字符
char EvtChar; // 事件字符
WORD wReserved1; // 保留
} DCB, *LPDCB;

使用示例

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
30
31
32
33
34
35
36
37
38
39
40
#include <windows.h>
#include <iostream>

int main() {
// 打开串口
HANDLE hSerial = CreateFile(
"COM1", // 串口名称
GENERIC_READ | GENERIC_WRITE, // 读写权限
0, // 独占访问
NULL, // 默认安全属性
OPEN_EXISTING, // 打开现有串口
0, // 属性标志
NULL // 不使用模板文件
);

if (hSerial == INVALID_HANDLE_VALUE) {
std::cerr << "Failed to open COM port. Error: " << GetLastError() << std::endl;
return 1;
}

// 获取串口状态
DCB dcbSerialParams = { 0 };
dcbSerialParams.DCBlength = sizeof(dcbSerialParams);

if (!GetCommState(hSerial, &dcbSerialParams)) {
std::cerr << "Failed to get COM port state. Error: " << GetLastError() << std::endl;
CloseHandle(hSerial);
return 1;
}

// 输出当前串口配置
std::cout << "Baud Rate: " << dcbSerialParams.BaudRate << std::endl;
std::cout << "Byte Size: " << static_cast<int>(dcbSerialParams.ByteSize) << std::endl;
std::cout << "Parity: " << static_cast<int>(dcbSerialParams.Parity) << std::endl;
std::cout << "Stop Bits: " << static_cast<int>(dcbSerialParams.StopBits) << std::endl;

// 关闭串口
CloseHandle(hSerial);
return 0;
}

主要字段详解

  • **BaudRate**:波特率,例如 9600、19200 等。
  • **ByteSize**:每个字节的数据位数,可以是 4 到 8。
  • **Parity**:奇偶校验位设置,常用值包括 NOPARITY (0),ODDPARITY (1),EVENPARITY (2)。
  • **StopBits**:停止位数,常用值为 ONESTOPBIT (0),ONE5STOPBITS (1),TWOSTOPBITS (2)。

注意事项

  • 结构体初始化:在调用 GetCommState 之前,确保 DCB 结构体的 DCBlength 字段已被正确设置为 sizeof(DCB)
  • 获取和设置状态:通常在调用 GetCommState 获取当前配置后,可以使用 SetCommState 修改配置并应用到串口设备上。
  • 设备句柄:确保传递给 GetCommState 的句柄是有效的,通常是通过 CreateFile 成功打开串口设备获得的句柄。

常见错误

  • **ERROR_INVALID_HANDLE**:无效的句柄,可能是因为串口未成功打开或句柄已关闭。
  • **ERROR_BAD_COMMAND**:请求的操作不能被串口设备执行,可能是由于串口不支持特定的配置。

GetCommState 函数在串口通信中非常重要,它让你能够读取和理解当前的串口配置,从而确保通信的正确性和稳定性。

windows C++ SetupComm()函数 详解

SetupComm 函数是 Windows API 中用于配置串口设备缓冲区大小的一个函数。它主要用于设置串口通信时的输入和输出缓冲区的大小。这在处理串口通信时非常重要,因为适当配置的缓冲区可以避免数据丢失或溢出。

函数原型

1
2
3
4
5
BOOL SetupComm(
HANDLE hFile,
DWORD dwInQueue,
DWORD dwOutQueue
);

参数详解

  1. hFile

    • 类型:HANDLE
    • 描述:这是一个串口设备的句柄。通常,该句柄由 CreateFile 函数获得,代表一个打开的串口通信端口(如 "COM1")。
  2. dwInQueue

    • 类型:DWORD
    • 描述:指定输入缓冲区的大小(以字节为单位)。这个缓冲区用于存储从串口接收到的数据。
  3. dwOutQueue

    • 类型:DWORD
    • 描述:指定输出缓冲区的大小(以字节为单位)。这个缓冲区用于存储将要通过串口发送的数据。

返回值

  • 成功:如果函数执行成功,返回 TRUE
  • 失败:如果函数执行失败,返回 FALSE,可以通过调用 GetLastError() 来获取更多错误信息。

使用示例

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
30
31
32
33
34
35
#include <windows.h>
#include <iostream>

int main() {
// 打开串口
HANDLE hSerial = CreateFile(
"COM1", // 串口名称
GENERIC_READ | GENERIC_WRITE, // 读写权限
0, // 独占访问
NULL, // 默认安全属性
OPEN_EXISTING, // 打开现有串口
0, // 属性标志
NULL // 不使用模板文件
);

if (hSerial == INVALID_HANDLE_VALUE) {
std::cerr << "Failed to open COM port. Error: " << GetLastError() << std::endl;
return 1;
}

// 设置输入缓冲区为 1024 字节,输出缓冲区为 1024 字节
if (!SetupComm(hSerial, 1024, 1024)) {
std::cerr << "Failed to setup COM port buffers. Error: " << GetLastError() << std::endl;
CloseHandle(hSerial);
return 1;
}

std::cout << "COM port buffers setup successfully." << std::endl;

// 进行其他串口通信操作...

// 关闭串口
CloseHandle(hSerial);
return 0;
}

注意事项

  • 缓冲区大小的设置:通常,输入和输出缓冲区的大小应根据应用程序的需求进行设置。较大的缓冲区可以容纳更多的数据,减少数据丢失的可能性,但也会占用更多的内存。
  • 句柄有效性:确保在调用 SetupComm 前,串口设备句柄是有效的。这意味着 CreateFile 成功打开了一个串口设备。
  • 缓冲区重设:如果需要更改缓冲区的大小,可以在打开串口设备后立即调用 SetupComm,以确保在任何数据传输之前正确配置缓冲区。

常见错误

  • ERROR_INVALID_HANDLE: 提供的句柄无效,可能是因为串口未成功打开。
  • ERROR_IO_PENDING: 该错误通常与重叠 I/O 操作有关,但在使用 SetupComm 时并不常见。

SetupComm 是串口通信设置中的一个基础函数,正确配置它可以确保串口数据通信的稳定性和效率。

windows C++ CreateFileA()函数 详解

CreateFileA 函数是 Windows API 中用于打开或创建文件、文件夹、符号链接、命名管道、通信设备等的一种函数。CreateFileA 是其 ANSI 版本,对应的 Unicode 版本为 CreateFileW。以下是 CreateFileA 函数的详解。

函数原型

1
2
3
4
5
6
7
8
9
HANDLE CreateFileA(
LPCSTR lpFileName,
DWORD dwDesiredAccess,
DWORD dwShareMode,
LPSECURITY_ATTRIBUTES lpSecurityAttributes,
DWORD dwCreationDisposition,
DWORD dwFlagsAndAttributes,
HANDLE hTemplateFile
);

参数详解

  1. lpFileName

    • 类型:LPCSTR
    • 描述:指向要打开或创建的对象的名称的指针。对于文件,这通常是文件的路径。如果是设备文件,则使用设备名称(例如 "\\\\.\\COM1")。
  2. dwDesiredAccess

    • 类型:DWORD
    • 描述:指定所需的访问权限。可以是以下常量的组合:
      • GENERIC_READ:读取访问。
      • GENERIC_WRITE:写入访问。
      • GENERIC_EXECUTE:执行访问。
      • GENERIC_ALL:所有访问权限。
  3. dwShareMode

    • 类型:DWORD
    • 描述:指定文件的共享模式,决定其他进程如何访问该文件。可以是以下常量的组合:
      • FILE_SHARE_READ:允许其他进程读取文件。
      • FILE_SHARE_WRITE:允许其他进程写入文件。
      • FILE_SHARE_DELETE:允许其他进程删除文件。
    • 如果此参数为 0,文件将被独占使用。
  4. lpSecurityAttributes

    • 类型:LPSECURITY_ATTRIBUTES
    • 描述:指向 SECURITY_ATTRIBUTES 结构的指针,该结构指定返回的句柄是否可被子进程继承以及文件或对象的安全描述符。如果为 NULL,句柄不可继承,且对象没有指定的安全描述符。
  5. dwCreationDisposition

    • 类型:DWORD
    • 描述:指定如何创建或打开文件,以下是常用的选项:
      • CREATE_NEW:创建新文件。如果文件已存在,函数将失败。
      • CREATE_ALWAYS:创建新文件。如果文件已存在,将覆盖该文件。
      • OPEN_EXISTING:打开现有文件。如果文件不存在,函数将失败。
      • OPEN_ALWAYS:打开文件,如果文件不存在则创建新文件。
      • TRUNCATE_EXISTING:打开现有文件并截断(清空)文件内容。该文件必须有写入权限。
  6. dwFlagsAndAttributes

    • 类型:DWORD
    • 描述:指定文件或设备的标志和属性。常用的标志包括:
      • FILE_ATTRIBUTE_ARCHIVE:文件归档属性。
      • FILE_ATTRIBUTE_HIDDEN:文件为隐藏文件。
      • FILE_ATTRIBUTE_NORMAL:无特殊属性集的文件。
      • FILE_ATTRIBUTE_READONLY:只读文件。
      • FILE_FLAG_DELETE_ON_CLOSE:文件在关闭时自动删除。
      • FILE_FLAG_SEQUENTIAL_SCAN:访问模式为顺序扫描。
      • FILE_FLAG_RANDOM_ACCESS:访问模式为随机访问。
  7. hTemplateFile

    • 类型:HANDLE
    • 描述:用于指定一个有效的模板文件句柄,模板文件的属性将复制到新创建的文件中。该参数通常用于创建新文件时设置与模板文件相同的属性。如果不需要模板文件,设置为 NULL

返回值

  • 成功:返回一个指向新打开文件、设备、管道等的句柄 (HANDLE)。你可以使用此句柄进行读写操作。
  • 失败:返回 INVALID_HANDLE_VALUE,可以调用 GetLastError() 获取详细的错误信息。

使用示例

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
#include <windows.h>
#include <iostream>

int main() {
HANDLE hFile = CreateFileA(
"example.txt", // 文件名
GENERIC_READ | GENERIC_WRITE, // 读写权限
0, // 独占模式,不共享
NULL, // 默认安全属性
CREATE_ALWAYS, // 总是创建新文件
FILE_ATTRIBUTE_NORMAL, // 普通文件
NULL // 不使用模板文件
);

if (hFile == INVALID_HANDLE_VALUE) {
std::cout << "Failed to create or open file. Error: " << GetLastError() << std::endl;
return 1;
}

std::cout << "File created/opened successfully." << std::endl;

// 进行文件操作...

CloseHandle(hFile); // 关闭文件句柄
return 0;
}

注意事项

  • 打开现有文件时,确保使用正确的权限设置(dwDesiredAccess),否则可能会导致访问失败。
  • 如果文件被其他进程占用且未使用共享模式,你可能会遇到无法访问文件的情况。
  • 在使用 CreateFileA 打开设备(如串口或并口)时,lpFileName 参数需要使用特定的格式(如 "\\\\.\\COM1")。

常见错误

  • ERROR_FILE_NOT_FOUND: 文件不存在,且未指定创建新文件。
  • ERROR_ACCESS_DENIED: 权限不足,无法访问文件。

这个函数的灵活性和多功能性使它在 Windows 编程中非常重要。

简介

  • Effective Modern C++ 中文版学习笔记,Scott Meyers著作,高博译

译者序

  • 现代C++在语言方面所进行的大刀阔斧,釜底抽薪式的变革,无需赘言。但是这些变革背后,更重要的反而是其保持不变者,即所谓C++语言的精神,或曰设计哲学。例如,由实际问题驱动,并立刻用于解决实际问题。现代C++中提供了并发API,在语言层面上支持并发程序设计,结束了在各种体系结构和操作系统之上存在很多互不兼容的第三方并发库的乱局,就是这种设计哲学的体现。程序员应该能够自由地选择自己的程序设计风格,而语言应该为该风格提供完备的支持
  • C++语言之难,主要还是在于众多语言特性之间的综合交叉。尤其对于新的语言特性,掌握其本身往往并不是很难,而要考虑到它与众多也已经存在的语言特性之发生的相互作用就不容易了。

绪论

  • 本书的写作目的并非对于C++11和C++14特性的泛泛介绍,而是为了揭示他们的高效应用。
  • 本书中的信息被分解成若干准则,称为条款。本书中的条款都是准则,而非规则,因为准则允许有例外。条款给出的建议并非最要紧的部分,建议背后的原理才是精华。只有掌握了原理,你才能判定,你的项目面临的具体情况是否真的违反了条款所指。本书的真正目标并不在于告诉你什么该做,什么不该做,而是想要传达对C++11和C++14运作原理的更深入理解

术语和惯例

  • C++98缺乏并发支持(仅对C++98和C++03成立)

  • C++11支持lambda表达式(对C++11和C++14成立)

  • C++14提供了广义返回值性别推导(仅对C++14成立)

  • C++11被最广泛接受的特性可能莫过于移动语义,而移动语义的基础在于区分左值表达式和右值表达式。因为,一个对象是右值意味着能够对其实施移动语义,而左值则一般不然。从概念上说(实践上并不总是成立),右值对应的是函数返回的临时对象,而左值对应的是可指涉的对象,而指涉的途径则无论通过名字,指针,还是左值引用皆可。

  • 有一种甄别表达式是否左值的使用方法富有启发性,那就是检查能否取得该表达式的地址。如果可以取得,那么该表达式基本上可以断定是左值。如果不可以,则其通常是右值。这种方法之所以说富有启发性,是因为它让你记得,表达式的型别与它是左值还是右值没有关系。换言之,给定一型别T,则既有T型别的左值,也有T型别的右值。这一点在处理右值引用型别的形参时尤其要注意,因为该形参本身是个左值。

    1
    2
    3
    4
    5
    class Widget
    {
    public:
    Widget(Widget&& rhs); // rhs是个左值,尽管它具有右值引用型别
    };
  • 在Widget的移动构造函数内部对rhs取址完全没有问题,所以rhs是个左值,尽管它的型别属于右值引用(基于类似的理由,我们可以得知,任何形参都是左值)

  • 若某对象是依据同一型别的另一对象初始化出来的,则该新对象称为提供初始化依据的对象的一个副本,即使该副本是由移动构造函数创建的。这样称呼情有可原,因为C++中并无术语用以区分某对象到底是经由复制否早函数创建的副本,还是经由移动构造函数创建的副本。

    1
    2
    3
    4
    void someFunc(Widget w);    // someFunc的形参w按值传递
    Widget wid; // wid是Widget型别的某个对象
    someFunc(wid); // 在这个对someFunc的调用中,w是wid经由复制构造函数创建的副本
    someFunc(std::move(wid)); // 在这个对someFunc的调用中,w是wid经由移动构造函数创建的副本
  • 右值的副本经常经由移动构造函数创建,而左值的副本通常经由复制构造函数创建。这也就是说,如果你仅仅了解到某个对象是另一个对象的副本,则还不能判断构造这个副本要花费多少成本。

  • 在函数调用中,调用方的表达式,称为函数的实参。实参的用处,是初始化函数的形参。

  • 在上面someFunc的第一次调用中,实参是wid。而在第二次调用中,实参则是std::move(wid)。在两次调用中,形参都是w。

  • 实参和形参有着重大的区别,因为形参都是左值,而用来作为其初始化依据的实参,则极可能是右值,也可能是左值。这一点在完美转发(perfect forwarding)的过程中尤其关系重大,在这样的一个过程中,传递给某个函数的实参会被传递给另一个函数,并保持其右值性(rvalueness)或左值性(lvalueness)

  • 设计良好的函数都是异常安全的,这意味着它们至少会提供基本异常安全保证(即基本保证)。提供了基本保证的函数能够向调用者确保即使有异常抛出,程序的不变量不会受到影响(即不会有数据结构被破坏),且不会发生资源泄露。而提供了强异常安全保证(即强保证)的函数则能够通过向调用者确保即使有异常抛出,程序状态会在调用前后保持不变。

  • 当提及函数对象时,我通常意指某个对象,其型别支持operator()成员函数。换言之,就是说该对象表现得像个函数。进一步泛化这一术语的话,就涵盖了指涉到成员函数的指针,从而得到了所谓的可调用物。一般情况下,你可以不用关心这些含义之前的细微差别,函数指针也好,可调用物也罢,你只需要知道他们在C++中表示某种函数调用语法加以调用就行了。

  • 经由lambda表达式创建的函数对象称为闭包,将lambda表达式和他们创建的闭包区分开来,意义不大,所以我经常把他们统称为lambda式。

  • 相似的,我也很少区分函数模板(即用以生成函数的模板)和模板函数(即从函数模板生成的函数)。类模板和模板类的情形同上。

  • C++中有很多事物能够加以声明和定义。声明的作用是引入名字和型别,而不给出细节,例如存储位置或具体实现

    1
    2
    3
    4
    extern int x;               // 对象生命
    class Widget; // 类声明
    bool func(const Widget& w); // 函数声明
    enum class Color; // 限定作用域的枚举声明
  • 定义则会给出存储位置和具体实现的细节。

  • 定义同时也可以当声明用。所以,除非某些场合非给出定义不可,我倾向于只使用声明。

  • 我把函数声明的形参型别和返回值型别这部分定义为函数的签名,而函数名字和形参名字则不属于签名的组成部分。

  • 函数声明除形参型别和返回值型别的其他组成元素(即可能存在的noexcept或constexpr)则被排除在外(noexcept和constexpr)

  • 签名的官方定义与我给出的稍有不同,但是在本书中,我们定义的更加使用(官方定义有时会省区返回值型别)

  • 标准有时会把某个操作的结果说成是未定义行为。意思是,其运行期行为不可预测,你当然会对这样的不确定性敬而远之。未定义行为的例子有,在方括号([])内使用越界值作为std::vector的下表,未初始化的迭代器实施提领操作,或者进入数据竞险(即两个或更多线程同时访问同一内存位置,且其中至少有一个执行写操作的情形)

  • 我将内建的指针,就是new表达式返回的那些指针,称为萝指针。而与裸指针形成对照的,则是智能指针。智能指针通常都重载了指针提领运算符(operator->和operator*)

第一张 型别推导

  • C++98仅有一套型别推导规则,用于函数模板。C++11对这套规则进行了一些改动,并且增加了两套规则,一套用于auto,另一套用于decltype。后来,C++14又扩展了能够运用auto和decltype的语境。型别推导应用返回的不断普及,使得人们不惜再去写下那些不言自明或是完全冗余的型别。
  • 想要使用现代C++高效编程,就离不开对型别推导操作的坚实理解。型别推导设计的语境实在不胜枚举: 在函数模板的调用中,在auto现身的大多数场景中,在decltype表达式中,特别是在C++14中那个神秘莫测的decltype(auto)结构中
  • 本章解释了模板型别推导如何运作,auto的型别推导如何构建在此运作规则之上,以及decltype独特的型别推导规则。

条款一:理解模板型别推导

  • 如果一个复杂系统的用户对于该系统的运作方式一无所知,然而却对其提供的服务表示满意,这就充分说明系统设计得好。
  • 模板的型别推导,是现代C++最广泛应用的特性之一–auto的基础。

简介

  • 单例模式相关学习笔记

单例模式

  • 单例模式,这种类型的设计模式属于创建型模式,它提供了一种创建对象的最佳方式。

  • 这种模式涉及到一个单一的类,该类负责创建自己的对象,同时确保只有单个对象被创建。这个类提供了一种访问其唯一的对象的方式,可以直接访问,不需要实例化该类的对象。

  • 单例模式是一种创建型设计模式,它确保一个类只有一个实例,并提供了一个全局访问点来访问该实例。

  • 注意:

    • 单例类只能有一个实例
    • 单例类必须自己创建自己的唯一实例
    • 单例类必须给所有其他对象提供这一实例
  • 单例模式是设计模式中最简单,最常见的一种。其主要目的是确保整个进程中,只有一个类的实例,并且提供一个统一的访问接口。常用于Logger类,通信接口类,线程池等。

基本原理

  • 限制用户直接访问类的构造函数,提供一个统一的public接口获取单例对象
  • 这里有一个先有鸡还是先有蛋的问题
    • 因为用户无法访问构造函数,所以无法创建对象
    • 因为无法创建对象,所以不能调用普通的getInstance()方法来获取单例对象
  • 解决这个问题的方法很简单,将 getInstance() 定义为static即可(这也会限制getInstance()内只能访问类的静态成员)

注意事项

  • 所有的构造函数是private
  • 拷贝构造,拷贝赋值运算符需要显示删除 =delete,防止编译器自动合成

C++单例模式的几种实现方式

版本一 饿汉式

1
2
3
4
5
6
7
8
9
10
11
12
class Singleton1 {
public:
static Singleton1* getInstance() { return &inst; }
Singleton1(const Singleton1&) = delete;
Singleton1& operator=(const Singleton1&) = delete;

private:
Singleton1() = default;
static Singleton1 inst;
};

Singleton1 Singleton1::inst;
  • 这个版本在程序启动时创建单例对象,即使没有使用也会创建,浪费资源。

版本二 懒汉式

  • 通过将单例对象的实例化会推迟到首次调用getInstance(),解决版本一的问题
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    class Singleton2 {
    public:
    static Singleton2* getInstance() {
    if (!pSingleton) {
    pSingleton = new Singleton2();
    }
    return pSingleton;
    }
    Singleton2(const Singleton2&) = delete;
    Singleton2& operator=(const Singleton2&) = delete;

    private:
    Singleton2() = default;
    static Singleton2* pSingleton;
    };

    Singleton2* Singleton2::pSingleton = nullptr;

版本三 线程安全

  • 在版本二中,如果多个线程同时调用getInstance()则有可能创建多个实例
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    class Singleton3 {
    public:
    static Singleton3* getInstance() {
    lock_guard<mutex> lck(mtx);
    if (!pSingleton) {
    pSingleton = new Singleton3();
    }
    return pSingleton;
    }
    Singleton3(const Singleton3&) = delete;
    Singleton3& operator=(const Singleton3&) = delete;

    private:
    Singleton3() = default;
    static Singleton3* pSingleton;
    static mutex mtx;
    };

    Singleton3* Singleton3::pSingleton = nullptr;
    mutex Singleton3::mtx;
  • 加锁可以解决线程安全的问题,但是版本三的问题在于效率太低,每次调用getInstance()都需要加锁,而加锁的开销又是相当高昂的

版本四 DCL(Double-Checked Locking)

  • 版本四是版本三的改进版本,只有在指针为空的时候才会进行加锁,然后再次判断指针是否为空。而一旦首次初始化完成之后,指针不为空,则不再进行加锁。既保证了线程安全,又不会导致后续每次调用都产生锁的开销
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    class Singleton4 {
    public:
    static Singleton4* getInstance() {
    if (!pSingleton) {
    lock_guard<mutex> lck(mtx);
    if (!pSingleton) {
    pSingleton = new Singleton4();
    }
    }
    return pSingleton;
    }
    Singleton4(const Singleton4&) = delete;
    Singleton4& operator=(const Singleton4&) = delete;

    private:
    Singleton4() = default;
    static Singleton4* pSingleton;
    static mutex mtx;
    };

    Singleton4* Singleton4::pSingleton = nullptr;
    mutex Singleton4::mtx;
  • DCL在很长一段时间内被认为是C++单例模式的最佳实践。但是也有文章表示DCL的正确性取决于内存模型。关于这部分的深入讨论可以参考以下两篇文章

版本五 Meyer’s Singleton

  • 这个版本利用局部静态变量来实现单例模式。最早由C++大佬,Effective C++系列的作者Scott Meyers提出,因此也被称为Meyers’ Singleton
  • TLDR: 这就是C++11之后的单例模式最佳实践,没有之一
    • 最简洁: 不需要额外定义类的静态成员
    • 线程安全:不需要额外加锁
    • 没有烦人的指针
1
2
3
4
5
6
7
8
9
10
11
12
13
class Singleton5 {
public:
static Singleton5& getInstance() {
static Singleton5 inst;
return inst;
}

Singleton5(const Singleton5&) = delete;
Singleton5& operator=(const Singleton5&) = delete;

private:
Singleton5() = default;
};