文章

C++

C++

从C到C++

命名空间

命名空间用于解决代码名称冲突的问题,降低命名冲突的风险。

1
2
3
namespace mySpace{
    void foo();
}

使用域解析运算符::在函数名称前给出命名空间。

1
2
3
4
int main()
{
    mySpace::foo();
}

也可以使用using指定命名空间或要调用的函数,其后再调用时就不需要使用命名空间了。

不要滥用using namespace std,更不要在头文件中使用using namespace std。

1
2
3
4
5
int main()
{
    using namespace std;
    cout << "hello world!" << endl;
}

C++17后,命名空间可以嵌套,还可以给命名空间起别名。

1
2
3
4
5
6
7
8
9
10
11
namespace Outer {
    namespace Inner {
        /* ... */
    }
}

namespace Outer::Inner {
    /* ... */
}

namespace mySpace = Outer::Inner;

字面量

字面量是写在代码中的量,包括:

  • 十进制123
  • 八进制0173
  • 十六进制0x7B
  • 二进制0b1111011
  • 浮点数3.14f
  • 双精度浮点值3.14
  • 字符’a’
  • C风格字符串”hello world”
  • C++17新增的十六进制浮点数0x3.ABCp-10

数值字面量可以使用单引号作为分隔符,如23’456’789。

变量

变量的声明和初始化:

1
int n = 7;

C++提供的变量类型有:

  • 整型
    • (signed) int/signed
    • unsigned (int)
    • (signed) short (int)
    • unsigned short (int)
    • (signed) long (int)
    • unsigned long (int)
    • (signed) long long (int)
    • unsigned long long (int)
  • 浮点型
    • float
    • double
    • long double
  • 字符型
    • char
    • char16_t
    • char32_t
    • wchar_t
  • bool
  • std::byte(C++17)

隐式转换不必说,显式类型转换有三种方式:

1
2
3
int i = (int)f;
int i = int(f);
int i = static_cast<int>(f);

从C而来的类型

枚举的本质是整型。

1
2
3
4
enum Season {
    Spring = 1,
    Summer
};

枚举类则是类型安全的,枚举值名不会自动超出封闭的作用域,因此枚举在使用时总需要作用域解析操作符

1
2
3
4
5
6
enum class Season {
    Spring,
    Summer
};

Season s = Season::Spring

结构体和类很相近。

1
2
3
4
struct Student {
    int age;
    char* name;
};

语句

C++17允许if或switch语句使用一个初始化器,变量只能在初始化器和大括号中使用。

1
2
3
if (int i = rand(); i % 2 == 1) {
	std::cout << i << "是一个奇数" << std::endl;
}

switch语句的表达式必须为整型或枚举,并与常量比较。C++17允许使用[[fallthrough]]有意忽略break。

1
2
3
4
5
6
7
8
9
Season s = Season::Spring;
switch (s) {
case Season::Spring:
	/* ... */
	[[fallthrough]];
case Season::Summer:
	/* ... */
	break;
}

函数

C++14允许函数返回类型推断。

1
2
3
auto addNumbers(int num1, int num2) {
	return num1 + num2;
}

函数都有一个预定义的局部变量__func__表示当前执行的函数名。

数组

虽然仍然可以使用C风格的数组,但是最好使用定长数组std::array和动态数组std::vector。

1
2
3
int myArray[3] = { 1, 2, 3 };
array<int, 3> arr = { 1, 2, 3 };
vector<int> vec = { 1, 2, 3 };

结构化绑定

C++17允许用中括号同时声明多个变量,并使用数组、结构体、对组或元组来初始化。

1
2
3
4
struct Point { double mX, mY; };
Point p;
p.mX = 1.0; p.mY = 2.0;
auto [x, y] = p;

初始化表列

使用初始化表列可以编写接收可变数量参数的函数。

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

int makeSum(initializer_list<int> lst) {
	int total = 0;
	for (int v : lst) {
		total += v;
	}
	return total;
}

调用函数时,可以使用:

1
int a = makeSum({ 1,2,3 });

字符串

代码中直接出现的字符串是字符串字面量,保存在字面量池中。生字符串使用R()R"分隔符序列(生字符串)分隔符序列"引导,其中不会出现转义字符。

std::string是basic_string模板类的一个实例。尽管string是一个类,但不妨把它当成一种内建类型。

string类重载了operator+,operator+=,operator==,operator!=,operator[],operator<等运算符,以符合使用者预期的方式工作。

std命名空间包含了许多辅助函数来进行string的转化,如:

  • string to_string(int val);
  • int stoi(const string& str, size_t* idx=0, int base=10);//idx接收第一个未能转化的字符索引

C++17引入了std::string_view类解决对参数为const string&但传入const char*会创建副本的问题。string_view是const string&的简单替代品,只包含字符串的指针和长度,从不复制字符串。通常按值传递string_view。

1
2
3
string_view extractExtension(string_view fileName) {
	return fileName.substr(fileName.rfind('.'));
}

在这个函数下,传入const char*和const string&都没有问题,也不会制作字符串的副本。只会值传递string_view,也就是指针和长度。

无法拼接string和string_view,也无法隐式地从string_view创建string。可以的方案是使用sv.data()或string(sv)。

C++特性(一)

指针和引用

指针和智能指针

nullptr是空指针常量,类型是指针类型。

不要使用C的指针和malloc(),free(),使用智能指针和new,delete,new[],delete[]。

最重要的智能指针是std::unique_ptr和std::shared_ptr。

std::unique_ptr类似普通指针,但当unique_ptr超出作用域或被删除是会自动释放内存或资源,因而不需要调用delete。shared_ptr使用引用计数,超出作用域时递减引用计数,计数为0时释放对象。

1
2
auto pc1 = make_unique<Complex>();
auto pc2 = make_shared<Complex>();

智能指针的reset()方法可以释放当前指针的资源并进行重设,如果不传入参数,则设为nullptr。

1
2
pc1.reset(new Complex());
pc1.reset();

release()方法解除智能指针的所有权。

1
2
3
4
Complex* p = pc1.release();
/* ... */
delete p;
p = nullptr;

引用

C++的引用可以看作一个已定义变量的别名,引用的内部实现是常指针,但请把引用视作变量本身。创建引用时必须初始化,且不能修改。

1
2
void func1(int& a) { a = 5; }
void func2(int* const a) { *a = 5; }

函数如果返回引用,则应当将返回值视作变量本身。

1
2
3
4
5
6
7
8
9
10
11
int a = 0;
int& getA() { return a; }

int main() {
	int x = getA();		//int x = a;
	int& y = getA();	//int& y = a;
	getA() = 100;		//a = 100;
	int* p = &getA();	//int* p = &a;
	getA()++;			//a++
    return 0;
}

常量引用作为函数参数可以增加效率:函数不会创建副本,只传递指针,且不修改原始变量。

1
2
3
4
5
6
7
8
9
10
void printString(const std::string& s) {
	std::cout << s << std::endl;
}

int main() {
	std::string s = "hello world";
	printString(s);
	printString("hello world");
	return 0;
}

如果要修改对象,则传递非常量引用。非常量引用的初始值必须为左值,函数此时不可以传入字面量。

三目运算符给出的是引用,因而可以做左值。

1
2
a > b ? a : b = 10;
*(a > b ? &a : &b) = 10;

右值引用

可以获取地址(有名称)的值称为左值,不可获取地址的称为右值。字面量、临时对象和临时值都是右值。

右值引用是对右值的引用,这用在临时对象上,在临时对象销毁前,某些值的复制可以用复制指针来代替。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
void fun(Complex& s) {
	std::cout << "左值引用:" << s << std::endl;
}

void fun(Complex&& s) {
	std::cout << "右值引用:" << s << std::endl;
}

int main() {
	Complex s1 = { 1.0,2.0 };
	Complex s2 = { 3.0,4.0 };
	fun(Complex());	//无参构造函数+右值引用+析构函数
	fun(s1);		//左值引用
	fun(s1 + s2);	//有参构造函数+右值引用+析构函数
	cout << "pause" << endl;
  return 0;
}

要注意,右值引用作为形参是左值,因为其有地址。

move()可以将左值移动为右值。

