C/C++学习笔记

个人整理

C

1 基础

char的具体实现是什么❔

有些C编译器把char实现为有符号类型,即char可表示的范围是-128-127。有些C编译器把char实现为无符号类型,即char可表示的范围是0-255。

如不想由编译器决定,根据C90标准,C语言允许在关键字char前面使用signed或unsigned。

何时刷新缓冲区❔

当缓冲区满、遇到换行字符或需要输入的时候(从缓冲区把数据发送到屏幕或文件被称为刷新缓冲区)。

sizeof后面是否要加圆括号❔

运算对象是类型时,圆括号必不可少;但是对于特定量,可有可无。

sizeof返回值与实现有关,如何打印❔

为了可移植性,stddef.h头文件(在包含stdio.h头文件时已包含其中)把size_t定义成系统使用sizeof返回的类型,这被称为底层类型(underlying type),printf()可使用z修饰符打印相应的类型。C还定义了ptrdiff_t类型和t修饰符来表示系统使用的两个地址差值的底层有符号整数类型。

负数的整数除法的规则是什么❔

C99以前不同的实现采用不同的方法,C99规定使用趋零截断。如-3.8截断为-3。

为什么有的C代码用and代替&&

并非所有的键盘都有和美式键盘一样的符号。因此C99标准新增了可代替逻辑运算符的拼写,它们被定义在ios646.h头文件中。如果在程序中包含该头文件,便可用and代替&&or代替||

注意:在C++中,备选拼写是关键字;已弃用iso646.h或 C++ 等效的ciso646头文件。有andorand_eqor_eqxorxor_eqnotnot_eqbitandbitor

指针之间可以相互赋值吗❔

不可以,指针之间的赋值比数值类型之间的赋值要严格。不用类型转换就可以把 int 类型的值赋给double类型的变量,但是两个类型的指针不能这样做。

什么是复合字面量❔

C99新增了复合字面量(compound literal)。

1
2
3
int diva[2] = {10, 20};
int *pt1;
pt1 = (int [2]){10, 20}; // 复合字面量,[]内数字可省略

空白字符分隔的字符串字面量会自动串联吗❔

会。

数组名是指针常量吗❔

数组名和指针很像,并且不能进行赋值或自增,但它不是指针常量。

数组名的类型是type[size],不是type* const。对数组取值,得到的是type(*)[size]

数组名会隐式转换为首元素指针右值,除了以下情况:

  1. sizeof
  2. &
  3. 用字符串字面量初始化字符数组
  4. _Alignofalignof

什么是翻译单元❔

通常在源代码(.c扩展名)中包含一个或多个头文件(.h 扩展名)。头文件会依次包含其他头文件,所以会包含多个单独的物理文件。但是,C预处理实际上是用包含的头文件内容替换#include指令。所以源代码文件和所有的头文件都看成是一个包含信息的单独文件,这个文件被称为翻译单元(translation unit)。描述一个具有文件作用域的变量时,它的实际可见范围是整个翻译单元。如果程序由多个源代码文件组成,那么该程序也将由多个翻译单元组成。每个翻译单元均对应一个源代码文件和它所包含的文件。

C语言有哪些存储类别说明符❔

C语言有6个关键字作为存储类别说明符:auto、register、static、extern、_Thread_local和typedef。typedef关键字与任何内存存储无关,把它归于此类有一些语法上的原因。尤其是,在绝大多数情况下,不能在声明中使用多个存储类别说明符,所以这意味着不能使用多个存储类别说明符作为typedef的一部分。唯一例外的是_Thread_local,它可以和static或extern一起使用。

为什么可以在一条声明中多次使用同一个限定符❔

C99为类型限定符增加了一个新属性:幂等(idempotent)。

在文件间共享const变量有什么方法❔

可以采用两个策略。

  1. 遵循外部变量的常用规则,即在一个文件中使用定义式声明,在其他文件中使用引用式声明(用extern关键字)。

    注意,extern用于普通变量时,只需要在声明处加extern;用于const变量时,声明和定义处都需要加extern。

    extern (C++) | Microsoft Learn

  2. 把const变量放在一个头文件中,用关键字static声明,然后在其他文件中包含该头文件。相当于给每个文件提供了一个单独的数据副本。

volatile用法❔

volatile限定符告知计算机,代理(而不是变量所在的程序)可以改变该变量的值。例如:

  1. 一个地址上可能储存着当前的时钟时间,无论程序做什么,地址上的值都随时间的变化而改变。
  2. 一个地址用于接受另一台计算机传入的信息。

restrict用法❔

restrict关键字允许编译器优化某部分代码以更好地支持计算。它只能用于指针,表明该指针是访问数据对象的唯一且初始的方式。

* () []的优先级❔

  1. 数组名后面的[]和函数名后面的()具有相同的优先级。它们比*(解引用运算符)的优先级高。

    1
    int *risks[10]; // 是一个指针数组,不是指向数组的指针

  2. []()的优先级相同,都是从左往右结合。

    1
    2
    int (*rusks)[10]; // 是一个指向数组的指针,该数组内含10个int类型的元素
    int *foo[3][4]; // foo是一个内含3个元素的数组,其中每个元素是由4个指向int的指针组成的数组

函数指针和函数名可以互换使用吗❔

可以。

C可以使用泛型吗❔

可以。利用C11新增的泛型选择表达式定义一个泛型宏,根据参数类型选择最合适的数学函数版本。

1
#define SQRT(X) _Generic((X), long double: sqrtl,  default: sqrt, float: sqrtf)(X)

