Yu's Blog

do something!

概述

  • 标准库容器定义的操作集合惊人得小, 并未给每个容器添加大量功能, 而是提供了一组算法, 这些算法中的大多数都独立于任何特定的容器
  • 一般情况下, 这些算法并不直接操作容器, 而是遍历由两个迭代器指定的一个元素范围来操作
  • 算法永远不会改变底层容器的大小, 可能改变容器中保存的元素的值, 也可能在容器内移动元素, 但永远不会直接添加或删除元素

初始泛型算法

只读算法

写容器元素的算法

重排容器元素的算法

定制操作

想算法传递函数

lambda 表达式

可调用对象

  • 函数
  • 函数指针
  • 重载了函数调用运算符的类
  • lambda 表达式

lambda 捕获和返回

  • 当定义一个 lambda 时, 编译器生成一个与 lambda 对应的新的(未命名的)类类型.(当向一个函数传递了一个 lambda 时, 同时定义了一个新类型和该类型的一个对象: 传递的参数就是次编译器生成的类类型的未命名对象)
  • & 告诉编译器采用捕获引用方式, = 则表示采用值捕获方式

参数绑定

标准库 bind 函数

  • 可以将 bind 函数看作一个通用的函数适配器, 它接受一个可调用对象, 生成一个新的可调用对象来适应原对象的参数列表
    1
    2
    3
    4
    auto newCallable = std::bind(callable, arg_list);
    // newCallable 本身是一个可调用对象
    // arg_list 是一个逗号分隔符的参数列表, 对应给定的 callable 参数
    // 当调用 newCallable 时, newCallable 会调用 callable, 并传递给它 arg_list 中的参数

使用 placeholders 名字

1
2
3
4
5
6
7
8
9
std::placeholders::_n;
// 占位符, 表示 newCallable 的参数, 它们占据了传递给 newCallable 的参数的位置. 数值 n 表示生成的可调用对象中参数的位置: _1 为 newCallable 的第一个参数, _2 为第二个参数, 以此类推

void func(int a, int b) {
std::cout << a << ' ' << b << std::endl;
}

auto f = std::bind(func, std::placeholders::_2, std::placeholders::_1);
f(3, 4); // out: 4 3

绑定引用参数

  • 默认情况下, bind 的那些不是占位符的参数被拷贝到 bind 返回的可调用对象中
  • 如果希望传递给 bind 一个对象而不拷贝它, 就必须使用标准库 ref 函数(ref 返回一个对象, 包含给定的引用, 此对象是可以拷贝的; cref 生成一个保存 const 引用的类)

再探迭代器(没看)

插入迭代器

iostream 迭代器

反向迭代器

泛型算法结构(没看)

5 类迭代器

算法形参模式

算法命名规范

特定容器算法

顺序容器概述

  • 顺序容器为程序员提供了控制元素存储和访问顺序的能力
  • vector, deque, list, forward_list, array, string

容器类概览

迭代器

容器类型成员

begin 和 end 成员

  • begin 指向容器中第一个元素
  • end 指向容器中尾元素之后位置

容器定义和初始化

赋值和 swap

  • 赋值运算符要求左边和右边的运算对象具有相同的类型, 它将右边运算对象中所有的元素拷贝到左边运算对象中
  • 顺序容器(array 除外)还定义了一个名为 assign 的成员, 允许从一个不同但相容的类型赋值, 或者从容器的一个子序列赋值
  • 除 array 外, swap 部队任何元素进行拷贝, 删除或插入操作. 可以保证在常数时间内完成

容器大小操作

关系运算符

  • 每个容器类型都支持相等运算符(== 和 !=); 除了无序关联容器外所有容器都支持关系运算符(>, >=, <, <=)
  • 关系运算符左右两边的运算对象必须是相同类型的容器, 且必须保存相同类型的元素
  • 只有当其元素类型也定义了相应的比较运算符时, 才可以使用关系运算符来比较两个容器
  • 容器的相等运算符实际上是使用元素的 == 运算符实现比较的, 而其他关系运算符是使用元素的 < 运算符. 如果元素类型不支持所需运算符, 那么保存这种元素的容器就不能使用相应的关系运算

顺序容器操作

向顺序容器添加元素

  • 当调用一个 emplace 成员函数时, 是将参数传递给元素类型的构造函数. emplace 成员使用这些参数在容器管理的内存空间中直接构造元素(push_back, insert 等是拷贝元素)

访问元素

  • 每个顺序容器都有一个 front 成员函数, 而除了 forward_list 之外的所有顺序容器都有一个 back 成员函数(要确保非空)
  • 在容器中访问元素的成员函数(front, back, 下标和 at)返回的都是引用; 如果容器是一个 const 对象, 则返回值是 const 的引用

删除元素

特殊的 forward_list 操作

改变容器大小

  • resize: 若当前大小大于所要求的大小, 容器后部的元素会被删除; 若当前大小小于新大小, 会将新元素添加到容器后部

容器操作可能使迭代器失效

  • 向容器添加元素和从容器中删除元素的操作可能会使指向容器元素的指针, 引用或迭代器失效

vector 对象是如何增长的

额外的 string 操作

构造 string 的其他方法

  • 当从一个 const char* 创建 string 时, 指针指向的数组必须以空字符结尾, 拷贝操作遇到空字符时停止; 若还传递给构造函数一个计数值, 数组就不必以空字符结尾.
  • substr 操作返回一个 string, 是原始 string 的一部分或全部的拷贝

改变 string 的其他方法

string 搜索操作

  • find 函数完成最简单的搜索, 查找参数制定的字符串, 若找到, 则返回第一个匹配位置的下标, 否则返回 npos

compare 函数

数值转换

  • to_string(val)
  • stoi, stol, stof, stod…