1
2
3
4
5
6
7
void fun(Complex&& s) {
	std::cout << "右值引用:" << s << std::endl;
}

void newfun(Complex&& s) {
	fun(move(s));
}

const

常量

C++的const常量是真实的常量,可以作为数组长度。

const的实现原理是符号表,编译过程中对常量直接进行替换。如果编译过程发现对常量使用&或extern,则给常量分配内存。

常量引用

引用都是常量,所以const引用是指常量引用。将对象作为参数传递时,优先选择常量引用参数,只有明确需要修改对象时才使用非常量引用。

常方法

常方法是指方法内不可修改类的任何非可变数据成员。

constexpr

常量表达式在编译时计算。

1
2
3
4
5
constexpr int get5() { return 5; }

int main() {
	int myArray[get5() + 1];
}

static

静态成员

静态成员位于类层次,不属于任何对象。

静态链接

静态函数只允许在本文件中使用,其他文件无法链接到静态函数。

也可以将该函数封装到没有名字的命名空间,起的作用和static相同。

extern

所有函数默认都为外部函数。

可以用extern声明全局变量,在extern声明时不会分配内存。但是不要使用全局变量。

函数静态变量

函数的静态变量属于函数定义,不属于任何一次单独执行的函数,所有被调用的函数共享这一变量。

类型

类型别名

用using指定类型别名。如string其实是类型别名。

1
using string  = basic_string<char, char_traits<char>, allocator<char>>;

可以使用std::function,也可以使用函数指针,可以为函数指针指定类型别名。

1
using MyFunction = bool(*)(int, int);

可以为类的成员成员指定类型别名,但不允许在没有对象的情况下解除非静态成员的引用。

1
2
3
Complex c = { 3.0,4.0 };
auto methodPtr = &Complex::abs;
cout << (c.*methodPtr)() << endl;

类型转换

const_cast()用于给变量添加或去除常量特性。C++17新增as_const(),相当于const_cast<T&>(obj)。

static_cast()显式执行类型转换,基本上完成C++类型规则允许的类型转换。可以转换指针和引用,但是不能转换对象。

1
2
3
4
5
6
7
8
9
Base* b;
Derived* d = new Derived();
b = d;
d = static_cast<Derived*>(b);

Base base;
Derived derived;
Base& br = derived;
Derived& dr = static_cast<Derived&>(br);

reinterpret_cast()重新解释类型,例如将void*解释为为具体的指针。

dynamic_cast()会进行运行时动态类型检测,可将某个类的指针或引用转化为同一继承结构层次中的其他类对象的指针或引用。指针转失败会返回nullptr,引用转失败抛出std::bad_cast。

类型推断

类型推断有两个关键字,auto和decltype。auto可以对类型作推断,并去除const限定符和引用,有时这会产生副本,可以使用auto& 或const auto&。decltype可以把表达式作为实参,而且也不会去除const限定符和引用。

1
decltype(foo())f = foo();

特性

  • [[noreturn]]声明函数不会交还调用点,例如函数将会导致进程或线程终止。
  • [[deprecated]]声明已被废弃,不建议使用。可以使用[[deprecated()]]为废弃添加解释说明。
  • [[fallthrough]]C++17,显式地允许switch忽略break语句。空case不需要声明。
  • [[nodiscard]]C++17,如果丢弃了nodiscard的返回值、类、结构体、枚举等,编译器会给出警告。
  • [[maybe_unused]]:给不使用的参数使用maybe_unused,编译器不再给出警告。

面向对象

类与对象

对象是对数据及数据的操作方法的封装,而同类型的对象抽象出其共性就是类。类通过一个简单的外部接口与外界发生关系。对象和对象之间通过消息进行通信。

类把属性和方法进行封装,对属性和方法进行访问控制。类的访问控制关键字包括public,private和protected。类的默认访问说明符是private,结构体的默认访问说明符是public。

友元使用关键词friend。友元函数可以访问类的私有成员,友元类中的函数全部都是友元函数。友元类一般作为传递消息的辅助类,若B是A的友元类,则一般A是B的子属性,用B来修改A。

C++面向对象模型

C++类对象中的成员变量和成员函数是分开存储的。C++类中的普通成员函数都隐式包含一个指向当前对象的this常指针。

const修饰成员函数,表示*this不能被修改。 此时this不仅是常指针,更是常量常指针。

1
2
double getReal() const;
double getReal();

如果一个类中有同名的常量和非常量函数,算是重载。常对象和非常对象可以分别调用。

对象的构造与析构

构造函数是与类名相同的特殊成员函数,没有任何返回类型的声明。

当栈中的对象超出作用域时,对象会被销毁,这时会发生两件事,调用对象的析构函数并释放对象的内存。先被创建的对象后释放。

无参构造函数(默认构造函数)

用类创建对象时,调用无参构造函数,无参构造函数的调用不能加空括号,否则编译器会将其视为函数声明。

1
2
Complex c1;		//调用默认构造函数
Complex c2();	//可以通过编译,但编译器认为这是一个函数声明

如果没有显式地声明构造函数,则编译器会提供默认的无参构造函数。

default和delete

如果希望C++保留默认构造函数,可以使用default。如果不希望使用构造函数,可以使用delete。

1
2
3
4
5
6
7
8
9
10
class MyClass1 {
public:
	MyClass1() = default;
	MyClass1(int);
};

class MyClass2 {
public:
	MyClass2() = delete;
};

可以将拷贝构造函数和operator=设为delete来禁止赋值和按值传递。

有参构造函数

有参构造函数有以下调用形式。

1
2
3
4
Complex c1(1.0, 2.0);
Complex c2 = { 1.0, 2.0 };
Complex c3{ 1.0, 2.0 };
Complex c4 = Complex(1.0, 2.0); //使用了匿名对象,随后使之成为c4,而非拷贝

拷贝构造函数

编译器生成的拷贝构造函数的具有默认形式:

1
2
MyClass::MyClass(const MyClass& c)
	:m1(c.m1), m2(c.m2) {}

如果没有显式地声明拷贝构造函数,编译器会提供默认的拷贝构造函数。

C++传递函数参数的默认方式是值传递,实参初始化形参时使用拷贝构造函数。

1
2
Complex c1 = old_c; //调用拷贝构造函数
Complex c2(old_c);

拷贝构造和赋值运算符

如果函数返回匿名对象,给对象赋值则会使匿名对象析构,如果使用匿名对象初始化一对象,则匿名对象会转化为新的对象。

1
2
3
4
5
6
7
8
9
10
Complex fun() {
	return Complex();
}

int main() {
	Complex c = fun();	//在fun()内调用构造函数
	Complex c2, c3;		//调用无参构造函数
	c2 = c;				//调用赋值运算符
	c3 = fun();			//调用赋值运算符和匿名对象的析构函数
}

总之,声明会使用拷贝构造函数,而赋值语句会使用赋值运算符。

移动语义

应当实现移动构造函数和移动赋值运算符,它们可以将右值的所有权交给现在的变量。只有知道源对象即将销毁时移动语义才有用。移动结束之后需要将源对象设为nullptr以防源对象的析构函数释放这块内存。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class Sheet {
public:
	Sheet(Sheet&& src)noexcept
		:Sheet() {
		swap(*this, src);
	}
	Sheet& operator = (Sheet&& rhs) noexcept {
		Sheet temp(move(rhs));
		swap(*this, temp);
		return *this;
	}
private:
	Sheet() = default;
};

标准库的swap()也是依赖移动语义实现的,避免了所有复制。

1
2
3
4
5
6
template <class _Ty, class>
void swap(_Ty& _Left, _Ty& _Right) noexcept(is_nothrow_move_constructible_v<_Ty>&& is_nothrow_move_assignable_v<_Ty>) {
    _Ty _Tmp = _STD move(_Left);  //T temp(std::move(left));
    _Left    = _STD move(_Right); //left = std::move(b);
    _Right   = _STD move(_Tmp);   //right = std::move(temp);
}

五规则和零规则

如果类中动态分配了内存,通常应当事先析构函数、拷贝构造函数、移动构造函数、赋值运算符与移动赋值运算符。

但在现代C++中,应当避免旧式的、动态分配的内存,而改用现代结构。

构造函数初始化器