2 标准库

为什么不建议使用gets()

gets()唯一的参数是words,它无法检查数组是否装得下输入行,若输入的字符串过长,会导致缓冲区溢出(buffer overflow),可能引发段错误(Segmentation fault,表明程序试图访问未分配的内存)。有些人通过系统编程利用gets()插入和运行一些破坏系统安全的代码。

C11标准委员会直接从标准中废除了gets()函数。

如何进行字符串与数字的转换❔

atoi()atol()atof()函数把字符串形式的数字分别转换成int、long 和double类型的数字。strtol()strtoul()strtod()函数把字符串形式的数字分别转换成long、unsigned long和double类型的数字。

C语言是否允许变长数组❔

C99标准允许声明变长数组(variable-length array,简称 VLA),C11放弃了这一创新的举措,把VLA设定为可选,而不是语言必备的特性。

文本文件与二进制文件区别❔

所有文件的内容都以二进制形式(0或1)储存。

如果文件最初使用二进制编码的字符(例如, ASCII或Unicode)表示文本(就像C字符串那样),该文件就是文本文件,其中包含文本内容。

如果文件中的二进制值代表机器语言代码或数值数据(使用相同的内部表示,假设,用于long或double类型的值)或图片或音乐编码,该文件就是二进制文件,其中包含二进制内容。

C 提供两种访问文件的途径:二进制模式和文本模式。在二进制模式中,程序可以访问文件的每个字节。而在文本模式中,程序所见的内容和文件的实际内容不同。程序以文本模式读取文件时,把本地环境表示的行末尾或文件结尾映射为C模式。例如,C文本模式程序在MS-DOS平台读取文件时,把\r\n转换成\n;写入文件时,把\n转换成\r\n。在其他环境中编写的文本模式程序也会做类似的转换。

虽然C提供了二进制模式和文本模式,但是这两种模式的实现可以相同。前面提到过,因为UNIX使用一种文件格式,这两种模式对于UNIX实现而言完全相同。Linux也是如此。

3 内存管理

什么是存储类别❔

不同的存储类别(storage class)具有不同的存储期(storage duration)、作用域(scope)和链接(linkage)。

作用域

描述程序中可访问标识符的区域。

一个C变量的作用域可以是块作用域、函数作用域、函数原型作用域或文件作用域。

定义在块中的变量具有块作用域(block scope),块作用域变量的可见范围是从定义处到包含该定义的块的末尾。块是用一对花括号括起来的代码区域。例如,整个函数体是一个块,函数中的任意复合语句也是一个块。

1
2
3
for (int i = 0; i < 10; i++)
printf("A C99 feature: i = %d", i);
// C99把块的概念扩展到包括for循环、while循环、do while循环和if语句所控制的代码,即使这些代码没有用花括号括起来,也算是块的一部分。所以,上面for循环中的变量i被视为for循环块的一部分,它的作用域仅限于for循环。一旦程序离开for循环,就不能再访问i。

函数作用域(function scope)仅用于goto语句的标签。这意味着即使一个标签首次出现在函数的内层块中,它的作用域也延伸至整个函数。如果在两个块中使用相同的标签会很混乱,标签的函数作用域防止了这样的事情发生。

函数原型作用域(function prototype scope)用于函数原型中的形参名(变量名),它范围是从形参定义处到原型声明结束,这意味着而形参名(如果有的话)通常无关紧要(除了变长数组)。

变量的定义在函数的外面,具有文件作用域(file scope)。具有文件作用域的变量,从它的定义处到该定义所在文件的末尾均可见。

链接

C变量有 3 种链接属性:外部链接、内部链接或无链接。

具有块作用域、函数作用域或函数原型作用域的变量都是无链接变量。这意味着这些变量属于定义它们的块、函数或原型私有。具有文件作用域的变量可以是外部链接或内部链接。外部链接变量可以在多文件程序中使用,内部链接变量只能在一个翻译单元中使用。

一些程序员把内部链接的文件作用域简称为文件作用域,把外部链接的文件作用域简称为全局作用域程序作用域

存储期

作用域和链接描述了标识符的可见性,存储期则描述了通过这些标识符访问的对象的生存期。

C对象有4种存储期:静态存储期、线程存储期、自动存储期、动态分配存储期。

所有的文件作用域变量都具有静态存储期

对于文件作用域变量,关键字static表明了其链接属性,而非存储期。以static声明的文件作用域变量具有内部链接。

具有线程存储期的对象,从被声明时到线程结束一直存在。以关键字_Thread_local声明一个对象时,每个线程都获得该变量的私有备份。

块作用域的变量通常都具有自动存储期(当程序进入定义这些变量的块时,为这些变量分配内存;当退出这个块时,释放刚才为变量分配的内存。),也能具有静态存储期(加上关键字static)。

存储类别 存储期 作用域 链接 声明方式
自动 自动 块内
寄存器 自动 块内,使用关键字register
静态外部链接 静态 文件 外部 所有函数外
静态内部链接 静态 文件 内部 所有函数外,使用关键字static
静态无链接 静态 块内,使用关键字static

可声明为register的数据类型有限。例如,处理器中的寄存器可能没有足够大的空间来储存double类型的值。

如果一个源代码文件使用的外部变量定义在另一个源代码文件中,则必须用extern在该文件中声明该变量。

与自动变量不同的是,如果未初始化外部变量,它们会被自动初始化为0。这一原则也适用于外部定义的数组元素。与自动变量的情况不同,只能使用常量表达式初始化文件作用域变量。

