C++后台面试常考
(一) C/C++方面
条款01:说说C++中的多态及其实现
条款02:volatile关键字
1、 Volatile关键字和const对应。遇到这个关键字声明的变量,编译器对访问该变量的代码就不再进行优化(不再把变量值放入寄存器中),从而可以提供对特殊地址的稳定访问(直接访问内存)。
2、 volatile用在如下的几个地方: 1) 中断服务程序中修改的供其它程序检测的变量需要加volatile; 2) 多任务(多线程)环境下各任务(线程)间共享的标志应该加volatile; 3) 存储器映射的硬件寄存器通常也要加volatile说明,因为每次对它的读写都可能由不同意义。
3、 和 const 修饰词类似,const 有常量指针和指针常量的说法,volatile 也有相应的概念。可以把一个非volatile int赋给volatile int,但是不能把非volatile对象赋给一个volatile对象。
4、 超链接:https://blog.csdn.net/whatday/article/details/52511071
条款03:带虚函数与否的空类大小
1、 不带虚函数:sizeof为1;带虚函数:sizeof为4(32位)。
2、 为了确保两个不同对象的地址不同,必须如此。类的实例化是在内存中分配一块地址,每个实例在内存中都有独一无二的二地址。同样,空类也会实例化,所以编译器会给空类隐含的添加一个字节,这样空类实例化后就有独一无二的地址了。所以,空类的sizeof为1,而不是0.
3、 带虚函数的类的对象存有虚函数表的地址,故为4(32)或8(64)。
条款04:字节对齐
1、 C++的struct或者class为空,那么字节是1,编译器自动添加了一个字节,为了保证该类型的两个对象在内存上地址是不一样的,这个和C是不一样的,C的空struct是0。
2、 对齐大小为其中类型大小最大的类型为准,但是不会超过4字节(在32位机,以int为准)。
3、 如果存在嵌套关系,对齐大小以基本类型为准。
4、 还可以强制设置对齐大小,如:#pragma pack (size) //size可以取1/2/4/8等2的指数。
5、 紧缩方式其实就是按1字节对齐,和#pragma pack (1) 效果一样。为了防止不同编译器对齐不一样,建议在代码里面指定对齐参数。
条款05:大小端字节序(如何判断)
1、 大端模式:是指数据的高字节保存在内存的低地址中,而数据的低字节保存在内存的高地址端。小端模式,是指数据的高字节保存在内存的高地址中,低位字节保存在在内存的低地址端。
2、 比如一个变量x的十六进制表示为 0x01 23 45 67,如果是大端存储则是这样存储的
高地址--------->低地址
67 45 23 01 数据的高位 放在低地址 低位放在高地址
而小端模式 是这样
01 23 45 67 数据的低位 放在低地址 高位放在高地址
3、 如何判断:1)使用union;2)转化为char类型
条款06:#pragma once的作用?
1、 为了避免同一个文件被include多次,C/C++中有两种方式,一种是#ifndef方式,一种是#pragma once方式。
2、 #ifndef的方式依赖于宏名字不能冲突,这不光可以保证同一个文件不会被包含多次,也能保证内容完全相同的两个文件不会被不小心同时包含。当然,缺点就是如果不同头文件的宏名不小心“撞车”,可能就会导致头文件明明存在,编译器却硬说找不到声明的状况——这种情况有时非常让人抓狂。
3、 #pragma once则由编译器提供保证:同一个文件不会被包含多次。注意这里所说的“同一个文件”是指物理上的一个文件,而不是指内容相同的两个文件。带来的好处是,你不必再费劲想个宏名了,当然也就不会出现宏名碰撞引发的奇怪问题。对应的缺点就是如果某个头文件有多份拷贝,本方法不能保证他们不被重复包含。
4、 #pragma once方式却不受一些较老版本的编译器支持,换言之,它的兼容性不够好。
条款07:static、const的用法?
1、static关键字至少有下列n个作用:
1) 函数体内static变量的作用范围为该函数体,不同于auto变量,该变量的内存只被分配一次,因此其值在下次调用时仍维持上次的值;
2) 在模块内的static全局变量可以被模块内所用函数访问,但不能被模块外其它函数访问;
3) 在模块内的static函数只可被这一模块内的其它函数调用,这个函数的使用范围被限制在声明它的模块内;
4) 在类中的static成员变量属于整个类所拥有,对类的所有对象只有一份拷贝;
5) 在类中的static成员函数属于整个类所拥有,这个函数不接收this指针,因而只能访问类的static成员变量。
2、const关键字至少有下列n个作用:
1) 欲阻止一个变量被改变,可以使用const关键字。在定义该const变量时,通常需要对它进行初始化,因为以后就没有机会再去改变它了;
2) 对指针来说,可以指定指针本身为const,也可以指定指针所指的数据为const,或二者同时指定为const;
3) 在一个函数声明中,const可以修饰形参,表明它是一个输入参数,在函数内部不能改变其值;
4) 对于类的成员函数,若指定其为const类型,则表明其是一个常函数,不能修改类的成员变量;
5) 对于类的成员函数,有时候必须指定其返回值为const类型,以使得其返回值不为“左值”。
条款08:深拷贝和浅拷贝
1、 浅拷贝:默认的拷贝构造函数(包括通过“=”来实现对象复制的)都执行的是浅拷贝。浅拷贝构造函数会将拷贝对象初始化为和源对象一样的对象,但是当类中有指针或引用类型的成员时,在对象被析构时将发生错误。原因是经过浅拷贝后,源对象和拷贝对象中的指针成员将指向同一片内存,在两个对象通过析构函数被撤销时,这片内存将被释放两次,从而发生错误。为了解决这问题,便有了深拷贝。
2、 深拷贝很好了解决了上面提到的问题,通过调用深拷贝构造函数可以为拷贝对象重新动态分配相应的内存空间,这样一来,源对象的指针成员就指向源对象的内存空间,而拷贝对象的指针成员指向拷贝对象的内存空间,在调用析构函数释放内存时,不会发生错误。
条款09:拷贝构造函数与拷贝复制函数需要注意的事项?
1、 考虑是需要深拷贝还是浅拷贝
2、 禁止复制?对底层资源祭出“引用计数法”?复制底部资源?转移底部资源的拥有权?
3、 在operator=中处理“自我赋值”,证同测试、调整顺序、copy-and-swap。
4、 复制对象时勿忘其每一个成分(基类成员)。
条款10:回调函数、可重入函数
1、 回调就是一种利用函数指针进行函数调用的过程,使用回调函数实际上就是在调用某个函数(通常是API函数)时,将自己的一个函数(这个函数为回调函数)的地址作为参数传递给那个函数。而那个函数在需要的时候,利用传递的地址调用回调函数,这时你可以利用这个机会在回调函数中处理消息或完成一定的操作。也就是,函数的参数之一是函数指针。
2、 可重入函数主要用于多任务环境中,一个可重入的函数简单来说就是可以被中断的函数,也就是说,可以在这个函数执行的任何时刻中断它,转入OS调度下去执行另外一段代码,而返回控制时不会出现什么错误;而不可重入的函数由于使用了一些系统资源,比如全局变量区,中断向量表等,所以它如果被中断的话,可能会出现问题,这类函数是不能运行在多任务环境下的
条款11:智能指针、对象管理资源
1、 智能指针std::shared_ptr、std::auto_ptr、std::weak_ptr。
2、 对象管理资源。RAII在构造函数中获得资源,析构函数中释放资源。
条款12:Singleton的正确写法
1、 只能有一个实例对象的类
2、 构造函数是private、本类的private静态成员。
条款13:类型大小(long、int等在32和64位系统中)
条款14:extern关键字
1、extern用于函数或变量的声明,即编译单元A使用编译单元B中的函数或变量时需要extern声明。为了避免不一致,提供方在自己的xxx_pub.h中提供对外部接口的声明,然后调用方include该头文件,从而省去extern这一步。
2、extern “C”使编译器按照C的规则编译该函数。(这是因为C不能重载,没有函数中间名;而C++支持函数重载,有函数中间名)
3、extern和static不能同时修饰一个变量。static修饰的全局变量声明与定义同时进行,也就是说当你在头文件中使用static声明了全局变量后,它也同时被定义了。static修饰全局变量的作用域只能是本身的编译单元,也就是说它的“全局”只对本编译单元有效,其他编译单元则看不到它。
条款15:new和malloc区别
1、 new分配内存按照数据类型进行分配,malloc分配内存按照大小分配;
2、 new不仅分配一段内存,而且会调用构造函数,但是malloc则不会。new的实现原理?但是还需要注意的是,之前看到过一个题说int* p = new int与int* p = new int()的区别,因为int属于C++内置对象,不会默认初始化,必须显示调用默认构造函数,但是对于自定义对象都会默认调用构造函数初始化。翻阅资料后,在C++11中两者没有区别了,自己测试的结构也都是为0;
3、 new返回的是指定对象的指针,而malloc返回的是void*,因此malloc的返回值一般都需要进行类型转化;
4、 new是一个操作符可以重载,malloc是一个库函数;
5、 new分配的内存要用delete销毁,malloc要用free来销毁;delete销毁的时候会调用对象的析构函数,而free则不会;
6、 malloc分配的内存不够的时候,可以用realloc扩容。扩容的原理?new没用这样操作;
7、 new如果分配失败了会抛出bad_malloc的异常,而malloc失败了会返回NULL。因此对于new,正确的姿势是采用try…catch语法,而malloc则应该判断指针的返回值。为了兼容很多c程序员的习惯,C++也可以采用new nothrow的方法禁止抛出异常而返回NULL;
8、 new和new[]的区别,new[]一次分配所有内存,多次调用构造函数,分别搭配使用delete和delete[],同理,delete[]多次调用析构函数,销毁数组中的每个对象。而malloc则只能sizeof(int) * n;
9、 如果不够可以继续谈new和malloc的实现,空闲链表,分配方法(首次适配原则,最佳适配原则,最差适配原则,快速适配原则)。delete和free的实现原理,free为什么直到销毁多大的空间?
条款16:指针和引用的区别
1、 指针保存的是所指对象的地址,引用是所指对象的别名,指针需要通过解引用间接访问,而引用是直接访问;
2、 指针可以改变地址,从而改变所指的对象,而引用必须从一而终;
3、 引用在定义的时候必须初始化,而指针则不需要;
4、 指针有指向常量的指针和指针常量,而引用没有常量引用;
5、 指针更灵活,用的好威力无比,用的不好处处是坑,而引用用起来则安全多了,但是比较死板。
条款17:指针与数组千丝万缕的联系
1、 一个一维int数组的数组名实际上是一个int* const 类型;
2、 一个二维int数组的数组名实际上是一个int (*const p)[n];
3、 数组名做参数会退化为指针,除了sizeof
条款18:智能指针是怎么实现的?什么时候改变引用计数?
1、 智能指针的实现:
1) 构造函数中计数初始化为1;
2) 拷贝构造函数中计数值加1;
3) 赋值运算符中,左边的对象引用计数减一,右边的对象引用计数加一;
4) 析构函数中引用计数减一;
5) 在赋值运算符和析构函数中,如果减一后为0,则调用delete释放对象。
2、 share_prt与weak_ptr的区别
条款19:C++四种类型转换
1、 四种转型:const_cast用于将对象的常量性转除,也就是const对象转为non-const对象;dynamic_const主要执行“安全向下转型”,也就是用来决定某对象是否归属继承体系中的某个类型,效率低下;reinterpret_const意图执行低级转型,例如,将int*转为int,实际动作可能取决于编译器,不可移植;static_const用来强迫隐式转换。
2、 之所以需要dynamic_cast,通常是因为你想在一个你认为derived class对象身上执行derived class操作函数,但你手上却只有一个“指向base”的pointer或reference,你只能靠它们来处理对象。有两个一般性做法可以避免这个问题。
3、 第一,使用容器并在其中存储直接指向derived class对象的指针。这个做法使你无法在同一容器内存储指针“指向所有可能之各种派生类”。可能需要多个容器存储不同的派生类指针。
4、 另一种做法就是在bass class内提供virtual函数,也就是通过多态替代dynamic_cast。
5、 尽量避免转型,如果避无可避,试着将它隐藏于某个函数背后。客户随后可以调用该函数,而不需将转型放进他们自己的代码内。
6、 为什么不使用C的强制转换?C的强制转换表面上看起来功能强大什么都能转,但是转化不够明确,不能进行错误检查,容易出错。
条款20:内联函数有什么优点?内联函数与宏定义的区别?
1、 宏定义在预编译的时候就会进行宏替换;内联函数在编译阶段,在调用内联函数的地方进行替换,减少了函数的调用过程,但是使得编译文件变大。因此,内联函数适合简单函数,对于复杂函数,即使定义了内联编译器可能也不会按照内联的方式进行编译。
2、 内联函数相比宏定义更安全,内联函数可以检查参数,而宏定义只是简单的文本替换。因此推荐使用内联函数,而不是宏定义。
3、 使用宏定义函数要特别注意给所有单元都加上括号,#define MUL(a, b) a * b,这很危险,正确写法:#defineMUL(a, b) ((a) * (b))
条款21:透彻了解inlining的里里外外
1、 使用inline函数可以“免除函数调用成本”,编译器最优化机制通常被设计用来浓缩那些“不含函数调用”的代码,inline函数有机会被优化。
2、 Inline只是对编译器的一个申请,不是强制命令。这项申请可以明确提出(inline +定义式),也可以隐喻的提出(类内定义的成员函数和friend函数)。
3、 Inline是个申请,编译器可以加以忽略,一般以下函数会被拒绝inline:函数太过复杂、virtual函数、构造和析构函数、通过函数指针调用的函数。
4、 Virtual函数不能为inline函数是因为:inline一般发生在编译过程,意味着“执行前,先将调用动作替换为被调用函数的本体(而此时virtual函数还不知道应该调用哪一个);virtual意味”等到运行期才确定调用哪个函数“。
5、 Inline函数无法随着程序库(静止连接)的升级而升级,需要重新编译。大部分调试器面对inline函数都束手无策。
6、 将inlining限制在小型、被频繁调用的函数身上。
条款22:C++内存管理
1、 C++内存分为那几块?(堆区,栈区,常量区,静态和全局区)
2、 每块存储哪些变量?
1、 栈:函数的参数和局部变量是分配在栈上(但不包括static声明的变量)。在函数被调用时,栈用来传递参数和返回值。由于栈的后进先出特点,所以栈特别方便用来保存/恢复调用现场。
2、 堆:用于存放进程运行中被动态分配的内存段,它的大小并不固定,可动态扩张或缩减。当进程调用malloc/free等函数分配内存时,新分配的内存就被动态添加到堆上(堆被扩张)/释放的内存从堆中被剔除(堆被缩减)
3、 全局区(静态区)(static):编译器编译时即分配内存。全局变量和静态变量的存储是放在一块的,初始化的全局变量和静态变量在一块区域。未初始化的全局变量和未初始化的静态变量在相邻的另一块区域。程序结束后由系统释放。
4、 文字常量区:常量字符串就是放在这里的。 程序结束后由系统释放
5、 程序代码区:存放函数体的二进制代码。
3、 学会迁移,可以说到malloc,从malloc说到操作系统的内存管理,说道内核态和用户态,然后就什么高端内存,slab层,伙伴算法,VMA可以巴拉巴拉了,接着可以迁移到fork()。
条款23:定位内存泄露
1、 在windows平台下通过CRT中的库函数进行检测;
2、 在可能泄漏的调用前后生成块的快照,比较前后的状态,定位泄漏的位置
3、 Linux下通过工具valgrind检测
条款24:手写strcpy、手写memcpy函数、手写strcat函数、手写strcmp函数
条款25:如何写可移植的C++程序