容器适配器

  • 三种顺序容器适配器: stack, queue, priority_queue
  • 一个容器适配器接受一种已有的容器类型, 使其行为看起来像一种不同的类型
  • 每个适配器都定义两个构造函数: 默认构造函数创建一个空对象, 接受一个容器的构造函数拷贝该容器来初始化适配器
    1
    2
    std::deque<int> deq;
    std::stack<int> stk(deq); // 从 deq 拷贝元素到 stk
  • 默认情况下, stack 和 queue 是基于 deque 实现的, priority_queue 是在 vector 之上实现的
  • 可以在创建一个适配器时将一个命名的顺序容器作为第二个类型参数, 来重载默认容器类型
    1
    std::stack<std::string, std::vector<std::string>> str_stk; // 在 vector 上实现的空栈
  • 每个容器适配器都基于底层容器类型的操作定义了自己的特殊操作, 我们只可以使用适配器操作, 而不能使用底层容器类型的操作

IO 类

  • iostream 定义了用于读写流的基本类型, fstream 定义了读写命名文件的类型, sstream 定义了读写内存 string 对象的类型
  • 为了支持使用宽字符的语言, 标准库定义了一组类型和对象来操作 wchar_t 类型的数据. 宽字符版本的类型和函数的名字以一个 w 开始(wcin, wcour, wcerr)
  • ifstream 和 istringstrean 都继承自 istream. 因此, 可以像使用 istream 对象一样来使用 ifstream 和 istringstream 对象

IO 对象无拷贝或赋值

  • 不能将形参或返回类型设置为流类型, 进行 IO 操作的函数通常以引用方式传递和返回流
  • 读写一个 IO 对象会改变其状态, 因此传递和返回的引用不能是 const 的

条件状态(未完成)

  • 一个流一旦发生错误, 其上后续的 IO 操作都会失败.
  • 只有当一个流处于无错误状态时, 才可以从它读取数据
    1
    while(std::cin >> word) {} // 读取应该判断状态

管理输出缓冲

  • 每个流都管理一个缓冲区, 用来保存程序读写的数据.(有了缓冲机制, 操作系统就可以将程序的多个输出操作组合成单一的系统级操作)
  • 导致缓冲刷新的原因
    • 程序正常结束, 作为 main 函数的 return 操作的一部分, 缓冲刷新被执行
    • 缓冲区满时, 需要刷新缓冲区, 而后新的数据才能继续写入缓冲区
    • 可以使用操纵符 endl 来显示刷新缓冲区
    • 在每个输出操作之后, 可以用操纵符 unithub 设置流的内部状态, 来清空缓冲. 默认情况下, 对 cerr 是设置 unitbuf 的, 因此写到 cerr 的内容都是立即刷新的
    • 一个输出流可能被关联到另一个流(流关联: 确保一个流在另一个流执行输出操作前先刷新(flush),避免数据输出顺序混乱). 在这种情况下, 当读写被关联的流时, 关联到的流的缓冲区会被刷新.(默认情况下, cin 和 cerr 都关联到 cout, 因此, 读 cin 或 写 cerr 都会导致 cout 的缓冲区被刷新)
  • 如果程序崩溃, 输出缓冲区不会被刷新
  • 当一个输入流被关联到一个输出流时, 任何试图从输入流读取数据的操作都会先刷新关联的输出流(标准库将 cout 和 cin 关联在一起)
    • 交互式系统通常应该关联输入流和输出流(这样所有输出, 包括用户提示信息, 都会在读操作之前被打印出来)

文件输入输出

使用文件流对象

  • 当一个 fstream 对象被销毁时, close 会自动被调用

文件模式

  • in: 以读方式打开
  • out: 以写方式打开
  • app: 每次写操作前均定位到文件末尾
  • ate: 打开文件后立即定位到文件末尾
  • trunc: 截断文件
  • binary: 以二进制方式进行 IO
  • 在每次打开文件时, 都要设置文件模式, 可能是显式地设置, 也可能是隐式地设置. 当程序未指定模式时, 就使用默认值
    • 与 ifstream 关联的文件默认以 in 模式打开
    • 与 ofstream 关联的文件默认以 out 模式打开
    • 与 fstream 关联的文件默认以 in 和 out 模式打开

string 流

  • istringstream 从 string 读取数据, ostringstream 向 string 写入数据, 而 stringstream 既可以从 string 读数据也可向 string 写数据

使用 istringstream

使用 ostringstream

定义抽象数据类型

  • 默认情况下, this 指针的类型是指向类类型非常量版本的常量指针(class_name *const)

构造函数

  • 构造函数不能被声明成 const(当我们创建类的一个 const 对象时, 直到构造函数完成初始化过程, 对象才能真正取得其常量属性. 因此, 构造函数在 const 对象的构造过程中可以向其写值)
  • 类没有显示地定义任何构造函数时, 那么编译器就会为我们隐式地定义一个默认构造函数, 其初始化类的数据成员流程如下:
    • 如果存在类内的初始值, 用它来初始化成员
    • 否则, 默认初始化该成员
  • 如果类中包含一个其他类类型的成员且这个成员的类型没有默认构造函数, 那么编译器将无法初始化该成员
  • 构造函数不应该轻易覆盖掉类内的初始值, 除非新赋的值与原值不同
  • 如果不能使用类内初始值(编译器不支持), 则所有构造函数都应该显式地初始化每个内置类型的成员
  • 没有出现在构造函数初始值列表的成员将通过相应的类内初始值(如果存在的话)初始化, 或者执行默认初始化

拷贝、赋值和析构

  • 不主动定义这些操作, 编译器会自动生成, 将对对象的每个成员执行拷贝, 赋值和销毁操作

访问控制与封装

  • 定义在 public 说明符之后的成员在整个程序内可被访问
  • 定义在 *private 说明符之后的成员可以被类的成员函数访问, 但是不能被使用该类的代码访问
  • class 和 strut 唯一的区别是默认访问权限不一样. struct(public), class(private)

友元

  • 类可以允许其他类或者函数访问它的非共有成员, 方法是令其他类或者函数成为它的友元
  • 友元声明只能出现在类定义的内部, 但是在类内出现的具体位置不限. 友元不是类的成员, 不受它所在区域访问控制级别的约束(一般来说, 最好在类定义开始或结束前的位置集中声明友元)
  • 友元的声明仅仅指定了访问的权限, 而非一个通常意义上的函数声明. 如果希望类的用户能够调用某个友元函数, 那么就必须在友元声明之外再专门对函数进行一次声明

类的其他特性

