IMPORTANT

此文档仍在调整补充。

封装 Encapsulation

将数据结构和操作数据的方法设置在一个类或结构体中。确保程序结构清晰安全和操作规范。

C++类

C++ 的类与 C 的结构体具备几乎完全一致的功能,仅在默认访问修饰符上有所区别:类默认为私有,结构体默认为公有。

TIP

C++ 以面向对象为核心思想进行程序设计,将类视为一个抽象出来的模板,将由此类创建的“变量”称为对象。C++ 允许在类与结构体中声明函数以定义对象行为。C++ 也支持嵌套定义的类。

类可以视为一张蓝图,拥有这张蓝图就能做出许多这样的产品,这些产品就是对象。

Info

C++ 允许在类定义中为成员变量直接使用 = 设置默认值。

结合面向对象程序设计的思想,C++ 还有以下特殊的类中成员函数:

构造函数

CLASS_NAME(...) {}

用于为创建的对象设置初始值,或者使用 new 分配堆内存或其他资源。其函数名与类名称相同,不标识返回类型,可拥有参数,可被重载;

析构函数

~CLASS_NAME() {}

用于释放对象使用的资源;其函数名与类名称相同,并在前面添加 ~;它不标识返回类型,不能拥有参数,因此也不能被重载;

复制构造函数

CLASS_NAME(const CLASS_NAME&) {}

用于将一个对象的值完全复制到另一个对象;若原对象使用了指针(尤其是指向堆内存)或是系统资源,应该考虑在复制新对象时为它创建独立副本。

Info

Q1. 复制构造函数为什么传入引用?

若函数中使用普通的值形式参数,在函数被调用时会将值直接复制到调用栈,但复制时会触发构造函数的调用,因而此函数会被再次调用,从而造成无限递归。但传入引用就不会导致复制,而是直接访问原数据。

Q2. 什么时候需要手动编写析构函数和复制构造函数?

若构造函数中出现手动堆内存分配,或是请求了系统资源,那么必须手动编写析构函数(防止内存或资源泄漏)和复制构造函数(防止资源重复释放)。如果有成员变量是指针类型时尤其应当注意。 使用智能指针可以从一定程度减少堆内存管理问题,简化析构函数和复制构造函数。

以上函数可添加 =delete 声明以停用,添加 =default 声明以启用默认空体函数。声明设置在函数头之后,像变量赋值那样:

class Test {
	Test() = delete;
	~Test() = default;
};

Info

特殊类成员函数也能被声明为私有,此类随后将只能显式调用公开函数才能实例化对象;

class Vehicle {
private:
	int id;
	string name;
	int performance;
	Vehicle(int id, string name, int performance) {
		this->id = id;
	this->name = std::move(name);
	this->performance = performance;
	}
/* Another version
	Vehicle(int id, string name, int performance) : id(id), name(name), performance(performance) {};
*/
public:
// Allocate on stack
	Vehicle instantiate_vehicle(int id, string name, int performance) {
	return Vehicle(id, std::move(name), performance);
  // Can also be written as
  // return {id, std::move(name), performance};
}
// Allocate on heap
	Vehicle* instantiate_vehicle_onHeap(int id, string name, int performance) {
		return new Vehicle(id, std::move(name), performance);
  		// Can also be written as
  		// return new {id, std::move(name), performance};
}
}

可进一步修改:添加一个同类型的指针并在公开函数加以判断,这样只能同时有一个此类对象被实例化,这就是类的单实例 (Singleton)

class Vehicle {
private:
	int id;
	string name;
	int performance;
	static Vehicle* a_car;
	Vehicle(int id, string name, int performance) {
		this->id = id;
		this->name = std::move(name);
		this->performance = performance;
	}
public:
	Vehicle* instantiate_vehicle(int id, string name, int performance) {
		if (a_car == nullptr) a_car = new Vehicle(id, name, performance);
			return a_car;
		}
	}
};

this 指针

this 指针是非静态类成员函数可以使用的一个隐藏指针,指向对象地址。通过一个类对象调用此函数时,能够使用 this 访问到此对象内的成员。这在类内成员与形式参数重名时非常实用。

访问控制

访问控制是封装的关键特性,良好的访问控制可让程序内的数据访问更加可靠安全。若不标注访问控制关键字,C++ 默认作为私有成员或私有继承。

第1列表示通过直接继承的子类能否访问父类成员,第2列表示在类外部通过该类对象能否访问成员。

修饰符派生类(子类)类外部(对象)
public
protected
private

继承时,可由子类决定以何种访问控制继承父类成员,且遵循权限更小者应用原则。被继承后,在子类使用父类成员按继承方式应用访问控制,此时仍然遵循上表规则。

例如若以 public 方式继承父类,在子类仍可直接像在父类那样使用 publicprotected 成员;

若以 protected 方式继承父类,父类的 public 成员在子类时作为 protected 成员使用,此时在外部不能通过子类对象访问它;

若以 private 方式继承父类,父类的 publicprotected 成员在子类时作为 private 成员使用;在上一条的基础上,任何继承子类的其他类均不能访问这个子类的任何成员。

有保护或私有成员的类中使用 friend 可以使指定的函数或类能够任意访问该类保护或私有成员,从而突破访问控制。

对象的初始化方式

C++ 允许多种实例化对象的方法;但不管是哪种初始化,它们都必须有构造函数的支持。

  1. 仅实例化

    string word;

    对构造函数的调用会自动完成,调用的是 string()

  2. 隐式调用构造函数

    string word = "Snowflake";

    寻找符合右侧值类型的构造函数,然后隐式地调用它。

  3. 显式调用构造函数

    string word = string("experimental");

    调用此构造函数,以完成对对象的实例化,并设置成员变量值。 [C++11 前] 实例化左侧对象,然后实例化右侧对象,并隐式调用复制构造函数; [C++11 及之后] 直接通过右侧构造函数实例化。

  4. 实例化并初始化

    string word("Biscuit");

    将变量名称和隐式调用构造函数结合的写法,在 C++11 及之后和 (3) 效果相同。

  5. [C++11 及之后] 初始化列表

    string word = {"phanomenon"};
  6. [C++11 及之后] 聚合初始化

    string word{"phanomenon"};

    [C++11] 类成员变量初始化要么全由默认初始化器完成,要么全由聚合初始化完成,不能混用;

    [C++14 及之后] 允许混用;

    class Test {
    	int id = -1;
    	int value = 0;
    };
     
    // Usage
    Test test_obj_1{1, 3};   // ✅ No problem
    Test test_obj_2{2};      // 🟡 Error in C++11

继承 Inheritance

为进一步提高代码的复用率,C++允许新类能连带取得已定义类的大部分成员和方法。继承的本质是在派生类中创建基类成员供派生类对象访问,构造的基类并不是一个独立的对象,而是视为对派生类的成员扩展。

派生类无法继承基类的 private 成员,派生类对象可访问基类 protected 成员;

若派生类拥有和基类相同标识符的成员,要访问基类成员,可通过:

  • 作用域运算符访问;
  • 将基类指针指向派生类,并访问;
  • 使用 using <class>::member 以使基类同名函数作用于派生类。

要初始化基类同名成员,需在派生类调用基类的构造函数。

特性

创建最高层级派生类对象时,自基类开始调用构造函数;删除最高层级派生类对象时,自最高层级开始调用派生类析构函数。

若一个类继承自其他类,那么在此类没有空构造函数(默认构造函数)的情况下,必须在这个类的构造函数初始化列表处显式调用基类构造函数。

class Animal {
	int id;
	Animal(int id) {};
};
 
class Lion : public Animal {
	// Lion() {};                   // ❌ 基类无默认构造函数,必须显式调用;
	Lion(int id) : Animal(id) {};   // ✅ 正确
};