除了以上存储类别,还可以使用malloc和free进行动态内存分配,即动态分配存储期

4 和C++区别

准确来说C++和C只有交集关系,C89之后诞生C++98和C99,然后是C++1x和C11,后续渐行渐远(C++不是C的超集)。

C++中有许多规则与C不同,这些不同使得C程序作为C++程序编译时可能以不同的方式运行或根本不能运行。

auto关键字用法❔

auto在C中是存储类别说明符(storage-class specifier),在C++中是类型说明符。

标识符处理❔

在C中,下面的代码不会产生冲突:

1
2
3
4
5
struct rect { 
double x;
double y;
};
int rect;

C++不允许这样做,因为它把标记名和变量名放在相同的名称空间中

声明一个有标记的结构、联合、枚举后,就可以在C++中使用这个标记作为类型名,而C必须带上struct、union、enum。

C++中使用嵌套结构时要使用::,C不需要。

void*

C++要求在把void*指针赋给任何类型的指针时必须进行强制类型转换。C没有这样的要求。

另外,C++可以把派生类对象的地址赋给基类指针。

函数原型❔

C可以没有函数原型,以下的代码在C中是正确的,在C++中是错误的。

1
2
3
4
5
6
int slice();
int main() {
slice(20, 50);
}
int slice(int a, int b) {
}

另外,C++允许重载。

char常量❔

C把char常量视为int类型,而C++将其视为char类型。

所以,C提供了一种方法可单独设置int类型中的每个字节。(C的早期版本不提供十六进制记法)

1
2
3
4
5
int x = 'ABCD';
char c = 'ABCD';
printf("%d %d %c %c\n", x, 'ABCD', c, 'ABCD');
// 输出 1094861636 1094861636 D D
// 如果把'ABCD'视为int类型,它是一个4字节的整数值。如果将其视为char类型,程序只使用最后一个字节。

const限定符❔

1
2
3
4
5
// 都在函数外部
// C++的声明
const double PI = 3.14159;
// 相当于下面C中的声明
static const double PI = 3.14159;

C中全局的const具有外部链接,C++中则具有内部链接。

C++可以使用关键字extern使一个const值具有外部链接。所以两种语言都可以创建内部链接和外部链接的const变量。它们的区别在于默认使用哪种链接

C++中可以使用const值来初始化其他const变量,在C中不能。

枚举❔

C++使用枚举比C严格。特别是,只能把enum常量赋给enum变量,然后把变量与其他值作比较。不经过显式强制类型转换,不能把int类型值赋给enum变量,而且也不能递增一个enum变量。

C++还有限定作用域的枚举和不限定作用域的枚举(enum后加上class)。

布尔类型❔

在C++中,布尔类型是bool,而且ture和false都是关键字。在C中,布尔类型是_Bool,但是要包含stdbool.h头文件才可以使用bool、true和false。

C++11中没有的C99/C11特性❔

指定初始化器;

复合初始化器(Compound initializer);

受限指针(Restricted pointer)(即,restric指针);

变长数组;

伸缩型数组成员;

带可变数量参数的宏。

C++

1 基础

对于类的特殊成员函数,用户显式声明和编译器隐式声明的关系❔

来源:

Engineering Distinguished Speaker Series: Howard Hinnant - YouTube

C++ class declarations

在头文件里实现函数的注意事项❔

除了inline函数在类的定义中实现的成员函数模板函数外,若直接在头文件中定义函数,当头文件被多个实现文件包含时,函数会被生成的多个obj包含,导致重定义错误,无法链接。

如何查询当前默认使用的C++标准❔

1
2
g++ -E -dM - </dev/null | grep "STDC_VERSION"
# 输出 #define __STDC_VERSION__ 201710L

-E意思是只做预处理。

-dM搭配-E表示展示所有预定义宏。

gcc相关命令参考Option Summary (Using the GNU Compiler Collection (GCC))

-意思是从标准输入中读取,而不是从该命令行上提供的文件名中读取。这是一个常见的 Unix 约定。

< /dev/null/dev/null 重定向标准输入,其长度为 0。因此将从标准输入读取并立即到达输入的末尾,使其仅打印预定义的宏(而不是输入中的任何宏,因为没有任何输入)。这是标准的shell 语法,不特定于g++的调用。

简单编译流程❔

preprocess,compile,assembly,link

1
2
3
4
g++ -E test.cpp -o test.i
g++ -S test.i -o test.s
g++ -c test.s -o test.o
g++ test.o -o test

也可以直接对源文件执行每一命令。链接时是否写.exe均可。

静态成员变量的类内初始化❔

使用const修饰的静态整型变量,比如static const int | char | long可以类内初始化。

使用constexpr修饰的所有静态变量,比如static constexpr int | float | double必须类内初始化。

非静态成员变量不能用constexpr修饰。

初始化有哪几种,顺序如何❔

声明初始化 -> 初始化列表 -> 构造函数初始化

dynamic_caststatic_cast的区别❔

1)在类层次间进行上行转换时,两者一样。

2)在类层次间进行下行转换时,dynamic_cast具有类型检查的功能,比static_cast更安全。转换失败dynamic_cast会返回空指针,而static_cast只会转换,难以发现错误。

constexpr和const区别❔

constexpr是C++11引入的关键字,指示值或返回值(如果可能)将在编译时进行计算。

const并未区分出编译期常量和运行期常量,constexpr限定在了编译期常量。因此可以用函数返回值给const变量赋值,但不能用其给constexpr变量赋值。