类成员再探

  • 使用 =default 告诉编译器合成默认的构造函数
  • 定义在类内部的成员函数是自动 inline 的
  • 通过在成员变量的声明中加入 mutable 关键字可以让其能在 const 成员函数中修改
  • 当提供一个类内初始值时, 必须以符号 = 或花括号表示

返回 *this 的成员函数

  • 一个 const 成员函数如果以引用形式返回 *this, 那么它的返回类型将是常量引用
  • 通过区分成员函数是否是 const 的, 可以对其进行重载. (即: this 隐式参数类型是 type* const 还是 const type* const)

类类型

  • 即使两个类的成员列表完全一致, 它们也是不同的类型
  • 就像可以把函数的声明和定义分离开来, 也能仅仅声明类而暂时不定义它
  • class name; 这种声明有时被叫做前向声明, 它向程序中引入了名字并且指明是一种类类型, 在它声明之后和定义之前是一个不完全类型
  • 不完全类型只能在非常有限的情景下使用: 可以定义指向这种类型的指针或引用, 也可以声明(但是不能定义)以不完全类型作为参数或返回类型的函数
  • 对于一个类来说, 在创建它的对象之前该类必须被定义过, 而不能仅仅被声明

友元再探

  • 类还可以把其他类定义成友元, 也可以把其他类的成员函数定义成友元
  • 友元关系不存在传递性, 即每个类负责控制自己的友元类或友元函数
  • 如果一个类想把一组重载函数声明成它的友元, 需要对这组函数中的每一个分别声明
  • 友元声明的作用是影响访问权限, 它本身并非普通意义上的声明(就算在类的内部定义友元函数, 也必须在类的外部提供相应的声明从而使得函数可见)

类的作用域

  • 当成员函数定义在类的外部时, 返回类型中使用的名字都位于类的作用域之外. 这时, 返回类型必须指明它是哪个类的成员

名字查找与类的作用域

  • 编译器处理完类中的全部声明后才会处理成员函数而定义(所以成员函数能使用类中定义的任何名字)

构造函数再探

构造函数初始值列表

  • 如果没有在构造函数的初始值列表中显式地初始化成员, 则该成员将在构造函数体之前执行默认初始化

构造函数的初始值有时必不可少

  • 如果成员是 const 或者是引用的话, 必须将其初始化
  • 当成员属于某种类类型且该类型没有定义默认构造函数时, 也必须将这个成员初始化
  • 随着构造函数体一开始执行, 初始化就完成了. 因此 const, 引用或者属于某种为提供默认构造函数的类类型, 必须通过构造函数初始值列表为这些成员提供初值

成员初始化的顺序

  • 成员初始化的顺序与它们在类定义中的出现顺序一致: 第一个成员先被初始化, 然后第二个

委托构造函数

  • 一个委托构造函数使用它所属类的其他构造函数执行它自己的初始化过程
  • 当一个构造函数委托给另一个构造函数时, 受委托的构造函数的初始值列表和函数体被依次执行

默认构造函数的作用

隐式的类类型转换

  • 如果构造函数只接受一个实参, 则它实际上定义了转换为此类类型的隐式转换机制, 有时我们把这个构造函数称作转换构造函数
  • 编译器只会自动地执行一步类型转换
  • 通过将构造函数说明为 explicit 阻止转换
  • 关键字 explicit 只对一个实参的构造函数有效
  • 只能在类内声明构造函数时使用 explicit 关键字, 在类外部定义时不应重复
  • 尽管编译器不会将 explicit 的构造函数用于隐式类型转换, 但是还是可以使用这样的构造函数显式地强制进行转换

聚合类

字面值常量类

  • 数据成员都必须是字面值类型
  • 类必须至少含有一个 constexpr 构造函数
  • 如果一个数据成员含有类内初始值, 则内置类型成员的初始值必须是一条常量表达式; 或者如果成员属于某种类类型, 则初始值必须使用成员自己的 constexpr 构造函数
  • 类必须使用析构函数的默认定义, 该成员负责销毁类的对象

constexpr 构造函数

  • 尽管构造函数不能是 const 的, 但是字面值常量类的构造函数可以是 constexpr 函数

类的静态成员

  • static 成员
  • 可以是 public 的或着是 private 的
  • 当在类的外部定义静态成员时, 不能重复 static 关键字, 该关键字只出现在类内部的声明语句
  • 因为静态数据成员不属于类的任何一个对象, 所以它们并不是在创建类的对象时被定义的
  • 类似于全局变量, 静态数据成员定义在任何函数之外. 因此一旦被定义, 就将一直存在与程序的整个生命周期中
  • 要想确保对象只定义一次, 最好的办法是把静态数据成员的定义与其他非内联函数的定义放在同一个文件中
  • 通常情况下, 类的静态成员不应该在类的内部初始化. 然而, 可以为静态成员提供 const 类型的类内初始值, 不过要求静态成员必须是字面值常量类型的 constexpr
    1
    2
    3
    4
    5
    6
    class Test {
    public:
    static constexpr int i = 100;
    static const int i = 100; // const 和 constexpr 在这里应该是一样的(不确定)
    static constexpr double j = 3.14;
    };
  • 即使一个常量静态数据成员在类内部被初始化了, 通常情况下也应该在类外部定义一下该成员(内部提供了初始值, 外部定义不能再制定一个初始值了)
  • 静态数据成员可以是不完全类型(静态数据成员的类型可以是它所属的类类型)
    1
    2
    3
    4
    5
    6
    class Test {
    public:
    static Test si; // yes, 静态成员可以是不完全类型
    Test *pi; // yes, 指针成员可以是不完全类型
    Test i; // error, 数据成员必须是完全类型
    };

函数基础

局部对象

函数声明

  • 定义函数的源文件应该把含有函数声明的头文件包含进来, 编译器负责验证函数的定义和声明是否匹配

分离式编译

参数传递

传值参数

传引用参数

const 形参和实参

  • 当用实参初始化形参时会忽略掉顶层 const. 换句话说, 形参的顶层 const 被忽略掉了. 当形参有顶层 const 时, 传给它常量对象或者非常量对象都是可以的
  • 由于顶层 const 被忽略了, 因此有无顶层 const 的形参对于函数重载是一样的
    1
    2
    void fcn(int i) {}
    void fcn(const int i) {} // error, 重复定义