菱形继承和虚继承

两个派生类都继承同一个基类,随后一个派生类又继承了这两个派生类,这就是菱形继承。菱形继承会使最高层派生类访问基类对象时产生多义性问题:它继承的几个类都各自拥有一个基类对象,编译器不能确定访问目标。这时要求这两个中间派生类使用虚继承以使最高层继承类只持有一个基类对象。

要使用虚继承,应该在期望继承的类上声明 virtual

class GlobalProperties {};
class Queue : public virtual GlobalProperties {};
class Stack : virtual public GlobalProperties {};
class QueueStack : public Queue, public Stack {};

声明为 virtual 只是为解决多重继承自同基类的实例化歧义问题,不会对此类造成任何影响;

最终继承类必须负责调用各层继承类的构造函数以分别完成对各个对象的实例化。

class QueueStack {
	QueueStack(...) : GlobalProperties(...), Queue(...), Stack(...) {};
};

多态 Polymorphism

多态的本质是:动态变化地通过调用同一个函数名却能调用不同的函数代码段。

要实现静态多态效果,在相同作用域内为同一个函数创建重载版本即可;

要实现动态多态效果(称为“重写”),必须:

  1. 已设置基类和它的派生类;
  2. 基类的此函数已被声明为 virtual,且未被声明为 final
  3. 派生类已重写基类此同名函数,且与基类同名函数的返回值一致;
  4. 设置基类指针,并指向目标函数所在的基类或继承自此基类的类对象。

virtual 相当于告诉编译器:需要检查继承此类的派生类是否重写了此函数。

通过改变基类指针指向的已继承类对象,即可访问到不同的函数段。

实现多态的第1要素是:函数拥有多个重写版本,即为基类函数标注 virtual,由继承者重写;

若基类函数未声明为 virtual,或者派生类同名函数与基类同名函数的参数列表不一致,派生类同名函数将隐藏基类同名函数。基类指针无论指向哪个已继承类对象,都将只能调用基类函数段

若基类函数声明为 virtual,且派生类同名函数与基类同名函数的参数列表完全一致,派生类同名函数将提供一个重写版本的基类同名函数。基类指针可根据所指向的对象自动确定调用哪个版本的函数段。

(>= C++11) 函数重写处的函数头之后可标注 override 以显式重写基类函数。

实现多态的第2要素是:虚函数表;

虚函数表是记录虚函数入口地址的列表;任何声明了虚函数,或继承的类当中有虚函数的类,都将为其设置虚函数表。类对象将得到此虚函数表起始地址。

函数声明

声明一个函数可使用以下方式:

[attr] return_type noptr-declarator (parameter-list) [cv] [ref] [except]
 
// >= C++11
[attr] auto noptr-declarator (parameter-list) [cv] [ref] [except] -> trailing
  • [可选] [C++11 及以后] [attr] 函数的属性值,使用 [[ATTRIBUTE]] 结构设置属性,部分可用属性有 noreturn, deprecated
  • return_type | auto 返回类型;
  • noptr-declarator 函数的标识符;
  • parameter-list 参数列表;
  • [可选] [cv] 常成员函数 const 和 停用优化 volatile 标记处;
  • [可选] [C++11 及以后] [ref] 函数引用限定,& 只能在左值使用;&& 只能在右值使用;
  • [可选] [except] 异常规定,函数能用 noexceptnoexcept() 标识是否返回异常或返回哪种异常;
  • [可选] [C++11 及以后] trailing 尾随返回类型。

string

C++的 string 类型是为字符串的动态增长和操作的需要而分配在堆内存中的一种内置数据类型。和一般 C 字符串(字符数组和字符指针)一样,可以使用下标访问 string 对象中的某个字符;除字符常量指针,它们也可以直接使用下标解引用赋值。

string 对象未使用 new 初始化,那么 string 对象的属性存储在栈中,而其字符串内容存储在堆中。

string 比普通 C 字符数组或字符指针的优势在于:

  • string 对象允许直接对其赋新的字符串,而 C 的字符数组或字符指针需要使用 strcpy()
  • string 对象允许直接与其他 string 对象或 C 字符串用逻辑运算符比较,而纯 C 字符串需要使用 strcmp()

string 的初始化

使用以下方法以初始化一个字符串 string 对象:

string word_1 = string("cupcake");
string word_2 = "What";
string word_3("milkyway");
string word_4 = {"Roasted_beef"};
 
string sentence = word_2 + " " + word_3;

在第5行,使用 + 运算符能连接 string 对象或字符数组并赋值给 string 对象,这是因为 string 类重载了 + 运算符。

Info

只有在左侧为 string 对象,右侧至少存在一个 string 对象时,才可使用 + 运算符连接原字符串并赋值到新字符串。

通过第1行或第3行给出的显式调用构造函数的方法还可使用其他重载版本初始化。

string 常用的成员函数

  • size() / length() 返回字符数;
  • find(string|char* sub_str[, int start=0]) 查找指定子串;找到时返回主串中的起始地址,未找到返回 string::npos
  • erase([int start=0]) 清除子字符串;
  • empty() 如果字符串为空串,返回 true

适用于可遍历对象的 for 循环 (range-based for loop)

这个特殊的 for 循环专为遍历对象打造,可以简化 for 循环当中的代码。

支持遍历以下类型:

  • 数组;
  • 标准库容器;
  • 以花括号括起的初始化列表;
  • 实现了 begin()end() 函数以返回存储值地址的自定义类。

它的结构如下:

for (auto& ELEMENT : RANGE?)

第1个项目是可迭代对象中每次遍历的单个元素,第2个项目是可迭代对象。

关键字和限定符

const 常量声明关键字

const 可用于标记变量和函数,从编译器角度表示“只读”状态,防止后续变量被修改;若发生修改,编译器在编译时会报告错误。

使用 const 时应当关注以下几点:

  • 严格 const 返回值的赋值目标。声明为 const 的函数返回值只能赋值到声明为 const 的对象;
  • const 能修饰成员函数。被修饰的成员函数将不得修改此 (this) 对象的成员变量值;
  • const 能修饰成员变量。被修饰的成员变量应当声明时初始化;
  • 强制链型 const 声明。若常量对象被引用或指向,必须将此引用或指向者也声明 const
  • 若使用 const 声明对象,除了被 mutable 修饰的成员,其他成员都不能修改。

static 静态声明关键字

Info

static 只能在定义处使用。

  • static 能修饰成员变量。被修饰的成员变量将成为同类对象的唯一公有变量;
  • static 能修饰成员函数。被修饰的成员函数将不可访问 this,它只能直接访问静态成员;它能够直接通过类名与访问运算符被访问;
  • static 修饰所有类成员,并停用特殊类函数,以此达到静态类效果

inline 内联声明关键字

若为函数声明:函数足够短,且短到只是简单地返回值或设置值,可将函数声明为 inline,如同 Macro 那样,使函数体直接嵌入到调用处,有机会减少函数调用跳转时的开销。常用于高性能需求程序。具体是否完成内联,由编译器决定。

(>= C++17) 若为类内静态变量声明:为类内静态变量添加 inline,可以在声明时完成初始化。

(>= C++17) 若为所有文件同名全局变量声明:该全局变量将不会引发重复定义的错误。

virtual 虚函数声明关键字

Info

virtual 只能用于声明非静态成员函数;只能在定义处使用。

virtual 是实现运行时多态的基石,它告诉编译器此函数可能在派生类中重写实现,需要尝试在派生类中查找已重写的同名函数。

  • 普通的虚函数:在基类函数声明,告诉派生类可重新实现此函数;
  • 虚析构函数:在基类函数声明,当通过基类指针释放派生类对象时,使得派生类析构函数也能被调用;
  • 纯虚函数:将基类的一个函数声明为此例:FUNCTION() = 0;,告诉编译器未实现,需要由派生类实现;此时基类将视为为抽象类,这类似 Java 类声明当中的 abstract。继承的类必须实现此函数。

