C++ 面试题
1. C++ 中值传递和引用传递的区别?
-
值传递:在函数调用时,会触发一次拷贝动作,所以对参数的修改不会影响原始的值。
-
引用传递:在函数调用时,不会触发拷贝动作,但是对参数的修改也会影响到原始的值。
2. C 和 C++ 的区别?
-
面向对象&面向过程
-
C语言是一门面向过程的语言,以函数为中心,将问题分解为一系列步骤,通过调用函数来解决问题。关注程序的执行流程,强调怎么做。
-
C++是一门既面向对象又面向过程的语言,主要支持面向对象,以对象为中心,将问题分解为一系列相互交互的对象,每个对象中包含数据和操作数据的方法。强调谁来做。
-
-
函数重载
-
C语言不支持函数重载,函数名必须唯一才行。
-
C++支持函数重载主要通过参数类型和参数个数。
-
💡 补充:
函数重载基本规则:
- 函数名必须相同
- 参数列表必须不同
- 参数类型不同
- 参数个数不同
- 参数顺序不同(仅当类型不同时有效)
- 返回值不同并不能区分重载
函数重载的原理:
主要是通过函数名修饰规则,每个编译器都会有自己的函数名修饰规则
C语言中函数名修饰规则,直接使用定义或声明的函数名,在链接时,编译器会直接使用函数名寻找函数地址
C++中函数名修饰规则(以 Gcc 为例),_Z + 函数名长度 + 函数名 + 类型首字母。
因此,这就是为什么C语言不支持函数重载,而C++支持,以及为什么返回值不能区分重载
-
模版
- C语言不支持模版编程
- C++支持模版,支持静态模版和动态形式的多态
-
内存管理
- C语言使用 malloc 和 free 来申请和释放内存
- C++由于对象的出现,使用 new 和 delete 操作符来管理内存,也支持使用智能指针来管理动态内存
💡 补充:
malloc/free 和 new/delete 的区别:
- malloc/free 是函数,new/delete是运算符
- malloc申请的空间不会初始化,new申请空间可以初始化
- 对于自定义类型,malloc/free不会调用构造和析构函数,new/delete 会调用构造函数析构函数
- malloc失败返回NULL,new失败抛异常
- malloc申请空间需要手动计算大小,new T\[\]中指定个数即可
- malloc返回 void* 需要类型转换,new指定类型,无需类型转换
-
标准库
- C语言没有标准库和算法的支持,需要自己造轮子
- C++有 STL,比如 vector、string、list、map等,以及算法库 algorithm,比如sort、find、reverse等
3. C++中的左值和右值?有什么区别?
- 左值:可以出现在赋值运算符的左边,并且可以被取地址,通常是有名字的变量
- 右值:不能出现在赋值运算符的左边,不可以被取地址,通常是字面量、常量、临时变量
4. 什么是引用折叠和万能引用
在 C++ 中,不能直接声明引用的引用 int && & x 是非法语法,但是,在模版函数中会间接出现。
cpp
template<typename T>
void f1(T &x) {}
template<typename T>
void f2(T &&x) {}
引用折叠:只要其中有一个是左值引用,则最终结果就是左值引用。只有当两者都是右值引用时,结果才是右值引用。
万能引用:是在函数模版中使用 T&& 这种写法,配合引用折叠的推导规则,让函数既能绑定左值作者又能绑定右值。
- 如果实参是左值时,T会被推导为左值引用类型,发生引用折叠。
- 如果实参是右值时,T会被推导为非引用类型T,没有发生引用折叠。
cpp
template<typename T>
void f2(T &&x) {}
5. 什么是 C++ 的移动语义和完美转发?
移动语义和完美转移都是 C++11 引入的新特性。
核心思想是:与其费力的拷贝一个即将消亡的对象资源,不如直接将其资源移动过来,从而避免高昂的拷贝操作。
移动语义是搭配移动构造或移动赋值来使用。而移动构造和移动赋值的参数都需要是右值引用,在函数内部使用交换(swap)操作,从而避免拷贝。
完美转发要解决的核心问题是,在编写模版函数时,保持参数原有的值类别(左值、右值)的属性不变,继续向下传递。
当一个实参是右值传递给形参右值引用时,右值引用形参本质上也是一个有名字的变量,所以它的属性属于左值,在传递或使用时,会被当成左值处理。因此需要完美转发继续保持右值的属性。
💡 补充:
什么场景下需要用到移动构造和移动赋值?
- 当函数返回一个对象时(将亡值),用移动构造函数可以避免返回值拷贝
- 当需要一个大对象从一个容器移动到另一个容器时,用移动赋值可以避免资源的拷贝
6. C++ 中 move 有什么作用?它的原理是什么?
std::move 的作用是,无论输入参数是左值还是右值,都被强制转成右值,这样可以触发移动构造或移动赋值来转移该对象的资源,而不是拷贝。
而 std::move 后的对象能否使用取决于移动构造和移动赋值的实现,如果在移动构造或移动赋值中,并没有使用 swap 交换资源操作,而还是使用拷贝动作,那么原对象还是可以使用的。
cpp
// move
template <class T>
LIBC_INLINE constexpr cpp::remove_reference_t<T> &&move(T &&t) {
return static_cast<typename cpp::remove_reference_t<T> &&>(t);
}
7. 什么是 RAII ?
RAII(Resource Acquisition Is Initialization)资源获取即初始化,核心思想是利用对象的生命周期来管理获取的动态资源(内存、文件指针、网络连接、互斥锁),从而避免资源泄漏。
- 资源获取在构造函数中完成
- 资源释放在析构函数中完成
这样,即使程序在执行过程中抛出异常或多路径返回,也能确保资源得到最终正确的释放,可以避免内存泄漏。
8. 介绍一下 C++ 中三种智能指针的使用场景
C++11 中主要引入了三种智能指针,分别是 std::unique_ptr、std::shared_ptr、std::weak_ptr。
-
std::unique_ptr是一种独占所有权的智能指针,意味着同一时间内只能有一个unique_ptr指向特定对象。不支持拷贝构造,支持移动。 -
std::shared_ptr是一种共享所有权的智能指针,多个shared_ptr可以指向同一个对象,内部使用引用计数来确保当最后一个shared_ptr被销毁时,对象才会被销毁。支持拷贝构造。 -
std::weak_ptr是一种不拥有对象所有权的智能指针,它指向一个由shared_ptr所管理的对象,weak_ptr主要解决shared_ptr之间的循环引用问题。- 循环引用:当两个对象相互持有
shared_ptr时,并且导致引用计数无法减为0,此时造成循环引用问题,通过将shared_ptr换成weak_ptr来解决
- 循环引用:当两个对象相互持有
9. C++11 中有哪些常用的新特性?
-
auto类型推导,可以让编译器在编译时推导出等号右侧表达式的类型,在一些复杂类型中使用,比如某个容器的迭代器,lambda表达式类型等。 -
initializer_list列表初始化,允许使用 {} 来构造对象/变量或传递参数,对于自定义类型,需要支持列表初始化的构造函数。 -
智能指针。
-
std::thread以及 RAII 风格的锁std::lock_guard和std::unique_lock。 -
移动构造和移动赋值。
-
std::function和std::bind。std::function是一个类模版,可以包装任何的可调用对象,包括函数指针、仿函数、lambda、bind等std::bind是一个通用的函数适配器,它接受一个可调用对象及其部分或全部参数,并返回一个新的可调用对象,当你调用这个新对象,它会用你预先绑定好的参数去调用原始的可调用对象
-
lambda是一个匿名函数对象,在调用的地方就地定义函数,而无需单独命名和定义函数。
10. C++ 中 static 的作用?
-
修饰局部变量:当
static修饰局部变量时,这个变量的存储位置会在静态区中保存,不会随着函数的局部作用于的结束而销毁,且该变量只会初始化一次。 -
修饰全局变量或函数:当
static修饰全局变量或函数时,限制了这些变量和函数的作用域,他们只能文件内部访问,不能跨文件访问,避免在不同文件中命名冲突。 -
修饰类的成员变量或函数:在类内部,
static修饰的成员变量和函数属于类本身,而不是属于类的任何实例对象,这意味着该类所有对象共享一个static成员变量,无需每个对象都存储一份拷贝。static成员函数可以在没有类实例的情况使用类名调用,并且static成员函数没有this指针。
11. C++ 中 const 的作用?
const 最主要的作用是声明一个常量,但 const 不仅可以用作普通常量,还可用作指针、引用、成员函数等。
-
const修饰普通常量。 -
const修饰引用,一般用于函数参数,表示函数不会修改传递的参数值。 -
const修饰指针,分为三种情况。-
const int* ptr指向常量的指针,不能通过解引用修改指向的内容,但可以修改指针的指向 -
int * const ptr指针常量,不能修改指针的指向,但是可以修改指针指向的内容 -
const int* const ptr指向常量的常量指针,不能修改指针指向,也不能修改指向的内容
-
-
const修改类成员函数,表示该函数不会修改类的任何成员变量,除非这些成员变量被声明为mutable,本质就是const obj * const this
12 C++ 中 define 和 const 的区别?
如果单纯从定义常量角度,优先使用 const。
#define 是一个预处理指针,用于定义宏,在预处理阶段进行文本替换,没有类型检查。
const 是一个关键字,用于定义常量,在编译时确定类型和值。
- 类型安全:
-
#define没有类型检查,单纯是替换,可能会有意向不到的错误。 -
const是强类型,编译器会对其进行类型检查。
- 作用域:
-
#define定义的宏在整个源文件中有效,直到被undef。 -
const则遵循 C++ 的作用域规则,仅在定义的作用域内中有效。
13. C++ 中 inline 的作用?
inline 的作用是建议编译器在调用地方展开内联函数,以减少函数调用的开销。对于频繁调用的短小函数可以在一定程度上提升程序的效率。内联函数主要替换宏函数,宏函数在不加括号时,可能有意想不到的错误。
-
内联是一种建议,编译器可以选择忽略
inline关键字。 -
类中的函数默认是内联函数。
-
内联函数不能进行声明和定义的分离,会报链接错误。
14. struct 和 class 的区别?
在C语言中,struct 仅能用于定义变量,而在C++中,struct被扩展为类,不仅可以包含成员变量,也可以包含成员函数。
主要区别:
-
class默认访问限定符private,struct默认访问限定符public(兼容C)。 -
struct默认继承时是public继承,class默认继承时priavte继承。
15. 什么是内存对齐?
内存对齐是计算机系统中一项重要的优化机制,它确保数据在内存的存储位置符合特定的要求,以提高访问效率。
基本规则:
-
第一个成员偏移量为0
-
后续成员偏移量对齐到 对齐数 的整数倍地址处。
- 对齐数:min(编译器默认对齐数 ,该成员大小)
-
结构体总大小为 最大对齐数 的整数倍地址处。
- 最大对齐数:min(结构体中变量类型最大者,编译器默认对齐数)
-
嵌套结构体的情况:嵌套结构对齐到自己的最大对齐数的整数倍处,结构体的整体大小就是 所有最大对齐数(含嵌套结构体的对齐数) 的整数倍。
- 嵌套结构体的最大对齐数:min(结构体中变量类型最大者,编译器默认对齐数,嵌套结构体的最大对齐数)
为什么要内存对齐?
-
提高效率:对齐的数据,可以让CPU减少内存的访问次数。
-
硬件限制:一些硬件架构要求比较对齐,使数据存储的起始地址可以整除数据实际占据内存的字节数。
16. C++ 中 sizeof 和 strlen 的区别?
-
sizeof是一个运算符,用于获取一个类型或一个对象的大小(以字节为单位),sizeof在编译时计算结果。 -
strlen是一个库函数,用于计算C风格的字符串长度(不包含\0),strlen在运行时计算结果,需要遍历字符串内容。
💡 补充:
sizeof对于指针,返回的是指针本身的大小,通常是4字节或8字节,取决于系统架构是32位还是64位。
17. C++ 中 explicit 的作用?
explicit 主要是防止单参数的构造函数被隐式类型转换,如果没有 explicit 关键字,Object obj = 10 这样的代码是可以编译通过的。
18. C++ 中 final 关键字的作用?
final 关键字在 C++11 中引入,主要用于防止类被继承或防止虚函数被重写。
-
防止类被继承:当一个类被声明为
final,则这个类不能被继承。 -
防止虚函数重写:当一个虚函数被声明为
final,则这个虚函数在派生类中不能被重写。
19. C++ 中的四种类型转换?
-
static_cast最常用的显示转换,使用场景:- 基本数据类型之间的转换
void*指针转换为具体类型的指针- 类层次结构中的上行转换(派生类转换为基类)
-
reinterpret_cast用于在两种不相关类型之间进行转换,其本质是对原始数据的底层位模式进行重新解释,也就是说转换后对原有内存的访问解释已经完全改变了。使用场景:- 任意指针类型之间的转换
- 指针和整数之间的转换
-
const_cast用于 const 类型到非 const 类型,去掉 const 属性。 -
dynamic_cast用于将基类的指针或者引用安全的转换为派生类的指针或引用。使用场景:- 安全的下行转换(基类 -> 派生类)
💡 补充:
如果基类的指针或者引⽤时指向派⽣类对象的,则转换回派⽣类指针或者引⽤时可以成功的,如果基类的指针指向基类对象,则转换失败返回nullptr,如果基类引⽤指向基类对象,则转换失败,抛出bad_cast异常。
其次dynamic_cast要求基类必须是多态类型,也就是基类中必须有虚函数。因为dynamic_cast是运⾏时通过虚表中存储的type_info判断基类指针指向的是基类对象还是派⽣类对象。
20. C++ 中 volatile 关键字的作用
在 C++ 中,volatile 关键字的作用是防止编译器对其变量进行优化,确保每次读写都是直接从内存中操作,而不是使用寄存器的值。
21. 什么是多态?
多态是面向对象的三大特征之一,指的是一个接口可以有多个不同的实现。
在 C++ 中,多态主要通过继承机制和虚函数实现。通过基类指针或者引用调用子类重写的虚函数。
重写:子类的虚函数与基类虚函数的返回值类型、函数名称、参数列表完全相同。
💡 补充:
在 C++ 中,多态分为静态多态和动态多态。静态多态通过函数重载和模版实现,而多态则是通过虚函数实现的。
- 静态多态:函数在编译时确定调用哪个函数,这种方式提高了效率,但是灵活性差
- 动态多态:函数在运行时确定调用哪个函数,则中方式更灵活,但是效率比较低
22. 多态的原理?
虚函数表机制
-
虚函数表:每个包含虚函数的类都有一个虚函数表,这是一个函数指针数组,存放该类所有的虚函数地址(重写的虚函数地址或从基类继承下来的虚函数)。
-
虚函数表指针:每个拥有虚函数表的对象在内存布局中包含一个隐藏的指针成员,指向该类的虚函数表。这个指针通常在内存布局的开始位置。
- 基类对象的虚函数表存放基类虚函数的地址。同类型的对象共用一个虚表,所以基类和派生类都有各自独立的虚函数表,虚函数指针指向的是不同的虚函数表
- 当基类包含虚函数时,即使派生类没有重写这些虚函数,派生类的虚函数表中仍会包含基类虚函数的地址。如果派生类还定义了新的虚函数,这些额外的虚函数也会被添加到派生类的虚函数表中。
- 当派生类重写了基类的虚函数,派生类的虚函数表中对应的虚函数就会被覆盖成派生类重写的虚函数地址
- 派生类的虚函数表中包含:1.基类的虚函数地址 2.派生类重写的虚函数地址完成覆盖 3.派生类自己的虚函数地址三个部分
多态的调用过程:
- 通过基类指针或引用找到该对象的虚函数表指针
- 在通过虚函数表指针找到指向的虚函数表
- 查找虚函数表找到对应的函数地址
- 调用该地址指向的函数
虚函数的调用比普通函数调用多了一个 vtable 查找过程,运行时略有开销。
💡 补充:
构造函数、静态成员函数和友元函数不能是虚函数。
析构函数可以是虚函数。
23. C++ 中析构函数一定要是虚函数吗?
C++ 中析构函数并一定要是虚函数,但是在多态条件下,一定建议声明其为虚函数。
当基类的析构函数声明为 virtual 关键字,此时派生的类析构函数只要定义,则派生类的析构函数与基类析构函数构造重写,编译器会对析构函数名称做了特殊处理,统一处理为 destructor。
析构函数声明为虚函数,主要为了解决当基类指针或引用指向派生类对象时,析构函数资源释放问题。
多态条件下:
- 析构函数不构成多态时:只会调用基类的析构函数,会导致派生类的析构函数不会被调用,从而引发内存泄漏
- 析构函数构成多态时:编译器会先调用派生类的析构函数,随后自动调用基类的析构函数
24. C++ 中重载/重写/隐藏的区别?
-
重载
- 两个函数在同一个作用域中
- 函数名相同,参数不同(参数的类型、个数、顺序)。与返回值无关
-
重写/覆盖
- 两个函数都是虚函数,分别在继承体系中的父类和子类的作用域中
- 函数名、参数、返回值必须相同,协变除外
-
隐藏
- 函数名/变量名相同,分别在继承体系中的父类和子类作用域中
- 两个函数只要不构成重写,就是隐藏
💡 补充:
协变:派生类重写基类虚函数时,与基类虚函数返回值类型可以不同。
- 即基类虚函数返回基类对象的指针或引用,派⽣类虚函数返回派⽣类对象的指针或者引⽤时,称为协变。
25. C++ 中的继承?什么是虚继承?
继承是使代码复用的重要手段,在原有类的基础上进行扩展,增加新的功能,这样产生的新类,被称为派生类。
虚继承主要用于解决菱形继承问题
菱形继承问题:假设有四个类A、B、C和D,其中B和C都继承自A,而D又同时继承自B和C。这样就形成一个菱形结构。若不使用虚继承,D类中会包含两个独立的A类对象,导致菱形继承存在二义性和数据冗余的问题。
- 解决二义性:通过指定类域的方式解决
- 解决数据冗余:通过虚继承的方式解决
虚继承是怎么解决的?
将A对象只在D对象中存储一份,而B和C对象中不存储A对象,而是存储一个虚基表指针,指向一个虚基表,在虚基表中存储与A对象地址的偏移量,通过对应虚基表中存储的偏移量访问A对象中的成员,解决数据存两份的问题。
在 C++ 中处理菱形继承问题时,virtual 关键字需要加在中间派生类继承基类的地方。
💡 补充:
基类定义了 static 静态成员,则整个继承体系里面只有一个这样的成员。无论派生出多少个子类,都只有一个 static 成员实例。
虚继承中:通过B的对象模型,发现菱形虚拟继承中B和C的对象模型跟D保持⼀致的⽅式去存储管理A对象,这样当B的指针访问A时,⽆论B指针切⽚指向D对象,还是B指针直接指向B对象,访问A成员都是通过虚基表指针的⽅式查找到A成员再访问。
26. C++ 中继承和组合的区别?
继承是一种 is-a 的关系,每个派生类都是一个基类。继承使派生类和基类之间的依赖关系很强,耦合度高(白箱复用)。
组合是一种 has-a 的关系,即 B 对象中有一个 A 对象。组合之间没有很强的依赖关系,耦合度低(黑箱复用)。
27. 什么是 C++ 的运算符重载?
通过运算符重载,我们可以定义对象/自定义类型如何使用运算符(+、-、*、/)等。
运算符重载由 operator + 运算符共同组成,具有形参和返回值。
- 运算符重载函数的参数应与该运算符原本的操作数量保持一致
- 当作为类的成员函数实现时,则左侧运算对象默认传给隐式的this指针,因此参数会比全局重载的运算符函数少一个
注意点:
- 只能重载已有的运算符,不能创建新的运算符
- 不能改变运算符的优先级、结合性和内在的语义
.*、::、?:、sizeof、.以上运算符不能被重载- 区分前置++和后置++时,C++规定,后置++需要添加一个int形参,来与前置++构成函数重载
28. C++ 中 using 和 typedef 的区别?
using 在 C++11 中引入,using 和 typedef 都可以为类型定义一个新的别名。
主要区别在于,using 可以定义模版别名,而 typedef 不能。并且 using 定义时语法更直观。
29. C++ 中 enum 和 enum class 的区别?
在 C++ 中,enum 和 enum class (强枚举类型)主要的区别在于作用域和类型安全。
-
作用域:
enum:枚举成员直接可以使用,不需要枚举类型的前缀enum class:枚举成员只能显式的指定枚举类型来使用
-
类型安全:
enum:传统枚举类型不安全,枚举成员会隐式转换为整数类型enum class:强枚举类型是类型安全的,不能隐式的转换为其他类型,必须显示转换
30. C++ 中 new 和 malloc 的区别?delete 和 free 的区别?
-
newvsmalloc:new是操作符,而malloc是函数new分配内存并调用构造函数,而malloc仅仅分配内存,不调用构造函数new可以指定类型,而malloc返回void*,需要显示的类型转换new分配失败抛出std::bad_alloc异常,而malloc返回NULL
-
deletevsfree:delete是操作符,而free是函数delete释放内存并调用析构函数,而free仅仅释放内存,不调用析构函数delete必须和new配对使用,而free必须与malloc配对使用delete和delete[]是不同的,前者用于单一对象,后者用于数组。free没有这种区分
31. C++ 中类定义中 delete 和 default 关键字作用?
delete 关键字用来禁用某些默认的成员函数,主要作用是禁用拷贝构造函数和拷贝赋值重载。
default 关键字用于显式的指示编译器为类的默认成员函数,生成默认的实现,经常使用在生成默认构造和默认析构函数上。
32. C++ 中 this 指针的作用?
this 指针是一个隐含在每个非静态成员函数中的参数,它指向的是调用该成员函数的对象的地址,主要作用包括:
- 访问类的成员变量和成员函数,特别是当局部变量和成员变量同名时,用
this指针可以作出区分 - 可以通过返回
*this指针来支持链式调用 - 在基类指针或引用调用派生类对象时,利用
this指针也可以构造多态
33. C++ 中 vector 的原理?resize 和 reserve 的区别?
vector 底层是一个顺序表,由 start、finish、end_of_storage 三个指针,分别指向不同的位置。
start:指向数组的首元素finish:指向最后一个元素的下一个位置end_of_storage:指向分配内存的末尾
resize 指的是 finish 的位置,reserve 指的是 end_of_storage 的位置。
-
resize(n):调整vector的大小为n(也就是调整的是finish指针)n大于当前大小,会向vector末尾添加初始化的新元素n小于当前大小,会删除超出部分的元素n大于capacity会自动扩容,满足容量需求(调整finish和end_of_storage)
-
reserve(n):预分配内存,确保vector可以存储n个元素(调整的是end_of_storage)
34. C++ 中的迭代器失效是什么?解决方案是什么?
C++ 中的迭代器失效指的是容器(vector、list等)由于修改操作(插入、删除等),导致之前获取的迭代器不再指向预期的元素,甚至引发未定义的行为。
迭代器失效的场景:
-
vector插入元素时,底层空间扩容时,导致所有迭代器失效。insert:在未扩容时,插入位置及其之后的迭代器失效,因为元素被向后移动。解决方案:insert返回新插入位置的迭代器erase:被删除位置及其之后的迭代器失效,因为元素被向前移动。解决方案:erase返回删除元素的下一个位置迭代器
-
list由于底层是链表,是一个一个节点,所以插入时不会有迭代器失效问题。list迭代器失效主要发生在元素被删除时。erase:被删除位置的迭代器失效。解决方案:erase返回删除元素的下一个位置迭代器
35. C++ 中 deque 的原理?
deque 是 C++ 容器库中的双端队列,主要用于实现适配器(栈、队列),它允许在队列的两端高效的插入和删除元素。
deque 支持随机访问,可以通过下标访问元素,deque 内部使用分段的连续空间组合而成,通过中控(指针数组)来管理多个内存块。
map:中控数组(指针数组)存储指向各个缓冲区(buffer)的指针buffer:实际存储元素的连续内存块(存储元素的个数通常是固定的)__deque_iterator:deque 的迭代器,包含四个指针(first、last、cur、node),分别指向缓冲区开始、缓冲区结尾、当前元素、以及回指中控数组的二级指针
当 deque 扩容时,不需要移动元素,只需分配新的 buffer,更新中控数组即可,因为每个缓冲区都是独立分布的。
deque 的随机访问能力不如 vector,中间元素的插入删除不如 list。
35. C++ 中 map 和 unordered_map 的区别?
-
底层实现:
map:基于有序的红黑树unordered_map:基于无序的哈希表
-
时间复杂度:
map:插入、删除、查找的时间复杂度时 O(log n)unordered_map:插入、删除、查找的时间复杂为 O(1)
-
内存使用:红黑树相比于哈希表内存使用的比较小。
-
迭代器稳定性:
map是树形结构,插入后迭代器有效,而unordered_map底层使用到了顺序表,扩容时,导致迭代器失效。 -
运算符重载:
map要求 key 支持小于和大于运算符unordered_map要求 key 支持等于以及取模操作
36. C++ 中 vector 和 list 的区别?
vector 的底层是顺序表,list 的底层是双向循环链表。
-
存储空间:
vector物理上是连续的list逻辑上连续,但物理上不连续
-
随机访问:
vector支持随机访问 O(1)list不支持,需要遍历 O(N)
-
任意位置的插入删除:
vector需要移动元素 O(N)list只需要改变指针的指向 O(1)
-
内存使用率:
vector只存储元素本身,无额外指针开销,内存利用率高list每个元素需额外存储前后节点的指针,内存开销更大
37. C++ 中 lock_guard 和 unique_lock 的区别?
两者都是 RAII 风格的锁管理类,用于管理互斥锁。
-
lock_guard是一个简单轻量级的锁管理类,在构造时锁定互斥锁,析构时释放互斥锁。 -
unique_lock提供了更复杂和灵活的功能。它允许显式的锁定和解锁、延迟锁定操作,还支持锁的所有权转移。条件变量只能使用unique_lock。
38. C++ 中 thread 的 join 和 detach 的区别?
join 和 detach 是 std::thread 的成员方法:
join:阻塞当前调用的线程,等待子线程完成。detach:将子线程从调用线程中分离出来,子线程在后台独立执行,不会阻塞当前调用线程。
简单说,join 是一种同步机制,保证子线程完成后主线程再继续,通常使用在需要关心子线程的执行结果,或只能确保子线程执行完后,主线程才能执行的场景。detach 则是不关心子线程的调用结果。
39. C++ 中 memcpy 和 memmove 的区别?
两者是 C 风格的内存拷贝函数,但他们的主要区别在于处理内存重叠区域的能力。
memcpy:用于从源地址复制指定数量的字节到目标地址。如果源地址和目标地址产生重叠,行为是为定义的,因为memcpy不处理重叠。memmove:用于从源地址复制指定数量的字节到目标地址。但与memcpy不同的是,如果源地址和目标地址产生重叠,memmove保证重叠情况下的数据也是被正确复制的。
这也就意味着 memmove 比 memcpy 做更多的工作,memmove 需要判断是否是重叠场景,决定从前复制还是从后复制。
40. 模版的优缺点?
优点:
- 代码重用性:模版允许我们编写与类型无关的代码,减少了重复代码,提高代码的重用性。
- 类型安全:模版可以在编译时进行类型检查,避免了运行时错误,提高了程序的安全性。
- 灵活性强:模版可以用来实现泛型编程,实现更为通用的算法。
缺点:
- 编译时间增加:因为模版会在编译时生成具体类型的代码,会导致编译时间显著增加。
- 难以调试:模版引起的错误往往信息量巨大且难以理解。
- 可读性和维护性:由于模版代码的泛型特性,代码的可读性和维护性会下降,比如标准库的代码,真的比较难理解。
41. C++ 的栈溢出?
栈溢出(Stack Overflow)是指程序使用的栈空间超过了操作系统为该线程预留的栈空间大小,栈空间被耗尽的一种现象,导致程序崩溃。
出现栈溢出的情况:
- 递归调用很深,撑爆栈空间
- 大局部变量,定义了很多过大的局部变量,超过了栈空间的限制
42. 什么是 C++ 的回调函数?为什么需要回调函数?
回调函数是一种通过可调用对象(函数指针、仿函数、std::function、lambda)将一个函数作为参数传递给另一个函数的机制。
实际上就是把函数的调用权从一个地方转移到另一个地方,这个调用会在未来某个时刻执行,而不是立即执行。
- 异步编程:在异步操作中,比如网络请求、文件读取、事件处理等,可以在操作完成后调用回调函数,而主程序可以继续执行其他任务,避免等待。
- 解耦代码:回调函数有助于将代码模块化和解耦,允许我们创建更灵活和可复用的代码。比如,一个通用的排序算法可以接受一个比较函数,来控制升序还是降序。
43. C++ 中为什么要使用 nullptr 而不是 NULL?
在 C++ 中 NULL 是一个宏,通常是 #define NULL 0,它实际上是个整形值。而 nullptr 是 C++11 推出的新关键字,有具体的类型 std::nullptr_t 能够明确表示空指针。
44. 什么是大端序?什么是小端序?
大端序和小端序指的是数据在内存中的存放顺序。
- 大端序:将数据的低位保存在内存的高地址处,高位存储在内存的低地址处。
- 小端序:将数据的低位存储在内存的低地址处,高位存储在内存的高地方处。
cpp
int num = 0x11223344;
假设从左向右是低地址到高地址
大端存储:11 22 33 44
小端存储:44 33 22 11
如何判断当前环境是否是大端还是小端?
cpp
bool isLittle() {
int num = 1;
return *(reinterpret_cast<char*>(&num)) == 1;
}
指针中存储的地址是指向内容的低地址 ,num低位是1,如果低地址也是1,就是小端存储,否则是大端存储。
45. C++ 中深拷贝和浅拷贝?
浅拷贝:浅拷贝只是简单的复制对象的值,而不复制对象所拥有的资源或内存。也就是说,两个对象共享同一个资源,当一个对象修改了资源,另一个对象也会受影响。(默认生成的拷贝构造和赋值就是浅拷贝)
深拷贝:深拷贝不仅复制对象的值,还会对于其动态申请的资源进行新的内存分配,并复制其的内容,这样,两个对象看到的就不是同一个资源,当其中一个修改也不会影响另外一个。
46. C++ 中友元类和友元函数有什么作用?
两者主要用于提供访问其他对象私有成员和保护成员的权限。
友元关系是一种单向的访问权限,并不会破坏封装性,同时也不会牵涉到类之间的继承关系。
友元的替代方案:getter 和 setter 方法
47. C++ 中如何设计一个线程安全的类?
- 使用互斥锁保护共享资源。
- 部分逻辑可以使用无锁编程,原子变量控制。
- 使用线程任务队列形式,当跨线程操作时,A线程不直接操作B线程中的内容,而是通过将要执行的任务投递到B线程的任务队列中,来通知B线程执行任务,这样保证所有的操作都在该线程中执行。
💡 补充:
读写锁:有时我们需要实现的场景是多线程可以同时读数据,但写数据时需要占据锁。这可以使用
std::shared_mutex(C++17)来实现。
48. C++ 中的 extern C 是什么?
使用 extern "C" 来告诉编译器按照 C 语言的编译方式处理某些代码,因为 C++ 支持函数重载,而 C 语言不支持。C++编译器会对函数名就行修饰,extern "C" 的作用就是让编译器按照 C 方式编译,避免函数名被修饰,保证 C语言库里的函数能被正确调用。
在 C++ 代码中包含 C语言头文件时,用 extern "C" 进行声明:
cpp
extern "C" {
#include "xxx.h"
}
49. C++ 指针和引用的区别?
- 引用语法代表变量的别名,不开辟空间,而指针是存储一个变量的自己,需要开辟空间
- 引用定义必须初始化,指针定义可以不初始化
- 引用不可以改变指向,指针可以改变指向
- sizeof(引用)算的是被引用对象的大小,sizeof(指针)表示指针的大小(根据系统架构决定,32位4字节,64位8字节)
- 不存在空的引用,必须有具体实体,但是存在空的指针
💡 补充:
引用的底层是通过指针实现的。
50. C++14、17的新特性?
C++14:
- 允许 lambda 表达式使用 auto 作为参数类型,使其成为泛型
- C++11中不能直接用 auto 做函数的返回类型,需要配合尾置返回类型。C++14可以直接用 auto 做返回类型,自动推导返回类型
- 二进制字面量,通过0b声明二进制数
0b1010 - 数字分隔符
int million = 100'0000 std::make_unique- 字面量后缀
C++17:
- 结构化绑定,可以解包元组、结构体、pair
- 内联变量,解决头文件中定义全局变量的重复的问题
std::optional表示可能有值也可能没值的语义,替代指针判空或特殊值std::string_view字符串的零拷贝只读视图,它不拥有数据,大幅减少std::string临时对象- 文件系统库
std::filesystem标准化的目录遍历和路径操作
51. C++ 中的 shared_from_this 是什么?
shared_from_this 让一个类的成员函数能够安全的获取该对象的 std::shared_ptr 指针,这样可以避免直接创建 shared_ptr 而导致的双重释放问题和潜在的悬挂指针问题。
双重释放问题:是因为 shared_ptr 的机制,shared_ptr(裸指针)都会创建一个全新的、独立的控制块。注意:当一个 shared_ptr 赋值或者构造一个 shared_ptr 时才会引用计数+1。
所以当你构造用 this 构造多个 shared_ptr 时,每个 shared_ptr 都是全新的控制快,每个引用计数都是1,这样最终导致多重释放问题。
悬挂指针问题:如果传裸 this 制作,可能调用对象已经释放了,造成悬空指针。用 shared_from_this 保证持有一份引用计数,使用完才释放。
shared_from_this() 的使用条件:
- 该对象被
shared_ptr管理 - 类继承
enable_shared_from_this<T>