Yu's Blog

do something!

TypeScript

  • TypeScript 是 JavaScript 的一个超集,在 JavaScript 的基础上增加了静态类型检查的超集。可以编译成纯 JavaScript,编译出来的 JavaScript 可以运行在任何浏览器上。

基础类型

string

1
let name: string = "Alice";

number

1
let age: number = 30;

boolean

1
let isDone: boolean = true;

array

1
let list: number[] = [1, 2, 3];

tuple

1
2
// 示已知类型和长度的数组
let person: [string, number] = ["Alice", 30];

enum

1
2
// 定义一组命名常量
enum Color { Red, Green, Blue };

any

1
2
// 任意类型,不进行类型检查
let value: any = 42;

void

1
2
// 无返回值(常用于函数)
function log(): void {}

null

1
2
// 表示空值
let empty: null = null;

undefined

1
2
// 表示未定义
let undef: undefined = undefined;

never

1
2
// 表示不会有返回值
function error(): never { throw new Error("error"); }

object

1
2
// 表示非原始类型
let obj: object = { name: "Alice" };

union

1
2
3
4
// 表示一个变量可以是多种类型之一
let id: string | number;
id = "123";
id = 456;

unknown

1
2
// 不确定类型,需类型检查后再使用
let value: unknown = "Hello";

变量声明

1
var [变量名] : [类型] = 值;
  • 当类型没有给出时,TypeScript 编译器利用类型推断来推断类型。

作用域

1
2
3
4
5
6
7
8
9
10
11
12
13
var global_num = 12          // 全局变量
class Numbers {
num_val = 13; // 实例变量
static sval = 10; // 静态变量

storeNum():void {
var local_num = 14; // 局部变量
}
}
console.log("全局变量为: "+global_num)
console.log(Numbers.sval) // 静态变量
var obj = new Numbers();
console.log("实例变量: "+obj.num_val)

函数

1
2
3
4
function function_name(param1[:type],param2[:type] = default_value):return_type { 
// 语句
return value;
}

匿名函数

1
2
3
4
5
6
var res = function( [arguments] ) { ... }
// 自调用
(function () {
var x = "Hello!!";
console.log(x)
})()

Map

1
2
3
4
5
6
7
8
9
10
let map = new Map([
["key1", "value1"],
["key2", "value2"]
]);
map.set('key1', 'value1');
const value = map.get('key1');
const exists = map.has('key1');
const removed = map.delete('key1');
map.clear();
const size = map.size;

接口

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
interface IPerson { 
firstName:string,
lastName:string,
sayHi: ()=>string
}

var customer:IPerson = {
firstName:"Tom",
lastName:"Hanks",
sayHi: ():string =>{return "Hi there"}
}

console.log("Customer 对象 ")
console.log(customer.firstName)
console.log(customer.lastName)
console.log(customer.sayHi())

var employee:IPerson = {
firstName:"Jim",
lastName:"Blakes",
sayHi: ():string =>{return "Hello!!!"}
}

console.log("Employee 对象 ")
console.log(employee.firstName)
console.log(employee.lastName)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
class Car { 
// 字段
engine:string;

// 构造函数
constructor(engine:string) {
this.engine = engine
}

// 方法
disp():void {
console.log("函数中显示发动机型号 : "+this.engine)
}
}

// 创建一个对象
var obj = new Car("XXSY1")

// 访问字段
console.log("读取发动机型号 : "+obj.engine)

// 访问方法
obj.disp()

泛型

1
2
3
4
5
6
7
8
9
10
11
12
function identity<T>(arg: T): T {
return arg;
}

interface KeyValuePair<K, V> {
key: K;
value: V;
}

function printArray<E>(arr: E[]): void {
arr.forEach(item => console.log(item));
}

模块

1
2
3
4
/// <reference path = "IShape.ts" /> 
export interface IShape {
draw();
}
1
2
3
4
5
6
import shape = require("./IShape"); 
export class Circle implements shape.IShape {
public draw() {
console.log("Cirlce is drawn (external module)");
}
}
1
2
3
4
5
6
import shape = require("./IShape"); 
export class Triangle implements shape.IShape {
public draw() {
console.log("Triangle is drawn (external module)");
}
}
1
2
3
4
5
6
7
8
9
10
import shape = require("./IShape"); 
import circle = require("./Circle");
import triangle = require("./Triangle");

function drawAllShapes(shapeToDraw: shape.IShape) {
shapeToDraw.draw();
}

drawAllShapes(new circle.Circle());
drawAllShapes(new triangle.Triangle());

控制内存分配

重载 new 和 delete

定位 new 表达式

运行时类型识别

dynamic_cast 运算符

typeid 运算符

使用 RTTI

type_info 类

枚举类型

类成员指针

数据成员指针

成员函数指针

将成员函数用作可调用对象

嵌套类

union: 一种节省空间的类

局部类

固有而不可移植的特性

位域

volatile 限定符

链接指示: extern “C”

异常处理

抛出异常

捕获异常

函数try 语句块与构造函数

noexcept 异常说明

异常类层次

命名空间

命名空间定义

使用命名空间成员

类、命名空间与作用域

重载与命名空间

多重继承与虚继承

多重继承

