C++八股和答案
printf怎么显示输出到屏幕
printf是如何将字符输出到显示器上的(IO与显示器)--OS_os_printf_jump_into_zehe的博客-CSDN博客
什么是CRTP
在CRTP中,通过将派生类作为模板参数传递给基类,实现了基类对派生类的访问。由于CRTP使用的是静态多态,因此在编译时就能够确定函数调用的具体实现,避免了动态多态带来的运行时开销。
三种继承方式
公有继承(public)、私有继承(private)、保护继承(protected)是常用的三种继承方式。
RAII 机制
RAII
是Resource Acquisition Is Initialization
的简称,其翻译过来就是“资源获取即初始化”,即在构造函数中申请分配资源,在析构函数中释放资源,它是C++
语言中的一种管理资源、避免泄漏的良好方法。
C++
语言的机制保证了,当创建一个类对象时,会自动调用构造函数,当对象超出作用域时会自动调用析构函数。RAII
正是利用这种机制,利用类来管理资源,将资源与类对象的生命周期绑定,即在对象创建时获取对应的资源,在对象生命周期内控制对资源的访问,使之始终保持有效,最后在对象析构时,释放所获取的资源。
Static 变量初始化时机
全局(静态)存储区:分为 DATA 段和 BSS 段 DATA 段存放初始化的全局变量和静态变量;
BSS 段存放未初始化的全局变量和静态变量。 其中BBS段在程序执行之前会被系统自动清0,所以未初始化的全局变量和 静态变量在程序执行之前已经为0。
函数中的静态变量什么时候初始化
- 在C中,初始化发生在代码执行之前;编译阶段分配好内存之后,就会进行 初始化,所以在C语言中无法使用变量对静态局部变量进行初始化。
- 在C++中,局部静态变量的初始化是在第一次执行到初始化语句时,且只会初始化一次;即C++在执行相关代码时才会进行初始化,所以在C++中是可以使用变量对静态局部变量进行初始化的。
static变量或者函数放在头文件中,有问题吗?
如果在头文件中定义static变量,被多个文件引用,编译可以顺利通过!即该头文件被包含了多少次,这些变量就定义了多少次。会存在多份拷贝。
C语言需要在头文件中放static inline函数,进行内联。
头文件中的 static 函数会在每个文件中生成一份代码,这造成代码冗余倒不是最大的问题,最大的问题是可能带来库文件与工程文件同一函数的代码的不一致性,这有风险。
一般来说推荐static使用在.cpp文件中。
在C++类中定义的静态变量也不能在头文件中初始化,一定要在cpp中初始化。
C++多态
运行期多态
- 程序中既存在父类也存在子类,父类中必须含有虚函数,子类中也必须重写父类中的虚函数。
- 父类指针指向子类对象或者父类引用绑定(指向) 子类对象
- 当通过父类的指针或引用,调用子类中重写的虚函数时就能看出多态性的表现了子类对象的地址赋值给父类对象指针,虽然看着是父类对象指针,但是其中的父类对象的vptr已经是子类对象的vptr了,所以执行覆盖的函数时查的子类的vtable,这样完成多态的
编译器多态
- 重载
- 模板
- CRTP
C/C++杂记:虚函数的实现的基本原理 - malecrab - 博客园
C++ 静态绑定和动态绑定
绑定(Binding)是指将变量和函数名转换为地址的过程。
早期绑定(Early binding):绝大部分的顺序执行逻辑中函数调用或某个确定数据类型的(不存在选择性)的类类型的对象对成员调用都属于早期绑定。
早期绑定意味着绑定的函数或者变量,已经在编译阶段,该语句已经被编译成“call 函数地址”或"callq 函数地址"这样的汇编指令格式。
所有函数都有唯一的地址。 因此,当编译器(或链接器)遇到函数调用时,它将用机器语言指令替换该函数调用,该指令告诉CPU跳转到该函数的地址,因此早期绑定也叫静态绑定。
在一些带有决策性的业务逻辑的代码中,要等到用户的反馈(通常是条件判断/参数类型判定...),直到运行时,根据决策的结果才能知道将调用哪个函数。这称为后期绑定(或动态绑定),动态绑定的技术的本源就是函数指针(也可以称为函数原型)。在C ++中运行时多态正是使用的就是函数指针。
C++ 构造函数最大作用
C++的构造函数的作用:初始化类对象的数据成员。
即类的对象被创建的时候,编译系统对该对象分配内存空间,并自动调用构造函数,完成类成员的初始化。
如果没有构造函数,对象的成员变量可能会包含未定义的值,这可能会导致程序出现错误或不可预测的行为。
C++ 虚析构的必要性
- 目的:避免内存泄漏。
- 如果析构函数是虚函数,基类的指针指向派生类的对象时,就会发生动态绑定, 调用派生类的析构函数;
- 如果析构函数不是虚函数,基类的指针指向派生类的对象时不会有动态绑定,调用时会根据指针的静态类型来判断要调用的函数,那么只调用了基类的析构函数,子类开辟的空间无法被正确的释放。
纯虚函数
纯虚函数:纯虚函数本质也是一个虚函数,纯虚函数的表现形式是在虚函数最后写上=0,纯虚函数要求派生类必须实现这个函数,主要作用是用来定义一个接口标准,便于框架或者模型的拓展,在Swift里面也常常说这样子是定义了一个协议 ,然后如果一个类有纯虚函数,则这个类不能实例化。
dynamic_cast实现原理
dynamic_cast
的检查是通过在对象的内存布局中查询虚表(vtable)来实现的。虚表是存储在具有虚函数的类对象中的一个数据结构,它包含了虚函数的地址信息。当使用 dynamic_cast
进行类型转换时,编译器会在运行时查找对象的虚表,并比较虚表中存储的类型信息和转换的目标类型信息,从而判断是否可以进行转换。
C++ 编译流程
内联函数在编译阶段展开
静态链接和动态链接
动态链接库具体原理
07 | 动态链接(上):地址无关代码是如何生成的?-极客时间
08 | 动态链接(下):延迟绑定与动态链接器是什么?-极客时间
链接器的工作流程也主要分为两步:
第一遍扫描完成文件合并、虚拟内存布局的分配以及符号信息收集;第二遍扫描则是完成了符号的重定位过程。
- 第一步是,链接器需要对编译器生成的多个目标(.o) 文件进行合并,一般采取的策略是相似段的合并,最终生成共享文件 (.so) 或者可执行文件。这个阶段中,链接器对输入的各个目标文件进行扫描,获取各个段的大小,并且同时会收集所有的符号定义以及引用信息,构建一个全局的符号表。当链接器构造好了最终的文件布局以及虚拟内存布局后,我们根据符号表,也就能确定了每个符号的虚拟地址了。
- 第二步是,链接器会对整个文件再进行第二遍扫描,这一阶段,会利用第一遍扫描得到的符号表信息,依次对文件中每个符号引用的地方进行地址替换。也就是对符号的解析以及重定位过程。
动态链接原理:
为了节约内存,让进程间可以共享代码,人们把可以被共享的代码都抽出来,放到一个文件中,多个进程共享这个文件就可以了。这个可共享的文件就是动态库文件。动态库文件中的符号要在加载时才被解析,所以这种技术就叫动态链接技术。
动态库文件被加载进内存以后,在物理内存只有一份,多个进程都可以将它映射进自己的虚拟地址空间。各个进程在映射时可以将动态库的代码段映射到任意的位置。
如果两个共享库之间有引用关系的话,引用者和被引用者之间的相对位置就不能确定了,这时就需要引入地址无关代码技术。对于内部函数或数据访问,因为其相对偏移是固定的,所以可以通过相对偏移寻址的方式来生成代码;对于外部和全局函数或数据访问,则通过 GOT 表的方式,利用间接跳转将对绝对地址的访问转换为对 GOT 表的相对偏移寻址,由此得到了地址无关的代码。
地址无关的代码除了可以在 so 中使用,同样可以在可执行文件中使用,可以通过 -pie 选项使得 gcc 编译地址无关的可执行文件。地址文件的可执行文件可以被加载到内存的任意位置执行,这会使得缓冲区溢出的难度增加,但代价是通过 GOT 访问地址会多一次访存,性能会下降。
动态链接性能牺牲
性能的牺牲主要来自于两个方面:
- 每次对全局符号的访问都要转换为对 GOT 表的访问,然后进行间接寻址,这必然要比直接的地址访问速度慢很多;
- 动态链接和静态链接的区别是将链接中重定位的过程推迟到程序加载时进行。因此在程序启动的时候,动态链接器需要对整个进程中依赖的 so 进行加载和链接,也就是对进程中所有 GOT 表中的符号进行解析重定位。这样就导致了程序在启动过程中速度的减慢。
延迟绑定技术
将函数地址的重定位工作一直推迟到第一次访问的时候再进行,这就是延迟绑定 (Lazy binding) 的技术。这样的话,对于整个程序运行过程中没有访问到的全局函数,可以完全避免对这类符号的重定位工作,也就提高了程序的性能。
Loader 的加载机制
动态链接会把不同模块之间,符号重定位的操作,推迟到程序运行的时候,而 ld-linux.so 就负责这个工作。所以我们经常称 ld.so 为动态链接器,又因为它还负责加载动态库文件,所以我们有时也叫它 loader,或者加载器。
启动动态链接器
ld-linux.so 在启动之后,首先需要完成自己的符号解析和重定位的过程,这个过程叫做动态链接器的自举 (Bootstrap)。
加载依赖共享文件
完成自举后,ld-linux.so 就可以放心的使用各种全局符号和外部符号了。接下来第二步是根据可执行文件的.dynamic 段信息依次加载程序依赖的共享库文件。程序的共享库依赖关系往往是一个图的关系,所以这里在加载共享库的过程也相当于是图遍历的过程,这里往往采用的是广度优先搜索的算法来遍历。
符号重定位与解析
在完成了共享文件的加载之后,全局符号表的信息就收集完成了,这时 ld-linux.so 就可以根据全局符号表和重定位表的信息依次对各个 so 和可执行文件进行重定位修正了。这个过程跟静态链接中重定位的过程类似。
init 函数调用
最后,有的 so 文件还会有.init 段,进行一些初始化函数的调用,例如 so 中全局变量的对象构造函数,或者用户自己生成在.init 段的初始化函数等。这些都会由 ld-linux.so 在最后的阶段进行一次调用。当这些完成之后,ld-linux.so 就会结束自己的使命,最终将程序的控制流转到可执行文件的入口函数中进行。
导出动态链接库中,有的函数你不想暴露给外面使用,有什么方法
在C++中,如果你不想将某些函数暴露给外部使用,有几种方法可以实现:
- 隐藏符号(Symbol Hiding):在导出动态链接库时,可以使用编译器的选项来隐藏特定函数的符号。这样做可以防止外部代码直接调用这些函数。具体的实现方式与编译器和平台相关。例如,在GCC中,可以使用-fvisibility=hidden选项来隐藏符号,然后通过__attribute__((visibility("default")))来显式地指定需要暴露的函数。
- 包装函数(Wrapper Functions):可以在动态链接库中定义一些包装函数,它们可以调用内部的隐藏函数。这样,外部代码只能访问包装函数,而无法直接调用隐藏函数。包装函数可以提供额外的逻辑或者验证,以保护内部函数的安全性。
- 使用命名空间(Namespace):将需要隐藏的函数放置在一个特定的命名空间中,然后只暴露给外部的函数放置在另一个命名空间中。这样,外部代码只能访问到暴露的命名空间,而无法直接访问隐藏的命名空间。
- 使用内部链接(Internal Linkage):在隐藏的函数声明前加上static关键字,将其变为具有内部链接的函数。这样,隐藏函数只能在当前编译单元内可见,无法被其他编译单元引用。
C++的空类有什么函数
默认产生6个成员函数:1、默认无参构造函数;2、拷贝构造函数;3、析构函数;4、重载赋值运算符;5、重载取址运算符;6、重载取址运算符(常成员函数)C++的空类默认会产生默认构造函数,默认析构函数,默认拷贝构造函数,默认赋值运算符,还有取址运算符&和取值运算符& const。但是它默认生成的函数,只有在被需要的时候才会产生,即定义一个类对象的时候才会有,如果不创建类对象,则不会创建类的构造函数,析构函数等。例如:
class Empty { public: Empty(); // 缺省构造函数 Empty( const Empty& ); // 拷贝构造函数 ~Empty(); // 析构函数 Empty& operator=( const Empty& ); // 赋值运算符 Empty* operator&(); // 取址运算符 const Empty* operator&() const; // 取址运算符 const };
为什么C++调用空指针对象的成员函数可以运行通过?
为什么C++调用空指针对象的成员函数可以运行通过? - 旺旺的回答 - 知乎为什么C++调用空指针对象的成员函数可以运行通过? - 知乎
C++ 重载 覆盖 隐藏
C++中重载、重写(覆盖)和隐藏的区别_走过_冬天的博客-CSDN博客
重载:是指同一可访问区内被声明的几个具有不同参数列(参数的类型,个数,顺序不同)的同名函数,根据参数列表确定调用哪个函数,重载不关心函数返回类型
class A{ public: void test(int i); void test(double i);//overload void test(int i, double j);//overload void test(double i, int j);//overload int test(int i); //错误,非重载。注意重载不关心函数返回类型。 };
隐藏:是指派生类的函数屏蔽了与其同名的基类函数,注意只要同名函数,不管参数列表是否相同,基类函数都会被隐藏。
#include <iostream> using namespace std; class Base { public: void fun(double ,int ){ cout << "Base::fun(double ,int )" << endl; } }; class Derive : public Base { public: void fun(int ){ cout << "Derive::fun(int )" << endl; } }; int main() { Derive pd; pd.fun(1);//Derive::fun(int ) pb.fun(0.01, 1);//error C2660: “Derive::fun”: 函数不接受 2 个参数 system("pause"); return 0; }
重写(覆盖):是指派生类中存在重新定义的函数。其函数名,参数列表,返回值类型,所有都必须同基类中被重写的函数一致。只有函数体不同(花括号内),派生类调用时会调用派生类的重写函数,不会调用被重写函数。重写的基类中被重写的函数必须有virtual修饰。
#include<iostream> using namespace std; class Base { public: virtual void fun(int i){ cout << "Base::fun(int) : " << i << endl;} }; class Derived : public Base { public: virtual void fun(int i){ cout << "Derived::fun(int) : " << i << endl;} }; int main() { Base b; Base * pb = new Derived(); pb->fun(3);//Derived::fun(int) system("pause"); return 0; }
C++ inline
首先,现代的编译器在决定是否将函数调用进行内联展开时,几乎不参考函数声明中inline修饰符;其次,inline关键字不仅能修饰函数,也可修饰变量(C++17以后),甚至能修饰命名空间(C++11以后);此外,inline更为关键的作用是允许同一个函数或变量的定义出现在多个编译单元之中;最后,修饰命名空间的inline关键字为程序库的版本控制提供了语言层面上的支持,这与最初的函数内联的含义更是相去甚远。
C++ inline 有什么用?? - 吼姆桑的回答 - 知乎C++ inline 有什么用?? - 知乎
假如要实现一个类,里面有个函数返回一个指向自己的智能指针会怎么写?
类只要继承自std::enable_shared_from_this<T>模板对象,调用shared_from_this()
#include <iostream> #include <memory> class A : public std::enable_shared_from_this<A>{ public: A(){ std::cout << "A constructor" << std::endl; } ~A(){ std::cout << "A destructor" << std::endl; } std::shared_ptr<A> getSelf() { return shared_from_this(); } }; int main() { std::shared_ptr<A> sp1(new A()); std::shared_ptr<A> sp2 = sp1->getSelf(); std::cout << "use count: " << sp1.use_count() << std::endl; return 0; }
C++右值引用和std::move
在C++中右值指的的临时值或常量,更准确的说法是保存在CPU寄存器中的值为右值,而保存在内存中的值为左值。
被声明出来的左、右值引用都是左值。
- 从性能上讲,左右值引用没有区别,传参使用左右值引用都可以避免拷贝。
- 右值引用可以直接指向右值,也可以通过std::move指向左值;而左值引用只能指向左值(const左值引用也能指向右值)。
- 作为函数形参时,右值引用更灵活。虽然const左值引用也可以做到左右值都接受,但它无法修改,有一定局限性。
std::move
执行到右值的无条件的转换,但就自身而言,它不移动任何东西。
右值引用和std::move的应用场景
- 实现移动语义
在实际场景中,右值引用和std::move被广泛用于在STL和自定义类中实现移动语义,避免拷贝,从而提升程序性能。
- vector::push_back使用std::move提高性能
可移动对象在<需要拷贝且被拷贝者之后不再被需要>的场景,建议使用std::move
触发移动语义,提升性能。
std::move 理解
move是为了转移对象的所有权,并不是移动对象,跟生活中的移动不一样(日常生活中的移动是把物体从一个地方变动到另外一个地方)。因此,我们可以move的对象是“没人要”的对象。
什么是move?理解C++ Value categories,move, move in Rust
为什么要引入移动语义
从实际内存布局的角度,很多语言——如 Java 和 Python——会在 A 对象里放 B 和 C 的指针(虽然这些语言里本身没有指针的概念)。而 C++ 则会直接把 B 和 C 对象放在 A 的内存空间里。这种行为既是优点也是缺点。说它是优点,是因为它保证了内存访问的局域性,而局域性在现代处理器架构上是绝对具有性能优势的。说它是缺点,是因为复制对象的开销大大增加:在 Java 类语言里复制的是指针,在 C++ 里是完整的对象。这就是为什么 C++ 需要移动语义这一优化,而 Java 类语言里则根本不需要这个概念。
完美转发
std::forward
只有当它的参数被绑定到一个右值时,才将参数转换为右值。
完美转发(perfect forwarding)意味着我们不仅转发对象,我们还转发显著的特征:它们的类型,是左值还是右值,是const
还是volatile
。结合到我们会处理引用形参,这意味着我们将使用通用引用(参见Item24),因为通用引用形参被传入实参时才确定是左值还是右值。
单独的forward不是完美转发;万能引用+forward才构成完美转发
Item 30:熟悉完美转发失败的情况 - Effective Modern C++
enable_shared_from_this
std::enable_shared_from_this - cppreference.com
STL enable_shared_from_this深入了解
weak_ptr使用场景
weak_ptr 的几个应用场景 —— 观察者、解决循环引用、弱回调_weak_ptr的使用场景_爱好学习的青年人的博客-CSDN博客
有时候我们需要“如果对象还活着,就调用它的成员函数,否则忽略之”的语意,就像Observable::notifyObservers()那样,我称之为“弱回调”。这也是可以实现的,利用weak_ptr,我们可以把weak_ptr绑到boost::function里,这样对象的生命期就不会被延长。然后在回调的时候先尝试提升为shared_ptr,如果提升成功,说明接受回调的对象还健在,那么就执行回调;如果提升失败,就不必劳神了。
shared ptr多线程安全问题
当我们谈论shared_ptr的线程安全性时,我们在谈论什么
为什么C++模板函数的声明与实现都放在.h文件中
因为编译器是一次只能处理一个编译单元, 也就是一次处理一个cpp文件,所以实例化时需要看到该模板的完整定义
当一个模板不被用到的时侯它就不该被实例化出来
malloc 和 new 区别
(类型安全、构造和析构、分配失败处理、大小计算)
1.使用malloc申请空间的时候,需要传递参数来标明申请空间的大小;而使用new来申请时,new会根据后面的类型来开辟相应的空间,不需要传递参数。
2.malloc申请空间之后返回的是一个void ,使用其他类型接收的时候需要进行类型转换;而new得到的就是一个相应类型的指针,不需要类型转换。
3.new会调用构造函数完成空间的初始化,而malloc不会对空间进行初始化;同样delete在销毁空间时会调用析构函数完成资源的清理,而free只会对空间进行销毁。
4.new/delete是操作符,malloc/free是函数。
5.malloc失败时,会返回nullptr,需要对malloc的返回值进行判空检查;new失败时会抛异常,不需要进行判空检查。
- 来源:new 是 C++ 中的运算符,而 malloc 是 C 的函数。
- 构造和析构:new 不仅分配内存,还会调用对象的构造函数来初始化对象。当使用 delete 释放内存时,它还会调用对象的析构函数。而 malloc 只是简单地分配一块内存,不会调用任何构造函数或析构函数。
- 类型安全:new 会返回正确的类型,不需要类型转换。而 malloc 返回的是 void*,需要进行类型转换。
- 错误处理:如果 new 无法分配内存(例如,内存不足),它会抛出 std::bad_alloc 异常(除非你使用了 nothrow 版本的 new,那样的话它会返回 nullptr)。而 malloc 在无法分配内存时只会返回 NULL(或 nullptr 在 C++ 中)。
- 内存分配大小:new 会自动计算需要分配的内存大小,而 malloc 需要显式地提供字节数。
malloc线程安全吗?多线程进行malloc,系统怎么设计?
malloc 是 线程安全的但是不可重入的。
多线程高并发请使用内存池;或者(如果可以)直接一次性分配好内存,后面复用。
如果并发量高、分配频繁,可以考虑用tcmalloc。
内存映射是有名还是匿名?
new 通过什么来初始化
New 和 delete 怎么实现的
- new的实现过程是:首先调用名为operator new的标准库函数,分配足够大的原始为类型化的内存,以保存指定类型的一个对象;接下来运行该类型的一个构造函数,用指定初始化构造对象;最后返回指向新分配并构造后的的对象的指针
- delete的实现过程:对指针指向的对象运行适当的析构函数;然后通过调用名为operator delete的标准库函数释放该对象所用内存
讲讲new的实现,其中new什么时候返回空指针,什么时候抛出异常,抛的是什么异常
New 实现
- 定位并保留要分配的对象的存储。 此阶段完成后,将分配正确的存储量,但它还不是对象。
- 初始化对象。 初始化完成后,将为成为对象的已分配存储显示足够的信息。
- 返回指向对象的指针,该对象所属的指针类型派生自 new-type-id 或 type-id。 程序使用此指针来访问最近分配的对象。
抛出异常
在符合标准的 C++ 实现上,不会。 new 的普通形式永远不会返回 NULL; 如果分配失败,将抛出std::bad_alloc 异常(新的 (nothrow) 形式不会抛出异常,如果分配失败将返回 NULL)。
返回空指针:
使用nothrow。在一些较旧的 C++ 编译器上(尤其是在语言标准化之前发布的编译器)或在显式禁用异常的情况下(例如,可能是某些用于嵌入式系统的编译器),new 可能会在失败时返回 NULL。 执行此操作的编译器不符合 C++ 标准。
如何保证类的对象只能被开辟在堆上
Class A { public: A() {} void destroy(){delete this;} private: ~A() {} };
如何保证类的对象只在栈上分配内存
使用new运算符,对象才会建立在堆上,因此,只要禁用new运算符就可以实现类对象只能建立在栈上。虽然我们不能影响new operator的能力(因为那是C++语言内建的),但我们可以利用一个事实:new operator 总是先调用 operator new,而后者我们是可以自行声明重写的。因此,将operator new()设为私有即可禁止对象被new在堆上。
代码如下:
Class A { private: void *operator new(size_t t) {} void operator delete(void *p) {} //重载new, delete必须同时进行重新改写 public: A() {} ~A() {} };
vector push_back和empalce_back 区别
emplace_back
使用完美转发,因此只要你没有遇到完美转发的限制(参见Item30),就可以传递任何实参以及组合到emplace_back
置入函数避免了插入函数所必需的临时对象的创建和销毁。
Item 42:考虑就地创建而非插入 - Effective Modern C++
- 原则上,置入函数有时会比插入函数高效,并且不会更差。
- 实际上,当以下条件满足时,置入函数更快:(1)值被构造到容器中,而不是直接赋值;(2)传入的类型与容器的元素类型不一致;(3)容器不拒绝已经存在的重复值。
- 置入函数可能执行插入函数拒绝的类型转换。
std::vector<bool>
vector 在扩容的时候,什么情况下调用移动构造 什么时候调用拷贝构造?
1.移动的对象要有移动构造函数
2.移动构造函数后面必须加 上noexcept关键字
必须同时满足上述两个条件,vector在扩容的时候才会调用移动构造
为什么会推荐使用std::make_shared
, 而不是直接构造std::shared_ptr
?
Item 21:优先考虑使用std::make_unique和std::make_shared而非new - Effective Modern C++
从零开始写一个shared_ptr-make_shared源代码解析
如果直接构造std::shared_ptr
, 那么是先分配一块内存给实例对象,再分配一块内存给引用计数模块(引用计数,删除器(deleter)等). 但是std::make_shared
可以一次性分配一整块内存给引用计数模块以及实例对象. 这样有两部分优点:优点1: 异常安全(Exception-Safety)
优点2: 减少开销(Reduce overhead)
const 和 contexptr 区别
一个 constexpr 变量是一个编译时完全确定的常数。一个 constexpr 函数至少对于某一组实参可以在编译期间产生一个编译期常数。
注意一个 constexpr 函数不保证在所有情况下都会产生一个编译期常数(因而也是可以作为普通函数来使用的)。编译器也没法通用地检查这点。编译器唯一强制的是:
- constexpr 变量必须立即初始化
- 初始化只能使用字面量或常量表达式,后者不允许调用任何非 constexpr 函数
函数调用的过程
栈底:ebp 存储帧指针
栈顶:esp 存储栈指针
指令寄存器:eip 存储下一条要执行的指令
什么是现场:通用寄存器的内容
为什么要保存现场:因为所有函数过程都是共享一套通用寄存器
调用者 P:
- (如果需要)保存通用寄存器
- 函数参数压栈将 Q 需要的参数放入到自己的栈帧中
- call 指令:将返回地址(call指令的下一条指令)压栈,控制权交给 Q(把目标函数地址放到 eip)
被调用者 Q:
- 保存 P 的现场(栈底指针 ebp、通用寄存器)任何过程的第一条指令都是把旧的 ebp 压栈 pushl %ebp
- 形成新的栈顶,把旧的 esp 作为 ebp movl %esp, %ebp
- 栈帧底部会分配一段连续空间,存储非静态局部变量,并赋值通过将栈底 esp 减去一个数,向下拓展分配栈帧
- 执行
- 存储返回值
- 退栈:即恢复到 P 的栈空间,恢复 P 的栈底指针(栈底),将 esp(栈顶)的值恢复为 ebp
- 返回:ret 指令会取出返回地址,放到 eip 中
调用者 P:
- 从指定寄存器 eax 取出返回值
函数调用时,CPU干了什么
函数调用时,CPU会执行以下操作:
- 将函数参数压入栈中。
- 跳转到被调用函数的第一条指令。
- 在被调用函数中,将返回地址压入栈中。
- 执行被调用函数。
- 返回时,将返回值存储在寄存器中。
- 弹出返回地址并跳转回调用函数。
这些操作都是由CPU执行的,其中栈的使用是为了保存上下文信息。上下文信息包括函数参数、局部变量、返回地址等。这些信息在函数调用结束后需要恢复,以便程序能够继续执行。如果没有栈的支持,那么程序就无法正确地执行函数调用。
STL map 下标运算符[] 为什么不存在会默认构造一个
无论容器是C数组、vector或者map,operator[]都应该返回一个值(引用),而不是指针。这保证了operator[]语义的一致性。
无论是原生的数组,还是 std::array,std::vector,当找通过键(二者只能用整数)检索值时,它们发生这种找不到还强行读取后的反应,是臭名昭著的未定义行为。
C++ lambda 表达式原理
编译器会把我们写的lambda表达式翻译成一个类,并重载 operator()来实现。比如我们写一个lambda表达式为
auto plus = [] (int a, int b) -> int { return a + b; } int c = plus(1, 2);
那么编译器会把我们写的表达式翻译为
// 类名是我随便起的 class LambdaClass { public: int operator () (int a, int b) const { return a + b; } }; LambdaClass plus; int c = plus(1, 2);
调用的时候编译器会生成一个Lambda的对象,并调用opeartor ()函数。(备注:这里的编译的翻译结果并不和真正的结果完全一致,只是把最主要的部分体现出来,其他的像类到函数指针的转换函数均省略)
捕获列表,对应LambdaClass类的private成员。
参数列表,对应LambdaClass类的成员函数的operator()的形参列表
mutable,对应 LambdaClass类成员函数 operator() 的const属性 ,但是只有在捕获列表捕获的参数不含有引用捕获的情况下才会生效,因为捕获列表只要包含引用捕获,那operator()函数就一定是非const函数。
返回类型,对应 LambdaClass类成员函数 operator() 的返回类型
函数体,对应 LambdaClass类成员函数 operator() 的函数体。
引用捕获和值捕获不同的一点就是,对应的成员是否为引用类型。
如果是一个引用捕获,要注意什么?
按引用捕获会导致闭包中包含了对某个局部变量或者形参的引用,变量或形参只在定义lambda的作用域中可用。如果该lambda创建的闭包生命周期超过了局部变量或者形参的生命周期,那么闭包中的引用将会变成悬空引用。
比起“[&]
”传达的意思,显式捕获能让人更容易想起“确保没有悬空变量”。
C++ STL容器如何解决线程安全的问题?
C++ STL容器如何解决线程安全的问题? - 果冻虾仁的回答 - 知乎C++ STL容器如何解决线程安全的问题? - 知乎
加锁
预先使用resize
什么是异常安全
当异常发生时, 既不会发生资源泄露,系统也不会处于一个不一致的状态。
异常安全的代码,可以没有任何 try 和 catch。
lock_guard和unique_lock区别,使用场景
std::unqiue_lock
使用更为自由的不变量,这样std::unique_lock
实例不会总与互斥量的数据类型相关,使用起来要比std:lock_guard
更加灵活。首先,可将std::adopt_lock
作为第二个参数传入构造函数,对互斥量进行管理;也可以将std::defer_lock
作为第二个参数传递进去,表明互斥量应保持解锁状态。这样,就可以被std::unique_lock
对象(不是互斥量)的lock()函数的所获取,或传递std::unique_lock
对象到std::lock()
中。
lock_guard 是不可移动的(moveable),即不能拷贝、赋值、移动,只能通过构造函数初始化和析构函数销毁,unique_lock 是可移动的,可以拷贝、赋值、移动。
unique_lock 提供了更多的控制锁的行为,比如锁超时、不锁定、条件变量等。unique_lock 比 lock_guard 更重,因为它有更多的功能,更多的开销。如果只需要简单的互斥保护,使用 lock_guard 更好。
unique_lock 支持手动解锁,而 lock_guard 不支持。
lock_guard 不可能用在函数参数传递或者返回过程中,只能用在简单的临界区代码段的互斥操作中
unique_lock 它不仅可以使用在简单的临界区代码段的互斥操作中,还能用在函数调用过程中
原子变量std::atomic的实现原理
Bus Lock
当CPU发出一个原子操作时,可以先锁住Bus(总线)。这样就可以防止其他CPU的内存操作。等原子操作结束,释放Bus。这样后续的内存操作就可以进行。这个方法可以实现原子操作,但是锁住Bus会导致后续无关内存操作都不能继续。实际上,我们只关心我们操作的地址数据。只要我们操作的地址锁住即可,而其他无关的地址数据访问依然可以继续。所以我们引入另一种解决方法。
Cacheline Lock
为了实现多核Cache一致性,现在的硬件基本采用MESI协议(或者MESI变种)维护一致性。因此我们可以借助多核Cache一致性协议MESI实现原子操作。
15 | MESI协议:多核CPU是如何同步高速缓存的?-极客时间
内存屏障是什么
(1)一类是强制读取主内存,强制刷新主内存的内存屏障,叫做Load屏障和Store屏障
(2)另外一类是禁止指令重排序的内存屏障,有四个分别叫做LoadLoad屏障、StoreStore屏障、LoadStore屏障、StoreLoad屏障
强制读取/刷新主内存的屏障
Load屏障:执行读取数据的时候,强制每次都从主内存读取最新的值
Store屏障:每次执行修改数据的时候,强制刷新回主内存。
禁止指令重排序的内存屏障
LoadLoad屏障
序列:load1指令 LoadLoad屏障 load2指令
作用:在load1指令和load2指令之间加上 LoadLoad屏障,强制先执行load1指令再执行load2指令;load1指令和load2指令不能进行重排序(LoadLoad屏障的前面load指令禁止和屏障后面的load指令进行重排序)。
StoreStore屏障
序列:store1指令 StoreStore屏障 store2指令
作用:在store1指令和store2指令之间加上StoreStore屏障,强制先执行store1指令再执行store2指令;store1指令不能和store2指令进行重排序(StoreStore屏障的前面的store指令禁止和屏障后面的store指令进行重排序)
LoadStore屏障
序列:load1指令 LoadStore屏障 store2指令
作用:在load1指令和store2指令之前加上LoadStore屏障,强制先执行load1指令再执行store2指令;load1指令和store2执行不能重排序(LoadStore屏障前面的load执行禁止和屏障后面的store指令进行重排序)
StoreLoad屏障
序列:store1指令 StoreLoad屏障 load2指令
作用:在store1指令和load2指令之间加上StoreLoad屏障,强制先执行store1指令再执行load2指令;
store1指令和load2指令执行不能重排序(StoreLoad屏障前面的Store指令禁止和屏障后面的Store/Load指令进行重排)
16 | 内存模型:有了MESI为什么还需要内存屏障?-极客时间
MESI协议
C++ 内存模型
同步操作和强制排序
<atomic> 头文件中定义了内存序,分别是:
- memory_order_relaxed:松散内存序,只用来保证对原子对象的操作是原子的
- memory_order_consume:C++17种明确建议不要使用,以后会被废弃
- memory_order_acquire:获得操作,在读取某原子对象时,当前线程的任何后面的读写操作都不允许重排到这个操作的前面去,并且其他线程在对同一个原子对象释放之前的所有内存写入都在当前线程可见
- memory_order_release:释放操作,在写入某原子对象时,当前线程的任何前面的读写操作都不允许重排到这个操作的后面去,并且当前线程的所有内存写入都在对同一个原子对象进行获取的其他线程可见
- memory_order_acq_rel:获得释放操作,一个读‐修改‐写操作同时具有获得语义和释放语义,即它前后的任何读写操作都不允许重排,并且其他线程在对同一个原子对象释放之前的所有内存写入都在当前线程可见,当前线程的所有内存写入都在对同一个原子对象进行获取的其他线程可见
- memory_order_seq_cst:顺序一致性语义,对于读操作相当于获取,对于写操作相当于释放,对于读‐修改‐写操作相当于获得释放,是所有原子操作的默认内存序(除此之外,顺序一致性还保证了多个原子量的修改在所有线程里观察到的修改顺序都相同;我们目前的讨论暂不涉及多个原子量的修改)
排序一致序列(sequentially consistent),获取-释放序列(memory_order_consume, memory_order_acquire, memory_order_release和memory_order_acq_rel),和自由序列(memory_order_relaxed)
C++ 加const能不能构成重载
C++ 加const能不能构成重载的几种情况_多个const修饰可以重载吗_learner_pu的博客-CSDN博客
值传递作为参数,加const构不构成重载——不构成
指针、引用作为参数加const构不构成重载——构成
类里的成员函数后面加const构不构成重载——构成
运行时类型识别(RTTI)
C++ 的 RTTI(Run-Time Type Information)是一种运行时类型信息机制,用于在程序运行时获取对象的类型信息。RTTI 主要包括两个关键字:typeid 和 dynamic_cast。
- typeid 运算符,用于返回表达式的类型。
- dynamic_cast 运算符,用于将基类的指针或引用安全地转换成派生类的指针或引用。
C++ 中的静态类型指的是:变量的数据类型在编译时、程序执行前就确定的。C++ 本身时一种静态类型语言,它在编译期间会确定数据类型并进行检查。
C++ 的动态类型:一般指的是对象所指的类型,在运行期间才能确定的,一般只有指针或者引用才用动态类型的说法。
C++ 模板实现底层原理
SFINAE
SFINAE最主要的作用,是保证编译器在泛型函数、偏特化、及一般重载函数中遴选函数原型的候选列表时不被打断。除此之外,它还有一个很重要的元编程作用就是实现部分的编译期自省和反射。
14 | SFINAE:不是错误的替换失败是怎么回事?-极客时间
C++ 深拷贝和浅拷贝区别
C++深拷贝与浅拷贝的区别 (简单易懂版)_乖舟的博客-CSDN博客
野指针、悬空指针、裸指针
noexcept 作用
noexcept 专门用来修饰函数,告诉编译器:这个函数不会抛出异常。编译器看到 noexcept,就得到了一个“保证”,就可以对函数做优化,不去加那些栈展开的额外代码,消除异常处理的成本。
#校招面试##C++#