constexpr可以置于类构造函数前。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// 修饰变量时constexpr包含了const的含义,完全相同
constexpr const int N = 5;
constexpr int N = 5;

// 此时必须同时使用const、constexpr,constexpr表示NP指针本身是常量表达式,而const表示指向的值是一个常量
static constexpr int N = 3;
int main()
{
constexpr const int *NP = &N;
return 0;
}

// C++11中对成员函数constexpr包含了const的含义
constexpr void f();
// 以后可能必须写成
constexpr void f() const;

前置++和后置++的区别❔

前置++先赋值后返回,后置++先返回后赋值。

在对内置类型使用的时候,经过编译器优化,两者效率没有区别。

对于自定义类,通常的实现方式是:前置++自增后返回引用;后置++先用临时对象保存原来的对象,然后对原对象调用前置++,再返回临时对象。所以自定义类的客户使用前置++效率更高。

纯虚函数可以有函数体吗❔

实现纯虚函数一般不常见,但纯虚析构函数必须实现。

如有函数体必须定义在类的外部。

cfront是什么❔

最初Stroustrup实现C++时,使用了一个C++到C的编译器程序,而不是开发直接的C++到目标代码的编译器。前者叫做cfront (表示C前端,C front end),它将C++源代码翻译成C源代码,然后使用一个标准C编译器对其进行编译。这种方法简化了向C的领域引入C++的过程。其他实现也采用这种方法将C++引入到其他平台。

随着C++的日渐普及,越来越多的实现转向创建C++编译器,直接将C+ +源代码生成目标代码。这种直接方法加速了编译过程,并强调C++是一种独立(虽然有些相似)的语言。

各种继承方式的作用❔

特征 公有继承 保护继承 私有继承
公有成员变成 派生类的公有成员 派生类的保护成员 派生类的私有成员
保护成员变成 派生类的保护成员 派生类的保护成员 派生类的私有成员
私有成员变成 只能通过基类接口访问 只能通过基类接口访问 只能通过基类接口访问
能否隐式向上转换 是(但只能在派生类中)

隐式向上转换指无需进行显式类型转换,就可以将基类指针或引用指向派生类对象。

可以使用using声明使派生类可以使用私有基类中的方法。

虚基类是什么❔

当派生类使用关键字virtual来指示派生时,基类就成为虚基类。从虚基类派生而来的类将只继承一个基类对象。

当类通过多条虚途径非虚途径继承某个特定的基类时,该类将包含一个表示所有的虚途径的基类子对象和多个分别表示各条非虚途径的基类子对象。

例如,假设类B被用作类C和D的虚基类,同时被用作类X和Y的非虚基类。类M从C、D、X、 Y派生而来。在这种情况下,类M从虚派生祖先(即类C和D)那里共继承了一个B类子对象,并从 每一个非虚派生祖先(即类X和Y)分别继承了一个B类子对象。因此,它包含三个B类子对象。

间接虚基类的派生类包含直接调用间接基类构造函数的构造函数。这对于间接非虚基类是非法的。

嵌套类是什么❔

在另一个类中声明的类被称为嵌套类(nested class)。

嵌套类、结构和枚举的作用域特征:

声明位置 包含它的类是否可以使用它 从包含它的类派生而来的类是否可以使用它 在外部是否可以使用
私有部分
保护部分
公有部分 是,通过类限定符来使用

C++为什么从语法上禁止函数模板的偏特化❔

函数模板偏特化和函数重载同时存在的话,编译器决定调用哪个函数?语义存在模糊性。

函数调用顺序:

  1. 普通函数及其重载函数,若无匹配进入2,有匹配返回
  2. 函数模板中最匹配的,若无匹配报错,有匹配进入3
  3. 该函数模板的全特化版本中最匹配的,若无匹配直接调用函数模板,有匹配返回

有以下三种办法实现函数模板偏特化的行为:

  1. 对类进行偏特化,重载类的函数调用运算符,利用函数对象实现。
  2. 利用标签分发进行函数重载。(可参考Effective C++条款47)
  3. 利用C++20的concept特性。

C++是否尽可能地解释为函数声明❔

是。

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
#include <iostream>

class Widget
{
int i;

public:
Widget()
{
}
Widget(int i)
{
this->i = i;
}
};

int main()
{
Widget w1;
Widget w2(); // 它没有声明名为w的Widget,而是声明了一个名为w的函数,该函数不带任何参数,并返回一个Widget。
Widget w3(1);
std::cout << typeid(w1).name() << std::endl;
std::cout << typeid(w2).name() << std::endl;
std::cout << typeid(w3).name() << std::endl;
}
/*
6Widget
F6WidgetvE
6Widget
*/

C++模板的定义是否只能放在头文件中❔

不是。但若将定义放在单独的源文件中,当模板被客户端代码使用时,必须在定义所在的源文件声明对应的实例化版本,否则会链接失败。

如何利用C++判断字节序❔

Little Endian即低位字节排放在内存的低地址端;Big Endian即高位字节排放在内存的低地址端。以0x1234为例,Little Endian下0x34放在低地址端。

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
#include <iostream>
using namespace std;

bool is_little_endian_1()
{
int a = 0x1234;
char b = *(char *)&a; // 取b等于a的低地址部分
return b == 0x34;
}

bool is_little_endian_2()
{
union NUM
{
int a;
char b;
} num;
num.a = 0x1234;
return num.b == 0x34;
}

int main()
{
std::cout << boolalpha << is_little_endian_1() << endl;
std::cout << is_little_endian_2() << noboolalpha << std::endl;
}

