0%

第一章 致读者

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 和高性能特性来简化开发流程。

简介

  • 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;
};

简介

  • python3 bs4模块相关笔记

python3 bs4模块 详解

BeautifulSoup 是一个用于从HTML和XML文档中提取数据的Python库。它为用户提供了简单的API,使得解析、导航和搜索文档树变得更加直观和高效。以下是BeautifulSoup库的一些关键概念和使用方法的详细介绍。

1. 安装

首先,你需要安装 BeautifulSoup4 以及一个解析器库(例如 lxmlhtml.parser)。

1
pip install beautifulsoup4 lxml

2. 基本使用

导入模块并创建 BeautifulSoup 对象

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
from bs4 import BeautifulSoup

html_doc = """
<html><head><title>The Dormouse's story</title></head>
<body>
<p class="title"><b>The Dormouse's story</b></p>
<p class="story">Once upon a time there were three little sisters; and their names were
<a href="http://example.com/elsie" class="sister" id="link1">Elsie</a>,
<a href="http://example.com/lacie" class="sister" id="link2">Lacie</a> and
<a href="http://example.com/tillie" class="sister" id="link3">Tillie</a>;
and they lived at the bottom of a well.</p>
<p class="story">...</p>
</body>
</html>
"""

soup = BeautifulSoup(html_doc, 'lxml') # 使用 lxml 解析器

3. 基本导航

BeautifulSoup 提供了多种方式来导航和操作文档树。

3.1 标签选择

你可以使用标签名直接选择标签。

1
2
3
print(soup.title)  # <title>The Dormouse's story</title>
print(soup.title.name) # title
print(soup.title.string) # The Dormouse's story

3.2 获取标签的属性

标签的属性可以当作字典来访问。

1
2
3
print(soup.a)  # <a href="http://example.com/elsie" class="sister" id="link1">Elsie</a>
print(soup.a['href']) # http://example.com/elsie
print(soup.a['class']) # ['sister']

3.3 直接子节点和所有子节点

contents 属性可以返回直接子节点列表,children 可以用于遍历直接子节点,而 descendants 则会递归遍历所有子节点。

1
2
3
4
5
6
print(soup.body.contents)  # 直接子节点
for child in soup.body.children:
print(child)

for descendant in soup.body.descendants:
print(descendant)

4. 搜索文档树

BeautifulSoup 提供了几种查找文档树中特定元素的方法。

4.1 find_all()

查找所有符合条件的元素。

1
2
3
links = soup.find_all('a')
for link in links:
print(link['href'])

4.2 find()

查找第一个符合条件的元素。

1
2
first_link = soup.find('a')
print(first_link['href']) # http://example.com/elsie

4.3 使用 CSS 选择器

你可以使用 .select() 方法通过CSS选择器语法查找元素。

1
print(soup.select('p.title'))  # 选择类名为 title 的 <p> 标签

5. 修改文档树

BeautifulSoup 允许你直接修改文档内容。

5.1 修改标签内容

你可以直接修改标签的 .string 属性来更改其内容。

1
2
soup.title.string = "New Title"
print(soup.title) # <title>New Title</title>

5.2 插入和删除标签

你可以使用 append()insert()decompose() 等方法来添加或删除标签。

1
2
3
4
5
new_tag = soup.new_tag("p")
new_tag.string = "This is a new paragraph."
soup.body.append(new_tag) # 添加新标签到 body

soup.p.decompose() # 删除第一个 <p> 标签

6. 输出修饰后的HTML

修改完文档树后,你可以使用 prettify() 方法以缩进格式输出HTML。

1
print(soup.prettify())

7. 使用示例

一个简单的例子,展示如何提取所有链接,并打印它们的文本和链接地址:

1
2
for link in soup.find_all('a'):
print(f"Text: {link.string}, URL: {link['href']}")

8. 处理复杂的HTML

BeautifulSoup 可以非常有效地处理和解析有错误或不完整的HTML。它会自动修复文档树,使其更易于解析。


这就是 BeautifulSoup 库的基本使用方法和主要功能。通过这些方法,你可以轻松地从复杂的HTML文档中提取所需的信息。

简介

  • pandas模块相关笔记

python3 pandas模块 详解

Pandas 是 Python 数据分析中最常用的库之一,它提供了数据结构和数据分析工具,尤其擅长处理表格数据。以下是 Pandas 模块的一些详细介绍。

1. 安装 Pandas

在使用 Pandas 之前,你需要确保已经安装了它。可以使用以下命令进行安装:

1
pip install pandas

2. 导入 Pandas

导入 Pandas 通常使用 pd 作为别名:

1
import pandas as pd

3. 核心数据结构

3.1 Series

Series 是 Pandas 的基本数据结构之一,它是一维的,类似于 Python 中的列表或字典。

1
2
3
4
5
import pandas as pd

# 创建一个简单的 Series
s = pd.Series([1, 3, 5, 7, 9])
print(s)

3.2 DataFrame

DataFrame 是 Pandas 中最重要的数据结构,它是一个二维的表格数据结构,可以理解为一个“表格”或“电子表格”。

1
2
3
4
5
6
7
8
9
# 创建一个简单的 DataFrame
data = {
'Name': ['Alice', 'Bob', 'Charlie'],
'Age': [25, 30, 35],
'City': ['New York', 'Los Angeles', 'Chicago']
}

df = pd.DataFrame(data)
print(df)

4. DataFrame 基本操作

4.1 查看数据

  • 头部和尾部:使用 head()tail() 方法可以查看数据的前几行和最后几行。
1
2
print(df.head())  # 默认前5行
print(df.tail(2)) # 最后2行
  • 数据概览:使用 info()describe() 方法可以获取 DataFrame 的基本信息和统计摘要。
1
2
print(df.info())  # 数据结构信息
print(df.describe()) # 数值列的统计信息

4.2 选择数据

  • 选择列:可以通过列名选择特定列。
1
2
print(df['Name'])  # 返回 Name 列
print(df[['Name', 'Age']]) # 返回 Name 和 Age 列
  • 选择行:使用 lociloc 方法选择特定的行。
1
2
3
4
5
# 根据标签选择
print(df.loc[0]) # 返回第0行

# 根据索引选择
print(df.iloc[0]) # 返回第0行

4.3 条件筛选

Pandas 提供了强大的条件筛选功能。

1
2
3
# 筛选 Age 大于 30 的行
filtered_df = df[df['Age'] > 30]
print(filtered_df)

5. 数据清理

5.1 缺失值处理

  • 检测缺失值:使用 isnull()notnull() 可以检测缺失值。
1
print(df.isnull())  # 检查所有数据的缺失情况
  • 填充缺失值:使用 fillna() 方法可以填充缺失值。
1
df_filled = df.fillna(0)  # 将缺失值填充为 0
  • 删除缺失值:使用 dropna() 方法可以删除含有缺失值的行或列。
1
df_dropped = df.dropna()  # 删除含有缺失值的行

5.2 数据转换

  • 修改数据类型:使用 astype() 可以转换数据类型。
1
df['Age'] = df['Age'].astype(float)
  • 字符串操作:可以使用 .str 访问字符串方法。
1
df['Name'] = df['Name'].str.upper()  # 将 Name 列的值全部转换为大写

6. 数据分析

6.1 排序

使用 sort_values() 方法可以对 DataFrame 进行排序。

1
df_sorted = df.sort_values(by='Age', ascending=False)  # 按 Age 降序排列

6.2 分组操作

使用 groupby() 方法可以对数据进行分组,并对每组数据进行聚合操作。

1
2
grouped = df.groupby('City')['Age'].mean()  # 按 City 分组,并计算 Age 的平均值
print(grouped)

6.3 合并数据

Pandas 提供了 merge()concat()join() 等方法来合并数据。

1
2
3
4
5
6
# 合并两个 DataFrame
df1 = pd.DataFrame({'key': ['A', 'B', 'C'], 'value': [1, 2, 3]})
df2 = pd.DataFrame({'key': ['A', 'B', 'D'], 'value': [4, 5, 6]})

merged_df = pd.merge(df1, df2, on='key', how='inner') # 内连接
print(merged_df)

7. 数据输入输出

  • 读取 CSV 文件:使用 read_csv() 方法可以从 CSV 文件读取数据。
1
df = pd.read_csv('data.csv')
  • 写入 文件:使用 to_csv() 方法可以将 DataFrame 保存为 CSV 文件。
1
df.to_csv('output.csv', index=False)

8. 可视化

Pandas 可以与 Matplotlib、Seaborn 等库结合使用来生成图表。

1
2
3
4
import matplotlib.pyplot as plt

df['Age'].plot(kind='hist')
plt.show()

9. 高级功能

9.1 透视表

使用 pivot_table() 方法可以生成透视表。

1
2
pivot = df.pivot_table(values='Age', index='City', columns='Name', aggfunc='mean')
print(pivot)

9.2 时间序列分析

Pandas 提供了强大的时间序列处理功能。

1
2
3
4
5
6
7
8
# 将列转换为日期格式
df['Date'] = pd.to_datetime(df['Date'])

# 设置索引为日期
df.set_index('Date', inplace=True)

# 进行重采样
df_resampled = df.resample('M').mean() # 按月重采样

10. 实用技巧

  • 链式操作:Pandas 支持链式操作,可以将多个操作链式连接在一起,代码更简洁。
1
result = df.dropna().sort_values(by='Age').reset_index(drop=True)
  • 使用 .apply() 函数apply() 函数可以对 Series 或 DataFrame 应用自定义函数。
1
df['Age_plus_one'] = df['Age'].apply(lambda x: x + 1)

以上是 Pandas 模块的简要详解,Pandas 功能丰富,在实际应用中还可以深入学习更多高级用法。

简介

  • python3中selenium模块相关笔记

python3 selenium模块 详解

Selenium 是一个流行的 Web 测试和自动化工具,通常用于通过编程方式控制浏览器执行各种任务。Selenium 提供了多种语言绑定,其中之一是 Python。通过 selenium 模块,你可以用 Python 编写脚本来自动化浏览器的操作,如填表、点击按钮、抓取数据等。

下面是 Selenium 模块的详细介绍,包括安装、基本用法以及常用功能。

1. 安装 Selenium

要在 Python 中使用 Selenium,首先需要安装它:

1
pip install selenium

2. 基本用法

在使用 Selenium 之前,你还需要下载一个适用于你所用浏览器的 WebDriver。例如,如果你使用 Chrome 浏览器,你需要下载 ChromeDriver。

2.1 导入模块并设置 WebDriver

1
2
3
4
5
6
7
from selenium import webdriver

# 设置 Chrome WebDriver 的路径
driver = webdriver.Chrome(executable_path='/path/to/chromedriver')

# 打开一个网页
driver.get('https://www.example.com')

2.2 查找元素

Selenium 提供多种方法来查找页面中的元素:

  • find_element_by_id(id)
  • find_element_by_name(name)
  • find_element_by_xpath(xpath)
  • find_element_by_css_selector(css_selector)
  • find_element_by_tag_name(tag_name)
  • find_element_by_class_name(class_name)
1
2
3
4
5
6
# 查找元素
element = driver.find_element_by_id('element_id')

# 对元素进行操作
element.click() # 点击元素
element.send_keys('text') # 输入文本

2.3 与页面交互

  • 点击按钮:
1
2
button = driver.find_element_by_xpath('//button[@id="submit"]')
button.click()
  • 输入文本:
1
2
input_box = driver.find_element_by_name('q')
input_box.send_keys('Selenium Python')
  • 获取文本内容:
1
2
text = driver.find_element_by_tag_name('h1').text
print(text)

3. 常用功能

3.1 等待页面加载

有时页面元素需要时间加载,Selenium 提供了两种等待机制:

  • 隐式等待: 在设置的时间内等待元素加载完成。
1
driver.implicitly_wait(10)  # 等待最多10秒
  • 显式等待: 明确等待某个条件满足。
1
2
3
4
5
6
7
from selenium.webdriver.common.by import By
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC

element = WebDriverWait(driver, 10).until(
EC.presence_of_element_located((By.ID, 'myElement'))
)

3.2 处理弹窗

可以用 switch_to.alert 来处理 JavaScript 弹窗:

1
2
3
alert = driver.switch_to.alert
alert.accept() # 接受弹窗
alert.dismiss() # 关闭弹窗

3.3 处理多窗口

Selenium 允许在多个窗口或标签页之间切换:

1
2
3
4
5
6
7
8
# 获取当前窗口句柄
main_window = driver.current_window_handle

# 获取所有窗口句柄
windows = driver.window_handles

# 切换到新窗口
driver.switch_to.window(windows[1])

3.4 截图

可以用 save_screenshot 方法截图:

1
driver.save_screenshot('screenshot.png')

4. 关闭浏览器

完成操作后,应该关闭浏览器:

1
driver.quit()

5. 完整示例

以下是一个完整的示例,展示如何使用 Selenium 进行简单的搜索操作:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
from selenium import webdriver
from selenium.webdriver.common.keys import Keys

# 设置 WebDriver
driver = webdriver.Chrome(executable_path='/path/to/chromedriver')

# 打开 Google
driver.get('https://www.google.com')

# 查找搜索框并输入内容
search_box = driver.find_element_by_name('q')
search_box.send_keys('Selenium Python')
search_box.send_keys(Keys.RETURN)

# 等待搜索结果页面加载并获取结果
driver.implicitly_wait(10)
results = driver.find_elements_by_xpath('//h3')

for result in results:
print(result.text)

# 关闭浏览器
driver.quit()

6. 高级功能

Selenium 还支持处理复杂的场景,如文件上传、拖放、iframe 操作等。了解这些功能可以通过官方文档或其他高级教程。

7. 参考文档

你可以在 Selenium 官方文档 中找到更多详细的内容和使用示例。

python3 selenium.WebDriver类 详解

WebDriver 类是 Selenium 中的核心类之一,用于控制和与浏览器交互。通过 WebDriver 类,您可以启动和操作各种浏览器(如 Chrome、Firefox、Safari 等),执行诸如打开网页、查找元素、模拟用户输入、点击、截屏等操作。

以下是 WebDriver 类的详细解释:

1. WebDriver 类简介

WebDriver 是一个抽象类,用于定义所有浏览器驱动程序(如 ChromeDriverFirefoxDriver 等)必须实现的接口。通过 WebDriver 类,可以执行一系列浏览器操作,如导航、窗口管理、页面交互等。

2. 浏览器驱动初始化

使用 WebDriver 类时,通常需要实例化一个特定浏览器的驱动程序。以下是几个常见的浏览器驱动初始化方式:

1
2
3
4
5
6
7
8
9
10
11
12
13
from selenium import webdriver

# 初始化 Chrome 浏览器驱动
driver = webdriver.Chrome()

# 初始化 Firefox 浏览器驱动
driver = webdriver.Firefox()

# 初始化 Safari 浏览器驱动
driver = webdriver.Safari()

# 初始化 Edge 浏览器驱动
driver = webdriver.Edge()

3. 常用方法与属性

WebDriver 类提供了许多方法和属性,用于控制和操作浏览器。以下是一些常用的方法和属性:

3.1 页面导航

  • get(url): 导航到指定的 URL。

    1
    driver.get("https://www.example.com")
  • current_url: 返回当前页面的 URL。

    1
    current_page_url = driver.current_url
  • title: 返回当前页面的标题。

    1
    page_title = driver.title
  • back(): 模拟浏览器的后退按钮。

    1
    driver.back()
  • forward(): 模拟浏览器的前进按钮。

    1
    driver.forward()
  • refresh(): 刷新当前页面。

    1
    driver.refresh()

3.2 元素查找

  • find_element_by_id(id): 根据元素的 id 查找元素。

    1
    element = driver.find_element_by_id("element-id")
  • find_element_by_name(name): 根据元素的 name 属性查找元素。

    1
    element = driver.find_element_by_name("element-name")
  • find_element_by_xpath(xpath): 根据 XPath 表达式查找元素。

    1
    element = driver.find_element_by_xpath("//div[@class='example']")
  • find_element_by_css_selector(selector): 根据 CSS 选择器查找元素。

    1
    element = driver.find_element_by_css_selector(".example-class")
  • find_elements_by_*: 对应于 find_element_by_* 的方法,用于查找多个符合条件的元素,返回一个元素列表。

3.3 浏览器窗口管理

  • maximize_window(): 最大化浏览器窗口。

    1
    driver.maximize_window()
  • minimize_window(): 最小化浏览器窗口。

    1
    driver.minimize_window()
  • set_window_size(width, height): 设置浏览器窗口大小。

    1
    driver.set_window_size(1024, 768)
  • get_window_size(): 获取当前窗口大小。

    1
    size = driver.get_window_size()
  • switch_to.window(window_name): 切换到指定的窗口或标签页。

    1
    driver.switch_to.window(driver.window_handles[1])

3.4 框架和窗口切换

  • switch_to.frame(frame_reference): 切换到指定的 iframe 或 frame。

    1
    driver.switch_to.frame("frame-name")
  • switch_to.default_content(): 退出 iframe 或 frame,返回到主页面内容。

    1
    driver.switch_to.default_content()

3.5 警告和弹出框处理

  • switch_to.alert: 切换到浏览器警告对话框(alert)。

    1
    2
    3
    alert = driver.switch_to.alert
    alert.accept() # 确认警告
    alert.dismiss() # 取消警告

3.6 执行 JavaScript

  • execute_script(script, *args): 在页面上执行 JavaScript 脚本。

    1
    driver.execute_script("alert('Hello, world!')")

3.7 截屏

  • save_screenshot(filename): 截取当前窗口并保存为文件。

    1
    driver.save_screenshot("screenshot.png")

4. 关闭与退出

  • close(): 关闭当前窗口。如果这是唯一的窗口,则会退出 WebDriver 会话。

    1
    driver.close()
  • quit(): 关闭所有关联的窗口,并退出 WebDriver 会话。

    1
    driver.quit()

5. 等待

在 WebDriver 中,等待是一个非常重要的概念,尤其是在处理动态加载的网页内容时。Selenium 提供了两种等待方式:

5.1 隐式等待

设置一个全局等待时间,WebDriver 会在查找元素时,轮询等待元素在指定时间内加载完成。

1
driver.implicitly_wait(10)  # 等待最多10秒

5.2 显式等待

显式等待指定某个条件,在指定时间内等待该条件满足。如果条件在超时时间内满足,继续执行;否则抛出异常。

1
2
3
4
5
6
7
from selenium.webdriver.common.by import By
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC

element = WebDriverWait(driver, 10).until(
EC.presence_of_element_located((By.ID, "element-id"))
)

6. 异常处理

使用 WebDriver 时,可能会遇到各种异常,如元素未找到、超时、无效的 URL 等。Selenium 提供了一些常见的异常类:

  • NoSuchElementException: 元素未找到。
  • TimeoutException: 操作超时。
  • WebDriverException: 一般性的 WebDriver 错误。

可以通过异常处理机制来捕获和处理这些错误。

1
2
3
4
5
6
from selenium.common.exceptions import NoSuchElementException

try:
driver.find_element_by_id("non-existent-id")
except NoSuchElementException:
print("Element not found!")

7. 示例代码

以下是一个简单的示例,展示了如何使用 WebDriver 类打开网页、查找元素并与之交互。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
from selenium import webdriver

# 初始化 Chrome 浏览器
driver = webdriver.Chrome()

# 打开网页
driver.get("https://www.example.com")

# 查找元素并与之交互
search_box = driver.find_element_by_name("q")
search_box.send_keys("Selenium WebDriver")
search_box.submit()

# 等待页面加载完成
driver.implicitly_wait(5)

# 获取页面标题
print(driver.title)

# 关闭浏览器
driver.quit()

总结

WebDriver 类是 Selenium 的核心,提供了丰富的功能来自动化浏览器操作。掌握 WebDriver 的各种方法和属性,是进行 Web 自动化测试的基础。

python3 WebDriver.get()函数 详解

WebDriver.get() 是 Selenium WebDriver 的一个函数,用于让 WebDriver 导航到指定的 URL(即加载网页)。该函数是自动化浏览器操作的基础之一。以下是 WebDriver.get() 函数的详细说明:

1. 函数定义

1
webdriver.get(url)
  • webdriver 是你初始化的浏览器对象(例如 Chrome、Firefox 等)。
  • url 是你希望 WebDriver 加载的网页的 URL,它必须是一个字符串。

2. 参数说明

  • url: 这是一个字符串,表示你想让 WebDriver 打开的网页地址。该 URL 必须以 http://https:// 开头。如果没有指定协议,WebDriver 将不会加载页面,并且可能抛出异常。

3. 返回值

  • 该方法没有返回值。WebDriver 将会打开指定的 URL,并在页面完全加载后继续执行后续代码。

4. 常见用途

  • 打开网页: 这是 WebDriver.get() 最常见的用途,用于在浏览器中导航到特定的网页。

    1
    2
    3
    4
    5
    6
    7
    from selenium import webdriver

    # 初始化 WebDriver
    driver = webdriver.Chrome()

    # 打开网页
    driver.get("https://www.example.com")
  • 与其他 WebDriver 操作结合: 在加载页面后,通常会执行其他操作,如查找元素、执行 JavaScript 等。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    from selenium import webdriver

    driver = webdriver.Chrome()
    driver.get("https://www.example.com")

    # 查找页面中的元素
    element = driver.find_element_by_id("example-id")

    # 执行其他操作
    element.click()

5. 注意事项

  • 等待页面加载: WebDriver.get() 会等待页面的完全加载,即等待所有同步加载的资源(HTML、CSS、JavaScript 等)都加载完成后,才会继续执行下一行代码。但是,某些动态内容(例如通过 AJAX 加载的数据)可能需要手动等待。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    from selenium.webdriver.support.ui import WebDriverWait
    from selenium.webdriver.support import expected_conditions as EC

    driver.get("https://www.example.com")

    # 等待某个元素加载完成
    WebDriverWait(driver, 10).until(
    EC.presence_of_element_located(("id", "example-id"))
    )
  • 异常处理: 如果给定的 URL 格式不正确,或由于其他原因无法加载页面,WebDriver 可能会抛出异常(如 WebDriverException)。在实际应用中,可以通过异常处理机制捕获并处理这些错误。

    1
    2
    3
    4
    try:
    driver.get("invalid-url")
    except Exception as e:
    print(f"An error occurred: {e}")

6. 关闭 WebDriver

在使用完 WebDriver 后,应该调用 quit() 方法来关闭浏览器并释放资源。

1
driver.quit()

总结

WebDriver.get() 是 Selenium WebDriver 中非常基本且重要的函数,用于导航到指定网页。理解和正确使用这个方法,是自动化浏览器操作的基础。

python3 WebDriver.delete_all_cookies()函数 详解

WebDriver.delete_all_cookies() 是 Selenium WebDriver 中的一个函数,用于删除当前会话中的所有 cookies。下面是对该函数的详细解释:

功能

delete_all_cookies() 方法的主要功能是在自动化测试过程中清除浏览器中所有的 cookies。Cookies 是存储在浏览器中的小数据文件,通常用于保持用户登录状态、存储用户偏好等。在某些测试场景中,可能需要删除这些 cookies 以确保测试的独立性和一致性。

用法

1
2
3
4
5
6
7
8
9
10
11
12
from selenium import webdriver

# 创建 WebDriver 实例
driver = webdriver.Chrome()

# 打开某个网页
driver.get("https://www.example.com")

# 删除所有 cookies
driver.delete_all_cookies()

# 继续执行其他操作

应用场景

  • 清除会话数据: 当你希望从一个干净的状态开始测试时,可以使用 delete_all_cookies() 来清除所有的 session 信息。
  • 模拟不同用户登录: 在测试不同用户登录时,删除 cookies 可以防止前一个用户的会话信息干扰到下一个用户。
  • 重置浏览器状态: 在多步骤的测试中,删除 cookies 可以帮助重置浏览器状态,以确保每个步骤都从同一初始状态开始。

注意事项

  • 这个方法会删除当前会话的所有 cookies,并且在调用 delete_all_cookies() 之后,如果页面依赖 cookies 工作,可能需要重新加载页面或者重新设置 cookies。
  • 删除 cookies 不会影响浏览器的历史记录或缓存,只会影响 cookies。

常见问题

  • 删除后无效: 如果删除 cookies 后仍然看到之前的数据,可能是因为浏览器的缓存问题,建议同时清除缓存。
  • 权限问题: 某些 cookies 可能由于安全设置而无法删除,这通常涉及到跨域或安全标志的 cookies。

通过使用 delete_all_cookies(),你可以确保在不同的测试场景中拥有一个干净的环境,从而提高测试的准确性和可靠性。

python3 WebDriver.find_elements()函数 详解

WebDriver.find_elements() 是 Selenium WebDriver 中用于查找网页元素的一个函数。它返回符合特定定位策略的所有元素列表。如果没有找到任何元素,则返回空列表。

函数签名

1
elements = driver.find_elements(by=By.XPATH, value="//tag[@attribute='value']")

参数

  • by: 用于指定查找元素的方式,通常使用 By 类提供的常量,例如 By.ID, By.NAME, By.XPATH 等。
  • value: 一个字符串,指定如何根据 by 参数查找元素的值。例如,如果 by=By.ID,那么 value 就是对应的元素 id 值。

常用的 By 定位方式

  • By.ID: 通过元素的 id 属性定位元素。
  • By.NAME: 通过元素的 name 属性定位元素。
  • By.CLASS_NAME: 通过元素的 class 属性定位元素。
  • By.TAG_NAME: 通过元素的标签名定位元素。
  • By.LINK_TEXT: 通过元素的文本内容(超链接)定位元素。
  • By.PARTIAL_LINK_TEXT: 通过部分文本内容定位超链接。
  • By.XPATH: 通过 XPath 表达式定位元素。
  • By.CSS_SELECTOR: 通过 CSS 选择器定位元素。

返回值

  • List[WebElement]: 由所有匹配元素组成的列表。如果没有找到元素,返回空列表。

示例代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
from selenium import webdriver
from selenium.webdriver.common.by import By

# 初始化 WebDriver(比如使用 Chrome 浏览器)
driver = webdriver.Chrome()

# 打开一个网址
driver.get("https://www.example.com")

# 使用 find_elements() 查找元素列表
elements = driver.find_elements(By.CLASS_NAME, "example-class")

# 遍历并操作元素
for element in elements:
print(element.text)

# 关闭 WebDriver
driver.quit()

注意事项

  • find_element() 不同,find_elements() 不会抛出 NoSuchElementException 异常。如果没有找到元素,返回的是一个空列表。
  • find_elements() 查找所有符合条件的元素,而 find_element() 仅返回第一个匹配的元素。

通过 find_elements(),可以轻松地处理网页中的多个相似元素,比如同一类的按钮、表单元素等。

简介

  • cmake官方文档: MasteringCMake 阅读笔记

目录结构

  • CMake在构建项目时有两个主要的目录: 源代码目录和二进制目录
    • 源代码目录是存放项目源代码的位置
    • 二进制目录,有时候也成为build目录,是CMake存放最终对象文件,库和可执行文件的位置。
  • CMake不会向源代码目录写任何文件,只会向二进制目录写文件。

基础用法

  • CMake用各种各样的本地开发工具,将一个或多个CMakeLists 文件作为输入,并生成项目文件或者Makefile使用
  • 下面是典型的CMake处理流程
    • 项目定义一个或多个CMakeLists文件
    • CMake配置并生成项目文件
    • 用户按需使用本地开发工具构建项目

CMakeLists文件

  • CMakeLists文件是包含使用CMake语言描述项目的文本文件。
  • CMake语言表示为一系列的注释,命令和变量。
  • CMake为什么要有自己的语言?
    • 因为CMake如果依赖于其他语言,例如Python,就需要在使用CMake时安装其他语言。
    • 有CMake语言能够更高效,更方便。

CMake中的Hello World

  • 示例

    1
    2
    3
    cmake_minimum_required(VERSION 3.20)
    project(Hello)
    add_executable(Hello Hello.c)
  • CMakeLists文件第一行总会是 cmake_minimum_requried.这使得CMake可以使用指定的版本。

  • 第二行应该是 project 命令。这个命令设置项目的名字,也可以指定其他的参数,例如语言,版本。

  • 最后使用 add_executable 命令用这些给定的源代码文件生成项目的可执行对象。

为CMake指定编译器

  • 环境变量 CC 用来指定 C编译器

  • 环境变量 CXX 用来指定 C++编译器

  • 可以在命令行中通过使用 -DCMAKE_CXX_COMPILER=cl 来指定编译器

  • 设置 LDFLAGS 用来初始化 链接参数

  • 设置 CXXFLAGS 用来初始化 CMAKE_CXX_FLAGS

  • 设置 CFLAGS 用来初始化 CMAKE_C_FLAGS

构建配置

  • 构建配置允许以不同的方式构建项目。CMake默认支持的方式有: Debug, Release, MinSizeRel, RelWithDebInfo
    • Debug: 打开了基本的调试符号
    • Release: 打开了基本的优化
    • MinSizeRel: 产生最小的,但不一定是最快的目标文件
    • RelWithDebInfo: 既有调试信息,也开启了优化的目标文件

简介

输出文件名和行号

  • 使用宏,例如SPDLOG_INFO

使用宏函数输出的日志也写入到文件

  • 使用函数 spdlog::set_default_logger();
  • 示例如下
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    static void InitLogger()
    {
    //创建控制台日志记录器
    auto console_sink = std::make_shared<spdlog::sinks::stdout_color_sink_mt>();
    console_sink->set_level(spdlog::level::debug);

    console_sink->set_pattern("[%Y-%m-%d %H:%M:%S.%e][thread %t][%s:%#][%l]: %v");

    // 创建文件日志记录器: 滚动记录,最大文件5M,文件数量100个
    std::string log_path = ROOT_PATH;
    log_path += "logs/rotating.txt";
    auto rotating_sink = std::make_shared<spdlog::sinks::rotating_file_sink_mt>(log_path, 1048576 * 5, 100);

    //同步记录器,
    std::vector<spdlog::sink_ptr> sinks{ console_sink, rotating_sink };
    auto logger = std::make_shared<spdlog::logger>("logger", sinks.begin(), sinks.end());
    spdlog::register_logger(logger); //注册为全局日志,通过log_write访问;

    // 宏相关配置
    spdlog::set_default_logger(logger);
    spdlog::set_pattern("[%Y-%m-%d %H:%M:%S.%e][thread %t][%s:%#][%l]: %v");
    spdlog::set_level(spdlog::level::warn);
    spdlog::flush_every(std::chrono::seconds(3)); //每3s刷新一次
    }

核心概念

  • logger: 日志对象,每个日志内包含一个sink组成的vector,每个sink可以分别设置优先级,logger本身也可设置优先级。

  • sink: 直译是水槽,实际上是引流的对象或者可以认为是输出目标,spdlog库内置了多种不同类型的logger可供选择

  • formatter: 格式化对象,绝大部分情况下spdlog默认的格式就足够用了,但是如果有个性化需求,可以进行自定义格式

  • level: 日志级别,不同的日志库可能会有不同的设置,但是基本情况下都会有debug,info,warn,error等级别划分来处理不同的情况,具体各个级别的情况可以根据自己的实际情况选取

  • 逻辑关系

    • 每个logger包含一个vector,该vector由一个或者多个 std::shared_ptr 组成,logger的每条日志都会调用sink对象,由sink对象按照formatter的格式输出到指定的地方(有可能是控制台,文件等)

formatter

  • formatter也即是格式化对象,用于控制日志的输出格式,spdlog自带了默认的formatter,一般情况下,我们无需任何修改,直接使用即可。需要注意的是,每个sink会有一个formatter
  • 默认formatter的格式为: [日期时间][logger名][log级别]log内容
    1
    2
    3
    4
    5
    [2022-10-13 17:00:55.795] [service] [debug] found env XXXXXXX : true
    [2022-10-13 17:00:55.795] [func_config] [debug] kafka_brokers : localhost:9092
    [2022-10-13 17:00:55.795] [func_config] [debug] kafka_main_topic : kafka_test
    [2022-10-13 17:00:55.795] [func_config] [debug] kafka_partition_value : -1
    [2022-10-13 17:00:55.795] [service] [info] initialized

sink

  • 每个sink对应着一个输出目标和输出格式,它内部包含一个formatter,输出目标可以是控制台,文件等地方。

  • 所有的sink都在命名空间spdlog::sinks下,可以自行探索

  • spdlog中创建控制台sink非常简单,该方式创建的sink会输出到命令行终端,且是彩色的。后缀的 _mt 代表多线程, _st 代表单线程

    1
    auto sink1 = std::make_shared<spdlog::sinks::stdout_color_sink_mt>();
  • 文件sink的类型有很多,这里展示几种经典类型

    1
    2
    3
    4
    5
    auto sink1 = std::make_shared<spdlog::sinks::basic_file_sink_mt>(log_file_name);//最简单的文件sink,只需要指定文件名

    auto sink2 = std::make_shared<spdlog::sinks::daily_file_sink_mt>(log_file_name, path, 14, 22);//每天的14点22分在path下创建新的文件

    auto sink3 = std::make_shared<spdlog::sinks::rotating_file_sink_mt>(log_file_name, 1024 * 1024 * 10, 100, false);//轮转文件,一个文件满了会写到下一个文件,第二个参数是单文件大小上限,第三个参数是文件数量最大值
  • 其他sink

    • ostream_sink
    • syslog_sink
  • sink的flush问题

    • 创建好sink后建议设置flush方式,否则可能无法立刻在文件中看到logger的内容
    • 以下为两种重要的flush方式设置(直接设置全局)
      1
      2
      spdlog::flush_every(std::chrono::seconds(1));
      spdlog::flush_on(spdlog::level::debug);