数组形参

main: 处理命令行选项

含有可变形参的函数

initializer_list 形参

  • initializer_list 对象中的元素永远是常量值
    1
    2
    3
    4
    5
    void error_msg(initializer_list<std::string> l) {
    for (auto beg = l.begin(); beg != l.end(); ++beg) {
    std::cout << *beg << std::endl;
    }
    }

省略符形参

  • 省略符形参是为了便于 C++ 程序访问某些特殊的 C 代码而设置的

返回类型和 return 语句

无返回值函数

有返回值函数

  • 返回一个值的方式和初始化一个变量或形参的方式完全一样: 返回的值用于初始化调用点的一个临时量, 该临时量就是函数调用的结果
  • 不要返回局部对象的引用或指针
  • 函数的返回类型决定函数调用是否是左值: 调用一个返回引用的函数得到左值, 其他返回类型得到右值
  • 函数可以返回花括号包围的值的列表. 类似于其他返回结果, 此处的列表也用来对表示函数返回的临时量进行初始化.

尾置返回类型

1
auto func(int i) -> int { return i; }

使用 decltype

1
2
int i = 0;
decltype(i) func(int i) { return i; }

返回数组指针

函数重载

  • 同一作用域内的几个函数名字相同但形参列表不同, 称为函数重载
  • main 函数不能重载
  • 不允许两个函数除了返回类型外其他所有的要素都相同
  • 一个拥有顶层 const 的形参无法和另一个没有顶层 const 的形参区分开来
    1
    2
    int func(int i);
    int func(const int i); // error
  • 如果形参是某种类型的指针或引用, 则通过区分其指向的是常量对象还是非常量对象可以实现函数重载, 此时 const 是底层的
    1
    2
    3
    4
    5
    int func(int *p);
    int func(const int *p); // yes

    int test(int &c);
    int test(const int &c); // yes

重载与作用域

  • 在内层作用域中声明函数, 将隐藏外层作用域中声明的同名实体
    1
    2
    3
    4
    5
    6
    7
    8
    9
    void func(std::string s) {
    std::cout << s << std::endl;
    }

    int main() {
    void func(int a);
    func("hello"); // error
    return 0;
    }
  • 当调用函数时, 编译器首先寻找对该函数的声明, 一旦在当前作用域中找到了所需的名字, 编译器就会忽略掉外层作用域中的同名实体

特殊用途语言特性

默认实参

  • 通常, 应该在函数声明中指定默认实参, 并将该声明放在合适的头文件中
  • 局部变量不能作为默认实参
  • 只要表达式的类型能转换成形参所需的类型, 该表达式就能作为默认实参
  • 用作默认实参的名字在函数声明所在的作用域内解析, 而这些名字的求值过程发生在函数调用时

内联函数和 constexpr 函数

  • 内联说明只是向编译器发出的一个请求, 编译器可以选择忽略这个请求
  • constexpr 函数是指能用于常量表达式的函数
  • constexpr 函数体内也可以包含其他语句, 只要这些语句在运行时不执行任何操作就行
  • 把内联函数和 constexpr 函数放在头文件中(可以在程序中多次定义, 因为编译器要想展开函数仅有函数声明是不够的, 还需要函数的定义. 但其多个定义必须完全一致, 因此通常定义在头文件中)

调试帮助

  • assert 依赖一个名为 NDEBUG 的预处理变量的状态

函数匹配

实参类型转换

函数指针

  • 函数指针指向的是函数而非对象
  • 函数的类型由它的返回类型和形参类型共同决定, 与函数名无关
  • 把函数名当作一个值使用时, 该函数自动地转换成指针
    1
    2
    pf = func;
    pf = &func; // 等价
  • 如果定义了指向重载函数的指针, 编译器通过指针类型决定选用哪个函数
  • decltype 返回的是函数类型, 不会将韩式类型自动转换成指针类型, 所以只有加上 * 才能得到指针
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    int func(int);

    using Func = int(int);
    typedef int Func(int);
    typedef decltype(func) Func; // 与上两行等价, 都是函数类型

    using FuncP = int(*)(int);
    typedef int (*Func)(int);
    typedef decltype(func) *FuncP; // 与上两行等价, 都是函数指针类型

    void useFunc(Func);
    void useFunc(FuncP); // 这两个声明语句声明的是同一个函数, 在第一个语句中, 编译器自动地将 Func 表示的函数类型转换成指针
  • 返回指向函数类型的指针必须把返回类型写成指针形式, 编译器不会自动地将函数返回类型当成对应的指针类型处理

简单语句

语句作用域

  • 可以在 if, switch, while, 和 for 语句的控制结构内定义变量

条件语句

if 语句

switch 语句

迭代语句

while 语句

传统的 for 语句

范围 for 语句

do while 语句

跳转语句

break 语句

continue 语句

goto 语句

try 语句块和异常处理

throw 表达式

1
throw runtime_error("ERROR");

try 语句块

1
2
3
4
5
6
7
try {
...
} catch (exception declaration) {
...
} catch (exception declaration) {
...
}

标准异常

基础

  • 当一个对象被用作右值时,用的是对象的值(内容);当对象被用作左值时,用的是对象的身份(在内存中的位置)
  • 在需要右值的地方可以用左值来代替,但不能把右值当成左值使用

基本概念

优先级与结合律

求值顺序

算术运算符

逻辑和关系运算符

  • 进行比较运算时除非比较的对象是布尔类型,否则不要使用布尔字面值 true 和 false 作为运算对象

赋值运算符

  • 赋值运算符满足右结合率
    1
    2
    int i, j;
    i = j = 0; // 都被赋值为 0

递增和递减运算符号

  • 前置版本将对象本身作为左值返回;后置版本则将对象原始值的副本作为右值返回

成员访问运算符

  • 解引用运算符的优先级低于点运算符,所以执行解引用运算的子表达式两端必须加上括号

条件运算符

  • cond ? expr1 : expr2;

