(1)final和override的作用?final为什么能提高代码执行效率?
override:保证在派生类中声明的重载函数,与基类的虚函数有相同的签名,作用就是用于编译期代码检查。
final:阻止类的进一步派生和虚函数的进一步重写,同时也是一种名为去虚拟化的优化技巧,相当于把运行期多态转换为了编译期多态,提高了执行效率。
(2)static的3种作用?
修饰局部变量,这意味着该变量只被初始化一次,并保留其值直到程序结束。
修饰全局变量,这意味着该变量具有文件作用域。
修饰成员变量,这意味着变量不会绑定到对象上。
(3)thread_local作用和原理?
thread_local用来定义一个线程本地变量,每个线程都拥有自己的thread_local对象副本,这些副本放在各个线程自己的TLS空间。thread_local描述的对象在thread开始时分配,而在thread结束时释放。
(4)一个变量可以既是const又是volatile吗?
可以。const的作用是告诉编译器,编译期间不允许对变量进行修改,编译器在编译期间往往会对const变量执行一种名为字符替换的优化。volatile的作用是告诉编译器,第一,编译期间不要对该变量进行优化;第二,运行期间,每次必须从内存中加载变量的值。const volatile表示一个变量在程序编译期不能被修改且不能被优化;在程序运行期,每次必须从内存中加载变量的值。
(5)NULL和nullptr区别?为什么要引入nullptr?
主流编译器中,NULL 实际上是一个整数常量,被定义为 0,在 C++11 之前,当我们想要将一个指针初始化为空时,我们通常使用 NULL;nullptr 是 C++11 中引入的新的关键字,专门用于表示空指针,它不是整数类型,而是特殊的指针类型nullptr_t。之所以引入nullptr,第一,NULL是整数类型,用户调用foo(NULL)的时候,不能区分调用的是foo(int)还是foo(int*)函数;第二,主流编译器中NULL值为0,通过0表示一个无效地址,但是有的架构下,0地址有特定用途,而nullptr指向的永远是一个无效地址。
(6)为什么noexcept能提高性能?
使用 noexcept 可以让编译器对代码进行优化,从而提高代码的性能。具体来说,为实现异常捕获的功能,c++引入了"栈回退"机制,编译器在编译函数的时候,会为函数生成额外的叫做"栈回退"的代码,使用noexcept 可以避免生成额外的代码来处理异常情况,这样可以减少代码量和执行时间。关闭异常捕获是比较危险的行为,一般只建议用在构造函数。
(7)delete[]是怎样知道数组长度的?
没有标准实现,一种常见的实现方法是,申请内存时,会在返回的指针前面存放这段内存的大小,调用delete[]的时候,就可以知道数组长度了。
(8)new,placement new,operator new的区别?怎么在把对象new在栈上?
operator new作用是分配一块内存,placement new作用是在已分配内存地址处,创建一个对象,new的作用则等于operator new + placement new。先在栈上声明一个数组,然后通过placement new 在这段地址处创建对象,这就实现了在栈上new一个对象。
(9)__cdecl和__stdcall区别?
都是 Microsoft Visual C++ 中用于声明函数调用约定的关键字。__cdecl 是 C/C++ 默认的调用约定,在 __cdecl 调用约定下,参数从右往左入栈,由调用方负责清理堆栈;在__stdcall 调用约定下,函数参数从右向左压入堆栈中,函数堆栈的清除工作由被调用方负责。这些关键字主要用于跨语言调用,以确保参数传递和堆栈清理的一致性。
(10)重载类的delete运算符,delete的时候会发生什么?
new的默认行为是先分配一段内存,然后调用对象的构造函数,把对象创建在这段内存上;delete的默认行为是先调用析构函数,然后释放内存。重载全局new和delete运算符号,会修改所有的new和delete内存行为,重载类的new和delete运算符,会修改针对这个类的new和delete内存行为。
(11)函数调用压栈流程?
不同系统下压栈的具体操作不同,但大致都有这么个过程:函数调用的时候,把被调用函数参数压栈,把预留的返回值存放位置压栈,把当前函数上下文,比如栈地址相关的寄存器和指令地址相关的寄存器内容压栈,函数返回的时候,弹出函数参数和返回值,弹出函数上下文内容到寄存器,恢复现场。
(12)声明和定义的作用,从编译角度说明?
声明的作用主要两点,第一,提供链接时需要的符号信息,这些信息存储在目标文件的重定位表和符号表当中;第二,提供类型大小信息,c++采取的是单文件编译策略,当不知道某个符号对应类型的定义的时候,需要在链接前预留出合适大小的内存空间,供链接时填充。
(13)现代大部分编程语言都没有头文件,c++为什么有头文件?头文件和模块的优劣比较?#include和前置声明的区别?
c++和采取模块机制的编程语言的一个重大区别在于,c++把函数和变量签名这部分信息保存在头文件内,而采取模块机制的编程语言把这部分信息保存在库内。头文件和模块相比,会拷贝很多无用的声明信息到当前文件内,从而导致编译非常慢,另一个缺点就是头文件机制很容易引发符号重定义错误。c++之所以采用头文件机制是因为,早期计算机的内存资源非常珍贵,如果把函数和变量签名信息都保存到二进制库中,会浪费更多的内存资源。
#include和前置声明本质上都是声明,区别在于#include在预处理期间做了一次拷贝声明的操作,前置声明的优势在于可以按需导入函数,而且可以解决循环依赖问题。
(14)C++11为什么引入枚举类?
传统的 C++ 枚举类型会将枚举值暴露在命名空间中,容易造成命名冲突,而枚举类则通过引入了作用域限定符来解决这个问题。其次,传统的 C++ 枚举类型是基于整数的,可以进行隐式的类型转换和比较操作,这可能会导致一些意想不到的错误,而枚举类则可以避免这个问题,因为它们只能进行显式的类型转换和比较操作。
(15)程序是从main 函数开始执行的吗?
不是,程序在执行前,会经历一个从磁盘加载程序到内存的过程,这个过程会执行全局变量的初始化。
(16)虚函数怎么实现的?真的更慢吗?
虚函数是通过虚函数表实现的,每个类都有自己的虚表,对象的首地址处存放有指向虚表的指针。当具体调用哪个虚函数可以在编译期间确定的时候,虚函数不一定更慢。
(17)构造函数、析构函数、重载运算符函数可以是虚函数吗?类成员函数模板可以是虚函数吗?
析构函数和重载运算符函数都可以是虚函数,而构造函数不能是虚函数,首先C++编译器层面不允许这种操作,第二构造函数不需要动态多态,C++引入虚函数的目的就是为了解决编译期间无法确定调用对象的问题,而对于构造函数这类特殊函数,编译期间就已经明确知道需要创建的对象类型。类成员函数模板不能是虚函数,因为C++在链接前是不知道成员函数模板被实例化多少次的,这就会导致编译器无法在编译期间确定虚表的大小。
(18)成员函数指针和普通函数指针区别?
普通函数指针属于指针类型,成员函数指针不是指针类型。通常来说,函数指针的长度等于机器字长,而成员函数指针长度比函数指针更长,其内部存放了对象地址和成员函数地址信息。在没有给出对象地址的情况下,调用成员函数指针会报错。
(19)各种变量存放在虚拟内存的哪个分区?
直接声明的变量、函数实参存储在栈区;new创建的对象,较小的对象存放在堆 区,较大的对象存放在共享内存区;常量和静态变量存放在静态存储区中的非代码区;所有函数存放在静态存储区中的代码区;字符常量也存放在代码区。
(20)对象的内存模型?发生继承时候的对象内存模型?
成员函数存放在代码区;静态成员变量存放在静态存储区;普通成员变量存放在对象内,且按照声明顺序依次存放;如果类声明了虚函数,那么对象的首地址处往往会存放一个指向虚表的指针,另外访问权限关键字可能会影响对象的内存布局,至于怎么影响,标准没有规定,不同编译器的实现可能不同。发生继承的时候,基类对象怎样存放,标准也没有规定,一般是按照继承顺序依次存放在内存当中,每个对象都可以有自己的虚表。
(21)什么是标准布局类型和trivial类型?有什么用?
引入标准布局类型是为了向C语言兼容,使得用户能够通过对象第一个成员的指针类型指向对象;引入trivial类型是为了提高对象初始化效率,memcpy比构造函数初始化效率效率更高。不考虑继承,一个类没有虚函数、所有非静态变量的访问权限相同,则是标准布局类型;不考虑继承,一个类没有自定义构造、自定义析构函数,没有虚函数,则是trivial类型。
(22)什么是类型擦除?实现方式?
类型擦除是一种,使得不同类型变量能够得到统一处理的技术。实现方式上可分为静态类型擦除了动态类型擦除,静态类型擦除通过模板或者宏实现,动态类型擦除可通过继承虚函数或者void类型实现。
(23)什么是多态?实现方式?
多态指的是一种相同的形式表现出不同行为的概念,分为静态多态和动态多态。代码层面,静态多态通过重载(overload)实现,动态多态通过覆盖(override)实现;原理层面,静态多态通过name mangling实现,动态多态通过虚表实现。
(24)inline的作用和原理?
c++17以前,inline关键字主要有两个作用:第一,作为内联优化建议,告诉编译器在调用处展开函数,只不过是否展开函数还是由编译器决定;第二,解决符号重定义问题,不同文件内定义了同签名的函数,若被inline关键字修饰,则不会引发符号重定义错误。c++17开始,inline只保留第二个作用,若用户希望函数内联展开,则可以使用__attribute((always_inline))__ 关键字,它是 GCC 和 Clang 中的一个扩展,用于强制内联函数
原理上,第一,内联展开相比于普通函数调用,少了函数上下文压栈的过程,因此效率更高,缺点就是容易引起代码膨胀。第二,被inline关键字修饰的函数名,编译期间会被标记为weak符号,链接目标文件的时候,多个同签名weak符号不会引发编译器报错,运行期间,会选取其中一个函数进行调用。
(25)inline用作内联展开这层含义的时候,构造函数、析构函数、虚函数可以被inline修饰吗?可以获取inline函数的指针吗?static inline和extern inline含义?
任何函数都可以被inline修饰,包括构造函数、析构函数、虚函数。这里提一下为什么虚函数可以内联,inline函数涉及到的是编译期解析,虚函数地址大多数情况下在运行期解析,但是某些情况下,具体调用哪个虚函数可以在编译期间确定,这个时候虚函数就能内联展开了。
inline只作为内联建议,是否展开由编译器决定,因此是可以获取inline函数指针的。
static inline指的是具有文件作用域的inline函数;extern inline作用比较特殊,外部单元把它当作普通函数进行调用,同单元内把它当作inline函数调用。