类型转换与多个基类

多重继承下的类作用域

虚继承

构造函数与虚继承

tuple 类型

定义和初始化 tuple

使用 tuple 返回多个值

bitset 类型

定义和初始化 bitset

bitset 操作

正则表达式

使用正则表达式库

匹配和 Regex 迭代器类型

使用子表达式

使用 regex_replace

随机数

随机数引擎和分布

其他随机数分布

IO 库再探

格式化输入与输出

未格式化的输入/输出操作

流随机访问

  • 面向对象编程和泛型编程都能处理在编写程序时不知道类型的情况。不同之处在于:OOP 能处理类型在程序运行之前都未知的情况;而在泛型编程中,在编译时就能获知类型了。

定义模板

  • 除了定义类型参数,还可以在模板中定义非类型参数,一个非类型参数表示一个值而非一个类型。通过一个特定的类型名而非关键字 class 或 typename 来指定非类型参数。一个非类型参数可以是一个整型,或者是一个指向对象或函数类型的指针或(左值)引用。绑定到非类型整型参数的实参必须是一个常量表达式。绑定到指针或引用非类型参数的实参必须具有静态的生存期。
  • 当编译器遇到一个模板定义时,它并不生成代码。只有当我们实例化出模板的一个特定版本时,编译器才会生成代码。(这一特性影响了我们如何组织代码以及错误何时被检测到)
  • 为了生成一个实例化版本,编译器需要掌握函数模板或类模板成员函数的定义。因此,与非模板代码不同,模板的头文件通常既包括声明也包括定义。

函数模板

类模板

  • 与函数模板的不同之处是,编译器不能为类模板推断模板参数类型。为了使用类模板,我们必须在模板名后的尖括号中提供额外信息,用来代替模板参数的模板实参列表。
  • 定义在类模板内的成员函数被隐式声明为内联函数
  • 默认情况下,一个类模板的成员函数只有当程序用到它时才进行实例化
  • 在一个类模板的作用域内,可以直接使用模板名而不必制定模板实参

模板参数

成员模板

控制实例化

效率与灵活性

模板实参推断

类型转换与模板类型参数

函数模板显式实参

尾置返回类型与类型转换

函数指针和实参推断

模板你实参推断和引用

理解 std::move

转发

重载与模板

可变参数模板

编写可变参数函数模板

包拓展

转发参数包

模板特例化

OOP: 概念

  • 面向对象程序设计的核心思想是数据抽象、继承和动态绑定
  • 在 C++ 语言中,使用基类的引用(或指针)调用一个虚函数时将发生动态绑定

定义基类和派生类

定义基类

  • 基类通常都应该定义各虚析构函数,即使该函数不执行任何实际操作也是如此
  • 成员函数如果没被声明为虚函数,则其解析过程发生在编译时而非运行时

定义派生类

  • 派生类经常(但不总是)覆盖它继承的虚函数。如果派生类没有覆盖其基类中的某个虚函数,则该虚函数的行为类似于其他的普通成员,派生类会直接继承其在基类中的版本
  • 在一个对象中,继承自基类的部分和派生类自定义的部分不一定是连续存储的
  • 每个类控制它自己的成员初始化过程,派生类也必须使用基类的构造函数来初始化它的基类部分
  • 除非特别指出,否则派生类对象的基类部分就会像数据成员一样执行默认初始化。如果想使用其他的基类构造函数,需要以类名加圆括号内的实参列表的形式为构造函数提供初始值
  • 首先初始化基类的部分,然后按照声明的顺序依次初始化派生类的成员
  • 如果基类定义了一个静态成员,则在整个继承体系中只存在该成员的唯一定义
  • 静态成员遵循通用的访问控制规则,如果基类中的成员是 private 的,则派生类无权访问它。假设静态成员是可访问的,则既能通过基类使用它也能通过派生类使用它

类型转换与继承

  • 如果表达式既不是引用也不是指针,则它的动态类型永远与静态类型一致
  • 如果已知某个基类向派生类的转换是安全的,则可以使用 static_const 来强制覆盖掉编译器的检查工作。否则,使用 dynamic_cast

虚函数

  • 当某个虚函数通过指针或引用调用时,编译器产生的代码直到运行时才能确定应该调用哪个版本的函数
  • 当通过一个具有普通类型(非引用非指针)的表达式调用虚函数时,在编译时就会将调用的版本确定下来
  • 如果虚函数使用默认实参,则基类和派生类中定义的默认实参最好一致

抽象基类

  • 含有(或者未经覆盖直接继承)纯虚函数的类是抽象基类
  • 不能创建抽象基类的对象
  • 派生类构造函数只初始化它的直接基类

访问控制与继承

  • protected 关键字来声明那些它希望与派生类分享但是不想被其他公共访问使用的成员
  • 就像友元关系不能传递一样,友元关系同样也不能继承。基类的友元在访问派生类成员时不具有特殊性,类似的,派生类的友元也不能随意访问基类的成员
  • 有时需要改变派生类的某个名字的访问级别,通过使用 using 声明

