C++

C++

Posted by Tong on August 25, 2019

«C++ Primer»

Basics

面向对象 vs. 面向过程(C语言)

面向过程就是分析出解决问题所需要的步骤,然后用函数把这些步骤一步一步实现,使用的时候一个一个依次调用就可以了;面向对象是把构成问题事务分解成各个对象,建立对象的目的不是为了完成一个步骤,而是为了描叙某个事物在整个解决问题的步骤中的行为。

可以拿生活中的实例来理解面向过程与面向对象,例如五子棋,面向过程的设计思路就是首先分析问题的步骤:1、开始游戏,2、黑子先走,3、绘制画面,4、判断输赢,5、轮到白子,6、绘制画面,7、判断输赢,8、返回步骤2,9、输出最后结果。把上面每个步骤用不同的方法来实现。

如果是面向对象的设计思想来解决问题。面向对象的设计则是从另外的思路来解决问题。整个五子棋可以分为1、黑白双方,这两方的行为是一模一样的,2、棋盘系统,负责绘制画面,3、规则系统,负责判定诸如犯规、输赢等。第一类对象(玩家对象)负责接受用户输入,并告知第二类对象(棋盘对象)棋子布局的变化,棋盘对象接收到了棋子的变化就要负责在屏幕上面显示出这种变化,同时利用第三类对象(规则系统)来对棋局进行判定。

可以明显地看出,面向对象是以功能来划分问题,而不是步骤。同样是绘制棋局,这样的行为在面向过程的设计中分散在了多个步骤中,很可能出现不同的绘制版本,因为通常设计人员会考虑到实际情况进行各种各样的简化。而面向对象的设计中,绘图只可能在棋盘对象中出现,从而保证了绘图的统一。

面向对象

面向对象的三大特性:封装、继承、多态

静态类型 (statically typed)

C++是一种静态类型语言,其含义是在编译阶段检查类型。其中,检查类型的过程称为类型检查 (type checking)。

endl

endl的作用是结束当前行,并将与设备关联的缓冲区(buffer)中的内容刷到设备中。缓冲刷新操作可以保证到目前为止程序所产生的所有输出都真正写入输出流中,而不是仅停留在内存中等待写入流。

预处理器 (preprocessor)

  • 在C++编译过程中执行的一段程序

  • 确保头文件多次包含仍能安全工作的常用技术

  • 头文件保护符 (header guard),头文件保护符依赖于预处理变量。预处理变量有两种状态:已定义和未定义。

Shallow vs Deep Copy

#include <iostream>
#include <cstring> // strlen()

using namespace std;

struct Test
{
	char* ptr;
};

void shallow_copy(Test& src, Test& dest)
{
	dest.ptr = src.ptr;
}

void deep_copy(Test& src, Test& dest)
{
	dest.ptr = (char*)malloc(strlen(src.ptr) + 1);
	strcpy_s(dest.ptr, strlen(src.ptr) + 1, src.ptr);
}

int main()
{
	Test src;
	char tmp = 'a';
	src.ptr = &tmp;
	cout << *src.ptr << endl;		// a

	Test dest_shallow;
	shallow_copy(src, dest_shallow);
	cout << *dest_shallow.ptr << endl;	// a

	Test dest_deep;
	deep_copy(src, dest_deep);
	cout << *dest_deep.ptr << endl;	// a

	*dest_shallow.ptr = 'b';
	cout << *src.ptr << endl;		// b

	cout << *dest_shallow.ptr << endl; // b

	cout << *dest_deep.ptr << endl; // c

	*dest_deep.ptr = 'c';
	cout << *src.ptr << endl;  // b

	cout << *dest_shallow.ptr << endl; // b

	cout << *dest_deep.ptr << endl; // c

	return 0;
}

STL - Standard Template Library

  • 容器(Container),是一种数据结构,如list,vector,和deques,以模板类的方法提供。为了访问容器中的数据,可以使用由容器类输出的迭代器;

  • 迭代器(Iterator),提供了访问容器中对象的方法。例如,可以使用一对迭代器指定list或vector中的一定范围的对象。迭代器就如同一个指针。事实上,C++的指针也是一种迭代器。但是,迭代器也可以是那些定义了operator*()以及其他类似于指针的操作符地方法的类对象;

  • 算法(Algorithm),是用来操作容器中的数据的模板函数。例如,STL用sort()来对一个vector中的数据进行排序,用find()来搜索一个list中的对象,函数本身与他们操作的数据的结构和类型无关,因此他们可以在从简单数组到高度复杂容器的任何数据结构上使用;

  • 仿函数(Function object)

  • 迭代适配器(Adaptor)

  • 空间配制器(allocator)

变量与基本类型

混用unsigned和signed类型

  • 如果表达式里既有带符号数又有无符号类型,当带符号类型取值为负时会出现异常结果,这是因为带符号数会自动地转换成无符号数。

  • 当从无符号数减去一个值时,不管这个值是不是无符号数,都必须确保结果不能是一个负值。

#include <iostream>

using namespace std;

int main()
{
	unsigned u = 10, u2 = 42;
	cout << u2 - u << endl;	// 32

	cout << u - u2 << endl;	// 4294967264

	int i = 10, i2 = 42;

	cout << i2 - i << endl;	// 32

	cout << i - i2 << endl;	// -32

	cout << i - u << endl;	// 0

	cout << u - i << endl;	// 0

	cout << i - u2 << endl;	// 4294967264

	cout << u2 - i << endl; // 32
}

字面值常量

  • 整形字面值
    • 20 // 十进制,默认情况下是带符号数,类型是int, long, long long中能容下当前值并且尺寸最小的那个
    • 024 // 以0开头,八进制。
    • 0x14 // 以0x或者0X开头, 十六进制。
    • 八进制和十六进制的类型是能容下其数值的int, unsigned int, long, unsigned long, long longunsigned long long中的尺寸最小者。如果一个都放不下,将产生错误。
    • 注意:十进制字面值不会是负数,-40的字面值仍然是40
  • 浮点型字面值
    • 默认的浮点型字面值是一个double
    • 表现为一个小数或以科学计数法表示的指数,其中指数部分用Ee标识: 3.14159, 3.1415926E0, 0., 0e0, .001

列表初始化 (list initialization)

  • 如果我们使用列表初始化且初始值存在丢失信息的风险,则编译器将报错。
#include <iostream>

using namespace std;

int main()
{
	int a = 1;

	int b = { 2 };

	int c{ 3 };	// 列表初始化

	int d(4);

	cout << "a: " << a << "; b: " << b << "; c: " << c << "; d: " << d << endl; // a: 1; b: 2; c: 3; d: 4

	long double ld = 3.1415;
	int a1(ld), b1 = ld;	// 转换执行,且确实丢失了部分值

	cout << "a1: " << a1 << "; b1: " << b1 << endl;	// a1: 3; b1: 3

	int c1{ ld }, d1 = { ld }; // ERROR!!! 转换未执行,因为存在丢失信息的危险
}

默认初始化

  • 如果内置类型的变量未被显式初始化,它的值由定义的位置决定。定义于任何函数体之外的变量被初始化为0。

  • 一种例外情况是,定义在函数体内部的内置类型变量将 不被初始化。一个未被初始化的内置类型变量的值是未定义的,如果试图拷贝或以其他形式访问此类值将引发错误。

  • 类的对象如果没有显式地初始化,则其值由类决定。

#include <iostream>

using namespace std;

string global_str;	// ""
int global_int;		// 0

int main()
{
	cout << "global_str: " << global_str << "; global_int: " << global_int << endl;
	// global_str: ; global_int: 0

	int local_int;		// Undefined
	string local_str;	// Undefined
}

declaration vs. definition

  • 声明 使得名字为程序所知,一个文件如果想使用别处定义的名字则必须包含对那个名字的声明。而 定义 负责创建与名字关联的实体。

  • 如果想声明一个变量而非定义它,就在变量名前添加关键字extern,而且不要显示地初始化变量。

  • 在函数体内部,如果试图初始化一个由extern关键子标记的变量,将引发错误。

  • 变量能且只能被定义一次,但是可以被多次声明。

extern int i; // 声明

int j; // 声明且定义

extern double pi = 3.14; // 定义

作用域

#include <iostream>

using namespace std;

int reused = 42;

int main()
{
	int unique = 0;
	cout << reused << " " << unique << endl;	// 42 0

	int reused = 0;
	cout << reused << " " << unique << endl;	// 0 0

	cout << ::reused << " " << unique << endl;	// 42 0, 因为显式地访问全局变量

}

#include <iostream>

using namespace std;

int main()
{
	int i = 100, sum = 0;
	for (int i = 0; i != 10; ++i)
		sum += i;
	cout << i << " " << sum << endl;	// 100 45

}

引用

#include <iostream>

using namespace std;

int main()
{
	int ival = 1.01;
	int& rvall = 1.01;	// ERROR: 引用类型的初始值必须是一个对象

	int& rval2 = ival;

	int& rval3;			// ERROR: 引用必须被初始化

    double& dval4 = ival; // ERROR: 类型必须相同,但有两个例外。

}

#include <iostream>

using namespace std;

int main()
{
	int i = 0, &r1 = i;
	double d = 0, &r2 = d;
	r2 = 3.14159;
	cout << "i: " << i << "; r1: " << r1 << "; d: " << d << "; r2: " << r2 << endl;
	// i: 0; r1: 0; d: 3.14159; r2: 3.14159

	r2 = r1;
	cout << "i: " << i << "; r1: " << r1 << "; d: " << d << "; r2: " << r2 << endl;
	// i : 0; r1 : 0; d : 0; r2: 0

	i = r2;
	cout << "i: " << i << "; r1: " << r1 << "; d: " << d << "; r2: " << r2 << endl;
	// i: 0; r1: 0; d: 0; r2: 0

	r1 = d;
	cout << "i: " << i << "; r1: " << r1 << "; d: " << d << "; r2: " << r2 << endl;
	// i : 0; r1 : 0; d : 0; r2: 0
}

const

  • 要想在多个文件间共享const对象,必须在变量的定义之前添加extern关键字。

  • 在初始化常量引用时允许用任意表达式作为初始值(不一定要类型相同),只要该表达式的结果能转换成引用的类型即可。

#include <iostream>

using namespace std;

int main()
{
	int i = 42;
	const int& r1 = i;			// 允许将const int&绑定到一个普通的int对象上

	const int& r2 = 42;			// 正确: r2是一个常量引用

	const int& r3 = r1 * 2;		// 正确: r3是一个常量引用

	int& r4 = r1 * 2;			// ERROR: r4是一个普通的非常量引用

	const double& r5 = i;		// 正确: r5是一个常量引用

}

  • 常量指针 (const pointer)

从右往左阅读。

#include <iostream>

using namespace std;

int main()
{
	int i = 42;
	int *const iptr = &i;

	*iptr = 0;	// 正确
	iptr = 0;   // ERROR: iptr是const

}
  • 指向常量的指针 (pointer to const)

从右往左阅读

#include <iostream>

using namespace std;

int main()
{
	int i = 42;
	const int* iptr = &i;

	*iptr = 0;	// ERROR: *iptr是const

	iptr = 0;   // 正确

}

顶层const vs. 底层const

  • 顶层const表示任意的对象是常量,例如指针本身

  • 底层const表示指针所指的对象是一个常量

  • 指针类型既可能是顶层也可能是底层const,这一点和其他类型相比区别明显

  • 在执行对象的拷贝操作时,顶层const不受什么影响,但是拷入和拷出的对象必须具有相同的底层const资格,或者两个对象的数据类型必须能够转换。一般来说,非常量可以转换成常量,反之则不行。

int i = 0;
int *const p1 = &i; // top-level const

const int ci = 42; // top-level const

const int *p2 = &ci; // low-level const

常量表达式

  • 常量表达式 (const expression)是指值不会改变并且在编译过程就能得到计算结果的表达式。

  • 字面值属于常量表达式,用常量表达式初始化的const对象也是常量表达式。

  • (C++ 11) 允许将变量声明为constexpr类型以便由编译器来验证变量的值是否是一个常量表达式。

  • 一般来说,如果认定一个变量是常量表达式,那就把它声明成constexpr类型。

#include <iostream>

using namespace std;

int size()
{
	return 10;
}

constexpr int size2()
{
	return 20;
}

int main()
{
	constexpr int mf = 20;				// 正确
	constexpr int limit = mf + 1;		// 正确
	constexpr int sz = size();			// 错误,除非size()是个constexpr函数
	constexpr int sz2 = size2();		// 正确
}

  • 指针如果定义为constexpr,它仅对指针有效,对指针所指的对象无效。
const int* p = nullptr;     // p是一个指向整型常量的指针

constexpr int *q = nullptr; // q是一个指向整数的常量指针
  • 一个constexpr指针的初始值必须是nullptr或者0,或者是存储与某个固定地址中的对象。

  • 函数体内定义的变量一般来说并非存放在固定地址中,因此constexpr指针不能指向这样的变量。相反的,定义于所有函数体之外的对象其地址固定不变,能用来初始化constexpr指针。除此之外,允许函数定义一类有效范围超出函数本身的变量,这类变量一样也有固定地址,constexpr指针也能指向这样的变量。

#include <iostream>

using namespace std;

constexpr int* np = nullptr;
int j = 0;
constexpr int i = 42;
// i, j都必须定义在函数体之外

int main()
{
	constexpr const int* p = &i;
	constexpr int* p1 = &j;
}

auto

  • auto能在一条语句声明多个变量,但是该语句种所有变量的初始基本数据类型必须一样。
#include <iostream>

using namespace std;

int main()
{
	auto i = 0, *p = &i;		// 正确
	auto sz = 0, pi = 3.14;		// ERROR! sz和pi类型不一致
}
  • auto一般会忽略顶层const,同时底层const则会保留下来。
#include <iostream>

using namespace std;

int main()
{
	int i = 0, &r = i;
	auto a = r; // a是一个整数

	const int ci = i, &cr = ci;

	auto b = ci;	// b是一个整数 (ci的顶层const特性被忽略掉了)

	auto c = cr;	// c是一个整数

	auto d = &i;	// d是一个整形指针

	auto e = &ci;	// e是一个指向整数常量的指针 (对常量对象取地址是一种底层const)

}

  • auto被设置成引用,原来的规则仍然适用
#include <iostream>

using namespace std;

int main()
{
	int i = 1;
	const int ci = i;

	auto& h = 42; // ERROR! 不能为非常量引用绑定字面值

	const auto& j = 42;	// 正确:可以为常量引用绑定字面值

	auto k = ci, & l = i;	// 正确:k是整数,l是整形引用

	auto& m = ci, * p = &ci;	// 正确:m是对整形常量的引用,p是指向整型常量的指针

	auto& n = i, * p2 = &ci;	// ERROR! i的类型是int,而&ci的类型是const int
}
#include <iostream>

using namespace std;

int main()
{
	const int i = 42;

	auto j = i;	// 整数

	++j;
	cout << j << endl;

	const auto& k = i;	// 整型常量的引用

	auto* p = &i;	// 指向一个整型常量的指针

	p = nullptr;

	const auto j2 = i, & k2 = i;	// j2是一个整数常量,k2是一个对整数常量的引用
}

decltype()

  • decltype((variable)) (双层括号)的结果永远是引用,而decltype(variable)结果只有当variable本身就是一个引用时才是引用。

  • 如果iint,那么表达式i=x的类型是int&

#include <iostream>

using namespace std;

int main()
{
	const int ci = 0, &cj = ci;
	decltype(ci) x = 0; // x的类型是const int

	decltype(cj) y = x; // y的类型是const int&, y绑定到变量x

	decltype(cj) z;	// ERROR! z是一个引用,必须初始化


	int i = 42, *p = &i, &r = i;
	decltype(r + 0) b;	// 正确:加法的结果是int,因此b是一个int

	decltype(*p) c;	// ERROR! c是int&, 必须初始化

	decltype((i)) d; // ERROR! d是int&, 必须初始化

	decltype(i) e; // 正确:e是一个未初始化的int
}

#include <iostream>

using namespace std;

int main()
{
	int a = 3, b = 5;
	decltype(a) c = a;
	decltype((b)) d = a;
	--c;
	++d;
	cout << a << " " << b << " " << c << " " << d << endl;
	// 4 5 2 4

}
#include <iostream>

using namespace std;

int main()
{
	int a = 3, b = 4;
	decltype(a) c = a;
	decltype(a = b) d = a;
	d = 5;
	cout << a << " " << b << " " << c << " " << d << endl;
	// 5 4 3 5

}

指针 (Pointer)

基本用法

a)	int** p;

b)	int *p[10];        // 指向int类型的指针数组p[10]

c)	int (*p)[10];      // 指向有10个int类型的数组的指针p

d)	int (*p)(int);     // 函数指针,指向有一个参数并且返回类型 均为int的函数

e)	int (*p[10])(int); // 函数指针的数组, 指向有一个参数并且返回类型均为int的函数的数组
#include <iostream>

using namespace std;

int main()
{
	int i = 42;
	int& r = i;
	int* p = &i;
	int& r2 = *p;
	cout << "i: " << i << endl;			// i: 42
	cout << "r: " << r << endl;			// r: 42
	cout << "p: " << p << endl;			// p: 0000005DB88FFB84
	cout << "*p: " << *p << endl;		// *p: 42
	cout << "r2: " << r2 << endl;		// r2: 42

	r2 = 10;
	cout << "i: " << i << endl;			// i: 10
	cout << "r: " << r << endl;			// r: 10
	cout << "p: " << p << endl;			// p: 0000005DB88FFB84
	cout << "*p: " << *p << endl;		// *p: 10
	cout << "r2: " << r2 << endl;		// r2: 10
}
int* a, b; // a是指向int的指针,b是int
#include <iostream>

using namespace std;

int main()
{
	char a = 'a';
	typedef char* pstring;
	const pstring cstr1 = &a; // 指向char的常量指针

	*cstr1 = 'b';	// 正确

	const char* cstr2 = &a;	// 指向const char的指针

	*cstr2 = 'c';	// ERROR! const char不能赋值

	const pstring* ps = &cstr1;	// ps是一个指针,它的对象是指向char的常量指针

}

空指针

#include <iostream>
#include <cstdlib>	// NULL

using namespace std;

int main()
{
	int* p1 = nullptr;    // C++ 11
	int* p2 = NULL; // preprocesor variable
	int* p1 = 0;

	int* p2;
	int a = 0;
	p2 = a;			// ERROR: 不能把int变量赋给指针
}

void*指针

  • void*可用于存放任意对象的地址。但我们不知道该地址中到底是个什么类型的对象。

  • 作用:拿它和别的指针比较;作为函数的输入或输出;或者赋给另外一个void*指针。

  • 不能直接操作void*指针所指的对象,因为我们并不知道这个对象是什么类型。

指向指针的引用

#include <iostream>
using namespace std;

int main()
{
	int i = 42;
	int* p;
	int*& r = p;

	r = &i;
	cout << i << " " << * p << " " << * r << endl;
	// 42 42 42

	* r = 0;
	cout << i << " " << * p << " " << * r << endl;
	// 0 0 0
}

指针 vs. 引用

  • 指针:指针是一个变量,只不过这个变量存储的是一个地址,指向内存的一个存储单元。而引用跟原来的变量实质上是同一个东西,只不过是原变量的一个别名而已。如:
    int a = 1;
    int *p = &a;
    int a = 1;
    int &b = a;
    

    上面定义了一个整形变量和一个指针变量p,该指针变量指向a的存储单元,即p的值是a存储单元的地址。而下面2句定义了一个整形变量a和这个整形a的引用b,事实上a和b是同一个东西,在内存占有同一个存储单元。

  • 可以有const指针,但是没有const引用;

  • 指针可以有多级,但是引用只能是一级(int **p;合法 而int &&a是不合法的)

  • 指针的值可以为空,但是引用的值不能为NULL,并且引用在定义的时候必须初始化;

  • 指针的值在初始化后可以改变,即指向其它的存储单元,而引用在进行初始化后就不会再改变了。

  • sizeof(引用)得到的是所指向的变量(对象)的大小,而sizeof(指针)得到的是指针本身的大小;

  • 指针和引用的自增(++)运算意义不一样;

string

initialization

#include <iostream>

#include <string>

using namespace std;

int main()
{
	string s1 = "Tong";            // 拷贝初始化(使用了等号)
	string s2("Tong");             // 直接初始化
	string s3(s2.begin(), s2.end());
	string s4(5, 'a');				// Attention: char instead of string as the second parameter, "aaaaa"

	string s5(s1, 2, 1);			// "n" (string, start, length)

	string s6 = s1.substr(2, 1);	// "n", (string, start, length)

	string s7 = to_string(123);		// "123"

	cout << s1 << endl << s2 << endl << s3 << endl << s4 << endl << s5 << endl << s6 << endl << s7;
}

默认初始化

#include <iostream>

#include <string>

using namespace std;

int main()
{
	string s;      // size() = 0
	char c = s[0]; // '\0'
	cout << c << endl;
	return 0;
}

getline()

  • cin自动忽略空白字符,getline会接收。
#include <iostream>

#include <string>

using namespace std;

int main()
{
	string line;
	while (getline(cin, line))
		cout << line << endl;
}

size()

  • string::size_type是个无符号类型

  • 如果一条表达式中已经有了size()函数就不要再使用int了,这样可以避免混用intunsigned可能带来的问题。

相加

  • C++语言中的字符串字面值并不是标准库类型string的对象。
string a = "Hello " + "Tong"; // ERROR! 不能直接运算两个字符串字面值

c_str()

// #include <string>
const char* c_str() const noexcept;
#include <iostream>
#include <string>

using namespace std;

int main()
{
	string s1 = "12345";
	cout << s1.size() << endl;		// 5

	cout << sizeof(string) << endl; // 28, 固定长度,和编译器有关

	cout << sizeof(s1) << endl;		// 28

	const char* c1 = s1.c_str();
	cout << c1 << endl;			// 12345

	cout << sizeof(c1) << endl; // 4, 因为指针是固定长度
}

atoi()

// #include <cstdlib>
int atoi (const char * str);

【函数说明】atoi() 函数会扫描参数 str 字符串,跳过前面的空白字符(例如空格,tab缩进等,可以通过 isspace() 函数来检测),直到遇上数字或正负符号才开始做转换,而再遇到非数字或字符串结束时(‘\0’)才结束转换,并将结果返回。

【返回值】返回转换后的整型数;如果 str 不能转换成 int 或者 str 为空字符串,那么将返回 0。

atoi()的参数是 const char* ,因此对于一个字符串str我们必须调用 c_str()的方法把这个string转换成 const char*类型的。

atoi()不会做范围检查,如果超出范围的话,超出上界,则输出上界,超出下界,则输出下界;

#include <iostream>
#include <cstdlib>	// atoi()
#include <string>	// c_str()

using namespace std;

int main()
{
	char s1[] = "-123";
	cout << atoi(s1) << endl; // -123

	const char* s2 = "aa-123";
	cout << atoi(s2) << endl; // 0

	string s = "aaa-";
	const char* s3 = s.c_str();
	cout << atoi(s3) << endl; // 0
}

stoi()

// #include <string>
int stoi (const string&  str, size_t* idx = 0, int base = 10);
int stoi (const wstring& str, size_t* idx = 0, int base = 10);

stoi()会做范围检查,默认范围是在int的范围内的,如果超出范围的话则会runtime error!

#include <iostream>
#include <string>	// stoi()

using namespace std;

int main()
{
	string s0 = "1234";
	cout << stoi(s0) << endl;	// 1234

	char s1[] = "-123";
	cout << stoi(s1) << endl;	// -123

	const char* s2 = "-12312";
	cout << stoi(s2) << endl;	// -12312

	string s3 = "aa-123";
	//cout << stoi(s3) << endl; // ERROR

	string s4 = "aaa-";
	//cout << stoi(s4) << endl; // ERROR

}

搜索操作

搜索操作返回指定字符出现的下标,如果未找到时则返回`npos`。

s.find(args)            // 查找s中args第一次出现的位置
s.rfind(args)           // 查找s中args最后一次出现的位置
s.find_first_of(args)   // 在s中查找args任何一个字符第一次出现的位置
s.find_last_of(args)    // 在s中查找args任何一个字符最后一次出现的位置
s.find_first_not_of(args) // 在s中查找第一个不在args的字符
s.find_last_not_of(args)  // 在s中查找最后一个不在args中的字符

寻找所有子字符串出现的所有位置

#include <iostream>

#include <string>

using namespace std;

int main()
{
	string s;
	cin >> s;
	string numbers("0123456789");

	string::size_type pos = 0;
	while ((pos = s.find_first_of(numbers, pos)) != string::npos)
	{
		cout << "Found number at index: " << pos << " element is " << s[pos] << endl;
		++pos;
	}
}

函数-替换string

#include <iostream>

#include <string>

using namespace std;

void replaceFcn(string& s, const string& old_val, const string& new_val)
{
	int len = old_val.size();
	int new_len = new_val.size();
	for (int i = 0; i < s.size() - len + 1;)
	{
		if (s.substr(i, len) == old_val)
		{
			s.erase(i, len);
			s.insert(i, new_val);
			i += new_len;
		}
		else
			++i;
	}
}

int main()
{
	string s;
	cin >> s;
	replaceFcn(s, "tho", "though");
	cout << s << endl;
}

vector

初始化

#include <iostream>
#include <vector>

using namespace std;

ostream& operator<<(ostream& stream, const vector<int>& nums)
{
	for (const int num : nums)
		stream << num << " ";
	return stream;
}

int main()
{
	// initialization

	vector<int> v1{ 1, 2, 3 };  // [1, 2, 3]

	cout << "v1" << endl << v1 << endl << endl;

	vector<int> v2(5, 0);		// [0, 0, 0, 0, 0]

	cout << "v2" << endl << v2 << endl << endl;

	vector<int> v3(v2.begin(), v2.end());
	cout << "v3" << endl << v3 << endl << endl;
	int arr4[] = { 10, 20, 30 };
	vector<int> v4(arr4, arr4 + sizeof(arr4) / sizeof(arr4[0]));
	cout << "v4" << endl << v4 << endl << endl;
	vector<int> v5;
	v5.push_back(1);
	v5.push_back(2);
	cout << "v5" << endl << v5 << endl << endl;
	vector<vector<int>> v6(3, vector<int>(4, -1));  // 3 x 4

    vector<string> v7{10, "hi"}; // ATTENTION! 尽量避免这种方式。系统检测出10无法初始化为string,所以最后v7是包含10个"hi"的vector

}

值初始化

  • 如果vector对象的元素是内置类型,比如int,则元素初始值自动设为0。如果元素是某种类型,比如string,则元素由类默认初始化。
#include <iostream>

#include <string>

#include <vector>

using namespace std;

int main()
{
	vector<int> ivec(10);
	cout << ivec[0] << endl;	// 10个元素,每个都初始化为0

	vector<string> svec(10);
	cout << svec[0] << endl;	// 10个元素,每个都是空string对象
}

size_type

vector<int>::size_type  // 正确
vector::size_type       // 错误

缓冲区溢出 (buffer overflow)

指通过下标访问不存在的元素的行为。

vector<int> ivec;
cout << ivec[0];

vector<int> ivec2(10);
cout << ivec2[10];

添加元素

  • 范围for语句体内不应改变其所遍历序列的大小

  • push_back()

void push_back (const value_type& val);
void push_back (value_type&& val);

在引入右值引用,转移构造函数,转移复制运算符之前,通常使用push_back()向容器中加入一个右值元素(临时对象)的时候,首先会调用构造函数构造这个临时对象,然后需要调用拷贝构造函数将这个临时对象放入容器中。原来的临时变量释放。这样造成的问题是临时变量申请的资源就浪费。 引入了右值引用,转移构造函数(请看这里)后,push_back()右值时就会调用构造函数和转移构造函数。

  • emplace_back()
template <class... Args>
  void emplace_back (Args&&... args);

在容器尾部添加一个元素,这个元素原地构造,不需要触发拷贝构造和转移构造。而且调用形式更加简洁,直接根据参数初始化临时对象的成员。

// https://blog.csdn.net/xiaolewennofollow/article/details/52559364

#include <vector>  
#include <string>  
#include <iostream>  

struct President
{
	std::string name;
	std::string country;
	int year;

	President(std::string p_name, std::string p_country, int p_year)
		: name(std::move(p_name)), country(std::move(p_country)), year(p_year)
	{
		std::cout << "I am being constructed.\n";
	}
	President(const President& other)
		: name(std::move(other.name)), country(std::move(other.country)), year(other.year)
	{
		std::cout << "I am being copy constructed.\n";
	}
	President(President&& other)
		: name(std::move(other.name)), country(std::move(other.country)), year(other.year)
	{
		std::cout << "I am being moved.\n";
	}
	President& operator=(const President& other);
};

int main()
{
	std::vector<President> elections;
	std::cout << "emplace_back:\n";
	elections.emplace_back("Nelson Mandela", "South Africa", 1994); //没有类的创建  

	std::vector<President> reElections;
	std::cout << "\npush_back:\n";
	reElections.push_back(President("Franklin Delano Roosevelt", "the USA", 1936));

	std::cout << "\nContents:\n";
	for (President const& president : elections) {
		std::cout << president.name << " was elected president of "
			<< president.country << " in " << president.year << ".\n";
	}
	for (President const& president : reElections) {
		std::cout << president.name << " was re-elected president of "
			<< president.country << " in " << president.year << ".\n";
	}

}
emplace_back:
I am being constructed.

push_back:
I am being constructed.
I am being moved.