2 标准库

为什么std::string没有emplace或emplace_back操作❔

std::vector<T>::emplace_back的好处在于它直接在向量里构造类型为T的对象,而不是构造临时对象并拷贝。

char是基本类型,不需要上述操作。

std::vector的增长因子k是如何设计的❔

有的实现里k=2,有的实现里k=1.5。

使用k=1.5更好,因为这样才有机会在分配时复用之前释放的内存。

考虑k=2的情况:

  1. 分配16字节。
  2. 分配32字节,释放16字节。16字节内存空洞。
  3. 分配64字节,释放32字节。48字节内存空洞(假设这次释放的32字节和上次释放的16字节相邻)。
  4. 分配128字节,释放64字节。112字节内存空洞。
  5. 设初始为x,第n次分配时,\(2^{n}\times x>(2^{n-1}-1)\times x\)。下次分配的内存一定比之前释放的总和大。

k=1.5则不会有以上情况。

容器的迭代器、指针和引用无效的特殊情况❔

string是STL中在swap过程中会导致迭代器、指针和引用变为无效的唯一容器。

deque只要没有删除操作发生,且插入操作只发生在容器的末尾,则指向数据的指针和引用就不会变为无效(迭代器有可能会变为无效)。deque是唯一的迭代器可能会变为无效而指针和引用不会变为无效的STL标准容器。

什么是STL中的分配子❔

1
2
3
4
5
6
7
8
9
tempalte <typename T>
class allocator {
public:
template<typename U>
struct rebind{
typedef allocator<U> other;
};
};
// 每个分配子模板A(如std::allocator、SpecialAllocator等)都要有一个被称为rebind的嵌套结构模板。rebind带有唯一的类型参数U,并且只定义了一个类型定义other。other仅仅是A<U>的名字。结果,通过引用Allocator::rebind<ListNode>::other,list<T>就能从T对象的分配子(称为Allocator)得到相应的ListNode对象的分配子。

什么情况下vector<char>优于string❔

在多线程环境中使用引用计数,由避免内存分配和字符拷贝所节省下来的时间还比不上花在背后同步控制上的时间。

许多string实现在背后使用了引用计数技术。vector的实现不允许使用引用计数,所以不会发生隐藏的多线程性能问题。

可以检查basic_string模板的拷贝构造函数是否添加了引用计数。

如何把vector和string数据传给旧的API❔

对于vector v,需要得到一个指向v中数据的指针把v中的数据作为数组来对待,只需使用&v[0]。

对于string s,对应的形式是s.c_str()。string中的数据不一定存储在连续的内存中。而且string的内部表示不一定以空字符结尾。

为什么说vector<bool>是假容器❔

它并不真的储存bool,相反,为了节省空间,它储存的是bool的紧凑表示,使用与位域(bitfield)一样的思想。

1
2
vector<bool> v;
bool *pb = &v[0]; // 错!表达式的右边是vector<bool>::reference*类型,而不是bool*类型

标准C++库的bitset同样使用紧凑表示,但bitset不支持插入和删除,不支持迭代器,不被视为STL容器。

deque<bool>确实存bool。

相等和等价的区别❔

相等(equality)的概念基于operator==,等价(equivalence)的概念基于operator<。如果两个值中的任何一个(按照一定的排序准则)都不在另一个的前面,那么这两个值(按照这一准则)就是等价的。

关联容器是基于等价而不是相等的,set的find成员函数使用等价概念,而非成员的find函数使用相等概念。

为什么总是要包含所有需要的头文件❔

C++标准与C的标准有所不同,它没有规定标准库中的头文件之间的相互包含关系。

不同的C++实现选择了不同的实现方式。可能一种实现里<vector>包含了<string>,虽然同时用到<vector>和<string>,但只需要包含<vector>。但其它实现不一定是这样。所以只要用到<string>就包含<string>,有助于代码移植。

实现一个自旋锁❔

注意,自旋锁互斥忙等会占用大量CPU时间。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class spinlock
{
std::atomic_flag flag;
public:
spinlock_mutex() : flag(ATOMIC_FLAG_INIT)
{
}
void lock()
{
while (flag.test_and_set(std::memory_order_acquire))
;
}
void unlock()
{
flag.clear(std::memory_order_release);
}
};

为什么说浮点值的算术原子运算并不存在❔

根据IEEE 754浮点数标准,容许采用多种形式表示同一数值。如80.0可以表示成5*(24),也可以表示成20*(22)。但compare_exchange_weakcompare_exchange_strong操作所采用的是bitwise comparison,效果等同于直接使用memcmp()函数。

原子操作的内存次序❔

先后一致次序:memory_order_seq_cst。要求在所有线程间进行全局同步。常见的x86和x86-64提供了开销相对低的方式以维持先后一致,但weakly-ordered machine(Alpha、ARM、PowerPC等)上开销较大。

获取-释放次序:memory_order_consume(C++17建议不用)、memory_order_acquirememory_order_releasememory_order_acq_rel

宽松次序:memory_order_relaxed。无须任何额外的同步操作,线程间仅存的共有信息是每个变量的改动序列。

3 内存管理

内存分区❔

栈区:由编译器自动分配和释放,存放函数的参数值、局部变量的值等,大小受限于操作系统和硬件。

堆区:由程序员手动分配和释放,通常用于动态内存分配,其大小受限于系统可用的虚拟内存大小。

全局/静态区:存放全局变量、静态变量等,包括初始化和未初始化的数据,其大小由编译器决定。

常量区:存储常量,一般不允许修改。