logger

  • 日志对象,每个logger内容包含了一个vector用于存放sink,每个sink都是互相独立

  • 因此一个日志对象在输出日志时可以同时输出到控制台和文件等位置

  • 如果整个项目中只需要一个logger,spdlog提供了最为便捷的logger,注意,该logger在全局公用,输出到控制台,多线程,彩色。

    1
    2
    //Use the default logger (stdout, multi-threaded, colored)
    spdlog::info("Hello, {}!", "World");
  • 创建特定的logger

    • 大部分情况下默认logger是不够用的,因为我们可能需要做不同模块各自的logger,可能需要logger输出到文件进行持久化,所以创建logger是很很重要的一件事。
  • 直接创建

    • 与创建sink类似,我们可以非常便捷的创建logger。
    • 由于大部分时候一个logger只会有一个sink,所以spdlog提供了创建logger的接口并封装了创建sink的过程
      1
      2
      auto console = spdlog::stdout_color_mt("some_unique_name");//一个输出到控制台的彩色多线程logger,可以指定名字
      auto file_logger = spdlog::rotating_logger_mt("file_logger", "logs/mylogfile", 1048576 * 5, 3);//一个输出到指定文件的轮转文件logger,后面的参数指定了文件的信息
  • 组合sinks方式创建

    • 有时候,单sink的logger不够用,那么可以先创建sink的vector,然后使用sinks_vector创建vector
    • 以下示例中,首先创建了sink的vector,然后创建了两个sink并放入vector,最后使用该vector创建了logger,其中 set_level 的过程不是必须的, register_logger 一般是必须的,否则只能在创建logger的地方使用该logger
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      std::vector<spdlog::sink_ptr> sinks;

      auto sink1 = std::make_shared<spdlog::sinks::stdout_color_sink_mt>();
      sink1->set_level(MyLoggers::getGlobalLevel());
      sinks.push_back(sink1);

      auto sink2 = std::make_shared<spdlog::sinks::rotating_file_sink_mt>(log_file_name, 1024 * 1024 * 10, 100, false);
      sink2->set_level(spdlog::level::debug);
      sinks.push_back(sink2);

      auto logger = std::make_shared<spdlog::logger>("logger_name", begin(sinks), end(sinks));
      logger->set_level(spdlog::level::debug);
      spdlog::register_logger(logger);
  • logger的注册与获取

    • 在一个地方创建了logger却智能在该处使用肯定是不好的,所以spdlog提供了全局注册和获取logger,我们只需要在某处先创建logger并注册,那么后面再其他地方使用时直接获取就可以了
    • 注册: spdlog::register_logger()
    • 获取: spdlog::get()
      1
      2
      //上面的代码中我们注册了一个logger,名字是logger_name,接下来尝试获取
      auto logger = MyLoggers::getLogger("logger_name");

logger的使用

  • logger的默认level是info,如果处于开发换进给或者生产环境,绘制需要debug级别以上,可以设置logger的级别

    1
    logger->set_level(spdlog::level::debug);
  • 可以设置全局logger级别

    1
    spdlog::set_level(spdlog::level::warn);
  • 可以设置sink级别的logger

    1
    sink1->set_level(spdlog::level::info);
  • 注意: 一个logger假如有多个sink,那么这些sink分别设置level是可以不同的,但是由于logger本身也有level,所以真正使用时,logger的level如果高于某个sink,会覆盖该sink的level,所以建议此时把logger的level手动设置为debug(默认为info)

1.1 概述

  • 使用方法一:

    • 下载好项目源代码,使用cmake编译,命令:cmake -S . -B build,然后进入build进行make,会获取到一个静态库 libspdlog.a
    • 之后通过包含include下的头文件和链接生成的静态库,使用spdlog
  • 使用方法二:

    • 将头文件和源文件分别添加到相应工程中的头文件和源文件
    • 然后再CMakeLists.txt中添加头文件目录和源文件目录,并且添加一条命令用来编译指定文件:target_compile_definitions(${PROJECT_NAME} PUBLIC SPDLOG_COMPILED_LIB)

1.2 基本用法

  • 输出日志信息到标准输出

    • spdlog::info("This is a log message.{0} {1}", "hello", "world")
    • spdlog::warn("This is a warn message");
    • spdlog::debug("This message should not be displayed");
    • spdlog::set_level(spdlog::level::trace); // set specifice logger's log level
    • spdlog::debug("This message should be displayed, because the level is setted trace");
  • 日志信息输出到文件

    • 创建基础日志文件:
      • #include "spdlog/sinks/basic_file_sink.h"
      • auto my_logger = spdlog::basic_logger_mt("file_logger", "logs/basic-log.txt", true);
      • 创建一个文件为logs/basic-log.txt,并设置输出的消息标识为file_logger的日志文件,通过使用my_logger来操作该日志文件
    • 将日志输出到日志文件中:
      • my_logger->info("Helo Info");
  • 输出行号:

    • SPDLOG_DEBUG("some debug message about program: {}", "robot");

spdlog spdlog::get()函数 详解

spdlog 是一个快速的 C++ 日志库,广泛用于高效日志记录。spdlog::get() 函数用于获取已经创建的日志记录器(logger)的共享指针。如果指定名称的日志记录器不存在,它会返回 nullptr

1. 函数定义

1
std::shared_ptr<spdlog::logger> spdlog::get(const std::string &name);

2. 参数说明

  • name: 一个 std::string 类型的参数,表示需要获取的日志记录器的名称。

3. 返回值

  • std::shared_ptr<spdlog::logger>: 返回一个指向指定名称的日志记录器的共享指针。如果指定名称的日志记录器未找到,则返回 nullptr

4. 使用场景

spdlog::get() 通常用于在应用程序的不同模块中共享同一个日志记录器。比如,你在主模块中创建了一个名为 “my_logger” 的日志记录器,之后可以在其他模块中通过 spdlog::get("my_logger") 获取同一个记录器,而无需重新创建。

5. 示例代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
#include <spdlog/spdlog.h>
#include <spdlog/sinks/basic_file_sink.h>

int main() {
// 创建一个名为 "file_logger" 的日志记录器
auto logger = spdlog::basic_logger_mt("file_logger", "logs.txt");

// 使用记录器进行日志记录
logger->info("This is an info message");

// 在其他地方获取并使用相同的记录器
auto same_logger = spdlog::get("file_logger");
if (same_logger) {
same_logger->warn("This is a warning message from the same logger");
} else {
spdlog::error("Logger not found");
}

return 0;
}

6. 注意事项

  • spdlog::get() 只返回已经通过 spdlog::register_logger() 或者 spdlog::basic_logger_mt() 等方法注册过的日志记录器。如果未注册过,返回的会是 nullptr
  • spdlog::get() 函数是线程安全的,因此可以在多线程环境中安全使用。

通过 spdlog::get(),你可以在不同模块中灵活地获取和使用日志记录器,确保日志管理的一致性。

简介

  • VSCode下C++程序的调试的配置文件是launch.json。这篇文章是关于launch.json文件的使用笔记,包括常见的参数,常用技巧和常见错误及其解决方法。

windows 程序调试阻塞,出现 unable to start Microsoft Visual Debug Console.

  • 这个是因为程序输出需要在命令窗口中,在launch.json中console参数是设置命令窗口的启动属性。如果设置了 externalTerminal 则会出现问题。

  • 解决方法

    • 将console的值设置为 integratedTerminal

console 参数 详解

  • console: Where to launch the debug target. Defaults to ‘internalConsole’ if not defined.

  • console: 需要调试的程序运行的地方。如果没有定义,默认为 internalConsole

  • console的值有:

    • externalTerminal:
      • Console applications will be launched in an external terminal window. The window will be reused in relaunch scenarios and will not automatically disappear when the application exits.
      • 控制台应用程序将在外部控制台窗口中启动。这个窗口将在程序再次启动时被重复使用,并且不会随着程序退出自动退出。
    • integratedTerminal
      • VS Code’s integrated terminal.
      • VSCode的集成终端
    • internalConsole
      • Output to the VS Code Debug Console. This doesn’t support reading console input (ex:’std::cin’ or ‘scanf’).
      • 输出到VSCode调试窗口。这个不支持从窗口读取数据,例如 std::cin或 scanf
    • newExternalWindow
      • Console applications will be launched in their own external console window which will end when the application stops. Non-console applications will run without a terminal, and stdout/stderr will be ignored.
      • 控制台应用程序将在其自己的外部控制台窗口中启动,该窗口将在应用程序停止时结束。非控制台应用程序在没有终端且标准输出和标准错误输出被忽略的情况下运行。

简介

  • 本书主要介绍如何使用Qt进行C++应用程序开发。
  • Qt实际上是一套应用程序开发类库,Qt类库由许多模块组成,例如核心的GUI组件模块Qt Widget,用于数据库访问的Qt SQL模块,用于二维图表显示的Qt Charts模块,用于数据三位显示的Qt Data Visualization模块,用于网络编程的Qt Network模块等。

第一章 认识Qt

1.1 Qt简介

  • C++是一种通用的标准编程语言,使用任何编辑器都可以编写C++源程序,然后利用C++编译器对程序进行编译,就可以生成可执行的程序。
  • 为了方便进行C++程序的编写和编译,有各种综合开发环境(Integrated Developing Environment, IDE),例如Visual Studio就是Windows平台上常见的编写C++程序的IDE。一个IDE不仅提供程序的编辑和编译,一般还提供一套基本类库,用于提供支持平台应用程序开发的各种基本类,例如Visual Studio使用MFC进行Windows平台的应用程序开发。
  • Qt是一套应用程序开发类库,但是与MFC不同,Qt是跨平台的开发类库。Qt支持PC和服务器的平台,包括Windows,Linux,macOS等,还支持移动和嵌入式操作系统,例如IOS,Embedded Linux,Andriod,WinRT等。跨平台意味着只需要编写一次程序,在不同平台上无需改动或只需要少许改动后在编译,就可以形成不同平台上运行的版本。

1.2 Qt的获取和安装

1.2.1 Qt的许可类型

  • Qt的许可类型分为商业许可和开源许可,开源许可又分为LGPLV3和GPLV2/GPLV3.

1.2.2 Qt的版本

  • Qt的版本更新比较快,且版本更新时会新增一些类或者停止维护一些以前版本的类。如果不是为了维护用旧版本编写的程序,一定要选用最新版本的Qt进行程序开发。

1.2.3 Qt的下载和安装

  • 从Qt官网可以下载最新版本的Qt软件。根据开发项目的不同,Qt分为桌面和移动设备应用开发,嵌入式设备开发两大类不同的安装包。

    • 桌面和移动设备应用开发就是开发在PC,服务器,手机,平板电脑等设备上运行的程序,操作系统平台可以是Windows,Linux,macOS,Andriod等。
    • 嵌入式设备开发是针对具体的嵌入式设备来开发应用程序,例如物联网设备,汽车电子设备,医疗设备等特定的嵌入式设备。
  • Qt的安装包分为在线安装包和离线安装包,为便于重复安装,最好下载离线安装包。

  • 在安装过程中会出现安装选项设置页面,在这个页面里选择需要安装的模块。这些模块包括内容如下

    • MinGW 编译器模块。MinGW是Minimalist GNU For Windows的缩写,MinGW是Windows平台上使用的GNU工具集导入库的集合。
    • 用于UWP编译的模块。UWP是Windows 10中Universal Windows Platform的简称,有不同编译器类型的UWP
    • 用于Windows平台上的MSVC编译器模块。要安装MSVC编译器的模块,需要计算机上已经安装相应版本的Visual Studio
    • 用于Andriod平台的模块,例如 Andriod x86和Android ARMv7
    • Sources是Qt的源程序类
    • Qt Charts是二维图标模块,用于绘制柱状图,饼图,曲线图等常用二维图表
    • Qt Data Visualization是三维数据图表模块,用于数据的三维显示,例如散点的三维空间分布,三维曲面等
    • Qt Purchsing,Qt WebEngine, Qt Network Auth(TP)等其他模块,括号里的TP表示技术预览(Technology Preview)
    • Qt Scritp(Deprecated)是脚本模块,括号里的”Deprecated”表示这是个已经过时的模块
  • “Tools”节点下面是一些工具软件,包括内容如下

    • Qt Creator 是用于Qt程序开发的IDE
    • MinGW 是MinGW编译工具链
    • StrawBerry Perl是一个Perl语言工具
  • 工具软件有

    • Assistant 是一个独立的查看Qt帮助文件的程序,集成在了Qt Creator中
    • Designer 是一个独立的进行窗口,对话框等界面可视化设计的程序。Designer也集成在了Qt Creator中,在Qt Creator中编辑或创建界面文件时,就可以自动打开并进行界面设计。
    • Linguist是一个编辑语言资源文件的程序,在开发多语言界面的应用程序时会用到。
  • 这三个工具软件可独立使用,前两个集成到了Qt Creator里,可在Qt Creator打开。所以Qt的主要工具是Qt Creator。