可以使用构造函数初始化器初始化成员。有参构造成员、const和引用必须在初始化器中赋值。 如果初始化器有多个成员,按照成员的定义顺序构造它们。

1
2
MyClass::MyClass(Complex c)
	:mC(c) {}

委托构造

委托构造允许构造函数调用该类的其他构造函数,这个调用必须使用构造函数初始化器,且必须是唯一的成员初始化器。

1
2
Complex::Complex()
	:Complex(0.0, 0.0) {}

运算符重载

运算符函数是一种特殊的成员函数或友元函数。成员函数具有this指针,而友元函数没有this指针。

二元算数运算符一般重载为全局函数,因为有时需要隐式的类型转换或自定义类型在运算符右边的情形。

1
2
3
friend Complex operator+(const Complex& c1, const Complex& c2){
	return Complex(c1.mR + c2.mR, c1.mI + c2.mI);
}

小心自赋值,如果在自赋值时先销毁了原变量是灾难性的。为保证自赋值安全和异常安全,常用的方法是进行地址检测、仔细地排列语句顺序或使用交换。

1
2
3
4
5
6
7
8
9
10
11
Widget& Widget::operator=(const Widget& rhs){
    Bitmap* pOrig = pb;
    pb = new Bitmap(*rhs.pb);
    delete pOrig;
    return *this;
}

Widget& Widget::operator=(Widget rhs){
    swap(rhs);
    return *this;
}

前置++和后置++用一个int占位参数进行区分。前置++返回引用,后置++返回值。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
//前置++
Complex& Complex::operator++()
{
	this->mR++;
	return *this;
}

//后置++
Complex Complex::operator++(int)
{
	auto tmp(*this);
	++(*this);
	return tmp;
}
不要重载&&和 ,这会让它们的短路功能失效。

为了满足链式编程的需求,重载«和»时需要返回流的引用。

1
2
3
4
friend std::ostream& operator<<(std::ostream& out, const Complex& c) {
	out << c.mR << " + " << c.mI << "i";
	return out;
}

对于一些容器,往往需要重载下标运算符。为了提供只读访问,往往还提供const版本。下标运算符并不一定只能接受整数,也可以接受其他类型作为键。

1
2
3
4
5
6
7
8
9
10
11
template <typename T>
T& AssociativeArray<T>::operator[](std::string_view key)
{
	for (auto& element : mData) {
		if (element.mKey == key)
			return element.mValue;
	}

	mData.push_back(Element(key, T()));
	return mData.back().mValue;
}

下标运算符不能接收多个参数,可以重载函数调用运算符。此外,重载函数调用运算符可以将对象伪装成函数指针,然后将函数对象当成回调函数传给其他函数。

为实现类型转换,需要重载类型转换运算符。类型转换运算符函数不需要返回值类型,因为运算符名即确定返回类型。

1
2
3
4
Complex::operator double() const
{
	return this->mR;
}

不过此时会出现多义性,可以用将构造函数或类型转换运算符函数标为explicit来禁用自动类型转换。

1
2
3
Complex c(1.0, 2.0);
double d2 = 1.0 + c;

将double()标为explicit后,下面可以使用:

1
2
Complex c(1.0, 2.0);
double d2 = static_cast<double>(1.0 + c);

代码重用

继承

继承模型

子类继承父类的全部成员变量和除了构造及析构以外的成员函数。

类型兼容性原则:子类就是特殊的父类。子类是父类成员叠加子类新成员得到的。

  • 子类对象可以当做父类对象使用
  • 子类对象可以直接赋值给父类对象
  • 子类对象可以初始化父类对象
  • 父类指针可以直接指向子类对象
  • 父类引用可以直接引用子类对象

当子类和父类有同名成员时:

  • 子类的成员屏蔽父类的同名成员
  • 访问父类同名成员需要使用父类的类名限定符
  • 父类成员的作用域延伸到所有子类

构造和析构

子类对象在创建时:

  • 首先调用父类的构造函数
  • 父类构造函数执行结束后,执行子类的构造函数。
  • 当父类的构造函数有参数时,需要在子类的初始化列表中显式调用
  • 析构函数的调用顺序与构造函数相反

当继承和组合混搭时:

  • 先构造父类,再构造成员变量,最后构造自己
  • 先析构自己,再析构成员变量,最后析构父类

多继承

多继承有可能会带来二义性,尤其是多继承了同属于一个父类的两个子类。虚继承可以保证被继承的父类只会构造一次。

解决“菱形”问题的最好方式是将最顶层的类设为抽象类,将所有方法设为纯虚方法,只声明方法而不提供定义。如果顶层的类提供了方法的实现,则可以虚继承这个类,子类将视虚基类没有任何方法的实现。

多继承的合理应用是混入类,这种混入类通常以-able结尾。

多态

重载、重写、重定义

重载:

  • 必须在同一个类中进行
  • 子类无法重载父类的函数,父类同名函数会被覆盖
  • 重载发生在编译期间,根据参数表列决定函数调用

重写:

  • 必须发生在父类和子类之间
  • 父类和子类必须有相同的函数原型
  • 使用virtual声明
  • 多态在运行期间根据具体类型决定函数调用

重定义:

  • 不使用virtual,子类覆盖父类函数

将所有方法都设为virtual,除了构造函数(因为实例化子类对象时必须逐个调用父类的构造函数)。尤其是析构函数,这可以防止意外地忽略析构函数的调用。

如果需要在子类中重写某一方法,始终使用override关键字,确保重写的正常进行,而不是意外地创建了新的虚方法。

1
2
3
4
5
6
7
8
9
10
class Base
{
public:
	virtual void fun();
};

class Derived :public Base {
public:
	virtual void fun() override;
};

不能重写静态方法,因为静态方法属于类而不属于对象。

应当重写重载方法的所有版本,可以显式地重写也可以使用using关键字,后者表示可以接收父类的其他所有重载方法。

1
2
3
4
5
class Derived :public Base {
public:
	using Base::fun;
	virtual void fun() override;
};

多态的实现

  • 子类继承父类
  • 子类重写父类虚函数
  • 父类指针/引用指向子类对象

不要到数组使用多态,因为指针步长不一定相等。

多态原理

当类中声明虚函数时,编译器会在类中生成虚函数表。虚函数表是一个存储类成员函数指针的数据结构,由编译器自动生成和维护,virtual成员函数会被编译器放入虚函数表中。

当存在虚函数表时,每个对象都有一个指向虚函数表的VPTR指针,VPTR一般作为类对象的第一个成员。

编译器不区分对象是子类对象还是父类对象,它只区分是否是虚函数,是在虚函数表中寻找函数的入口地址

初始化子类的vptr时,vptr会分步依次指向父类的虚函数表。

模板

类模板

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
template <typename T>
class Grid
{
public:
	explicit Grid(size_t width = kDefaultWidth, size_t height = kDefaultHeight);
	virtual ~Grid() = default;
	Grid(const Grid& src) = default;
	Grid<T>& operator=(const Grid& rhs) = default;
	Grid(Grid&& src) = default;
	Grid<T>& operator=(Grid&& rhs) = default;

	std::optional<T>& at(size_t x, size_t y);
	const std::optional<T>& at(size_t x, size_t y)const;

	size_t getWidth() { return mWidth; }
	size_t getHeight() { return mHeight; }

	static const size_t kDefaultWidth = 10;
	static const size_t kDefaultHeight = 10;

private:
	void verifyCoordinate(size_t x, size_t y)const;
	std::vector<std::vector<std::optional<T>>> mCells;
	size_t mWidth, mHeight;
};

类定义中,编译器根据需要将Grid解释为Grid<T>。但在类定义之外需要使用Grid<T>

模板机制

编编译器遇到模板方法的定义时,会进行语法检查,但不进行编译。在遇到具体化的模板时,编译器才会将模板参数替换为具体参数而编译。编译器为模板化方法生成代码的方式是在编译之前替换模板参数。编译器并不是把模板处理成能够处理任意类型的函数或类,而只是单纯地生成了多个类。

非类型模板参数

