C/C++
{Back to Index}
Table of Contents
1 运行
1.1 Tool Chain
Tool Chain 是三种元素的集合,按顺序执行,最终将代码转换成可执行程序:
- 预处理器
- 编译器
- 链接器
1.1.1 预处理
在 GCC 中,可以通过下面的命令生成.i文件:
gcc -E demo.c -o demo.i -nostdinc -P
-E 表示只进行预编译, -nostdinc will bypass standard #include files 。
1.1.2 编译
可以使用下面的命令生成 .s 文件
gcc -S demo.c -o demo.s
1.1.3 汇编
汇编的结果是产生目标文件,在 GCC 下的后缀为.o
1.1.4 链接
目标文件已经是二进制文件,与可执行文件的组织形式类似,只是有些函数和全局变量的地址还未找到,程序不能执行。链接的作用就是找到这些目标地址,将所有的目标文件组织成一个可以执行的二进制文件。
1.2 动态链接库 1
生成动态链接库是直接使用gcc命令并且需要添加-fPIC(-fpic) 以及-shared 参数
- fPIC 或 -fpic 参数的作用是使得 gcc 生成的代码是与位置无关的,也就是使用相对位置。
- shared参数的作用是告诉编译器生成一个动态链接库。
2 类型
2.1 整数类型
Figure 3: Integer Types, Sizes, and Format Specifiers
2.1.1 size_t
通常用 unsigned long long
实作,并用 %zd (十进制) 或 %zx(十六进制) 作为格式描述符。
2.2 枚举类型
enum class Race { Dinan, Teklan, Ivyn, Moiran, Camite, Julian, Aidan }; Race langobard_race = Race::Aidan;
2.3 POD
尽量在 POD 中以 从大到小 排列成员。
2.4 初始化
2.4.1 基本类型初始化为零
int a = 0; int b{}; int c = {}; int d = { 0 }; int e; // 不要使用这种方式
2.4.2 基本类型初始化为任意值
int e = 42; int f{ 42 }; int g = { 42 }; int h( 42 );
2.4.3 数组初始化
int array_1[]{ 1, 2, 3 }; // array of 3 integers int array_2[5]{}; // array of 5 integers, all elements are initialized to 0 int array_3[5]{ 1, 2, 3 }; // array of 5 integers, first 3 elements are 1, 2, 3, the rest are 0 int array_4[5]; // array of 5 uninitialized integers (dangerous!)
2.4.4 POD 初始化
#include <cstdint> struct Pod { uint64_t a; char b[256]; bool c; }; int main() { Pod pod1{}; // all fields are zeroed Pod pod2 = {}; // all fields are zeroed Pod pod3{ 42, "hello" }; // a = 42, b = "hello", c = 0 Pod pod4{ 42, "hello", true }; // a = 42, b = "hello", c = true Pod pod5 = { 42, "hello", true }; // same as above Pod pod6 = { .a = 42, .b = "hello", .c = true }; // same as above }
2.4.5 类初始化
struct Taxonomist { Taxonomist() { printf("(no argument)\n"); } Taxonomist(int x) { printf("int: %d\n", x); } Taxonomist(char x) { printf("char: %c\n", x); } Taxonomist(float x) { printf("float: %f\n", x); } }; int main() { Taxonomist t1; // (no argument) Taxonomist t2{ 'c' }; // char: c Taxonomist t3{ 65537 }; // int: 65537 Taxonomist t4 { 6.02e23f }; // float: 602000000000000000000000 Taxonomist t5('g'); // char: g Taxonomist t6 = { 'l' }; // char: l Taxonomist t7{}; // (no argument) Taxonomist t8(); // WRONG, 这是个函数声明,这就是要引入大括号初始化语法的一个主要原因,另一个主要原因是 Narrowing conversion }
2.4.6 类成员初始化
不能使用小括号来初始化成员变量。
struct JohanVanDerSmut { bool gold = true; // equal sign int year_of_smelting_accident { 1970 }; // curly braces char key_location[8] = { "x-rated" }; // equal sign with curly braces };
2.4.7 Narrowing Conversion
每当发生 Implicit Narrowing Conversions 时,大括号初始化会产生告警, 这可以帮助减少 bug 。
float a{ 1 }; float b{ 2 }; int narrowed(a / b); // narrowing conversion printf("narrowed: %d\n", narrowed); int result { a / b }; // Compiler generated warning printf("result: %d\n", result);
3 对象
3.1 拷贝语义
3.1.1 拷贝构造
DerivedClass(const DerivedClass& other) : BaseClass(other) { // Copy constructor code }
3.1.2 拷贝赋值
DerivedClass& operator=(const DerivedClass& other) { if (this != &other) { // Copy the base class part BaseClass::operator=(other); // Copy the derived class part derivedMember = other.derivedMember; } return *this; }
拷贝赋值和拷贝构造的主要区别在于,在拷贝赋值中, 旧值可能已经有了一个值,必须在赋新值前,清理旧值的资源。
3.2 移动语义
3.2.1 移动构造
DerivedClass(DerivedClass&& other) noexcept : BaseClass(std::move(other)) { // move other's resources to this }
移动构造和拷贝构造类似 ,区别在于前者采用了右值引用而非左值引用。如果不加上 noexcept
,编译期会退而使用拷贝构造函数。
3.2.2 移动赋值
DerivedClass& operator=(DerivedClass&& other) noexcept { if (this == &other) { return *this; } BaseClass::operator=(std::move(other)); // move other's members to this return *this; }
除了自引用检查和旧值清理逻辑之外,移动赋值的逻辑和移动构造的逻辑是相同的。
3.3 编译器生成的方法
有五种方法与对象的内存控制相关:
- 析构函数
- 拷贝构造
- 移动构造
- 拷贝赋值
- 移动赋值
- 如果什么方法都不定制,编译器为这五个方法自动生成默认实现
- 如果定义了析构函数,拷贝构造,或拷贝复制这三个方法的任何一个,就会得到所有这三个函数的实现
- 如果只定义移动语义相关的方法,编译器会自动提供默认析构函数
Figure 4: A chart illustrating which methods the compiler generates when given various inputs
4 多态
4.1 接口 (运行时多态)
4.2 Template (编译期多态)
4.2.1 类型转换函数
4.2.1.1 const_cast
const int& const_val = 10; auto& val = const_cast<int&>(const_val); val = 20; log_info("const_val: %d, val: %d", const_val, val); // const_val: 20, val: 20
也可以使用 const_cast
从对象中去除 volatile
修饰符。
4.2.1.2 static_cast
short s = 600; void *target = &s; auto as_short = static_cast<short *>(target); log_info("as_short: %d", *as_short); // 600 float f = 3.14; auto as_int = static_cast<int *>(&f); // static_cast from 'float *' to 'int *' is not allowed
4.2.1.3 reinterpret_cast
通常用于 cast 两个完全不搭界的类型,使用者必须自行负责转换的正确性。
auto timer = reinterpret_cast<const uint64_t*>(0x7e00); printf("Time: %llu\n", *timer); // Segment fault
4.2.2 非类型模版参数
template <size_t Index, typename T, size_t Length> T& get(T (&arr)[Length]) { static_assert(Index < Length, "Index out of bounds"); return arr[Index]; } int arr[] = {1, 2, 3, 4, 5}; std::cout << get<2>(arr) << std::endl; // 3
5 语句
5.1 命名空间
5.1.1 using
using
会 ① 将符号导入块中,② 如果在命名空间中使用 using
,则会将符号导入当前命名空间。
namespace BroopKidron13::Shaltanc { enum class Color { Mauve, Pink, Russet } } int main() { using namespace BroopKidron13::Shaltanc::Color; const auto shaltanac_grass = Color::Russet; if (shaltanac_grass == Color::Russet) { printf("The grass is russet\n"); } }
5.1.2 constexpr if
constexpr if
主要用途是根据类型参数的某些属性在函数模版中提供自定义行为。 在运行期间,constexpr if 语句消失。
这个语句可以取代预处理器宏。
#include <cstdio> #include <stdexcept> #include <type_traits> template <typename T> auto value_of(T x) { if constexpr(std::is_pointer<T>::value) { if(!x) throw std::runtime_error{ "Null pointer dereference." }; return *x; } else { return x; } } int main() { unsigned long level{ 8998 }; auto level_ptr = &level; auto& level_ref = level; printf("Power level = %lu\n", value_of(level_ptr)); ++*level_ptr; printf("Power level = %lu\n", value_of(level_ref)); ++level_ref; printf("It's over %lu!\n", value_of(level++)); try { level_ptr = nullptr; value_of(level_ptr); } catch(const std::exception& e) { printf("Exception: %s\n", e.what()); } }
6 函数
6.1 volatile
不能在 volatile 对象上调用非 volatile 方法。
6.2 可调用对象
6.2.1 函数指针
static void _print(int age, std::string name) { log_info("age=%d, name=%s", age, name.c_str()); } static void testCallableFuncPtr() { log_info("=== %s ===", __func__); using FuncPtr = void (*)(int, std::string); FuncPtr funcPtr = _print; funcPtr(30, "Paul"); }
6.2.2 具有 operator() 成员函数的类对象
static void testCallableFunctor() { log_info("=== %s ===", __func__); struct Functor { void operator()(int age, std::string name) { log_info("age=%d, name=%s", age, name.c_str()); } }; Functor functor; functor(30, "Paul"); // 使用 std::function 包装可调用对象 std::function<void(int, std::string)> func = functor; func(26, "Ryan"); }
6.2.3 可转换为函数指针的类对象
static void testCallableClassFuncPtr() { log_info("=== %s ===", __func__); using FuncPtr = void (*)(int, std::string); struct MyClass { operator FuncPtr() { return _print; } static void _print(int age, std::string name) { log_info("[MyClass::_print] age=%d, name=%s", age, name.c_str()); } }; MyClass myClass; myClass(30, "Paul"); std::function<void(int, std::string)> func = myClass; // 使用 std::function 包装可调用对象 func(26, "Ryan"); }
6.2.4 类成员函数指针或类成员变量指针
这种类型的可调用对象,需要借助 binder
才能转换成 std::function
static void testCallableClassMemberFuncPtr() { log_info("=== %s ===", __func__); struct MyClass { void print(int age, std::string name) { log_info("[%p::print] age=%d, name=%s", this, age, name.c_str()); } }; using FuncPtr = void (MyClass::*)(int, std::string); MyClass myObj; log_info("myObj=%p", &myObj); FuncPtr funcPtr = &MyClass::print; (myObj.*funcPtr)(30, "Paul"); // 使用 std::function 包装可调用对象 std::function<void(int, std::string)> func = std::bind(funcPtr, &myObj, std::placeholders::_1, std::placeholders::_2); func(26, "Ryan"); }
static void testCallableClassMemberPtr() { log_info("=== %s ===", __func__); struct MyClass { MyClass() : value(10) {} int value; }; using Ptr = int MyClass::*; MyClass myObj; log_info("myObj=%p", &myObj); Ptr ptr = &MyClass::value; log_info("myObj.*ptr=%d", myObj.*ptr); myObj.*Ptr(&MyClass::value) = 20; // way 1 log_info("myObj.*ptr=%d", myObj.*ptr); myObj.*ptr = 30; // way 2 log_info("myObj.*ptr=%d", myObj.*ptr); // 使用 std::function 包装可调用对象 std::function<int(MyClass&)> func = std::bind(ptr, std::placeholders::_1); log_info("func(myObj)=%d", func(myObj)); }
6.3 binder
std::bind
用来将可调用对象与其参数一起进行绑定。绑定后的结果可以使用 std::function
进行保存,
并延迟到需要的时候调用。主要有两大作用:
- 将可调用对象与其参数一起绑定成一个 仿函数
- 将多元(参数个数为n,n>1)可调用对象转换为一元或者(n-1)元可调用对象,即绑定部分参数
绑定器函数使用语法格式如下:
// 绑定非类成员函数/变量 auto f = std::bind(可调用对象地址, 绑定的参数/占位符); // 绑定类成员函/变量 auto f = std::bind(类函数地址/成员地址(其实是偏移量), 类实例对象地址, 绑定的参数/占位符);
7 智能指针
Figure 5: Smart Pointers in stdlib and Boost
8 迭代器
Figure 6: Iterator categories and their nested relationships
9 Memory Order 2 , 3 , 4
Memory Order 控制了执行结果在多核中的可见顺序,这个可见顺序与代码序不一定一致:
- 原因一是 汇编指令优化重排
- 原因二是 CPU 实际执行时乱序执行 以及 部分 CPU 架构上没有做到内存强一致性 (内存强一致性:执行结果出现的顺序应该和指令顺序一样,不存在重排乱序) ,导致后面的代码执行完成的时候,前面的代码修改的内存却还没改变
在可能出现问题的场景下,需要手动干预以避免问题,汇编(软件)和 CPU(硬件) 都提供了相应的指令取进行干预控制,C++ 的 atomic中的 Memory Order 可以看成是这些控制的 封装 ,隐藏了底层,之所以有六种是因为这种控制是有代价,从松散到严格开销越来越高,在某些场景下,是允许部分重排的,只是对于小部分重排会导致问题的才需要加以控制,那么只需要衉一些低开销的控制即可。
Memory Order 的作用是:
- 干预汇编重排
- 干预硬件乱序执行
- 控制执行结果在多核间的可见性
可以说,Memory Order 是用来限制编译器以及 CPU 对单线程当中的指令执行顺序进行重排的 程度 (此外还包括对 cache 的控制方法)。
这种限制,决定了以 atomic 操作为基准点(边界),对其之前后的内存访问命令,能够在多大的范围内自由重排, 也被称为栅栏 。
六种模型参数本质上是限制单线程内部的指令重排顺序, 并不是同步不同线程之间的指令顺序 。
通过不同的参数选择,来控制带有模型参数的变量前后的指令被重排顺序的程度。
9.1 硬件内存模型
随着 CPU 的不断发展,CPU 的计算能力远超过从主存 (DRAM) 中读写速度。为了提升数据读写速度,慢慢的加入了 Cache, Store Buffer, Invalidate Queue 等硬件,并允许 CPU 的乱序执行:
实际的 CPU 在运行指令过程中并非表现得一条执行完了才执行下一条指令,比如一个 MOV 指令会导致 CPU 的 Load Unit 忙而 ALU (逻辑计算单元) 空闲,因此在等取值的同时,预先做下一个能够做的计算指令,这样的乱序执行提升了CPU 的使用率。
Store Buffer, Invalidate Queue 的出现带来了不同的内存一致性模型,从而导致执行结果在多核下的可见顺序不同,所以在编程时考虑这点,以下总结了常见的四种内存一致性模型以及它们对执行结果的在多核中可见顺序的影响。
9.1.1 顺序存储模型(SC: Sequential Consistency)
Figure 7: 顺序存储模型
多核 cache 间使用 MESI 协议进行数据同步,不存在数据一致性问题,因此在这种模型下,多线程程序的运行与所期望的执行情况是一致的, 不会出现内存访问乱序的情况。
顺序存储模型是最简单的存储模型,也称为 强定序模型 。CPU 会按照代码来执行所有的 load 与 store 动作,即按照它们在程序的顺序流中出现的次序来执行。从主存储器和 CPU 的角度来看,load 和 store 是顺序地对主存储器进行访问。
可以把顺序存储模型想象成单核系统。
9.1.2 存储定序模型
9.1.2.1 完全存储定序(TSO: Total Store Order)【引发 store-load 乱序】
Figure 8: Total Store Order (TSO)
为了提高 CPU 的性能,芯片设计人员在 CPU 中包含了一个存储缓存区(store buffer),它的作用是为 store 指令提供缓冲,使得CPU不用等待存储器的响应。所以对于写而言,只要 store buffer 里还有空间,写就只需要1个时钟周期,所以 store buffer 的存在可以很好的减少写开销。
Store Buffer 必须严格按照 FIFO 的次序将数据发送到主存(所谓的FIFO表示先进入store buffer的指令数据必须先于后面的指令数据写到存储器中),CPU 必须要严格保证 store buffer 的顺序执行,这种内存模型就叫做完全存储定序(TSO)。x86 CPU 就是这种内存模型。
相比于以前的内存模型而言,store 的时候数据会先被放到store buffer里面,然后再被写到L1 cache里。 因此这引入了访问乱序的根源 :因为 store 操作会放入 SB ,而 load 操作直接被 CPU 执行,产生了两条执行路径,因这种原因产生的乱序称之为 store-load 乱序 。
9.1.2.2 部分存储定序(PSO: Part Store Order)【引发 store-store 乱序】
TSO 在有 store buffer 的情况下已经带来了不小的性能提升,但是芯片设计人员并不满足于这一点,于是在 TSO 模型的基础上继续放宽内存访问限制:允许 CPU 以非 FIFO 来处理 store buffer 缓冲区中的指令。 CPU 只需保证地址相关指令在store buffer中才会以FIFO的形式进行处理 ,即对 同一个相同的地址做store,才会有严格执行顺序制约 ,而其他的则可以乱序处理,这被称为部分存储定序(PSO) ,产生的乱序称为 store-store乱序 。
9.1.3 宽松存储模型(RMO: Relax Memory Order) 【引入 load-load 和 load-store 乱序】
为了榨取更多的性能,在 PSO 的模型的基础上,更进一步的放宽了内存一致性模型,不仅允许 store-load,store-store 乱序,还进一步允许 load-load ,load-store 乱序, 只要是地址无关的指令 ,在读写访问的时候都可以打乱所有顺序,这就是宽松内存模型(RMO)。
这是一种乱序随可见的内存一致性模型,ARM 的很多微架构就是使用 RMO 模型。
9.1.4 干预工具
9.1.4.1 内存屏障(memory barrier)
内存屏障的最根本的作用就是提供一个机制,要求CPU在这个时候必须以顺序存储一致性模型的方式来处理load与store指令,这样才不会出现内存访问不一致的情况。
内存屏障可以细分为:
write memory barrier 【也称 sfence (sotre fence) ,解决 store-load/store-store 问题】
将 store buffer 中的数据全部刷进 cache 。刷到 cache 这部分的数据被更新了,就会触发 cache 的 mesi 进行同步,发送 cacheline 的 invalidate message 告知其它 cache 持有的数据失效了,赶紧标注一下然后同步,然后其他的 core 会返回 invalidate ack 之后这时才会继续向下执行。
read memory barrier 【也称 lfence (load fence) ,解决 load-store/load-load 问题】
flush Invalidate Queue
full memory barrier
read memory barrier + write memory barrier
9.1.4.2 Invalidate Message Queue (针对 TSO)
使用 memory barrier 方式需要在 cacheline 上发送 message,一来一回需要等待时间,这对于 CPU 设计者来说同样是不可接受的。因此又引入了Invalidate Queue 。
有了 Invalidate Queue 之后,发送的 Invalidate Message 只需要 push 到对应 core 的 Invalidate Queue 即可,然后这个core就会返回继续执行,中间不需要等待。这样cache之间的沟通就不会有很大的阻塞了。
在被通知的CPU上运行的线程其实也需要内存屏障(load fence) ,因为如果不及时处理 Invalidate Queue,那就仍然持有旧数据。
9.1.4.3 Lock 指令 【X86 平台】
Lock 不是一种内存屏障,但是它能完成类似内存屏障的功能。它会对CPU总线和高速缓存加锁,可以理解为CPU指令级的一种锁,实现了以下作用:
- 先对总线/缓存加锁,然后执行后面的指令。
- Lock 后的写操作会通过 cache 间的 MESI 协议让其他 CPU 相关的 cache line 失效。
- 最后释放锁后会把高速缓存中的脏数据全部刷新回主内存。
- 在锁住总线的时候,其他 CPU 的读写请求都会被阻塞,直到锁释放。
9.2 软件内存模型语义
9.2.1 memory_order_seq_cst
要求底层提供顺序一致性模型,如果程序的运行底层架构是非内存强一致模型,则使用cpu提供的内存屏障等操作保证强一致,同时要求代码进行编译的时候不能够做任何指令重排。
该模型可以解决一切问题。
9.2.2 memory_order_release/memory_order_acquire
允许cpu或者编译器做一定的指令乱序重排,但是由于TSO,PSO的存在,可能产生的store-load/store-store乱序从而导致问题。那么涉及到多核交互的时候,就需要手动使用release, acquire去避免这样的问题。
与memory_order_seq_cst最大的不同的是,它是对具体代码可能出现的乱序做具体解决而不是要求全部都不能重排。
- load(acquire) 之后的所有写操作(包含非依赖关系),不允许被移动到这个 load(acquire) 的前面,一定在 load 之后执行。
- store(release) 之前的所有读写操作(包含非依赖关系),不允许被重排到这个 store(release) 的后面,一定在 store 之前执行。
- 如果 store(release) 在 load(acquire) 之前执行了,那么 store(release) 之前的写操作对 load(acquire) 之后的读写操作可见。
- 绿色区域的代码依然可以允许编译器或 CPU 为了优化目的重排序,但不能超越屏障。
9.2.3 memory_order_relaxed
完全放开,让编译器和cpu自由搞,如果cpu是SC的话,cpu层不会出现乱序,但是编译层可能会做重排,结果也是无法保证的。
可用在代码上没有乱序要求的场景或者没有多核交互的情况下,以提升性能。
9.2.4 memory_order_acq_rel
- 前面无法被重排到后面,后面无法被重排到前面。
- 可以看见其他线程施加 release 之前的所有写入,同时自己之前所有写入对其他施加 acquire 语义的线程可见。
10 C++11
10.1 auto
10.1.1 限制
不能作为函数参数使用
int func(auto a, auto b) // error { cout << "a: " << a <<", b: " << b << endl; }
不能用于类的非静态成员变量的初始化
class Test { auto v1 = 0; // error static auto v2 = 0; // error,类的静态非常量成员不允许在类内部直接初始化 static const auto v3 = 10; // ok };
- 不能使用auto关键字定义数组
不能使用auto推导出模板参数
template <typename T> struct Test{} int func() { Test<double> t; Test<auto> t1 = t; // error, 无法推导出模板类型 return 0; }
10.1.2 常见应用
- STL的容器遍历
泛型编程
#include <iostream> #include <string> using namespace std; class T1 { public: static int get() { return 10; } }; class T2 { public: static string get() { return "hello, world"; } }; template <class A> void func(void) { auto val = A::get(); cout << "val: " << val << endl; } int main() { func<T1>(); func<T2>(); return 0; }
10.2 decltype
它的作用是在编译器编译的时候推导出一个表达式的类型。
10.2.1 推导普通变量或者普通表达式或者类表达式
#include <iostream> #include <string> using namespace std; class Test { public: string text; static const int value = 110; }; int main() { int x = 99; const int &y = x; decltype(x) a = x; // int decltype(y) b = x; // const int & decltype(Test::value) c = 0; // const int Test t; decltype(t.text) d = "hello, world"; // string cout << "a: " << typeid(a).name() << endl; cout << "b: " << typeid(b).name() << endl; cout << "c: " << typeid(c).name() << endl; cout << "d: " << typeid(d).name() << endl; return 0; }
10.2.2 推导函数返回值
class Test{...}; //函数声明 int func_int(); // 返回值为 int int& func_int_r(); // 返回值为 int& int&& func_int_rr(); // 返回值为 int&& const int func_cint(); // 返回值为 const int const int& func_cint_r(); // 返回值为 const int& const int&& func_cint_rr(); // 返回值为 const int&& const Test func_ctest(); // 返回值为 const Test //decltype类型推导 int n = 100; decltype(func_int()) a = 0; // int decltype(func_int_r()) b = n; // int& decltype(func_int_rr()) c = 0; // int&& decltype(func_cint()) d = 0; // int decltype(func_cint_r()) e = n; // const int & decltype(func_cint_rr()) f = 0; // const int && decltype(func_ctest()) g = Test(); // const Test
10.2.3 推导表达式类型的引用
表达式是一个左值,或者被括号( )包围,使用 decltype推导出的是表达式类型的引用。
#include <iostream> #include <vector> using namespace std; class Test { public: int num; }; int main() { const Test obj; decltype(obj.num) a = 0; // int decltype((obj.num)) b = a; // const int & int n = 0, m = 0; decltype(n + m) c = 0; // int decltype(n = n + m) d = n; // int & return 0; }
10.3 using= 【类型别名】
类型别名可以出现在任何作用域:块,类,或者命名空间。(作用和 typedef 相同)
该技法还可以为模版定义类型别名(这是 typedef 无法做到的),用于减少 模版参数 :
#include <cstdio> #include <stdexcept> template <typename To, typename From> struct NarrowCaster { To cast(From value) const { const auto converted = static_cast<To>(value); const auto backwards = static_cast<From>(converted); if(value != backwards) throw std::runtime_error{ "Narrowed!" }; return converted; } }; template <typename From> using short_caster = NarrowCaster<short, From>; int main() { try { const short_caster<int> caster; const auto cyclic_short = caster.cast(142857); printf("cyclic_short: %d\n", cyclic_short); } catch(const std::runtime_error& e) { printf("Exception: %s\n", e.what()); } }
10.4 constexpr
在使用中建议将 const 和 constexpr 的功能区分开,即凡是表达"只读"语义的场景都使用 const,表达"常量"语义的场景都使用 constexpr。
10.4.1 修饰普通函数/类成员函数
使用前提条件:
- 函数必须要有返回值,并且return 返回的表达式必须是常量表达式
- 函数在使用之前,必须有对应的定义语句,不能只看见声明
- 函数体中,不能出现非常量表达式之外的语句,但可以有 using 、typedef、static_assert 和 return
10.4.2 修饰模板函数
由于模板中类型的不确定性, 如果 constexpr 修饰的模板函数实例化结果不满足常量表达式函数的要求(主要看对应类型的参数是否是一个右值),则 constexpr 会被自动忽略,相当于一个普通函数。
10.4.3 修饰构造函数
常量构造函数有一个要求: 构造函数的函数体必须为空,并且必须采用初始化列表的方式为各个成员赋值。
10.5 委托构造/继承构造
委托构造函数允许使用同一个类中的一个构造函数调用其它的构造函数,从而简化相关变量的初始化:
#include <iostream> using namespace std; class Test { public: Test() {}; Test(int max) { this->m_max = max > 0 ? max : 100; } Test(int max, int min):Test(max) { this->m_min = min > 0 && min < max ? min : 1; } Test(int max, int min, int mid):Test(max, min) { this->m_middle = mid < max && mid > min ? mid : 50; } int m_min; int m_max; int m_middle; }; int main() { Test t(90, 30, 60); cout << "min: " << t.m_min << ", middle: " << t.m_middle << ", max: " << t.m_max << endl; return 0; }
继承构造函数的使用方法是这样的:通过使用using 类名::构造函数名(其实类名和构造函数名是一样的)来声明使用基类的构造函数,这样子类中就可以不定义相同的构造函数了,直接使用基类的构造函数来构造派生类对象:
#include <iostream> #include <string> using namespace std; class Base { public: Base(int i) :m_i(i) {} Base(int i, double j) :m_i(i), m_j(j) {} Base(int i, double j, string k) :m_i(i), m_j(j), m_k(k) {} void func(int i) { cout << "base class: i = " << i << endl; } void func(int i, string str) { cout << "base class: i = " << i << ", str = " << str << endl; } int m_i; double m_j; string m_k; }; class Child : public Base { public: using Base::Base; // 继承构造 using Base::func; // 如果在子类中隐藏了父类中的同名函数,也可以通过using的方式在子类中使用基类中的这些父类函数 void func() { cout << "child class: i'am luffy!!!" << endl; } }; int main() { Child c(250); c.func(); c.func(19); c.func(19, "luffy"); return 0; }