1.3 Qt Creator初步使用

1.4 编写一个Hello World程序

1.4.1 新建一个项目

  • Qt Creator可以创建多种项目,各类应用程序如下

    • Qt Widgets Application,支持桌面平台的有图形用户界面(Graphic User Interface,GUI)界面的应用程序。GUI的设计完全基于C++语言,采用Qt提供的一套C++类库。
    • Qt Console Application,控制台应用程序,无GUI界面,一般用于学习C/C++语言。
    • Qt Quick Application, 创建可部署的Qt Quick 2应用程序。Qt Quick是Qt支持的一套GUI开发架构,其界面设计采用QML语言,程序架构采用C++语言。利用Qt Quick可以设计非常炫的用户界面,一般用于移动设备或嵌入式设备上无边框的应用程序的设计。
    • Qt Quick Controls 2 Application,创建基于Qt Quick Controls 2组件的可部署的Qt Quick 2应用程序。
    • Qt Canvas 3D Application,创建Qt Canvas 3D QML项目,也是基于QML语言的界面设计,支持3D画布。
  • 界面的基类(base class),有3中基类可以选择

    • QMainWindow 是主窗口类,主窗口具有主菜单栏,工具栏和状态栏,类似于一般的应用程序的主窗口;
    • QWidget 是所有具有可视界面类的基类,选择QWidget创建的界面对各种界面组件都可以支持
    • QDialog 是对话框类,可建立一个基于对话框的界面

第二章 GUI应用程序设计基础

  • 本章深入的介绍Qt Creator设计GUI应用程序的基本方法,包括Qt创建的应用程序项目的 基本组织结构,可视化设计的UI界面文件的原理和运行机制,信号与槽的使用方法,窗体可视化设计的底层原理,应用程序的窗体,组件布局,菜单,工具栏,Actions等常见设计元素的使用方法。

2.1 UI文件设计与运行机制

2.1.1 项目文件组成

  • 一个Widget Application项目包含一下一些文件
    • 项目组织文件 samp2_1.pro,存储项目设置的文件
    • 主程序入口文件main.cpp,实现main()函数的程序文件
    • 窗体界面文件widget.ui,一个XML格式存储的窗体上的元件及其布局的文件
    • widget.h是所设计的窗体类的头文件,widget.cpp是widget.h里定义类的实现文件。在C++里,任何窗体或界面组件都是用类封装的,一个类一般有一个头文件和一个源文件。

2.1.2 项目管理文件

  • 后缀为.pro的文件是项目的管理文件,文件名就是项目的名称

2.1.3 界面文件

  • 后缀为 .ui 的文件是可视化设计的窗体的定义文件,例如 widget.ui
  • 本书后面将成这个集成在Qt Creator中的Qt Designer为 UI设计器,以便与独立运行的Qt Designer区别开来

2.1.4 主函数文件

  • main函数是应用程序的入口。它的主要功能是创建应用程序,创建窗口,显示窗口,并运行从应用程序,开始应用程序的消息循环和事件处理。

2.1.5 窗体相关的文件

  • 为了搞清楚窗体类的定义,以及界面功能的实现原理,香茗居编译后会自动生成一个文件 ui_widget.h,这样对于一个窗体,就有四个文件
    • widget.h 定义窗体类的头文件定义了类Widget
    • widget.cpp Widget类的功能实现源程序文件
    • widget.ui 窗体界面文件,由UI设计器自动生成,存储了窗体上各个组件的属性设置和布局
    • ui_widget.h 编译后,根据窗体上的组件及其属性,信号与槽的关联自动生成的一个类的定义文件,类的名称是Ui_Widget

2.2

2.2.3 信号与槽

  • 信号与槽(Signal & Slot)是Qt编程的基础,也是Qt的一大创新。因为有了信号与槽的编程机制,在Qt中处理界面各个组件的交互操作时变得更加直观和简单。

  • 信号(Signal)就是在特定情况下被发射的事件,例如PushButton最常见的信号就是鼠标单击时发射的clicked()信号,一个ComboBox最常见的信号是选择的列表项变化时发射的CurrentIndexChanged()信号。GUI程序设计的主要内容就是对界面上各组件的信号的相应,只需要知道什么情况下发射哪些信号,合理的去响应和处理这些信号就可以了。

  • 槽(Slot)就是对信号响应的函数。槽就是一个函数,与一般的C++函数是一样的,可以定义在类的任何部分(public, private或protected),可以具有任何参数,也可以被直接调用。槽函数与一般函数不同的是: 槽函数可以与一个信号关联,当信号被发射时,关联的槽函数被自动执行

  • 信号与槽关联是用QObject::connect()函数实现的,其基本格式是:

    1
    QObject::connect(sender, SIGNAL(signal()), reciver, SLOT(slot()));
  • connect()是QObject类的一个静态函数,而QObject是所有Qt类的基类,在实际调用时可以忽略前面的限定符,所以可以直接写为:

    1
    connect(sender, SIGNAL(signal()), reciver, SLOT(slot()));
    • 其中,sender是发射信号的对象的名称,signal()是信号名称。信号可以看作是特殊的函数,需要带括号,有参数时还需要指明参数。receiver是接收信号的对象名称,slot()是槽函数的名称,需要带括号,有参数时还需要指明参数。
    • SIGNAL和SLOT是Qt的宏,用于指明信号和槽,并将它们的参数转换为相应的字符串。例如
      1
      QObject::connect(btnClose, SIGNAL(clicked()), Widget, SLOT(close()));
    • 其作用就是将btnClose按钮的clicked()信号与窗体(Widget)的槽函数close()相关联,这样,当单击btnClose按钮时,就会执行Widget的Close()槽函数。
  • 关于信号与槽的使用,有以下一些规则需要注意

    • 一个信号可以连接多个槽,例如
      1
      2
      connect(spinNum, SIGNAL(valueChanged(int)), this, SLOT(addFun(int)));
      connect(spinNum, SIGNAL(valueChanged(int)), this, SLOT(updateStatus(int)));
      • 这是当一个对象spinNUm的数值发生变化时,所在窗体有两个槽进行响应,一个addFun()用于计算,一个updateStatus()用于更新状态。
      • 当一个信号与多个槽函数关联时,槽函数按照建立连接时的顺序依次执行。
      • 当信号和槽函数带有参数时,在connect()函数里,要写明参数的类型,但可以不写参数名称。
    • 多个信号可以连接同一个槽,例如
      1
      2
      3
      connect(ui->rBtnBlue, SIGNAL(clicked()), this, SLOT(setTextFontColor()));
      connect(ui->rBtnRed, SIGNAL(clicked()), this, SLOG(setTextFontColor()));
      connect(ui->rBtnBlack, SIGNAL(clicked()), this, SLOG(setTextFontColor()));
      • 这样,当任何一个RadioButton被单击时,都会执行setTextFontColor()函数。
    • 一个信号可以连接另外一个信号,例如
      1
      connect(spinNum, SIGNAL(valueChanged(int)), this, SIGNAL(refreshInfo(int)));
      • 这样,当一个信号发射时,也会发射另外一个信号,实现某些特殊的功能。
    • 严格情况下,信号与槽的参数个数和类型需要一致,至少信号的参数不能少于槽的参数。如果不匹配,会出现编译错误或运行错误。
    • 在使用信号与槽的类中,必须在类的定义中加入宏Q_OBJECT
    • 当一个信号被发射时,与其关联的槽函数通常被立即执行,就像正常调用一个函数一样。只有当信号关联的所有槽函数执行完毕后,才会执行发射信号处后面的代码。
  • 信号与槽机制是Qt GUI编程的基础,使用信号与槽机制可以比较容易的将信号与响应代码关联起来。

第三章 Qt类库概述

  • Qt是一个用标准C++编写的跨平台开发类库,它对标准C++进行了扩展,引入了元对象系统,信号与槽,属性等特性,使应用程序的开发变得更高效。本章将介绍Qt的这些核心特点,对于理解和编写高效的Qt C++程序是大有帮助的。
  • 本章还介绍头文件中Qt的一些全局定义,包括数据类型,函数和宏等,介绍Qt的容器类及其响应迭代器的使用方法。这些全局定义和容器类在程序中经常用到,了解其原理便于理解后面遇到的一些实例程序。
  • Qt类库中大量的类是以模块形式分类组织的,包括基本模块和扩展模块等,本章对这些模块做一个总体的介绍。一个模块通常就是一个编程主题,例如数据库,图表,网格等。本书后面的章节一般是每章介绍一个编程主题。

3.1 Qt核心特点

3.1.1 概述

  • Qt本身并不是一种编程语言,它实质上是一个跨平台的C++开发类库,是用标准C++编写的类库,它为开发GUI应用程序和非GUI应用程序提供了各种类。

  • Qt对标准C++进行了扩展,引入了一些新的概念和功能,例如信号与槽,对象属性等。Qt的元对象编译器(Meta-Object Compiler, MOC)是一个预处理器,在源程序被编译前先将这些Qt特性的程序转换为标准C++兼容的形式,然后再由标准C++编译器进行编译。这就是为什么在使用信号与槽机制的类里,必须添加一个Q_OBJECT宏的原因,只有添加了这个宏,moc才能对类里的信号与槽的代码进行预处理。

  • Qt Core模块是Qt类库的核心,所有其他模块都依赖于此模块。Qt 为C++语言增加的特性就是在Qt Core模块里实现的,这些扩展特性由Qt的元对象系统实现,包括信号与槽机制,属性系统,动态类型转换等。

3.1.2 元对象系统

  • Qt的元对象系统(Meta-Object System)提供了对象之间通信的信号与槽机制,运行时类型信息和动态属性系统。

  • 元对象系统由以下三个基础组成

    • QObject类是所有使用元对象系统的类的基类。
    • 在一个类的private部分声明Q_OBJECT宏,使得类可以使用元对象的特性,例如动态属性,信号与槽。
    • MOC(元对象编译器)为每个QObject的子类提供必要的代码来实现元对象系统的特性。
  • 构建项目时,MOC工具读取C++源文件,当它发现类的定义里有Q_OBJECT宏时,它就会为这个类生成另外一个包含有元对象支持代码的C++源文件,这个生成的源文件连同类的实现文件一起被编译和链接。

  • 除了信号与槽机制外,元对象还提供如下一些功能

    • QOBject::metaObject()函数返回类关联的元对象,元对象类QMetaObject包含了访问对象的一些接口函数,例如QMetaObject::className()函数可在运行时返回类的名称字符串。
      1
      2
      QObject *obj = new QPushButton;
      obj->metaObject()->className(); // 返回"QPushButton
    • QMetaObject::newInstance()函数创建类的一个新的实例
    • QObject::inherits(const char* className)函数判断一个对象实例是否是名称为className的类或QObject的子类的实例。例如
      1
      2
      3
      4
      QTimer *timer = new QTimer; // QTimer是QObject的子类
      timer->inherits("QTimer"); // 返回true
      timer->inherits("QObject"); // 返回true
      timer->inherits("QAbstractButton"); // 返回false,不是QAbstractButton的子类
    • QObject::tr()和QObject::trUtf8()函数可翻译字符串,用于多语言界面设计。
    • QObject::setProperty()和QObject::property()函数用于通过属性名称动态设置和获取属性值。
  • 对于QObject及其子类,还可以使用qobject_cast()函数进行动态映射(dynamic cast)。

  • 使用动态投射,使得程序可以在运行时对不同的对象做不同的处理。