Contents:
Nelson Mandela was elected president of South Africa in 1994.
Franklin Delano Roosevelt was re-elected president of the USA in 1936.

动态扩展

  • 由下面例子可以看到,vector在添加元素时,会多次重新分配内存。但是,当vector在删除元素的时候,容量并不会随着减小。vector的增容方式为: 0 1 2 3 4 6 … 从第三项开始基本遵循capacity = capacity + capacity / 2这个规律。
#include <iostream>
#include <vector>

using namespace std;

ostream& operator<<(ostream& stream, const vector<int>& nums)
{
	for (const int num : nums)
		stream << num << " ";
	return stream;
}

int main()
{
	vector<int> v;
	cout << "orignal capacity: " << v.capacity() << endl;
	auto capacity = v.capacity();
	for (int i = 0; i < 1000; ++i)
	{
		v.push_back(i);
		if (v.capacity() != capacity)
		{
			cout << "new capacity: " << v.capacity() << endl;
			capacity = v.capacity();
		}
	}
}
orignal capacity: 0
new capacity: 1
new capacity: 2
new capacity: 3
new capacity: 4
new capacity: 6
new capacity: 9
new capacity: 13
new capacity: 19
new capacity: 28
new capacity: 42
new capacity: 63
new capacity: 94
new capacity: 141
new capacity: 211
new capacity: 316
new capacity: 474
new capacity: 711
new capacity: 1066
  • reserve()
void reserve (size_type n);

reserve()使用空间置配器 ( allocator ) 来分配内存,也就是,没有存储元素的那部分内存是原生 ( raw ) 内存。要是n小于或等于v.capacity(),那么,v将忽略这个操作。 要是n大于v.capacity(),那么,v将会重新分配内存,并保证新的容量至少为n。

#include <iostream>
#include <vector>
using namespace std;

ostream& operator<<(ostream& stream, const vector<int>& nums)
{
	for (const int num : nums)
		stream << num << " ";
	return stream;
}

int main()
{
	vector<int> v;
	v.reserve(500);
	cout << v.size() << endl;		// 0
	cout << v.capacity() << endl;   // 500
}
  • resize()
void resize (size_type n);
void resize (size_type n, const value_type& val);

resize()的行为将取决于参数n, * 当n小于元素的个数,那么,容器的前n个元素将得到保留,其它的元素将被移除并销毁。 * 当n大于元素的个数,那么,新的元素将被默认初始化,并添加到容器末尾。要是指定了val,那么,将使用val来初始化新的元素。 * 要是n大于容器的容量,那么,在添加新元素之前,容器的内存将进行重新分配。

#include <iostream>
#include <vector>
using namespace std;

ostream& operator<<(ostream& stream, const vector<int>& nums)
{
	for (const int num : nums)
		stream << num << " ";
	return stream;
}

int main()
{
	vector<int> v;
	v.resize(500);
	cout << v.size() << endl;		// 500
	cout << v.capacity() << endl;   // 500
}

删除元素

  • pop_back(): 删除尾部元素,内存没有被真正释放
#include <iostream>
#include <vector>

using namespace std;

int main()
{
	// pop_back()
	vector<int> v{ 1, 2, 3 };
	cout << v.size() << endl;		// 3
	cout << v.capacity() << endl;   // 3
	v.pop_back();
	cout << v.size() << endl;		// 2
	cout << v.capacity() << endl;   // 3
}
  • erase(): 从指定容器删除指定位置的元素或某段范围内的元素。返回值是一个迭代器,指向删除元素下一个元素;如果是删除某范围内的元素时:返回值也表示一个迭代器,指向最后一个删除元素的下一个元素。内存没有被真正释放。
#include <iostream>
#include <vector>

using namespace std;

ostream& operator<<(ostream& stream, const vector<int>& nums)
{
	for (const int num : nums)
		stream << num << " ";
	return stream;
}

int main()
{
	// iterator erase(const_iterator position);
	// iterator erase(const_iterator first, const_iterator last);
	vector<int> v{ 1, 2, 3, 4, 5, 6, 7, 8 };
	auto it = v.erase(v.begin() + 1);
	cout << * it << endl;				// 3
	cout << v << endl;					// 1 3 4 5 6 7 8
	cout << v.size() << endl;			// 7
	cout << v.capacity() << endl;       // 8

	it = v.erase(v.begin(), v.begin() + 3);
	cout << * it << endl;				// 5
	cout << v << endl;					// 5 6 7 8
	cout << v.size() << endl;			// 4
	cout << v.capacity() << endl;       // 8
}
  • clear(): size()一定会变为0,但是capacity()不保证会变成0.
#include <iostream>
#include <vector>
#include <algorithm>

using namespace std;

ostream& operator<<(ostream& stream, const vector<int>& nums)
{
	for (const int num : nums)
		stream << num << " ";
	return stream;
}

int main()
{
	vector<int> v{ 1, 2, 3, 4, 5, 6, 7, 8 };
	cout << v.size() << endl;			// 8
	cout << v.capacity() << endl;		// 8
	v.clear();
	cout << v.size() << endl;			// 0
	cout << v.capacity() << endl;		// 8
}
  • swap(): vector<T>().swap(x); // clear x reallocating. 这才是释放vector内存的方法。
#include <iostream>
#include <vector>
#include <algorithm>

using namespace std;

ostream& operator<<(ostream& stream, const vector<int>& nums)
{
	for (const int num : nums)
		stream << num << " ";
	return stream;
}

int main()
{
	vector<int> v{ 1, 2, 3, 4, 5, 6, 7, 8 };
	cout << v.size() << endl;			// 8
	cout << v.capacity() << endl;		// 8
	vector<int>().swap(v);
	cout << v.size() << endl;			// 0
	cout << v.capacity() << endl;		// 0
}
  • shrink_to_fit(): Requests the container to reduce its capacity to fit its size. 所以我们可以通过先调用clear()再调用shrink_to_fit()的方式来释放内存。
#include <iostream>
#include <vector>
#include <algorithm>

using namespace std;

ostream& operator<<(ostream& stream, const vector<int>& nums)
{
	for (const int num : nums)
		stream << num << " ";
	return stream;
}

int main()
{
	vector<int> v{ 1, 2, 3, 4, 5, 6, 7, 8 };
	cout << v.size() << endl;			// 8
	cout << v.capacity() << endl;		// 8
	v.clear();
	cout << v.size() << endl;			// 0
	cout << v.capacity() << endl;		// 8
	v.shrink_to_fit();
	cout << v.size() << endl;			// 0
	cout << v.capacity() << endl;		// 0
}

比较大小

类似Python,逐位比较。

#include <iostream>
#include <vector>
#include <unordered_set>
#include <set>
using namespace std;

ostream& operator<<(ostream& stream, const vector<int>& nums)
{
	for (const int num : nums)
		stream << num << " ";
	return stream;
}

int main()
{
	vector<int> v1{ 1, 2 };
	vector<int> v2{ 1, 3 };
	vector<int> v3{ 1, 2, 3 };
	vector<int> v4{ 4, 1 };
	cout << (v1 < v2) << endl; // 1
	cout << (v1 < v3) << endl; // 1
	cout << (v2 < v3) << endl; // 0
	cout << (v2 < v4) << endl; // 1
}

迭代器 (iterator)

迭代器类型

vector<int>::iterator it;           // 可读写
string::iterator it2;               // 可读写

vector<int>::const_iterator it3;    // 只读,不能写
string::const_iterator it4;         // 只读,不能写
  • being()end()返回的具体类型由对象是否是常量决定。

  • cbegin() cend()返回的一定是const_iterator

迭代器运算

  • 只要两个迭代器指向的是同一个容器中的元素或者尾元素的下一个位置,就能将其相减,所得结果是两个迭代器的距离。其类型是名为difference_type的带符号整型数,可正可负。

iterator失效情形

  • erase(it)后并没由对it进行赋值。如下例:明显可以看到使用erase删除某一个结点之后,vector迭代器虽然还是指向当前位置,而且也引起了元素前挪,但是由于删除结点的迭代器就已经失效,指向删除点后面的元素的迭代器也全部失效,所以不能对当前迭代器进行任何操作;需要对迭代器重新赋值或者接收erase它的返回。
#include <iostream>
#include <vector>
using namespace std;

int main()
{
	vector<int> v{ 1, 2, 3, 4 };
	for (auto it = v.begin(); it != v.end(); ++it)
	{
		if (*it % 2)
			v.erase(it);
		cout << * it << " ";
	}
}
  • erase(it)it移动了多次,导致越界。
#include <iostream>
#include <vector>
using namespace std;

int main()
{
	vector<int> v{ 1, 2, 3, 4 };
	for (auto it = v.begin(); it != v.end(); ++it)
	{
		if (*it % 2 == 0)
			it = v.erase(it);
		cout << * it << " ";
	}
}

解决办法:

#include <iostream>
#include <vector>
using namespace std;

int main()
{
	vector<int> v{ 1, 2, 3, 4 };
	for (auto it = v.begin(); it != v.end();)
	{
		if (*it % 2 == 0)
			it = v.erase(it);
		else
			++it;
	}
}
  • push_back()导致capacity变化,而it并未更新。当插入(push_back)一个元素后,capacity的返回值与没有插入元素之前相比有改变,进行了深拷贝,而迭代器所指向的位置已经被析构函数释放所以导致迭代器失效。而当你在不增容的情况下尾插数据并不会导致迭代器失效。insert使迭代器失效的与push_back类似。
#include <iostream>
#include <vector>
using namespace std;

int main()
{
	vector<int> v{ 1, 2, 3, 4, 5 };
	for (auto it = v.begin(); it != v.end(); ++it)
	{
		if (*it % 2 == 0)
			v.push_back(1);
		cout << * it << endl;
	}
}

如果我们预留了足够的capacity,那么问题就能解决。

#include <iostream>
#include <vector>
using namespace std;

int main()
{
	vector<int> v{ 1, 2, 3, 4, 5 };
	v.reserve(500); // IMPORTANT!!!
	for (auto it = v.begin(); it != v.end(); ++it)
	{
		if (*it % 2 == 0)
			v.push_back(1);
		cout << * it << endl;
	}
}
  • end()迭代器并未更新
#include <iostream>
#include <vector>
using namespace std;

int main()
{
	vector<int> v{ 1, 2, 3, 4, 5 };
	v.reserve(500); // IMPORTANT!!!
	vector<int>::iterator end = v.end();
	for (auto it = v.begin(); it != end; ++it)
	{
		if (*it % 2 == 0)
			v.push_back(1); // push_back()导致v.end()发生了改变
		cout << *it << endl;
	}
}
  • push_back()导致无限循环
#include <iostream>
#include <vector>
using namespace std;

int main()
{
	vector<int> v{ 1, 2, 3, 4, 5 };
	for (auto it = v.begin(); it != v.end(); ++it)
	{
		if (*it % 2 == 0)
			v.push_back(0); // 0会导致一直添加0
		cout << * it << endl;
	}
}

数组 (array)

vector vs. array

  • creation:
    • vector: sequential container to store elements
    • array: original data structure, based on index concept
  • Memory
    • vector: occupy more memory than array
    • array: memory-efficient
  • Length
    • vector: length vary
    • array: fixed size length
  • Usage
    • vector: frequent insertion and deletion
    • array: frequent element acces
  • Resize
    • vector: resize vector is dynamic in nature
    • array: resizing arrays is expensiv
  • Structure
    • vector: template class, c++ only construct.
    • array: contiguous memory location
  • Indexing
    • vector: non-indexed based stuctrue
    • array: index based with the lowest address as first, and highest address as last
  • Access
    • vector: access element is time-consuming although based on a position of element
    • array: access element is constant time operation irrespective of element location

初始化

  • 如果在函数内部体内定义了某种内置类型的数组,那么默认初始化会令数组含有未定义的值。
#include <iostream>

#include <string>

#include <vector>

using namespace std;

int a1[10];

int main()
{
	cout << a1[0] << endl;	// 0

	int a2[10];
	cout << a2[0] << endl;	// -858993460

	vector<int> v(10);
	cout << v[0] << endl;		// 0
}
  • 如果对数组显式初始化,并且维度比提供的初始值数量大,则用提供的初始值初始化靠前的元素,剩下的元素被初始化成默认值。
#include <iostream>

#include <string>

using namespace std;

int main()
{
	int a1[5] = { 0, 1, 2 }; // 等价于 a1[] = {0, 1, 2, 0, 0}

	string a2[3] = { "hi", "bye" }; // 等价于 a2[] = {"hi", "bye", ""}

	int a3[2] = { 0, 1, 2, 3 }; // ERROR! 初始值太多
}
  • 不允许用数组拷贝或者赋值
int a[] = {0, 1, 2};

int a2[] = a; // ERROR!

a2 = a; // ERROR!
  • size必须是const(变量)或者constexpr(函数返回值)
#include <iostream>

#include <string>

using namespace std;

int txt_size()
{
	return 10;
}

int main()
{
	unsigned buf_size = 1024;

	int ia1[buf_size]; // ERROR! buf_size不是constant

	int ia2[4 * 7 - 14]; // 正确

	int ia3[txt_size()]; // 返回值必须是constexpr

	char st[11] = "fundamental"; // 容量小1, 应该为st[12]
}

复杂的声明

  • 从右向左

  • 从内向外

int *ptrs[10]; // ptrs是含有10个整型指针的数组

int &refs[10]; // ERROR! 不存在引用的数组

int (*Parray)[10] = &arr; // Parray指向一个含有10个整数的数组

int (&arrRef)[10] = arr; // arrRef引用一个含有是个整数的数组

int *(&arry)[10] = ptrs; // arry是数组的引用,该数组含有10个指针

size类型

  • 在使用数组下标的时候,通常将其定义为size_t类型。size_t是一种机器相关的无符号类型,它被设计得足够大以便能表示内存中任意对象的大小。在<cstddef>中定义了它。

指针与数组

  • 在大多数表达式中,使用数组类型的对象其实是使用一个指向该数组首元素的指针。

  • 当使用数组作为一个auto变量的初始值时,推断得到的类型时指针而非数组。

  • decltype(array)返回的类型还是数组,长度为array的长度。

#include <iostream>

#include <string>

using namespace std;

int main()
{
	string nums[] = { "one", "two", "three" };

	string* p = &nums[0]; // p指向nums的第一个元素

	string* p2 = nums; // 等价于 p2 = &nums[0]

	auto p3(nums); // p3是一个string指针,指向nums的第一个元素

	// p3 = 2; // ERROR!

	auto p4(&nums[0]); // 等价于p3

	decltype(nums) nums2 = { "hello" };	// nums2 = {"hello", "", ""}

}

begin(), end()

#include <iostream>

#include <string>

using namespace std;

int main()
{
	string nums[] = { "one", "two", "three" };

	string* pbeg = begin(nums);

	string* pend = end(nums);
}

指针相减

  • 两个指针相减的结果的类型是ptrdiff_t,也是定义在<cstddef>中。

下标(与vector不同)

  • 数组用的下标是有符号的,vector用的是无符号的。

  • 虽然可以处理负值,但是结果地址必须指向原来指针所指同一数组中的元素(或是同一数组尾元素的下一位置)。

#include <iostream>

#include <string>

using namespace std;

int main()
{
	string nums[] = { "one", "two", "three" };

	string* p = &nums[2];

	string k = p[-2]; // 得到nums[0]

}

多维数组

  • 注意多层for循环的使用

  • 要使用for处理多维数组,除了最内层的循环外,其他所有循环的控制变量都应该是引用类型。

  • 当使用多维数组的名字时,其会被自动转换成指向数组首元素的指针。

#include <iostream>

#include <string>

using namespace std;

int main()
{
	int ia[5][5];

	for (size_t i = 0; i != 5; ++i)
		for (size_t j = 0; j != 5; ++j)
			ia[i][j] = i * 5 + j;


	size_t cnt = 0;
	for (auto& row : ia)
		for (auto& col : row)
		{
			col = cnt;
			++cnt;
		}


	// 声明成了引用类型,为了避免数组被自动转成指针

	for (const auto& row : ia)		
		for (auto col : row)
			cout << col << endl;


	// ERROR! row 的类型是int*, 无法遍历
	for (auto row : ia)
		for (auto col : row)
			cout << col << endl;
}

#include <iostream>

#include <string>

using namespace std;

int main()
{
	int ia[3][4];

	int(*p)[4] = ia; //	p是指向含有4个整数的数组

	p = &ia[2];		 // p指向ia的尾元素

}

表达式

左值,右值

  • 当一个对象被用作右值的时候,用的是对象的值(内容);当对象被用作左值的时候,用的是对象的身份(在内存中的位置)。

  • 左值是变量的地址,右值是变量存储的内容。变量本质即存储空间的名称,编译后变为对应地址。

  • 左值就是有名字的变量(对象),可以被赋值,可以在多条语句中使用,而右值呢,就是临时变量(对象),没有名字,只能在一条语句中出现,不能被赋值。

  • 左值引用

int a = 3;
int & ref_a = a; //指针是一种数据类型,而引用不是。当其用作函数传参时,传递的就是变量的左值即地址。
  • 右值引用
    • 消除两个对象交互时不必要的对象拷贝,节省运算存储资源,提高效率;
    • 能够更简洁明确地定义泛型函数;
    • 右值引用形式:类型 && a= 被引用的对象。与左值的区别在于:右值是临时变量,如函数返回值,且不变。
    • 右值引用可以理解为右值的引用,右值初始化后临时变量消失。

求值顺序

#include <iostream>

#include <vector>

using namespace std;

int f1()
{
	return 1;
}

int f2()
{
	return 2;
}

int main()
{
	int i = f1() * f2(); // 我们无法知道f1()先运行还是f2()先运行

	int n = 0;
	cout << n << " " << ++n << endl; // UNDEFINED, 但可以输出,例如 1 1
}
f() + g() * h() + j(); // 函数的调用顺序没有明确规定

算数运算

  • 小整数类型的运算对象被提升成较大的整数类型,所有运算对象最终会转变成同一类型。

  • (C++ 11) 商一律向0取整。(-m)/nm/(-n)等价于-(m/n)m %(-n)等于m%n(-m)%n等于-(m%n)

bool b = true;
bool b2 = -b; // b2仍然是true!!!

逻辑与关系运算符

  • &&||采取短路求值策略 (short-circuit evaluation)。
    • 对于&&,当且仅当左侧运算对象为真时才对右侧运算对象求值。
    • 对于||,当且仅当左侧运算对象为假时才对右侧运算对象求值。
  • ==!=会先计算左右两侧的值,然后再进行比较。哪边的值先算是未定义的。
#include <iostream>

#include <vector>

using namespace std;

int f1()
{
	return 1;
}

int f2()
{
	return 2;
}

int main()
{
	const char* cp = "Hello world";
	if (cp && *cp)
	{ // 这里会输出

		cout << cp << endl; // Hello world

		cout << * cp << endl; // H
	}
	else
		cout << "NOT OK" << endl;


	int i = 0, j = 20, k = 21;
	cout << (i != j < k) << endl; // 等价于 i != (j < k)

}

赋值运算符

  • 赋值运算符的优先级低于关系运算符的优先级。
#include <iostream>

using namespace std;

int main()
{
	int i;
	cout << (i = 2 < 3) << endl;	// 1, 等价于 i = (2 < 3)

	cout << i << endl;				// 1

	cout << ((i = 2) < 3) << endl;	// 1

    cout << i << endl;				// 2

}

#include <iostream>

using namespace std;

int getPtr()
{
	return 20;
}

int main()
{
	int p = 10;
	p = getPtr() != 20; // 等价于 p = (getPtr() != 20)

	cout << "p: " << p << endl;	// p: 0
}

递增运算符

  • 前置版本的递增运算符++i避免了不必要的工作,它把值加1后直接返回改变了的运算对象。

  • 后置版本需要将原始值存储下来以便于返回这个未修改的内容。如果我们不需要修改前的值,那么后置版本的操作就是一种浪费。

  • 后置递增运算符的优先级高于解引用运算符。

#include <iostream>

#include <vector>

using namespace std;

int main()
{
	vector<int> v{ 1, 2, 3 };
	auto pbeg = v.begin();
	while (pbeg != v.end() && *pbeg >= 0)
		cout << *pbeg++ << endl;
        // 相当于*(pbeg++),pbeg的值加1,然后返回初始值(加1之前),最后解引用
    	// 1 2 3

}
#include <iostream>

#include <vector>

using namespace std;

int main()
{
	vector<int> v{ 1, 2, 3 };
	auto pbeg = v.begin();
	while (pbeg != v.end() && *pbeg >= 0)
		cout << *++pbeg << endl;
		// 2, 3, 最后ERROR!!!

}
vec[ival++] <= vec[ival]; // undefined, 我们不知道哪边会先算

成员访问运算符

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

#include <string>

using namespace std;

int main()
{
	string s1 = "a string";
	string* p = &s1;
	auto n = s1.size();	// 运行string对象s1的size成员

	n = (*p).size(); // 正确

    n = p->size();	// 正确

    n = *p.size(); // ERROR!!!
}

移位运算符

  • 移位运算符比算数运算符低,但比关系运算符,赋值运算符和条件运算符的优先级高。
cout << 42 + 10; // right
cout << (10 < 42); // right
cout  << 10 < 42; // ERROR!!!

sizeof()

  • sizeof是运算符,不是函数,因此不把它所要求得长度的对象叫做参数,习惯上叫做操作数。

  • 基础数据类型

#include <iostream>

using namespace std;

int main()
{
    cout << sizeof(bool) << endl;           // 1
	cout << sizeof(char) << endl;			// 1
	cout << sizeof(short) << endl;			// 2
	cout << sizeof(int) << endl;			// 4
	cout << sizeof(long) << endl;			// 4
	cout << sizeof(long long) << endl;		// 8
	cout << sizeof(float) << endl;          // 4
	cout << sizeof(double) << endl;			// 8
}
  • 指针
#include <iostream>

using namespace std;

int main()
{
	cout << sizeof(char*) << endl;			// 8 (x64); 4 (x86)
	cout << sizeof(long long*) << endl;		// 8 (x64); 4 (x86)

	// cout << sizeof(void) << endl;		// ERROR!!!
	cout << sizeof(void*) << endl;			// 8 (x64); 4 (x86)}
}
  • 数组
#include <iostream>

using namespace std;

int main()
{
	int a[] = { 1, 2, 3 };
	cout << sizeof(a) << endl;					// 12
	cout << sizeof(a) / sizeof(a[0]) << endl;	// 3

	int* b = a;
	cout << sizeof(b) << endl;					// 4 (x86); 8 (x64). 指针是恒定大小

	int (&c)[3] = a;							// 这是对数组的引用
	cout << sizeof(c) << endl;					// 12

	int d[5] = { 1, 2, 3 };
	cout << sizeof(d) << endl;					// 20
	cout << sizeof(d) / sizeof(d[0]) << endl;	// 5
}
#include <iostream>
#include <cstring> // strlen()

using namespace std;

int main()
{
	char a[] = "abcdef";
	cout << sizeof(a) << endl;	// 7

	char b[] = { 'a', 'b', 'c', 'd', 'e', 'f' };
	cout << sizeof(b) << endl; // 6

	string c("abcdef");
	cout << sizeof(c) << endl; // 28, 这里不代表字符串的长度,而是string类的大小, 在不同编译器上是恒定的

	char d[] = { 'a', '\0', 'c', 'd', 'e', 'f' };
	cout << sizeof(d) << endl; // 6
	cout << strlen(d) << endl; // 1
}
  • 表达式: sizeof()求得表达式的计算结果的类型大小
#include <iostream>

using namespace std;

int main()
{
	char c = '1';
	int n = 1;
	cout << sizeof(c + n) << endl;		// 4
	cout << sizeof(c += n) << endl;		// 1
}
  • 函数:求得返回类型的大小,但是不执行函数体!
#include <iostream>

using namespace std;

int add(int& a, int& b)
{
	return a += b;
}

int main()
{
	int a = 1;
	int b = 1;
	cout << sizeof(add(a, b)) << endl;		// 4
	cout << a << endl;						// 1
}
  • union
    • union的大小取决于它所有的成员中,占用空间最大的一个成员的大小,因union中的所有成员起始地址都是一样的。
    • 复合数据类型,如unionstructclass的对齐方式为成员中对齐方式最大的成员的对齐方式。
    • 通过程序可以改变编译器对界,使用#pragma pack(x)宏可以改变编译器的对界方式,默认是8。
    • C++固有类型的对界取编译器对界方式与自身大小中较小的一个。例如,指定编译器按2对界,int类型的大小是4,则int的对界为2和4中较小的2。
#include <iostream>

using namespace std;

union u1
{
	char a;
	int b;
};

union u2
{
	char a[13];
	int b;
};

union u3
{
	char a[13];
	char b;
};

int main()
{
	cout << sizeof(u1) << endl;	// 4, 以4对齐
	cout << sizeof(u2) << endl;	// 16, 以4对齐, 4 * 4 = 16, 这是离13最近的对界。
	cout << sizeof(u3) << endl;	// 13, 以1对齐
}
#pragma pack(2)

#include <iostream>

using namespace std;

union u1
{
	char a;
	int b;
};

union u2
{
	char a[13];
	int b;
};

union u3
{
	char a[13];
	char b;
};

int main()
{
	cout << sizeof(u1) << endl;	// 4, 以2对齐
	cout << sizeof(u2) << endl;	// 14, 以2对齐, 2 * 7 = 14, 这是离13最近的对界。
	cout << sizeof(u3) << endl;	// 13, 以1对齐
}
  • struct: 这里计算sizeof既要考虑数据对齐(整体上最大元素的size对齐,满足之后还要满足struct中其他元素的对齐),又要考虑最节约存储空间的原则。
#include <iostream>

using namespace std;

struct s0
{

};

struct s1
{
	char a;
	double b;
	int c;
	char d;
};

struct s2
{
	char a;
	char d;
	int c;
	double b;
};

int main()
{
	// 空类型必须在内存中占有一定空间,否则无法使用那些实例。至于占用多少内存,由编译器决定
	cout << sizeof(s0) << endl; // 1

	// 以8对齐
	// 先放a, 从0开始放,下一个空闲起始位为1
	// 但是由于b占8位,要放到8的对界上,所以从8开始放,下一个空闲起始位为16
	// c占4位,下一个空闲起始位为20
	// d占1位,总共放了21位,但大小要在8的倍数处结束,所以24
	cout << sizeof(s1) << endl;	// 24

	// 以8对齐
	// 先放a, 从0开始放,下一个空闲起始位为1
	// 但是由于d占1位,下一个空闲起始位为2
	// c占4位,下一个空闲起始位为6
	// d占8位,要放到8的对界上,所以从8开始放,总共大小为16
	cout << sizeof(s2) << endl;	// 16
}
  • 嵌套struct
#include <iostream>

using namespace std;

struct s1
{
	char a[8];
};

struct s2
{
	double a;
};

struct s3
{
	s1 s;
	char a;
};

struct s4
{
	s2 s;
	char a;
};

int main()
{
	cout << sizeof(s1) << endl;	// 8
	cout << sizeof(s2) << endl; // 8

	cout << sizeof(s3) << endl; // 9, 因为它的对齐方式还是1
	cout << sizeof(s4) << endl; // 16,因为它的对齐方式是8
}
  • class无虚函数:成员函数(除了虚函数)不会分配空间,所以sizeof时只计算数据成员的大小,计算方法类似struct
#include <iostream>

using namespace std;

// 1
class c0
{

};

// 16
class c1
{
private:
	char a;
	double b;
public:
};

// 16
class c2
{
private:
	char a;
	double b;
public:
	int foo() { return 1; }
};

int main()
{
	cout << sizeof(c0) << endl; // 1
	cout << sizeof(c1) << endl;	// 16
	cout << sizeof(c2) << endl;	// 16
}
  • class包含虚函数(不管几个):单继承情况下,只要class中存在virtual函数(不管几个虚函数),编译器在编译时就会自动插入一个指向虚函数表的指针vptr(大小为4字节). 为了效率问题,编译器(gcc 和 微软)一般会把虚指针放在类的内存空间的最前面的位置,不管虚函数声明的位置。
#include <iostream>

using namespace std;

// 8
class c3
{
public:
	virtual void foo() { }
	char a;
};


// 8
class c4
{
public:
	virtual void foo() { }
	int a;
};

// 12
class c5
{
public:
	virtual void foo() { }
	char a;
	int b;
};

// 8
class c6
{
public:
	virtual void foo() { }
	char a;
	char b;
	char c;
	char d;
};


// 8
class c7
{
public:
	short a;
	virtual void foo() { }
	char b;
};

// 8
class c8
{
public:
	short a;
	char b;
	virtual void foo() { }
};

// 24, NO IDEA WHY THIS IS 24???
class c9
{
public:
	double a;
	char b;
	virtual void foo() { }
};

// 24
class c10
{
public:
	char b;
	double a;
	virtual void foo() { }
};

int main()
{
	cout << sizeof(c3) << endl;	// 8
	cout << sizeof(c4) << endl;	// 8
	cout << sizeof(c5) << endl;	// 12, 以4对齐
	cout << sizeof(c6) << endl;	// 8, 以4对齐
	cout << sizeof(c7) << endl;	// 8, 以4对齐
	cout << sizeof(c8) << endl;	// 8, 以4对齐
	cout << sizeof(c9) << endl;	// 24, 以8对齐
	cout << sizeof(c10) << endl;// 24, 以8对齐
}
  • class包含static:因为static成员是分配在全局区为类的所有对象共享(VC编译器可能为了方便将其放入文字常量表), sizeof时不应该计入static成员。
#include <iostream>