因此,不仅可以参数化类型,也可以参数化变量。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
template<typename T, size_t WIDTH, size_t HEIGHT>
class Grid
{
public:
	Grid() = default;
	virtual ~Grid() = default;
	Grid(const Grid& src) = default;
	std::optional<T>& at(size_t x, size_t y);
	const std::optional<T>& at(size_t x, size_t y) const;
	//C风格数组不支持移动语义
	size_t getHeight() { return HEIGHT; }
	size_t getWIDTH() { return WIDTH; }

private:
	void verifyCoordinate(size_t x, size_t y)const;
	std::optional<T> mCells[WIDTH][HEIGHT];
};

template<typename T, size_t WIDTH, size_t HEIGHT>
void Grid<T, WIDTH, HEIGHT>::verifyCoordinate(size_t x, size_t y) const
{
	if (x >= WIDTH || y >= HEIGHT) {
		throw std::out_of_range("");
	}
}

template<typename T, size_t WIDTH, size_t HEIGHT>
std::optional<T>& Grid<T, WIDTH, HEIGHT>::at(size_t x, size_t y)
{
	verifyCoordinate(x, y);
	return mCells[x][y];
}

template<typename T, size_t WIDTH, size_t HEIGHT>
const std::optional<T>& Grid<T, WIDTH, HEIGHT>::at(size_t x, size_t y) const
{
	return const_cast<std::optional<T>&>(std::as_const(*this).at(x, y));
}

问题在于,必须用常量整数来具体化模板类,而且具体化出的不同模板类本质上是不同的类。

此外,模板参数可以拥有默认值。

可以做如下的定义:

1
2
template <typename T, const T DEFAULT = T()>
class Grid {/* Ommited */ };

调用时可以传入一个默认元素来初始化。但是非类型参数不能是对象,甚至不能是double和float。非类型参数被限定为整型、枚举、指针和引用。

1
2
	Grid<int> grid1;	// 以0初始化
	Grid<int, 10> grid2;// 以10初始化

更好的方式是使用T引用作为非类型模板参数:

1
2
template <typename T, const T& DEFAULT>
class Grid {/*Ommited*/ };

C++17标准规定,第二个参数传入的引用必须是转换的常量表达式,不允许引用子对象、临时对象、字符串字面量等。在C++17前,实参不能是临时的,不能是无链接(内部或外部)的命名左值。

1
2
3
4
5
6
7
8
9
10
namespace {
	int defaultInt = 10;
	Complex defaultComplex(1.0, 2.0);
}

int main() {
	Grid<int, defaultInt> grid1;
	Grid<Complex, defaultComplex> grid2;
	return 0;
}

方法模板

类中有时需要更多的模板参数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
template <typename T>
class Grid
{
public:
	//Omitted

	template<typename E>
	Grid(const Grid<E>& src);

	template<typename E>
	Grid<T>& operator=(const Grid<E>& rhs);

	void swap(Grid& other) noexcept;
};

template<typename T>
template<typename E>
Grid<T>::Grid(const Grid<E>& src)
	:Grid(src.getWidth(),src.getHeight())
{
	for (size_t i = 0; i < mWidth; i++) {
		for (size_t j = 0; j < mHeight; j++) {
			mCells[i][j] = src.at(i, j);
		}
	}
}

template<typename T>
template<typename E>
Grid<T>& Grid<T>::operator=(const Grid<E>& rhs)
{
	Grid<T> temp(rhs);
	swap(temp);
	return *this;
}

template<typename T>
void Grid<T>::swap(Grid& other) noexcept
{
	using std::swap;
	swap(mWidth, other.mWidth);
	swap(mHeigh, other.mHeight);
	swap(mCells, other.mCells);
}

类模板特例化

有时需要对特定的类型提供不同的实现。

1
2
template<>
class Grid<const char*> {};

特例化的类并不会获得类模板的任何代码,必须重新编写整个类的实现。

可以编写部分特例化的模板类。

1
2
template <size_t WIDTH, size_t HEIGHT>
class Grid<char*> {/* Ommited */ };

部分特例化的模板类可以用在为所有指针类型编写特例化的类上,例如为拷贝构造和赋值运算提供深拷贝。

template <typename T>
class Grid<T*>
{
public:
	explicit Grid(size_t width = kDefaultWidth, size_t height = kDefaultHeight);
	virtual ~Grid() = default;
	Grid(const Grid& src);
	Grid<T*>& operator=(const Grid& rhs);
	Grid(Grid&& src) = default;
	Grid<T*>& operator=(Grid&& rhs) = default;

	void swap(Grid& other)noexcept;

	std::unique_ptr<T>& at(size_t x, size_t y);
	const std::unique_ptr<T>& at(size_t x, size_t y)const;

	size_t getWidth() { return mWidth; }
	size_t getHeight() { return mHeight; }

	static const size_t kDefaultWidth = 10;
	static const size_t kDefaultHeight = 10;

private:
	void verifyCoordinate(size_t x, size_t y)const;
	std::vector<std::vector<std::unique_ptr<T>>> mCells;
	size_t mWidth, mHeight;
};

函数模板

调用模板函数时,可以使用<>,但不指定也会进行自动类型推导。编译器会优先考虑普通函数,空<>会要求匹配函数模板。函数模板的类型匹配是严格的,不会进行自动类型转换。可以用普通函数重载模板函数。

函数模板可以实现自动类型推导。

1
2
template <typename RetType, typename T1, typename T2>
RetType add(const T1 t1, const T2 t2) { return t1 + t2; }

调用时可以使用

1
2
auto result1 = add<long long, int, int>(1, 2);
auto result2 = add<long long>(1, 2);

模板函数的定义也可以使用自动类型推导。不过要注意auto可能会去掉const和引用,这时应该使用decltype(auto)。

1
2
template <typename T1, typename T2>
decltype(auto) add(const T1 t1, const T2 t2) { return t1 + t2; }

函数不允许部分特例化,但是可以通过重载实现期望的功能。

1
2
3
4
5
template<typename T>
size_t find<T*>(T* const& value, T* const* arr, size_t size);	//无法编译

template<typename T>
size_t find(T* const& value, T* const* arr, size_t size);

可变模板

1
2
template <typename T>
constexpr T pi = T(3.141592653589793238462643383279502884);

调用时:

1
float piFloat = pi<float>;

模板模板参数

实现以下类可以让用户指定底层容器:

1
2
3
4
5
6
7
8
9
10
template <typename T, typename Cotainer>
class Grid
{
public:
	//Ommited
	typename Cotainer::value_types& at(size_t x, size_t y);
private:
	//Ommited
	std::vector<Cotainer> mCells;
};

用户应当使用:

1
Grid<int, vector<optional<int>>> grid;

但是用户更应该使用:

1
Grid<int, vector> grid;

此时需求的是模板作为参数,应该如此声明:

1
2
3
4
5
6
7
8
9
10
11
12
template <typename T,
	template <class E, class Allocator = std::allocator<E>> class Cotainer = std::vector>
class Grid
{
public:
	//Ommited
	typename std::optional<T>& at(size_t x, size_t y);
private:
	//Ommited
	std::vector<Cotainer<std::optional<T>>> mCells;
};

模板递归

如果需要实现N维容器模板,就需要用到模板递归。基本情形作为特例化单独实现。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
template <typename T,size_t N>
class NDGrid
{
public:
	explicit NDGrid(size_t size = kDefaultSize);
	virtual ~NDGrid() = default;

	NDGrid<T, N - 1>& operator[](size_t x);
	const NDGrid<T, N - 1>& operator[](size_t x)const;

	void resize(size_t new_size);
	size_t getSize() const { return mElements.size(); }
	static const size_t kDefaultSize = 10;

private:
	std::vector<NDGrid<T, N - 1>> mElements;
};

template <typename T>
class NDGrid<T, 1>
{
public:
	explicit NDGrid(size_t size = kDefaultSize);
	virtual ~NDGrid() = default;

	T& operator[](size_t x);
	const T& operator[](size_t x)const;

	void resize(size_t new_size);
	size_t getSize() const { return mElements.size(); }
	static const size_t kDefaultSize = 10;

private:
	std::vector<T> mElements;
};

可变参数模板、参数包、折叠表达式

可以使用参数包实现可变参数模板。

1
2
3
4
5
template<typename... Types>
class MyClass {};

template<typename T1, typename... Types> //至少一个参数
class MyClass {};

不能直接遍历参数包,需要使用递归。

