######################################## 基础概念 ######################################## 一些比较好的网站: - `GotW `_ - `Dr.Dobbs `_ - `Andrzej's C++ blog `_ - `CppCoreGuidelines `_ 还有一些待查看的知识: - `stdcall `_ 变量的大小 **************************************** +-----------+------+------+ | 变量 | 环境 | 大小 | +===========+======+======+ | 指针 | 32 | 4 | +-----------+------+------+ | char | 32 | 1 | +-----------+------+------+ | short | 32 | 2 | +-----------+------+------+ | int | 32 | 4 | +-----------+------+------+ | float | 32 | 4 | +-----------+------+------+ | double | 32 | 8 | +-----------+------+------+ | long | 32 | 4 | +-----------+------+------+ | long long | 32 | 8 | +-----------+------+------+ 32 位和 64 位唯一的区别就是指针和 long 变成了 8。两者默认都是 4 字节对齐。 32 位环境中 float 的有效位是 7 位,double 的有效位是 16 位 另外就是 0x7fff_ffff 代表了 int32 的最大值,0x8000_0000 代表了 in32 的最小值。0xffff_ffff 实际上是 -1 枚举 **************************************** 通过 ``enum`` 关键字可以定义枚举类型。枚举类型可以声明变量,进行赋值: .. code-block:: cpp enum Shirt { Small = 1, Medium = 2, Large = 3, Xlarge = 5 }; Shirt shirt = Shirt::Small; Shirt shirt = Shirt(2) 枚举值的取值范围 ======================================== 假设枚举类型中的最大值为Max,最小值为Min。枚举类型的上限为RightLimit,下限为LeftLimit,则: RightLimit = :math:`2^k-1` , k为使 RightLimit > Max 的最小值 LeftLimit = :math:`- 2^k+1` , k为使 LeftLimit < Min 的最大值。当 Min ≥ 0 时,LeftLimit = 0 强类型枚举 ======================================== :abbr:`强类型枚举 (Scope Enum)` 可以通过 ``enum class`` 声明。其与普通枚举不同之处在于: - 强类型枚举无法被隐式转换为整数 - 强类型枚举无法与整数比较 - 不同强类型枚举的枚举值无法进行比较 - 使用强类型枚举可以指定枚举的底层储存方式。 例如: .. code-block:: cpp enum class Egg : unsigned { Small, Medium, Large, Jumbo } .. hint:: - 强类型枚举也可以通过 ``enum struct`` 的方式声明 - 强类型枚举的底层储存方式默认为 int .. seealso:: - `C++11强类型枚举——枚举类 `_ 字面量 **************************************** 自定义字面量 [1]_ ======================================== 通过 ``operator ""X`` 可以自定义字面量,X 是你的字面量后缀,**必须** 以下划线开头。 *operator ""* 可以有两种重载形式: +----------------------------------------------------+------------+ | 形式 | 说明 | +====================================================+============+ | operator ""X(unsigned long long) | 匹配实数 | +----------------------------------------------------+------------+ | operator ""X(const char\*, unsigned long long len) | 匹配字符串 | +----------------------------------------------------+------------+ 重载时有以下内容需要注意: - 重载时的 *X* 的名字必须以下划线开头。 - 重载的第一个参数必须为 ``unsigned long long`` , ``unsigned long double`` , ``long double`` , ``char`` , ``const char*`` 之一。第二个参数必须为 ``unsigned long long``。 - 当第一个参数为 ``const char*`` 时,使用第二种重载形式,否则选择第一种重载形式。 - 字面量在匹配 ``operator ""`` 时不会发生任何形式的隐式类型转换。 .. code-block:: cpp int operator ""_G(long double b){ // 匹配 10.2_G return 10; }; int operator ""_G(unsigned long long b){ // 匹配 10_G return 20; } int operator ""_G(const char* s, unsigned long long len){ // 匹配 "10"_G return 30; } .. caution:: u8 前缀所代表的 UTF-8 字符串最初在 C++11 中被引入,但是其所代表的确切类型 char8_t 直到 C++17 才出现,但是由于实现不理想, u8 在 C++20 中已经不被鼓励使用。所有 u8 相关的操作在 C++20 中被遭到禁用。 - `char8_t backward compatibility remediation `_ - `char8_t: A type for UTF-8 characters and strings `_ .. [1] N4860 P28 内存和对象 **************************************** 对象模型 ======================================== C++ 程序中包含了对对象的创建、销毁、引用、访问以及操作。对象的创建有三种方式: 1. 通过类型定义创建一个对象 2. 通过 new 表达式创建对象 3. 通过隐式创建一个对象(即当改变 union 的 :abbr:`激活成员 (Active Member)` 或创建一个临时对象时 ) 一个对象需要在创建后需要占用内存空间、需要有名字、有生命周期、有类型,甚至有多态,根据实现的不同,C++ 得以在运行时或得对象的类型。 如果一个对象被包含在其他对象中,那么就叫它 :abbr:`子对象 (Subobject)` ,子对象可以是 **成员子对象** 、 **基类子对象** 、 **数组元素** 。如果一个对象不是任何其他对象的子对象,那么就叫它 :abbr:`完全对象 (Complete Object)` 。 Lambda 表达式 **************************************** Lambda 表达式创建了一个 闭包_ :一种匿名函数对象 - Lambda 表达式是一个右值 - Lambda 是一个可调用对象 lambda 表达式的完整语法定义如下: .. code-block:: [捕获列表](参数列表) mutable(可选) 异常 -> 返回类型{ // 函数体 } 捕获列表有三种方式: ======== ========================== 捕获方式 效果 ======== ========================== var 按值捕获对象 = 按值捕获作用于内的所有对象 & 参数按引用捕获 this 类中所有变量对 lambda 可见 ======== ========================== 直接使用符号的话代表捕获当前作用域内的所有对象,否则代表捕获一个变量,多个变量以都好分开: .. code-block:: cpp int a = 10; auto l1 = [&](){}; //按引用捕获当前作用域内所有可见对象 auto l2 = [&l1, &a]; // 按引用捕获 l1 和 a Lambda 的返回类型可以省略,在 C++14 中可以使用 auto 自动推断返回类型: .. code-block:: cpp auto add = [](auto x, auto y) -> decltype(x+y){ return x+y; }; Lambda 作为函数参数 ======================================== Lambda 作为函数参数引入时有三种方式: #. 使用模板参数 .. code-block:: cpp template void show(T obj){ obj(); } int main(){ show([](){ cout<<"hello, world"; }); return 0; } #. 使用退化的Lambda .. code-block:: cpp using foo = void (int) ; void fun(foo f){ f(1); } int main() { auto s = [](int a){ std::cout< void show(std::function obj){ obj(); } int main(){ show([](){ cout<<"hello, world"; }); return 0; } .. important:: 在使用 Lambda 捕获局部变量时一定要注意变量的生命周期,尤其是在 Qt 的 connect 中,一旦 Lambda 访问了已经析构的变量,就可能导致程序在未抛出任何有效信息的情况下直接崩溃 .. seealso:: - `《深入理解C++11》笔记-lambda函数 `_ - `深入理解c++中的Lambda表达式 `_ - `C++11如何使用lambda函数模板做参数 `_ - `Lambda函数 `_ - `C++11 之 lambda函数的详细使用 `_ - `C++模板元编程---lambda表达式简单实现 `_ - `C++ 使用lambda表达式作为函数参数 `_ .. _闭包: https://en.cppreference.com/w/cpp/language/lambda 列表初始化 **************************************** 让我们来看一种比较特殊的语法: .. code-block:: cpp std::string func(){ return {}; } 上述代码意味着 func() 返回一个使用默认构造函数构造的 *std::string* 。通过在大括号内填充不同的参数,可以调用不同的构造函数。 [#list_initialization]_ .. [#list_initialization] `列表初始化 `_ auto **************************************** auto 是 C++ 11 引入的关键字,主要用于省去冗余的类型声明。 - auto 不能推断 cv 限定 - auto 无法推断引用类型。因此使用 *auto&* 才能创建左值引用,否则会导致对象拷贝 - 原地初始化时无法使用 auto .. note:: 某次给 lua 写模块时,由于使用 auto 导致了第二次调用模块函数时的 core dump。改成 auto& 解决了问题 函数 **************************************** 在函数传递参数的时候需要注意实参的顺序,实参是从右向左一次入栈的,因此对于下列代码: [#now1]_ .. code-block:: c int f(int a, int b, int c){ return 0; } int main(){ return f(printf("a"),printf("b"),printf("c")); } 的执行结果为:: cba .. [#now1] `牛客网题一 `_ 指针 **************************************** 成员函数指针: - 通过对指定成员函数指针可以调用基类函数。例如: .. code-block:: cpp struct A{ virtual int func(){ return 10; } }; struct B: public A{ int func() override{ return 20; } }; int main() { auto p = new B; cout<A::func(); return 0; } 程序运行的结果为:: 10 另外是指针和数组的区别 - 以 *int \** 方式声明的为指针,sizeof 的结果在 32 位系统下为 4,在 64 位系统下为 8 - 以 *int []* 方式声明的为数组,sizeof 的结果为数组的大小(变量的大小乘以数组的尺寸) - 另外,如果函数的参数为 int [],那么它返回的尺寸是指针的大小 - 将数组类型赋值到指针上,就会丢失数组的信息 - 二维数组的两个下标 int [r][c] 分别代表行和列 二维数组的创建方式为 new int\* [],传递方式为 int\*\* 或者: .. code-block:: cpp void func(int a[4][4]); 行数和列数不能省略 多维数组的最高维只用来判断参数类型,因此 ``int array[5][6]==int array[][6]`` ,但是其他维度必须写明大小。并且多维数组在传递后再使用for···auto会报错。 开关位 **************************************** 通过按位与和按位或我们可以实现将变量特定的位打开,例如: .. code-block:: cpp unsigned a = 0xaabaa; auto b = (a & 0xff0ff) | 0x00a00; // b == 0xaaaaa 可见,要关闭位可以使用 0xffxxff 的形式,要打开位可以使用 0x00x00 的形式。要批量设置某一个范围的内存,需要先将内存清零再设置 指针 **************************************** 指针的声明有以几种方式: +-------------+--------------+--------------------+ | 声明方式 | 含义 | 储存的内容 | +=============+==============+====================+ | int \*p | p 是指针 | int 变量的地址 | +-------------+--------------+--------------------+ | int \*\*p | p 是二级指针 | 指针的地址 | +-------------+--------------+--------------------+ | int p[] | p 是数组 | int 数字 | +-------------+--------------+--------------------+ | int (\*p)[] | p 是数组指针 | 指向一个数组的地址 | +-------------+--------------+--------------------+ | int \*p[] | p 是指针数组 | 数组 | +-------------+--------------+--------------------+ | int p[4][4] | p 是二维数组 | 包含四个元素的数组 | +-------------+--------------+--------------------+ 另一种是串数组: .. code-block:: cpp #include int main(){ static char *s[] = {"black", "white", "pink", "violet"}; char **ptr[] = {s+3, s+2, s+1, s}, ***p; p = ptr; ++p; printf("%s", **p+1); return 0; } s + 1 会调到字符串数组的第一个索引的位置 因此数组 ptr 指向的三个储存的三个串分别是 {"pink", "white","black", "black"} 将 ptr 赋值给 p 后,p 就丢失了数组信息,这是对其执行 +1 操作只是对字符操作罢了。因此 ++p 指向的是 ink\\0 。打印截止到 \\0 为止 野指针是指没有初始化的指针。悬空指针是指值为 nullptr 的指针 sizeof **************************************** sizeof 对类型得到的是类型的大小 sizeof 对数组得到的是数组的大小 sizeof 对指针得到的是指针的大小 当数组被传递到函数后,就丢失了它的尺寸信息 const **************************************** - const 变量必须在声明时进行初始化 - const* 是导致指针的内容无法更改 - \*const 会导致指针的指向无法更改 更一般的,C++ 还引入了 constexpr 用来在编译器对表达式/函数求值 volatile **************************************** `C/C++ 中 volatile 关键字详解 | 菜鸟教程 `_ 大小端 **************************************** 两个十六进制数一共是八位 [#大小端]_ 大小端是指数据在内存中储存的形式,寄存器中总是大端的 大端就是书面上常用的形式,小端就是将字节顺序逆序排列。地址从左向右是升高的 大端小端没有谁优谁劣,各自优势便是对方劣势: - 小端模式 :强制转换数据不需要调整字节内容,1、2、4字节的存储方式一样。 - 大端模式 :符号位的判定固定为第一个字节,容易判断正负。 一般计算机是小端序的,而通信是大端序的 .. [#大小端] https://blog.csdn.net/qq_29350001/article/details/54428265 移位运算 **************************************** 移位运算是在寄存器中的运算,与大小端无关。向左移位总是相当于乘以二的次幂,向右移位总是相当于除以二的次幂 内联函数 **************************************** - 内联函数必须放到头文件中,编译器必须能看到内敛函数的定义 - inline 函数应该简洁,如果语句较多,不适合定义为内联函数 - inline 函数中,一般不建议有循环、if 或 switch 语句,否则,函数定义时即使有 inline 关键字,编译器也可能会把该函数作为非内联函数处理。 - inline 函数要在函数被调用之前声明。 内存对齐 **************************************** 使用 ``__attribute__((packed))`` 和 ``__attribute__((aligned(0)))`` 可以防止编译器对结构体进行对齐 预编译指令 **************************************** .. code-block:: cpp #define S(x) #x #define X(x) x #define v abc int main() { cout<*c;// 堆成员使用成员指针 } 函数 ======================================== 函数也是有类型的,其类型由函数返回值和函数参数唯一确定,下面以vs2017(c++11)编译结果为基准: .. code-block:: cpp void f(int){} // 该函数的类型为: void(int) void (*fa)(int) =f; if (typeid(*fa) == typeid(void(int))) cout << setw(20) << left << "fa的类型:" << typeid(fa).name() << endl << setw(20) << left << "void(int)的类型:" << typeid(void(int)).name() << endl; cout << setw(20) << left << "f的类型" << typeid(f).name(); 输出结果为: ================= ==================== fa的类型: void (__cdecl*)(int) void(int)的类型: void __cdecl(int) f的类型 void __cdecl(int) ================= ==================== 可见: **函数名就代表了函数类型,同时也代表了函数的地址** ,它的值与对应函数指针的值相同,但是取地址的值不同: .. code-block:: cpp cout << setw(10) << left << "f的值:" << f << endl << setw(10) << left << "fa的值:" << fa << endl << setw(10) << left << "&f的值:" << &f << endl << setw(10) << left << "&fa的值:" << &fa << endl; 输出结果为: ========= ================ f的值: 00007FF67F781442 fa的值: 00007FF67F781442 &f的值: 00007FF67F781442 &fa的值: 000000064C10F9E8 ========= ================ 零成本抽象 **************************************** .. admonition:: 注 整理自 `c++的zero overhead abstraction是什么? `_ 所谓“零成本抽象”有两个层面的意思: - 不需要为没有使用到的语言特性付出代价。 - 使用某种语言特性,不会带来运行时的代价。 总的来说,这就是一种极度强调运行时性能,把所有解释抽象的工作都放在编译时完成的思路。 用对象内存布局举例,对于一个类,如果没有定义任何虚函数,也没有继承任何定义了虚函数的类,那么这个类的对象在内存中的布局与 C 语言的 struct 基本就是一致的,没有多余的虚表。 - 你没有用到虚函数带来的运行时多态特性,就不需要付出虚表带来的运行时开销。 - 你用到了“用类来抽象数据”这个特性,它没有带来任何额外的运行时开销,跟你分别单独操纵类的成员是一样的效率。 “零成本抽象”是一种语言特性的标准,C++ 有的特性是零成本的,但并不是所有的特性都是零成本的,比如刚才提到的虚表。 也不是说只要是“零成本抽象”就是好的语言特性,有些语言特性必须拿到运行时的信息(比如动态分派),有些语言特性做在运行时比编译时更好(比如真・泛型),还有的语言特性带来的运行时开销小到可以忽略(比如已经比较成熟了的异常机制),就不需要做成“零成本抽象”,把所有工夫都赶在编译时完成。 自己写码如何做到零代价抽象? C++设计者中告诉我们,C++ 中只有 2 个语言特性是不遵守零开销原则的,运行时类型识别(RTTI)和异常,所以实现零开销抽象的必要条件是不使用这两个特性。能否最终实现零开销,还是需要 100% 了解和掌控自己写的代码。