位运算符

  • 如果运算对象是小整型,则它的值会被自动提升成较大的整数类型
  • 尽量使用无符号数进行位运算

sizeof 运算符

  • 返回一条表达式或一个类型名字所占的字节数
  • sizeof 不会实际求运算对象的值
  • 对解引用指针执行 sizeof 得到指针指向的对象所占空间的大小,指针不虚有效
  • 对数组执行 sizeof 运算得到整个数组所占空间的大小

逗号运算符

  • 含有两个运算对象,按照从左向右的顺序依次求值
  • 首先对左侧的表达式求值,然后将求值结果丢弃掉,真正的结果是右侧表达式的值

类型转换

算术转换

  • 若运算对象要么都是带符号的、要么都不带符号,则小类型转换为大类型
  • 有一个有符号、一个无符号,且无符号类型不小于有符号类型,那么有符号类型转换为无符号的类型
  • 若有符号类型大于无符号类型,此时转换的结果依赖于机器。如果无符号类型的所有值都能存在该有符号类型中,则无符号类型的运算对象转换成有符号类型;如果不能,那么带符号类型的运算对象转换成无符号类型。

其他隐式类型转换

显式转换

static_cast

  • 任何具有明确定义的类型转换,只要不包含底层 const,都可以使用 static_cast
  • 把一个较大的算术类型赋值给较小的类型(避免编译器警告)
  • 对于编译器无法自动执行的类型转换也有用
    1
    2
    3
    4
    int i = 0;
    void *p = &i;
    // int *dp = p; // 不能自动转换,会报错
    int *dp = static_cast<int *>(p);

dynamic_cast

  • 支持运行时类型识别

const_cast

  • const_cast 只能改变运算对象的底层 const
    1
    2
    const char *pc;
    char *p = const_cast<char*>(pc); // yes, 但是通过 p 写值是为定义的行为
  • 如果一个类本身不是一个常量, 使用强制类型转换获得写权限是合法的行为; 然而如果对象是一个常量, 再使用 const_cast 执行写操作就会产生为定义的后果
  • 只有 const_cast 能改变表达式的常量属性, 使用其他形式的命名强制类型转换改变表达式的常量属性都将引发编译器错误

reinterpret_cast

  • 常用于为运算对象的位模式提供较低层次上的重新解释

命名空间的 using 声明

  • using namespace::name;
  • 头文件不应包含 using 声明

标准库类型 string

  • 默认初始化 string 会得到一个空的 string,该 string 对象中没有任何字符
    1
    std::string str; // str 中没有任何字符
  • 若提供一个字符串字面值,则该字面值中除了最后那个空字符外其他所有的字符都被拷贝到新创建的 string 对象中

定义和初始化 string 对象

string 对象上的操作

  • 当把 string 对象和字符字面值及字符串字面值混在一条语句中使用时,必须确保每个加法运算符的两侧的运算对象至少有一个是 string

处理 string 对象中的字符

标准库类型 vector

  • 模板本身不是类或函数,相反可以将模板看作为编译器生成类或函数编写的一份说明。编译器根据模板创建类或函数的过程称为实例化,当使用模板时,需要指出编译器应把类或函数实例化成何种类型

定义和初始化 vector 对象

1
2
3
4
std::vector<int> v1(10); // 10 个 0
std::vector<int> v2{10}; // 1 个 10
std::vector<int> v3(10, 1); // 10 个 1
std::vector<int> v4{10, 1}; // 10, 1

向 vector 对象中添加元素

其他 vector 操作

  • 相等性运算符和关系运算符依照字典顺序进行比较

迭代器介绍

  • 类似指针类型,迭代器也提供了对对象的间接访问
  • 迭代器有有效和无效之分。有效迭代器要么指向某个元素,要么指向容器中尾元素的下一位置;其余都是无效迭代器

使用迭代器

  • 若容器为空,begin 和 end 返回的是同一个迭代器
  • *iter: 返回元素的引用
  • iter->mem: 等价于 (*iter).mem
  • ++iter / –iter
  • iter1 == iter2 / iter1 != iter2
  • 试图解引用一个非法迭代器或尾后迭代器都是未被定义的行为
  • 如果 vector 中对象是常量,则只能使用 const_iterator;若不是,既能使用 iterator,又能使用 const_iterator
  • 凡是使用了迭代器的循环体,要注意不要轻易改变容器的大小,容易使得迭代器失效

迭代器运算

  • iter + n / iter - n
  • iter += n / iter -= n
  • iter1 - iter2: 测距离
  • = < <=: 比较位置

数组

  • 数组大小确定不变

定义和初始化内置数组

  • 编译时维度要已知

字符数组

1
2
3
4
char a1[] = {'C', '+', '+'}; // 无空字符
char a2[] = {'C', '+', '+', '\0'};
char a3[] = "C++"; // 自动添加空字符
const char a4[6] = "Daniel"; // error,没有空间放空字符

访问数组元素

指针和数组

  • 在大多数表达式中,使用数据类型的对象其实是使用一个指向该数组首元素的指针
    1
    2
    3
    int a[] = {1, 2};
    auto b(a); // b 是一个int*
    b = 42; // error
  • 使用 decltype 时上述转换不会发生,decltype(a) 返回的类型是由 2 个整数构成的数组

C 风格字符串

与旧代码的接口

  • c_str 函数的返回值是一个 C 风格的字符串,是一个指针,指向一个以空字符结束的字符数组,而这个数组所存的数据恰好与那个 string 对象的一样。结果指针的类型是 const char*,从而确保不会改变字符数组的内容
  • 无法保证 c_str 函数返回的数组一直有效,事实上,如果后续的操作改变了 string 的值就可能让之前返回的数组失去效用

多维数组

  • 多维数组其实是数组的数组

基本内置类型

  • C++ 定义了一套包括算术类型空类型在内的基本数据类型。算术类型包括了字符、整形数、布尔值和浮点数;空类型不对应具体的值,仅用于一些特殊的场合

算术类型

  • 整型和浮点型: bool, char, wchar_t, char16_t, char32_t, short, int, long, long long, float, double, long double
  • 尺寸在不同机器上有所差别
  • 除去布尔型和拓展的字符型外,其他整型可以划分为带符号的和无符号的
  • char 类型既可能是无符号类型,又可能是有符号类型,由编译器决定