1
2
3
4
5
6
7
void processValues() { }// 空实现,递归终止

template <typename T1, typename... Tn>
void processValues(T1 arg, Tn... args) {
	handleValue(arg);
	processValues(args...);	// processValues(a1, a2, a3);
}

可以使用转发引用,保证左值按引用传递为左值,右值按引用传递为右值。

1
2
3
4
5
6
7
8
void processValues() { }// 空实现,递归终止

template <typename T1, typename... Tn>
void processValues(T1&& arg, Tn&&... args) {
	handleValue(std::forward<T1>(arg));
	processValues(std::forward<Tn>(args)...);
	//processValues(std::forward<A1>a1, std::forward<A2>a2, std::forward<A3>a3);
}

继承混入类也可以使用参数包:

1
2
template<typename... Mixins>
class MyClass :public Mixins...{};

C++17允许折叠表达式,使用折叠表达式就不需要递归了。如下折叠了逗号运算符:

1
2
3
4
5
template <typename... Tn>
void processValues(Tn&... args) {
	(handleValue(args), ...);
	//(handleValue(a1), (handleValue(a2), handleValue(a3)));
}

二元折叠需要一个初始值,顺带保证了至少需要一个参数。

1
2
3
4
5
template <typename T, typename... Values>
void sumValues(const T& init, const Values&... values) {
	return (init + ... + values);
	//return (((init + v1) + v2) + v3);
}

C++特性(二)

异常

异常的实现

异常是一种程序控制机制,与函数机制独立互补。异常超越了函数,其对函数进行跨越式回跳。

构造函数没有返回类型,因此只能通过异常机制来解决出错。但是构造失败使得析构函数无法调用,必须在构造函数内清理资源。

析构函数不应抛出任何异常,析构函数是释放对象使用的内存和资源的一个机会,如果因异常提前退出则会失去这个机会。所有析构函数都被隐式标记为noexcept,所以异常都应该在析构函数内部处理。

try块中的程序如果发生异常则用throw抛出异常对象,try块内栈解旋,全部栈对象析构,顺序与构造相反。抛出异常的唯一方式是使用关键字throw。

抛出和捕获异常

可以抛出任何类型的异常,但应当抛出std::expection对象。

应当按const引用捕获异常,避免按值捕获时出现的对象截断。

catch语句按顺序匹配抛出的异常,捕获严格基于类型匹配,不存在转换。如果没有匹配的catch语句,则调用terminate,缺省功能是用abort终止程序。

catch(…)捕获所有类型的异常。如果无法处理异常,可以用throw继续向上抛出,不要使用throw e,这会发生截断。

可以使用std::throw_with_nested()抛出嵌套异常,用dynamic_cast<const nested_exception*>()检测嵌套异常无则给出空指针,有则可以用rethrow_nested()重新抛出。也可以直接用rethrow_if_nested()重新抛出。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
void doSomething() {
	try {
		throw runtime_error("runtime_error");
	}
	catch (const std::runtime_error&) {
		cout << __func__ << "捕获了runtime_error" << endl;
		cout << __func__ << "抛出了MyException" << endl;
		throw_with_nested(MyException("MyException嵌套runtime_error"));
	}
}

int main() {
	try {
		doSomething();
	}
	catch (const MyException & e) {
		cout << __func__ << "捕获了MyException:" << e.what() << endl;
		try {
			rethrow_if_nested(e);
		}
		catch (const runtime_error & e) {
			cout << "嵌套的异常:" << e.what() << endl;
		}
	}
}

异常的多态性

标准异常体系:

  • exception
    • logic_error
      • domain_error
      • length_error
      • invalid_argument
      • out_of_range
      • future_error
      • bad_optional_access
    • runtime_error
      • range_error
      • overflow_error
      • underflow_error
      • regex_error
      • system_error
        • ios_base::failure
        • filesystem::filesystem_error
    • bad_type_id
    • bad_cast
      • bad_any_cast
    • bad_weak_ptr
    • bad_function_call
    • bad_exception
    • bad_alloc
      • bad_array_new_length
    • bad_variant_access

多数异常类要求在构造函数中设置what()返回的字符串,编写异常类的派生类时,需要重写what()方法。

I/O流

标准输入输出流

cin为标准输入流,cout为缓冲的标准输出流,cerr为非缓冲的错误流,clog为cerr的缓冲版本。

缓冲流的数据会首先存入缓冲区,待缓存区的刷新一并流入目的地。刷新缓冲区的方式有:

  • flush()
  • 遇到sential标记,如endl
  • 流离开作用域被析构时
  • 流缓存满时
  • 要求从对应的输入流输入数据时(如cin输入时,cout刷新)

输出流

C++流可以正确解析C风格的转义字符。

<<运算符返回流的引用,因而支持链式编程。

put()接收单格字符,write()接收字符数组和长度输出字符串,没有特殊的形式。

1
2
3
4
const char* s = "hello world!";
cout << s << endl;
cout.write(s, strlen(s));
cout.put('\n');
  • good()用于判断流是否处于可用状态,good()==!(eof() fail())
  • eof()流是否到达文件末尾
  • fail()流的最近一次操作是否失败,将流转化为布尔型时给出的是!fail()
  • clear()重置流的错误状态
1
2
cout.flush();
if (cout) { cerr << "刷新cout失败" << endl; }

C++流能够识别输出操作算子,其用于改变流的行为。

  • boolalpha:将布尔型输出为true和false而非1和0,默认为noboolalpha。
  • hex/oct/dec:十六进制/八进制/十进制
  • setprecision:设置小数的输出位数
  • setw:设置数值数据的宽度
  • setfill:设置宽度不足时用于填充的字符
  • showpoint/noshowpoint:不带小数的浮点数是否显示小数点
  • put_money:写入格式化的货币值
  • put_time:写入格式化的时间值
  • quoted:将字符串加引号,并转义字符串内的引号

除了setw外,上述操作算子持续有效,直到重置操作算子。

输入流

>>运算符从流中获取数据,遇到空白字符时断开。对流的输入会改变流(主要是流的位置)。

输入流同样具有good(),eof(),fail()方法检测流的状态。

get()从流中返回下一个字符,传入字符引用返回流引用,传入字符指针和字符数返回流引用,传入字符串引用读取一行。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
string readName(istream& stream){
	string name;
	while (stream) {
		int next = stream.get(); //
		if (!stream || next == std::char_traits<char>::eof()) {
			break;
		}
		name += static_cast<char>(next);
	}
	return name;
}

string readName(istream& stream) {
	string name;
	char next;
	while (stream.get(next)) {
		name += next;
	}
	return name;
}

unget()将读入的前一个字符放回流中,fail()可以查看是否成功。

putback()将指定字符放回流中。

peek()查看流中下一个字符。

getline()读取一行,行尾的空白字符不包含在内,和get()的区别在于get()会把行尾字符留在流内。可以指定最多读取的字节数,也可以用流引用和字符串引用。

  • boolalpha/noboolalpha:将false或0解释为false,其余为true
  • hex/oct/dec:十六进制/八进制/十进制
  • skipws/noskipws:是否跳过空白字符
  • ws:跳过当前位置的一串空白字符
  • get_money:读取格式化的货币值
  • get_time:读取格式化的时间值
  • quoted:读取引号内的字符串,并转义字符串内的引号

流的定位

所有输入输出流都可以使用seek(),输入流的seek()本质上是seekg(),输出流的seek本质上是seekp()。seek()可以接受绝对位置或相对位置,位置std::streampos和偏移量std::streamoff以字节计数。

  • ios_base::beg流的开头
  • ios_base::end流的结尾
  • ios_base::cur流的当前位置

tell()也分为tellg()和tellp(),其可以返回当前位置的绝对位置。

输入流可以用tie()链接至输出流的地址,当输入流请求数据时会自动刷新链接的输出流。解除链接可以传入nullptr。

字符串流

字符串流的 std::ostringstream和std::istringstream继承std::ostream和std::istream,用法是类似的。

文件流

文件流的构造函数可以接收文件名和打开方式作为参数。

  • ios_base::in打开文件读,没有文件不会创建
  • ios_base::out打开文件写,存在文件则清空
  • ios_base::ate打开文件后定位到末尾,没有文件不创建
  • ios_base::app每次写入时都会重新定位到末尾
  • ios_base::tranc打开文件,没有文件不创建,存在文件则清空
  • ios_base::binary二进制