using namespace std;

// 16
class c1
{
private:
	char a;
	double b;
public:
};

// 16
class c2
{
private:
	char a;
	double b;
	static int c;
public:
};

int main()
{
	cout << sizeof(c1) << endl; // 16
	cout << sizeof(c2) << endl;	// 16
}
  • class单继承:类似嵌套struct,注意以谁对齐
#include <iostream>

using namespace std;

// 13
class c1
{
private:
	char a[13];
public:
};

// 14
class c2 : public c1
{
private:
	char b;
};

// 20
class c3 : public c1
{
private:
	int c;
};

int main()
{
	cout << sizeof(c1) << endl; // 13
	cout << sizeof(c2) << endl;	// 14
	cout << sizeof(c3) << endl;	// 20, a占据13位,c从地址16开始,所以一共20
}
  • class多继承
#include <iostream>

using namespace std;

// 13
class c1
{
private:
	char a[13];
public:
};

// 1
class c2
{
private:
	char b;
};

// 20
class c3 : public c1, c2
{
private:
	int c;
};

int main()
{
	cout << sizeof(c1) << endl; // 13
	cout << sizeof(c2) << endl;	// 1
	cout << sizeof(c3) << endl;	// 20, VS把c1, c2当作整体来看待,总共14,然后从16开始,总共20
}

类型转换

  • 何时发生隐式类型转换?
    • 整型提升 (integral promotion): 在大多数表达式中,比int类型小的整型值首先提升为较大的整数类型。
    • 在条件中,非布尔值转换成布尔值。
    • 初始化过程中,初始值转换成变量的类型;在赋值语句中,右侧运算对象转换成左侧运算对象的类型。
    • 如果算术运算或关系运算的运算对象有多种类型,需要转换成同一种类型。
    • 函数调用时也会发生类型转换。
  • 整型提升
    • bool, char, signed char, unsigned char, short, unsgined short –> int
    • wchar_t, char16_t, char32_t –> int, unsigned int, long, unsigned long, long long, unsigned long long中的最小的一种类型,前提是转换后的类型要能容纳原类型所有可能的值。
  • 如果某个运算符的运算对象类型不一致,这些运算对象将转换成同一种类型。但是某个运算对象的类型是无符号类型,那么转换的结果就要依赖于机器中各个整数类型的相对大小了。

  • 首先,执行整型提升,如果结果的类型匹配,无须进行进一步的转换。如果两个(提升后的)运算对象类型要么都是带符号的,要么都是无符号的,则小类型的运算对象转换成较大的类型。

  • 如果一个运算对象是无符号类型,另一个是有符号类型。
    • 如果无符号类型不小于带符号类型,那么带符号的运算对象转换成无符号的。
    • 如果带符号类型大于无符号类型,那么转换的结果依赖于机器。
      • 如果无符号类型的所有值都能存在该带符号类型中,则无符号类型转换成带符号类型。
      • 如果不能,带符号类型转换成无符号类型。
  • 强制类型转换
    • statci_cast: 任何具有明确定义的类型转换,只要不包含底层const,都可以使用static_cast
    • dynamic_cast
    • const_cast
    • reinterpret_cast
  • statci_cast: 任何具有明确定义的类型转换,只要不包含底层const,都可以使用static_cast
    • 把一个较大的算术类型赋值给较小类型(精度损失警告信息就会被关闭)
    • 找回存在于void*中的值(我们应确保指针的值保持不变,确保转换后的类型就是指针所指的类型,否则会有Undefined的后果)
#include <iostream>

#include <string>

using namespace std;

int main()
{
	int i = 3;
	int j = 2;
	double slope = static_cast<double>(j) / i;
	cout << slope << endl;

	void* p = &slope;
	double* dp = static_cast<double*>(p);
	cout << *dp << endl;
}

  • const_cast: 只能改变运算对象的底层const,不能改变类型。
    • 将常量对象转换成非常量对象,但是执行写操作就会产生Undefined的后果
    • 将非常量转换成常量,运用于函数重载。
#include <iostream>

#include <string>

using namespace std;

int main()
{
	const char* pc;
	char* p = const_cast<char*>(pc); // 正确:但是通过p写值是未定义的行为
}
#include <iostream>
#include <string>

using namespace std;

const string& shorterString(const string& s1, const string& s2)
{
	return s1.size() <= s2.size() ? s1 : s2;
}

string& shorterString(string& s1, string& s2)
{
	auto& r = shorterString(const_cast<const string&>(s1), const_cast<const string&>(s2));
	return const_cast<string&>(r);
}

int main()
{
	string s1 = "Hello";
	string s2 = "Tong";
	string& res = shorterString(s1, s2);
	res = "April";
	cout << "res: " << res << endl;	// April

	cout << "s1: " << s1 << endl;	// Hello

    cout << "s2: " << s2 << endl;	// April
}

  • reinterpret_cast: 通常为运算对象的位模式提供较低层次上的重新解释。
#include <iostream>

#include <string>

using namespace std;

int main()
{
	int* ip;
	char* pc = reinterpret_cast<char*>(ip);
	string str(pc); // 危险!!! 可能会有异常的行为

}

语句

条件语句

  • 悬垂else (dangling else): else离它最近的尚未匹配的if匹配。

  • switch有多个case时

#include <iostream>

using namespace std;

int main()
{
	unsigned evenCnt = 0, oddCnt = 0;
	int digit = 5;

	switch (digit)
	{
	case 1: case 3: case 5: case 7: case 9:
		oddCnt++;
		break;

	case 2: case 4: case 6: case 8: case 10:
		evenCnt++;
		break;
	}
}

  • switchcase label必须是整形常量表达式。
#include <iostream>

#include <string>

using namespace std;