类型转换

  • no bool –> bool: 初始值为 0,则结果为 false,否则结果为 true
  • bool –> no bool: 初始值为 false,则结果为 0,否则为 1
  • 浮点数 –> 整型数: 近似处理,结果值仅保留浮点数中小数点之前的部分
  • 整型数 –> 浮点数: 小数部分记为 0,如果该整数所占的空间超过了浮点类型的容量,精度可能有损失
  • 当赋给无符号类型一个超出它表示范围的值时,结果是初始值对无符号类型表示数值总数取模后的余数
  • 当赋给带符号类型一个超出它表示范围的值时,结果是为定义的

字面值常量

  • 形如 42 的值被称作字面值常量,这样的值一望而知。每个字面值常量都对应一种数据类系,字面值常量的形式和值决定了它的数据类型

整型和浮点型字面值

  • 整型字面值具体的数据类型由它的值和符号决定
  • 默认情况下,十进制字面值是带符号数,八进制和十六进制字面值既可能是带符号的也可能是无符号的
  • 十进制字面值的类型是 int、long 和 long long 中尺寸最小的那个(前提是这种类型要能容纳下当前的值)
  • 八进制和十六进制字面值的类型是能容纳其数值的 int、unsigned int、long、unsigned long、long long 和 unsigned long long 中的尺寸最小者
  • 如果一个字面值连与之关联的最大的数据类型都放不下,将产生错误
  • 类型 short 没有对应的字面值
  • 尽管整型字面值可以存储在带符号数据类型中,但严格来说,十进制字面值不会是负数。形如 -42 的负十进制字面值,那个负号并不在字面值之内,它的作用仅仅是对字面值取负值而已
  • 浮点型字面值表现为一个小数或以科学计数法表示的指数,其中指数部分用 E 或 e 标识。默认的,浮点型字面值是一个 double 类型

字符和字符串字面值

  • 由单引号括起来的一个字符称为 char 型字面值,双引号括起来的零个或多个字符则构成字符串字面值
  • 字符串字面值的类型实际上是由常量字符构成的数组,编译器在每个字符串的结尾处添加一个空字符(\0)
  • 两个字符串字面值位置紧邻且仅由空格、缩进和换行符分隔,则它们实际上是一个整体
  • 通过添加前缀和后缀,可以改变转型、浮点型和字符型字面值的默认类型
    1
    2
    3
    4
    5
    L'a' // wchar_t
    u8"hi!" // utf-8 字符串字面值(utf-8 用 8 位编码一个 Unicode 字符)
    42ULL // unsigned long long
    1E-3F // float
    3.14L // long double

布尔字面值和指针字面值

  • true 和 false 是布尔类型的字面值
  • nullptr 是指针字面值

变量

  • 变量提供一个具名的、可供程序操作的存储空间

变量定义

  • 初始化不是赋值,初始化的含义是创建变量时赋予其一个初始值,而赋值的含义是把对象的当前值擦除,而以一个新值来替代

列表初始化

  • 即用花括号来初始化变量
  • 当用于内置类型的变量时,使用列表初始化且初始值存在丢失的风险,则编译器将报错
    1
    int a{3.14};

默认初始化

  • 定义变量时没有指定初值,则变量被默认初始化,此时变量被赋予了默认值。默认值到底是什么由变量类型决定,同时定义变量的位置也会对此有影响
  • 内置类型的变量未被显示初始化,它的值由定义的位置决定
    • 定义于任何函数体之外的变量被初始化为 0
    • 定义在函数体内部的内置类型变量将不被初始化
  • 每个类各自决定其初始化对象的方式。而且,是否允许不经初始化就定义对象也有类自己决定。如果类允许这种行为,它将决定对象的初始值到底是什么
  • 绝大多数类都支持无须显式初始化而定义对象,这样的类提供了一个合适的默认值

变量声明和定义的关系

  • 为了支持分离式编译,C++ 语言将声明和定义区分开来。声明使得名字为程序所知(一个文件如果想使用别处定义的名字则必须包含对那个名字的声明);定义负责创建与名字关联的实体
  • 若想要声明一个变量而非定义它,就在变量名前添加关键字 extern,而且不要显式地初始化变量
  • 任何包含了显式初始化的声明即成为定义
  • 变量能且只能被定义一次,但是可以被多次声明

标识符

名字的作用域

  • 作用域中一旦声明了某个名字,它所嵌套着的所有作用域中都能访问该名字。同时,允许在内存作用域中重新定义外层作用域已有的名字

复合类型

  • 指基于其他类型定义的类型

引用

  • 引用为对象起了另外一个名字,引用类型引用另外一种类型
  • 引用一旦初始化完成,将和它的初始化对象一直绑定在一起
  • 由于无法令引用重新绑定到另外一个对象,因此引用必须初始化
  • 引用本身不是一个对象,所以不能定义引用的引用
  • 正常情况下,引用的类型要和与之绑定的对象严格匹配;引用只能绑定在对象上,而不能与字面值或某个表达式的计算结果绑定在一起

指针

  • 指针是指向另外一种类型的符合类型
  • nullptr 是一种特殊类型的字面值,可以被转换成任意其他的指针类型
  • 对于两个类型相同的合法指针,可以用相等操作符 == 或不相等操作符 != 类比较它们,比较的结果是布尔类型。如果两个指针存放的地址值相同,则它们相等;反之它们不相等。相等有三种可能:
    • 都为空
    • 都指向同一个对象
    • 都指向了同一个对象的下一地址
      • 需要注意的是,一个指针指向某对象,同时另一个指针指向另外对象的下一地址,此时也有可能出项这两个指针指相同的情况(不太理解)
  • void* 是一种特殊的指针类型,可用于存放任意对象的地址

与引用的不同点

  • 指针本身就是一个对象,允许对指针赋值和拷贝
  • 指针的生命周期内可以先后指向几个不同的对象
  • 指针无须在定义时赋初值
  • 和其他内置类型一样,在块作用域内定义的指针如果没有初始化,也将拥有一个不确定的值