打开方式可以通过逻辑进行组合。

双向I/O

双向流是iostream的子类,fstream是iostream的子类。双向I/O有两个指针分别保存读位置和写位置,同时支持输入流和输出流的方法。

多线程

线程

C++11的标准库可以使用std::thread创建线程。thread的构造函数是一个可变参数模板,其后的参数被传入第一个参数对应的函数中。线程函数的参数总是被复制到复制到线程的某个内部存储之中,可以通过std::ref()或cref()按引用传递。

1
thread t1(mySum, 1, 2);

可以使用函数对象,重载()运算符,传入thread的构造函数。建议使用统一初始化。函数对象总是被复制到复制到线程的某个内部存储之中,可以通过std::ref()或cref()按引用传递。

1
thread t1{ mySumClass{1,2} };

lambda表达式创建线程:

1
2
3
4
thread t1([a, b] {
	cout << a << " + " << b << " = " << a + b << endl;
	}
);

成员函数创建线程:

1
2
MyClass c(100);
thread t1{ &MyClass::fun,&c };

线程开始之后处于结合状态,即使线程已经执行完毕。销毁一个结合的线程前必须调用join()或detach()。join()会阻塞直到线程执行完毕,detach()会将线程对象与底层OS线程分离,两者都会使得线程变得不可结合。

thread_local关键字允许变量对每个线程拥有独立的副本,且该变量会在线程声明周期持续存在。

异常应当在线程内部捕获和处理。可以将异常复制并在其他线程中重新抛出。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
void doSomething() {
	throw runtime_error("运行时异常");
}

void threadFun(exception_ptr& err) {
	try {
		doSomething();
	}
	catch (...) {
		//exception_ptr current_exception() noexcept
		//返回一个exception_ptr对象,引用目前正在处理的异常或其副本
		err = current_exception();
	}
}

void doWorkInThread() {
	exception_ptr error;
	thread t(threadFun, ref(error));
	t.join();//为方便演示,在此阻塞
	if (error) {
		//[[noreturn]] void rethrow_exception(exception_ptr p)
		//重新抛出p引用的异常
		rethrow_exception(error);
	}
}

int main() {
	try {
		doWorkInThread();
	}
	catch (const exception & e) {
		cout << "捕获" << e.what() << endl;
	}

}

相关的函数还有template exception_ptr make_exception_ptr(E e) noexception,其创建一个引用给定对象的exception_ptr对象,相当于:

1
2
3
4
5
6
try {
	throw e;
}
catch (...) {
	return current_exception();
}

原子操作库

std::atomic<>类型保证变量的原子性,基本类型都有命名后的相应的原子类型。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
void increment(atomic<int>& counter) {
	for (int i = 0; i < 100; i++) {
		++counter;
		this_thread::sleep_for(1ms);
	}
}

int main() {
	atomic<int> counter(0);
	vector<thread> threads;
	for (int i = 0; i < 10; i++) {
		threads.push_back(thread{ increment, ref(counter) });
	}
	for (auto& t : threads) {
		t.join();
	}
	cout << counter << endl;
}

原子操作不使用锁,效率较高。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
//要求T是is_trivially_copy的
template < class T >
struct atomic {
	//判断atomic<T>中的T对象是否为lock free的,若是返回true。
	//lock free是指在运行时,底层没有显式的同步机制,具体取决于指定类型的大小。
	//多个线程并发访问T不会出现data race,任何线程在任何时刻都可以不受限制的访问T。
	bool is_lock_free() const volatile;
	bool is_lock_free() const;
	atomic() = default;
	constexpr atomic(T val);
	//禁止拷贝
	atomic(const atomic&) = delete;
	//禁止赋值,但是可以显式转换再赋值
	//如atomic<int> a = static_cast<int>(b) 这里atomic<int> b
	atomic& operator=(const atomic&) = delete;
	atomic& operator=(const atomic&) volatile = delete;
	//可以通过T类型对atomic赋值
	//如atomic<int> a; a = 10;
	T operator=(T val) volatile;
	T operator=(T val);
	//读取被封装的T类型值,是个类型转换操作,默认内存序是memory_order_seq需要其它内存序则调用load
	//如:atomic<int> a , a == 0或者cout << a << endl都使用了类型转换函数
	operator T() const volatile;
	operator T() const;

	//以下函数可以指定内存序memory_order
	//将T的值置为val,并返回原来T的值
	T exchange(T val, memory_order = memory_order_seq_cst) volatile;
	T exchange(T val, memory_order = memory_order_seq_cst);
	//将T值设为val
	void store(T val, memory_order = memory_order_seq_cst) volatile;
	void store(T val, memory_order = memory_order_seq_cst);
	//访问T值
	T load(memory_order = memory_order_seq_cst) const volatile;
	T load(memory_order = memory_order_seq_cst) const;
	//该若本atomic的T值和expected相同则用val值替换本atomic的T值,返回true;若不同则用本atomic的T值替换expected,返回false。
	bool compare_exchange_weak(T& expected, T val, memory_order = memory_order_seq_cst) volatile;
	bool compare_exchange_weak(T&, T, memory_order = memory_order_seq_cst);
	bool compare_exchange_strong(T&, T, memory_order = memory_order_seq_cst) volatile;
	bool compare_exchange_strong(T&, T, memory_order = memory_order_seq_cst);
};

对整数和指针,atomic还有特殊化的原子操作。

对整数:

  • atomic::fetch_add
  • atomic::fetch_sub
  • atomic::fetch_and
  • atomic::fetch_or
  • atomic::fetch_xor
  • atomic::operator++
  • atomic::operator–
  • operator (comp. assign.)

对指针:

  • atomic::fetch_add
  • atomic::fetch_sub
  • atomic::operator++
  • atomic::operator–
  • operator (comp. assign.)

互斥

  • 希望读写共享内存的线程试图锁定互斥体对象,如果另一个线程持有锁,在该线程阻塞,直到锁被释放或超时。
  • 一旦线程获得锁,线程可以随意访问共享内存。
  • 线程对共享内存操作完毕后,释放锁。没有机制保证哪个其他的线程有限获得锁。

非定时互斥体类包括std::mutex,recursive_mutex,shared_mutex。

  • lock(),阻塞,尝试获取锁。
  • try_lock(),尝试获取锁,返回是否成功。
  • unlock(),释放锁。

mutex是具有独占所有权语义的互斥体类,只能有一个线程获得互斥体。

recursive_mutex和mutex基本相同,区别在于其可以在同一个互斥体上再次调用lock()和try_lock(),而调用unlock()的次数应该和上锁的次数相同。如果互斥体不是递归的,拥有所有权的线程不能在这个互斥体上继续调用lock()和try_lock(),否则可能导致死锁。

shared_mutex还有共享所有权语义。独占所有权相当于写者,共享所有权相当于读者。读写锁同时只能有一个写者或多个读者,但不能同时既有读者又有写者,读写锁的性能一般比普通锁要好。共享所有权使用的方法是lock_shared(),try_lock_shared()和unlock_shared()。

定时互斥体类包括std::timed_mutex,recursive_timed_mutex,shared_timed_mutex。额外支持:

  • try_lock_for(rel_time),在给定的相对时间内阻塞,尝试获取锁,返回是否成功。
  • try_lock_until(abs_time),在给定的绝对时间内阻塞,尝试获取锁,返回是否成功。
  • try_lock_shared_for(rel_time)
  • try_lock_shared_until(abs_time)