继承中的类作用域

  • 派生类的作用域位于基类作用域之内
  • 和其他作用域一样吗,派生类也能重用定义在其直接基类或间接基类中的名字,此时定义在内层作用域(即派生类)的名字将隐藏定义在外层作用域(即基类)的名字
  • 可以使用作用域运算符来使用一个被隐藏的基类成员
  • 名字查找优先与类型检查。
  • 声明在内层作用域的函数并不会重载声明在外层作用域的函数,因此,定义派生类中的函数也不会重载其基类中的成员。如果派生类的成员与基类的某个成员同名,则派生类将在其作用域内隐藏该基类成员。即使派生类成员和基类成员的形参列表不一致,基类成员也仍然会被隐藏掉
  • 如果派生类希望所有的重载版本对于它来说都是可见的,那么它就需要覆盖所有的版本,或者一个也不覆盖。有时一个类仅需覆盖重载集合中的一些而非全部函数,此时,如果我们不得不覆盖基类中的每一个版本的话,显然操作将极其烦琐。一种好的解决方案是为重载的成员提供一条 using 声明语句,这样就无需覆盖基类中的每一个重载版本类。

构造函数与拷贝控制

虚析构函数

  • 如果基类的析构函数不是虚函数,则 delete 一个指向派生类对象的基类指针将产生未定义的行为。

合成拷贝控制与继承

  • 当派生类定义了拷贝或移动操作时,该操作负责拷贝或移动包括基类部分成员在内的整个对象

派生类的拷贝控制成员

如果构造函数或析构函数调用了某个虚函数,则我们应该执行与构造函数或析构函数所属类型相对应的虚函数版本

继承的构造函数

容器与继承

  • 当派生类对象被赋值给基类对象时,其中的派生类部分将被切掉,因此容器和存在基础关系的类型无法兼容
  • 当希望在容器中存放具有基础关系的对象时,实际上存放的通常是基类的指针。

基本概念

  • 重载运算符也包括返回类型、参数列表以及函数体
  • 重载运算符函数的参数数量与该运算符作用的运算对象数量一样多
  • 当一个重载的运算符是成员函数时,this 绑定到左侧运算对象。成员运算符函数的(显式)参数数量比运算对象的数量少一个
  • 对于一个运算符来说,它或者是类的成员,或者至少含有一个类类型的参数
  • 当运算符作用于内置类型的运算对象时,无法改变该运算符的含义
  • 既可以使用运算符的形式调用,也能像普通函数一样直接调用运算符函数
    1
    2
    data1 + data2; // 普通的表达式
    operator+(data1, data2); // 等价的函数调用
  • 某些运算符指定了运算对象求值的顺序。因为使用重载的运算符本质上是一次函数调用,所以这些关于运算对象求值顺序的规则无法应用到重载的运算符上(因此不建议重载逗号、取地址、逻辑与和逻辑或运算符)
  • 选择作为成员或者非成员
    • 赋值、下标、调用和成员访问箭头运算符必须是成员
    • 复合赋值运算符一般来说应该是成员,但并非必须,这一点与赋值运算符略有不同
    • 改变对象状态的运算符或者与给定类型密切相关的运算符,如递增、递减和解引用运算符,通常应该是成员
    • 具有对称的运算符可能转换任意一端的运算对象,例如算法、相等性、关系和位运算符,因此它们通常应该是普通的非成员函数

输入和输出运算符

  • 输入输出运算符必须是非成员函数,否则其左侧运算对象将是类的一个对象,不合规

重载输出运算符 <<

  • 通常情况下,输出运算符的第一个形参是一个非常量 ostream 对象的引用。之所以 ostream 是非常量是因为向流写入内容会改变状态;而该形参是引用是因为我们无法直接复制一个 ostream 对象
  • 第二个形参一般来说是一个常量的引用
  • 为了与其他输出运算符保持一致,operator<< 一般要返回他的 ostream 形参
    1
    2
    3
    4
    ostream &operator<<(ostream &os, const Test t) {
    os << t.data;
    return os;
    }
  • 通常,输出运算符应该主要负责打印对象的内容而非控制格式,也不应该打印换行符(和内置输出运算符保持一致)

重载输入运算符 >>

  • 通常,输入运算符的第一个形参是运算符将要读取的流的引用,第二个形参是将要读入到的(非常量,因为读入数据会使其改变)对象的引用
    1
    2
    3
    4
    istream &operator>>(istream &is, Test t) {
    is >> t.data;
    return is;
    }
  • 输入运算符应该要处理输入可能失败的情况,而输出运算符不需要

算术和关系运算符

  • 通常把算术和关系运算符定义成非成员函数以允许对左侧或右侧的运算对象进行转换。
  • 这两个运算符一般不需要改变运算对象的状态,所以形参都是常量的引用
    1
    2
    3
    4
    5
    Test operator+(const Test &t1, const Test &t2) {
    Test t = t1;
    t.data += t2.data;
    return t;
    }

相等运算符

1
2
3
4
5
6
bool operator==(const Test &l, const Test &r) {
return l.data == r.data;
}
bool operator!=(const Test &l, const Test &r) {
return !(l == r);
}

关系运算符

  • 如果存在唯一一种逻辑可靠的 < 定义,则应该考虑为这个类定义 < 运算符。
  • 如果类同时还包含 ==,则当且仅当 < 的定义和 == 产生的结果一致时才定义 < 运算符