理解复合类型的声明

  • int *p1, p2; // p1 是指向 int 的指针,p2 是 int
  • 引用本身不是一个对象,因此不能定义指向引用的指针;但指针是对象,所以存在对指针的引用
    1
    2
    3
    int *p;
    int *&r = p; // r 是一个对指针 p 的引用
    // 注: 要理解 r 的类型到底是什么,最简单的办法是从右向左阅读 r 的定义。离变量名最近的符号对变量的类型有最直接的影响,因此 r 是一个引用。声明符的其余部分用以确定 r 引用的类型是什么,此例中的符号 * 说明 r 引用的是一个指针

const 限定符

  • const 对象一旦创建后其值就不能再改变,所以 const 对象必须初始化
    1
    2
    3
    const int i = getSize(); // 正确,运行时初始化
    const int j = 42; // 正确,编译时初始化
    const int k; // 错误
  • 只能在 const 对象上执行不改变其内容的操作
  • 默认情况下,const 对象仅在文件内有效(编译时初始化的 const 变量),编译器将在编译过程中把用到该变量的地方都替换成对应的值。为了执行上述替换,编译器必须知道变量的初始值,如果程序包含多个文件,则每个用了 const 对象的文件都必须得能访问它的初始值才行。所以就必须在每一个用到变量的文件中都有对它的定义。当多个文件中出现了同名的 const 变量时,其实等同于在不同文件中分别定义了独立的变量。
  • 有些 const 变量的初始值不是一个常量表达式,但又想在文件间共享。这种情况下,不希望编译器为每个文件分别生成独立的变量,想让这类 const 对象和其他对象一样,只在一个文件中定义 const,而在多个文件中声明并使用它。解决的办法是对于 const 变量不管是声明还是定义都添加 extern 关键字
    1
    2
    extern const int bufSize = function(); // 源文件中定义
    extern const int bufSize(); // 头文件中声明

const 的引用

  • 把引用绑定到 const 对象上,称之为对常量的引用。(不能修改它所绑定的对象)
    1
    2
    3
    4
    const int ci = 1024;
    const int &r1 = ci;
    r1 = 42; // error
    int &r2 = ci; // error,试图让一个非常量引用指向一个常量对象
  • 引用的类型必须与其引用对象的类型一直,但有两个例外
    • 初始化常量引用时允许用任意表达式作为初始值,只要该表达式的结果能转换成引用的类型就行(允许为一个常量引用绑定非常量的对象、字面值,甚至是个一般表达式)
      1
      2
      3
      4
      5
      int i = 42;
      const int &r1 = 1; // 允许绑定到一个普通对象上
      const int &r2 = 42;
      const int &r3 = r1 * 2;
      int &r4 = r1 * 2; // error,r4 是非常量引用
      • 当一个常量引用被绑定到另外一种类型上时到底发生了什么
        1
        2
        3
        4
        5
        6
        7
        double dval = 3.14;
        const int &ri = dval;
        // 此处 ri 引用了一个 int 类型的数。对 ri 的操作应该是整数运算,但 dval 却是一个 double 而非 int。因此为了确保让 ri 绑定一个整数,编译器把上述代码变成了如下形式:
        const int temp = dval; // 由 double 生成一个临时的 int 常量
        const int &ri = temp; // 让 ri 绑定到这个临时量
        // 在这种情况下,ri 绑定了一个临时量对象。所谓临时量对象就是当编译器需要一个空间来暂存表达式的求值结果时临时创建的一个未命名的对象。
        // 探讨 ri 不是常量时,若执行了类似于上述代码会有什么后果?若 ri 不是常量,就允许对 ri 赋值,这样就会改变 ri 所引用对象的值。但此时绑定的对象是一个临时量而非 dval。程序员既然让 ri 引用 dval,就肯定想通过 ri 改变 dval 的值,否则干什么要给 ri 赋值呢?所以,非常量引用初始化时类型必须一致。
  • 对 const 的引用可能引用一个并非 const 的对象(因为对象也可能是个非常量,所以允许通过其他途径改变它的值)
    1
    2
    3
    4
    5
    6
    int i = 42;
    int &r = i;
    const int &cr = i;
    r = 0;
    std::cout << i << ' ' << r << ' ' << cr << std::endl; // 0 0 0
    // cr = 1; // error

指针和 const

指向常量的指针(const type *)

  • 与引用一样,也可以令指针指向常量或非常量。类似于常量引用,指向常量的指针不能用于改变其所指对象的值。要想存放常量对象的地址,只能使用指向常量的指针
    1
    2
    3
    4
    const double pi = 3.14;
    double *ptr = &pi; // error
    const double *cptr = &pi; // yes
    *cptr = 3; // error
  • 指针的类型必须与其所指对象的类型一致,但是有两个例外
    • 允许令一个指向常量的指针指向一个非常量对象
  • 和常量引用一样,指向常量的指针也没有规定其所指的对象必须是一个常量。所谓指向常量的指针仅仅要求不能通过该指针改变对象的值,而没有规定那个对象的值不能通过其他途径改变

常量指针(type *const)

  • 指针是对象而引用不是,因此就像其他对象类型一样,允许把指针本身定位常量。常量指针必须初始化,而且一旦初始化完成,则它的值(也就是存放在指针中的那个地址)就不能再改变了。
    1
    2
    int a;
    int *const pa = &a; pa 将一直指向 a