锁类是RAII类,析构时自动释放关联的互斥体。C++标准定义了std::lock_guard,unique_lock,shared_lock,scoped_lock。

  • lock_guard的构造函数。
    • exclicit lock_guard(mutex_type& m)接收一个互斥体的引用。阻塞,尝试获得互斥体的锁。
    • lock_guard(mutex_type& m, adopt_lock_t)假定该线程已获得该互斥体的锁,管理并在销毁时自动释放互斥体。
  • unique_lock允许将获得锁推迟到计算需要时,可以用owns_lock()确定是否已经获得了锁。unique_lock类也有lock(),try_lock(),try_lock_for(),try_lock_until(),unlock()等方法。
    • exclicit unique_lock(mutex_type& m)。
    • unique_lock(mutex_type& m, defer_lock_t) noexception不立即尝试获得锁,锁可以稍后获得。
    • unique_lock(mutex_type& m, try_to_lock_t)尝试获得互斥体的锁,失败不阻塞,会在稍后获得锁。
    • unique_lock(mutex_type& m, adopt_lock_t)。
    • unique_lock(mutex_type& m, const chrono::time_point<Clock, Duration>& abs_time)在绝对时间前尝试获得锁。
    • unique_lock(mutex_type& m, const chrono::duration<Rep, Period>& rel_time)在相对时间前尝试获得锁。
  • share_lock的构造函数与unique_lock相同,但获得的是共享锁,而非独占锁。
  • scoped_lock与lock_guard类似,但接收可变数遍的互斥体。

一个线程安全的写入:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class Counter {
public:
	Counter(int id, int numIterations):mId(id),mNumIterations(numIterations){}
	void operator()()const {
		for (int i = 0; i < mNumIterations; i++) {
			lock_guard lock(sMutex);
			cout << "Counter " << mId << " has value " << i << endl;
		}
	}

private:
	int mId;
	int mNumIterations;
	static mutex sMutex;
};

mutex Counter::sMutex;

用scoped_lock接收可变数量的互斥体:

1
2
3
4
5
6
7
mutex mut1;
mutex mut2;

void fun() {
	scoped_lock locks(mut1, mut2);
	//已获得锁
}//自动释放

lock()和try_lock()可以通过可变参数模板同时获得多个锁,前者并不保证有序,且如果获得失败,已获得的锁都会unlock()。

1
2
3
4
5
6
7
8
9
mutex mut1;
mutex mut2;

void fun() {
	unique_lock lock1(mut1, defer_lock);//不立即尝试获得锁
	unique_lock lock2(mut2, defer_lock);
	lock(mut1, mut2);
	//已获得锁
}//自动释放

call_once()搭配once_flag()可以保证函数只被调用一次。

1
2
3
4
5
6
7
8
9
10
11
12
13
once_flag gOnceFlag;

void initializeSharedResources(){
	// ... 初始化多线程需要的共享资源
	cout << "共享资源已初始化完成" << endl;
}

void processingFunction() {
	//确保共享资源已初始化
	call_once(gOnceFlag, initializeSharedResources);
	// ... 执行代码
	cout << "处理中..." << endl;
}

future

future和promise是线程间传递结果的通信通道。

1
2
3
4
5
6
7
8
9
10
11
12
13
void doWork(promise<int> thePromise) {
	// ... 干点什么
	thePromise.set_value(42);
}

int main() {
	promise<int> myPromise;
	auto theFuture = myPromise.get_future();
	thread theThread(doWork, std::move(myPromise));
	// ... 去干其他的什么
	int result = theFuture.get();//阻塞
	theThread.join();
}

std::packaged_task可以更方便地使用promise。

1
2
3
4
5
6
7
8
9
10
int mySum(int a, int b) { return a + b; }

int main() {
	packaged_task<int(int, int)> task(mySum);
	auto theFuture = task.get_future();
	thread theThread(std::move(task), 39, 3);
	// ... 去干别的活
	int result = theFuture.get();
	theThread.join();
}

std::async()允许默认或选择函数的执行方式,launch::async表示在一个不同的线程上异步执行,而launch::deferred强制在get()调用时,在主调线程上同步地执行函数。

1
2
3
4
5
6
7
8
int Func() { return 42; }

int main() {
	auto myFuture = async(Func);
	//auto myFuture = async(launch::async, Func);
	//auto myFuture = async(launch::deferred, Func);
	int result = myFuture.get();
}

future只要求T可以移动构造,因而只能使用一次get(),结果将会被移动出future。而shared_future要求T的拷贝构造,从而可以多次调用get()。可使用std::future::share()或给share_future构造函数传入(移入)future以创建shared_future。

C++标准库

STL三大部分为算法、容器、迭代器。

容器通过类模板技术,实现数据类型和容器模型的分离;迭代器技术实现了遍历容器的同一方法,也为STL的算法统一性的提供奠定了基础;算法通过函数对象实现了自定义数据类型的算法运算,所以说,STL的算法也提供了统一性。

迭代器

迭代器分为只读迭代器、只写迭代器、前向迭代器、双向迭代器、随机迭代器。

  • 只写迭代器
    • operator++
    • operator*
    • 拷贝构造函数
    • operator=
  • 只读迭代器
    • operator++
    • operator*
    • operator->
    • operator=
    • operator==
    • operator!=
  • 前向迭代器
    • 只读迭代器加上默认构造函数
  • 双向迭代器
    • 前向迭代器加上operator–
  • 随机迭代器
    • 双向迭代器加上
    • operator+
    • operator-
    • operator+=
    • operator-=
    • operator<
    • operator>
    • operator<=
    • operator<=
    • operator[]

从顺序上分,迭代器分为iterator,const_iterator,reverse_iterator,const_reverse_iterator。

容器提供了begin()和end()返回第一个元素和最后一个元素之后的迭代器。除此之外还有cbegin(),cend(),rbegin(),rend(),crbegin(),crend()。

标准库还支持这些函数的非成员函数版本,建议使用非成员版本。

容器及工具类

标准库中的所有容器都是类模板,容器是同构的,只允许一种类型。

所有容器提供的是值语义而非引用语义,所有存储的元素必须能够拷贝,必须提供拷贝构造。

动态数组 vector

vector的类模板包含元素类型和分配器类型。

1
template <class _Ty, class _Alloc = allocator<_Ty>> class vector;
  • 长度 int size()
  • 容量 int capacity()
  • 是否为空bool empty()
  • 构造函数
    • vector<T> vec
    • vector(n)
    • vector(n, elem)
    • vector(const vector& vec)
  • 重用,删除所有元素并重新添加
    • void assign(n)
    • void assign(n, elem)
  • 交换
    • void swap(vec)
  • 比较:按字典序比较,比较则要求元素实现operator==或operator<
  • 遍历
    • for(auto it = vec.begin(); it != vec.end(); ++it)
    • for(auto& ele : vec)
  • 访问
    • T& operator[](pos)不提供边界检查
    • T& at(pos)提供边界检查
    • T& front()头部元素
    • T& back()尾部元素
    • void push_back(T t)尾部追加元素
    • void pop_back()删除尾部元素
  • 删除
    • void clear()
    • iterator erase(pos)返回下一个元素的迭代器
    • iterator erase(beg, end)
  • 插入
    • iterator insert(pos, elem)
    • iterator insert(pos, n, elem)
    • iterator insert(pos, beg, end)
  • emplace直接放置到位直接在特定位置分配空间并构建对象
    • T& emplace_back(n, elem)

双向链表 list

list不支持随机访问,即不支持operator[],此外在迭代器中也只支持++it和–it。

  • splice(it, l)在it处串联list
  • remove()删除特定元素
  • remove_if()同上
  • unique()根据operator==或用户提供的二元谓词删除重复元素。
  • merge()合并两个链表,两个链表必须都为正序排列。
  • sort()排序
  • reverse()逆序

单向链表 forward_list

单向链表支持单向迭代,为此,不支持begin(),end(),back()。但是提供了before_begin(),用于指向第一个元素之前。除此之外,insert(),splice()等方法都不支持,但支持insert_after(),splice()等方法。

双端数组 deque

deque不要求元素保存在连续内存中,其支持首尾两端的常量时间的元素插入和删除。

deque还支持push_front(),pop_front(),emplace_front()。

定长数组 array

1
template <class _Ty, size_t _Size> class array;

array是C风格的,大小固定的分配在栈上的数组,因此不支持push_back()等方法,但支持fill()以填满array。

队列 queue

1
template <class _Ty, class _Container = deque<_Ty>> class queue;

queue实际上是对deque或list的包装,其实现“先入先出”语义,主要支持push_back()和pop_front(),事实上只需调用push()或emplace()和pop()即可。其他可以调用的方法只有front(),back(),size(),empty(),swap()。

优先队列 priority_queue

1
template <class _Ty, class _Container = vector<_Ty>, class _Pr = less<typename _Container::value_type>> class priority_queue