赋值运算符

  • 类还可以定义其他赋值运算符以使用别的类型作为右侧运算对象(必须定义为成员函数)

复合赋值运算符

  • 复合赋值运算符不非得是类的成员,不过倾向于把包括复合赋值在内的所有赋值运算都定义在类的内部
    1
    2
    3
    4
    Test& Test::operator+=(const Test &r) {
    data += r.data;
    return *this;
    }

下标运算符

  • 表示容器的类通常可以通过元素在容器中的位置访问元素,这些类一般会定义下标运算符 operator[]
  • 下标运算符必须是成员函数
  • 为了与下标的原始定义兼容,下标运算符通常以所访问元素的引用作为返回值,这样做的好处是下标可以出现在赋值运算符的任意一端
  • 最好同时定义下标运算符的常量版本和非常量版本,当作用于一个常量对象时,下标运算符返回常量引用以确保不会给返回的对象赋值
    1
    2
    3
    4
    5
    6
    7
    int operator[](std::size_t n) {
    return data[n];
    }

    const int operator[](std::size_t n) const {
    return data[n];
    }

递增和递减运算符

  • 对于内置类型来说,递增和递减运算符既有前置版本也有后置版本。因此我们也应该定义两个版本

前置递增 / 递减运算符

  • 为了与内置类型一致,前置运算符应该返回递增或递减后对象的引用
    1
    2
    3
    4
    5
    6
    7
    8
    Test& Test::operator++() {
    ++data;
    return *this;
    }
    Test& Test::operator--() {
    --data;
    return *this;
    }

区分前置和后置运算符

  • 后置版本接受一个额外的(不被使用)int 类型的形参
  • 为了与内置版本一致,后置运算符应该返回对象的原值(递增或递减之前的值),返回的形式是一个值而非引用
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    Test Test::operator++(int) {
    Test t = *this;
    ++*this;
    return t;
    }

    Test Test::operator--(int) {
    Test t = *this;
    --*this;
    return t;
    }

成员访问运算符

  • 箭头运算符必须是类的成员;解引用运算符通常也是类的成员,尽管并非必须如此
    1
    2
    3
    4
    5
    6
    7
    8
    int & operator*() const {
    auto p = ...;
    return (*p)[curr];
    }

    int * operator->() const {
    return & this->operator*();
    }
  • 重载的箭头运算符必须返回类的指针或者自定义了箭头运算符的某个类的对象,point->mem 执行过程如下
    • 如果 point 是指针,则应用内置的箭头运算符,表达式等价于(*point).mem。首先解引用指针,然后从所得的对象中获取指定的成员。如果 point 所指的类型没有名为 mem 的成员,持续会发生错误
    • 如果 point 是定义了 operator-> 的类的一个对象,则我们使用 point.operator->()的结果来获取 mem,其中,如果该结果是一个指针,则执行第一步;如果该结果本身含有重载的 operator->(),则重复调用当前步骤。最终,当这一过程结束时程序或者返回了所需的内容,或者返回一些表示程序错误的信息

函数调用运算符

  • 如果类重载了函数调用运算符,则我们可以像使用函数一样使用该类的对象。因为这样的类同时也能存储状态,所以与普通函数相比它们更加灵活
  • 函数调用运算符必须是成员函数。一个类可以定义多个不同版本的调用运算符,互相之间应该在参数数量或类型上有所区别
  • 如果类定义了调用运算符,则该类的对象称作函数对象
    1
    2
    3
    4
    5
    6
    7
    8
    9
    class Test {
    public:
    void operator()(int n) {
    std::cout << n << std::endl;
    }
    };

    Test t;
    t(1);

lambda 是函数对象

  • 当我们编写了一个 lambda 后,编译器将该表达式翻译成一个未命名类的未命名对象。在 lambda 表达式产生的类中含有一个重载的函数调用运算符

标准库定义的函数对象

可调用对象与 fuction

  • C++ 语言有几种可调用对象:函数、函数指针、lambda 表达式、bind 创建的对象以及重载了函数调用运算符的类
  • 和其他对象一样,可调用的对象也有类型。例如,每个 lambda 有它自己唯一的(未命名)类类型;函数及函数指针的类型则由其返回值类型和实参类型决定,等等
  • 两个不同类型的可调用对象却可能共享同一种的调用形式。调用形式指明了调用返回的类型以及传递给调用的实参类型。一种调用形式对应一个函数类型
  • function 是一个模板,需要传递能够表示的对象的调用形式,例如:function<int(int,int)>
  • 不能(直接)将重载函数的名字存入 function 类型的对象中
    • 法一:存储函数指针(通过参数区分),而非函数的名字
    • 法二:使用 lambda 包裹

重载、类型转换与运算符

类型转换运算符

  • 类的一种特殊成员函数,负责将一个类类型的值转换成其他类型,一般形式如下,type 表示某种类型
    1
    2
    3
    operator type() const {
    return value;
    }
  • 类型转换运算符可以面向任意类型(除了 void)进行定义,只要该类型能作为函数的返回类型。因此,不允许转换成数组或者函数类型,但允许转换成指针(包括数组指针及函数指针)或者引用类型
  • 一个类型转换函数必须是类的成员函数;不能声明返回类型,形参列表也必须为空。且通常函数是 const 类型
  • 为了防止编译器自动进行类型转换,可以将函数设置为 explicit
  • 向 bool 的类型转换通常用在条件部分,因此 operator bool 一般定义为 explicit