3.1.3 属性系统

  • Qt提供一个 Q_PROPERTY()宏可以定义属性,它也是基于元对象系统实现的。Qt的属性系统与C++编译器无关,可以用任何标准的C++编译器编译定义了属性的QtC++程序。
  • Q_PROPERTY宏定义一个返回值类型为type,名称为name的属性,用READ,WRITE关键字定义属性的读取,写入函数,还有其他的一些关键字定义属性的一些操作特性。属性的类型可以是QVariant支持的任何类型,也可以用户自定义类型。
  • Q_PROPERTY宏定义属性的一些主要关键字的意义如下
  • READ: 指定一个读取属性值的函数,没有MEMBER关键字时必须设置READ
  • WRITE: 指定一个设定属性值的函数,只读属性没有WRITE设置
  • MEMBER: 指定一个成员变量与属性关联,成为可读可写的属性,无需再设置READ和WRITE
  • RESET: 是可选的,用于指定一个设置属性缺省值的函数
  • NOTIFY: 是可选的,用于设置一个信号,当属性值变化时发射此信号
  • DESIGNABLE: 表示属性是否在Qt Designer里可见,缺省为true
  • CONSTANT: 表示属性值是一个常数,对于一个对象实例,READ指定的函数返回值是常数,但是每个实例的返回值可以不一样。具有CONSTANT关键字的属性不能有WRITE和NOTIFY关键字。
  • FINAL: 表示所定义的属性不能被子类重载

3.1.4 信号与槽

  • 信号与槽是Qt的一个核心特点,也是它区别于其他框架的重要特性。信号与槽是对象间进行通信的机制,也需要由Qt的元对象系统支持才能实现的。

  • Qt使用信号与槽的机制实现对象间通信,它隐藏了复杂的底层实现,完成信号与槽的关联后,发射信号时并不需要知道Qt是如何找到槽函数的。Qt的信号与槽机制与Delphi和C++ Builder的”事件–响应”比较类似,但是更加灵活。

  • 某些开发架构使用回调函数(callback)实现对象间通信。与回调函数相比,信号与槽的执行速度稍微慢一点,因为需要查找连接的对象和槽函数,但是这种差别在应用程序运行时是感觉不到的,而其提供的灵活性却比回调函数强很多。

  • 对信号与槽的特点和用法做一些补充

    1. connect()函数的不同参数形式
    • QObject::connect()函数有多重参数形式,一种参数形式的函数原型是
      1
      QMetaObject::Connection QObject::connect(const QObject *sender, const char *signal, const QObject *receiver, const char *method, Qt::ConnectionType type = Qt::AutoConnection);
    • 使用这种参数形式的connect()进行信号与槽函数的连接时,一般句法如下
      1
      connect(sender, SIGNAL(signal()), receiver, SLOT(slot()));
    • 这里使用了宏SIGNAL()和SLOT()指定信号和槽函数,而且如果信号和槽函数带有参数,还需要注明参数类型,例如
      1
      connect(spinNum, SIGNAL(valueChanged(int)), this, SLOT(updateStatus(int)));
    • 另外一种参数形式的connect()函数的原型是:
      1
      QMetaObject::Connection QObject::connect(const QObject *sender, const QMetaMethod &signal, const QObject *receiver, const QMetaMethod &method, Qt::ConnectionType type = Qt::AutoConnection);
    • 对于具有默认参数的信号与槽(即信号名称是唯一的,没有参数不同而同名的两个信号),可以使用这种函数指针形式进行关联,例如
      1
      connect(lineEdit, &QLineEdit::textChanged, this, &widget::on_textChanged);
  • 不管是哪种参数形式的connect()函数,最后一个参数Qt::ConnectionType type,缺省值为Qt::AutoConnection。枚举类型Qt::ConnectionType表示了信号与槽之间的关联方式,有以下几种取值:

    • Qt::AutoConnection(缺省值): 如果信号的接收者与发射者在同一个线程,就使用Qt::DirectConnection方式;否则使用Qt::QueuedConnection方式,在信号发射时自动确定关联方式
    • Qt::DirectConnection: 信号被发射时槽函数立即执行,槽函数与信号在同一个线程
    • Qt::QueuedConnection: 在事件循环回到接收者线程后执行槽函数,槽函数与信号在不同的线程。
    • Qt::BlockingQueuedConnection: 与Qt::QueuedConnection相似,只是信号线程会阻塞直到槽函数执行完毕。当信号与槽函数在同一个线程时绝对不能使用这种方式,否则会造成死锁。
  1. 使用sender()获得信号发射者
    • 在槽函数里,使用QObject::sender()可以获取信号发射者的指针。如果知道信号发射者的类型,可以将指针投射为确定的类型,然后使用这个确定类的接口函数。
  2. 自定义信号及其使用
    • 在自己设计的类里也可以自定义信号,信号就是在类定义里声明的一个函数,但是这个函数无需实现,只需要发射(emit)
    • 例如,在下面的自定义类QPerson的signals部分定义一个信号ageChanged(int)
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      class QPerson : public QObject
      {
      Q_OBJECT
      private:
      int m_age = 10;
      public:
      void incAge();
      signals:
      void ageChanged(int value);
      }
  • 信号函数必须是无返回值的函数,但是可以有输入参数。信号函数无需实现,只需在某些条件下发射信号。例如,在incAge()函数中发射信号,其代码如下
    1
    2
    3
    4
    5
    void QPerson::incAge()
    {
    m_age++;
    emit ageChanged(m_age); // 发射信号
    }
  • 在incAge()函数里,当私有变量m_age变化后,发射信号ageChanged(int),表示年龄发生了变化。至于是否有此信号相关联的槽函数,信号发射者并不管。如果在使用QPerson类对象的程序中为此信号关联了槽函数,在incAge()函数里发射此信号时,就会执行相关联的槽函数。至于是否立即执行槽函数,发射信号的线程是否等待槽函数执行完之后再执行后面的代码,与connect()函数设置信号与槽关联时设置的连接类型以及信号与槽是否在同一个线程有关。

3.2 Qt全局定义

  • 头文件包含了Qt类库的一些全局定义,包括基本数据类型,函数和宏,一般的Qt类的头文件都会包含该文件,所以不用显示包含这个头文件也可以使用其中的定义。

3.2.1 数据类型定义

  • 为了确保在各个平台上各数据类型都有统一确定的长度,Qt为各种常见数据类型定义了类型符号,例如qint8就是signed char的类型定义,即 typedef signed char qint8;

3.2.2 函数

  • 头文件包含一些常用函数的定义,这些函数多以模板类型作为参数,返回相应的模板类型,模板类型可以用任何其他类型替换。若是以double或float类型作为参数的,一般有两个参数版本的额同名函数,例如qFuzzyIsNull(double d)和qFuzzyIsNull(float f)。
  • 还有一些基础的数学运算函数在头文件中定义,例如三角运算函数,弧度与角度之间的转换函数等。

3.2.3 宏定义

  • 头文件中定义了很多宏,以下是一些比较常用的

  • QT_VERSION

    • 这个宏展开为数值形式0xMMNNPP(MM = major, NN = minor, PP = patch)表示Qt编译器版本,例如Qt编译器版本为Qt 5.9.1,则QT_VERSION为0x050901。这个宏常用于条件编译设置,根据Qt版本不同,编译不同的代码段。
      1
      2
      3
      4
      5
      6
      #if QT_VERSION >= 0x040100
      QIcon icon = style()->standardIcon(QStyle::SP_TrashIcon);
      #else
      QPixmap pixmap = style()->standardPixmap(QStyle::SP_TrashIcon);
      QIcon icon(pixmap);
      #endif
  • QT_VERSION_CHECK

    • 这个宏展开为Qt版本号的一个整数表示,例如
      1
      2
      3
      4
      5
      #if (QT_VERSION >= QT_VERSION_CHECK(5, 0, 0))
      #include <QtWidgets>
      #else
      #include <QtGui>
      #endif
  • QT_VERSION_STR

    • 这个宏展开为Qt版本号的字符串,例如 “5.9.0”
  • Q_BYTE_ORDER, Q_BIG_ENDIAN 和 Q_LITTLE_ENDIAN

    • Q_BYTE_ORDER 表示系统内存中数据的字节序,
    • Q_BIG_ENDIAN 表示大端字节序
    • Q_LITTLE_ENDIAN 表示小端字节序。
    • 在需要判断系统字节序时会用到,例如
      1
      2
      3
      #if Q_BYTE_ORDER == Q_LITTLE_ENDIAN
      ...
      #endif
  • Q_DECL_IMPORT, Q_DECL_EXPORT

    • 在使用或设计共享库时,用于导入或到处库的内容
  • Q_DECL_OVERRIDE

    • 在类定义中,用于重载一个虚函数,例如在某个类中重载虚函数paintEvent(),可以定义如下
      1
      void paintEvent(QPaintEvent*) Q_DECL_OVERRIDE;
    • 使用Q_DECL_OVERRIDE宏后,如果重载的虚函数没有进行任何重载操作,编译器将会报错。
  • Q_DECL_FINAL

    • 这个宏将一个虚函数定义为最终级别,不能再被重载,或定义一个类不能再被继承,实例如下
      1
      2
      3
      class QRect Q_DECL_FINAL {  // QRect不能再被继承
      // ...
      };
  • Q_UNUSED(name)

    • 这个宏用于在函数中定义不在函数体里使用的参数,示例如下
      1
      2
      3
      4
      5
      void MainWindow::on_imageSaved(int id, const QString &filename)
      {
      Q_UNUSED(id);
      LabInfo->setText("图片保存为: " + filename);
      }
    • 在这个函数里,id参数没有使用。如果不用Q_UNUSED(id)定义,编译器会出现参数未使用的警告。
  • foreach(variable, container)

    • foreach用于容器类的遍历,例如
      1
      2
      foreach (const QString &codecName, recorder->supportedAudioCodecs())
      ui->comboCodec->addItem(codecName);
  • forever

    • forever用于构造一个无限循环,例如
      1
      2
      3
      forever {
      ...
      }
  • qDebug(const char *message, …)

    • 在debugger窗体显示信息,如果编译器设置了 Qt_NO_DEBUG_OUTPUT,则不作任何输出,例如
      1
      qDebug("Item in list: %d", myList.size());
  • 类似的宏还有 qWarning, qCritical, qFatal, qInfo等,也是用于在debugger窗体显示信息。

3.3 容器类