代码区:存放程序的二进制码。

结构体成员变量如何存放❔

在不使用#pragma pack(N)宏的情况下:

  1. 结构(struct或联合union)的数据成员,第一个数据成员放在offset为0的地方,以后每个数据成员存储的起始位置要从自己大小的整数倍开始(比如int在32位机为4字节,则要从4的整数倍地址开始存储)。

  2. 结构体的总大小,也就是sizeof的结果,必须是其内部最大成员的整数倍,不足的要补齐。

  3. 结构体作为成员时,结构体成员要从其内部最大元素大小的整数倍地址开始存储。(struct a里存有struct b,b里有char、int、double等元素时,那么b应该从8的整数倍地址处开始存储,因为sizeof(double) = 8 bytes)

若使用#pragma pack(N)宏,编译器按N值和上述对齐值中的较小值进行对齐。当N值大于等于所有数据成员长度的时候,宏不产生任何效果。

C++03的auto_ptr为什么在C++11被废弃❔

C++03中并不支持移动语义,而std::auto_ptr 却试图用复制构造函数来实现移动构造函数的功能,结果导致其无法与vector 等容器兼容,论为失败品。

0宽度位域有何作用❔

1
2
3
4
5
6
7
8
struct stuff 
{
unsigned int field1: 30;
unsigned int : 2; // 使用未命名位域显式填充(一个位域成员不允许跨越两个unsigned int的边界)。
unsigned int field2: 4;
unsigned int : 0; // 0宽度位域,使下一个位域field3存在下一个unsigned int中。
unsigned int field3: 3;
};

4 C++11

新类型❔

支持64位(或更宽)的整型:long long unsigned long long

支持16位和32位的字符表示:char_16t char32_t

原始字符串:cout << R"(King)" << endl;

初始化列表❔

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
int x = {5};
double y{2.75};
short quar[5]{4, 5, 2, 76, 1};

class Stump
{
private:
int roots;
double weight;
public:
Stump{int r, double w) : roots(r), weight(w) {}
};

Stump s1(3,15.6);
stump s2{5,43.4};
Stump s3 = {432.1};
// 如果类有将模板std::initializer_list作为参数的构造函数,则只有该构造函数可以使用列表初始化形式。
vector<int> a1(10) ;
// uninitialized vector with 10 elements
vector<int> a2{10};
// initializer list, a2 has 1 element set to 10
vector<int> a3{4,6,1};
// 3 elements set to 4,6,1

声明❔

auto、decltype、返回类型后置、using别名、nullptr。

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
// auto
for (std::initializer_list<double>::iterator p = i1.begin(); p != i1.end(); p++)
// 替换
for (auto p = i1.begin(); p != i1.end(); p++}

// decltype
int j = 3;
int &k = j;
const int &n = j;
decltype(n) i1; // i1 type const int &
decltype(j) i2; // i2 type int
decltype((j)) i3; // i3 type int &
decltype(k + 1) i4; // i4 type int

// 返回类型后置
template<typename T, typename U)
auto eff(T t, U u) -> decltype(T*U)
{
// ...
}

// 模板别名
// using可用于模板部分具体化,但typedef不能
template<typename T>
using arr12 = std::array<T, 12>; // template for multiple aliases
arr12<double> a1;

// nullptr
// 用于表示空指针,它是指针类型,不能转换为整型类型。不能将nullptr传给接受int参数的函数。
// 为向后兼容,C++11仍允许使用0来表示空指针,因此表达式nulptr==0为true。

智能指针❔

抛弃auto_prt,新增shared_prtunique_ptrweak_ptr

所有新增的智能指针都能与STL容器和移动语义协同工作。

异常规范❔

抛弃异常规范,新增noexcept

1
2
3
4
5
// 抛弃
void f501(int) throw(bad_dog); // 可能抛出bad_dog类型的异常
void f733(1ong long) throw(); // 不抛出异常
// 新增
void f875(short, short) noexcept; // 不抛出异常

作用域内枚举❔

旧形式的枚举,同一个作用域内两个枚举的枚举成员不能同名。

新形式的枚举,需要显式限定。New1::never

1
2
3
4
5
// 旧形式
enum 01d1 {yes, no, maybe};
// 新形式
enum Class Newl {never, sometimes, often, always};
enum struct New2 {never, lever, sever};

类的变化❔

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
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
// explicit禁止单参数构造函数导致的自动转换(即隐式调用)
// C++11拓展了explicit的这种用法,使得可对转换函数做类似的处理
class Plebe
{
operator int() const;
explicit operator double(); const;
};
Plebe a, b;
int n = a;
double x = b; // 不合法
x = double(b) ;

// 类内成员初始化 (如有成员初始化列表,会被成员初始化列表覆盖)
class Session
{
int mem1 = 10;
double mem2 {1966.54};
short mem3;
public:
Session(){}
// Session() : mem1(10), mem2(1966.54) {}
Session(short s) : mem3(s) {}
Session(int n, double d, short s) : mem1(n), mem2(d), mem3(s) {}
}

// 如果提供了析构函数、复制构造函数或复制赋值运算符,编译器将不会自动提供移动构造函数和移动赋值运算符;如果提供了移动构造函数或移动赋值运算符,编译器将不会自动提供复制构造函数和复制赋值运算符。

// 用关键字default显式声明6个特殊成员函数(构造,析构,复制构造,复制赋值,移动构造,移动赋值)的默认版本。
// 用关键字delete禁止编译器使用任何成员函数。