避免有二义性的类型转换

函数匹配与重载运算符

拷贝、赋值与销毁

  • 拷贝控制操作
    • 拷贝构造函数
    • 拷贝赋值运算符
    • 移动构造函数
    • 移动赋值运算符
    • 析构函数
  • 如果一个类没有定义所有这些拷贝控制成员, 编译器会自动为它定义缺失的操作

拷贝构造函数

  • 如果一个构造函数的第一个参数是自身类类型的引用, 且任何额外参数都有默认值, 此构造函数是拷贝构造函数
  • 拷贝构造函数在几种情况下都会被隐式调用, 因此, 拷贝构造函数通常不应该是 explicit 的

合成拷贝构造函数

  • 与合成默认构造函数不同, 即使我们定义了其他构造函数, 编译器也会为我们合成一个拷贝构造函数
  • 一般情况下, 合成的拷贝构造函数会将其参数的成员逐个拷贝到正在创建的对象中, 编译器从给定对象中依次将每个非 static 成员拷贝到正在创建的对象中
  • 每个成员的类型决定了它如何拷贝: 对类类型的成员, 会使用其拷贝构造函数来拷贝; 内置类型的成员则直接拷贝
  • 虽然我们不能直接拷贝一个数组, 但合成拷贝构造函数会逐元素地拷贝一个数组类型的成员, 如果数组元素是类类型, 则使用元素的拷贝构造函数来进行拷贝
    1
    2
    3
    4
    5
    6
    7
    8
    9
    class Test {
    public:
    int a[2] = {0,0};
    };

    Test t1;
    t1.a[0] = 1;
    Test t2(t1);
    std::cout << t1.a[0] << " " << t2.a[0] << std::endl; // 1 1

参数和返回值

  • 在函数调用过程中, 具有非引用类型的参数要进行拷贝初始化
  • 当一个函数具有非引用的返回类型时, 返回值会被用来初始化调用方的结果
  • 拷贝构造函数被用来初始化非引用类类型参数, 这一特性解释了为什么拷贝构造函数自己的参数必须是引用类型

编译器可以绕过拷贝构造函数

  • 在拷贝初始化过程中, 编译器可以(但不是必须)跳过拷贝\移动构造函数, 直接创建对象
    1
    2
    std::string str = "yzy"; // 拷贝初始化
    std::string str("yzy"); // 编译器略过了拷贝构造函数
  • 即使编译器略过了拷贝/移动构造函数, 但在这个程序点上, 拷贝/移动构造函数必须是存在且可访问的(例如, 不能是 private 的)

拷贝赋值运算符

  • 重载运算符本质上是函数, 其名字由 operator 关键字后接表示要定义的运算符的符号组成. 因此, 赋值运算符就是一个名为 operator= 的函数
  • 某些运算符, 包括赋值运算符, 必须定义为成员函数
  • 如果一个运算符是一个成员函数, 其左侧运算对象就绑定到隐式的 this 参数
  • 拷贝赋值运算符接受一个与其所在类相同类型的参数. 为了与内置类型的赋值保持一致, 赋值运算符通常返回一个指向其左侧运算对象的引用

析构函数

  • 由于析构函数不接受参数, 因此不能被重载
  • 在一个构造函数中, 成员的初始化是在函数体执行之前完成的, 且按照它们在类中出现的顺序进行初始化; 在一个析构函数中, 首先执行函数体, 然后销毁成员, 成员按初始化顺序的逆序销毁
  • 对于临时对象, 当创建它的完整表达式结束时被销毁

三/五法则

  • 需要析构函数的类也需要拷贝和赋值操作
  • 需要拷贝操作的类也需要赋值操作, 反之亦然

使用 =default

  • 可以通过将拷贝控制成员定义为 =default 来显式地要求编译器生成合成的版本
  • 在类内用 =default 修饰成员时, 合成的函数将隐式地声明为内联的; 若不希望合成的成员是内联函数, 应该只对成员的类外定义使用 =default

阻止拷贝

  • 虽然大多数类应该定义拷贝构造函数和拷贝赋值运算符, 但对某些类来说, 这些操作没有合理的意义(eg: iostream 类阻止了拷贝, 以避免多个对象写入或读取相同的 IO 缓冲)
  • 为了阻止拷贝, 看起来可能应该不定义拷贝控制成员. 但是, 这种策略是无效的(若类未定义这些操作, 编译器为它生成合成的版本)

定义删除的函数

  • 在函数的参数列表后面加上 =delete 来指出我们希望将它定义为删除的
  • 对于析构函数已删除的类型, 不能定义该类型的变量, 但可以定义该类型动态分配的对象(但是不能释放这些对象)