若类中存在虚函数,该类将多出一个虚函数表指针 (在64位架构系统上大小为 8Bytes),置于对象内存起址。

friend 朋友关键字

friend 可以用于在类中声明外部函数、它的完整参数列表和返回值,使得此外部函数能访问此类的私有成员;也可以用于在类中声明另一个外部类,使得外部类能够访问此类的私有成员。

class Test {
	...
	friend class Test2;
	friend void accessible();
};

override 重写限定符

Info

override 不是一个保留关键字。

override 是用于子类成员函数明确重写父类成员函数的限定符,子类函数若被声明为 override,那么编译器还会检查:父类函数已经标注 virtual;子类函数重写的形参列表、返回值与父类的相同。这样可以明确函数已在子类重写。

final 禁用多态限定符

Info

final 不是一个保留关键字。

在C++,final 常用于类头部或重写版本虚函数头部之后,用于标记该类不可继承或该函数不可重写。它也拥有和 override 相同的作用。

mutable 始终可变成员变量关键字

mutable 用于修饰类内非静态成员变量,能够忽略逻辑上的 const 修饰,使非静态常成员函数(void func() const)仍能修改这个成员变量;常用于公共变量(例如多线程、操作系统同步控制变量)。

auto 自动类型判断关键字

带有 auto 声明的变量可以自动根据右值的类型为其设置类型,此过程由编译器完成。因此被标注变量必须同时初始化;

