########################################
基础概念
########################################
一些比较好的网站:
- `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% 了解和掌控自己写的代码。