顶层 const

  • 指针本身是一个对象,它又可以指向另外一个对象。因此,指针本身是不是常量以及指针所指的是不是一个常量就是两个相互独立的问题。
  • 顶层 const 表示指针本身是个常量
  • 底层const 表示指针所指的对象是个常量
  • 顶层 const 可以表示任意的对象是常量,这一点对任何数据类型都适用
  • 底层 const 则与指针和引用等复合类型的基本类型部分有关
  • 指针类型既可以是顶层 const 也可以是底层 const
    1
    2
    3
    4
    int i = 0;
    int *const p1 = &i; // 不能改变 p1 的值,顶层
    const int ci = 42; // 不能改变 ci 的值,顶层
    const int *p2 = &ci; // 允许改变 p2 的值,底层
  • 当执行对象的拷贝操作时,常量是顶层 const 还是 底层 const 区别明显
    • 顶层 const 不受什么影响(拷贝操作并不会改变被拷贝对象的值,因此,拷入和拷出的对象是否是常量都没什么影响)
    • 底层 const 的限制不能忽视。当执行对象的拷贝操作时,拷入和拷出的对象必须具有相同的底层 const 资格(等号左边的值必须具有底层 const,否则赋值之后没有底层 const 就能修改值了),或者两个对象的数据类型必须能够转换。一般来说,非常量可以转换为常量,反之则不行。
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      int i = 0;
      int *const p1 = &i; // 不能改变 p1 的值,顶层
      const int ci = 42; // 不能改变 ci 的值,顶层
      const int *p2 = &ci; // 允许改变 p2 的值,底层
      const int *const p3 = p2; // 底层 和 顶层
      const int &r = ci; // 底层
      int *p = p3; // error,p3 包含底层 const 定义,而 p 没有
      p2 = p3; // yes,p2 和 p3 都是底层 const
      p2 = &i; // yes,int* 能转换成 const int*
      int &r = ci; // error,普通 int& 不能绑定到 int 常量上
      const int &r2 = i; // yes,const int& 可以绑定到一个普通 int 上
  • const type &var 是顶层 const(const 使得 var 内部的值不能改变,引用使得 var 本身不能改变)。顶层 const 和底层 const 应该是针对指针

constexpr 和常量表达式

  • 常量表达式是指值不会改变并且在编译过程就能得到计算结果的表达式。
  • 显然,字面值属于常量表达式
  • 用常量表达式初始化的 const 对象也是常量表达式
  • 一个对象(或表达式)是不是常量表达式由它的数据类型和初始值共同决定
    1
    2
    3
    4
    const int max_files = 20;        // yes
    const int limit = max_files + 1; // yes
    int size = 27; // no,这是变量
    const int sz = get_size(); // no,get_size()不是常量表达式

constexpr 变量

  • 允许将变量声明为 constexpr 类型以便编译器来验证变量的值是否是一个常量表达式
    • 为了防止用 const 声明时,可能不是有常量赋值的隐藏 bug
    • 声明为 constexpr 的变量一定是一个常量,而且必须用常量表达式初始化
  • 定义于所有函数体之外的对象其地址固定不变,能用来初始化 constexpr 制作
  • constexpr 声明中如果定义了一个指针,限定符 constexpr 仅对指针有效,与指针所指的对象无关(常量指针,不能跟换指针)

处理类型

类型别名

typedef

1
2
3
4
typedef int *IntPtr, Int;
// IntPtr = int*
// Int = int
// 与定义变量一样,星号只对旁边起作用

using

1
using IntPtr = int*;

auto 类型说明符

  • auto 让编译器通过初始值来推算变量的类型
  • 编译器推断出来的 auto 类型有时候和初始值的类型并不完全一样,编译器会适当地改变结果类型使其更符合初始化规则
    • 当引用被当作初始值时,真正参与初始化的其实是引用对象的值。此时编译器以引用对象的类型作为 auto 的类型
      1
      2
      int i = 0, &r = i;
      auto a = r; // a 是 int 类型
    • auto 一般会忽略顶层 const,同时底层 const 则会保留下来
      1
      2
      3
      4
      5
      6
      7
      8
      int i = 0, &r = i;
      auto a = r; // a 是 int 类型
      const int ci = i, &cr = ci, *pi = &ci;
      auto b = ci; // b 是 int 类型
      auto c = cr; // c 是 int 类型(const int &cr 是顶层 const)
      auto d = &i; // d 是 int* 类型
      auto e = &ci; // e 是 const int* 类型
      auto f = pi; // f 是 const int* 类型(const int *pi 是底层 const)
  • 如果希望推断出的 auto 类型是一个顶层 const,需要明确指出
    1
    2
    const int ci = i;
    const auto g = ci; // auto = int, g = const int
  • 还可以将引用的类型设为 auto
    1
    2
    3
    auto &u = ci; // 此时 u 是 const int & 类型(会默认带上 const,若不带上 =,u 就可以改变 ci 的值了,这样是不行的)
    auto &v = 42; // error,不能为非常量引用绑定字面值
    const auto &w = 42; // yes
  • 设置一个类型为 auto 的引用时,初始值中的顶层常量属性依然保留。(eg: u)

decltype 类型指示符

  • 从表达式的类型推断出要定义的变量的类型
  • 编译器分析表达式并得到其类型,却不实际计算表达式的值
  • decltype 处理顶层 const 和引用的方式与 auto 有些不同。若 decltype 使用的表达式是一个变量,则返回该变量的类型(包括顶层 const 和引用在内)
    1
    2
    3
    4
    const int ci = 0, &cj = ci;
    decltype(ci) x = 0; // x = const int
    decltype(cj) y = x; // y = const int &
    decltype(cj) z; // error
  • 引用从来都作为其所指对象的同义词出现,只有用在 decltype 处是个例外
  • 若 decltype 使用的表达式不是一个变量,则 decltype 返回表达式结果对应的类型
    1
    2
    3
    4
    int i = 42, *p = &i, &r = i;
    decltype(r) c = i; // c = int &
    decltype(r + 0) d = i; // d = int
    decltype(*p) e; // error,e = int &
  • 若表达式的内容是解引用,则 decltype 将得到引用类型(解引用指针可以得到指针所指的对象,并且还能给这个对象赋值,因此,decltype(*p) 的结果就是 int&,而非 int)
  • 若 decltype 使用的是一个不加括号的变量,则得到的结果就是该变量的类型;若给变量加上了一层或多层括号,编译器就会把它当成是一个表达式。
    • 变量是一种可以作为赋值语句左值的特殊表达式,所以这样的 decltype 就会得到引用类型
    • decltype((var)) 的结果永远是引用
    • decltype(var) 结果只有当 var 本身就是一个引用时才是引用
      1
      2
      3
      int i = 42;
      decltype(i) a = i; // a = int
      decltype((i)) b = i; // b = int&

初始输入输出

  • 一个流就是一个字符序列,是从 IO 设备读出或写入 IO 设备的