auto 还可以用于函数形参列表等其他位置,请查看 [C++ 标准](# C++ 历史和C++ 标准的更替)。

左值和右值

左值

在语法上,位于赋值符左侧的对象被称为“左值”。左值能够接受右侧传来的值,它应该是变量、实例化的对象,或返回普通引用的函数:

int lvalue_int = 15;
std::string lvalue_str{"Boost"};
 
class Test {
public:
    int id;
	int& set_id() { return id; }    
};
 
Test test;
test.set_id() = 12;

大多数左值也能作为右值使用。

右值

在语法上,位于赋值符右侧的对象被称为“右值”。右值将值传递(赋值)给左值,它可以是字面量、字符串常量。

引用 &

在 C 中,要使变量能够通过其他变量名称访问,或者要使传入的参数得到真实的修改,需要将此变量设置为指针;但在 C++ 允许在参数列表使用 & 以传入此变量地址,且无需在传入实参时主动添加 &

引用也可以用于函数,像初始化函数指针那样,但把指针符 * 改为引用符 & 初始化即可。

相对于指针,引用的特性有:

  • 语法透明:更安全且明确地访问变量值,而不会被理解为修改数组或者可能的内存操作不当;
  • 需初始化:以引用形式传入的变量必须已初始化,以避免不当访问内存段;
  • 不可修改被引用对象:引用到一个变量后将不能再修改;
  • 此对象的另一名称:引用的本质是为目标变量取别名,在使用上与普通变量别无二致;
  • 停用复制:向函数传递引用,可以停用传递时复制;函数返回引用时,同样可以避免复制时的开销;
  • 支持运算符多级使用:在重载运算符时返回引用,可支持多级操作符使用。

WARNING

不要返回函数内分配在栈上的局部变量引用,此函数运行结束后该栈内存已被释放。

可通过以下形式取别名:

int val_1 = 5;
// C
int* val_ptr = &val_1;
// C++
int& val_1_ref= val_1;
int* val_1_ptr = &val_1_ref;

使用引用可以明确操作修改此变量的值(而并非是一个连续数组),省去解引用的麻烦,同时避免不当地访问内存段。若只需读取引用值,应声明参数为 const 以避免被引用对象被修改。

WARNING

引用符号不会改变重载时参数列表的判断。例如同时声明 getVal(int)getVal(int&),这会产生冲突,引用只会使此对象传入时不会被复制,而是直接访问,由此可直接修改传入对象。事实上,使用引用型参数与使用普通参数别无二致,以常规用法使用即可。

右值引用 (>=C++ 11)

这是一种特殊的引用,由两个引用符 && 表示,这被称为右值引用;普通引用被称为左值引用;

“右值引用”在语法上只接受具备右值特性的字面常量。

int val_1 = 7;
int&& val_2 = val_1;              // ❌ Cannot do this!
int&& val_3 = 9;                  // ✅ Correct

右值引用几乎很少单独作为变量出现,请关注它的以下最常见用法;

右值引用的关键是所有权的转移:能够完成对象的移动,例如可创建一个移动构造函数:

CLASS_NAME(CLASS_NAME&& cls);

下例创建了一个 obj 对象,然后将对象的内容整个移动至新对象 moved_obj。其中,std::move() 能将对象转换为右值引用以完成移动。

CLASS_NAME obj;
CLASS_NAME moved_obj = CLASS_NAME(std::move(obj));

重定义运算符

基本运算符本质上也是一组函数的定义。基本运算符只能操作基本数据类型,要使用运算符操作类对象或不受支持的混合类型操作,需要重新定义运算符。重新定义运算符的操作本质上还是使用底层支持的运算符功能,重新定义只是再次进行了封装。

重新定义运算符需要关注:

  • 需要事先了解该运算符的函数形式;

  • 💡建议 单目运算符常重载为成员函数,双目运算符常重载为全局函数;

  • 🛑注意 流运算符应当重载为朋友函数。若重载为类成员,需要以下方形式访问,这是与直观相反的:

    class CLASS {
    public:
        int value;
        ...
        ostream& operator << (ostream& ost) {
            return ost << value;
        }
    };
     
    CLASS instance;
    instance.operator<<(cout); // aka. instance << cout;    Bad overload! :(
  • ⛔限制 赋值运算符必须重载为类的成员函数,原因之一是避免基本类型赋值操作被覆盖;

  • 💡建议 重载操作应当遵循该运算符的原始运算规则;

  • 💡建议 运算符应当返回引用,以便支持多级使用。 例如输出流支持多级使用,正是因为输出流运算符返回的是输出流对象:

    cout << "Anchor" << "Butter" << endl;

    这相当于:

    (((cout << "Anchor") << "Butter") << endl);

输出流运算符 <<

建议作为全局函数定义,以便于在使用 >> 时使编译器查找实现。

输出流运算符会将右侧值传递到标准输出流,并返回输出流对象的引用。

ostream& operator << (ostream& ost, const <TYPENAME>) {}

输入流运算符 >>

建议作为全局函数定义,以便于在使用 << 时使编译器查找实现。

输入流运算符会从左侧标准输入流获取值,赋值到右侧对象,并返回输入流对象的引用。

istream& operator >> (istream& ist, <TYPENAME>) {}

赋值运算符 =

与复制构造函数类似,但重载赋值运算符能够使复制时支持多级复制。

CLASS& operator = (const CLASS& rrand) { return *this; }

移动型赋值运算符 = noexcept

移动型赋值运算符能将右侧对象完全复制到左侧,同时清除(置空)右侧对象。

CLASS& operator = (CLASS&& rrand) noexcept { return *this; }

算术运算符 + - * /

// In class: CLASS + CLASS
CLASS operator ? (const CLASS& rrand) const { return instance; }
// In class: CLASS + int
CLASS operator ? (const int& rrand) const { return instance; }
// Out of class: CLASS + CLASS
CLASS operator ? (const CLASS& lrand, const CLASS& rrand) { return instance; }
// Out of class: int + CLASS
CLASS operator ? (const int& lrand, const CLASS& rrand) { return instance; }

模板

有时候一个类当中的一些操作能够适用于许多其他类型,并且由此希望在不为这些其他类型分别定义成员或者重载较多函数。使函数或类成员能够接受多种类型的统一化机制被称为模板,这又被称为泛用类型。

模板像函数那样接受参数,也可以像函数参数那样,为模板参数设置默认值。

以下是一个模板函数的示例,可传入任何操作符支持的变量类型。

template<class Wildcard_1, class Wildcard_2 = int>
void add(Wildcard_1 l_value, Wildcard_2 r_value) {
    std::cout << l_value + r_value << std::endl;
}

类中有接受泛用类型的成员时,需要将类声明为模板类,在声明此类的对象时,也需要指定类型。

若类模板参数都设置了默认值,在声明时可不设置模板提示符。

template<class Wildcard_1 = int, class Wildcard_2 = int>
class Calculator {
public:
    Wildcard_1 value_1;
    Wildcard_2 value_2;
 
    void add() {
        std::cout << value_1 + value_2 << std::endl;
    }
};
 
// On declaration
Calculator<int, float> calc = Calculator<int, float>();
Calculator calc_2 = Calculator();       // ✅ That's also no problem!

若要通过模板类获取其定义的嵌套类,可通过作用域运算符访问。

IMPORTANT

在声明一个模板类中定义的嵌套类时,应当明确 typename 声明;若缺乏此类型声明,编译器也可认为此声明是访问了模板类中的静态成员。

// C++17 ONLY
class Test {
	inline static int size = 0;
       ...
       class size {
          ...
       };
};
 
template<typename Wildcard>
class Example {
   	Wildcard::size;		// ❌ Cannot implicitly declare a variable
    						// ❌ Compiler does not know whether is a class or a 
                           //     static member
   	typename Wildcard::size; // ✅ Explicitly declared that this is a type (class)
};
 
int main() {
    
    
    return 0;
}

调用模板函数的一些方法

template<typename Wildcard = int>
inline void speak(Wildcard data = 3) {
    cout << "surprise! " << data << endl;
}
 
int main() {
    // 指定类型的函数指针 Function pointer that specified type
    void (*intSpeakPtr)(int) = &speak<int>;
    intSpeakPtr(5);
    // 自动判断类型的函数指针 Function pointer that checking type by compiler
    auto speakPtr = &speak<double>;
    speakPtr(3.14);
    // 使用 using 形式创建函数指针,这类似 typedef 类型别名定义
    using IntSpeakPtr = void (*)(int);
    IntSpeakPtr ptr = &speak<int>;
    ptr(7);
    // 使用 function 库类型创建函数指针,需要导入 functional
    std::function<void(float)> func = &speak<float>;
    func(2.718f);
    
}

输入输出流

C++将与外部输入输出设备进行数据传送的过程统合为流的概念,不论是与设备,还是与文件系统进行输入输出,这个过程都统称为流。以下是C++流类的关系:

ios_base
  └── ios
       ├── istream (输入流)
       │    ├── ifstream (文件输入流)
       │    └── istringstream (字符串输入流)
       └── ostream (输出流)
            ├── ofstream (文件输出流)
            └── ostringstream (字符串输出流)
       istream + ostream -> iostream (输入/输出流)
            ├── fstream (文件输入/输出流)
            └── stringstream (字符串输入/输出流)

C++的标准设备抽象

C++定义了以下标准输入输出设备的抽象:

  • std::cin 标准输入流;
  • std::cout 标准输出流;
  • std::cerr 标准错误流;
  • std::clog 标准日志流。

标准模板库 STL

标准模板库提供适用于绝大多数类型及其简单组合的一套数据结构、操作方法和堆内存管理方案。

Info

若是需要实例化对象时并置入容器,应使用 emplace() 替代后续标准库模板当中的 push(),前者具备更好的性能;

WARNING

进行元素访问操作时,必须检查元素是否存在,尤其是使用了堆内存的对象;标准模板库不提供元素访问检查以确保性能最大化。

迭代器

以下标准模板库的数据结构类型几乎都支持以下迭代器:

  • begin() Vector 数组起始位置迭代器;
  • end() Vector 数组末端之后的迭代器;
  • rbegin() Vector 数组末端位置的反向迭代器;
  • rend() Vector 数组起始位置之前的反向迭代器。

Info

对迭代器解引用可获取到对应元素的值或对象,可进一步访问对象的成员或函数;

在以上迭代器标识符前加上 c 就是迭代器的常量版本。不能通过常量迭代器修改值。

迭代范例:

vector<int> a_vector = {1, 5, 3, 8, 9};
for (auto iterator = a_vector.begin(); iterator != a_vector.end(); iterator++) {
	cout << *iterator << " ";
}
 
for (auto element : a_vector) {
	cout << element << " ";
}

vector

需要导入 vector

Vector 是 C++ 标准模板库中的动态数组容器,支持随机访问,即支持下标访问。

支持的声明或初始化方式

std::vector<Wildcard> vec1;
// Example
std::vector<string> vec2("Minecraft", "Mojang Studio");
std::vector<int> vec3 = {12, 67, 46, 79};
std::vector<int> vec4(vec3);
std::vector<int> vec5 = vector<int>(vec4.begin(), vec4.end());

成员函数

容量管理

  • unsigned long? size() 获取当前元素数量;
  • unsigned long? capacity() 获取总元素容量值;
  • bool empty() 检查是否为空;
  • resize(unsigned long?) 调整总元素容量值;
  • reserve(unsigned long?) 为目标保留一些元素容量;

元素访问与变更

  • Wildcard& at(unsigned long?) 通过索引值访问,会检查边界,越界将抛出异常;
  • Wildcard& front() 访问第1个元素;
  • Wildcard& back() 访问最后1个元素;
  • vector* data() 获取 Vector 的数据数组指针,可通过下标或偏移量解引用以访问值。
  • push_back(Wildcard) / emplace_back(Wildcard...) 追加1个元素;
  • pop_back() 移除末端元素;
  • insert(iterator, Wildcard) 前插1个元素;
  • erase(iterator) 移除指定位置的元素; erase(iterator, iterator) 移除指定范围的元素;
  • clear() 清空 Vector;
  • swap(vector&)*(仅函数行为)*交换两个 Vector 的数组部分;

list

需要导入 list

List 是 C++ 标准模板库中的双向链表容器,不支持随机访问。

可以用和 vector 相同的方式初始化 list;

如果从需求上只需单向链表,可导入 forward_list

成员函数

容量管理

  • unsigned long? size() 获取当前元素数量;
  • unsigned long? capacity() 获取总元素容量值;
  • bool empty() 检查是否为空;
  • resize(unsigned long?) 调整总元素容量值;

元素访问与变更

  • front() 访问第1个元素;
  • back() 访问最后1个元素;
  • push_front(Wildcard) / emplace_front(Wildcard...) 在开头添加元素;
  • push_back(Wildcard) / emplace_back(Wildcard...) 在末尾添加元素;
  • pop_front() 移除开头元素;
  • pop_back() 移除末尾元素;
  • insert(iterator, Wildcard) 在指定位置插入元素;
  • erase(iterator) 删除指定位置的元素;
  • clear() 清空所有元素;
  • swap(list&)*(仅函数行为)*交换两个 list 的链表部分;
  • merge(list&) 合并两个已排序的 list;
  • splice(iterator, list&) 将另一个 list 的元素移动到当前list;
  • remove(Wildcard) 删除所有指定值的元素;
  • remove_if(condition) 删除满足条件的元素;
  • unique() 删除连续的重复元素;
  • sort() 对元素进行升序排序;
  • reverse() 反转元素顺序。

stack

需要导入 stack

Stack 是 C++ 标准模板库中的栈容器,不支持随机访问,只能加入元素到栈头,从栈头移除元素;只能访问栈头元素的引用。

成员函数

  • top() 访问栈头元素;
  • push(Wildcard) / emplace(Wildcard...) 在栈头添加元素;
  • pop() 从栈头移除元素;
  • size() 获取栈元素数量;
  • empty() 检查栈是否空;
  • swap() 交换两个栈。

queue

需要导入 queue

Queue 是 C++ 标准模板库中的队列容器,不支持随机访问,只能加入元素到队尾,从队头移除元素;只能访问队头和队尾元素的引用。

成员函数

  • front() 访问队头元素;
  • back() 访问队尾元素;
  • push(Wildcard) / emplace(Wildcard...) 在队尾添加元素;
  • pop() 移除队头元素;
  • size() 获取队列元素数量;
  • empty() 检查队列是否空;
  • swap() 交换两个队列。

set

需要导入 set

Set 是 C++ 标准模板库中的集合类型,存储的任何一个值都是唯一的。它会在元素变更时自动排序。

若无需排序,可使用 unordered_set;如果希望键可重复,可使用 multiset

成员函数

  • insert(Wildcard) 加入一个元素;
  • emplace(Wildcard...) 加入至少一个元素;
  • erase(Wildcard | iterator) 删除一个元素,返回删除元素数量;
  • find(Wildcard) 查找指定元素,返回迭代器;未找到时返回 end() 迭代器;
  • count(Wildcard) 查找指定元素,返回数量;
  • merge() 将传入的集合元素移动并合并到当前集合。对于 set,当前集合是 set 且已存在同名键时,不移动;
  • size() 获取集合元素数量;
  • empty() 检查集合是否空;
  • clear() 清空集合。

map

需要导入 map

Map 是 C++ 标准模板库中的键值对集合类型,存储的键名都是唯一的。这和 Python 的字典很相似。map 需要指定两个类型以完成声明。它会在元素变更时自动对键排序。

若无需排序,可使用 unordered_map;如果希望键可重复,可使用 multimap

Info

支持通过下标访问目标键存储的值,如果在下标给出的键不存在,会自动创建并初始化值类型;也可通过 at() 访问目标键,不存在时会抛出异常而不会新建。

成员函数

  • insert({Wildcard, Wildcard}) 加入一个新的键值对;可用初始化列表或 std::make_pair() 初始化;
  • emplace(Wildcard, Wildcard) 加入一个新的键值对;
  • erase(Wildcard | iterator) 删除一个元素,返回删除元素数量;
  • find(Wildcard) 查找指定元素,返回迭代器;未找到时返回 end() 迭代器;
  • count(Wildcard) 查找指定元素,返回数量;
  • size() 获取键值集合元素数量;
  • empty() 检查键值集合是否空;
  • clear() 清空键值集合。

deque

需要导入 deque

Deque 是 C++ 标准模板库中的双端队列类型,支持随机访问。

成员函数

  • front() 访问队头元素;
  • back() 访问队尾元素;
  • push_front(Wildcard) 在队头添加元素;
  • push_back(Wildcard) 在队尾添加元素;
  • pop_front() 移除队头元素;
  • pop_back() 移除队尾元素;
  • insert(iterator) 在迭代器前加入元素;
  • erase(iterator) 删除迭代器处元素;
  • size() 获取键双端队列元素数量;
  • empty() 检查双端队列是否空;
  • clear() 清空双端队列。

初始化列表

自 C++11 起,C++ 可使用 {} 来表示初始化对象所需的一系列值,这被称为初始化列表;

初始化列表统一了基本数据类型、实例化自定类和数组的初始化方式;使容器类型能以多个元素同时初始化;解决隐式调用构造函数时被误解为函数声明的问题;同时阻止窄化转换:

std::string phrase();  // 🟠调用无参数构造函数,但容易使人理解为函数声明
int value = {3.5};     // ❌初始化列表阻止窄化转换 double -> int
char letter = {257};   // ❌阻止窄化转换 int -> char

以下是最常见的初始化列表方式,这种结构能用于初始化几乎一切 C++ 对象:

std::string phrase {"Nitro"};  // 直接列表初始化
std::string phrase_2 = {"Streetlight"}  // 复制列表初始化

初始化列表的类型是 std::initializer_list<>,代码内的初始化列表会由编译器自动实例化为此类型的对象,且在构造时首先寻找接受 std::initializer_list 的构造函数。

初始化列表支持 for-each (Range-based for loop) 遍历型循环,可以这样编写这个构造函数:

CLASS(std::initializer_list<int> init_list) {
	for (const auto& item : init_list) {}
}

匿名函数和 lambda

有时候在某些需要使用函数但仅需使用一次的场合下,再封装一个函数就会显得较为冗余(例如为一个事件触发器设置带参数的操作过程),C++为此提供了匿名函数(又称 lambda),它使函数参数表和函数操作过程能够以参数形式直接内联到另一个参数之内。

匿名函数的基本格式如下:

[CAPTURE] {
    // Something to do...
};
 
// 以下多出的项目均为可选
[CAPTURE](Parameter) mutable noexcept -> RETURN_TYPE {
	// Something to do...
};

要使匿名函数在声明时调用,在后方添加 ()

其中:

  • [CAPTURE] 以方括号括起的外部对象列表,在此列表的且在作用域的对象可以被此匿名函数访问; 支持的形式:

    • (空) 匿名函数不需要访问外部对象;

    • 对象名 直接指定可被匿名函数以只读形式访问的对象;

    • = 在作用域内的外部对象可被匿名函数以只读形式访问;

    • & 在作用域内的外部对象可被匿名函数访问并修改;

    • (>=C++14) VAR = EXTERNAL 用外部对象初始化一个对象供匿名函数使用。

    Info

    通过捕获列表传入的对象默认是具备 const 常量声明的,要使其在函数内可被修改,需要添加 mutable 声明。要使其修改外部对象,而不是修改一个副本,需要为对象添加 & 引用标记。

    需要区分匿名函数的捕获列表参数列表

    • 捕获列表用于:设置外部对象在匿名函数内的可见性及可修改性(在对象前额外添加 & 使对象直接被引用并且可以修改,或为匿名函数添加 mutable 声明使其仅在函数内部被修改);
    • 参数列表用于:与一般函数一样,向匿名函数传递参数,行为与一般函数的实际参数一致。

    以下是可能的示例:

    // 最简化匿名函数,要使匿名函数在函数外部能被调用,需在末端添加()
    [] {
        cout << "Good!" << endl;
    }();
     
    // 在匿名函数中调用函数
    inline void speak() { cout << "surprise!" << endl; }
    void (*ptr)() = speak;
    [ptr] {
        ptr();
    }();
  • (Parameter) 匿名函数的参数列表,(<C++20) 当后续函数声明未设置时,此处可省略;(>=C++20) 此处始终可省略;

匿名函数也能初始化一个函数指针对象,并将其作为函数使用。实际上是将此对象作为了函数指针。

以下示例仅限于C++17及以上,必须带有 const 声明,因为硬编码的基本类型值可能被传入。

auto print = [](const auto& message) {
    cout << message;
};
 
print("Hello");          // 🟡 Hard coded value
print(45 + '\n');        // 🟠 Hard coded value, output: 55

类型转换

WARNING

类型转换只能用于将数据传递到新对象时改变数据的解读方式(只能用于右值),不能用于改变原对象的类型(不能用于左值)。

隐式类型转换

与大多数程序设计语言一样,C++支持人类可理解的隐式类型转换,转换的目标是运算表达式中出现的最大类型,但将结果赋值到对象时根据对象的类型进行转换。

转换的一般规则:

  1. bool → char → short → int → long → long long
  2. float → double → long double

隐式类型转换不可用于更大类型转更小类型的初始化列表中,必须将其显式类型转换;

double average = 36.87;
int result = {average};     // ❌ Cannot do this

隐式类型转换适用于:

  1. 纯数类型和字符类型的转换;

  2. 将函数标识符转换为函数指针(函数标识符转换为纯数 long 类型);

  3. 将派生类地址转换为基类指针(基类指针指向派生类对象);

  4. 类中有参数列表符合要求的构造函数,通过初始化列表完成实例化对象;

    // Example
    class EXAMPLE {
    	int value;
        int remain;
    public:
    	EXAMPLE (int value, int remain = 5) /* : this->value(value) */ {
    		this->value = value;
    	}
        operator int() {
            return value;
        }
    };
     
    // Usage example
    EXAMPLE example = {3, 9};   // ✅ No problem

显式类型转换/强制类型转换

在语句中用圆括号或类型转换标签直接指定类型,在数据不变的情况下改变其解读方式(但浮点型转整型会截断小数部分),这就是显式(强制)类型转换。

使用圆括号并指定类型的方式称为C风格强制类型转换;C++提供这些额外的方式,在尖括号中指定类型,在圆括号中指定对象:

  • 静态强制类型转换 static_cast<>()

    是否能够完成转换将在编译时确定,原类型与转换类型必须具有相关性。例如 double 类型不能通过强制类型转换变为 string 类型; 可将派生类对象转换为基类对象,派生类的成员随后将无法再访问;

  • 动态强制类型转换 dynamic_cast<>() 此方法适用于具备多态特性的继承类族,将在程序运行时进行转换检查,若转换失败将返回 nullptr,若含引用将抛出 std::bad_cast 异常。

  • 常量与松散声明移除转换 const_cast<>() 此方法能够移除原类型含有 constvolatile 关键字的对象以解除编译器静态检查限制,能向无 const 标记的函数的形参传递实参,或者向无 const 标记的对象赋值有 const 标记的对象,用于适配接口参数没有 const 的情况。但是,仍需确保接口函数不会修改其数据,特别是分配于只读的代码区的数据

    const int value = 5;
    const int& value_2 = (int&) value;       // C style type transforming
    cout << value_2 << endl;
    int& value_3 = const_cast<int&>(value);  // Use const_cast and remove it
    value_3 = 3;                             // ⛔ You can do this, but avoid!
    cout << value_2 << endl;
  • 强制重新解释类型转换 reinterpret_cast<>() 此方法能够将目标对象直接按照一个类型的解读方式进行解读,而不进行任何转换检查。若目标程序需要跨平台,因编译器规范可能不同和内存的存储方式可能不同,应该避免使用此方法。

explicit 声明

为避免可能存在的不当的隐式类型转换造成数据处理异常,C++提供 explicit 显式函数关键字,被声明为 explicit 的函数不能被隐式调用,只能显式调用。

例如上方的初始化列表实例化对象的方式,若构造函数添加 explicit,将只能通过以下方式实例化:

explicit EXAMPLE::EXAMPLE (int value, int remain = 5);
 
EXAMPLE example_1 = EXAMPLE(3, 9);   // ✅ No problem
EXAMPLE example_2(3, 9);             // ✅ No problem
EXAMPLE example_3 = {3, 9};          // ❌ Cannot do this

智能指针

智能指针是C++标准库提供的,能够支持在合适位置自动释放堆内存的指针。合理使用智能指针能够解决手动管理内存可能造成的内存泄漏问题。

使用智能指针需要导入 memory

IMPORTANT

std::auto_ptr() 已在 C++17 标准中移除。若使用 C++17 及更新标准,应使用专用指针创建函数,请查看示例。

C++ 提供以下智能指针:

  • std::unique_ptr 独享型堆内存对象,只能有1个 unique_ptr 可指向同一个堆内存对象;只能通过右值引用方式移动;通过 delete 可以删除堆内存对象; 若此指针离开作用域,此堆内存对象会自动释放。

    要将独占智能指针连接到堆内存对象,可使用以下方式:

    // "Wildcard" is a typename, "n" is an instantiated object from the type
    // Create method 1  🟠Removed in C++17
    std::unique_ptr<Wildcard> auto_ptr(new Wildcard(n));
    // Create method 2  💡Recommend ONLY in C++14 and after
    auto auto_ptr = std::make_unique<Wildcard>(n);
     
    // Move
    std::unique_ptr<Wildcard> auto_ptr2 = std::move(auto_ptr);
  • std::shared_ptr 共享型堆内存对象,多个 shared_ptr 可指向同一个堆内存对象;有计数器追踪一个堆内存对象; 有新的共享指针指向此堆内存时,计数器自增;有共享指针离开其作用域时,计数器自减;若减至0,此堆内存对象会自动释放。

    要将共享智能指针连接到堆内存对象,可使用以下方式:

    // "Wildcard" is a typename, "n" is an instantiated object from the type
    // Create method 1  🟠Removed in C++17
    std::shared_ptr<Wildcard> auto_ptr(new Wildcard(n));
    // Create method 2  💡Recommend
    auto auto_ptr = std::make_shared<Wildcard>(n);
  • std::weak_ptr

    共享型堆内存对象,但不会增加计数,只能从 shared_ptr 初始化,访问对象需要转换为 shared_ptr

    要将共享智能指针连接到堆内存对象,可使用以下方式:

    // "Wildcard" is a typename, "n" is an instantiated object from the type
    // Create shared pointer first
    auto shared = std::make_shared<Wildcard>(n);
     
    // Linking weak pointer to shared pointer
    std::weak_ptr<Wildcard> auto_weak_ptr = shared;

常用函数

  • 将指针移动(weak_ptr 不适用)

    ANY_POINTER pointer = std::move(pointer);
  • [成员] 释放所有权,返回普通指针(仅 unique_ptr

    release()
  • [成员] 释放对象堆内存(重置指针为空)。如果提供一个普通指针,原堆内存会被释放,并将普通指针转为智能指针自动管理

    reset(__p = pointer())
  • [成员] 获取引用计数(仅 shared_ptr

    use_count()
  • [成员] 检查堆内存对象是否已被释放

    lock()      // Return true if heap object exists
    expired()   // Return false if heap object exists

异常处理

为使程序遇到各种意外状况时能够统一规范地处理,C++引入了异常处理。异常可理解为一种特殊的返回值,它能被捕获并转入处理。异常可以是任何类型。

返回的异常会逐级由 try-catch 代码块检测,类型不匹配时不会捕获并向上层 try-catch 传递,若无 try-catch 代码块捕获此异常,将调用 std::terminate() 捕获异常并结束程序。

要捕获并处理异常,需要建立以 try 和紧随其后的多个 catch 命名的代码块:

try {
	// Check exception
}
catch (EXCEPTION_TYPE) {  // Recommend adding reference symbol for catch block
    // Process target exception
}
...
catch (...) {
    // Process all exception
}

若在 try 中发生的异常在后续 catch 被捕获,运行此 catch 语句的代码后将直接转入 catch 之后的代码继续运行,而不是回到 try 中发生异常之后的位置。

若希望捕获所有异常,阻止程序因异常终止,可以在 try-catch 末端加入 catch(...) 块。

未处理异常

异常被抛出后将沿着函数调用顺序逐渐下传,交给低层可能的 try-catch 块捕获,若最终未被 main() 捕获,程序随后将直接调用 std::terminate() 结束。

若异常未进入 try,视编译器决定是否回滚栈(GCC 会设置回滚,回滚到 main() 处;含 -fno-exceptions 选项的 Clang 不设置回滚);若异常进入 try 但未被 catch 捕获,程序会回滚栈到最近 try 层。

回滚时,被回滚层的局部变量会调用析构函数以释放。

回滚完成后,调用 std::terminate() 结束程序。

C++ 标准异常类

导入 exception 可以使用 C++ 标准库提供的异常处理类,以下是位于标准库 std 中的异常类,它们全部继承自 exception

std::exception
    ├── std::bad_alloc                // 内存分配失败(new)
    ├── std::bad_cast                 // 动态转换失败(dynamic_cast)
    ├── std::bad_typeid               // typeid操作符应用于空指针
    ├── std::bad_exception            // 意外异常
    ├── std::logic_error              // 逻辑错误
    │   ├── std::domain_error         // 参数不在有效范围内
    │   ├── std::invalid_argument     // 无效参数
    │   ├── std::length_error         // 超出最大允许大小
    │   └── std::out_of_range         // 超出有效范围
    └── std::runtime_error            // 运行时错误
        ├── std::overflow_error       // 算术上溢
        ├── std::underflow_error      // 算术下溢
        ├── std::range_error          // 超出有效范围
        └── std::system_error         // 系统相关错误

手动编写异常类

继承标准库当中的异常类可以自定义异常的类型,需要重写 what()。继承哪个异常类取决于要实现异常的类型在逻辑上的位置。

(>=C++11) noexcept 声明

函数操作中确实不会返回异常的可以在函数头后部添加 noexcept 明确此函数不抛出异常。但声明 noexcept 的函数当中若抛出了异常且此函数未处理,此程序仍会结束。此声明由人为保证,应确保函数确实不会抛出异常时使用。

C++ 历史和 C++ 标准的更替

早期 C++

  • 1979 年,含类功能的 C 被实现 加入特性:类、成员函数、继承类、分离编译、访问控制、朋友功能、实参类型检查、形参默认值、内联函数、重载赋值运算符、构造与析构函数、空实参表调用和带 void 实参表调用等效、函数的调用与返回
  • 1985 年,Cfront 1.0 发布 加入特性:虚函数、函数与运算符重载、引用、newdelete 堆内存分配运算符、常量关键字、作用域解析运算符
  • 1985 年,C++ 第1个版本发布
  • 1989 年,Cfront 2.0 发布 加入特性:多重继承、成员指针、protected 访问控制、类型安全链接、抽象类、静态与 const 修饰成员函数、指定类的堆内存分配和释放
  • 1990 年,Annotated C++ 参考手册发布,指导 C++ 发展的标准化,直到 ISO 标准开始出现 加入特性:名称空间、异常处理、嵌套类、模板(aka. 泛型)
  • 1991 年,C++ 第2个版本发布

标准 C++

Info

此节内容来自于 C++ 参考手册网站 https://www.en.cppreference.com/;

可访问 https://www.en.cppreference.com/w/cpp/language/history.html 查看详细的 C++ 标准历史更替;

有些更新标准中的内容也可以用于以旧标准编译的代码;因为新版本编译器允许这么做,但是可能会生成警告。

  • 1990 年,美国国家标准学会 C++ 委员会成立
  • 1991 年,国际标准化组织 C++ 委员会成立

C++98

在 1998 年发布的第1个 C++ 标准 (ISO/IEC 14882:1998)。同年发布 C++ 第3个版本。

  • 加入特性: 运行时类型识别(动态类型转换)、typeid()、类型转换运算符(operator <type constructor>())、mutablebool、条件判断内定义、模板实例化、成员模板
  • 新增 C++ 库: auto_ptr, iostream, complex_numbers etc.
  • 标准模板库基本内容: 容器、算法、迭代器、函数对象

C++11

在 2011 年发布的 C++ 标准 (ISO/IEC 14882:2011),是一个主要版本。

(部分内容已删去)

  • 匿名函数(aka. lambda 表达式);

  • 右值引用、移动构造函数和移动赋值运算符;

  • 范围型(遍历型)循环(for(typename an_item_in_list : list_array(traversable)));

  • 大括号与等号初始化器、初始化列表(std::initialize_list 类型,默认初始化为0,阻止窄向转换 (narrowing-conversion),减少和函数调用混淆的可能,统一不同类型的初始化方式);

  • 指针专用字面值 nullptr

  • 使用 64 位的超长整型 long long

  • [C++ Lib] 自动管理堆内存的智能指针;

  • 类型获取运算符 decltype(),初始化对象时自动类型判断 auto

  • 可为成员函数设置 defaultdelete 注记;

  • 禁用继承(对于类)与禁用重写(对于虚函数)限定符 final

  • 明确重写限定符 override

  • 新增函数声明方式:尾随型返回值(auto function() -> <return_type>);

  • 作用域限定枚举(enum struct|class NAME {}),借助类型关键字,将枚举值当作成员常量处理,要将其赋值给整型变量时需要进行显式强制转换;

  • 编译时常量表达式关键字 constexpr,使一些仅由常量组成的表达式或使用这些常量的非虚函数在编译时就计算它们的值,在 C++11,只允许函数体有一条返回语句;在 C++11 之后,允许创建局部变量、设置条件判断和多条返回语句;

  • 字面类型;包括基本数据类型、由它们构成的数组,以及符合条件的类:

    • 非静态成员变量都是字面类型;
    • [C++20 前] 有1个 constexpr 普通构造函数;
    • 若是继承类,父类必须也是字面类型;
    • 使用默认析构函数。
  • 委托构造函数(默认值通过一个构造函数给出,这个构造函数实际上是带常量去调用拥有更多形参的一个重载版本);继承构造函数(它也可以看作是一种委托构造函数);

  • 分别支持 UTF-16 和 UTF-32 编码字符存储的 char16_tchar32_t 类型;

  • 使用 using 创建类型别名(过去使用 typedef <original> <alias>;,现在可用 using <alias> = <original>;,对于函数指针,可用 using <alias> = <return> (*)(<argument_list>));

  • 类型别名模板,使得可以为模板(泛型)类型创建别名,而不影响模板功能;以下是创建一个图的别名例子:

    template<typename key_type, typename value_type>
    using CustomMap = std::map<key_type, value_type, hash_map<key_type>>;
     
    // Usage example
    CustomMap<string, int> sheet = {};
  • 可变参数模板(模板可用 ... 标识传入实参数量可变,例如 template <typename Wildcard, typename ... [name]>);以下示例中,函数将所有传入参数尝试连接到一个字符串上。

    template<typename Wildcard, typename ...Wildcard_rest>
    std::string connector(Wildcard first_arg, Wildcard_rest ...rest_args) {
        if (sizeof...(rest_args) != 0) {
            return std::string(first_arg) + connector(rest_args...);
        }
        else {
            return std::string(first_arg);
        }
    }
  • 多种字符串字面量类型

    • 普通型 "This is a string",受文件编码影响;
    • 纯字符串 R"(This is a raw string)",不处理转义字符;
    • 宽字符串 L"This is a wide string",编码为 wchar_t,Windows / Linux 占 2 / 4 字节每字符;
    • UTF-8 字符串 u8"This is an utf-8 string",编码为 char (C++11 ~ C++17);
    • UTF-16 字符串 u"This is an utf-16 string",编码为 char16_t
    • UTF-32 字符串 U"This is an uft-32 string",编码为 char32_t

    纯字符串可以组合其他编码字符串使用。

    Info

    若纯字符串中含有 )" 时,需要添加额外的字符区分字符串边界:

    char* str_1 = R"(An important "(TASK)")";         // ❌
    char* str_2 = R"123(An important "(TASK)")321";   // ✅

    这些额外的字符置于两侧边界圆括号外,它们以中心对称形式排列;不能使用空格。

  • 自定义字面值(aka. 硬编码值)后缀,通过预先定义后缀使得字面值的类型确定且易读;只能用于字面值,不能用于变量;

    Info

    定义后缀需要通过实现操作符函数完成:

    <return_type> operator"" <suffix_name>(argument);
    

    可接受的参数:

    • 整型:unsigned long long | unsigned long long int
    • 浮点型:long double
    • 字符型:char | wchar_t | char16_t | char32_tchar8_t(自 C++20 起)
    • 字符串型:const char*, std::size_t (这里有2个参数,字符类型可从以上4个任选)

    后缀定义运算符函数也是严格类型检查,只能接受给出的这些类型组合。

    基本用法示例

    unsigned int operator"" _u32(unsigned long long int value) {
        return value;
    }
     
    string operator"" _kmph(const char* str, size_t length) {
        return std::string(str) + "km/h";
    }
     
    // Usage
    unsigned int value = 233_u32;
    auto speed = "233"_kmph;
  • 使用两层中括号以设置属性 [[Attribute]]

  • noexcept 限定符和 noexcept() 运算符;noexcept() 接受一个表达式,根据表达式类型检查所有涉及的函数调用是否设置 noexcept;如果全部声明,返回 true

  • 多线程内存模型;

  • 垃圾回收接口;(没有编译器实装,C++23 移除)

  • 类型字节对齐检查运算符 alignof(typename/object),类型声明时字节对齐设置运算符 alignas(int)

  • 静态断言 static_assert(),在 C++17 前的函数结构为:static_assert(bool_expression, err_msg),自从 C++17 新增了一个重载版本:static_assert(bool_expression)

  • [C++ Lib] 新增许多算法,例如 std::shuffle()std::is_sorted()std::move()std::move_backward()

  • [C++ Lib] 新增库:分数型表示 ratio、高精度时间 chrono

  • [C++ Lib] 返回迭代器的函数:std::begin()std::end()std::next()std::prev()

  • [C++ Lib] 在线程间传递异常:std::exception_ptrstd::error_codestd::error_condition

C++ 14

在 2014 年发布的 C++ 标准。

  • 变量模板,用于定义常量并按需要设置为不同类型;

    template<typename Wildcard>
    Wildcard ConstantVal = 3.5;
     
    // Usage
    ConstantVal<int>;
  • 模板(泛型) lambda,使用 auto 作为参数类型,从而使匿名函数能接受任意类型;

  • 初始化 lambda 捕获,允许在 [] 中初始化变量;最重要的用处是移动捕获;

    auto ptr = std::make_unique<int>();
    auto lambda_function = [inner_ptr = std::move(ptr)] {};
  • 减少 constexpr 标注的函数限制,现在允许在 constexpr 函数内部使用条件判断语句和多条返回语句;

  • 自动推断返回类型,允许在函数返回值声明处使用 auto;内部任何返回语句必须能够被统一为相同类型;

  • 二进制字面值,允许用 0b 开头表示一个二进制整型;

  • 整型字面值分隔符,允许用 ' 单引号分隔以提高易读性;

  • 允许初始化时混用聚合体和默认成员初始化器,详见 [对象的初始化方式 (6)](# 对象的初始化方式);

  • [C++ Lib] 创建独占型智能指针函数 std::make_unique()

  • [C++ Lib] 互斥锁 std::shared_timed_mutex和读写锁 std::shared_lock

  • [C++ Lib] 静态编译工具 std::integer_sequence

  • [C++ Lib] std::exchange(new, original)new 赋值至 original,然后返回 original;适用于移动对象;

  • [C++ Lib] std::quoted() 用于输出时添加引号,用于输入时去除最外层一对引号。

C++17

在 2017 年发布的 C++ 标准,是一个主要版本。

(部分内容已删去)

  • 像 Python 那样允许多变量接受返回值,这个过程被称为解包 (decompose);以下是一个简单实例:

    std::pair<std::string, int> ret(const std::map<std::string, int>& inner_map, int position) {
        return {inner_map.begin()->first, inner_map.begin()->second};
    }
     
    auto [key, value] = ret(json_simulator, 0);
  • 允许在条件判断中初始化一个变量;然后进行条件判断;

    if (int status = tcp_connect("localhost", 12333); status != -1)
  • 现在允许对类内静态成员变量标注内联 inline,从而允许直接对它们初始化而无需另写定义语句;

    // Before C++17
    class Test {
        static int value = 5;	// ❌ Always get error
    };
     
    class Test {
        static int value;
    };
    int Test::value = 5;        // 🟡 Static member variable should
    							//    be defined and be assigned separately!
     
    // C++17 and after
    class Test {
        static inline int value = 5;  // ✅
    };
  • 在可变模板函数上,进一步支持对二元运算的简化表达。前例请见 C++11 中的可变模板;

    template<typename ...Wildcard>
    std::string str_connector(Wildcard ...parameters) {
        return (std::string(parameters) + ...);
    }
     
    // Usage
    std::string after_connection = str_connector("Forza", "Horizon", " ", "5");
    std::cout << after_connection << std::endl;

    WARNING

    此方法应该尽可能只用在简单对象间的操作与运算。

  • 现在能为 if 设置编译时检查常量表达式 constexpr,使其像 #ifdef#ifndef 那样由编译器根据编译代码内容决定应该生成哪个(哪些)分支的 if 语句;如果多个调用者分别满足不同条件,那么分别编译;

  • auto 现在能作为非类型的模板参数;

    template<auto value>
    constexpr auto ConstantValue = value;
     
    constexpr auto var = ConstantValue<16>;
  • [[nodiscard]] 属性:若调用处未接受返回值,发出编译警告;

  • [[maybe_unused]] 属性:停止对一个未使用对象的警告;

  • [C++ Lib] std::optional 返回一个“可能存在”值;使用此类型可以无需建立特殊的错误值;

  • [C++ Lib] std::variant C++ 特别改造的联合体类型;

  • [C++ Lib] std::any C++ 特别改造的无类型指针 void*;要转换为某个类型,使用 std::any_cast<>()

  • [C++ Lib] std::filesystem C++ 独立于任何平台的文件操作方法;

  • [C++ Lib] 许多标准库算法可用 std::execution::par 以启用多核工作流处理;

  • [C++ Lib] 字节数据类型:std::byte

C++20

// TODO

Commonly Found Question

内存泄漏

若通过堆内存分配的空间未被程序释放,这就是内存泄漏。

内存泄漏可发生自:

  • 程序结束前,未进行堆内存释放(未使用 free()delete 运算符);

    (这包括:在已分配堆内存之后,因异常未捕获造成程序直接终止,未运行堆内存释放语句)。

  • 程序运行期间,一个已分配的堆内存段已经没有任何指针指向它,地址值彻底丢失;

  • 在存储了堆内存指针的数据结构中,仅这个数据结构的空间被释放,而未释放堆内存;

  • (此情况较少,一般为刻意操作)两个共享指针相互引用。

Info

尽管现代操作系统在进程结束时会清理分配给进程的所有内存,但是仍不应依赖此功能而不考虑内存分配问题。若对于长期运行的进程来说,内存泄漏最终会使此进程因超过操作系统的内存分配限制而终止。

可通过下述方法解决潜在内存泄漏:

  • 检查代码的内存分配,特别是位于条件判断中的内存分配语句,关注 IDE 静态检查工具的警告信息;
  • [仅C++] 使用智能指针;
  • 对于类内深层变量较难把握是否释放的情况,可用 Valgrind (Linux) 进行内存问题检查。

在函数以外分配堆内存

虽然可以这么做,但不要在函数以外分配堆内存(假设声明在头文件且被外部导入),这可能会导致:

  • 不确定的初始化时机,它可能被提前使用(例如另一个也在函数外的变量使用了它);
  • 能在任何函数内回收它的内存,在某处回收后不能保证后续函数不使用它;
  • 各种其他繁杂问题…