合成的拷贝控制成员可能是删除的

  • 如果类的某个成员的析构函数是删除的或不可访问的, 则类的合成析构函数被定义为删除的
  • 如果类的某个成员的拷贝构造函数是删除的或不可访问的, 则类的合成拷贝构造函数被定义为删除的; 如果类的某个成员的析构函数是删除的或不可访问的, 则类合成的拷贝构造函数也被定义为删除的
  • 如果类的某个成员的拷贝赋值运算符是删除或不可访问的, 或是类有个 const 的或引用成员, 则类的合成拷贝赋值运算符被定义为删除的
  • 如果类的某个成员的析构函数是删除的或不可访问的, 或是类有一个引用成员, 它没有类内初始化器, 或是类有一个 const 成员, 它没有类内初始化器且其类型未显示定义默认构造函数, 则该类的默认构造函数被定义为删除的
  • 本质上: 如果一个类有数据成员不能默认构造, 拷贝, 复制或销毁, 则对应的成员函数将被定义为删除的

private 拷贝控制

  • 老版本是通过将拷贝构造函数和拷贝赋值运算符声明为 private 的来阻止拷贝(新版本应该使用 =delete)

拷贝控制和资源管理

行为像值的类

定义行为像指针的类

交换操作

  • 如果一个类定义了自己的 swap, 那么 swap 算法函数将使用类自定义版本

拷贝控制实例

动态内存管理类

对象移动

  • 在某些情况下, 对象拷贝后就立即被销毁了, 在这些情况下, 移动而非拷贝对象会大幅度提升性能
  • 使用移动而不是拷贝另一个原因源于 IO 类或 unique_ptr 这样的类, 这些类都包含不能被共享的资源(如 IO 缓冲或指针), 因此, 这些类型的对象不能拷贝但可以移动

右值引用

  • 所谓右值引用就是必须绑定到右值的引用, 通过 && 而不是 & 来获得右值引用
  • 右值引用有一个重要的性质: 只能绑定到一个将要销毁的对象
  • 一个左值表达式表示的是一个对象的身份, 而一个右值表达式表示的是对象的值
  • 类似任何引用, 一个右值引用也不过是某个对象的另一个名字而已
  • 不能将一个右值引用绑定到一个左值上
  • 左值有持久的状态, 而右值要么是字面常量, 要么是在表达式求值过程中创建的临时对象
  • 右值引用指向将要被销毁的对象, 因此, 我们可以从绑定到右值引用的对象窃取状态
  • 变量是左值, 因此不能将一个右值引用直接绑定到一个变量上, 即使这个变量是右值引用类型也不行

标准库 move 函数

  • 虽然不能将一个右值引用直接绑定到一个左值上, 但可以显式地将一个左值转换为对应的右值引用类型, 还可以通过调用一个名为 move 的新标准库函数来获得绑定到左值上的右值引用
  • 可以销毁一个移后原对象, 也可以赋予它新值, 但不能使用一个移后源对象的值
  • 使用 move 的代码应该使用 std::move 而不是 move, 这样可以避免潜在的名字冲突

移动构造函数和移动赋值运算符

  • 由于移动操作窃取资源, 通常不分配任何资源. 因此, 移动操作通常不会抛出任何异常(当编写一个不抛出异常的移动操作时, 应该将此事通知标准库, 避免标准库做一些额外的工作)
  • 必须在类头文件的声明和定义中都指定 noexcept
  • 在移动操作之后, 移后原对象必须保持有效的, 可析构的状态, 但是用户不能对其值进行任何假设
  • 只有当一个类没有定义任何自己版本的拷贝控制成员, 且它的所有数据成员都能移动构造或移动赋值时, 编译器才会为它合成移动构造函数或移动赋值运算符
  • 与拷贝操作不同,移动操作永远不会隐式定义为删除的函数。若显式地要求编译器生成 =default 的移动操作,且编译器不能移动所有成员,则编译器会将移动操作定义为删除的函数
  • 将合成的移动操作定义为删除的函数需满足:
    • 有类成员定义了自己的拷贝构造函数且未定义移动构造函数,或者是有类成员未定义自己的拷贝构造函数且编译器不能为其合成移动构造函数。移动赋值运算符的情况类似
    • 如果有类成员的移动构造函数或移动赋值运算符被定义为删除的或是不可访问的,则类的移动构造函数或移动赋值运算符被定义为删除的
    • 类似拷贝构造函数,如果类的析构函数被定义为删除的或不可访问的,则类的移动构造函数被定义为删除的
    • 类似拷贝赋值运算符,如果有类成员是 const 的或是引用,则类的移动赋值运算符被定义为删除的
  • 定义了一个移动构造函数或移动赋值运算符的类必须也定义自己的拷贝操作,否则,这些成员默认被定义为删除的
  • 如果一个类有一个可用的拷贝构造函数而没有移动构造函数,则其对象是通过拷贝构造函数来“移动”的。拷贝赋值运算符和移动赋值运算符的情况类似

右值引用和成员函数

  • 区分移动和拷贝的重载函数通常有一个版本接受一个 const T&,而另一个版本接受一个 T&&
    1
    2
    void push_back(const std::string&);
    void push_back(std::string&&);