int main()
{
	double d = 3.14;

	// d 不能用在switch里

	switch (d) {
		/* * /
	}
}
#include <iostream>

using namespace std;

int main()
{
	constexpr unsigned ival = 512, jval = 1024, kval = 4096; // 注意!!! 这里使用了constexpr
	unsigned bufsize;
	unsigned swt = 512;
	switch (swt) {
	case ival:
		bufsize = ival * sizeof(int);
		break;
	case jval:
		bufsize = jval * sizeof(int);
		break;
	case kval:
		bufsize = kval * sizeof(int);
		break;
	}
}

跳转语句

  • goto跳转到同一函数内的另一条语句。注意:不能将程序的控制权从变量的作用域之外转移到作用域之内。
#include <iostream>

using namespace std;

int main()
{
	goto end;			// ERROR!
	int ix = 10;
end:
	ix = 42;
}

#include <iostream>

using namespace std;

int main()
{
begin:
	int sz = 10;
	goto begin;			// OK. 因为每次都会生成一个新的sz
}

异常处理

  • 组成部分
    • throw表达式,异常检测部分使用throw表达式来表示它遇到了无法处理的问题。throw引发 (raise) 了异常。
    • try语句块,异常处理部分使用try语句块处理异常。try语句块中代码抛出的异常通常会被某个catch子句处理。因为catch子句“处理”异常,所以它们也被称作 异常处理代码 (exception handler)
    • 一套异常类 (exception class),用于在throw表达式和相关的catch子句之间传递异常的具体信息。
  • 异常类的头文件
    • exception头文件定义了最通用的异常类exception。它只报告异常的发生,不提供任何额外信息。
    • stdexcept头文件定义了几种常用的异常类。
    • new头文件定义了bad_alloc异常类型。
    • type_info头文件定义了bad_cast异常类型。
  • stdexcept
    • exception最常见的问题
    • run_time_error只有在运行时才能检测出的问题
    • range_error运行时错误:生成的结果超出了有意义的值域范围
    • overflow_error运行时错误:计算上溢
    • underflow_error运行时错误:计算下溢
    • logic_error程序逻辑错误
    • domain_error逻辑错误:参数对应的结果值不存在
    • invalid_argument逻辑错误:无效参数
    • length_error逻辑错误:试图创建一个超出该类型最大长度的对象
    • out_of_range逻辑错误:使用一个超出的有效范围的值
  • 程序在遇到抛出异常的代码前,其执行路径可能已经经过了多个try语句块,一个try语句块可能调用了包含另一个try语句块的函数。寻找处理代码的过程与函数调用链刚好相反。当异常被抛出时,首先搜索抛出该异常的函数。如果没找到匹配的catch子句,终止该函数,并在调用该函数的函数中继续寻找。以此类推,直到找到适当类型的catch子句为止。如果最终还是没能找到任何匹配的catch子句,程序转到名为terminate的标准库函数。该函数的行为与系统有关,一般情况下,执行该函数将导致程序非正常退出。

  • 如果一段程序没有try语句块且发生了异常,系统会调用terminate函数并终止当前程序的执行。
#include <iostream>

#include <stdexcept>

using namespace std;

int main()
{
	int a, b;
	cin >> a >> b;
	try
	{
		if (b == 0) throw runtime_error("Divide by 0.");
		cout << a / b << endl;
	}
	catch (runtime_error err)
	{
		cout << err.what() << endl;
	}
}

函数

Basics

  • 局部静态对象 (local static object)
    • 在程序的执行路径第一次经过对象定义语句时初始化,并且直到程序终止时才被销毁,在此期间即使对象所在的函数结束执行也不会对它有影响。
    • 如果局部静态对象没有显示的初始值,它将执行值初始化,内置类型的局部静态变量初始化为0。
#include <iostream>

using namespace std;

size_t count_calls()
{
	static size_t ctr = 0;
	return ++ctr;
}

int main()
{
	for (size_t i = 0; i != 10; ++i)
		cout << count_calls() << endl;
	return 0;
}
  • 相同名字的函数必须有具有明显区别的函数形参列表。
#include <iostream>

using namespace std;

void fcn(const int i)
{
	return;
}

void fcn(int i)	// ERROR! 因为顶层const被忽略掉了,所以传入这两个fcn的参数可以完全一样
{
	return;
}



int main()
{
	fcn(10);
	return 0;
}
  • 尽量使用常量引用,因为我们不能把const对象,字面值或者需要类型转换的对象传递给普通的引用形参。

数组形参

  • 声明方式
    • void print(const int*);
    • void print(const int[]);
    • void print(const int[10]); 这里为维度表示我们期望数组含有多少元素,实际不一定。
int i = 0, j[2] = {0, 1};
print(&i); // OK
print(j); // OK
  • 管理指针形参
    • 使用标记指定数组长度。要求数组本身包含一个结束标记,例如C风格字符串。
    • 使用标准库规范,例如begin(j), end(j)
    • 显示传递一个表示数组大小的形参。
#include <iostream>

using namespace std;

void print(const char* cp)
{
	if (cp)
		while (*cp)
			cout << * cp++;
	cout << endl;
}

void print(const int* beg, const int* end)
{
	while (beg != end)
		cout << * beg++ << endl;
}

void print(const int ia[], size_t size)
{
	for (size_t i = 0; i != size; ++i)
		cout << ia[i] << endl;
}

int main()
{
	print("Hello Tong!");

	int j[2] = { 0, 1 };
	print(begin(j), end(j));

	print(j, end(j) - begin(j));
}
  • 数组的引用形参 (必须声明长度)
#include <iostream>

using namespace std;

void print(int(&arr)[10])
{
	for (auto elem : arr)
		cout << elem << endl;
}

int main()
{
	int i = 0;
	int j[2] = { 0, 1 };
	int k[10] = { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 };
	print(&i);		// ERROR
	print(j);		// ERROR, 没有10个整数
	print(k);		// OK
}
  • 传递多维数组
void print(int (*matrix)[10], int rowSize) {}
void print(int matrix[][10], int rowSize) {}

initializer_list

  • 如果函数的实参类型数量未知但是全部实参的类型都相同,我们可以使用initializer_list类型的形参。

  • initializer_list对象中的元素永远是常量值,我们无法改变initializer_list对象中元素的值。

#include <iostream>
#include <initializer_list>

using namespace std;

void error_msg(initializer_list<string> il)
{
	// 注意!!! auto后面不能加&, 因为il.begin()不是左值,所以没法引用

	for (auto beg = il.begin(); beg != il.end(); ++beg)
		cout << * beg << " ";
	cout << endl;
}

int main()
{
	error_msg({ "functionX", "expected: 10", "actual: 11" });
}

返回值

  • 什么情况返回常量的引用无效?
#include <iostream>

using namespace std;

const string& getString()
{
	string s("Hello");
	if (s.size() > 2)
		return s;		// ERROR! 返回局部对象的引用
	else
		return "Tong";	// ERROR! "Tong"是一个局部临时量
}

int main()
{
	auto a = getString();
}

#include <iostream>

using namespace std;

// Legal program

int& get(int* arry, int index)
{
	return arry[index];
}

int main()
{
	int ia[10];
	for (int i = 0; i != 10; ++i)
    	get(ia, i) = i;
	for (auto i : ia)
		cout << i << " ";
	cout << endl;
}
  • 返回数组指针
// arrT是个类型别名,它表示含有10个整数的数组

typedef int arrT[10];
using arrT = int[10];


// 返回一个指向含有10个整数的数组的指针

arrT* func(int i);
int (*func(int i))[10];

auto func(int i) -> int(*)[10]; // C++ 11

decltype(odd)* func(int i);
#include <iostream>

using namespace std;

int odd[] = { 1, 3, 5, 7, 9 };
int even[] = { 0, 2, 4, 6, 8 };

// 注意:要想表示arrPtr返回指针还必须在函数声明时加一个*符号

decltype(odd)* arrPtr(int i)       
{
	return (i % 2) ? &odd : &even; // 返回一个指向数组的指针

}

int main()
{
	decltype(5);
}

函数重载

  • 顶层const不影响传入函数的对象。一个拥有顶层const的形参无法和另一个没有顶层const的形参区分开来。
Record lookup(Phone);
Record lookup(const Phone); // 重复声明了

Record lookup(Phone*);
Record lookup(Phone* const); // 重复声明了
  • 如果形参是某种类型的指针或引用,则通过区分其指向的是常量对象还是非常量对象可以实现函数重载,此时的const是底层的。
    • 我们只能把const对象 (或者指向const的指针)传递给const形参。
    • 但是非常量可以转换成const
    • 当我们传递一个非常量对象或者指向非常量对象的指针时,编译器会优先选用非常量版本的函数。
Record lookup(Phone&);
Record lookup(const Phone&); // 新函数

Record lookup(Phone*);
Record lookup(const Phone*); // 新函数
  • 调用重载函数的三个结果
    • 编译器找到一个与实参 最佳匹配 (best match) 的函数,并生成调用该函数的代码。
    • 找不到任何一个函数与调用的实参匹配,此时编译器发出 无匹配 (no match) 的错误信息。
    • 有多余一个函数可以匹配,但是每一个都不是明显的最佳选择。此时也会发生错误,称为 二义性调用 (ambiguous call)
  • 不允许两个函数除了返回类型之外的其他要素都相同。但是,如果返回类型不同,而且形参也不同,那也没问题。
#include <iostream>
#include <string>

using namespace std;

int* reset(int* i) {
	return i;
}

// OK

double* reset(double* d)
{
	return d;
}

int main()
{
	double d = 10;
	double* p = reset(&d);
}

默认实参

  • 一旦某个形参被赋予了默认值,它后面的所有形参都必须有默认值。

  • 函数的后续声明只能为之前那些没有默认值的形参添加默认实参,而且该形参右侧的所有形参必须都有默认值。

  • 局部变量不能作为默认实参。

  • 只要表达式的类型能转换成形参所需的类型,该表达式就能作为默认实参。

内联函数

  • 将函数指定为内联函数 (inline),通常就是将它在每个调用点上“内联地”展开。

  • 内联说明只是向编译器发出的一个请求,编译器可以选择忽略这个请求。

  • 一般来说,内联机制用于优化规模较小,流程直接,频繁调用的函数。

  • constexpr函数被隐式地指定为内联函数。

  • constexpr函数不一定返回常量表达式。

#include <iostream>
#include <string>

using namespace std;

constexpr int new_sz() { return 42; }

constexpr int foo = new_sz(); // OK

constexpr size_t scale(size_t cnt) { return new_sz() * cnt; } // 如果cnt是常量表达式,那么scale(cnt)也是

int main()
{
	int arr[scale(2)]; // OK

	int i = 2;
	int a2[scale(i)]; // ERROR!!! i不是常量表达式

}

调试帮助

  • assert是一种 预处理宏 (preprocessor marco) 。所谓预处理宏其实是一个预处理变量,行为类似内联函数。

  • assert<cassert>头文件里。应该使用assert而不是std::assert

  • assert的行为依赖于一个名为NDEBUG的预处理变量的状态。如果定义了NDEBUG,则assert什么也不做。默认状态下没有定义NDEBUG,此时assert将执行运行时检查。

  • C++编译器定义了_ _func_ _ (输出当前调试的函数的名字)。

  • 预处理器定义了

    • _ _FILE_ _ 存放文件名的字符串字面值
    • _ _LINE_ _ 存放当前行号的整型字面值
    • _ _TIME_ _ 存放文件编译时间的字符串字面值
    • _ _DATE_ _ 存放文件编译日期的字符串字面值

函数匹配

  • 函数匹配第一步是选定本次调用对应的重载函数集,集合中的函数称为 候选函数 (candidate function)
    • 候选函数有两个特征:1)与被调用的函数同名,2)其声明在调用点可见。
  • 第二步考察本次调用提供的实参,然后从候选函数中选出能被这组实参调用的函数,这些新选出的函数称为 可行函数 (viable function)
    • 可行函数有两个特征:1)其形参数量与本次调用提供的实参数量相等,2)每个实参的类型与对应的形参类型相同,或者能转换成形参的类型。
  • 第三步是从可行函数中选择与本次调用最匹配的函数。 这一过程中,逐一检查函数调用提供的实参,寻找形参类型与实参类型最匹配的那个可行函数。
    • 基本思想是,实参类型与形参类型越接近,它们匹配得越好。
  • 如果有且只有一个函数满足下列条件,则匹配成功:
    • 该函数的实参的匹配都不劣于其他可行函数需要的匹配。
    • 至少有一个实参的匹配优于其他可行函数提供的匹配。
  • 如果函数含有默认实参,则我们在调用该函数时传入的实参数量可能少于它实际使用的实参数量。
#include <iostream>
#include <string>

using namespace std;

void f()
{
	cout << "I am f()" << endl;
}

void f(int i)
{
	cout << "I am f(int)" << endl;
}

void f(int a, int b)
{
	cout << "I am f(int, int)" << endl;
}

void f(double a, double b = 3.14)
{
	cout << "I am f(double, double)" << endl;
}

int main()
{
	f(5.6);		// I am f(double, double)

	f(42);		// I am f(int)

	f(42, 0);   // I am f(int, int)

	f(2.56, 3.5); // I am f(double, double)

	f(2.5, 42); // ERROR!

}
  • 为了确定最佳匹配,编译器将实参类型到形参类型的转换划分成5个等级
    1. 精确匹配
      • 实参和形参类型相同。
      • 实参从数组类型或函数类型转换成对应的指针类型。
      • 向实参添加顶层const或者从实参中删除顶层const
    2. 通过const转换实现的匹配。
    3. 通过类型提升实现的匹配。(小整数型一般都会提升到int类型或者更大的整数类型)
    4. 通过算数类型转换实现的匹配。(所有算数类型转换的级别都一样)
    5. 通过类类型转换实现的匹配。
#include <iostream>

using namespace std;

void f(short i)
{
	cout << "I am f(short)" << endl;
}

void f(int i)
{
	cout << "I am f(int)" << endl;
}

void f(long i)
{
	cout << "I am f(long)" << endl;
}

void f(float i)
{
	cout << "I am f(float)" << endl;
}

int main()
{
	f('a');	// f(int), 因为char --> int

	f(3.14); // ERROR! 二义性调用,算术类型转换成long和float都有可能

}
  • 如果重载函数的区别在于它们的引用类型的形参是否引用了const,或者指针类型的形参是否指向const,则当调用发生时编译器通过实参是否是常量来决定选择哪个函数。
#include <iostream>

using namespace std;

void f(int& i)
{
	cout << "I am f(int&)" << endl;
}

void f(const int& i)
{
	cout << "I am f(const int&)" << endl;
}

int main()
{
	f('a');	// I am f(const int&)

	const int a = 5;
	f(a);	// I am f(const int&)

	int b = 5;
	f(b);	// I am f(int&)

}

函数指针

  • 声明
#include <iostream>

using namespace std;

bool lengthCompare(const string& a, const string& b)
{
	return a.length() < b.length();
}

int main()
{
	bool (*pf)(const string&, const string&); // pf指向一个函数,该函数的参数是两个const string的引用,返回值是bool类型

	bool* f(const string&, const string&); // 声明一个名为f的函数,该函数返回bool*

	pf = lengthCompare; // OK, &可加可不加

	pf = &lengthCompare; // OK

	bool b1 = pf("hello", "goodbye"); // OK, *可加可不加

	bool b2 = (*pf)("hello", "goodbye"); // OK

	bool b3 = lengthCompare("hello", "goodbye"); // OK

}
  • 函数指针形参
#include <iostream>

using namespace std;

bool lengthCompare(const string& a, const string& b)
{
	return a.length() >= b.length();
}

// 等价于 void getLonger(const string& a, const string& b, bool (*pf)(const string&, const string&))
void getLonger(const string& a, const string& b, bool pf(const string&, const string&))
{
	if (pf(a, b))
		cout << a + " is longer" << endl;
	else
		cout << b + " is longer" << endl;
}

int main()
{
	getLonger("Tong", "Yihui", lengthCompare); // 自动将函数lengthCompare转换成指向该函数的指针
}
  • 声明函数类型
// Func, Func2是函数类型

typedef bool Func(const string&, const string&);
typedef decltype(lengthCompare) Func2;

// FuncP, FuncP2是指向函数的指针

typedef bool (*FuncP)(const string&, const string&);
typedef decltype(lengthCompare)* FuncP2;
  • 返回指向函数的指针 (我们必须把返回类型写成指针形式)
using F = int(int*, int); // F是函数类型,不是指针

using PF = int(*)(int*, int); // PF是指针类型

PF f1(int); // OK

F f1(int);  // ERROR: F是函数,我们不能返回函数

F* f1(int); // OK

auto f1(int) - > int (*)(int*, int); // OK

decltype(f1)* f2(int); // decltype返回函数类型而非指针类型。我们必须加上一个*。
#include <iostream>

#include <vector>

using namespace std;

int add(int a, int b)
{
	return a + b;
}

int sub(int a, int b)
{
	return a - b;
}

int mul(int a, int b)
{
	return a * b;
}

int divide(int a, int b)
{
	return a / b;
}


int main()
{
	vector<int(*)(int, int)> v;

	v.push_back(add);
	v.push_back(sub);
	v.push_back(mul);
	v.push_back(divide);
	cout << v[0](5, 3) << endl;
	cout << v[1](5, 3) << endl;
	cout << v[2](5, 3) << endl;
	cout << v[3](5, 3) << endl;
}
#include <iostream>
#include <fstream>
#include <algorithm>
#include <string>

using namespace std;

void printLast10Lines(char* fileName)
{
	const int K = 10;
	ifstream file(fileName);
	string L[K];
	int size = 0;

	/* read file line by line into circular array */
	/* peek() so an EOF following a line ending is not considered a separate line */
	while (file.peek() != EOF)
	{
		getline(file, L[size % K]);
		++size;
	}

	/* compute start of circular array, and the size of it*/
	int start = size > K ? (size % K) : 0;
	int count = min(K, size);

	/* print elements int the order they were read * /
	for (int i = 0; i < count; ++i)
		cout << L[(start + i) % K] << endl;
}

int main()
{
	return 0;
}

Reverse String

#include <iostream>
#include <algorithm> // swap
#include <string>

using namespace std;

void reverse(string& s)
{
	if (s.size() == 0) return;
	int start = 0;
	int end = s.size() - 1;
	while (start < end)
		swap(s[start++], s[end--]);
}

int main()
{
	string s = "Tong";
	reverse(s);
	cout << s << endl;
	return 0;
}

Copy Node

编写方法,传入参数为指向Node结构的指针,返回传入数据结构的完整拷贝。其中,Node数据结构含有两个指向其他Node的指针。

#include <iostream>
#include <map>

using namespace std;

struct Node
{
	Node* ptr1;
	Node* ptr2;
};

typedef map<Node*, Node*> NodeMap;

Node* copy_recursive(Node* cur, NodeMap& node_map)
{
	if (!cur) return nullptr;

	NodeMap::iterator i = node_map.find(cur);
	if (i != node_map.end()) return i->second; // we have been here before, return the copy

	Node* node = new Node;
	node_map[cur] = node; // map current before traversing links

	node->ptr1 = copy_recursive(cur->ptr1, node_map);
	node->ptr2 = copy_recursive(cur->ptr2, node_map);
	return node;
}

Node* copy_structure(Node* root)
{
	NodeMap node_map;	// we will need an empty map

	return copy_recursive(root, node_map);
}


int main()
{

	return 0;
}

Basics

  • 类的基本思想是 数据抽象 (data abstraction)封装 (encapsulation)
    • 数据抽象是一种依赖于 接口 (interface)实现 (implementation) 的分离的编程技术。
    • 类的接口包括用户所能执行的操作;
    • 类的实现则包括类的数据成员、负责接口实现的函数体以及定义类所需的各种私有函数。
    • 封装实现了类的接口和实现的分离。类的用户只能使用接口而无法访问实现部分。
  • 类有两项基本能力:一是数据抽象,即定义数据成员和函数成员的能力;二是封装,即保护类的成员不被随意访问的能力。通过将类的实现细节设为private,我们就能完成类的封装。

  • 类要想实现数据抽象和封装,需要首先定义一个 抽象数据类型 (abstract data type)

  • new A v. new A()
    • 如果该类没有定义构造函数(由编译器合成默认构造函数)也没有虚函数,那么class c = new class;将不调用合成的默认构造函数,而class c = new class();则会调用默认构造函数。
    • 如果该类没有定义构造函数(由编译器合成默认构造函数)但有虚函数,那么class c = new class;和class c = new class();一样,都会调用默认构造函数。
    • 如果该类定义了默认构造函数,那么class c = new class;和class c = new class();一样,都会调用默认构造函数。
#include <iostream>

// https://www.cnblogs.com/youxin/p/3735064.html

class A
{
public:
	int getA()
	{
		return a;
	}

private:
	int a;
};


int main()
{
	A* a = new A;
	A* b = new A();

	std::cout << a->getA() << std::endl;   // -842150451

	std::cout << b->getA() << std::endl;   // 0

	return 0;
}

定义抽象数据类型

  • 定义在类内部的函数是隐式的inline函数。

  • 成员函数通过一个名为this的额外的隐式参数来访问调用它的那个对象。this是一个常量指针,我们不允许改变this中保存的地址。默认情况下,this的类型是指向类类型非常量版本的常量指针。

  • 在参数列表后面加const表示this是一个指向常量的指针。像这样使用const的成员函数被称作为 常量成员函数 (const member function)

  • 常量对象,以及常量对象的引用或指针都只能调用常量成员函数。

  • 编译器分两步处理类:首先编译成员的声明,然后才轮到成员函数体。因此,成员函数体可以随意使用类中的其他成员而无须在意这些成员出现的次序。

  • 一般来说,如果非成员函数是类接口的组成部分,则这些函数的声明应该与类在同一个文件内。

构造函数 (constructor)

  • 无论何时只要类的对象被创建,就会执行构造函数。

  • 构造函数不能被声明为const

  • 构造函数在const对象的构造过程中可以向其写值。

  • 只有当类没有声明任何构造函数时,编译器才会自动地生成 默认构造函数 (default constructor)

  • 编译器创建的构造函数又被称为 合成的默认构造函数 (synthesized default constructor)
    • 如果存在类内的初始值,用它来初始化成员。
    • 否则,默认初始化该成员。
  • 如果类包含有内置类型或者复合类型地成员,则只有当这些成员全都被赋予了类内地初始值时,这个类才适合于使用合成的默认构造函数。

  • 如果成员是const或者是引用的话,必须将其初始化。

  • 如果成员属于某种类类型且该类没有定义默认构造函数时,也必须将这个成员初始化。

  • 如果成员是const, 引用,或者属于某种未提供默认构造函数的类类型,我们必须通过 构造函数初始值列表 为这些成员提供初值。
class ConstRef
{
public:
	ConstRef(int ii);
private:
	int i;
	const int ci;
	int& ri;
};

ConstRef::ConstRef(int ii)
{
	i = ii;			// OK

	ci = ii;		// ERROR! 不能给const赋值

	ri = i;			// ERROR! ri没被初始化
}

ConstRef::ConstRef(int ii) : i(ii),  ci(ii), ri(i) { } // OK, 这里必须使用构造函数初始值列表
  • 成员的初始化顺序与它们在类定义中的出现顺序一致。构造初始值列表只说明用于初始化成员的值,而不限定初始化的具体执行顺序。
class X
{
	int i;
	int j;
public:
	X(int val) : j(val), i (j) { } // Undefined, i会被先初始化

};
  • 类只能有一个默认构造函数。
#include <iostream>

using namespace std;

class X
{
	int i;
	int j;
public:
	X(int val = 1) : j(val), i (val) { }
	X(istream& is = cin) { is >> i >> j; }			// ERROR! 两个默认构造函数了
};
  • 委托构造函数 (delegating constructor): 当一个构造函数委托给另一个构造函数时,受委托的构造函数的初始值列表和函数体被依次执行。
#include <iostream>

#include <string>

using namespace std;

class Sales_data
{
	friend istream& read(istream&, Sales_data&);
public:
	Sales_data(string s, unsigned cnt, double price) : bookNo(s), units_sold(cnt), revenue(cnt*price) { }
	Sales_data() : Sales_data("", 0, 0) {}
	Sales_data(string s) : Sales_data(s, 0, 0) {}
	Sales_data(istream& is) : Sales_data() { read(is, *this); }

private:
	string bookNo;
	unsigned units_sold = 0;
	double revenue = 0.0;
};

istream& read(istream& is, Sales_data& item)
{
	double price = 0;
	is >> item.bookNo >> item.units_sold >> price;
	item.revenue = price * item.units_sold;
	return is;
}
  • 当对象被默认初始化或值初始化时自动执行默认构造函数。

  • 默认初始化在以下情况发生:
    • 当我们在块作用域内不适用任何初始值定义一个非静态变量或者数组时。
    • 当一个类本身含有类类型的成员且使用合成的默认构造函数时。
    • 当类类型的成员没有在构造函数初始值列表中显示地初始化时。
  • 值初始化在以下情况发生:
    • 在数组初始化地过程中如果我们提供的初始值数量少于数组的大小时。
    • 当我们不使用初始值定义一个局部静态变量时。
    • 当我们通过书写形如T()的表达式显式地请求值初始化时,其中T是类型名。
  • 如果构造函数只接受 一个 实参,则它实际上定义了转换为此类类型的隐式转换机制,这种构造函数称作 转换构造函数 (converting constructor)

  • 编译器只会自动地执行一步类型转换。
string null_book = "9-999-99999-9";
item.combine(null_book); // OK, 我们只需要把string转换成Sales_data


item.combine("9-999-99999-9"); // ERROR! 我们要进行两步转换,先把"99...99"换成string, 再换成Sales_data

item.combine(string("9-999-99999-9")); // OK

item.combine(Sales_data("9-999-99999-9")); // OK
  • 在要求隐式转换的程序上下文中,我们可以通过将构造函数声明为explicit加以阻止。

  • explicit只对一个实参的构造函数有效。

  • 只能在类内声明构造函数时使用explicit,在类外定义时不应重复。

  • 当我们使用explicit关键字声明构造函数时,它将只能以直接初始化的形式使用。

访问控制与封装

  • 我们使用 访问说明符 (access specifiers) 加强类的封装性。

  • public, protected, private
    • private: 只能由该类中的函数、其友元函数访问,不能被任何其他访问,该类的对象也不能访问.
    • protected: 可以被该类中的函数、子类的函数、以及其友元函数访问,但不能被该类的对象访问
    • public: 可以被该类中的函数、子类的函数、其友元函数访问,也可以由该类的对象访问
    • 注:友元函数包括两种:设为友元的全局函数,设为友元类中的成员函数.
  • 默认的访问权限:structpublic的,classprivate的。

  • 类可以允许其他类或者函数访问它地非公有成员,方法是令其他类或者函数成为它的 友元 (friend)。如果类想把一个函数作为它的友元,只需要增加一条以friend关键字开始的函数声明语句即可。
    • 友元声明只能出现在类定义的内部,但是在类内出现的具体位置不限,友元不是类的成员也不受它所在区域访问控制级别的约束。
  • 封装的优点
    • 确保用户代码不会无意间破坏封装对象的状态。
    • 被封装的类的具体实现细节可以随时改变,而无需调整用户级别的代码。
  • 尽管当类的定义发生改变时无须更改用户代码,但是使用了该类的源文件必须重新编译。

类的其他特性

  • 可以在类的内部把inline作为声明的一部分显示地声明成员函数,同样的,也能在类的外部用inline关键字修饰函数地定义。

  • 可变数据成员 (mutable data member)永远不会是const,即使它是const对象的成员。任何成员函数,包括const函数在内部都能改变它的值。

  • 通过区分成员函数是否是const的,我们可以对其进行重载。

  • 前向声明 (forward declaration)。在类声明之后,定义之前是一个 不完全类型 (incomplete type)

  • 不完全类型只能在非常有限的情境下使用
    • 可以定义指向这种类型的指针或引用。
    • 可以声明(但是不能定义)以不完全类型作为参数或者返回类型的函数。
  • 如果一个类指定了友元类,则友元类的成员函数可以访问此类包括非公有成员在内的所有成员。

  • 友元关系不存在传递性。每个类负责控制自己的友元类或友元函数。

  • 如果一个类想把一组重载函数声明成它的友元,它需要对这组函数中的每一个分别声明。

  • 友元本身不一定真的声明在当前作用域中。

  • 就算在类的内部定义该函数,我们也必须在类的外部提供相应的声明从而使得函数可见。

类的作用域

  • 一个类就是一个作用域。

  • 编译器处理完类中的全部声明后才会处理成员函数的定义。

  • 在类中,如果成员使用了外层作用域中的某个名字,而该名字代表一种类型,则类不能在之后重新定义该名字。

  • 类型名通常出现在类的开始处,这样就能确保所有使用该类型的成员都出现在类名的定义之后。

聚合类 (aggregate class)

  • 聚合类是的用户可以直接访问其成员,并且具有特殊的初始化语法形式。

  • 聚合类的条件
    • 所有成员都是public的。
    • 没有定义任何构造函数。
    • 没有类内初始值。
    • 没有基类,也没有virtual函数。
  • 与初始化数组元素的规则一样,如果初始值列表中的元素个数少于类的成员数量,则靠后的成员被值初始化。初始值列表的元素个数绝对不能超过类的成员数量。
#include <string>

using namespace std;

struct Data
{
	int ival;
	string s;
};

Data vall = { 0, "Anna" };

字面值常量类

  • 数据成员都是字面值类型的聚合类是字面值常量类。

  • 如果一个类不是聚合类,但它符合下述要求,也是一个字面值常量类:
    • 数据成员都必须是字面值类型。(整型,浮点型)
    • 类必须至少含有一个constexpr函数。
    • 如果一个数据成员含有类初始值,则内置类型成员的初始值必须是一条常量表达式;或者如果成员属于某种类类型,则初始值必须使用成员自己的constexpr函数。
    • 类必须使用析构函数的默认定义,该成员负责销毁类的对象。
  • 一个字面值常量类必须至少提供一个constexpr构造函数。

  • constexpr构造函数必须初始化所有数据成员,初始值或者使用constexpr构造函数,或者是一条常量表达式。

  • constexpr构造函数体一般来说应该是空的。

静态成员

  • 类的静态成员存在于任何对象之外,对象中不包含任何与静态数据成员有关的数据。

  • 静态成员函数不与任何对象绑定在一起,它们不包含this指针。所以我们不能在static函数体内使用this指针。

  • 静态成员函数不能声明成const的。

  • 我们既可以在类的内部也可以在类的外部定义静态成员函数。

  • 当在类的外部定义静态成员时,不能重复static关键字,该关键字只出现在类内部的声明语句。

  • 静态数据成员不是由类的构造函数初始化的。一般来说,我们不能在类的内部初始化静态成员。必须在类的外部定义和初始化每个静态成员。一个静态成员只能定义一次。

  • 如果在类的内部提供了一个初始值,则成员的定义不能再指定一个初始值了。

  • 通常情况下,类的静态成员不应该在类的内部初始化。然而,我们可以为静态成员提供const整数类型的类内初始值,不过要求静态成员必须是字面值常量类型的constexpr。初始值必须是常量表达式。

  • 即使一个常量静态数据成员在类内部被初始化了,通常情况下也应该在类的外部定义以下该成员。

  • 静态成员和普通成员的一个区别是我们可以使用静态成员作为默认实参。

  • 静态成员可以是不完全类型。特别的,静态数据成员的类型可以就是它所属的类类型,而非静态数据成员则受到限制,只能声明成它所属类的指针或引用。

class Bar
{
public:
	int i;
private:
	static Bar mem1;	// OK
	Bar* mem2;			// OK
	Bar mem3;			// ERROR!
};

继承

多态继承

定义: 指允许不同类的对象对同一消息做出响应。即同一消息可以根据发送对象的不同而采用多种不同的行为方式。(发送消息就是函数调用)。 操作:在基类的函数前加上virtual关键字,在派生类中重写该函数, 运行时将会根据对象的实际类型来调用相应的函数。如果对象类型是派生类,就调用派生类的函数; 如果对象类型是基类,就调用基类的函数。

析构函数

  • 基类的析构函数为何要声明为virtual?
#include <iostream>
#include <cstring> // strlen()

using namespace std;

class Foo
{
public:
	void f() { cout << "I am Foo" << endl; }
};

class Bar : public Foo
{
public:
	void f() { cout << "I am Bar" << endl; }
};


int main()
{
	Foo* p = new Bar();
	p->f();		// I am Foo

	return 0;
}

上述程序最后会调用Foo::f(),这是因为p是指向Foo的指针,而f()不是虚拟的。为确保p->f()会调用继承关系最末端的子类的f()实现,我们需要将f()声明为虚函数。

析构函数用于释放内存和资源。Foo的析构函数若不是虚拟的,那么,即使p实际上是Bar类型的,还是会调用Foo的析构函数。

所以,原因就是确保正确调用继承关系最末端的子类的析构函数。

#include <iostream>
#include <cstring> // strlen()

using namespace std;

class Foo
{
public:
	virtual void f() { cout << "I am Foo" << endl; }
};

class Bar : public Foo
{
public:
	void f() { cout << "I am Bar" << endl; }
};


int main()
{
	Foo* p = new Bar();
	p->f();		// I am Bar

	return 0;
}
  • 调用顺序
#include <iostream>

using namespace std;

class Shape
{
public:
	int edge_length;
	virtual ~Shape()
	{
		cout << "Shape deleted\n";
	}
};

class Triangle : public Shape
{
public:
	~Triangle()
	{
		cout << "Triangle deleted\n";
	}
};

int main()
{
	Triangle* x = new Triangle();
	delete x;

	cout << endl << endl;

	Shape* y = new Triangle();
	delete y;
	return 0;
}
Triangle deleted
Shape deleted


Triangle deleted
Shape deleted

虚函数

  • 虚函数(virtual):基类希望其派生类进行覆盖的函数。虚函数(virtual function) 需要虚函数表(vtable, Virtual Table)才能实现。如果一个类由函数声明成虚拟的,就会生成vtable,存放这个类的虚函数地址。此外,编译器还会在类里加入隐藏的vptr变量。若子类没有覆写虚函数,该子类的vtable就会存放父类的函数地址。调用这个虚函数时,就会通过vtable解析函数的地址。在C++里,动态绑定(dynamic binding)就是通过vtable机制实现的。由此,将子类对象赋值给基类指针时,vptr变量就会指向子类的vtable。这样一来,就能确保继承关系最末端的子类虚函数会被调用到。
#include <iostream>

using namespace std;

class Shape
{
public:
	int edge_length;
	virtual int circumference()
	{
		cout << "Circumference of Base Class\n";
		return 0;
	}
};

class Triangle : public Shape
{
public:
	int circumference()
	{
		cout << "Circumference of Triangle Class\n";
		return 3 * edge_length;
	}
};

int main()
{
	Shape* x = new Shape();
	x->circumference(); // "Circumference of Base Class"
	Shape* y = new Triangle();
	y->circumference(); // "Circumference of Triangle Class"
	return 0;
}
  • 纯虚(pure virtual, =0): double net_price(std::size_t) const = 0;
    • 含有纯虚函数的类是抽象基类,不能创建抽象基类的对象。

内联函数

C++ 内联函数是通常与类一起使用。

对内联函数进行任何修改,都需要重新编译函数的所有客户端,因为编译器需要重新更换一次所有的代码,否则将会继续使用旧的函数。

如果想把一个函数定义为内联函数,则需要在函数名前面放置关键字 inline,在调用函数之前需要对函数进行定义。如果已定义的函数多于一行,编译器会忽略 inline 限定符。

在类定义中的定义的函数都是内联函数,即使没有使用 inline 说明符。

定义: 当函数被声明为内联函数之后, 编译器会将其内联展开, 而不是按通常的函数调用机制进行调用.

const

https://www.cnblogs.com/MATU/p/5283454.html

class Test(){
public:
    Test(){}
    const int foo(int a);  //当const在函数名前面的时候修饰的是函数返回值。
    const int foo(int a) const; //当const在函数名后面表示是常成员函数,该函数不能修改对象内的任何成员,只能发生读操作,不能发生写操作。
};

IO库

  • <iostream>

  • <fstream>

  • <sstream>

IO类

  • ifstreamistringstream都继承自istream。因此,我们可以像使用istream对象一样来使用ifstreamistringstream对象。

  • 由于不能拷贝IO对象,因此我们也不能将形参或返回类型设置为流类型。

  • 进行IO操作的函数通常以引用方式传递和返回流。

  • 读写一个IO对象会改变其状态,因此传递和返回的引用不能是const的。

  • 每个输出流都管理一个缓冲区,用来保存程序读写的数据。

  • 导致缓冲刷新(即,数据真正写到输出设备或文件)的原因有很多:

    • 程序正常结束,作为main函数的return操作的一部分,缓冲刷新被执行。
    • 缓冲区满时,需要刷新缓冲,而后新的数据才能继续写入缓冲区。
    • 我们可以使用操纵符如endl来显式刷新缓冲区。
    • 在每个输出操作之后,我们可以用操纵符unitbuf设置流的内部状态,来清空缓冲区。默认情况下,对cerr是设置unitbuf ,因此写到cerr的内容都是立即刷新的。
    • 一个输出流可能被关联到另一个流。在这种情况下,当读写被关联的流时,关联到的流的缓冲区会被刷新。例如,默认情况下,cincerr都关联到cout。因此,读cincerr都会导致cout的缓冲区被刷新。
#include <iostream>

using namespace std;

int main()
{
	cout << "hi!" << endl;		// 完成换行,然后刷新缓冲区

	cout << "hi!" << flush;		// flush刷新缓冲区,不附加任何额外字符

	cout << "hi!" << ends;		// ends向缓冲区插入一个空字符,然后刷新缓冲区

	cout << unitbuf;		// 所有输出操作后都会立即刷新缓冲区

	cout << nounitbuf;		// 回到正常的缓冲方式

}
  • tie有两个版本
    • 一个版本不带参数,返回指向输出流的指针。如果本对象当前关联到一个输出流,则返回的就是指向这个流的指针,如果对象未关联到流,则返回空指针。
    • 另一个版本接受一个指向ostream的指针,将自己关联到此ostream。即,x.tie(&o)将流x关联到输出流o
  • 每个流同时最多关联到一个流,但多个流可以同时关联到同一个ostream

  • 每个IO对象都维护一组条件状态,用来指出此对象上是否可以进行IO操作。
#include <iostream>

using namespace std;

int main()
{
	cin.tie(&cout); // 将cin和cout关联在一起(其实默认已经是这样)

	ostream* old_tie = cin.tie(nullptr); // old_tie指向当前关联到cin的流(如果有的话),cin不再与其他流关联

	cin.tie(&cerr); // 将cin与cerr关联。读取cin会刷新cerr而不是cout

	cin.tie(old_tie);	// 重建cin和cout间的正常关联
}

文件输出输入

  • 如果我们定义了一个空文件六对象,可以随后调用open来将它与文件关联起来。
ifstream in(ifile);  // 构筑一个ifstream并打开给定文件

ofstream out;        // 输出文件流未与任何文件相关联

out.open(ifile + ".copy");  // 打开指定文件
  • 因为调用可能失败,进行open是否成功的检测通常是一个好习惯。
if (out) // 检查是否成功
{
    // 如果成功了...
}
  • 一旦一个文件流已经打开,它就保持与对应文件的关联。对一个已经打开的文件流调用open会失败,并会导致failbit被置位。为了将文件流关联到另一个文件,必须首先关闭已经关联的文件。
in.close();
in.open(ifile + "2");
  • 当一个fstream对象被销毁时,close会被自动调用。

  • 文件模式 (file mode)
    • ofstream::in以读方式打开
    • ofstream::out以写方式打开
    • ofstream::app每次写操作前均定位到文件末尾
    • ofstream::ate打开文件后立即定位到文件末尾
    • ofstream::trunc截断文件
    • ofstream::binary以二进制方式进行IO
  • 默认情况下,即使我们没有指定trunc,以out模式打开的文件也会被截断。

  • 保留被ofstream打开的文件中的已有数据的唯一方法是显式指定appin模式。

  • 如果未显式指定输出模式,文件隐式地以out模式打开。

顺序容器 (sequential container)

概述

  • 顺序容器类型
    • vector: 可变大小数组
    • deque: 双端队列
    • list: 双向链表
    • forward_list: 单向链表
    • array: 固定大小数组
    • string: 与vector相似,但专门用于保存字符
  • stringvector将元素保存在连续地内存空间中。由于元素是连续存储的,由元素下标来计算其地址是非常快速的。

  • listforward_list的设计目的是令容器任何位置的添加和删除操作都很快速。作为代价,它们不支持元素的随机访问:为了访问一个元素,我们只能遍历整个容器。这两个容器的额外内存开销很大。

  • deque支持快速随机访问(类似vectorstring)。在deque两端添加或删除元素都是很快的。

  • 只有string, vector, deque, array的迭代器支持下列运算
    • iter + n
    • iter - n
    • iter1 += n
    • iter1 -= n
    • iter1 - iter2
    • >, >=, <, <=
  • forward_list迭代器不支持递减运算符 (--)。

  • 最好使用while (begin != end) 而不是while (begin < end)。因为有些容器不支持<>,例如list

  • 为了创建一个容器为另一个容器的拷贝,两个容器的类型及其元素类型必须匹配。不过,当传递迭代器参数来拷贝一个范围时,就不要求容器类型时相同的了。
#include <iostream>

#include <vector>

#include <list>

#include <forward_list>

#include <deque>

#include <string>

using namespace std;

int main()
{
	list<string> authors = { "Tong", "Yihui" };
	vector<const char*> articles = { "a", "an", "the" };

	list<string> list2(authors); // OK:类型匹配

	deque<string> authList(authors); // ERROR: 容器类型不匹配

	vector<string> words(articles); // ERROR: 容器类型不匹配

	forward_list<string> words(articles.begin(), articles.end());	// OK

}
  • 只有当其元素类型也定义了相应的比较运算符时,我们才可以使用关系运算符来比较两个容器。

array

  • 标准库array具有固定大小。

  • 当定义一个array时,除了指定元素类型,还要指定容器大小。

array<int, 42> a1;
  • 与其他容器不同,一个默认构造的array是非空的:它包含了与其大小一样多的元素。这些元素都被默认初始化。

  • 如果我们对array进行列表初始化,初始值的数目必须等于或小于array的大小。如果初始值数目小于array的大小,则它们被用来初始化array中靠前的元素,所有剩余元素都会进行值初始化。

  • 虽然我们不能对内置数组类型进行拷贝或对象赋值操作,但array并无此限制。

  • 与其他容器一样,array也要求初始值的类型必须与要创建的容器类型相同。此外,array还要求元素类型和大小也都一样。

赋值和swap

  • assign操作不适用于容器和array

  • assign允许我们从一个不同但相容的类型赋值,或者从容器的一个子序列赋值。

#include <iostream>

#include <vector>

using namespace std;

int main()
{
	vector<int> a1 = { 1, 2, 3, 4, 5 };
	vector<int> a2;
	a2.assign(a1.begin(), a1.begin() + 2); // { 1, 2 }
	a2.assign(3, 1);	// { 1, 1, 1 }
	a2.assign({ 3, 2, 1 });		// {3, 2, 1}
}
  • 赋值相关运算会导致指向左边容器内部的迭代器,引用和指针失效。而swap操作将容器内容交换不会导致指向容器的迭代器,引用和指针失效 (容器类型为arraystring的情况除外)。

  • array外,swap不对任何元素进行拷贝,删除或插入操作,因此可以保证在常数时间内完成。

  • 与其他容器不同,swap两个array会真正交换它们的元素。因此,交换两个array所需的时间与array中的元素的数目成正比。

顺序容器操作

  • 向一个vectorstringdeque插入元素会使所有指向容器的迭代器,引用和指针失效。

  • 在一个vectorstring的尾部之外的任何位置,或是一个deque的首尾之外的任何位置添加元素,都需要移动元素。

  • 向一个vectorstring添加元素可能引起整个对象存储空间的重新分配。重新分配一个对象的存储空间需要分配新的内存,并将元素从旧的空间移动到新的空间。

  • insert函数将元素插入到迭代器所指定的位置 之前

#include <iostream>

#include <vector>

using namespace std;

int main()
{
	vector<int> v1 = { 1, 2, 3, 4, 5 };
	vector<int> v2 = { 10, 20, 30 };
	v1.insert(v1.begin(), v2.end() - 2, v2.end()); // v1: { 20 30 1 2 3 4 5 }

	v1.insert(v1.end(), { 100, 200 }); // v1: { 20 30 1 2 3 4 5 100 200 }

	v1.insert(v1.end(), 3, 666);		// v1: { 20 30 1 2 3 4 5 100 200 666 666 666 }

}
  • 接收元素个数或范围的insert版本返回指向第一个新加入元素的迭代器。如果范围为空,不插入任何元素,insert操作会将第一个参数返回。

  • emplace_frontemplaceemplace_back构造而不是拷贝元素。参数必须与元素类型的构造函数相匹配。

  • 包括array在内的每一个顺序容器都有一个front成员函数,而除forward_list之外的所有顺序容器都有一个back成员函数。这两个操作分别返回首元素和尾元素的引用。

#include <iostream>

#include <vector>

using namespace std;

int main()
{
	vector<int> v1 = { 1, 2, 3, 4, 5 };
	auto& v1 = v1.back(); // 获得指向最后一个元素的引用

	auto v2 = v1.back(); // 获得最后一个元素的拷贝

}
  • c.at(n)返回下标为n的元素的引用。如果下标越界,则抛出一out_of_range异常。

  • 删除deque中除首尾位置之外的任何元素都会使所有迭代器,引用和指针失效。

  • 指向vectorstring中删除点之后位置的迭代器,引用和指针都会失效。

  • 在一个forward_list中添加或删除元素的操作通过改变给定元素之后的元素来完成的。

迭代器失效

  • 在向容器添加元素后
    • 如果容器是vectorstring,且存储空间被重新分配,则指向容器的迭代器,指针和引用都会失效。如果存储空间未重新分配,指向插入位置之前的元素的迭代器,指针和引用仍有效,但指向插入位置之后元素的迭代器,指针和引用将会失效。
    • 对于deque,插入到除首尾位置之外的任何位置都会导致迭代器,指针和引用失效。如果在首尾位置添加元素,迭代器会失效,但指向存在的元素的引用和指针不会失效。
    • 对于listforward_list,指向容器的迭代器(包括尾后迭代器和首前迭代器),指针和引用仍有效。
  • 从一个容器删除元素后,
    • 指向被删除元素的迭代器,指针和引用会失效
    • 对于listforward_list,指向容器的迭代器(包括尾后迭代器和首前迭代器),指针和引用仍有效。
    • 对于deque,如果在首尾之外的任何位置删除元素,那么指向被删除元素外其他元素的迭代器,引用或指针也会失效。如果是删除deque的尾元素,则尾后迭代器也会失效,但其他迭代器,引用和指针不受影响;如果是删除首元素,这些也不会受影响。
    • 对于vectorstring,指向被删除元素之前元素的迭代器,引用和指针仍有效。注意:当我删除元素时,尾后迭代器总是会失效。

删除偶数元素,复制奇数元素

#include <iostream>

#include <vector>

using namespace std;

int main()
{
	vector<int> vi;
	for (vector<int>::size_type i = 0; i < 10; ++i)
		vi.push_back(static_cast<int>(i));

	auto iter = vi.begin();
	while (iter != vi.end())
	{
		if (*iter % 2)
		{
			iter = vi.insert(iter, *iter);
			iter += 2;
		}
		else
			iter = vi.erase(iter);
	}

	for (const auto& v : vi)
		cout << v << " ";
	cout << endl;
}
#include <iostream>

#include <list>

using namespace std;

int main()
{
	list<int> vi;
	for (list<int>::size_type i = 0; i < 10; ++i)
		vi.push_back(static_cast<int>(i));

	auto iter = vi.begin();
	while (iter != vi.end())
	{
		if (*iter % 2)
		{
			iter = vi.insert(iter, *iter);
			++iter;
			++iter;			// list 不支持 iter += 2

		}
		else
			iter = vi.erase(iter);
	}

	for (const auto& v : vi)
		cout << v << " ";
	cout << endl;
}

#include <iostream>

#include <forward_list>

using namespace std;

int main()
{
	forward_list<int> vi;
	auto cur = vi.before_begin();
	for (forward_list<int>::size_type i = 0; i < 10; ++i)
		cur = vi.insert_after(cur, static_cast<int>(i));

	auto prev = vi.before_begin();
	auto curr = vi.begin();

	while (curr != vi.end())
	{
		if (*curr % 2)
		{
			curr = vi.insert_after(curr, *curr);
			prev = curr;
			++curr;
		}
		else
		{
			curr = vi.erase_after(prev);
		}
	}

	for (const auto& v : vi)
		cout << v << " ";
	cout << endl;
}

容量管理

  • shrink_to_fit只适用于vector, string, deque

  • capacityreserve只适用于vectorstring

  • 容器的size是指它已经保存的元素的数目,而capacity则是在不分配新的内存空间的前提下它最多可以保存多少元素。

  • 为什么list没有capacity函数?因为list不支持随机访问,它里面的元素是分散存储在内存里的。所以我们没有必要重新分配新的内存空间。所以它的capacity就是它的size

容器适配器 (adapter)

  • 顺序容器适配器:stack, queue, priority_queue

  • 默认情况下,stackqueue是基于deque实现的。priority_queue是在vector之上实现的。

  • 我们可以在创建一个适配器时将一个命名的顺序容器作为第二个类型参数,来重载默认容器类型。

stack<string, vector<string>> str_stk; // 在vector上实现的空栈
  • stack可以被除arrayforward_list之外的任何容器类型来构造。

  • queue可以被listdequeue构造。

  • priority_queue可以被vectordeque构造。

泛型算法 (generic algorithm)

  • 算法永远不会改变底层容器的大小。

  • 根据支持的操作不同,迭代器可以分为五类:输入,输出,前向,双向以及随机访问迭代器。

  • 链表特有的操作会修改给定的链表。

  • accumulate()


#include <iostream>

#include <vector>

#include <string>

#include <numeric>  // accumulate

using std::cout;
using std::cin;
using std::endl;
using std::vector;
using std::string;

int main()
{
	vector<string> s{ "Hello ", "Tong" };
	string s1 = accumulate(s.begin(), s.end(), ""); // ERROR! 如果我们传递一个字符串字面值,用于保存和的对象将是const char*。由于const char*没有+运算符,此调用将产生编译错误。

	string s2 = accumulate(s.begin(), s.end(), string("")); // OK. "Hello Tong"

}

#include <iostream>

#include <algorithm>

#include <vector>

#include <string>

#include <numeric>

using std::cout;
using std::cin;
using std::endl;
using std::vector;
using std::string;

int main()
{
	vector<double> d{ 1.1, 2.2, 3.3 };
	cout << accumulate(d.begin(), d.end(), 0) << endl;	// 6, 因为第三个参数决定我们用哪种数值进行运算

	cout << accumulate(d.begin(), d.end(), 0.0) << endl;	// 6.6

}
  • equal可以比较两个不同类型容器中的元素,而且元素类型也不必一样。但是,equal基于一个非常重要的假设:它假定第二个序列至少和第一个序列一样长。而且,我们必须能使用==来比较来自两个序列中的元素。
#include <iostream>

#include <algorithm>

#include <vector>

#include <string>

#include <numeric>

#include <list>

using std::cout;
using std::cin;
using std::endl;
using std::vector;
using std::string;
using std::list;

int main()
{
	vector<string> s1{ "Tong", "April" };
	vector<string> s2{ "Tong" };
	cout << equal(s1.begin(), s1.end(), s2.begin()) << endl;	// ERROR!!!

	cout << equal(s2.begin(), s2.end(), s1.begin()) << endl;	// 1

	vector<const char*> s3{ "Tong", "April" };
	cout << equal(s1.begin(), s1.end(), s3.begin()) << endl;	// 1

	list<const char*> s4{ "Tong", "April" };
	cout << equal(s3.begin(), s3.end(), s4.begin()) << endl;	// 1

}

  • back_inserter在头文件<iterator>里,是一种向容器中添加元素的迭代器。当我们通过一个插入迭代器赋值时,一个与赋值右侧值相等的元素被添加到容器中。
#include <iostream>

#include <algorithm>	// fill_n

#include <vector>

#include <iterator>  // back_inserter

using std::cout;
using std::cin;
using std::endl;
using std::vector;

int main()
{
	vector<int> vec;
	auto it = back_inserter(vec);
	* it = 42; // { 42 }

	fill_n(back_inserter(vec), 5, 0);	// { 42 0 0 0 0 0 }

}
#include <iostream>

#include <algorithm>	// fill_n

#include <vector>

using std::cout;
using std::cin;
using std::endl;
using std::vector;

int main()
{
	vector<int> vec;
	vec.reserve(10);
	fill_n(vec.begin(), 10, 0);		// vec的size依然为0,应该用resize()
}

  • copy()是向目的位置迭代器指向的输出序列中的元素写入数据的算法。传递给copy的目的序列至少要包含与输入序列一样多的元素。
#include <iostream>

#include <algorithm>	// copy

#include <vector>

#include <list>

using std::cout;
using std::cin;
using std::endl;
using std::vector;
using std::list;

int main()
{
	vector<int> vec;
	list<int> lst;
	int i;
	while (cin >> i)
		lst.push_back(i);

	copy(lst.cbegin(), lst.cend(), vec.begin()); // ERROR! vec的size为0

}
  • unique并不真的删除元素,他只是覆盖相邻的重复元素,使得不重复元素出现在序列开始部分。unique返回的迭代器指向最后一个不重复元素之后的位置。此位置之后的元素仍然存在。
#include <iostream>

#include <algorithm>	// copy

#include <vector>

#include <list>

using std::cout;
using std::cin;
using std::endl;
using std::vector;
using std::list;

std::ostream& operator<<(std::ostream& os, vector<int>& v)
{
	for (auto e : v)
		os << e << " ";
	os << endl;
	return os;
}

int main()
{
	vector<int> vec{ 1,2,2,3,3,3,4,4,5 };
	sort(vec.begin(), vec.end());
	cout << vec;		// 1 2 2 3 3 3 4 4 5

	auto end_unique = unique(vec.begin(), vec.end());
	cout << vec;		// 1 2 3 4 5 3 4 4 5

	vec.erase(end_unique, vec.end());
	cout << vec;		// 1 2 3 4 5

}
  • 为什么算法不能改变容器大小?back_inserter可以? 因为对于算法,我们只传递了iterator,对本身容器没法直接访问,但是对于back_inserter,我们需要传递容器本身,所以有权限增加/删减内部元素。

定制操作

谓词 (predicate)

  • 谓词是一个可调用的表达式,其返回结果是一个能用作条件的值。

  • 标准库算法所使用的谓词分为两类:一元谓词(意味着它们只接受一个参数),和二元谓词(意味着它们有两个参数)。

#include <iostream>

#include <algorithm>	// stable_sort

#include <vector>

#include <string>

using std::cout;
using std::cin;
using std::endl;
using std::vector;
using std::string;

std::ostream& operator<<(std::ostream& os, vector<string>& v)
{
	for (auto e : v)
		os << e << " ";
	os << endl;
	return os;
}

bool isShorter(const string& s1, const string& s2)
{
	return s1.size() < s2.size();
}

int main()
{
	vector<string> vs{ "Tong", "April", "Yihui", "Ling", "Wang" };
	std::stable_sort(vs.begin(), vs.end(), isShorter);
	cout << vs;
}

lambda表达式

  • 可调用对象:对于一个对象或一个表达式,如果可以对其使用调用运算符(),则称它为可调用的。

  • 可调用对象:1)函数,2)函数指针,3)重载了函数调用运算符的类,4)lambda表达式。

  • 一个lambda表达式表示一个可调用的代码单元。可以将其理解为一个未命名的内联函数。

  • 与普通函数不同,lambda必须使用尾置返回。

[capture list](parameter list) -> return type { function body }
  • 如果lambda的函数体包括return语句之外的内容,且未指定返回类型,则返回void。被推断返回voidlambda不能返回值,除非我们指定返回类型。
#include <iostream>

#include <algorithm>	// transform

#include <vector>

using std::cout;
using std::cin;
using std::endl;
using std::vector;

std::ostream& operator<<(std::ostream& os, vector<int>& v)
{
	for (auto e : v)
		os << e << " ";
	os << endl;
	return os;
}

int main()
{
	vector<int> vi{ -1, -2, -3, 1, 2, 4 };
	std::transform(vi.begin(), vi.end(), vi.begin(),
		[](int i) { return i < 0 ? -i : i; });		// OK

	cout << vi;	 // {1, 2, 3, 1, 2, 4}

	std::transform(vi.begin(), vi.end(), vi.begin(),
		[](int i) { if (i < 0) return -i; else return i; });	// 这个应该有错误,因为编译器应该推断为void类型。但是VS上运行没问题

	std::transform(vi.begin(), vi.end(), vi.begin(),
		[](int i) -> int { if (i < 0) return -i; else return i; });	// OK
}
  • 与普通函数不同,lambda不能有默认参数。

  • 一个lambda只有在其捕获列表中捕获一个它所在函数中的局部变量,才能在函数体中使用该变量。

  • 一个lambda可以直接使用定义在当前函数之外的名字和局部static变量。

#include <iostream>

#include <algorithm>	// stable_sort

#include <vector>

#include <string>

using std::cout;
using std::cin;
using std::endl;
using std::vector;
using std::string;

std::ostream& operator<<(std::ostream& os, vector<string>& v)
{
	for (auto e : v)
		os << e << " ";
	os << endl;
	return os;
}

bool isShorter(const string& s1, const string& s2)
{
	return s1.size() < s2.size();
}

int main()
{
	vector<string> vs{ "Tong", "April", "Yihui", "Ling", "Wang" };
	std::stable_sort(vs.begin(), vs.end(), isShorter);
	const int sz = 5;
	auto wc = find_if(vs.begin(), vs.end(),
		[sz](const string& a)
		{ return a.size() >= sz; });		// 获取一个迭代器,指向第一个满足size() >= sz的元素

	auto count = vs.end() - wc;
	cout << count << endl;
}
  • lambda采用的值捕获方式的前提是变量可以拷贝。与参数不同,被捕获的变量的值是在lambda创建时拷贝,而不是调用时拷贝。

  • lambda可以采用引用捕获。但必须确保被引用的对象在lambda执行的时候时存在的。

  • 引用捕获有时是必要的,例如不能拷贝的ostream对象。

  • 如果函数返回一个lambda,此lambda也不能包含引用捕获。

  • 隐式捕获:指示编译器推断捕获列表,写一个&=&告诉编译器采用捕获引用方式,=则表示采用值捕获方式。

  • 如果我们可以混合使用隐式捕获和显式捕获。当我们混合使用时,捕获列表中的第一个元素必须是一个&=。而且,显式捕获的变量必须使用与隐式捕获不同的方式。

  • 如果我们希望能改变一个被捕获的值被拷贝的变量的值,就必须在参数列表首加上关键子mutable

int main()
{
	size_t v1 = 42;
	auto f = [v1] () mutable { return ++v1; };
	v1 = 0;
	auto j = f(); // j == 43
}

参数绑定

  • bind函数在<functional>中。它可以被看作是一个通用的函数适配器,接受一个可调用对象,生成一个新的可调用对象来“适应”原对象的参数列表。
auto newCallable = bind(callable, arg_list);
#include <iostream>

#include <algorithm>	// stable_sort

#include <vector>

#include <string>

#include <functional>

using std::cout;
using std::cin;
using std::endl;
using std::vector;
using std::string;
using std::placeholders::_1;

std::ostream& operator<<(std::ostream& os, vector<string>& v)
{
	for (auto e : v)
		os << e << " ";
	os << endl;
	return os;
}

bool isShorter(const string& s1, const string& s2)
{
	return s1.size() < s2.size();
}

int main()
{
	vector<string> vs{ "Tong", "April", "Yihui", "Ling", "Wang" };
	std::stable_sort(vs.begin(), vs.end(), isShorter);
	const int sz = 5;

	auto check_size = [](const string& a, int sz) { return a.size() >= sz; };
	auto checkSz = std::bind(check_size, _ 1, sz);
	auto wc = find_if(vs.begin(), vs.end(), checkSz);

	auto count = vs.end() - wc;
	cout << count << endl;
}
  • 名字 _n都定义在一个名为placeholders的命名空间中,而这个命名空间本身定义在std命名空间中。在头文件<functional>中。
auto g = bind(f, a, b, _ 2, c, _ 1);

// 注意对应关系

// g(_1, _2)

// f(a, b, _2, c, _1)
  • 默认情况下,bind那些不是占位符的参数被拷贝到bind返回的可调用对象中。

  • 为了绑定引用参数,我们可以使用标准库ref或者cref函数 (头文件<functional>)。

for_each(words.begin(), words.end(), bind(print, ref(os), _ 1, ' '));

插入迭代器

  • back_inserter创建一个使用push_back的迭代器。

  • front_inserter创建一个使用push_front的迭代器。

  • inserter创建一个使用insert的迭代器。当调用inserter(c, iter)时,我们得到一个迭代器,接下来使用它时,会将元素插入到iter原来所指向的元素之前的位置。最后得到的迭代器指向原来的元素。

#include <iostream>

#include <algorithm>	// copy

#include <vector>

#include <list>

#include <iterator>

using std::cout;
using std::cin;
using std::endl;
using std::vector;
using std::list;

template <typename T>
std::ostream& operator<<(std::ostream& os, list<T>& v)
{
	for (auto e : v)
		os << e << " ";
	os << endl;
	return os;
}

int main()
{
	vector<int> v{ 1, 2, 3 };
	list<int> v1, v2, v3;
	std::copy(v.begin(), v.end(), std::front_inserter(v1));		// 3 2 1

	std::copy(v.begin(), v.end(), std::back_inserter(v2));		// 1 2 3

	std::copy(v.begin(), v.end(), std::inserter(v3, v3.begin()));	// 1 2 3

}
  • unique_copy
#include <iostream>     // std::cout

#include <algorithm>    // std::unique_copy, std::sort, std::distance

#include <vector>       // std::vector

bool myfunction(int i, int j) {
	return (i == j);
}

int main() {
	int myints[] = { 10,20,20,20,30,30,20,20,10 };
	std::vector<int> myvector(9);                            // 0  0  0  0  0  0  0  0  0

	// using default comparison:

	std::vector<int>::iterator it;
	it = std::unique_copy(myints, myints + 9, myvector.begin());   // 10 20 30 20 10 0  0  0  0
															  //                ^

	std::sort(myvector.begin(), it);                          // 10 10 20 20 30 0  0  0  0
															  //                ^

	// using predicate comparison:

	it = std::unique_copy(myvector.begin(), it, myvector.begin(), myfunction);
	// 10 20 30 20 30 0  0  0  0
	//          ^

	myvector.resize(std::distance(myvector.begin(), it));    // 10 20 30

	// print out content:
	std::cout << "myvector contains:";
	for (it = myvector.begin(); it != myvector.end(); ++it)
		std::cout << ' ' << * it;
	std::cout << '\n';

	return 0;
}

iostream迭代器

  • istream_iterator要读取的类型必须定义了输入运算符。

  • 我们可以默认初始化迭代器,这样就创建了一个可以当作尾后值使用的迭代器。

#include <iostream>     // std::cout

#include <algorithm>    // std::unique_copy, std::sort, std::distance

#include <vector>       // std::vector

#include <iterator>

using std::istream_iterator;
using std::cout;
using std::cin;
using std::endl;
using std::vector;

template <typename T>
std::ostream& operator<<(std::ostream& os, vector<T>& v)
{
	for (auto e : v)
		os << e << " ";
	os << endl;
	return os;
}

int main()
{
	std::istream_iterator<int> in_iter(cin), eof;
	vector<int> v;
	while (in_iter != eof)
		v.push_back(*in_iter++);
	return 0;
}
#include <iostream>     // std::cout

#include <algorithm>    // std::unique_copy, std::sort, std::distance

#include <vector>       // std::vector

#include <iterator>

using std::istream_iterator;
using std::cout;
using std::cin;
using std::endl;
using std::vector;

template <typename T>
std::ostream& operator<<(std::ostream& os, vector<T>& v)
{
	for (auto e : v)
		os << e << " ";
	os << endl;
	return os;
}

int main()
{
	std::istream_iterator<int> in_iter(cin), eof;
	vector<int> v(in_iter, eof);
	cout << v;
	return 0;
}
  • 当创建一个ostream_iterator时,我们可以提供(可选的)第二参数,它是一个字符串,在输出每个元素后都会打印此字符串。
#include <iostream>     // std::cout

#include <algorithm>    // std::unique_copy, std::sort, std::distance

#include <vector>       // std::vector

#include <iterator>

using std::ostream_iterator;
using std::cout;
using std::cin;
using std::endl;
using std::vector;

template <typename T>
std::ostream& operator<<(std::ostream& os, vector<T>& v)
{
	for (auto e : v)
		os << e << " ";
	os << endl;
	return os;
}

int main()
{
	std::ostream_iterator<int> out_iter(cout, "-");
	vector<int> v{ 1, 2, 3, 4, 5 };

    // 下面等价于copy(v.begin(), v.end(), out_iter);

	for (auto e : v)
		* out_iter++ = e;
	cout << endl; // 1-2-3-4-5-

}

反向迭代器

  • 将一个反向迭代器转换成一个普通迭代器,可以用base(),但是它们会指向不同的元素,因为“左闭右开”的特性。

  • 当我们从一个普通迭代器初始化一个反向迭代器,或是给一个反向迭代器赋值时,结果迭代器与原迭代器指向的并不是相同的元素。

#include <iostream>     // std::cout

#include <algorithm>    // std::unique_copy, std::sort, std::distance

#include <iterator>

#include <string>

using std::cout;
using std::cin;
using std::endl;
using std::string;

int main()
{
	string s("Hello, Tong, how are you");
	auto rcomma = find(s.crbegin(), s.crend(), ',');
	cout << string(s.crbegin(), rcomma) << endl;	// "uoy era woh "

	cout << string(rcomma.base(), s.cend()) << endl;	// " how are you"

}

关联容器

  • 严格弱序 (strict weak ordering):关联容器所使用的关键字间的关系。在一个严格弱序中,可以比较任意两个值并确定哪个更小。若任何一个都不小于另一个,则认为两个值相等。

  • 对于有序容器-map, multimap, set以及multiset,关键字类型必须定义元素比较的方法。

  • 默认情况下,标准库使用关键字类型的<运算符来比较两个关键字。

  • 我们可以定义map<vector<int>::iterator, int>,因为vector<int>::iterator支持使用<进行比较。

  • 我们不可以定义map<list<int>::iterator, int>因为它不支持比较。

#include <iostream>

#include <set>

using namespace std;

struct Person
{
	int age;
	string name;
};

bool compareAge(const Person& p1, const Person& p2)
{
	return p1.age < p2.age;
}

int main()
{
	Person p1 = { 1 };
	Person p2 = { 2 };

	set<Person, decltype(compareAge)*> dict1(compareAge);
	set<Person, bool (*)(const Person & p1, const Person & p2 )> dict2(compareAge);

	dict2.insert(p1);
	dict2.insert(p2);

}
  • set的迭代器iteratorconst_iterator都是const的,这两种类型只允许只读访问,不能修改。

  • set类型中的元素是const的,map中的元素是pair,其第一个成员是const的。

  • multiset不能使用back_inserter,因为它没有push_back。但是它可以使用inserter

  • map添加元素
    • word_count.insert({word, 1});
    • word_count.insert(make_pair(word, 1));
    • word_count.insert(pair<string, size_t>(word, 1));
    • word_count.insert(map<string, size_t>::value_type(word, 1));
  • 对于mapset,只有当元素的关键字不再c中时才插入insert(或构造emplace)元素。函数返回一个pair,包含一个迭代器,指向具有指定关键字的元素,以及一个指示插入是否成功的bool值。
#include <iostream>

#include <map>	// map

#include <string>	// string

#include <cstddef> // size_t

using namespace std;

int main()
{
	map<string, size_t> word_count;
	string word;
	while (cin >> word)
	{
		auto ret = word_count.insert({ word, 1 });
		if (!ret.second)
			++ret.first->second;   // 等价于 ++((ret.first)->second)

	}
}

  • 由于一个multi容器中的关键字不必唯一,在这些类型上调用insert总会插入一个元素,返回一个指向新元素的迭代器。

  • 关联容器提供一个额外的erase操作,它接受一个key_type参数。此版本删除所有匹配给定关键字的元素(如果存在的话),返回实际删除的元素的数量。

  • 由于下标运算符可能插入一个新元素,我们只可以对非constmap使用下标操作。

  • c[k] vs. c.at(k)
    • c[k]返回关键字为k的元素:如果k不在c中,添加一个关键字为k的元素,对其进行值初始化。
    • c.at(k)访问关键字为k的元素,带参数检查:若k不在c中,抛出一个out_of_range异常。
  • vectorstring不同,map的下标运算符返回的类型与解引用map迭代器得到的类型不同。

  • 如果一个multimapmultiset中有多个元素具有给定关键字,则这些元素在容器中会相邻存储。
#include <iostream>

#include <map>	// map

#include <string>	// string

#include <cstddef> // size_t

using namespace std;

int main()
{
	multimap<string, string> authors;
	authors.insert({ "Tong", "good book" });
	authors.insert({ "April", "better book" });
	authors.insert({ "April", "best book" });
	authors.insert({ "April", "bestest book" });
	authors.insert({ "Tong", "awesome book" });
	auto entries = authors.count("Tong");
	auto iter = authors.find("Tong");
	while (entries)
	{
		cout << iter->second << endl;
		++iter;
		--entries;
	}
}
  • c.lower_bound(k):返回一个迭代器,指向第一个关键字不小于k的元素

  • c.upper_bound(k):返回一个迭代器,指向第一个关键字大于k的元素

  • c.equal_range(k):返回一个迭代器pair,表示关键字等于k的元素的范围。若k不存在,pair的两个成员均等于c.end()

#include <iostream>

#include <map>	// map

#include <string>	// string

#include <cstddef> // size_t

using namespace std;

int main()
{
	multimap<string, string> authors;
	authors.insert({ "Tong", "good book" });
	authors.insert({ "April", "better book" });
	authors.insert({ "April", "best book" });
	authors.insert({ "April", "bestest book" });
	authors.insert({ "Tong", "awesome book" });
	auto entries = authors.count("Tong");
	auto iter = authors.find("Tong");

	for (auto beg = authors.lower_bound("Tong"),
		end = authors.upper_bound("Tong");
		beg != end; ++beg)
		cout << beg->second << endl;

	for (auto pos = authors.equal_range("Tong");
		pos.first != pos.second; ++pos.first)
		cout << pos.first->second << endl;
}

管理桶

  • 无序容器在存储上组织为一组桶,每个桶保存零个或多个元素。

  • 无序容器使用一个哈希函数将元素映射到桶。

  • 容器将具有一个特定哈希值的所有元素都保存在相同的桶中。

  • 如果容器允许重复关键字,所有具有相同关键字的元素也都会在同一个桶中。

  • 无序容器的性能依赖于哈希函数的质量和桶的数量和大小。

  • 无序容器使用关键字类型的==运算符和一个hash<key_type>类型的对象来组织元素。

unordered_<> and ordered_<>

原文链接:https://blog.csdn.net/haluoluo211/article/details/82468061

  • Implementation
    • set基于红黑树实现,红黑树具有自动排序的功能,因此map内部所有的数据,在任何时候,都是有序的。
    • unordered_set基于哈希表,数据插入和查找的时间复杂度很低,几乎是常数时间,而代价是消耗比较多的内存,无自动排序功能。底层实现上,用一个下标范围比较大的数组来存储元素,形成很多的桶,利用hash函数对key进行映射到不同区域进行保存。
  • Ordering
    • set: increasing order (by default)
    • unordered_set: no ordering
  • Search time
    • set: log(n)
    • unordered_set: average -> O(1); worst case -> O(n)
  • Insertion time
    • set: log(n) + rebalance
    • unordered_set: same as search
  • Deletion time
    • set: log(n) + rebalance
    • unordered_set: same as search
  • Key的类型
#include <iostream>
#include <vector>
#include <unordered_set>
#include <set>
using namespace std;

ostream& operator<<(ostream& stream, const vector<int>& nums)
{
	for (const int num : nums)
		stream << num << " ";
	return stream;
}

int main()
{
	vector<int> v{ 1, 2, 3, 4, 5 };
	// unordered_set<vector<int>> usv; // unordered_set不能使用vector<int>作为key,因为vector<int>没有hash函数,除非自己定义一个

	set<vector<int>> sv;  // set可以使用vector<int>作为key,因为vector 重载了 operator<

    sv.insert({ 1, 2 });
	sv.insert({ 1, 3 });
	sv.insert({ 1, 2, 3 });
	sv.insert({ 4, 1 });
	for (vector<int> v : sv)
		cout << v << endl;
}

上面程序输出

1 2
1 2 3
1 3
4 1

动态内存

  • 静态内存用来保存局部static对象,类static数据成员,以及定义在任何函数之外的变量。

  • 栈内存用来保存定义在函数内的非static对象。

  • 分配在静态或栈内存中的对象由编译器自动创建和销毁。

  • 栈对象仅在其定义的程序块运行时才存在;static对象在使用之前分配,在程序结束时销毁。

  • 每个程序还拥有一个内存池,这部分被称作自由空间 (free store)或堆 (heap)。

  • 程序用堆来存储 动态分配 (dynamically allocate) 的对象—即,那些在程序运行时分配的对象。动态对象的生存期由程序来控制。也就是说,当动态对象不再使用时,我们的代码必须显式地销毁它们。

  • <memory>

  • 使用动态内存的三种原因:

    • 程序不知道自己需要使用多少对象
    • 程序不知道所需对象的准确类型
    • 程序需要在多个对象间共享数据

#### shared_ptr

  • make_shared是最安全地分配和使用动态内存的方法。如果我们不传递任何参数,对象就会进行值初始化。

  • 一旦一个shared_ptr的计数器变为0,它就会自动释放自己所管理的对象。

#include <iostream>

#include <memory>	 

using namespace std;

int main()
{
	shared_ptr<int> p1 = make_shared<int>(42);
	auto p2 = make_shared<int>();
	auto p3(p2); // 拷贝了一个shared_ptr, 计数器都会递增

	cout << p1.use_count() << endl; // 1

	p1 = p3; // p1原指向的计数器减1,所以自动销毁;p3指向的加1,所以为3

	cout << p1.use_count() << endl; // 3

    shared_ptr<int> p4(new int(42)); // OK

    shared_ptr<int> p5 = new int(42); // ERROR! 必须使用直接初始化形式,在函数传递参数也要注意

    int* x(new int(42));

    shared_ptr<int> p6(x); // OK, 但不建议这么做,我们无法知道对象何时会被销毁

}
  • 如果将shared_ptr存放于一个容器中,而后不再需要全部元素,而只使用其中一部分,要记得用erase删除不再需要的那些元素。

  • get()返回一个内置指针,指向智能指针管理的对象。但是,不要把另一个智能指针也绑定到get返回的指针上。

#include <iostream>

#include <memory>	 

using namespace std;

int main()
{
	shared_ptr<int> p(new int(42));
	int* q = p.get(); // OK

	{
		shared_ptr<int> p1(q); // Undefined!!! 两个独立的shared_ptr指向相同的内存

	}

	int foo = * p; // Undefined!!! p指向的内存已经被释放了

}
  • 如果使用智能指针,即使程序块过早结束,智能指针类也能确保在内存不再需要时将其释放。

直接管理内存

  • new
int *pi1 = new int; // 默认初始化,未定义
int *pi2 = new int(); // 值初始化,0
  • 默认情况下,如果new不能分配所要求的内存空间,它会抛出一个类型为bad_alloc的异常。

  • 定位new (placement new)允许我们向new传递额外的参数。

  • nothrow可以防止抛出异常,并且返回一个空指针。

#include <iostream>

#include <memory>	 

using namespace std;

int main()
{
	int* p1 = new int;
	int* p2 = new (nothrow) int; // 如果失败,返回一个空指针

}

  • 传递给delete的指针必须是指向动态分配的内存,或者是一个空指针。

  • 释放一块并非new分配的内存,或者将相同的指针值释放多次,其行为是未定义的。

#include <iostream>

#include <memory>	 

using namespace std;

int main()
{
	int i;
	int* pi1 = &i;
	int* pi2 = nullptr;

	double* pd = new double(33);
	double* pd2 = pd;

	delete i; // ERROR! i不是指针

	delete pi1; // Undefined! pi1指向一个局部变量

	delete pd; // OK

	delete pd2; // Undefined! pd2指向的内存已经被释放了

	delete pi2; // OK, 释放一个空指针总是没有错误的

}
  • 由内置指针(而不是智能指针)管理的动态内存在被显式释放前一直都会存在。

智能指针陷阱

  • 不使用相同的内置指针值初始化(或reset)多个智能指针。

  • delete get()返回的指针。

  • 不使用get()初始化或reset另一个智能指针。

  • 如果你使用get()返回的指针,记住当最后一个对应的智能指针销毁后,你的指针就变为无效了。

  • 如果你使用智能指针管理的资源不是new分配的内存,记住传递给它一个删除器(deleter)。

unique_ptr

  • 一个unique_ptr“拥有”它所指向的对象。某个时刻只能有一个unique_ptr指向一个给定对象。

  • unique_ptr不支持普通的拷贝或者赋值操作。

  • release会切断unique_ptr和它原来管理的对象间的联系,返回指针,并将原unique_ptr置空。返回的指针通常被用来初始化另一个智能指针或给另一个智能指针赋值。如果我们不用另一个智能指针来保存release返回的指针,我们的程序就要负责资源的释放。

#include <iostream>

#include <memory>	 

using namespace std;

int main()
{
	unique_ptr<int> p1(new int(42));

	unique_ptr<int> p2(p1); // ERROR! unique_ptr不支持拷贝

	unique_ptr<int> p3;
	p3 = p1; // ERROR! unique_ptr不支持拷贝

	unique_ptr<int> p4(p1.release()); // OK, p1 -> p4, p1被置空
	unique_ptr<int> p5(new int(100));

	p4.reset(p5.release()); // OK, p5 -> p4, reset释放了p4原来指向的内存

	p4.release(); // ERROR! p4不会释放内存,而且我们丢失了指针

	auto p = p4.release(); // OK, 我们必须记得 delete (p)

}

  • 不能拷贝unique_ptr的规则有一个例外,我们可以拷贝或赋值一个将要被销毁的unique_ptr。例如从函数返回一个unique_ptr
#include <iostream>

#include <memory>	 

using namespace std;

unique_ptr<int> clone(int p)
{
	return unique_ptr<int>(new int(p));
}

unique_ptr<int> clone2(int p)
{
	unique_ptr<int> ret(new int(p));
	return ret;
}

int main()
{
	auto p1 = clone(42);
	auto p2 = clone2(42);
}

weak_ptr

  • weak_ptr是一种不控制所指向对象生存期的智能指针,它指向一个由shared_ptr管理的对象,并且不会改变shared_ptr的引用计数。

  • 由于对象可能不存在,我们不能使用weak_ptr直接访问对象,而必须调用lock。此函数检查weak_ptr所指向的对象是否仍存在。如果存在,lock返回一个指向共享对象的shared_ptr。否则返回一个空shared_ptr

动态数组

  • 动态数组并不是数组类型,而是一个数组类型元素的指针。
int* pia = new int[10]; // 未初始化

int* pia2 = new int[10](); // 值初始化为0

int* pia3 = new int[10]{1, 2, 3, 4, 5, 6, 7, 8, 9, 10}; // OK
  • 当我们用new分配一个大小为0的数组时,new返回的是一个合法的飞空指针。
char arr[0];   // ERROR! 不能定义长度为0的数组

char* cp = new char[0]; // OK. 但cp不能解引用
  • 为了释放动态数组,我们要在指针前加上一个空方括号。

  • 数组中的元素按逆序销毁。

typedef int arrT[42];
int* p = new arrT;
delete [] p;
  • 特殊的unique_ptr可以指向一个动态数组,并且使用下标运算符访问数组中的元素。
unique_ptr<int []> up(new int[10]);
up.release(); // 自动用delete[]销毁其指针

for (size_t i = 0; i != 10; ++i)
    up[i] = i;
  • shared_ptr不支持管理动态数组,除非自己定义删除器。而且无法用下标运算符。
shared_ptr<int> sp(new int[10], [](int* p) { delete[] p; });
sp.reset(); // 使用我们提供的lambda释放数组
for (size_t i = 0; i != 10; ++i)
    *(sp.get() + i) = i;

allocator

  • <memory>

  • 它帮助我们将内存分配和对象构造分离开来。

#include <iostream>

#include <memory>	 

#include <string>

using namespace std;


int main()
{
	int n = 30;
	allocator<string> alloc;
	auto const p = alloc.allocate(n); // 分配30个未初始化的string

	auto q = p;  // q指向最后构造的元素之后的元素

	alloc.construct(q++, 10, 'c');

	cout << * p << endl;

	while (q != p)
		alloc.destroy(--q); // 释放我们真正构造的string

	alloc.deallocate(p, n); // 大小参数必须和之前相同
}

编写一个智能指针类

智能指针是一种数据类型,一般用模板实现,模拟指针行为的同时还提供自动垃圾回收机制。它会自动记录SmartPointer<T*>对象的引用计数,一旦T类型对象的引用计数为零,就会释放该对象。

#include <iostream>

using namespace std;

template <class T>
class SmartPointer
{
public:
	SmartPointer(T* ptr)
	{
		ref = ptr;
		ref_count = (unsigned*)malloc(sizeof(unsigned));
		*ref_count = 1;
	}

	SmartPointer(SmartPointer<T>& sptr)
	{
		ref = sptr.ref;
		ref_count = sptr.ref_count;
		++(*ref_count);
	}

	SmartPointer<T>& operator=(SmartPointer<T>& sptr)
	{
		if (this == &sptr) return *this;

		/* If already assigned to an object, remove one reference */
		if (*ref_count > 0) remove();

		ref = sptr.ref;
		ref_count = sptr.ref_count;
		++(*ref_count);
		return *this;
	}

	~SmartPointer()
	{
		remove(); // Remove one reference to object

	}

	T getValue()
	{
		return *ref;
	}

protected:
	void remove()
	{
		--(*ref_count);
		if (*ref_count == 0) delete ref;
		free(ref_count);
		ref = nullptr;
		ref_count = nullptr;
	}

	T* ref;
	unsigned* ref_count;
};