// 在一个构造函数的定义中使用另一个构造函数称为委托。可以利用成员初始化列表进行委托。
class Notes{
int k;
double x;
std::string st;
public:
Notes();
Notes(int) ;
Notes(int, double);
Notes(intdouble, std::string);
};
Notes::Notes(int kk, double xx,std: :string stt) : k(kk),x(xx),st(stt) {/* do stuff */}
Notes::Notes() : Notes(0, 0.01, "Oh") {/* do other stuff */}
Notes::Notes(int kk) : Notes(kk, 0.01, "Ah") {/* do yet other stuff */}
Notes::Notes(int kk, double xx ) : Notes(kk, xx,"Uh"} {/* ditto */}

// C++98提供了一种让名称空间中函数可用的语法,C++11将这种方法用于构造函数。
class BS
{
int q;
double w;
public:
BS() : q(0), w(0) {}
BS(int k) : q(k), W(100) {}
BS(double x) : q(-1), w(x) {}
BS(int k,double x) : q(k),W(x) {}
void Show() const {std::cout << q <<","<< w << '\n';}
};

class DR : public BS
{
short j;
public:
using BS::BS;
DR() : j(-100) {} // DR needs its own default constructor
DR(double x) : BS(2*x), j(int(x)) {}
DR(int i) : j(-2), BS(i,0.5*i) {}
void Show() const {std::cout << j << ","; BS::Show();}
};
int main()
{
DR o1; // use DR()
DR o2(18.81); // use DR(double) instead of BS(double)
DR o3(10, 1.8); // use BS(int, double)
}

// 参数列表后加上说明符override指出要覆盖一个虚函数
// 参数列表后加上说明符final指出禁止派生类覆盖特定的虚函数
// 说明符override和final并非关键字,而是具有特殊含义的标识符。这意味着编译器根据上下文确定它们是否有特殊含义:在其他上下文中,可将它们用作常规标识符,如变量名或枚举。

模板及STL❔

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// 范围for
std::vector<int> v(6);
for(auto &x : v)
x = std::rand();

// 新STL容器
// forward list, unordered map, unordered multimap, unordered set, unordered multiset, array

// 新STL方法
// cbegin(), cend(), crbegin(), crend()

// valarray
// C++11添加了两个函数(begin()和end(),它们都接受valarray作为参数,返回迭代器,以应用基于范围的STL算法

// 废弃export

// C++11以前声明嵌套模板要求将>>用空格分开,C++11不再有该要求

移动语义❔

右值引用使用&&表示,可关联到右值(即可出现在赋值表达式右边,但不能对其应用地址运算符的值)。

右值包括字面常量(C风格字符串除外,它表示地址)、诸如x+y等表达式以及返回值的函数(条件是该函数返回的不是引用)。

1
2
3
4
5
6
int x = 10;
int y = 23;
int && r1 = 13;
int && r2 = x + y;
double && r3 = std::sqrt(2.0);
// 将右值关联到右值引用导致该右值被存储到特定的位置,且可以获取该位置的地址。虽然不能将运算符&用于13,但可将其用于r1。通过将数据与特定的地址关联,使得可以通过右值引用来访问该数据。

引入右值引用后,值类别分为左值、纯右值、亡值。值类别

std:move源码:

1
2
3
4
5
template <typename T>
constexpr typename std::remove_reference_t<T> &&move(T &&t) noexcept
{
return static_cast<typename std::remove_reference_t<T> &&>(t);
}

std:forward源码:

1
2
3
4
5
6
7
8
9
10
11
12
13
template <typename T>
constexpr T &&forward(typename std::remove_reference_t<T> &t) noexcept
{
return static_cast<T &&>(t);
}

template <typename T>
constexpr T &&forward(typename std::remove_reference_t<T> &&t) noexcept
{
static_assert(!std::is_lvalue_reference_v<T>,
"std::forward must not be used to convert an rvalue to an lvalue");
return static_cast<T &&>(t);
}

Lambda❔

在C++中引入lambda的主要目的是,能够将类似于函数的表达式用作接受函数指针或函数对象的函数的参数。

包装器❔

C++11提供了bindmen_fnreference_wrapperfunctionbind可替代bind1stbind2nd,但更灵活;mem_fn 能够将成员函数作为常规函数进行传递;reference_wrapper能够创建行为像引用但可被复制的对象;function能够以统一的方式处理多种类似于函数(函数指针、函数对象、lambda)的形式(减少以函数为参数的模板的实例化次数)。

可变参数模板❔

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
#include <iostream>
#include <string>

// definition for 0 parameters
void show_list() {}

// definition for 1 parameter
template< typename T>
void show_list (const T& value)
{
std: :cout << value << '\n' ;
}

// definition for 2 or more parameters
template<typename T, typename... Args>
void show_list(const T& value, const Args&... args)
{
std::cout << value << ",";
show_list(args...);
}

注意,当多个可变参数模板重载时,优先选择更特化的。

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
#include <iostream>

void print()
{
std::cout << "end" << std::endl;
}

// 优先
template <typename T, typename... Args>
void print(const T &firstArg, const Args &...args)
{
std::cout << firstArg << " " << sizeof...(args) << std::endl; // sizeof ... args代表获取参数个数
print(args...);
}

template <typename... Types>
void print(const Types &...args)
{
std::cout << "never" << std::endl;
}

int main()
{
print('A', 'B', 'C');
return 0;
}

/*
输出
A 2
B 1
C 0
end
*/

并发编程❔

原子操作库提供了头文件atomic,线程支持库提供了头文件threadmutexcondition_variablefuture

