【八股cover#2】CPP语法 Q&A与知识点

发布时间 2023-06-11 20:05:30作者: dayceng

CPP语法 Q&A与知识点

简历cover

1、熟练使用C的指针应用及内存管理

指针与引用的区别

1.指针是一个变量,存储的是一个地址引用跟原来的变量实质上是同一个东西,是原变量的别名;

2.指针可以有多级引用只有一级

3.指针可以为空,引用不能为NULL且在定义时必须初始化;

4.指针在初始化后可以改变指向,而引用在初始化之后不可再改变;

5.sizeof指针得到的是本指针的大小,sizeof引用得到的是引用所指向变量的大小;(即sizeof返回的是变量的大小)

6.当把指针作为参数进行传递时,也是将实参的一个拷贝传递给形参,两者指向的地址相同,但不是同一个变量,在函数中改变这个变量的指向不影响实参,而引用却可以;

7.引用本质是一个指针,同样会占4字节内存,指针是具体变量,需要占用存储空间(具体情况还要具体分析);

8.引用在声明时必须初始化为另一变量,一旦出现必须为typename refname &varname形式,指针声明和定义可以分开,可以先只声明指针变量而不初始化,等用到时再指向具体变量。

9.引用一旦初始化之后就不可以再改变 (变量可以被引用为多次,但引用只能作为一个变量引用)。指针变量可以重新指向别的变量;

10.不存在指向空值的引用,必须有具体实体;但是存在指向空值的指针。

const

const的作用

被const修饰的值不能再改变,是只读变量,因此在定义时就要给它赋初值

具体体现如下:

常量指针(底层const)

定义了一个指针,但这个指针指向的是一个只读对象,因此无法通过常量指针来修改对象的值

常量指针强调的是指针所指对象的不可改变性

形式一般如下:

​ (1)const 数据类型 *指针变量 = 变量名

​ (2)数据类型 const *指针变量 = 变量名

int tmp = 10;
const int *a = &tmp;
int const *a = &tmp;

*a = 9;//错误,只读常量
tmp = 9;//正确
指针常量(顶层const)

定义了一个指针,该指针的值只能在定义时进行初始化,在其他地方不能改变。

指针常量强调的是指针本身的不可变性

形式如下:

数据类型* const 指针变量 = 变量名

int tmp = 10;
int tmp2 = 12;

int* const p = &tmp;

p = &tmp2;//错误,指针常量的指向不能再改变
*p = 9;//正确	
记忆点
  • 常量就是不可修改的值
  • 数据类型*的是指针常量

tips:

int const *r;//常量指针

先看最右边,变量名是r,r是个指针

从右往左看,const int

所以r是一个指向const int的指针

ps:(*r)不能改变,但是r本身不是const int,所以可以改变

int *const r;//指针常量

先看最右边,变量名是r

然后从右往左,r是个const,并且还有*

所以r是个const指针

再往左看,该指针还指向int

ps:这意味着r的值本身不能改变,不能给r赋值,但是int没有const修饰,所以(*r)是可以改变的

new和malloc

定义

new 是 C++ 中的一个关键字,用于动态分配对象。它会自动计算需要分配的内存大小,并返回指向新分配的对象的指针。使用 new 分配内存时,还可以调用相应的构造函数以初始化对象。

malloc 则是标准 C 库函数之一,用于动态分配指定大小的内存块。malloc 函数接受一个参数,即需要分配的字节数,返回一个指向新分配内存的指针。使用 malloc 分配内存时,必须手动进行内存初始化。

两者异同(具体)

​ 1、使用new分配内存时,如果失败会返回异常bad_alloc,而malloc分配内存失败会返回NULL

​ 2、使用new操作符申请内存分配时不用指定内存块的大小,而malloc需要显式指出所需内存的尺寸

​ 3、opeartor new/operator delete可以被重载,⽽malloc/free并不允许重载。

​ 4、new/delete会调用构造函数和析构函数以完成对象的构造/析构,malloc不会管这些