int main()
{
	return 0;
}

Typical beginner error

#include <iostream>

#include <memory>

int main()
{
	int a = 0;
	auto a_ptr = std::unique_ptr<int>(&a);
	return 0;
}
  • Create a smart pointer from a pointer to a stack-managed variable
  • The variable ends up being owned both by the smart pointer and the stack and gets deleted twice → Error!

内存泄漏 (Memory Leak)

Memory leak if nobody has freed the memory.

  • 忘记delete或者delete两次同一个地址
#include <iostream>

using namespace std;

int main()
{
	double* ptr_1 = nullptr;
	double* ptr_2 = nullptr;
	int size = 10;

	// Allocate memory for two arrays on the heap

	ptr_1 = new double[size];
	ptr_2 = new double[size];
	cout << "1: " << ptr_1 << " 2: " << ptr_2 << endl;		// 1: 00FF5670 2: 00FF5398

	ptr_2 = ptr_1; // ptr_2 overwritten. PROBLEM: no change to access the previous memory of ptr_2, 之前的内存没法删除了

	cout << "1: " << ptr_1 << " 2: " << ptr_2 << endl;		// 1: 00FF5670 2: 00FF5670

	delete[] ptr_1;
	delete[] ptr_2;    // 同一个地址删了两次

	return 0;
}
  • If we run out of memory, std::bad_alloc error is thrown.