右值和左值引用成员函数

  • 引用限定符可以是 & 或 &&,分别指出 this 可以指向一个左值或右值。类似 const 限定符,引用限定符只能用于(非 const)成员函数,且必须同时出现在函数的声明和定义中。(一个函数可以同时用 const 和引用限定,在此情况下,引用限定必须跟随在 const 限定符之后)
    1
    2
    3
    4
    class Foo{
    public:
    Foo &operator=(const Foo&) &; // 只能向可修改的左值赋值
    };

重载和引用函数

  • 就像一个成员函数可以根据是否有 const 来区分重载版本一样,引用限定符也可以区分重载版本
  • 如果一个成员函数有引用限定符,则具有相同参数列表的所有版本都必须有引用限定符

动态内存与智能指针

  • 默认初始化的智能指针中保存着一个空指针
  • 智能指针的使用方式与普通指针类似
  • 解引用一个智能指针返回它指向的对象
  • 如果在一个条件判断中使用智能指针, 效果就是检测它是否为空

shared_ptr 类

  • p->get(): 返回 p 中保存的指针(要小心使用, 若智能指针释放了其对象, 返回的指针所指向的对象也就消失了)
  • swap(p, q) \ p.swap(q): 交换 p 和 q 中的指针
  • make_shared(args): 返回一个 shared_ptr, 指向一个动态分配的类型为 T 的对象. 使用 args 初始化此对象
  • shared_ptrp(q): p 是 shared_ptr q 的拷贝, 此操作会递增 q 中的计数器. q 中的指针必须能转换为 T*
  • p = q: p 和 q 都是 shared_ptr, 所保存的指针必须能互相转换. 此操作会递减 p 的引用计数, 递增 q 的引用计数; 若 p 的引用计数变为 0, 则将其管理的原内存释放
  • p.use_count(): 返回与 p 共享对象的智能指针数量(可能很慢, 主要用于调试)
  • p.unique(): 若 p.use_count() 为 1, 返回 true, 否则返回 false

直接管理内存

shared_ptr 和 new 结合使用

  • std::shared_ptr p(new int{1024});
  • 不能将一个内置指针隐式转换为一个智能指针, 必须使用直接初始化形式
  • std::shard_ptr p(u): p 从 unique_ptr u 那里接管了对象的所有权, 将 u 置为空
  • shared_ptrp(q, d): p 接管了内置指针 q 所指的对象的所有权. q 必须能转换为 T* 类型. p 将使用可调用对象 d 来代替 delete
  • shared_ptr p(p2, d): p 是 shared_ptr p2 的拷贝, 为一的区别是 p 将用可调用对象 d 来代替 delete
  • p.reset() \ p.reset(q) \ p.reset(q, d): 若 p 是唯一指向其对象的 shared_ptr, reset 会释放此对象, 若传递了可选的参数内置指针 q, 会令 p 指向 q, 否则会将 p 置为空. 若还传递了参数 d, 将会调用 d 而不是 delete 来释放 q

智能指针和异常

unique_ptr

  • std::unique_ptrp(new int{1024});
  • unique_ptr 拥有它指向的对象, 因此不支持普通的拷贝或赋值操作
  • std::unique_ptr<T, D> u: 使用一个类型为 D 的可调用对象来释放指针
  • std::unique_ptr<T, D> u(d): 用类型为 D 的对象 d 代替 delete
  • u = nullptr: 释放 u 指向的对象, 将 u 置为空
  • u.release(): u 放弃对指针的控制器, 返回指针, 并将 u 置为空
  • u.reset() \ u.reset(q) \ u.reset(nullptr): 释放 u 指向的对象, 如果提供了内置指针 q, 令 u 指向这个对象; 否则将 u 置为空
  • 不能拷贝 unique_ptr 的规则有一个例外: 可以拷贝或赋值一个将要销毁的 unique_ptr, 最常见的例子是从函数返回一个 unique_ptr
    1
    2
    3
    4
    5
    6
    7
    8
    9
    std::unique_ptr<int> clone(int p) {
    return std::unique_ptr<int>(new int{p});
    }

    std::unique_ptr<int> clone(int p) {
    std::unique_ptr<int> ret(new int{p});
    ...
    return ret;
    }

weak_ptr

  • weak_ptr 是一种不控制所指向对象生存期的智能指针, 它指向一个 shared_ptr 管理的对象
  • 将一个 weak_ptr 绑定到一个 shared_ptr 不会改变 shared_ptr 的引用计数
  • 一旦最后一个指向对象的 shared_ptr 被销毁, 对象就会被释放, 即使有 weak_ptr 指向对象, 对象也还是会被释放
  • weak_ptr w(sp): 与 shared_ptr sp 指向相同对象的 weak_ptr, T 必须能转换为 sp 指向的类型
  • w = p: p 可以是一个 shared_ptr 或一个 weak_ptr
  • w.reset(): 将 w 置为空
  • w.use_count(): 与 w 共享对象的 shared_ptr 的数量
  • w.expired(): 若 w.use_count() 为 0, 返回 true, 否则返回 false
  • w.lock(): 如果 expired 为 true, 返回一个空 shared_ptr, 否则返回一个指向 w 的对象的 shared_ptr(由于对象可能不存在, 不要直接使用 weak_ptr 访问对象, 而必须调用 lock)

动态数组