3.3.1 容器类概述

  • Qt提供了多个基于模板的容器类,这些容器类可以用于存储指定类型的数据项,例如常用的字符串列表类QStringList就是从容器类QList继承的,实现对字符串列表的添加,存储,删除等操作。

  • Qt的容器壁标准模板库(STL)中的容器类更轻巧,安全和易于使用。这些容器类是隐式共享和可重入的,而且他们进行了速度和存储优化,因此可以减少可执行文件的大小。此外,它们还是线程安全的,也就是说它们作为只读容器时可以被多个线程访问

  • 容器类是基于模板的类,如常用的容器类QList,T是一个具体的类型,可以是int, float等简单的类型,也可以是QString, QDate等类,但是不能是QObject或任何其子类。T必须是一个可赋值的类型,即T必须提供一个缺省的构造函数,一个可复制构造函数和一个赋值运算符。

  • Qt的容器类分为顺序容器(sequential containers)和关联容器(associative containers)

  • 容器迭代类用于遍历容器里的数据项,有Java类型的迭代类和STL类型的迭代类。Java类型的迭代类易于使用,提供高级功能,而STL类型的迭代类效率更高一些。

  • Qt还提供了foreach宏用于遍历容器内的所有数据项。

3.3.2 顺序容器类

  • Qt的顺序容器类有 QList, QLinkedList, QVector, QStack和QQueue

  • QList

    • QList是最常用的容器类,虽然它是以数组列表(array-list)的形式实现的,但是在其前或后添加数据非常快。QList以下标索引的方式对数据项进行访问。
  • QLinkedList

    • QLinkedList是链式列表(linked-list),数据项不是用连续的内存存储的,它基于迭代器访问数据项,并且插入和删除数据项的操作时间相同。
      除了不提供基于下标索引的数据项访问外,QLinkedList的其他接口函数与QList基本相同。
  • QVector

    • QVector提供动态数组的功能,以下标索引访问数据。
    • QVector的函数接口与QList几乎完全相同,QVector的性能比QList更高,因为QVector的数据项是连续存储的。
  • QStack

    • QStack是提供类似于堆栈的后入先出(LIFO)操作的容器类,push()和pop()是主要的接口函数。
  • QQueue

    • QQueue是提供类似于队列先入先出(FIFO)操作的容器类。enqueue()和dequeue()是主要的操作函数

3.3.3 关联容器类

  • Qt还提供关联容器类 QMap, QMultiMap, QHash, QMultiHash和QSet

  • QMultiMap和QMultiHash支持一个键关联多个值,QHash和QMultiHash类使用散列(Hash)函数进行查找,查找速度较快

  • QSet

    • QSet是基于散列列表的集合模板类,它存储数据的顺序是不定的,查找值的速度非常快。QSet内部就是用QHash实现的。
    • 测试一个值是否包含于这个集合,用contains()函数
  • QMap

    • QMap<Key, T>提供一个字典(关联数组),一个键映射到一个值。QMap存储数据是按照键的顺序,如果不在乎存储顺序,使用QHash会更快
  • QMultiMap

    • QMultiMap是QMap的子类,是用于处理多值映射的便利类
  • QHash

    • QHash是基于散列表来实现字典功能的模板类,QHash<Key, T>存储的键值对具有非常快的查找速度。
  • QHash与QMap的功能和用法相似,区别在于以下几点

    • QHash比QMap的查找速度更快
    • 在QMap上遍历时,数据项是按照键排序的,而QHash的数据项是任意顺序的
    • QMap的键必须提供 “<” 运算符,QHash的键必须提供 “==” 运算符和一个名称为 qHash() 的全局散列函数
  • QMultiHash

    • QMultiHash是QHash的子类,是哟弄个与处理多值映射的便利类,其用法与QMultiMap类似

3.4 容器类的迭代

  • 迭代器(iterator)为访问容器类里的数据项提供了统一的方法,Qt有两种迭代器类: Java类型的迭代器和STL类型的迭代器
  • Java类型的迭代器更易于使用,且提供一些高级功能,而STL类型的迭代器效率更高。

3.4.1 Java类型迭代器

3.4.2 STL类型迭代器

  • STL迭代器与Qt和STL的原生算法兼容,并且进行了速度优化

  • 对于每一个容器类,都有两个STL类型迭代器:一个用于只读访问,一个用于读写访问。无需修改数据时一定使用只读迭代器,因为它们速度更快。

  • STL类型的迭代器是数组的指针,所以 “++” 运算符使迭代器指向下一个数据项, “*”运算符返回数据项内容。

  • 隐式共享(Implicit Sharing)是对象的管理方法,一个对象被隐式共享,只是传递该对象的一个指针给使用者,而不实际复制对象数据,只有在使用者修改数据时,才实质复制共享对象给使用者。

  • 对于STL类型的容器,隐式共享还涉及到另外一个问题,即当有一个迭代器在操作一个容器变量时,不要去复制这个容器变量。

3.4.3 foreach关键字

  • 如果只是想遍历容器中所有的项,可以使用foreach关键字。foreach是头文件中定义的一个宏。
  • foreach关键字遍历一个容器变量是创建了容器的一个副本,所以不能修改原来容器变量的数据项

3.5 Qt类库的模块

  • Qt类库里大量的类根据功能分为各种模块,这些模块又分为几大类
    • Qt基本模块(Qt Essentials): 提供了Qt在所有平台上的基本功能
    • Qt附加模块(Qt Add-Ons): 实现一些特定功能的提供附加价值的模块
    • 增值模块(Value-Add Modules): 单独发布的提供额外价值的模块或工具
    • 技术预览模块(Technology Preview Modules): 一些处于开发阶段,但是可以作为技术预览使用的模块
    • Qt工具(Qt Tools): 帮助应用程序开发的一些工具。

3.5.1 Qt基本模块

  • Qt基本模块是Qt在所有平台上的基本功能,它们在所有的开发平台和目标平台上都可用,在Qt5所有版本上是源代码和二进制兼容的。具体的基本模块如下
    • Qt Core: 其他模块都用到的核心非图形类
    • Qt GUI: 设计GUI界面的基础类,包括OpenGL
    • Qt Multimedia: 音频,视频,摄像头和广播功能的类
    • Qt Multimedia Widgets: 实现多媒体功能的界面组件类
    • Qt Network: 使网路编程更简单和轻便的类
    • Qt QML: 用于QML和JavaScript语言的类
    • Qt Quick: 用于构建具有定制用户界面的动态应用程序的声明框架
    • Qt Quick Controls: 创建桌面样式用户界面,基于Qt Quick的用户界面控件
    • Qt Quick Dialogs: 用于Qt Quick的系统对话框类型
    • Qt Quick Layouts: 用于Qt Quick 2界面元素的布局项
    • Qt SQL: 使用SQL用于数据库操作的类
    • Qt Test: 用于应用程序和库进行单元测试的类
    • Qt Widgets: 用于构建GUI界面的C++图形组件类

3.5.2 Qt附加模块

3.5.3 增值模块

3.5.4 技术预览模块

4.5.5 Qt 工具

  • Qt工具在所有支持的平台上都可以使用,用于帮助应用程序的开发和设计
    • Qt Designer: 用于扩展Qt Designer的类
    • Qt Help: 在应用程序中集成在线文档的类,实现类似于Qt Assistant的功能
    • Qt UI Tools: 操作Qt Designer生成的窗体的类

第四章 常用界面设计组件

第五章 Model/View结构

  • Model/View(模型/视图)结构是Qt中用界面组件显示与编辑数据结构的一种结构,视图(View)是显示和编辑数据的界面组件,模型(Model)是视图与原始数据之间的接口。Model/View结构的典型应用是在是数据库应用程序中。
  • 主要的视图组件有 QListVew, QTreeView和QTableView

第六章 对话框与多窗体设计

  • 在一个完整的应用程序设计中,不可避免地会涉及多个窗体,对话框的设计和调用,如何设计和调用这些对话框和窗体是搞清楚一个庞大的应用程序设计的基础。本章将介绍对话框和多窗体设计,调用方式,数据传递等问题,主要包括以下几点
    • Qt 提供的标准对话框的使用,例如打开文件对话框,选择颜色对话框,字体对话框,消息提示和确认选择对话框等
    • 自定义对话框的设计和调用,如何获取返回值,在对话框中如何操作主窗体等
    • 在一个应用程序中如何设计多种窗体,基于QDialog,QWidget和QMainWindow创建的窗体的调用方式有哪些,它们之间有什么区别
    • 如何创建一个在多页组件中管理的多窗体应用,类似于现在流行的多页浏览器的界面效果,子窗体如何与主窗体实现交互
    • 如何创建MDI(Multi-document interface)应用程序
    • 如何创建一个带有启动界面(Splash)和登录界面的窗体,如何保存和读取应用程序设置的参数。

第七章 文件系统和文件读写

  • 文件的读写是很多程序具有的功能,甚至某些应用程序就是围绕着某一种格式文件的处理而开发的,所以文件读写是应用程序开发的一个基本功能。
  • 本章介绍Qt中如何实现文本文件,二进制文件的读写,以及文件和目录的管理功能。

第八章 绘图

  • GUI用户界面的优势是通过可视化的界面元素为用户提供遍历的操作,界面上的按钮,编辑框等各种界面组件其实都是通过绘图而得到的。Qt的二维绘图基本功能是使用QPainter在绘图设备上绘图,绘图设备包括QWidget,QPixmap等,通过绘制一些基本的点,线,圆等基本形状组成自己需要的图形,得到的图形是不可交互操作的图形。
  • Qt还提供了Graphics View架构,使用GraphicsView, QGraphicsScene和各种QGraphicsItem类绘图,在一个场景中可以绘制大量图件,且每个图件是可选择,可交互的,如同矢量图编辑软件那样可以操作每个图件。Graphics View架构为用户绘制复杂的组件化图形提供了便利。

第九章 Qt Charts

  • Qt Charts是Qt提供的图表模块,在Qt 5.7以前只有商业版才有Qt Charts,但是从Qt 5.7开始,社区版本也包含了Qt Charts。Qt Charts可以很方便的绘制常见的折线图,柱状图,饼图等图表。

第十章 Data Visualization

  • Data Visualization是Qt提供的用于数据三维显示的模块。在Qt 5.7以前只有商业版才有Qt Visualization,但是从Qt 5.7开始,社区版本也包含了Qt Visualization
  • Data Visualization用于数据的三维显示,包括三维柱状图,三维空间散点,三维曲面等。

第十一章 数据库

  • Qt SQL模块提供数据库编程的支持,Qt支持多种常见的数据库,例如 MySQL, Oracle, MS SQL Server, SQlite等。Qt SQL模块包括多个类,可以实现数据库连接,SQL语句执行,数据获取与界面显示等功能,数据与界面之间使用Model/View架构,从而可以方便的实现数据的界面显示和操作。

第十二章 自定义插件和库

  • 当UI设计器提供的界面组件不满足实际需求时,可以从QWidget继承自定义界面组件。有两种方法使用自定义界面组件,一种是提升发(promotion);另一种是为UI设计器设计自定义界面组件的Widget插件,直接安装到UI设计器的组件面板里。

第十三章 多线程

  • Qt为多线程提供了完整的支持。QThread是线程类,是实现多线程操作的核心类,一般从QThread继承定义自己的线程类。线程之间的同步是其交互的主要问题,Qt提供了QMutex,QMutexLocker,QReadWriteLock,QwaitCondition,QSemaphore等多种类用于实现线程之间的同步。Qt还有Qt Concurrent模块,提供一些高级的API实现多线程编程而无需使用QMutex, QwaitCondition和QSemaphore等基础操作。使用Qt Concurrent实现的多线程程序可以自动根据处理器内核个数调整线程个数。
  • 本章主要介绍用QThread实现多线程编程的方法,以及用QMutex,QWaitCondition,QSemaphore等实现线程同步的方法。