#include <iostream>
#include <cmath>
#include <algorithm>

using namespace std;

int main()
{
	double* data = nullptr;
	size_t size = pow(1024, 3) / 8; // Produce 1 GB

	for (int i = 0; i < 5; ++i)
	{
		// Allocate memory for the data

		data = new double[size];
		fill(data, data + size, 1.23);

		cout << "Iteration: " << i << " done!" << endl;
	}

	delete[] data; // PROBLEM: This will only free the last allocation!!!

	return 0;
}

悬挂指针(Dangling pointer)

Dangling pointer if somebody has freed the memory in a function.

#include <iostream>

using namespace std;

int main()
{
	int size = 5;
	int* ptr_1 = new int[size];
	int* ptr_2 = ptr_1;

	ptr_1[0] = 100;
	cout << "1: " << ptr_1 << " 2: " << ptr_2 << endl;	// 1: 01245D78 2: 01245D78
	cout << "ptr_2 [0]: " << ptr_2[0] << endl;	// ptr_2 [0]: 100

	delete[] ptr_1; // Free memory.

	ptr_1 = nullptr;
	cout << "1: " << ptr_1 << " 2: " << ptr_2 << endl;	// 1: 00000000 2: 01245D78

	// PROBLEM: Data under ptr_2 does not exist anymore!

	cout << "ptr_2 [0]: " << ptr_2[0] << endl; // ptr_2[0]: -572662307

	return 0;
}

普通指针如何实现一块内存只有一个指针指向这种功能

拷贝控制

  • 拷贝控制操作
    • 拷贝构造函数 (copy constructor)
    • 拷贝赋值运算符 (copy-assignment operator)
    • 移动构造函数 (move constructor)
    • 移动赋值运算符 (move-assignment operator)
    • 析构函数 (destructor)

拷贝构造函数

  • 拷贝构造函数的第一个参数必须是一个引用类型。因为拷贝构造函数被用来初始化非引用类类型参数。如果其参数不是引用类型,则调用永远不会成功—为了调用拷贝构造函数,我们必须拷贝它的实参,但为了拷贝实参,我们又需要拷贝构造函数,如此无限循环。
class foo
{
public:
    Foo ();
    Foo (const Foo&);
}
  • 拷贝构造函数通常不应该是explicit的。

  • 拷贝初始化通常是由拷贝构造函数或者移动构造函数来完成的。

  • 拷贝构造发生的情况
    • =定义变量时
    • 将一个对象作为实参传递给一个非引用类型的形参
    • 从一个返回类型未非引用类型的函数返回一个对象
    • 用花括号列表初始化一个数组中的元素或一个聚合类中的成员
    • 初始化标准库容器或是调用其insertpush成员采用拷贝初始化。但是,用emplace创建的元素都直接初始化。
  • 初始化问题
#include <iostream>

using namespace std;

class A
{
private:
	int value;

public:
	A(int n) { value = n; }

	// PROBLEM: 复制构造函数A(A other)传入的参数是A的一个实例。由于是传值函数,我们把形参复制到实参会调用复制构造函数。因此,如果允许复制构造函数传值,就会在复制构造函数内调用复制构造函数,就会形成无休止的递归调用从而导致栈溢出

	A(A other) { value = other.value; }  

	void Print() { cout << value << endl; }
};

int main()
{
	A a = 10;
	A b = a;
	b.Print();

	return 0;
}

将上述代码修改如下后才能运行

#include <iostream>

using namespace std;

class A
{
private:
	int value;

public:
	A(int n) { value = n; }
	A(const A& other) { value = other.value; }  

	void Print() { cout << value << endl; }
};

int main()
{
	A a = 10;
	A b = a;
	b.Print();

	return 0;
}

拷贝赋值函数

  • 赋值运算符通常应该返回一个指向其左侧运算对象的引用。
class foo
{
public:
    Foo& operator=(const Foo&);
}

析构函数

  • 构造函数初始化对象的非static数据成员;析构函数释放对象使用的资源,并销毁对象的非static数据成员。

  • 隐式销毁一个内置指针类型的成员不会delete它所指向的对象。

  • 无论何时一个对象被销毁,就会自动调用其析构函数:
    • 变量离开作用域时被销毁。
    • 当一个对象被销毁时,其成员被销毁。
    • 容器(无论是标准库容器还是数组)被销毁时,其元素被销毁。
    • 对于动态分配的对象,当对指向它的指针应用delete运算符时被销毁。
    • 对于临时对象,当创建它的完整表达式结束时被销毁。
  • 当指向一个对象的引用或指针离开作用域时,析构函数不会执行。
#include <iostream>

#include <string>

using namespace std;

class HasPtr
{
public:
	HasPtr(const string& s = string()) :
		ps(new string(s)), i(0) { }

	HasPtr(const HasPtr& ori) :
		ps(new string(*ori.ps)), i(ori.i) { }

	HasPtr& operator=(const HasPtr& ori)
	{
		auto new_ps = new string(*ori.ps);
		delete ps;
		ps = new_ps;
		i = ori.i;
		return *this;
	}

	~HasPtr() { delete ps; }

	string getPs() { return *ps; }

private:
	string* ps;
	int i;
};

int main()
{
	HasPtr ori("Tong");
	HasPtr sec = ori;
	HasPtr thi;
	thi = ori;
	cout << sec.getPs() << endl;
	cout << thi.getPs() << endl;
}
  • 如果一个类需要自定义析构函数,几乎可以肯定它也需要自定义拷贝赋值运算符和拷贝构造函数。

  • 需要拷贝操作的类也需要赋值操作,反之亦然。但这不必然意味着也需要析构函数。

阻止拷贝

  • 我们只能对具有合成版本的成员函数使用=default(即,默认构造函数或拷贝控制成员),这样来显式地要求编译器生成合成的版本。

  • 当我们在类内用=default修饰成员的声明时,合成的函数将隐式地声明为内联地(就像任何其他类内声明地成员函数一样)。如果我们不希望合成的成员时内联函数,应该只对成员外定义使用=default,就像对拷贝赋值运算符所做的那样。

  • 我们可以通过将拷贝构造函数和拷贝赋值运算符定义为=delete来阻止拷贝。

  • =default不同
    • =delete必须出现在函数第一次声明的时候。
    • =delete可以对任何函数指定
  • 如果析构函数被删除,就无法销毁此类型的对象了。

  • 对于一个删除了析构函数的类型,编译器将不允许定义该类型的变量或创建该类的临时对象,但可以动态分配这种类型的对象。但是,我们不能释放指向该类型动态分配对象的指针。
struct NoDtor
{
	NoDtor() = default;
	~NoDtor() = delete;
};

NoDtor nd; // ERROR

NoDtor* p = new NoDtor(); // OK

delete p; // ERROR
  • 如果一个类有数据成员不能默认构造,拷贝,复制或销毁,则对应的成员函数将被定义为删除的。

拷贝控制和资源管理

  • 编写赋值运算符时,
    • 如果将一个对象赋予它自身,赋值运算符必须能正确工作。
    • 大多数赋值运算符组合了析构函数和拷贝构造函数的工作。一个好的方法是先将右侧运算对象拷贝到一个局部临时变量。当拷贝完成时,销毁左侧运算对象的现有成员就是安全的了。一旦左侧运算对象的资源被销毁,就只剩下将数据从临时对象拷贝到左侧运算对象的成员中了。
  • 错误的示范
// ERROR

HasPtr& HasPtr::operator=(const HasPtr& rhs)
{
	delete ps;
	ps = new string(*rhs.ps); // 如果 rhs和*this是同一个对象,我们无法从已经释放的内存中拷贝数据!
	i = rhs.i;
	return * this;
}
  • 正确的示范
// OK

HasPtr& HasPtr::operator=(const HasPtr& rhs)
{
	auto newp = new string(*rhs.ps);
	delete ps;
	ps = newp;
	i = rhs.i;
	return * this;
}
  • 定义一个引用计数的类
#include <iostream>

#include <string>

#include <cstddef> // size_t

using namespace std;

class HasPtr
{
public:
	HasPtr(const string& s = string()) :
		ps(new string(s)), i(0), use(new size_t(1)) { }

	HasPtr(const HasPtr& ori) :
		ps(new string(*ori.ps)), i(ori.i), use(ori.use) { ++* use; }

	HasPtr& operator=(const HasPtr& rhs);

	~HasPtr();

	string getPs() { return *ps; }

private:
	string* ps;
	int i;
	size_t* use; // 用来记录有多少个对象共享*ps的成员

};


HasPtr& HasPtr::operator=(const HasPtr& rhs)
{
	++ * rhs.use;
	if (-- * use == 0)
	{
		delete ps;
		delete use;
	}
	ps = rhs.ps;
	i = rhs.i;
	use = rhs.use;
	return * this;
}

HasPtr::~HasPtr()
{
	if (-- * use == 0)
	{
		delete ps;
		delete use;
	}
}


int main()
{
	HasPtr ori("Tong");
	HasPtr sec = ori;
	HasPtr thi;
	thi = ori;
	cout << sec.getPs() << endl;
	cout << thi.getPs() << endl;
}

动态内存管理类


对象移动

  • 标准库容器,stringshared_ptr类既支持移动也支持拷贝。IO类和unique_ptr类可以移动但不能拷贝。

  • 右值引用指向将要被销毁的对象。

  • 左值引用可绑定
    • 返回左值引用的函数
    • 赋值运算符
    • 下标运算符
    • 解引用
    • 前置递增/递减
    • 变量表达式
  • 右值引用或const左值引用可绑定
    • 返回非引用类型的函数
    • 算数运算符
    • 关系运算符
    • 位运算符
    • 后置递增/递减运算符
  • 左值持久,右值短暂。

  • 变量时左值,因此我们不能将一个右值引用直接绑定到一个变量上,即使这个变量是右值引用类型也不行。

  • std::move<utility>里。

  • 当我们使用move时,直接调用std::move而不是move
int&& rr1 = 42; // OK

int&& rr2 = rr1; // ERROR!

int&& rr3 = std::move(rr1); // OK, rr1称作移后源 (moved-from) 对象
  • 我们可以销毁一个移后源对象,也可以赋予它新值,但不能使用一个移后源对象的值。

  • 一旦资源完成移动,源对象必须不再指向被移动的资源,这些资源的所有权已经归属新创建的对象。

  • noexcept通知标准库我们的构造函数不抛出任何异常,否则标准库会认为移动我们的类对象时可能会抛出异常,并且为了处理这种可能性而做一些额外的工作。

  • 不抛出异常的移动构造函数和移动赋值运算符必须标记为noexcept

  • 必须在类头文件的声明中和定义中(如果定义在类外的话)都指定noexcept

  • 在移动操作之后,移后源对象必须保持有效的,可析构的状态,但是用户不能对其值进行任何假设。

  • 只有当一个类没有定义任何自己版本的拷贝控制成员,且类的每个非static数据成员都可以移动时,编译器才会为它合成移动构造函数或移动赋值匀速那副。

  • 什么时候将合成的移动操作定义为删除的函数?
    • 有类成员定义了自己的拷贝构造函数且未定义移动构造函数,或者是有类成员未定义自己的拷贝构造函数且编译器不能为其合成移动构造函数。移动赋值运算符类似。
    • 有类成员的移动构造函数或移动赋值运算符被定义为删除的或是不可访问的。
    • 如果类的析构函数被定义为删除的或不可访问的。
    • 如果有类成员是const的或是引用,则类的移动赋值运算符被定义为删除的。
  • 定义了一个移动构造函数或移动赋值运算符的类必须也定义自己的拷贝操作。否则,这些成员默认地被定义为删除的。

  • 如果一个类有一个可用地拷贝构造函数而没有移动构造函数,则其对象是通过拷贝构造函数来“移动”地。

  • 移动迭代器 (move iterator):移动迭代器地解引用运算符生成一个右值引用。我们可以调用make_move_iterator函数将一个普通迭代器转换成一个移动迭代器。

  • 区分移动和拷贝地重载函数通常有一个版本接受一个const T&,而另一个版本接受一个T&&

  • 指出this左值/右值属性的方式与定义const成员函数相同,即,在参数列表后放置一个引用限定符 (reference qualifier) &
class Foo
{
public:
    Foo& operator=(const Foo&) &; // 只能向可修改的左值赋值

};

Foo& Foo::operator=(const Foo& rhs) &
{
    return * this;
}
  • 一个函数可以同时用const和引用限定。在此情况下,引用限定符必须跟随在const限定符之后。

  • 如果一个成员函数有引用限定符,则具有相同参数列表的所有版本都必须有引用限定符。

#include <iostream>

#include <vector>

#include <algorithm>

using namespace std;

class Foo
{
public:
	Foo sorted() &&; // 可用于可改变得右值

	Foo sorted() const &; // 可用于任何类型的Foo

private:
	vector<int> data;

};

Foo Foo::sorted() &&
{
	cout << "I am for a right value" << endl;
	sort(data.begin(), data.end());
	return * this;
}

Foo Foo::sorted() const &
{
	cout << "I am for a left value" << endl;
	Foo ret(* this);
	sort(ret.data.begin(), ret.data.end());
	return ret;
}

Foo& retFoo(Foo& f)
{
	return f;
}

Foo retVal()
{
	return Foo();
}

int main()
{
	retVal().sorted(); // 调用Foo::sorted() &&
	Foo f;
	retFoo(f).sorted(); // 调用Foo::sorted() const &
}

重载类型与类型转换

基本概念

  • 当一个重载的运算符是成员函数时,this绑定到左侧运算对象。成员运算符函数的(显式)参数数量比运算对象的数量少一个。

  • 对于一个运算符函数来说,它或者是类的成员,或者至少含有一个类类型的参数。

  • 对于一个重载的运算符来说,其优先级和结合律与对应的内置运算符保持一致。

  • 运算符的重载版本无法保留求职顺序和/或短路求值属性。

  • 通常情况下,不应该重载逗号,取地址,逻辑与和逻辑或运算符。

  • 判断该将运算符定义为成员函数还是普通的非成员函数:

    • 赋值(=),下标([]),调用(()),和成员访问箭头(->)运算符 必须 是成员。
    • 复合赋值运算符一般来说应该是成员,但并非必须,这一点与赋值运算符略有不同。
    • 改变对象状态的运算符或者与给定类型密切相关的运算符,如递增(++),递减(--)和解引用(*)运算符,通常应该是成员。
    • 具有对称性的运算符可能转换任意一端的运算对象,例如算术,相等性,关系和位运算符等,因此它们通常应该是普通的非成员函数。

输入和输出运算符

  • 通常,输出运算符应该主要负责打印对象的内容而非控制格式,输出运算符不应该打印换行符。

  • 输入输出运算符必须是非成员函数。

  • IO运算符一般被声明为友元。

  • 输入运算符必须处理输入可能失败的情况,而输出运算符不需要。

  • 当读取操作发生错误时,输入运算符应该负责从错误中恢复。

  • 在执行输入运算符时可能发生以下错误:

    • 当流含有错误类型的数据时读取操作可能失败。
    • 当读取操作到达文件末尾或者遇到输入流的其他错误时也会失败。
#include <iostream>

#include <vector>

std::istream& operator>>(std::istream& is, std::vector<int>& v)
{
	int i;
	while (is >> i)
		v.push_back(i);
	return is;
}

std::ostream& operator<<(std::ostream& os, const std::vector<int>& v)
{
	for (const int& e : v)
		os << e << " ";
	return os;
}

int main()
{
	std::vector<int> v;
	std::cin >> v;
	std::cout << v;
}

赋值运算符

  • 赋值运算符必须定义成类的成员,复合赋值(+=)运算符通常情况下也应该这样做。这两类运算符都应该返回左侧运算对象的引用。
class StrVec
{
public:
    StrVec& operator=(std::initializer_list<std::string>);    
};

StrVec& StrVec::operator=(initializer_list<string> il)
{
    auto data = alloc_n_copy(il.begin(), il.end());
    free();
    elements = data.first;
    first_free = cap = data.second;
    return * this;
}

下标运算符

  • 下标运算符必须是成员函数。

  • 如果一个类包含下标运算符,则通常会定义两个版本:一个返回普通引用,另一个是类的常量成员并且返回常量引用。

递增和递减运算符

  • 定义递增和递减运算符的类应该同时定义迁至版本和后置版本。这些运算符通常应该被定义成类的成员。

  • 为了与内置版本保持一致,前置运算符应该返回递增或递减后对象的引用。

  • 为了与内置版本保持一致,后置运算符应该返回对象的原值(递增或递减之前的值),返回的形式是一个值而非引用。而且,后置版本接受一个额外的不被使用的int类型的形参。

  • 要想显式调用后置运算符,我们必须为它的整型参数传递一个值。

StrBlobPtr& operator++(); //  前置

StrBlobPtr operator++(int); // 后置

p.operator++(0);  // 后置

p.operator++(); // 前置

成员访问运算符

  • 箭头(->)运算符必须是类的成员。

  • 解引用(*)运算符通常也是类的成员,尽管并非必须如此。

  • 重载的箭头运算符必须返回类的指针或者自定义了箭头运算符的某个类的对象。

函数调用运算符

#include <functional>
#include <vector>
#include <string>
#include <iostream>
#include <algorithm>

int main() {
  using namespace std::placeholders;

  std::vector<int> vi { 1000, 2000, 3000, 4000, 5000 };
  std::cout << std::count_if(vi.begin(), vi.end(), std::bind(std::greater<int>(), _1, 1024)) << std::endl;

  std::vector<std::string> vs { "pooh", "pooh", "abc", "pooh" };
  std::cout << * std::find_if(vs.begin(), vs.end(), std::bind(std::not_equal_to<std::string>(), _1, "pooh")) << std::endl;

  std::transform(vi.begin(), vi.end(), vi.begin(), std::bind(std::multiplies<int>(), _1, 2));
  for (const auto &i : vi)
    std::cout << i << ' ';
  std::cout << std::endl;

  return 0;
}
  • 两个不同类型的可调用对象可能共享同一种调用形式 (call signature)。

  • 调用形式指明了调用返回的类型以及传递给调用的实参类型。例如int(int, int)

  • 可以定义一个函数表(function table)用于存储指向一些可调用对象的“指针”。

  • function定义在<functional>中。

map<string, fucntion<int(int, int)>> binops = {
	{"+", add},
	{"-", std::minus<int>()},
	{"/", divide()},
	{"*", [](int i, int j) { return i * j; }},
	{"%", mod}
};

1_

重载,类型转换与运算符

  • 一个类型转换函数必须是类的成员函数,它不能声明返回类型,形参列表也必须为空。类型转换函数通常应该是const

  • 不允许转换成数组或者函数类型,但允许转换成指针(包括数组指针及函数指针)或者引用类型。

operator type() const;
  • 因为bool是一种算术类型,所以类类型的对象转换成bool后就能用在任何需要算数类型的上下文中。这可能会引发意想不到的结果。如果istream含有向bool的类型转换时,下面代码将编译通过
int i =42;
cin << i; // OK, cin转换成bool,然后提升成int,并左移42位
  • 为了防止异常发生,C++11引入了显式的类型转换运算符 (explicit conversion operator)
class SmallInt
{
public:
    explicit operator int() const { return val; }    
};

SmallInt si = 3; // OK, 构造函数不是显式的

si + 3; // ERROR!!! 此处需要隐式的类型转换,但类的运算符是显式的

static_cast<int>(si) + 3; // OK, 显式的转换
  • 我们必须通过显式的强制类型转换才能执行类型转换。存在一个例外,如果表达式被用作条件,则编译器会将显式的类型转换自动应用于它。

  • 当我们使用两个用户定义的类型转换时,如果转换函数之前或之后存在标准类型转换,则标准类型转换将决定最佳匹配到底是哪个。如果它们所需的标准类型转换级别一致,会产生二义性。

#include <iostream>

using namespace std;

void calc(int a)
{
	cout << "I am int version" << endl;
}

void calc(long double a)
{
	cout << "I am long double version" << endl;
}

int main()
{
	double d = 3.1;
	calc(d);	// ERROR

	short s = 1;
	calc(s); // int version
}
  • 表达式中运算符的候选函数集既应该包括成员函数,也应该非成员函数。

  • 如果我们对同一个类既提供了转换目标是算术类型的类型转换,也提供了重载的运算符,则将会遇到重载运算符与内置运算符的二义性问题。

面向对象程序设计

OOP:概述

  • 面向对象程序设计 (object-oriented programming) 的核心思想是数据抽象,继承和动态绑定。

  • 使用数据抽象,我们可以将类的接口与实现分离。

  • 使用继承,可以定义相似类型并对其相似关系建模。

  • 使用动态绑定,可以在一定程度上忽略相似类型的区别,而以统一的方式使用它们的对象。函数的运行版本由实参决定,即在运行时选择函数的版本,所以动态绑定有时又被称为 运行时绑定 (run-time binding)

  • 派生类必须在其内部对所有重新定义的虚函数进行声明。派生类可以在这样的函数之前加上virtual关键字,但是不是必须的。C++11新标准允许派生类显式地注明它将使用哪个成员函数改写基类地虚函数,具体措施是在该函数地形参列表之后增加一个override关键字。

  • 在C++语言中,当我们使用基类的引用(或指针)调用一个虚函数时将发生动态绑定。

定义基类和派生类

  • 基类通常都应该定义一个虚析构函数,即使该函数不执行任何实际操作也是如此。

  • 任何构造函数之外的非静态函数都可以是虚函数。

  • 关键字virtual只能出现在类内部的声明语句之前,而不能用于类外部的函数定义。

  • 如果基类把一个函数声明成虚函数,则该函数在派生类中隐式地也是虚函数。

  • 成员函数如果没被声明为虚函数,则其解析过程发生在编译时而非运行时。

  • protected成员:基类希望它的派生类有权访问该成员,同时禁止其他用户访问。

  • 派生类必须通过使用 类派生列表 (class derivation list) 明确指出它是从哪个(哪些)基类继承而来的。

  • 类派生列表的形式是:首先是一个冒号,后面紧跟以逗号分隔的基类列表,其中每个基类前面可以有以下三种访问说明符中的一个:public, protected, private

  • 我们可以把派生类对象或者派生类对象的引用用在需要基类引用的地方;同样的,我们也可以把派生类对象的指针用在需要基类指针的地方。

  • 首先初始化基类的部分,然后按照声明的顺序依次初始化派生类的成员。

  • 如果基类定义了一个静态成员,则在整个继承体系中只存在该成员的唯一定义。

  • C++11新标准提供了一种防止继承发生的方法,即在类名后跟一个关键字final

  • 表达式的静态类型在编译时总是已知的,它是变量声明时的类型或表达式生成的类型;动态类型则是变量或表达式表示的内存中的对象的类型。

  • 基类的指针或引用的静态类型可能与其动态类型不一致。

  • 如果表达式既不是引用也不是指针,则它的动态类型永远与静态类型一致。

  • 不能将基类转换成派生类。

  • 派生类向基类的自动类型转换只对指针或引用类型有效,在派生类类型和基类类型之间不存在这样的转换。

  • 当我们用一个派生类对象为一个基类对象初始化或赋值时,只有该派生类对象中的基类部分会被拷贝,移动或赋值,它的派生类部分将被忽略掉。

虚函数

  • 我们必须为每一个虚函数都提供定义,而不管它是否被用到了,这是因为编译器也无法确定到底会使用哪个虚函数。

  • 当且仅当对通过指针或引用调用虚函数时,才会在运行时解析该调用,也只有在这种情况下对象的动态类型才有可能与静态类型不同。

  • OOP的核心思想是多态性 (polymorphism)。我们把具有继承关系的多个类型称为多态类型,因为我们能使用这些类型的“多种类型”而无须在意它们的差异。引用或指针的静态类型与动态类型不同这一事实正是C++语言支持多态性的根本存在。

  • 基类中的虚函数在派生类中隐含地也是一个虚函数。当派生类覆盖了某个虚函数时,该函数在基类中的型参必须与派生类中的形参严格匹配。

  • 如果我们使用override标记了某个函数,但该函数并没有覆盖已存在的虚函数,此时编译器将报错。

  • 如果我们已经把函数定义成final了,则之后任何尝试覆盖该函数的操作都将引发错误。

  • 如果虚函数使用默认实参,则基类和派生类中定义的默认实参最好一致。

  • 如果我们通过基类的引用或指针调用函数,则使用基类中定义的默认实参,即使实际运行的派生类中的函数版本。此时,传入派生类函数的将是基类函数定义的默认实参。

  • 通常情况下,只有成员函数(或友元)中的代码才需要使用作用域运算符来回避虚函数的机制。

抽象基类

  • 和普通的虚函数不一样,一个纯虚函数无须定义。

  • 我们通过在函数体的位置(即在声明语句的分号之前)书写=0就可以将一个虚函数说明为纯虚函数。=0只能出现在类内部的虚函数声明语句处。

  • 我们也可以为纯虚函数提供定义,不过函数体必须定义在类的外部。也就是说,我们不能在类的内部为一个=0的函数提供函数体。

  • 含有(或者未经覆盖直接继承)纯虚函数额类是抽象基类 (abstract base class)。

  • 我们不能创建抽象基类的对象。

  • 抽象基类负责定义接口,而后续的其他类可以覆盖该接口。

  • 重构 (refactoring) 负责重新设计类的体系以便将操作和/或数据从一个类移动到另一个类中。

访问控制与继承

  • protected成员
    • 对于类的用户来说是不可访问的。
    • 对于派生类的成员和友元来说是可访问的。
    • 派生类的成员或友元只能通过派生类对象来访问基类的受保护成员。派生类对于一个基类对象中的受保护成员没有任何访问特权。
class Base
{
protected:
    int prot_mem;    
};

class Sneaky : public Base
{
    friend void clobber(Sneaky&); // 能访问Sneaky::prot_mem

    friend void clobber(Base&); // 不能访问Base::prot_mem

};
  • 派生访问说明符对于派生类的成员(及友元)能否访问其直接基类的成员没什么影响。对基类成员的访问权限只与基类中的访问说明符有关。

  • 派生访问说明符的目的是控制派生类用户(包括派生类的派生类在内)对于基类成员的访问权限。

  • 如果基类的公有成员是可访问的,则派生类向基类的类型转换也是可访问的;反之则不行

  • 就像友元关系不能传递一样,友元关系同样也不能继承。

  • 有时我们需要改变派生类继承的某个名字的访问级别,通过使用using声明可以达到这一目的。

class Derived : private Base
{
public:
    using Base::size;
protected:
    using Base::n;
};
  • 默认情况下,使用class关键字定义的派生类是私有继承的;而使用struct关键字定义的派生类是公有继承的。
class Base {
public:
  void pub_mem(); // public member

protected:
  int prot_mem; // protected member

private:
  char priv_mem; // private member

};

struct Pub_Derv : public Base {
  // ok: derived classes can access protected members

  int f() { return prot_mem; }

  // error: private members are inaccessible to derived classes

  //char g() { return priv_mem; }

};

struct Priv_Derv : private Base {
  // ok: private derivation doesn’t affect access in the derived class

  int f() { return prot_mem; }

  // error: private members are inaccessible to derived classes

  //char g() { return priv_mem; }

};

struct Prot_Derv : protected Base {
  // ok: derived classes can access protected members

  int f() { return prot_mem; }

  // error: private members are inaccessible to derived classes

  //char g() { return priv_mem; }

};

struct Derived_from_Public : public Pub_Derv {
  // ok: Base::prot_mem remains protected in Pub_Derv
  int use_base() { return prot_mem; }
};

struct Derived_from_Private : public Priv_Derv {
  // error: Base::prot_mem is private in Priv_Derv

  //int use_base() { return prot_mem; }

};

struct Derived_from_Protected : public Prot_Derv {
  // ok: Base::prot_mem remains protected in Prot_Derv

  int use_base() { return prot_mem; }
};

int main() {
  Pub_Derv d1; Priv_Derv d2; Prot_Derv d3;
  Derived_from_Public dd1; Derived_from_Private dd2; Derived_from_Protected dd3;
  // Below are user code
  Base * p = &d1;  // legal

  p = &d2;        // illegal, user code may use the derived-to-base conversion
                  // only if D inherits publicly from B

  p = &d3;        // illegal, same reason as d2

  p = &dd1;       // legal

  p = &dd2;       // illegal, same reason as d2

  p = &dd3;       // illegal, same reason as d2

  return 0;
}