​ 5、malloc/free是C/C++的标准库函数,new/delete是C++的运算符

​ 6、new操作符自由存储区上为对象动态分配内存空间,malloc函数上动态分配空间

malloc底层实现

在Linux下,如果开辟空间小于128K,使用brk()函数来增加堆的有效区域并申请内存空间;

如果大于128K则使用mmap()函数在堆和栈之间文件映射区域申请一块虚拟内存

new申请内存时操作系统做了什么事?

new 运算符调用 C++ 运行库中的 operator new 函数,该函数像操作系统请求一段足够大的连续内存块。然后操作系统会在虚拟地址空间中寻找可用的物理内存也,将这些页映射到进程的地址空间中,此时操作系统会记录下已分配的内存块信息(在释放时使用)。

最后的结果就是 operator new 函数会返回一个指向新分配内存空间的指针。

深拷贝与浅拷贝

浅拷贝只是拷贝了一个指针,并没有新开辟一个地址,拷贝的指针和原来的指针指向同一块地址

如果原来的指针所指向的资源释放了,那么再释放浅拷贝资源就会出现错误。

深拷贝不仅拷贝值,还开辟出一块新的空间来存放新的值,即使原先的对象被析构掉,并且释放内存,也不会影响到深拷贝得到的值。

在自己实现拷贝赋值时,如果有指针变量的话需要自行实现深拷贝

struct、union的异同

相同点:

  1. 都可以用来将多个不同类型的数据组合成为一个新的数据类型。
  2. 都可以通过成员访问运算符"."来访问结构体或联合体中的成员变量。
  3. 结构体和联合体中的成员变量都可以是任意C语言中的数据类型,包括基本类型、指针类型、结构体类型、联合体类型等。
  • struct中每个变量依次存储;(各个成员变量占用独立的内存空间并且互相不影响)union中,每个变量都是从偏移地址零开始存储,同一时刻只有一个成员存储于该地址(所有成员变量共用同一块内存空间)
  • struct内存大小遵循结构对齐原则
    • 数据成员对齐规则:每个数据成员存储的起始位置要从该成员大小的整数倍开始
    • 数据成员包含结构体:结构体成员要从其内部最大元素对象的整数倍地址开始存储
    • 结构体总大小:其内部最大基本成员的整数倍,不足则要补齐
  • union内存大小为其最大成员的整数倍
  • union主要用于节省内存和在不同类型之间进行转换,而struct则主要用于组织一组相关数据并且容易扩展。
union的赋值

因为所有的成员都共享同一个内存空间,所以 union 的内存大小等于最大成员的大小,而且不同成员的地址是相同的。

在对union进行赋值操作时,实际上是把数据从当前类型转换为了另一个类型,该过程会涉及大小端问题(在小端模式下,低地址存放的是最低有效字节高地址存放的是最高有效字节;而在大端模式下则正好相反。)

因此,当我们对一个 union 进行赋值时,需要考虑当前系统的大小端模式。如果目标系统和源系统的大小端模式相同,则直接进行赋值即可;否则需要通过字节交换来保证正确性。具体来说,在小端模式下,如果要将一个 4 字节的整数赋值给一个 2 字节的 short 类型的成员,就需要将前两个字节复制给 short 类型的成员;而在大端模式下,则需要将后两个字节复制给 short 类型的成员。