《C++并发编程实战》第二版中各并发程序库的简要对比:

5 C++14

变量模板❔

1
2
3
4
5
6
7
8
9
10
11
#include <iostream>

template <class T>
constexpr T kPI = T(3.1415926535897932385);

int main()
{
std::cout << kPI<int> << std::endl;
std::cout << kPI<double> << std::endl;
return 0;
}

decltype(auto)❔

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
#include <iostream>

// C++11
// template <typename T, typename U>
// auto func(T &&t, U &&u) -> decltype(std::forward<T>(t) + std::forward<U>(u))
// {
// return std::forward<T>(t) + std::forward<U>(u);
// };

// C++14
template <typename T, typename U>
decltype(auto) func(T &&t, U &&u)
{
return std::forward<T>(t) + std::forward<U>(u);
};

int main()
{
std::cout << func(1, 2.) << std::endl;
std::cout << std::boolalpha << std::is_same_v<decltype(func(1, 2.)), double> << std::endl; // true
return 0;
}

智能指针❔

补充了make_unique

6 C++17

折叠表达式❔

不使用显式递归的情况下使用可变参数模板。

1
2
3
4
5
6
7
8
9
10
11
12
#include <iostream>

template <typename... Args>
auto sum(Args... args)
{
return (... + args);
}
int main()
{
std::cout << sum(1, 2, 3) << std::endl;
return 0;
}

编译期if❔

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#include <iostream>

template <int N, int... Ns>
auto sum()
{
if constexpr (0 == sizeof...(Ns))
return N;
else
return N + sum<Ns...>();
}

int main()
{
std::cout << sum<1, 2, 3>() << std::endl;
return 0;
}

PMR❔

PMR即polymorphic memory resource,使用std::pmr命名空间下的polymorphic_allocatormemory_resource等类型,可以逐对象而不是逐类型指定内存分配器类型。

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
#include <iostream>
#include <string>
#include <vector>
#include <array>
#include <memory_resource>
#include <cstddef>

int main()
{
std::array<std::byte, 200000> buf;
std::pmr::monotonic_buffer_resource pool{buf.data(), buf.size()};

using basic_string = std::basic_string<char, std::char_traits<char>, std::pmr::polymorphic_allocator<char>>;

std::vector<std::basic_string<char, std::char_traits<char>, std::pmr::polymorphic_allocator<char>>,
std::pmr::polymorphic_allocator<std::basic_string<char, std::char_traits<char>, std::pmr::polymorphic_allocator<char>>>>
coll(&pool);
// 简写为 std::pmr::vector<std::pmr::string> coll(&pool);

for (int i = 0; i < 1000; ++i)
{
coll.emplace_back("just a non-SSO string");
}
return 0;
}

如何利用缓存行大小❔

std::hardware_destructive_interference_size:作为两个对象之间的偏移量,以避免由于不同线程的不同运行时访问模式而导致的破坏性干扰(假共享)。

std::hardware_constructive_interference_size:作为两个对象的组合内存占用大小和基本对齐的限制,以促进它们之间的构造性干扰(真共享)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
#include <atomic>

struct keep_apart
{
alignas(std::hardware_destructive_interference_size) std::atomic<int> a;
alignas(std::hardware_destructive_interference_size) std::atomic<int> b;
};

struct together
{
std::atomic<int> a;
int b;
};

struct kennel
{
// 其它数据成员
alignas(sizeof(together)) together pack;
};
static_assert(sizeof(together) <= std::hardware_constructive_interference_size);

std::hardware_destructive_interference_size, std::hardware_constructive_interference_size - cppreference.com

P0154R1 constexpr std::hardware_{constructive,destructive}_interference_size (open-std.org)

7 C++20

协程❔

参见笔记:C++协程 - Homeworld

consteval和constinit❔

当函数声明为constexpr时,只有当其所有参数都是编译期常量时,返回值才是编译期常量。

consteval仅用于函数的声明,且调用该函数使用的参数必须是编译期常量,以保证该函数一定在编译期计算。

constinit声明拥有静态或线程存储期的变量,不能和constexpr或consteval一同使用。声明引用时,constinit等价于constexpr;声明对象时,constexpr强制对象为const,而constinit不要求。

三路比较运算符❔

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
#include <compare>
#include <iostream>
#include <set>

struct Point
{
int x;
int y;
auto operator<=>(const Point &) const = default;
};

int main()
{
Point pt1{1, 1}, pt2{1, 2};
std::set<Point> s;
s.insert(pt1);

std::cout << std::boolalpha
<< (pt1 == pt2) << ' ' // false,operator==被隐式预置
<< (pt1 != pt2) << ' ' // true
<< (pt1 < pt2) << ' ' // true
<< (pt1 <= pt2) << ' ' // true
<< (pt1 > pt2) << ' ' // false
<< (pt1 >= pt2) << ' '; // false
}

Default comparisons (since C++20) - cppreference.com

约束模板类型❔

利用concept约束模板类型。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
#include <iostream>

// C++11
// template <typename T, std::enable_if_t<std::is_integral_v<T>, T> = 0>
// T add(T a, T b)
// {
// return a + b;
// }

// C++20
template <typename T>
concept integral = std::is_integral_v<T>;

template <integral T>
T add(T a, T b)
{
return a + b;
}

int main()
{
std::cout << add(1, 2) << std::endl;
return 0;
}

C/C++学习笔记
https://reddish.fun/posts/Notebook/C-CPP-note/
作者
bit704
发布于
2023年3月5日
许可协议