继承中的类作用域

  • 当存在继承关系时,派生类的作用域嵌套在其基类的作用域之内。如果一个名字在派生类的作用域内无法正确解析,则编译器将继续在外层的基类作用域中寻找该名字的定义。

  • 一个对象,引用或指针的静态类型决定了该对象的哪些成员是可见的。

  • 派生类的成员将隐藏同名的基类成员。

  • 定义派生类中的函数不会重载其基类中的成员。

  • 如果派生类(即内层作用域)的成员与基类(即外层作用域)的某个成员同名,则派生类将在其作用域内隐藏该基类成员。即使派生类成员和基类成员的形参列表不一致,基类成员也仍然会被隐藏掉。

  • 基类与派生类中的虚函数必须有相同的形参列表。假如基类与派生类的函数接受的实参不同,则我们就无法通过基类的引用或指针调用派生类的虚函数了。

  • 有时一个类仅需覆盖重载集合中的一些而非全部函数,此时,如果我们不得不覆盖基类中的每一个版本的话,显然操作将极其繁琐。

构造函数与拷贝控制

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

  • 如果一个类定义了析构函数,即使它通过=default的形式使用了合成的版本,编译器也不会为这个类合成移动操作。

  • 派生类中删除的拷贝控制与基类的关系
    • 如果基类中的默认构造函数,拷贝构造函数,拷贝赋值运算或析构函数是被删除的函数或者不可访问,则派生类中对应的成员将是被删除的,原因是编译器不能使用基类成员来执行派生类对象基类部分的构造,赋值或销毁操作。
    • 如果在基类中有一个不可访问或删除掉的析构函数,则派生类中合成的默认和拷贝构造函数将是被删除的,因为编译器无法销毁派生类对象的基类部分。
    • 编译器将不会合成一个删除掉的移动操作。当我们使用=default请求一个移动操作时,如果基类中的对应操作是删除的或不可访问的,那么派生类中该函数将是被删除的,原因是派生类对象的基类部分不可移动。同样,如果基类的析构函数是删除的或不可访问的,则派生类的移动构造函数也将是被删除的。
  • 当派生类定义了拷贝或移动操作时,该操作负责拷贝或移动包括基类部分成员在内的整个对象。

  • 和构造函数及赋值运算符不同的是,析构函数只负责销毁派生类自己分配的资源。派生类的基类部分也是自动销毁的。

  • 在默认情况下,基类默认构造函数初始化派生类对象的基类部分。如果我们想拷贝(或移动)基类部分,则必须在派生类的构造函数初始值列表中显式地使用基类的拷贝(或移动)构造函数。

  • 与拷贝和移动构造函数一样,派生类的赋值运算符也必须显式地为其基类部分赋值。

  • 对象销毁的顺序正好与其创建的顺序相反:派生类析构函数首先执行,然后是基类的析构函数,以此类推。

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

  • 派生类继承基类构造函数的方式是提供一条注明了(直接)基类名的using声明语句。

  • 通常情况下,using声明语句只是令某个名字在当前作用域内可见。而当作用与构造函数时,using声明语句将令编译器产生代码。对于基类的每个构造函数,编译器都生成一个与之对应的派生类构造函数。

  • 如果派生类含有自己的数据成员,则这些成员将被默认初始化。
class Bulk_quote : public Disc_quote
{
public:
    using Disc_quote::Disc_quote;
    double net_price(std::size_t) const;
};

// 等价于下面

class Bulk_quote : public Disc_quote
{
public:
    Bulk_quote(const std::string& book, double price, std::size_t qty, double disc) :
        Disc_quote(book, price, qty, disc) { }
    double net_price(std::size_t) const;
};
  • 和普通成员的using声明不一样,一个构造函数的using声明不会改变该构造函数的访问级别。

  • 一个using声明语句不能指定explicitconstexpr。如果基类的构造函数是explicit或者constexpr,则继承的构造函数也拥有相同的属性。

  • 当一个基类构造函数默认实参时,这些实参并不会被继承。相反,派生类将获得多个继承的构造函数,其中每个构造函数分别省略掉一个含有默认实参的形参。例如,如果一个基类有一个接受两个形参的构造函数,其中第二个形参含有默认实参,则派生类将获得两个构造函数:一个构造函数接受两个形参(没有默认实参),另一个构造函数只接受一个形参,它对应于基类中最左侧的没有默认值的那个形参。

  • 如果基类含有几个构造函数,则除了两个例外,大多数时候派生类会继承所有这些构造函数:

    • 第一个例外是派生类可以继承一部分构造函数,而为其他构造函数定义自己的版本。如果派生类定义的构造函数与基类的构造函数具有相同的参数列表,则该构造函数将不会被继承。定义在派生类中的构造函数将替换继承而来的构造函数。
    • 第二个例外是默认,拷贝和移动构造函数不会被继承。这些构造函数按照正常规则被合成。继承的构造函数不会被作为用户定义的构造函数来使用。因此,如果一个类只含有继承的构造函数,则它也将拥有一个合成的默认构造函数。

容器与继承

  • 当派生类对象赋值给基类对象时,其中的派生类部分将被“切掉”,因此容器和存在继承关系的类型无法兼容。

  • 当我们希望在容器中存放具有继承关系的对象时,我们实际上存放的通常是基类的指针(或智能指针)。这些指针所指对象的动态类型可能是基类类型,也可能是派生类类型。

文本查询程序再探

  • Is A: 当我们令一个类公有地继承另一个类时,派生类应当反映与基类的“是一种 (Is A)”关系。在设计良好的类体系中,公有派生类的对象应该可以用在任何需要基类对象的地方。

  • Has A:具有这种关系的类暗含成员的意思。

模板与泛型编程

  • 一个模板就是一个创建类或函数的蓝图或者说公式。

定义模板

  • 模板定义以关键字template开始,后跟一个模板参数列表 (template parameter list),这是一个逗号分隔的一个或多个模板参数的列表。

  • 在模板定义中,模板参数列表不能为空。类型参数前必须使用关键字classtypename。这两个关键字含义相同,可以互换使用。

template <typename T>
int compare(const T& v1, const T& v2)
{
	if (v1 < v2) return -1;
	if (v2 < v1) return 1;
	return 0;
}
  • 一个非类型参数 (nontype parameter) 表示一个值而非一个类型。非类型模板参数的模板实参必须是常量表达式。

  • 我们不能用一个普通(非static)局部变量或动态对象作为指针或引用非类型模板参数的实参。

template<unsigned N, unsigned M>
int compare(const char(&p1)[N], const char(&p2)[M])
{
	return strcmp(p1, p2);
}

// 如果我们调用 compare("hi", "mom");

// 编译器会实例化出如下版本

// int compare(const char(&p1)[3], const char(&p2)[4])
  • 当编译器遇到一个模板定义时,它并不生成代码。只有当我们实例化出模板的一个特定版本时,编译器才会生成代码。当我们使用(而不是定义)模板时,编译器才生成代码,这一特性影响了我们如何组织代码以及错误何时被检测到。

  • 为了生成一个模板的实例化版本,编译器需要掌握函数模板或类模板成员函数的定义。因此,与非模板代码不同,模板的头文件通常既包括声明也包括定义。

  • 函数模板和类模板成员函数的定义通常放在头文件中。

  • 保证传递给模板的实参支持模板所要求的操作,以及这些操作在模板中能正确工作,是调用者的责任。

  • 一个类模板的每个实例都形成一个独立的类。类型Blob<string>与任何其他Blob类型都没有关联,也不会对任何其他Blob类型的成员有特殊访问权限。

  • 默认情况下,对于一个实例化了的类模板,其成员只有在使用时才被实例化。

#pragma once

#include <vector>

#include <memory>

#include <utility> // move

#include <initializer_list>

#include <string>

using namespace std;

template <typename T>
class Blob
{
public:
	typedef T value_type;
	typedef typename vector<T>::size_type size_type;

	Blob();
	Blob(initializer_list<T> il);

	size_type size() const { return data->size(); }
	bool empty() const { return data->empty(); }
	void push_back(const T& t) { data->push_back(t); }
	void push_back(T&& t) { data.push_back(std::move(t)); }

	void pop_back();

	T& back();
	T& operator[](size_type i);

private:
	shared_ptr<vector<T>> data;
	void check(size_type i, const string& msg) const;
};

template <typename T>
void Blob<T>::check(size_type t, const string& msg) const
{
	if (i >= data->size())
		throw out_of_range(msg);
}

template <typename T>
T& Blob<T>::back()
{
	check(0, "back on empty Blob");
	return data->back();
}

template <typename T>
void Blob<T>::pop_back()
{
	check(0, "pop_back on empty Blob");
	data->pop_back();
}

template <typename T>
Blob<T>::Blob() : data(make_shared<vector<T>>()) { }

template <typename T>
Blob<T>::Blob(initializer_list<T> il) : data(make_shared<vector<T>>(il)) { }
  • 在一个类模板的作用域内,我们可以直接使用模板名而不必指定模板实参。

  • 当一个类包含一个友元声明时,类与友元各自是否是模板是相互无关的。如果一个类模板包含一个非模板友元,则友元被授权可以访问所有模板实例。如果友元自身是模板,类可以授权给所有友元模板实例,也可以只授权给特定实例。

  • 为了让所有实例称为友元,友元声明中必须使用与类模板本身不同的模板参数。

  • C++ 11中,我们可以将模板类型参数声明为友元。因此,对于某个类型名FooFoo将成为Bar<Foo>的友元。

template <typename T>
class binary
{
    friend T;
};
  • C++ 11允许我们为类模板定义一个类型别名。
template <typename T> using twin = pair<T, T>;
twin<string> authors; // authors is pair<string, string>
  • 一个模板参数名的可用范围是在其声明之后,至模板声明或定义结束之前。

  • 模板参数会隐藏外层作用域中声明的相同名字。

  • 在模板内不能宠用模板参数名。

  • 一个特定文件所需要的所有模板的声明通常一起放置在文件开始位置,出现于任何使用这些模板的代码之前。

  • 默认情况下,C++语言假定通过作用域运算符访问的是名字不是类型。因此,如果我们希望使用一个模板类型参数的类型成员,就必须显式告诉编译器该名字。

template <typename T>
typename T::value_type top(const T& c)
{
    /*...*/
}
  • 当我们希望通知编译器一个名字表示类型时,必须使用关键字typename,而不能使用class

  • C++ 11中,我们可以为函数和类模板提供默认实参。

  • 对于一个模板参数,只有当它右侧的所有参数都有默认实参时,它才可以有默认实参。

  • 一个类(无论是普通类还是类模板)可以包含本身是模板的成员函数。这种成员被称为 成员模板 (member template)。成员模板不能是虚函数。

  • 当我们在类模板外定义一个成员模板时,必须同时为类模板和成员模板提供模板参数列表。类模板的参数列表在前,后跟成员自己的模板参数列表。

  • 当模板使用时才会进行实例化意味着,相同的实例可能出现在多个对象文件中。在多个文件中实例化相同模板的额外开销可能非常严重。在新标准中,我们可以通过 显式实例化 (explicit instantiation) 来避免这种开销。

extern template class Blob<string>; // 声明

template int compare(const int&, const int&); //定义
  • 对于每个实例化声明,在程序中某个位置必须有其显式的实例化定义。

  • 一个类模板的实例化定义会实例化该模板的所有成员,包括内联的成员函数。

  • 在一个类模板的实例化定义中,所用类型必须能用于模板的所有成员函数。

  • 实例化什么时候发生?

    • notice the pointer/reference stuff: they don’t require instantiation since no data is actually allocated (a pointer is just a few bytes to contain the address, has no need to have all the data stored.)
    • Only when stuff is allocated then the template has to be completely resolved (and that happens at compile-time, that’s why they usually need both declaration and definition.. there’s no linking phase yet)
#include <iostream>
#include <string>

using namespace std;


template <typename T> class Stack {
  typedef typename T::ThisDoesntExist StaticAssert; // T::NotExisting doesn't exist at all!
};


void f1(Stack<char>); // No instantiation, compiles

class Exercise {
  Stack<double> &rsd; // No instantiation, compiles (references don't need instantiation, are similar to pointers in this)

  Stack<int>    si; // Instantiation! Doesn't compile!!
};


int main(){

  Stack<char> * sc; // No Instantiation, this compiles successfully since a pointer doesn't need instantiation

  f1(*sc); // Instantiation of Stack<char>! Doesn't compile!!

  int iObj = sizeof(Stack<string>); // Instantiation of Stack<string>, doesn't compile!!

}
  • shared_ptrunique_ptr
    • 第一个明显不同是它们管理所保存的指针的策略-前者给予我们共享指针所有权的能力;后者则独占指针。
    • 另一个差异是它们允许用户重载默认删除器的方式。我们可以很容易地重载一个shared_ptr的删除器,只要在创建或reset指针时传递给它一个可调用对象即可。与之相反,删除器的类型是一个unique_ptr对象的类型的一部分。用户必须在定义unique_ptr时以显示模板实参的形式提供删除器的类型。
  • 通过在编译时绑定删除器,unique_ptr避免了间接调用删除器的运行时开销。通过在运行时绑定删除器,shared_ptr使用户重载删除器更为方便。

  • 实现shared_ptr
#pragma once

#include <cstddef> // size_t
#include <functional> // function
#include <utility> // move

using std::size_t;
using std::function;

namespace custom_pointer
{

	struct Delete
	{
		template <typename T>
		auto operator() (T* p) const { delete p; }
	};

	template <typename T> class shared_ptr;

	template <typename T> void swap(shared_ptr<T>& lhs, shared_ptr<T>& rhs)
	{
		std::swap(lhs.ptr, rhs.ptr);
		std::swap(lhs.ref_count, rhs.ref_count);
		std::swap(lhs.deleter, rhs.deleter);
	}

	template <typename T>
	class shared_ptr
	{
	public:
		shared_ptr() : ptr(nullptr), ref_count{ new size_t(1) }, deleter(Delete()) { }

		explicit shared_ptr(T* raw_ptr) : ptr(raw_ptr), ref_count(new size_t(1)), deleter(Delete()) { }

		shared_ptr(const shared_ptr& other) : ptr(other.ptr), ref_count(other.ref_count), deleter(other.deleter)
		{
			++* ref_count;
		}

		shared_ptr(shared_ptr&& other) noexcept : ptr(other.ptr), ref_count(other.ref_count), deleter(std::move(other.deleter))
		{
			other.ptr = nullptr;
			other.ref_count = nullptr;
		}

		shared_ptr& operator=(const shared_ptr& rhs)
		{
			// increment first to ensure safty for self-assignment

			++* rhs.ref_count;
			decrement_and_destroy();
			ptr = rhs.ptr;
			ref_count = rhs.ref_count;
			deleter = rhs.deleter;
			return *this;
		}

		shared_ptr& operator=(shared_ptr&& rhs) noexcept
		{
			custom_pointer::swap(*this, rhs);
			rhs.decrement_and_destroy();
			return *this;
		}

		operator bool() const { return ptr ? true : false; }

		T& operator*() const { return *ptr; }

		T* operator->() const { return &*ptr; }

		size_t use_count() const { return *ref_count; }

		T* get() const { return ptr; }

		bool unique() const { return 1 == *ref_count; }

		void swap(shared_ptr& rhs) { custom_pointer::swap(*this, rhs); }

		void reset() { decrement_and_destroy(); }

		void reset(T* other_ptr)
		{
			if (ptr != other_ptr)
			{
				decrement_and_destroy();
				ptr = other_ptr;
				ref_count = new size_t(1);
			}
		}

		void reset(T* other_ptr, const function<void(T*)>& d)
		{
			reset(other_ptr);
			deleter = d;
		}

		~shared_ptr() { decrement_and_destroy(); }

	private:
		T* ptr;
		size_t* ref_count;
		function<void(T*)> deleter;

		void decrement_and_destroy()
		{
			if (ptr && (0 == -- * ref_count))
			{
				delete ref_count;
				deleter(ptr);
			}
			else if (!ptr)
			{
				delete ref_count;
			}
			ref_count = nullptr;
			ptr = nullptr;
		}
	};
}
  • 实现unique_ptr
#pragma once

#include <cstddef> // size_t, nullptr_t
#include <functional> // function
#include <utility> // move
#include <iostream>

using std::size_t;
using std::function;
using std::ostream;
using std::cerr;
using std::endl;

namespace custom_pointer
{

	class DebugDelete
	{
	public:
		DebugDelete(ostream& s = cerr) : os(s) { }

		template <typename T>
		void operator() (T* p) const
		{
			os << "deleting ptr" << endl;
			delete p;
		}

	private:
		ostream& os;
	};

	template <typename, typename> class unique_ptr;

	template <typename T, typename D>
	void swap(unique_ptr<T, D>& lhs, unique_ptr<T, D>& rhs)
	{
		std::swap(lhs.ptr, rhs.ptr);
		std::swap(lhs.deleter, rhs.deleter);
	}

	template <typename T, typename D = DebugDelete>
	class unique_ptr
	{
		friend void swap<T, D>(unique_ptr<T, D>& lhs, unique_ptr<T, D>& rhs);

	public:
		unique_ptr(const unique_ptr&) = delete;
		unique_ptr& operator=(const unique_ptr&) = delete;



		unique_ptr() = default;
		explicit unique_ptr(T* raw_ptr) : ptr(raw_ptr) { }

		unique_ptr(unique_ptr&& other) noexcept : ptr(other.ptr)
		{
			other.ptr = nullptr;
		}

		unique_ptr& operator=(unique_ptr&& rhs) noexcept
		{
			if (this->ptr != rhs.ptr)
			{
				deleter(ptr);
				ptr = nullptr;
				custom_pointer::swap(*this, rhs);
			}
			return *this;
		}

		unique_ptr& operator=(nullptr_t n) noexcept
		{
			if (n == nullptr)
			{
				deleter(ptr);
				ptr = nullptr;
			}
			return *this;
		}


		operator bool() const { return ptr ? true : false; }

		T& operator*() const { return *ptr; }

		T* operator->() const { return &*ptr; }

		T* get() const { return ptr; }

		void swap(unique_ptr& rhs) { custom_pointer::swap(*this, rhs); }

		void reset()
		{
			deleter(ptr);
			ptr = nullptr;
		}

		void reset(T* other_ptr)
		{
			if (ptr != other_ptr)
			{
				deleter(ptr);
				ptr = other_ptr;
			}
		}

		T* release()
		{
			T* ret = ptr;
			ptr = nullptr;
			return ret;
		}

		~unique_ptr() { deleter(ptr); }

	private:
		T* ptr;
		D deleter = D();
	};
}

模板实参推断

  • 顶层const无论是在形参中还是在实参中,都会被忽略。

  • 能在调用中应用于函数模板的类型转换只包括如下两项:
    • const转换:可以将一个非const对象的引用(或指针)传递给一个const的引用(或指针)形参。
    • 数组或函数指针转换:如果函数形参不是引用类型,则可以对数组或函数类型的实参应用正常的指针转换。一个数组实参可以转换为一个指向其首元素的指针。类似的,一个函数实参可以转换为一个该函数类型的指针。
  • 将实参传递给带模板类型的形参时,能够自动应用的类型转换只有const转换及数组或函数到指针的转换。

  • 如果函数参数类型不是模板参数,则对实参进行正常的类型转换。
template <class T> int compare(const T&, const T&);

compare("hi", "world"); // ERROR!!! (char[3], char[6])

compare("bye", "dad"); // OK, (char*, char*)
template <typename T> T calc(T, int);
template <typename T> T fcn(T, T);

double d;
float f;
char c;

calc(c, 'c'); // OK. T is a char

calc(d, f); // OK. T is a double

fcn(c, 'c'); // OK. T is a char

fcn(d, f); // ERROR!!! (double, float) 不一致
template <typename T> f1(T, T);
template <typename T1, typename T2) f2(T1, T2);
int i = 0;
int j = 42;
int* p1 = &i;
int* p2 = &j;
const int* cp1 = &i;
const int* cp2 = &j;

f1(p1, p2); // f1<int*>(int*, int*)

f2(p1, p2);  // f2<int*, int*>(int*, int*)

f1(cp1, cp2); // f1<const int*>(const int*, const int*)

f2(cp1, cp2); // f2<const int*, const int*>(const int*, const int*)

f1(p1, cp1); // ERROR!!! (int*, const int*)

f2(p1, cp1); // f2<int*, const int*>(int*, const int*)
  • 用户可以提供一个显式模板实参 (explicit template argument)
template <typename T1, typename T2, typename T3>
T1 sum(T2, T3); // T1 无法推断

auto val3 = sum<long long>(i, lng); // long long sum(int, long)
  • 显式模板实参按由左至右的顺序与对应的模板参数匹配。
template <typename T1, typename T2, typename T3>
T3 sum(T2, T1); // T1 无法推断

auto val3 = sum<long long>(i, lng); // ERROR! 无法推断,long long匹配的是T1

auto val2 = sum<long long, int, long>(i, lng); // OK
  • 尾置返回类型与类型转换
#include <type_traits>

using std::remove_reference;

template <typename It>
auto fcn2(It beg, It end) -> decltype(*beg)
{
	return *beg;	// 返回一个元素的引用

}

template <typename It>
auto fcn2(It beg, It end) -> decltype(*beg + 0)
{
	return *beg;	// 返回一个元素的拷贝

}

template <typename It>
auto fcn2(It beg, It end) -> typename remove_reference<decltype(* beg)>::type
{
	return * beg;	// 返回一个元素的拷贝

}
  • 引用折叠:对于一个给定类型X,
    • X& &, X& &&, X&& &都折叠成类型X&
    • X&& &&折叠成X&&
  • 引用折叠只能应用于间接创建的引用的引用,如类型别名或模板参数。
template <typename T> void f1(T&);
template <typename T> void f2(const T&);
template <typename T> void f3(T&&);

f1(i); // T: int

f1(ci); // T: const int

f1(5); // ERROR!

f2(i); // T: int

f2(ci); // T: int

f2(5); // T: int

f3(i); // T: int&

f3(ci); // T: const int&

f3(5); // T: int

  • 如果一个函数参数是指向模板参数类型的右值引用(如,T&&),则可以传递给它任意类型的实参。如果将一个左值传递给这样的参数,则函数参数被实例化为一个普通的左值引用(T&)。

  • std::move是一个使用右值引用模板很好的例子。

template <typename T>
typename remove_reference<T>::type&& move(T&& t)
{
    return static_cast<typename remove_reference<T>::type&&>(t);
}
  • 如果一个函数参数是指向模板类型参数的右值引用(如T&&),它对应的实参的const属性和左值/右值属性将得到保持。

  • 当用于一个指向模板参数类型的右值引用函数参数(T&&)时,forward会保持实参类型的所有细节。

  • std::move相同,对std::forward不适用using声明是一个好主意。(<utility>)

template <typename F, typename T1, typename T2>
void flip(F f, T1&& t1, T2&& t2)
{
    f(std::forward<T2>(t2), std::forward<T1>(t1));
}

重载与模板

  • 当有多个重载版本对一个调用提供同样好的匹配时,应选择最特例化的版本。

  • 对于一个调用,如果一个非函数模板与一个函数模板提供同样好的匹配,则选择非模板函数。

  • 在定义任何函数之前,记得声明所有重载的函数版本。这样就不必担心编译器由于未遇到你希望调用的函数而实例化一个并非你所需的版本。

template <typename T> void f(T);
template <typename T> void f(const T*);
template <typename T> void g(T);
template <typename T> void g(T*);

int i = 42, *p = &i;
const int ci = 0, *p2 = &ci;

g(42);      // g(T)

g(p);       // g(T*)

g(ci);      // g(T)

g(p2);      // g(T*)

f(42);      // f(T)

f(p);       // f(T)

f(ci);      // f(T)

f(p2);      // f(const T*)

可变参数模板

  • 一个 可变参数模板 (variadic template) 就是一个可以接受可变数目参数的模板函数或模板类。可变数目的参数称为参数包-(模板参数包,函数参数包)。

  • 在函数参数列表中,如果一个参数的类型时一个模板参数包,则此参数也是一个函数参数包。

template <typename T, typename... Args>
void foo(const T& t, const Args& ... rest);
  • 实现make_shared
#include <iostream>
#include <memory>
#include <string>

namespace ch16 //to differ from std::make_shared
{
    template <typename T, typename ... Args>
    auto make_shared(Args&&... args) -> std::shared_ptr<T>
    {
        return std::shared_ptr<T>(new T(std::forward<Args>(args)...));
    }
}

struct Foo
{
    explicit Foo(int b) : bar(b){ }
    int bar;
};

int main()
{
    auto num = ch16::make_shared<int>(42);
    std::cout << * num << std::endl;

    auto str = ch16::make_shared<std::string>(10, 'c');
    std::cout << * str << std::endl;

    auto foo = ch16::make_shared<Foo>(99);
    std::cout << foo->bar << std::endl;

    return 0;
}

模板特例化

  • 特例化的本质是实例化一个模板,而非重载它。因此,特例化不影响函数匹配。
template <typename T> int compare(const T&, const T&);

template <>
int compare(const char* const& p1, const char* const& p2)
{
    return strcmp(p1, p2);
}
  • 与函数模板不同,类模板的特例化不必为所有模板参数提供实参。我们可以只指定一部分而非所有模板参数,或是参数的一部分而非全部特性。一个类模板的 部分特例化 (partial specialization) 本身是一个模板,使用它时用户还必须为那些在特例化版本中未指定的模板参数提供实参。

  • 我们只可以特例化特定成员函数而不是特例化整个模板类。

标准库特殊设施

tuple类型

  • tuple可以有任意数量的成员。

  • tuple构造函数是explicit的,因此我们必须使用直接初始化语法。

  • 为了使用tuple的相等或不等运算符,对每对成员使用==运算符必须都是合法的;为了使用关系运算符,对每对成员使用<必须都是合法的。

  • 由于tuple定义了<==运算符,我们可以tuple序列传递给算法,并且可以在无序容器中将tuple作为关键字类型。

#include <iostream>

#include <list>

#include <tuple>

#include <vector>

#include <string>

#include <cstddef>

using namespace std;

int main()
{
	tuple<size_t, size_t, size_t> threeD{ 1, 2, 3 };

	tuple<string, vector<double>, int, list<int>> someVal("constants", { 3.14, 2.718 }, 42, { 0,1,2,3,4,5 });

	auto item = make_tuple("0-999", 3, 20.00);  // tuple<const char*, int, double>

	auto book = get<0>(item);

	auto cnt = get<1>(item);

	get<2>(item) * = 0.8;		// 返回指定成员的引用

	typedef decltype(item) trans;

	size_t sz = tuple_size<trans>::value; // 3

	tuple_element<1, trans>::type cnt = get<1>(item);  // int

}

bitset类型

  • string的下标编号习惯与bitset恰好相反:string中下标最大字符(最右字符)用来初始化bitset中的低位(下标为0的二进制位)。
#include <bitset>

using namespace std;

int main()
{
    bitset<32> bitvec(1U); // 32

	bitset<13> bitvec(0xbeef); // 初始值的高位被丢弃。 1111011101111

	bitset<20> bitvec2(0xbeef); // 高位被置0.  00001011111011101111

	bitset<128> bitvec3(~0ULL);	// long long 0ULL是64个0,因此~0ULL是64个1,高位是0

	bitset<32> bitvec4("1100"); // 2,3两位为1,剩余两位为0.

	string str("1111111000011100101");

	bitset<32> bitvec5(str, 5, 4);	// 从str[5]开始的四个二进制位

	bitset<32> bitvec6(str, str.size() - 4); // 使用最后四个字符    
}

正则表达式

  • 一个正则表达式的语法是否正确是在运行时解析的。

随机数

  • 定义在头文件random中的随机数库通过一组协作的类来解决这些问题:随机数引擎 (random-number engines)随机数分布类 (random-number distribution)
    • 引擎:类型,生成随机unsigned整数序列
    • 分布:类型,使用引擎返回服从特定概率分布的随机数。
  • C++程序不应该使用库函数rand,而应使用default_random_engine类和恰当的分布类对象。
#include <iostream>

#include <cstddef>

#include <random>

using namespace std;

int main()
{
    // 生成0-9(包含0,9)均匀分布的随机数

    uniform_int_distribution<unsigned> u(0, 9);
	default_random_engine e;
	for (size_t i = 0; i < 10; ++i)
		cout << u(e) << " ";
}
  • 当我们说随机数发生器时,是指分布对象和引擎对象的组合。

  • 一个给定的随机数发生器一直会生成相同的随机数序列。一个函数如果定义了局部的随机数发生器,应该将其(包括引擎和分布对象)定义为static的。否则,每次调用函数都会生成相同的序列。

  • 我们通常希望每次运行程序都会生成不同的随机结果,可以通过提供一个种子来达到这一目的。

  • 函数time(头文件<ctime>)接受单个指针参数,它指向用于写入时间的数据结构。如果此指针为空,则函数简单地返回时间。由于time返回以秒计的时间,因此这种方式只适用于生成种子的间隔为秒级或更长的应用。

  • 如果程序作为一个自动过程的一部分反复运行,将time的返回值作为种子的方式就无效了;它可能多次使用的都是相同的种子。

default_random_engine e1(time(0));
  • 最常用但不正确的从rand获得一个随机浮点数的方法是用rand()结果除以RAND_MAX,即,系统定义的rand可以生成的最大随机数的上界。这种方法不正确的原因是随机整数的精度通常低于随机浮点数。这样,有一些浮点值就永远不会被生成了。

  • 生成随机浮点数