(总结一下:https://blog.csdn.net/weixin_57544072/article/details/121583724)

栈溢出是发生在那些场景,伪递归和递归的区别,伪递归会发生栈溢出吗

函数调用的层数过多或者函数内部申请的局部变量过多时有可能会导致栈空间不足,进而发生栈溢出错误。

栈溢出问题通常会出现在递归调用大量的局部变量申请等情况中。

伪递归和递归的区别在于:递归是函数自己直接或者间接地调用自己,而伪递归则是通过手动维护一个栈结构来实现函数调用的过程,从而避免了因为递归过深导致的栈溢出问题

由于伪递归手动维护了一个栈,所以不会发生栈溢出的情况。但是使用伪递归需要额外的空间来维护栈结构,因此也可能会有其他的空间限制问题。

def fake_recursive(n):
    stack = [n]
    while stack:
        i = stack.pop()
        if i <= 0:
            continue
        else:
            print(i)
            stack.append(i-1)
            stack.append(i-2)

fake_recursive(10)

这个函数使用一个列表作为栈来维护递归调用过程中需要保存的信息。具体来说,它首先将输入参数n加入到栈中,然后进入while循环,只要栈不为空就一直循环。在每次循环中,它从栈顶弹出一个元素i,并且检查它是否小于等于0。如果是,则跳过当前循环;否则,打印i并且将i-1和i-2依次加入到栈中,以模拟递归调用。

memcpy()函数需要注意哪些问题

函数原型声明void *memcpy(void *dest, void *src, unsigned int count);

memcpy函数用于把资源内存(src所指向的内存区域)中连续的count个字节数据拷贝到目标内存(dest所指向的内存区域)

  • 数据长度count的单位是字节,1byte = 8bit
  • 数据类型为char,则数据长度就等于元素的个数;其他数据类型则要注意数据长度的值
  • n * sizeof(type_name)的写法

gcc编译一个程序会经历哪些过程

当使用gcc编译一个程序时,它会经历以下过程:

  1. 预处理阶段(Preprocessing Stage):在此阶段中,gcc将预处理指令展开并将宏定义替换为其相应的值。同时,包含在源代码中的头文件也会被包含到该文件中。【展开预处理指令、将宏定义替换为对应值、加入头文件
  2. 编译阶段(Compilation Stage):在此阶段中,gcc将翻译C语言源代码为汇编语言程序。这个阶段中,源代码被翻译成汇编代码,但是还没有链接到目标文件。【源代码转为汇编代码(未链接目标文件)】
  3. 汇编阶段(Assembly Stage):在此阶段中,gcc将汇编代码转换为可重定位二进制目标文件。这个阶段中,汇编代码被转换成机器码,并且生成一个包含符号表和其他元数据的目标文件。【汇编代码转换为二进制目标文件,汇编代码被转为机器码
  4. 链接阶段(Linking Stage):在此阶段中,gcc将多个目标文件组合成单个可执行文件或库文件。这个阶段中,系统库和用户库的符号被解析,并将所有函数和变量链接起来,产生最终的可执行文件或库文件。【系统库和用户库的符号被解析,并将所有函数和变量链接起来,最终将多个目标文件组合成单个可执行文件或库文件

2、C++的封装继承多态(三大特性)

三大特性定义

  1. 继承:继承是指一个类可以从另一个类中继承属性和方法

    ​ 继承可以让代码更加灵活和可复用,因为一个派生类可以使用基类中的成员变量和成员函数,而不必重复编写同样的代码。在C++中,通过关键字"public"、"protected"和"private"来控制继承的访问权限。

  2. 封装:封装是指将数据和行为组合到一个单元中,并对外部世界隐藏其具体实现细节,只保留一些公共接口供其他对象进行访问

    ​ 封装可以防止外部对象直接访问对象内部的数据,从而保证程序的安全性和稳定性。在C++中,可以使用访问修饰符"public"、"protected"和"private"来控制成员变量和成员函数的访问权限。

  3. 多态:多态是指同一种操作作用于不同的对象上,可以有不同的解释和执行方式

    ​ 多态可以增加代码的可扩展性和可维护性,因为它可以允许我们在运行时动态地选择要调用的特定方法。在C++中,可以通过虚函数和纯虚函数实现多态,其中虚函数是在基类中定义的、可以被派生类覆盖的函数,而纯虚函数是没有具体实现的虚函数,在派生类中必须覆盖它。

    例子:

    每种交通工具使用时的操作方式和性能都不同。例如,私家车需要用油、刹车和油门等部件来操控;摩托车也需要上述部件以及手动变速器等;而自行车则需要踩脚踏板来进行推进。

    这个例子中,交通工具可以被看作是一个基类,而各种不同类型的交通工具则是派生类。每个交通工具都有其特定的行驶方法和性能,就像每个派生类都有其独特的行为和属性。当你在路上行驶时,无论你使用哪种交通工具,你都只需要关心如何进行加速、制动和转向等共同的操作,而不必关心底层实现细节。这就是多态性在生活中的应用。

    在 C++ 中,多态性就是指通过基类的指针或引用调用派生类的函数时,根据实际对象类型决定调用哪个版本的函数。因此,在上述的交通工具例子中,如果我们定义一个名为 Vehicle 的基类并声明其中一个虚函数 drive(),然后在派生类 CarMotorcycleBicycle 中分别重写这个虚函数,并创建一个 Vehicle * 指针指向不同类型的交通工具,就可以实现多态性。当你调用 drive() 函数时,程序会根据实际对象类型决定调用哪个版本的函数,从而实现不同类型的交通工具有不同的行驶方法和性能。

C++的继承有没有什么缺点

​ 1、父类和子类之间紧密耦合(子类的修改有可能会影响到父类)

​ 2、继承过深导致可维护性下降

​ 3、可能会导致多态性能下降。因为继承中如果有虚函数的话,那么运行时需要进行动态绑定,这会导致性能下降

继承是否破坏了封装?

先说结论:继承并不会直接破坏封装,因为可以通过访问权限来控制基类中的成员对派生类的可见性。但是如果派生类没有正确使用访问权限或者直接访问基类的私有成员,就可能会破坏基类的封装性。

  • 如果使用了public继承,则基类中public成员在派生类中仍然是public的,protected成员在派生类中变为protected的,private成员在派生类中仍然是不可访问的。
  • 如果使用了protected继承,则基类中public和protected成员在派生类中都变为protected的,private成员在派生类中仍然是不可访问的。
  • 如果使用了private继承,则基类中所有的成员在派生类中都变为private的,即全部成员在派生类之外不可见。

虚函数

定义

虚函数是一种特殊的成员函数,它允许派生类重写该函数以实现多态性

如果基类中的函数被声明为虚函数,则派生类可以覆盖该函数并提供自己的实现。在运行时,程序会根据实际对象类型来调用相应的函数,从而实现多态性。

虚函数的作用是在运行时决定调用哪个版本的函数,而不是在编译时决定。编译器会给每一个含有虚函数的类生成一个虚函数表,里面存储了该类中所有虚函数的地址,每个对象都有一个指向它所属类的 V-Table 指针,通过这个指针可以在运行时找到正确的虚函数地址。

正常函数与虚函数的区别

虚函数通过虚函数表进行动态绑定,使得在运行时能够根据对象类型动态选择调用哪个函数;

正常虚函数则在编译时就确定了调用哪个函数(静态绑定),因此不需要使用虚函数表进行动态查找。

无论是虚函数还是非虚函数,都是通过函数地址进行调用的

虚函数表指针的偏移量如何计算?如何真正找到我想访问的函数?(如何手动强行调用虚表中的函数?)
  1. 获取对象的指针。
  2. 访问该对象的虚函数表指针。
  3. 根据要调用的虚函数的索引,在虚函数表中获取相应的函数指针。
  4. 将函数指针转换为正确的函数类型。
  5. 通过函数指针调用虚函数。

手动调用虚函数表中的函数是一种非常危险的操作,容易引发许多问题,比如内存泄漏、未定义行为等

构造函数、析构函数可以是虚函数吗?

构造函数和析构函数可以是虚函数,但是不建议将其声明为虚函数

原因如下:

  • 将构造/析构函数声明为虚函数也无法实现多态性。因为其调用顺序是固定的,不能通过基类指针或引用来调用派生类的函数

  • 构造/析构函数被应用于对象的创建和销毁过程中,此时对象的状态可能不稳定,使用虚函数可能会导致错误

析构为什么要声明为virtual

如果一个类带有虚函数(具有多态性质的类),则必须为其声明虚析构函数,确保调用 delete 时能够正确地删除对象。

如果没有声明虚析构函数,则在使用 delete 时可能会导致内存泄漏。(只调用基类的析构函数,而不调用派生类的析构函数)

3、STL常用容器

vector

底层实现

中分配了一段连续的内存空间来存放元素(vector内部维护了一个指向存储数据的连续内存空间的指针)

扩容机制

如果当前的vector已经满了,在新增数据时就要分配一块更大的内存,将原来的数据复制到新内存中并释放原始内存块,然后再把新数据插入

对vector的任何操作,一旦引起空间重置,那么指向原有vector的所有迭代器都会失效

size() 和 capacity()

  • 堆中分配内存,元素连续存放,内存空间只会增长不会减少
    • vector有两个函数,一个是capacity(),在不分配新内存下最多可以保存的元素个数,另一个size(),返回当前已经存储数据的个数
  • 对于vector来说,capacity是永远大于等于size的
    • capacity和size相等时,vector就会扩容,capacity变大 (翻倍)

vector 扩容的方式有两种:倍增加法

倍增是指每次扩容都将 vector 容量乘以 2,而加法则是每次扩容增加一个固定的值,例如 10 或 20。

一般来说,可以考虑将容量选择为当前 vector 大小的两倍,这样可以减少扩容次数,同时又不会浪费太多内存。

resize()和reserve()

resize()改变当前容器内含有的元素的数量【对应size(),500ml瓶子里的水】

reserve()改变当前容器的最大容量【对应capacity(),500ml瓶子换成600ml】

list

std::list是一个环状双向链表容器(其结点与list本身是分开设计的),它的每个元素(节点)都是单独分配内存的。其内部实现方式可以确保不同元素在内存中的位置不相邻,然后我们可以通过指针来访问对应数据。

list的迭代器

有以下特点:

1、因为list是双向链表,所以其迭代器也是双向的,可向前向后遍历链表

2、虽然list不支持随机访问,但是list的迭代器支持"++"和"--"等操作(但是不支持"+="和"-="),通过这些操作可以实现类似随机访问的效果。

vector和list的区别

它们的主要区别在于底层数据结构不同,因此它们在插入、删除和随机访问元素时的性能表现有所不同。

具体来说,vector底层使用的是动态数组,它可以在尾部高效地添加和删除元素,支持随机访问,但在中间或头部插入/删除元素时会比较耗费时间。这是因为需要将被插入/删除元素后面的所有元素向前/后移动,以便腾出空间,这个操作的时间复杂度是O(n)。

相比之下,list底层使用的是双向链表,虽然不支持随机访问,但在中间或头部插入/删除元素时非常高效,只需要更新相邻元素的指针即可,不需要像vector一样移动其他元素。这个操作的时间复杂度是O(1)。

如果需要频繁进行随机访问,而且元素数量不是很大,那么vector可能更适合;如果需要频繁进行元素插入/删除操作,并且元素数量较大,那么list可能更适合。

具体:

1.vector底层实现是数组; list是双向链表;

2.vector是顺序内存,支持随机访问,list不行;

3.vector在中间节点进行插入删除会导致内存拷贝,list不会;

4.vector一次性分配好内存,不够时才进行翻倍扩容;list每次插入新节点都会进行内存申请;

5.vector随机访问性能好,插入删除性能差;list随机访问性能差,插入删除性能好.

deque(双端数组)

deque(双端队列)提供了与vector相似的接口,但是更加适用于头尾操作。

deque可以在常数时间内对头部和尾部进行插入和删除操作,因此它非常适合需要频繁在头尾插入和删除元素的场景。

deque还提供了随机访问迭代器,使得可以快速访问其中的任何元素。与vector不同,deque的存储空间不必在一个连续的块中分配,这意味着deque可以更好地处理大型数据集,而不会因为连续内存分配失败而崩溃。

deque由一段一段的定量连续空间构成,这使得其迭代器别的复杂

栈与队列(stack&queue)

Stack(栈)是一个后进先出(LIFO)的数据结构,而Queue(队列)是一个先进先出(FIFO)的数据结构

栈与队列底层架构为deque,是deque的配接器(Adapter)

堆是一种动态分配内存的数据结构,它与栈不同,堆的内存空间可以在程序运行过程中随时申请和释放。堆中的每个元素都有一个键(key),该键决定了元素在堆中的位置。

堆是建立在完全二叉树这种结构上的,通常分为大根堆和小根堆两种(堆一般用于实现优先队列动态数组等数据结构)

在堆中,为什么是以两倍的方式扩容而不是三倍四倍,或者其他方式呢?

这是因为它可以更高效地利用内存空间减少内存分配和释放操作的次数,并且可以更快地达到目标大小从而减少扩容的次数。

这种方式也使得数组的大小始终都是2的幂次方,便于计算数组下标,提高代码效率。

相比之下,采用三倍或四倍方式扩容,则可能导致浪费更多的内存空间,增加内存分配和释放操作的数量,降低程序性能。

map&set

map和set都是C++ STL中的关联容器,其中map用于存储键-值对,而set则仅存储值

共同点:
都是C++的关联容器,只是通过它提供的接口对里面的元素进行访问,底层都是采用红黑树实现。
不同点:
set: 用来判断某一个元素是不是在一个组里面
map: 映射,相当于字典,把一个值映射成另一个值,可以创建字典
优点:
【map】有序性是map结构的最大优点,其查找某一个数的时间为O(logn);

​ 【set】自动去重,支持按照元素大小排序

共同的缺点:
每次插入值的时候,都需要调整红黑树,对效率有一定影响

为什么插入数据(insert)后,之前保存的iterator不会失效?

map 和 set 存储的是节点,不需要内存拷贝和移动。而 vector 在插入数据时可能会重新开辟内存,其iterator指向内存的某个位置,而不是节点指针,Map和Set的iterator指向节点指针。

为何map和set的插入删除效率比其他序列容器高?

因为 map 和 set 底部使用红黑树实现,插入和删除的时间复杂度是 O(ogn),而向 vector 这样的序列容器插入和删除的时间复杂度是 O(N)

4、C++11常用特性(智能指针等)

智能指针

智能指针是一种资源管理类。常见的有:unique_ptr(独占式,一个unique_ptr对象只能搭配一个具体对象使用,不能被复制或共享)、shared_ptr(共享式,多个对象共享一个资源,采用引用计数机制来管理对象的生命周期)、weak_ptr(用来避免shared_ptr循环引用导致的内存泄漏)

定义unique_ptr时需要将其绑定到一个new返回的指针上

unique_ptr不支持普通拷贝和赋值,因为其拥有指向的对象;

shared ptr的实现机制是在拷贝构造时使用同一份引用计数

同一个shared ptr被多个线程"读"是安全的,被多个线程"写"是不安全的;

类型推导

auto

auto关键字可以用于自动推导变量的类型。使用auto定义的变量会根据其所赋的值的类型来推导出变量的类型

使用auto定义的变量必须在定义时进行初始化,否则编译器将无法推导出变量的类型

除此之外还有以下规则:

  • auto无法推导出模板参数,不能定义数组,可以定义指针
  • auto只能用于自动推导函数内部定义的变量,而不能用于类的成员变量(因为类成员变量的大小布局必须在编译期间确定,而auto作用于运行时期)
decltype

​ decltype是用来推导表达式类型的关键字。它可以在不执行代码的情况下,根据表达式返回值的类型推断出整个表达式的类型

好的,下面举一个简单的例子:

#include <iostream>
using namespace std;

int main() {
    int x = 1, y = 2;
    decltype(x + y) z; // 推导x + y的类型并定义z
    z = x + y;
    cout << "z的值为:" << z << endl;
    return 0;
}

在这个例子中,我们使用“decltype(x+y)”来推导x+y表达式的类型,并将其赋给变量z。由于x和y都是int类型,所以x+y的类型也是int,于是z就被定义成了int类型。最后输出z的值为3。

需要注意的是,decltype并不会执行表达式,它只是推导类型。因此如果表达式中有函数调用等会产生副作用的操作,这些操作并不会被执行。此外,如果表达式中包含未定义的变量,编译器会报错。

左值、右值

​ 在编程语言中,一个表达式的值可以被分为左值和右值。左值指可被取地址的表达式,通常是一个变量或者对象;而右值则指不能被取地址的表达式,通常是字面常量或者表达式计算后的结果,例如 1+2。

int a = 1; // a 是左值,可以被取地址

int b = a; // a 是右值,不能被取地址

简单来说,左值就是“可以放在等号左边”的值,右值就是“只能放在等号右边”的值。

左值可以出现在赋值操作符、引用操作符以及取地址操作符等需要使用内存地址的地方,而右值仅能用于运算和比较等操作中

右值引用

​ 是 C++11 中引入的一种引用类型,使用 && 符号来声明,它可以绑定到右值(即不能被修改的对象,如临时对象、字面量等)上。

与左值引用相对应,右值引用通常用于实现移动语义完美转发

移动语义

​ 指的是将资源所有权从一个对象转移到另一个对象而不进行深拷贝,这可以提高程序的性能。

浅拷贝:

​ a和b的指针指向了同一块内存,就是浅拷贝,只是数据的简单赋值;

深拷贝:

​ 深拷贝就是在拷贝对象时,如果被拷贝对象内部还有指针引用指向其它资源,自己需要重新开辟一块新内存存储资源

完美转发

​ 则是指在函数调用中以原封不动的方式传递参数,使得被调用函数能够接收和处理原始参数。

左值引用

​ 举个例子就知道了,C++中的引用就是是左值引用

左值引用是指通过使用 & 符号声明的引用类型,它可以绑定到左值(即具有内存地址的对象),并且可以被修改。与之相对的是右值引用,它是使用 && 符号声明的引用类型,可以绑定到右值(即不能被修改的对象,如临时对象、字面量等)。

nullptr

C++11引入nullptr关键字来区分空指针和0。nullptr的类型为nullptr_t,能够转换为任何指针或成员指针的类型,也可以进行相等或不等的比较

可以简单理解为nullptr代表空指针,NULL直接等于0。在C11中建议用nullptr代替NULL

范围for循环

其实就是增强for,例如

#include <stdio.h>

int main(){
    int arr[] = {10, 20, 30, 40, 50};
    // 遍历整个数组
    for(int x : arr) {
        printf("%d ", x);
    }  
    return 0;
}

列表初始化

引用LRU题解里面的LRUCache初始化部分

class LRUCache {
    struct ListNode{//定义节点结构体
        int key;
        int val;
        ListNode* next;
        ListNode* pre;
    };
    ListNode* dummy;
    int maxSize;//最大缓存数量
    int nodeNums;//当前缓存中的节点数量
    //定义哈希表,key是int,val是节点
    unordered_map<int, ListNode*> hash;
    
public:
    LRUCache(int capacity): maxSize(capacity), dummy(new ListNode){//不用参数列表也行
        nodeNums = 0;
        //dummy的 next 和 prev 指针都指向自身,这样当缓存为空时,dummy既是头节点也是尾节点
        dummy->next = dummy;
        dummy->pre = dummy;
    }
    ...
};

这里在初始化LRUCache类时使用了列表初始化的方式

others

原子操作允许程序原子性地读取和修改共享数据,从而避免了竞态条件和数据竞争。

线程库提供了创建和管理线程的接口,使得多任务编程和并行计算变得简单。

互斥量和条件变量允许线程之间以安全的方式同步和通信。

future/promise机制则提供了一种方便的方法来异步处理结果。