优先队列只保证队列头的元素有着最高的优先级,底层容器可以是vector或deque,默认优先级是按照operator<来进行比较的。

优先队列仅支持push()和emplace()插入,pop()删除。top()获取头部元素的常量引用,除此之外还有size(),empty(),swap()。

堆栈 stack

1
template <class _Ty, class _Container = deque<_Ty>> class stack;

堆栈与队列几乎一致,但提供“先入后出”语义。底层容器可以是vector,list或queue。

  • push()在顶部添加元素
  • pop()删除顶部元素
  • top()返回顶部元素的引用或常量引用
  • empty()
  • size()
  • swap()
  • 比较运算符

对组 pair

1
template <class _Ty1, class _Ty2> struct pair;

对组拥有公共成员first和second,定义了operator==和operator<。

1
2
3
4
auto p1 = make_pair(42, 2.718);// make_pair工具函数模板
auto p2 = pair(42, 2.718);// 构造函数的模板参数推导
auto [i, d] = p1;// 结构化绑定

有序表 map和multimap

1
2
3
4
5
template <class _Kty,
	class _Ty,
	class _Pr = less<_Kty>,
	class _Alloc = allocator<pair<const _Kty, _Ty>>>
class map;

map依据值对元素进行排序。

  • 插入
    • pair insert(k, v)返回pair.second表示是否插入成功,pair.first表示迭代器
    • pair insert(pair)
    • pair insert_or_assign(k, v)如果存在则重写
    • V& operatr[k]访问并替换,总会创建值对象
    • emplace()
    • emplace_hint()
    • try_emplace()
  • 查找和计数
    • find(k)如果没找到返回end()
    • count(k)用于确认key是否存在
  • 删除
    • erase(k)

插入元素并判断是否插入成功:

1
2
3
if (auto [iter, success] = map1.insert({ "Steve", 42 }); success) {
	cout << "插入成功" << endl;
}

迭代器应当使用常量迭代器,因为如果修改元素的键会破坏排序。

1
2
3
4
5
6
7
for (auto iter = cbegin(map1); iter != cend(map1); ++iter) {
	cout << iter->second  << endl;
}

for (const auto& [key, value] : map1) {
	cout << value << endl;
}

C++17后所有关联容器都使用节点,可以用extract()提取节点句柄,insert()插入节点句柄。节点句柄只能移动,是节点中存储元素的所有者。

1
map2.insert(map1.extract("Steve"));

merge()允许合并两个关联容器,如果无法移动则节点留在源容器中。

multimap允许多个元素使用同一个键,因而不提供operator[],at()。插入元素也总会成功,不会返回pair,不提供insert_or_assign()或try_emplace()。

  • 查找
    • lower_bound()
    • upper_bound()
    • equal_range()

有序集合 set和multiset

set和map基本相同,但值本身就是键,不显示保存键。因此没有operator[],insert_or_assign(),try_emplace()。

哈希表

1
2
3
4
5
6
template <class _Kty,
	class _Ty,
	class _Hasher = hash<_Kty>,
	class _Keyeq = equal_to<_Kty>,
	class _Alloc = allocator<pair<const _Kty, _Ty>>>
class unordered_map

unordered_map和map类似,但内部使用哈希表。包含一些哈希专用方法。

  • load_factor()每个桶的平均元素数
  • bucket_count()桶的数目
  • local_iterator/cosnt_local_iterator用于遍历单格桶中的元素
  • bucket(key)返回包含该key的桶索引
  • begin(n)返回索引为n的桶的第一个元素的local_iterator
  • end(n)返回索引为n的桶的最后一个元素之后元素的local_iterator。
  • cbegin(n)
  • cend(n)

哈希表是无序容器,不支持upper_bound()和lower_bound(),但支持equal_range()。

unordered_multimap之于unordered_map,相当于map之于multimap。

除此之外,还有unordered_set和unordered_multiset,用法类似。

标志位集合 bitset

bitset是定长的位序列。可以用set(),reset(),flip()对特定位进行操作,operator[]可设置和访问单个字段的值。

bitset支持所有按位操作运算符,使之行为与真正的位序列相同。

算法

std::function

function用于创建指向函数、函数对象或lambda表达式的类型,可以当做函数指针使用,也可以用作回调函数的参数。

1
2
function<int(int, int)> f = mySum; //std::function
auto fp = mySum; //函数指针

lambda表达式

lambda表达式的基本语法是[]()->{},()用于接收参数,在无参数时可以省略,->称为拖尾返回类型,可由编译器自动推断,{}内为lambda表达式体。

1
2
auto l1 = [](int a, int b)->int {return a + b; };
auto l2 = [] {return 42; };

只需将参数类型指定为auto,就可以实现泛型。

1
auto isQualified = [](auto score) { return score >= 60; };

lambda表达式会被编译为函数对象,[]用于在当前环境中捕获变量,最后成为函数对象的const或非const成员。如果需要按引用可以使用&,具体有:

  • [=]值捕捉全部变量,只有使用的变量才会被捕捉,下同。
  • [&]引用捕捉全部变量。
  • [x]值捕捉x。
  • [&x]引用捕捉x。
  • [=,&x]
  • [&,x]
  • [this]捕捉当前对象。
  • [*this]捕捉当前对象的副本。

不过仍然应当手动指定要捕捉的变量。

函数对象的operator()是const函数,如果需要修改成员,则可以指定mutable,此时不能省略()。

1
2
int x = 21;
auto l = [x]()mutable {x *= 2; cout << x << endl; };

捕捉时也可以包含表达式。

1
2
auto p = std::make_unique<int>(42);
auto myPrint = [p = std::move(p)]{ cout << *p << endl; };

函数对象

重载函数调用运算符的类,其对象称为函数对象。

C++支持透明运算符仿函数,允许忽略模板类型参数。建议使用透明运算符仿函数。

1
2
3
4
double geometricMeanTransparent(const vector<int>& v) {
	double mult = accumulate(cbegin(v), cend(v), 1, multiplies<>());
	return pow(mult, 1.0 / v.size());
}

C++函数对象

C++提供了5类二元算数运算符的仿函数类模板:plus、minus、multiplies、divides、modulus。此外提供了一元的取反操作。

C++还提供了所有标准的比较:equal_to、not_equal_to、less、greater、less_equal、greater_to。例如,priority_queue和关联容器使用less作为元素的默认比较。

升序排序的优先队列:

1
priority_queue<int, vector<int>, greater<>> myQueue;

C++提供了三个逻辑函数对象:logical_not、logical_or、logical_or。

C++的所有按位操作函数对象:bit_and、bit_or、bit_xor、bit_not。

函数对象适配器

绑定器用于将参数绑定特定的值,使用std::bind()不仅可以绑定参数,还可以重新安排函数参数。

1
2
3
4
5
6
7
8
9
10
11
void fun(int n, string_view s) {
	cout << "fun(" << n << ", " << s << ")" << endl;
}

int main() {
	string str = "hello world!";
	auto f1 = bind(fun, placeholders::_1, str);
	f1(42);	// fun(42, hello world!)
	auto f2 = bind(fun, placeholders::_2, placeholders::_1);
	f2("Test", 42);	// fun(42, Test)
}

如果要绑定引用或const引用,需要使用std::ref()和cref()。

1
2
3
4
void increment(int& value) { ++value; }
int index = 0;
auto incr = bind(increment, ref(index));
incr();

当函数被重载时,需要显式指定绑定的函数。

1
2
3
void overloaded(int n){}
void overloaded(float f){}
auto f = bind((void(*)(float))overloaded, placeholders::_1);

寻找第一个大于或等于100的元素,用绑定器或lambda表达式实现:

1
2
3
4
5
6
7
auto endIter = end(myVector);
auto it = find_if(begin(myVector), endIter,
	bind(greater_equal<>(), placeholders::_1, 100));
//auto it = find_if(begin(myVector), endIter, [](int i) {return i >= 100; });
if (it != endIter) {
	cout << "找到了" << *it << endl;
}

取反器类似于绑定器,但是对调用结果取反。

1
auto it = find_if(begin(myVector), endIter, not_fn(perfectScore));
本文由作者按照 CC BY 4.0 进行授权