default_random_engine e;
uniform_real_distribution<double> u(0, 1);
for (size_t i = 0; i < 10; ++i)
    cout << u(e) << " ";
  • 每个分布模板都有一个默认模板实参。
uniform_real_distribution<> u(0, 1); // 默认生成double
  • 由于引擎返回相同的随机数序列,所以我们必须在循环外声明引擎对象。否则,每步循环都会创建一个新引擎,从而每步循环都会生成相同的值。类似的,分布对象也要保持状态,因此也应该在循环外定义。

IO库再探

  • 格式控制,未格式化IO和随机访问。

  • 标准库定义了一组 操作符 (manipulator) 来修改流的状态。一个操作符是一个函数或是一个对象,会影响流的状态,并能用作输入或输出运算符的运算对象。

  • 操作符用于两大类输出控制:控制数值的输出形式以及控制补白的数量和位置。

  • 当操作符改变流的格式状态时,通常改变后的状态对所有后续IO都生效。

  • 操作符hexoctdec只影响整型运算对象,浮点值的表示形式不受影响。

  • 操纵符setprecision和其他接收参数的操纵符都定义在头文件iomanip中。

  • setw类似endl,不改变输出流的内部状态。它只决定下一个输出的大小。

用于大型程序的工具

异常处理

  • 当执行一个throw时,跟在throw后面的语句将不再被执行。相反,程序的控制权从throw转移到与之匹配的catch模块。

  • 栈展开 (stack unwinding)过程沿着嵌套函数的调用链不断查找,直到找到了异常匹配的catch子句为止;或者也可能一直没找到匹配的catch,程序将调用标准库函数terminate,它将终止当前的程序。

  • 在栈展开的过程中,运行类类型的局部对象的析构函数。因为这些析构函数是自动执行的,所以它们不应该抛出异常。一旦在栈展开的过程中析构函数抛出了异常,并且析构函数自身没能捕获到该异常,则程序将终止。

  • 抛出指针要求在任何对应的处理代码存在的地方,指针所指对象都必须存在。

exception* p = &r;
throw *p; // OK. exception
// throw p; // RUNTIME ERROR!
void exercise(int *b, int *e)
{
	vector<int> v(b, e);
	int* p = new int[v.size()];
	ifstream in("ints");
	// exception occurs here  // ERROR! memory leak
}
  • catch子句声明的类型决定了处理代码所能捕获的异常类型。这个类型必须是完全类型,它可以是左值引用,但不能是右值引用。

  • 异常声明的静态类型将决定catch语句所能执行的操作。如果catch的参数是基类类型,则catch无法使用派生类特有的任何成员。

  • 通常情况下,如果catch接受的异常与某个继承体系有关,则最好将该catch的参数定义成引用类型。

  • 如果在多个catch语句的类型之间存在着继承关系,则我们应该把继承链最底端的类放在前面,而将继承链最顶端的类放在后面。

  • 在执行某些校正操作之后,当前catch可能会决定由调用链上一层的函数接着处理异常。一条catch语句通过 重新抛出 (rethrowing) 的操作将异常传递给另外一个catch语句。

throw;
  • 为了一次性捕获所有异常,我们使用省略号作为异常声明,这样的处理代码称为捕获所有异常的处理代码。

  • 如果catch(...)与其他几个catch语句一起出现,则catch(...)必须在最后的位置。

void manip()
{
    try
    {
        //
    }
    catch (...)
    {
        throw;
    }
}
  • 处理构造函数初始值异常的唯一方法是将构造函数写成函数try语句块。
template <typename T>
Blob<T>::Blob(std::initializer_list<T> il) try : data(std::make_shared<std::vector<T>>(il))
{

}
catch (const std::bad_alloc &e) { handle_out_of_memory(e); }

  • noexcept说明要么出现在该函数的所有声明语句和定义语句中,要么一次也不出现。该说明应该在函数的尾置返回类型之前。我们可以在函数指针的声明和定义中指定noexcept

  • 在成员函数中,noexcept说明符需要跟在const及引用限定符之后,而在finaloverride或虚函数的=0之前。

  • noexcept可以用在两种情况下:一是我们确认函数不会抛出异常,二是我们根本不知道该如何处理异常。

void recoup(int) noexcept; // recoup不会抛出异常

void recoup(int) throw(); // 等价的声明
  • noexcept有两层含义:
    • 当跟在函数参数列表后面时它是异常说明符。
    • 当作noexcept异常说明的bool实参出现时,它是一个运算符。
  • 如果一个虚函数承诺了不会抛出异常,则后续派生出来的虚函数也必须做出同样的承诺;与之相反,如果基类的虚函数允许抛出异常,则派生类的对应函数既可以允许抛出异常,也可以不允许抛出异常。

命名空间

  • 命名空间作用域后面无须分号。

  • 全局命名空间以隐式的方式声明,并且在所有程序中都存在。::member_name表示全局命名空间中的一个成员。

  • 和普通的嵌套命名空间不同,内联命名空间中的名字可以被外层命名空间直接使用。关键字inline必须出现在命名空间第一次定义的地方,后续再打开命名空间的时候可以写inline,也可以不写。

inline namespace FifthEd
{

}
  • 未命名的命名空间是指关键字namespace后紧跟花括号括起来的一些列声明语句。未命名的命名空间中定义的变量拥有静态声明周期:它们在第一次使用前创建,并且直到程序结束才销毁。

  • 和其他命名空间不同,未命名的命名空间尽在特定的文件内部有效,其作用范围不会横跨多个不同的文件。

  • using声明(using std::cout;)和using指示(using namespace std;)在作用域上的区别直接决定了它们工作方式的不同。对于using声明来说,我们指示简单地令名字在局部作用域内有效。相反,using指示是令整个命名空间地所有内容变得有效。通常情况下,命名空间中会含有一些不能出现在局部作用域中的定义,因此,using指示一般被看作是出现在最近的外层作用域中。

  • 当我们给函数传递一个类类型的对象时,除了在常规的作用域查找外还会查找实参类所属的命名空间。

  • 当类声明了一个友元时,该友元声明并没有使得友元本身可见。然而,一个另外的未声明的类或函数如果第一次出现在友元声明中,则我们认为它是最近的外层命名空间的成员。

namespace A
{
    class C
    {
        friend void f2();
        friend void f(const C&);
    }
}

int main()
{
    A::C cobj;
    f(cobj);    // OK:因为f接受一个类类型的实参,而且f在C所属的命名空间进行了隐式的声明,所以f能被找到。

    f2();       // ERROR:因为f2没实参,而且不存在其他声明,所以它无法被找到
}
  • 一个using声明引入的函数将重载该声明语句所属作用域中已有其他同名函数。
    • 如果using声明出现在局部作用域中,则引入的名字将隐藏外层作用域的相关声明。
    • 如果using声明所在的作用域中已经有一个函数与新引入的函数同名且形参列表相同,则该using声明将引发错误。
  • using声明不同的是,对于using指示来说,引入一个与已有函数形参列表完全相同的函数并不会引发错误。此时,只要我们指明调用的是命名空间中的函数版本还是当前作用域的版本即可。
#include <iostream>

using namespace std;

namespace primerLib
{
	void compute() { cout << "compute()" << endl; }
	void compute(const void*) { cout << "compute(const void*)" << endl; }
}

using primerLib::compute;

void compute(int) { cout << "compute(int)" << endl; }
void compute(double, double = 3.4) { cout << "compute(double, double)" << endl; }
void compute(char*, char* = 0) { cout << "compute(char*, char*)" << endl; }

void f()
{
	compute(0);
}

int main()
{
	f(); // compute(int)

}
#include <iostream>

using namespace std;

namespace primerLib {
void compute() { cout << "compute()" << endl; }
void compute(const void*) { cout << "compute(const void*)" << endl; }
}

void compute(int) { cout << "compute(int)" << endl; }
void compute(double, double = 3.4) {
  cout << "compute(double, double)" << endl;
}
void compute(char*, char* = 0) { cout << "compute(char*, char*)" << endl; }

void f() {
  using primerLib::compute;
  compute(0);
}

int main() {
  f();  // compute(const void*)
}

多重继承与虚继承

  • 每个基类包含一个可选的访问说明符。如果访问说明符被忽略掉了,则关键字class对应的默认访问说明符是private,关键字struct对应的是public

  • 在C++11新标准中,允许派生类从它的一个或几个基类中继承构造函数。

    • 但是如果从多个基类中继承了相同的构造函数,则程序将产生错误。
    • 如果一个类从它的多个基类中继承了相同的构造函数,则这个类必须为该构造函数定义它自己的版本。
#pragma once

#include <string>

#include <memory>

struct Base1 {
  Base1() = default;
  Base1(const std::string&);
  Base1(std::shared_ptr<int>);
};

struct Base2 {
  Base2() = default;
  Base2(const std::string&);
  Base2(int);
};

// ERROR! D1试图从两个基类中都继承D1::D1(const string&)

struct D1 : public Base1, public Base2 {
  using Base1::Base1;
  using Base2::Base2;
};

// OK

struct D2 : public Base1, public Base2 {
  using Base1::Base1;
  using Base2::Base2;

  D2(const std::string& s) : Base1(s), Base2(s) {}
  D2() = default;  // 一旦D2定义了它自己的构造函数,则必须出现
};
  • 析构函数的调用顺序正好与构造函数相反。

  • 当一个类拥有多个基类时,有可能出现派生类从两个或更多基类中继承了同名成员的情况。此时,不加前缀限定符直接使用该名字将引发二义性。

  • 虚继承的目的令某个类作出声明,承诺愿意共享它的基类。其中,共享的基类子对象称为虚基类。在这种机制下,不论虚基类在继承体系中出现了多少次,在派生类中都只包含唯一一个共享的虚基类子对象。

  • 虚派生类只影响从指定了虚基类的派生类中进一步派生出的类,它不会影响派生类本身。

class Raccoon : public virtual ZooAnimal { }; // OK

class Raccoon : virtual public ZooAnimal { }; // OK
  • 假定类B定义了一个名为x的成员,D1和D2都是从B虚继承得到的,D继承了D1和D2,则在D的作用域中,x通过D的两个基类都是可见的。如果我们通过D的对象使用x,有三种可能性:
    • 如果在D1和D2中都没有x的定义,则x将被解析成B的成员,此时不存在二义性,一个D的对象只含有x的一个实例。
    • 如果x时B的成员,同时是D1和D2中某一个的成员,则同样没有二义性,派生类的x比共享虚基类B的x优先级更高。
    • 如果在D1和D2中都有x的定义,则直接访问x将产生二义性问题。
  • 只要我们能创建虚基类的派生类对象,该派生类的构造函数就必须初始化它的虚基类。

  • 含有虚基类的对象的构造顺序:首先使用提供给最低层派生类构造函数的初始值初始化该对象的虚基类子部分,接下来按照直接基类在派生列表中出现的次序一次对其进行初始化。

  • 如果没有显式初始化虚基类,则虚基类的默认构造函数将被调用。如果没有默认构造函数,则代码将发生错误。

  • 虚基类总是先于非基类构造,与它们在继承体系中的次序和位置无关。

特殊工具与技术

控制内存分配

  • 当我们使用一条new表达式时,实际执行了三步。
    • 第一步,new表达式调用了一个名为operator new(或者opearator new[])的标准库函数。该函数分配一块足够大的,原始的,未命名的内存空间以便存储特定类型的对象(或对象的数组)。
    • 第二步,编译器运行相应的构造函数以构造这些对象,并为其传入初始值。
    • 第三步,对象被分配了空间并构造完成,返回一个指向该对象的指针。
string* sp = new string("a value");
string* arr = new string[10];
  • 当我们使用一条delete表达式删除一个动态分配的对象时,实际执行了两步。
    • 第一步,对所指的对象或者数组中的元素执行对应的析构函数。
    • 第二步,编译器调用名为operator delete(或operator delete[])的标准库函数释放内存空间。
delete sp;
delete[] arr;
  • mallocfree定义在头文件cstdlib中。

  • malloc函数接受一个表示待分配字结束的size_t,返回指向分配空间的指针或者返回0以表示分配失败。

  • free函数接受一个void*,它是malloc返回的指针的副本,free将相关内存返回给系统。调用free(0)没有任何意义。

void* operator new(size_t size)
{
    if (void* mem = malloc(size))
        return mem;
    else
        throw bad_alloc();
}

void operator delete(void* mem) noexcept { free(mem); }
  • 定位new允许我们在一个特定的,预先分配的内存地址上构造对象。

  • 当只传入一个指针类型的实参时,定位new表达式构造对象但是不分配内存。

  • 调用析构函数会销毁对象,但是不会释放内存。

string* sp = new string("a value");
sp->~string();

运行时类型识别

  • 运行时类型识别 (run-time type identification, RTTI) 的功能由两个运算符实现:
    • typeid运算符,用于返回表达式的类型。
    • dynamic_cast运算符,用于将基类的指针或引用安全地转换成派生类的指针或引用。
  • 这两个运算符特别适用于以下情况:我们想使用基类对象的指针或引用执行某个派生类操作并且该操作不是虚函数。

  • 一般来说,只要有可能我们应该尽量使用虚函数。当操作被定义成虚函数时,编译器将根据对象的动态类型自动地选择正确的函数版本。

  • 指针类型的dynamic_cast
// 如果失败,结果为0

if (Derived* dp = dynamic_cast<Derived*>(bp))  
{

}
else
{

}
  • 我们可以对一个空指针执行dynamic_cast,结果是所需类型的空指针。

  • 在条件部分执行dynamic_cast操作可以确保类型转换和结果检查在同一表达式中完成。

  • 引用类型的dynamic_cast

void f(const Base& b) {
  try {
    const Derived& d = dynamic_cast<const Derived&>(b);
  } catch (bad_cast) {
    // bad_cast定义在<typeinfo>标准库头文件中
  }
}
#include <iostream>

using namespace std;

class A {
 public:
  virtual ~A() {}
};

class B : public A {
 public:
  virtual ~B() {}
};

class C : public B {
 public:
  virtual ~C() {}
};

class D : public B, public A {
 public:
  virtual ~D() {}
};

int main() {
  A* pa = new C;
  B* pb = dynamic_cast<B*>(pa);  // OK

  B* pbb = new B;
  C* pc = dynamic_cast<C*>(pbb);  // 0. pbb指向的不是C或者C的派生类

  A* paa = new D;
  B* pbbb = dynamic_cast<B*>(paa);  // OK

  try {
    C& rc1 = dynamic_cast<C&>(*pa);  // ok: dynamic type of pa1 is C

    C& rc2 = dynamic_cast<C&>(
        *paa);  // error: dynamic type of pa2 is not C or its derived class

  } catch (std::bad_cast e) {
    cout << e.what() << endl;
  }

  return 0;
}
  • typeid表达式的结果是一个常量对象的引用,该对象的类型是标准库类型type_info或者type_info的公有派生类型。type_info定义在typeinfo头文件中。

  • typeid忽略顶层const

  • 如果表达式是一个引用,则typeid返回该引用所引对象的类型。

  • typeid作用于数组或函数时,并不会执行向指针的标准类型转换。

  • 如果我们对数组执行typeid(a),则所得的结果是数组类型而非指针类型。

int arr[] = { 1, 2, 3 };
cout << typeid(arr).name() << endl; // int [3]
  • typeid作用于指针时(而非指针所指的对象),返回的结果时该指针的静态编译时类型。如果是空指针,则typeid(*p)将抛出一个名为bad_typeid的异常。

  • typeid是否需要运行时检查决定了表达式是否会被求值。
    • 只有当类型含有虚函数时,编译器才会对表达式求值。
    • 如果类型不含有虚函数,则typeid返回表达式的静态类型。
  • RTTI在某些情况特别有用,例如当我们想为具有继承关系的类实现相等运算符时。
#pragma once

#include <typeinfo>

bool operator==(const Base& lhs, const Base& rhs) {
  return typeid(lhs) == typeid(rhs) && lhs.equal(rhs);
}

class Base {
  friend bool operator==(const Base&, const Base&);

 public:
  int a;

 protected:
  virtual bool equal(const Base&) const;
};

class Derived : public Base {
 public:
  int b;

 protected:
  bool equal(const Base&) const;
};

bool Derived::equal(const Base& rhs) const {
  auto r = dynamic_cast<const Derived&>(rhs);
  return b == r.b && a == r.a;
}

bool Base::equal(const Base& rhs) const { return a == rhs.a; }
#include <iostream>

#include <typeinfo>

using namespace std;

class A {
 public:
  virtual ~A() {}
};

class B : public A {
 public:
  virtual ~B() {}
};

class C : public B {
 public:
  virtual ~C() {}
};

int main() {
  A* pa = new C;
  cout << typeid(pa).name() << endl;  // class A * __ ptr64

  cout << typeid(*pa).name() << endl;  // class C

  C cobj;
  A& ra = cobj;
  cout << typeid(ra).name() << endl;  // class C

  B* px = new B;
  A& ra2 = *px;
  cout << typeid(ra2).name() << endl;  // class B

  return 0;
}

枚举类型

  • 枚举类型 (enumeration) 数据字面值常量类型。

  • 限定作用域的枚举类型:枚举成员在枚举类型的作用域外是不可访问的。不可以直接使用append,要使用modes::append

enum class modes {input, output, append};

enum struct modes {input, output, append};
  • 不限定作用域的枚举类型:枚举成员的作用域与枚举本身的作用域相同。注意:不能重复定义枚举成员。
enum color {red, yellow, gree};
enum {floatPrec = 6, doublePrec = 10, double_doublePrec = 10};
  • 默认情况下,限定作用域的enum成员类型是int。我们也可以指定想用的类型。
enum intValues : unsinged long long { charTyp = 255, shortTyp = 65535 };
  • 我们可以提前声明enum
enum intValues : unsigned long long; // 不限定作用域的,必须指定成员类型。

enum class open_modes; // 限定作用域的,可以使用默认类型int
  • 要想初始化一个enum对象,必须使用该enum类型的一个对象或者它的一个枚举成员。
enum Tokens {INLINE = 128, VIRTUAL = 129};

void ff(Tokens);
void ff(int);

int main()
{
    Tokens cur = INLINE;
    ff(128);    // ff(int)

    ff(INLINE); // ff(Tokens)

    ff(cur);    // ff(Tokens)

    return 0;
}

类成员指针

  • 成员指针是指可以指向类的非静态成员的指针。一般情况下,指针指向一个对象,但是成员指针指示的是类的成员,而非类的对象。

  • 成员指针的类型囊括了类的类型以及成员的类型。当初始化一个这样的指针时,我们令其指向类的某个成员,但是不指定该成员所属的对象;直到使用成员指针时,才提供成员所属的对象。

  • 与普通函数指针不同的是,在成员函数和指向该成员的指针之间不存在自动转换规则。必须显式地使用取地址运算符。

  • 在声明指向成员函数的指针并使用这样的指针进行函数调用时,括号必不可少:(C::*p)(parms)(obj.*p)(args)

#include <iostream>

#include <string>

using namespace std;

class Screen
{
public:
	typedef string::size_type pos;

	static const string Screen::* data() { return &Screen::contents; }

	char get_cursor() const { return contents[cursor]; }
	char get() const;
	char get(pos ht, pos wd) const;
private:
	string contents;
	pos cursor;
	pos height, width;
};

int main()
{
	const string Screen::* pdata = Screen::data(); // 等价于 auto pdata = Screen::data();

	Screen myScreen, * pScreen = &myScreen;

	auto s = myScreen.*pdata;

	s = pScreen->*pdata;

	auto pmf = &Screen::get_cursor;
	char (Screen:: * pmf2)(Screen::pos, Screen::pos) const;
	pmf2 = &Screen::get;

	Screen myScreen, * pScreen = &myScreen;

	char c1 = (pScreen->*pmf)();
	char c2 = (myScreen.* pmf2)(0, 0);

	return 0;
}
  • 生成可调用对象
    • function
    • mem_fn
    • bind:与function类似,使用bind时,必须将函数中用于表示执行对象的隐式形参转换成显式的。与mem_fn类似,bind生成的可调用对象的第一个实参既可以是指针,也可以是引用。
#include <iostream>

#include <string>

#include <functional>

using namespace std;
using namespace std::placeholders;

int main()
{
	function<bool(const string&)> f1 = &string::empty;

	auto f2 = mem_fn(&string::empty);

	auto f3 = bind(&string::empty, _ 1);

	string s1 = "";
	string s2 = "hello";
	string s3 = "he";
	cout << f1(s1) << endl;
	cout << f2(s2) << endl;
	cout << f3(s3) << endl;
	return 0;
}

嵌套类

  • 嵌套类是一个独立的类,与外层类基本没什么关系。

  • 外层类的对象和嵌套类的对象是相互独立的。在嵌套类的兑现那个中不包含任何外层类定义的成员;类似的,在外层类的对象中也不包含任何嵌套类定义的成员。

union: 一种节省空间的类

  • 联合union是一种特殊的类。一个union可以有多个数据成员,但是在任意时刻只有一个数据成员可以有值。

  • 当我们给union某个成员赋值之后,该union的其他成员就变成未定义的状态了。

  • union不能含有引用类型的成员,除此之外,它的成员可以是绝大多数类型。

  • 含有构造函数或析构函数的类类型也可以作为union的成员类型。

  • union可以为其成员指定publicprotectedprivate标记。

  • 默认情况下,union的成员都是公开的。

  • union可以定义包括构造函数和析构函数在内的成员函数。

  • union不能含有虚函数。

union Token
{
    char cval;
    int ival;
    double dval;
};

Token first_token = {'a'};
Token last_token;
Toekn* pt = new Token;

last_token.cval = 'z';
pt->ival = 42;
  • 匿名union不能包含受保护的成员或私有成员,也不能定义成员函数。
union
{
    char cval;
    int ival;
    double dval;
};

cval = 'c';
ival = 42;

局部类

  • 类可以定义在某个函数的内部,我们称这样的类为 局部类 (local class)

  • 局部类定义的类型只在定义它的作用域内可见。

  • 局部类的所有成员(包括函数在内)都必须完整定义在类的内部。

  • 在局部类不允许声明静态数据成员。

固有的不可移植的特性

  • 类可以将其非静态数据成员定义成 位域 (bit-field),在一个位域中含有一定数量的二进制位。

  • 当一个程序需要向其他程序或硬件设备传递二进制数据时,通常会用到位域。

  • 位域在内存中的布局是机器相关的。

  • 位域的类型必须是整型或枚举类型。

  • 位域的声明形式是在成员名字之后紧跟一个冒号以及常量表达式,该表达式用于指定成员所占的二进制位数。

  • 取地址运算符(&)不能作用于位域,因此任何指针都无法指向类的位域。

  • volatile作用是指示编译器,即使代码不对变量做任何改动,该变量的值仍可能会被外界修改。操作系统,硬件或其他线程都有可能修改该变量。该变量的值有可能遭受意料之外的修改,因此,每一次使用时,编译器都会重新从内存中获取这个值。可以方式编译器执行一些我们不想要的优化。

  • volatile在多线程程序也很有用,对应全局变量,任意线程都可能修改这些共享的变量。我们可能不希望编译器对这些变量进行优化。

  • constvolatile的一个重要区别是我们不能使用合成的拷贝/移动构造函数及赋值运算符初始化volatile对象或从volatile对象赋值。

线程

Basics

#include <iostream>
#include <thread>  // thread

#include <functional>  // ref

#include <utility>  // utility

using std::thread;
using std::cout;
using std::endl;
using std::ref;

void f1(int n) { cout << "Thread " << n << " is running..." << endl; }

void f2(int& n) {
  cout << "Thread " << n << " is running..." << endl;
  ++n;
}

int main() {
  int n = 0;
  thread t1;
  thread t2(f1, n + 1);
  thread t3(f2, ref(n));
  thread t4(std::move(t3));
  t2.join();  // join(): 原始线程会等待新线程执行结束之后,再去销毁线程对象

  t4.join();

  cout << n << endl;
}
  • join()的作用前面已经提到,主线程等待子线程结束方可执行下一步(串行),detach() 是的子线程放飞自我,独立于主线程并发执行,主线程后续代码段无需等待。看看效果:

  • 可被 joinable 的 std::thread 对象必须在他们销毁之前被主线程 join 或者将其设置为 detached.

  • get_id: 获取线程 ID,返回一个类型为 std::thread::id 的对象

  • joinable: 检查线程是否可被 join。检查当前的线程对象是否表示了一个活动的执行线程,由默认构造函数创建的线程是不能被 join 的。另外,如果某个线程 已经执行完任务,但是没有被 join 的话,该线程依然会被认为是一个活动的执行线程,因此也是可以被 join 的。

  • detach: Detach 线程。 将当前线程对象所代表的执行实例与该线程对象分离,使得线程的执行可以单独进行。一旦线程执行完毕,它所分配的资源将会被释放。

  • swap: Swap 线程,交换两个线程对象所代表的底层句柄(underlying handles)。

  • native_handle: 返回 native handle(由于 std::thread 的实现和操作系统相关,因此该函数返回与 std::thread 具体实现相关的线程句柄,例如在符合 Posix 标准的平台下(如 Unix/Linux)是 Pthread 库)。

多线程

  1. mutex.lock() vs. unique_lock
    • The std::unique_lock calls unlock on the mutex in its destructor.
    • The benefit of this is that in case some exception is thrown, you are sure that the mutex will unlock when leaving the scope where the std::unique_lock is defined.
  2. std::unique_lock vs. std::lock_guard vs. std::scoped_lock
    • The difference is that you can lock and unlock a std::unique_lock. std::lock_guard will be locked only once on construction and unlocked on destruction.
    • std::unique_lock has other features that allow it to e.g.: be constructed without locking the mutex immediately but to build the RAII wrapper (see here).
    • Note that these days one should use std::scoped_lock (since C++17) instead of std::lock_guard.
    • The scoped_lock is a strictly superior version of lock_guard that locks an arbitrary number of mutexes all at once (using the same deadlock-avoidance algorithm as std::lock). In new code, you should only ever use scoped_lock.
  3. [condition_variable](https://en.cppreference.com/w/cpp/thread/condition_variable) vs. mutex
    • A condition variable allows a thread to be signaled when something of interest to that thread occurs.
  4. std::future provides a mechanism to access the result of async
  5. yield() provides a hint to the implementation to reschedule the execution of threads, allowing other threads to run.
  6. atomic
    • The primary std::atomic template may be instantiated with any TriviallyCopyable type T satisfying both CopyConstructible and CopyAssignable.
    • std::string cannot be used with std::atomic as it is not TriviallyCopyable
std::unique_lock<std::mutex> lk1(mutex1, std::defer_lock);
std::unique_lock<std::mutex> lk2(mutex2, std::defer_lock);
std::lock(lk1, lk2);
{
    // safely locked as if using std::lock
    std::scoped_lock<std::mutex, std::mutex> lock(mutex1, mutex2);     
}
#include <condition_variable>
#include <iostream>
#include <mutex>
#include <string>
#include <thread>

std::mutex m;
std::condition_variable cv;
std::string data;
bool ready = false;
bool processed = false;

void worker_thread() {
  // Wait until main() sends data
  std::unique_lock<std::mutex> lk(m);
  cv.wait(lk, [] { return ready; });

  // after the wait, we own the lock.
  std::cout << "Worker thread is processing data\n";
  data += " after processing";

  // Send data back to main()
  processed = true;
  std::cout << "Worker thread signals data processing completed\n";

  // Manual unlocking is done before notifying, to avoid waking up
  // the waiting thread only to block again (see notify_one for details)
  lk.unlock();
  cv.notify_one();
}

int main() {
  std::thread worker(worker_thread);

  data = "Example data";
  // send data to the worker thread
  {
    std::lock_guard<std::mutex> lk(m);
    ready = true;
    std::cout << "main() signals data ready for processing\n";
  }
  cv.notify_one();

  // wait for the worker
  {
    std::unique_lock<std::mutex> lk(m);
    cv.wait(lk, [] { return processed; });
  }
  std::cout << "Back in main(), data = " << data << '\n';

  worker.join();
}
#include <future>
#include <iostream>
#include <thread>

int main() {
  // future from a packaged_task
  std::packaged_task<int()> task([] { return 7; });  // wrap the function
  std::future<int> f1 = task.get_future();           // get a future
  std::thread t(std::move(task));                    // launch on a thread

  // future from an async()
  std::future<int> f2 = std::async(std::launch::async, [] { return 8; });

  // future from a promise
  std::promise<int> p;
  std::future<int> f3 = p.get_future();
  std::thread([&p] { p.set_value_at_thread_exit(9); }).detach();

  std::cout << "Waiting..." << std::flush;
  f1.wait();
  f2.wait();
  f3.wait();
  std::cout << "Done!\nResults are: " << f1.get() << ' ' << f2.get() << ' '
            << f3.get() << '\n';
  t.join();
}
#include <chrono>
#include <iostream>
#include <thread>

// "busy sleep" while suggesting that other threads run
// for a small amount of time
void little_sleep(std::chrono::microseconds us) {
  auto start = std::chrono::high_resolution_clock::now();
  auto end = start + us;
  do {
    std::this_thread::yield();
  } while (std::chrono::high_resolution_clock::now() < end);
}

int main() {
  auto start = std::chrono::high_resolution_clock::now();

  little_sleep(std::chrono::microseconds(100));

  auto elapsed = std::chrono::high_resolution_clock::now() - start;
  std::cout
      << "waited for "
      << std::chrono::duration_cast<std::chrono::microseconds>(elapsed).count()
      << " microseconds\n";
}