new 和数组

  • type *p = new type[size];
  • delete [] p;
  • 动态分配一个空数组是合法的
    1
    2
    char arr[0]; // error: 不能定义长度为 0 的数组
    char *cp = new char[0]; // yes, 但 cp 不能解引用
  • 标准库提供了一个可以管理 new 分配的数组的 unique_ptr 版本(可以使用下标运算符来访问数组中的元素)
    1
    2
    std::unique_ptr<int[]> up(new int[10]);
    up.release();
  • shared_ptr 不直接支持管理动态数组. 如果希望使用 shared_ptr 管理一个动态数组, 必须提供自己定义的删除器. 如果为定义删除器, 则代码是为定义的, 因为默认情况下, shared_ptr 使用 delete 销毁它所指向的对象. 如果为定义删除器, 则代码是为定义的, 因为默认情况下, shared_ptr 使用 delete 销毁它所指向的对象.(shared_ptr 未定义下标运算符, 而且智能指针类型不支持指针算术运算, 因此, 为了访问数组中的元素, 必须使用 get 获取一个内置指针去访问)
    1
    2
    std::unique_ptr<int> sp(new int[10], [](int *p){ delete [] p;});
    sp.reset();

allocator 类

  • new 有一些灵活性上的局限, 其中一方面表现在它将内存分配和对象构造组合在了一起. 当分配一块大内存时, 通常计划在这块内存上按需构造对象, 在此情况下, 希望将内存分配和对象构造分离
  • allocator 类将内存分配和对象构造分离开来, 提供一种类型感知的内存分配方法, 它分配的内存是原始的, 未构造的.

方法

  • allocator a: 定义一个名为 a 的 allocator 对象, 它可以为类型为 T 的对象分配内存
  • a.allocate(n): 分配一段原始的, 未构造的内存, 保存 n 个类型为 T 的对象
  • a.deallocate(p, n): 释放从 T* 指针 p 中地址开始的内存, 这块内存保存了 n 个类型为 T 的对象; p 必须是先前由 allocate 返回的指针, 且 n 必须是 p 创建时所要求的大小. 在调用 deallocate 之前, 用户必须对每个在这块内存中创建的对象调用 destroy
  • a.construct(p, args): p 必须是一个类型为 T* 的指针, 指向一块原始内存; args 被传递给类型为 T 的构造函数, 用来在 p 指向的内存中构造一个对象(为了使用 allocate 返回的内存, 必须用 construct 构造对象)
  • a.destroy(p): p 为 T* 类型的指针, 此算法对 p 指向的对象执行析构函数(每个构造之后的元素需单独调用)

拷贝和填充为初始化内存的算法

  • uninitialized_copy(b, e, b2)
  • uninitialized_copy_n(b, n, b2)
  • uninitialized_fill(b, e, t)
  • uninitialized_fill_n(b, n, t)

使用关联容器

关联容器概述

  • 关联容器中的元素是按关键字来保存和访问的
  • 关联容器不支持顺序容器的位置相关的操作
  • 关联容器的迭代器都是双向的

定义关联容器

map

set

multimap

multiset

关键字类型的要求

有序容器的关键字类型

  • 对于有序容器(map, multimap, set, multiset), 关键字类型必须定义元素的比较方法
  • 默认情况下, 标准库使用关键字类型的 < 运算符来比较两个关键字
  • 可以提供自己定义的操作来代替关键字上的 < 运算符. 所提供的操作必须在关键字类型上定义一个严格弱序(可以将严格弱序看作小于等于)
    • 两个关键字不能同时小于等于对方
    • 如果 k1 <= k2, 且 k2 <= k3, 那么 k1 必须 <= k3
    • 如果存在两个关键字, 任何一个都不小于等于另一个, 那么这两个关键字等价. 若 k1 等价于 k2, k2 等价于 k3, 那么 k1 等价于 k3
  • 在实际编程中, 重要的是, 如果一个类型定义了行为正常的 < 运算符, 则它可以用作关键字类型

pair 类型

  • make_pair(v1, v2);

关联容器操作

关联容器迭代器

  • 当解引用一个关联容器迭代器时, 会得到一个类型为容器的 value_type 的值的引用.
    • 对 map 而言, value_type 是一个 pair 类型, 其 first 成员保存 const 的关键字, second 成员保存值
    • set 的迭代器是 const 的

添加元素

删除元素

map 的下标操作

  • map 下标运算符接受一个索引, 获取与此关键字相关联的值. 与其他下标运算符不同的是, 如果关键字并不在 map 中, 会为它创建一个元素并插入到 map 中, 关联值将进行值初始化
  • map 下标运算符返回一个左值

访问元素

  • find

无序容器

  • 不是使用比较运算符来组织元素, 而是使用一个哈希函数和关键字类型的 == 运算符
  • 无序容器在存储上组织为一组桶, 每个铜保存零个或多个元素
  • 默认情况下, 无序容器使用关键字类型的 == 运算符来比较元素, 使用一个 hash 类型的对象来生成每个元素的哈希值(标准库为内置类型提供了 hash 模板)
  • 直接定义关键字类型为自定义类类型的无序容器时, 需要提供 hash 模板版本和 == 运算符(或者在使用定义无序容器时传入这两种可调用对象)