13.1 QThread创建多线程程序

13.1.1 QThread类功能简介

  • QThread类提供不依赖于平台的管理线程的方法。一个QThread类的对象管理一个线程,一般从QThread继承一个自定义类,并重定义虚函数run(),在run()函数里实现线程需要完成的任务。

  • 将应用程序的线程称为主线程,额外创建的线程称为工作线程。一般在主线程里创建工作线程,并调用start()开始执行工作线程的任务。start()会在内部调用run()函数,进入工作线程的事件循环,在run()函数里调用exit()或quit()可以结束线程的事件循环,或者在主线程里调用terminate()强制结束线程。

  • QThread类的主要接口函数,信号和槽函数如下

    • 公共函数
      • bool isFinished(): 线程是否结束
      • bool isRunning(): 线程是否正在运行
      • Priority priority(): 返回线程的优先级
      • void setPriority(Priority priority): 设置线程的优先级
      • void exit(int returnCode = 0): 退出线程的事件循环,退出码为 returnCode,0表示成功退出,否则表示有错误
      • bool wait(unsigned long time): 阻止线程执行,直到线程结束(从run()函数返回),或等待时间超过time(毫秒)
    • 公共槽函数
      • void quit(): 退出线程的事件循环,并返回代码0,等效于exit(0)
      • void start(Priority priority): 内部调用run()开始执行线程,操作系统根据priority参数进行调度
      • void terminate(): 终止线程的运行,但不是立即结束线程,而是等待操作系统结束线程。使用terminate()之后应使用wait()
    • 信号
      • void finished(): 在线程就要结束时发射此信号
      • void started(): 在线程开始执行,run()函数被调用之前发射此信号
    • 静态公共成员
      • int idelThreadCount(): 返回系统上能够运行的线程的理想个数
      • void msleep(unsigned long msecs): 强制当前线程休眠msecs毫秒
      • void sleep(unsigned long secs): 强制当前线程休眠secs秒
      • void usleep(unsigned long usecs): 强制当前线程休眠usecs微妙
    • 保护函数
      • virtual void run(): start()调用run()函数开始线程任务的执行,所以在run()函数里实现线程的任务功能
      • int exec(): 由run()函数调用,进入此线程的事件循环,等待exit()退出。
  • QThread是QObject的子类,所以可以使用信号和槽机制。QThread自身定义了started()和finished()两个信号,started()信号在线程开始执行之前发射,也就是在run()函数被调用之前,finished()信号在线程就要结束时发射。

13.2 线程同步

13.2.1 线程同步的概念

  • 在多线程应用程序中,由于多个线程的存在,线程之间可能需要访问同一个变量,或一个线程需要等待另外一个线程完成某个操作后才产生相应的动作。

13.2.2 基于互斥量的线程同步

  • QMutex和QMutexLocker是基于互斥量的线程同步类,QMutex定义的实例是一个互斥量,QMutex主要提供3个函数

    • lock(): 锁定互斥量,如果另外一个线程锁定了这个互斥量,它将阻塞执行直到其他线程解锁这个互斥量。
    • unlock(): 解锁一个互斥量,需要与lock()配对使用
    • tryLock(): 试图锁定一个互斥量,如果成功锁定就返回true;如果其他线程已经锁定了这个互斥量,就返回false,但不阻塞程序执行。
  • 定义的互斥量mutex相当于一个标牌,可以这样来理解互斥量: 列车上的卫生间一次智能进一个人,当一个人尝试进入卫生间就是lock(),如果有人占用,它就只能等待;等里面的人出来,腾出了卫生间是unlock(),这个等待的人才可以进入并且锁住卫生间的门,就是lock(),使用完卫生间之后他再出来时就是unlock()

  • QMutex需要配对使用lock()和unlock()来实现代码段的保护,在一些逻辑复杂的代码段或可能发生一场的代码中,配对就可能出错。

  • QMutexLocker是另外一个简化了互斥量处理的类。QMutexLocker的构造函数接受一个互斥量作为参数并将其锁定,QMutexLocker的析构函数则将此互斥量解锁,所以在QMutexLocker实例变量的生存期内的代码段得到保护,自动进行互斥量的锁定和解锁。(与C++的自动锁原理一样,构造函数锁住互斥量,析构函数释放互斥量)

13.2.3 基于QReadWriteLock的线程同步

  • 使用互斥量时存在一个问题: 每次只能有一个线程获得互斥量的权限。如果在一个程序中有多个线程读取某个变量,使用互斥量时也需要排队。而实际上若只是读取一个变量,是可以让多个线程同时访问的,这样互斥量就会降低程序的性能。

  • Qt提供了QReadWriteLock类,它是基于读或写的模式进行代码段锁定的,在多个线程读写一个共享数据时,可以解决上面所说的互斥量存在的问题。

  • QReadWriteLock以读或写锁定的同步方法允许以读或写的方式保护一段代码,它可以允许多个线程以只读方式同步访问资源,但是只要有一个线程以写方式访问资源,其他线程必须等待直到写操作结束。

  • QReadWriteLock提供一下几个主要的函数

    • lockForRead(): 以只读方式锁定资源,如果有其他线程以写入方式锁定,这个函数会阻塞
    • lockForWrite(): 以写入方式锁定资源,如果本线程或其他线程以读或写模式锁定资源,这个函数就会阻塞。
    • unlock(): 解锁
    • tryLockForRead(): 是lockForRead()的非阻塞版本
    • tryLockForWrite(): 是lockForRead()的非阻塞版本
  • QReadLocker和QWriteLocker是QReadWriteLock的简便形式,如同QMutexLocker是QMutex的简便版本一样,无需与unlock()配对使用。使用QReadLocker和QWriterLocker。

13.2.4 基于QWaitCondition的线程同步

  • 在多线程的程序中,多个线程之间的同步实际上就是他们之间的协调问题。前面采用的互斥量和基于QReadWriteLock的方法都是对资源的锁定和解锁,避免同时访问资源时发生冲突。在一个线程解锁资源后,不能及时通知其他线程。

  • QWaitCondition提供了另外一种改进的线程同步方法,QWaitCondition与QMutex结合,可以使一个线程在满足一定条件时通知其他多个线程,使他们及时做出响应,这样比只使用互斥量效率更高一些。

  • QWaitCondition提供如下一些函数

    • wait(Qmutex *lockedMutex): 解锁互斥量lockedMutex,并阻塞等待唤醒条件,被唤醒后锁定lockedMutex并退出函数
    • wakeAll(): 唤醒所有处于等待状态的线程,线程唤醒的顺序不确定,由操作系统的调度策略决定
    • wakeOne(): 唤醒一个处于等待状态的线程,唤醒哪个线程不确定,由操作系统的调度策略决定
  • QWaitCondition一般用于 “生产者/消费者(producer/consumer)”模型中。

13.2.5 基于信号量的线程同步

  • 信号量(Semaphore)是另一种限制对共享资源进行访问的线程同步机制,它与互斥量(Mutex)相似,但是有区别。一个互斥量只能被锁定依次,而信号量可以多次使用。信号量通常用来保护一定数量的相同的资源,例如数据采集时的双缓冲区。

  • QSemaphore是实现信号量功能的类,它提供以下几个基本的函数

    • acquire(int n): 尝试获得n个资源。如果没有这么多资源,线程将阻塞直到有n个资源可用
    • release(int n): 释放n个资源,如果信号量的资源已全部可用之后再release(),就可以创建更多的资源,增加可用资源的个数
    • int available(): 返回当前信号量可用的资源个数,这个个数永远不可能为负数,如果为0,就说明当前没有资源可用
    • bool tryAcquire(int n = 1): 尝试获取n个资源,不成功时不阻塞线程。
  • 在定义QSemaphore的实例时,可以传递一个数值作为初始可用的资源个数。

第十四章 网络编程

  • Qt网络模块提供了用于编写TCP/IP客户端和服务端程序的各种类,例如用于TCP通信的QTcpSocket和QTcpServer,用于UDP通信的QudpSocket,还有用于实现HTTp,FTP等普通网络协议的高级类,例如QNetworkRequest,QNetworkReply和QNetworkAccessManager。

14.1 主机信息查询

14.1.1 QHostInfo 和 QNetworkInterface 类

  • 查询一个主机的MAC地址或IP地址是网络应用程序中经常用到的功能。

  • QHostInfo的静态函数 localHostName() 可获取本机的主机名,静态函数 fromName() 可以通过主机名获取IP地址,静态函数 lookupHost() 可以通过一个主机名,以异步方式查找这个主机的IP地址。

  • QNetworkInterface 可以获得运行应用程序的主机的所有IP地址和网络接口列表。静态函数 allInterfaces() 返回主机上所有的网络接口的列表,一个网络接口可能包括多个的IP地址,每个IP地址与掩码或广播地址关联。如果无需直到子网掩码和广播的IP地址,使用静态函数 allAddresses() 可以获得主机上的所有IP地址列表。

14.2 TCP通信

14.2.1 TCP通信概述

  • TCP(Transmission Control Protocol)是一种被大多数Internet网络协议(例如HTTP和FTP)用于数据传输的低级网络协议,它是可靠的,面向流,面向连接的传输协议,特别适合用于连续数据传输。

14.3 QUdpSocket 实现 UDP通信

14.3.1 UDP通信概述

  • UDP(User Datagram Protocol,用户数据包协议)是轻量的,不可靠的,面向数据报(datagram),无连接的协议,它可以用于对可靠性要求不高的场合。与TCP通信不同,两个程序之间进行UDP通信无需预先建立持久的socket连接,UDP每次发送数据报都需要指定目标地址和端口。
  • UDP消息传送有单播,广播,组播三种模式
    • 单播(unicast)模式: 一个UDP客户端发出的数据包只发送到另一个指定地址和端口的UDP客户端,是一对一的数据传输
    • 广播(broadcast)模式:一个UDP客户端发出的数据包,在同一个网络范围内其他所有的UDP客户端都可以收到。
    • 组播(multicast)模式: 也称多播。UDP客户端加入到另一个组播IP地址指定的多播组,成员向组播地址发送的数据包组内成员都可以接收到。
  • 使用广播和多播模式,UDP可以实现一些比较灵活的通信功能,而TCP通信只有单播模式,没有广播和多播模式。所以UDP通信虽然不能保证数据传输的准确性,但是具有灵活性,一般的即时通信软件都是基于UDP通信的。

14.4 基于HTTP协议的网络应用程序

14.4.1 实现高层网络操作的类

  • Qt网络模块提供一些类实现OSI七层网络模型中高层的网络协议,例如HTTP,FTP,SNMP等,这些类主要是QNetworkRequest, QNetworkReply和QNetowrkAccessManager

第十五章 多媒体

  • 多媒体功能指的主要是计算机的音频和视频的输入,输出,显示和播放等功能,Qt的多媒体模块为音频和视频播放,录音,摄像头拍照和录像等提供支持,甚至还提供数字收音机的支持。

15.1 Qt多媒体模块功能概述

  • 利用Qt多媒体模块提供的各种类,可以实现一般的音频,视频的输入和输出。

第十六章 应用程序设计辅助功能

  • 本章介绍Qt应用程序设计的一些辅助功能,包括设计多语言界面,使用样式表定制界面和组件的外观,使用QStyle设置界面外观,以及应用程序发布等。