C++面试题
语言相关基础题
对象复用的了解,零拷贝的了解
对象复用
指得是设计模式,对象可以采用不同的设计模式达到复用的目的,最常见的就是继承和组合模式了。
零拷贝:
零拷贝主要的任务就是避免CPU将数据从一块存储拷贝到另外一块存储,主要就是利用各种零拷贝技术,避免让CPU做大量的数据拷贝任务,减少不必要的拷贝,或者让别的组件来做这一类简单的数据传输任务,让CPU解脱出来专注于别的任务。这样就可以让系统资源的利用更加有效。
零拷贝技术常见linux中,例如用户空间到内核空间的拷贝,这个是没有必要的,我们可以采用零拷贝技术,这个技术就是通过mmap,直接将内核空间的数据通过映射的方法映射到用户空间上,即物理上共用这段数据。
介绍C++所有的构造函数
默认构造函数、一般构造函数、拷贝构造函数
- 默认构造函数(无参数):如果创建一个类你没有写任何构造函数,则系统会自动生成默认的构造函数,或者写了一个不带任何形参的构造函数
- 一般构造函数:一般构造函数可以有各种参数形式,一个类可以有多个一般构造函数,前提是参数的个数或者类型不同(基于c++的重载函数原理)
- 拷贝构造函数参数为类对象本身的引用,用于根据一个已存在的对象复制出一个新的该类的对象,一般在函数中会将已存在对象的数据成员的值复制一份到新创建的对象中。参数(对象的引用)是不可变的(const类型)。此函数经常用在函数调用时用户定义类型的值传递及返回。
为什么要内存对齐?
- 平台原因(移植原因):不是所有的硬件平台都能访问任意地址上的任意数据的;某些硬件平台只能在某些地址处取某些特定类型的数据,否则抛出硬件异常。
- 性能原因:数据结构(尤其是栈)应该尽可能地在自然边界上对齐。原因在于,为了访问未对齐的内存,处理器需要作两次内存访问;而对齐的内存访问仅需要一次访问。
成员初始化列表的概念,为什么用成员初始化列表会快一些?
类型一 : 内置数据类型,复合类型(指针,引用)
类型二 : 用户定义类型(类类型)
对于类型一,在成员初始化列表和构造函数体内进行,在性能和结果上都是一样的
对于类型二,结果上相同,但是性能上存在很大的差别
因为类类型的数据成员对象在进入函数体是已经构造完成,也就是说在成员初始化列表处进行构造对象的工作,这是调用一个构造函数,
在进入函数体之后,进行的是 对已经构造好的类对象的赋值,又调用个拷贝赋值操作符才能完成(如果并未提供,则使用编译器提供的默认按成员赋值行为)
简单的来说:
对于用户定义类型:
- 如果使用类初始化列表,直接调用对应的构造函数即完成初始化
- 如果在构造函数中初始化,那么首先调用默认的构造函数,然后调用指定的构造函数
所以对于用户定义类型,使用列表初始化可以减少一次默认构造函数调用过程
c/c++ 程序调试方法
- printf 大法(日志)
自己封装宏函数,进行打印出错位置的文件,行号,函数
通过gcc -DDEBUG_EN 打开调试信息输出
#ifdefine DEBUG_EN
#define DEBUG(fmt, args...) \
do { \
printf("DEBUG:%s-%d-%s "fmt, __FILE__, __LINE__, __FUNCTION__, ##args);\
}while(0)
#define ERROR(fmt, args...) \
do { \
printf("ERROR:%s-%d-%s "fmt, __FILE__, __LINE__, __FUNCTION__, ##args);\
}while(0)
#else
#define DEBUG(fmt, args) do{}while(0)
#define ERROR(fmt, args) do{}while(0)
#endif
- core-dump/map 调试
当程序运行的过程中异常终止或崩溃,操作系统会将程序当时的内存状态记录下来,保存在一个文件中,这种行为就叫做Core Dump.
MAP 文件是程序的全局符号、源文件和代码行号信息的唯一的文本表示方法,是整个程序工程信息的静态文本,通常由linker生成。
- gdb
通过运行程序,打断点、单步、查看变量的值等方式在运行时定位bug。
file <文件名> | 加载被调试的可执行程序文件 |
---|---|
b <行号>/<函数名称> | 在第几行或者某个函数第一行代码前设置断点 |
r | 运行 |
s | 单步执行一行代码 |
n | 执行一行代码,执行函数调用(如果有) |
c | 继续运行程序至下一个断点或者结束 |
p<变量名称> | 查看变量值 |
q | 退出 |
引用是否能实现动态绑定,为什么引用可以实现
因为对象的类型是确定的,在编译期就确定了,指针或引用是在运行期根据他们绑定的具体对象确定。
在什么情况下系统会调用拷贝构造函数:(三种情况)
(1)用类的一个对象去初始化另一个对象时
(2)当函数的形参是类的对象时(也就是值传递时),如果是引用传递则不会调用
(3)当函数的返回值是类的对象或引用时
左值和右值
左值:可以对表达式取地址,有名字的的值就是左值,一般指表达式结束后依然存在的持久对象
右值:不能对表达式取地址,没有名字的值,就是右值,一般指表达式结束后不再存在的临时对象
- 纯右值 --- 用于识别临时变量和一些不跟对象关联的值
- 将亡值 --- 具有转移语义的对象
右值引用可以实现转移语义和完美转发的新特性
c++的访问限定符
- public
- protected
- private
在类的内部,不受限定符号的约束,可以随意访问,在类的外部,只能访问类中public的成员
struct 和 class 的区别
struct 和class 都可以定义类,struct连的成员权限都是public的
map
底层实现:红黑树
- map -- 无序map
key 和 value , key 是不可以重复的,所有元素的值都会自动排序,key不允许重复
- unordered map -- 有序map
key 和 value , key是可以重复的,所有元素的值都会自动排序,key不允许重复
vector
连续存储的容器,内存分配在堆上面,动态数组
底层实现:数组
两倍容量增长:vector一次性分配好内存, 在增加新元素的时候,如果没有超过当前的容量,那么直接添加,然后调整迭代器,如果超过了当前的容量, 则vector会重新配置原数组的内存的2倍空间,将原空间元素内存拷贝到新空间,释放掉原空间,且此时迭代器会失效
性能:
- 查询访问的时候:O(1)
- 插入的时候:
插入末尾:空间不够,则需要申请内存,和释放原空间,对数据进行拷贝
空间够,则直接插入,速度很快
插入中间:空间不够,则需要申请内存,和释放原空间,对数据进行拷贝
空间够,内存拷贝
删除数据的时候: 删除中间的数据,需要内存拷贝
删除尾巴的数据,很快
- 适用场景:经常随机方案,且不对非尾部节点进行插入和删除
list
动态链表,内存分配在堆上,每增加一个数据,则会开辟一个数据的空间,删除一个数据,则会释放掉一个数据的空间
底层实现:双向链表
- 访问:性能很差,只能快速访问头尾节点
- 插入:很快,常数的时间
- 删除:很快,常数的时间
- 适用场景:大量增删的场景
set
集合,所有元素都会根据元素的值进行排序,且不允许重复
底层实现:红黑树(一种平衡二叉树)
适用场景:有序不重复集合
迭代器
迭代器是类模版,表现的像指针。封装了指针的一些行为,重载了指针的++/--/->/*
等操作符号,相当于一种智能指针。可以根据不同的数据结构,来实现 ++ 和 -- 操作
terator模式
是运用于一种聚合对象的模式,把不同集合内的访问逻辑抽象出来,使得不暴露对象的内部结构而达到遍历集合的效果- 运用范围:底层聚合支持类,如vector,stack,list及
ostream_iterator
的扩展
迭代器时如何删除元素的?
- 对于vector,deque序列容器来说,内存是连续分配的,使用erase(iteraotor)后,后边的迭代器都会失效,删除一个元素,会导致后面的元素全部向前移动一个位置,但是 erase方法会返回下一个有效的iterator,如
vector val = { 1,2,3,4,5,6 };
vector::iterator iter;
for (iter = val.begin(); iter != val.end(); )
{
if (3 == *iter)
iter = val.erase(iter); //返回下一个有效的迭代器,无需+1
else
++iter;
}
- 对于关联容器如:map,multimap,set,multiset,内存是随机分配的,删除当前的iterator,仅仅是当前的iterator失效而已,只要在erase的时候,iterator递增即可。因为map之类的容器,底层实现是红黑树,插入和删除一个节点,对其他节点没有影响,如
set valset = { 1,2,3,4,5,6 };
set::iterator iter;
for (iter = valset.begin(); iter != valset.end(); )
{
if (3 == *iter)
valset.erase(iter++);
else
++iter;
}
- 对于list容器来说,是不连续分配的内存,且list调用erase方法,是可以返回下一个有效的iterator,因此可以使用方法1 和 方法2
epoll原理
- 调用epoll_create方法,创建epoll对象
- 再使用epoll_ctrl方法,操作epoll对象,把需要操作的文件描述符添加进去进行监控,这些文件描述符会以epoll_event结构体的形式组成一颗红黑树,阻塞epoll_wait
- 当某个fd有事件发生时,内核就会把该fd事件结构体放到链表中,返回发生事件的链表
resize 和 reverse
resize 是改变容器内含有元素的数量,它会创建元素,且会将值默认为0,如果resize后需要追加数据,则是在尾部追加
reverse 是改变容器的最大容量,它不会创建元素
编译与底层 c++源文件到可执行文件经历的过程
预处理阶段:将源代码文件中头文件,宏定义进行分析和替换,生成预编译文件
编译阶段:将预编译文件转换成特定的汇编代码,生成汇编文件
汇编阶段:将编译阶段的汇编文件转换成机器码,生成可重定位目标文件
链接阶段:将多个目标文件及所需的库链接成最终的可执行文件
编译过程及内存管理
""和<>的区别
"" :
- 先从当前头文件目录中找
- 编译器设置的头文件 (可以显式的 是用 -I来指定)
- 在系统变量的CPLUS_INCUCLUDE_PATH/C_INCLUDE_PATH中指定的头文件路径
<>:
- 编译器设置的头文件 (可以显式的 是用 -I来指定)
- 在系统变量的CPLUS_INCUCLUDE_PATH/C_INCLUDE_PATH中指定的头文件路径
malloc原理
向内存申请一块连续可用的空间,并返回指向这块空间的指针
- 如果开辟成功,则返回一个指向开辟好空间的指针
- 如果开辟失败,则返回一个NULL指针,因此malloc的返回值一定要做检查
- 返回值的类型是void* ,所以malloc函数并不知道开辟空间的类型,具体在使用的时候使用者自己来决定
- 如果参数 size 为0,malloc的行为是标准未定义的,取决于编译器
- 头文件均为#include
calloc
向内存申请一块连续可用的空间,并返回指向这块空间的指针
void* calloc(size_t num, size_t size);
- 功能是为 num 个大小为 size 的元素开辟一块空间,并且把空间的每个字节初始化为0
- 与函数 malloc 的区别只在于 calloc 会在返回地址之前把申请的空间的每个字节初始化为全0
realloc
向内存申请一块续可用的空间,并返回指向这块空间的指针
void* realloc(void* ptr, size_t size);
- ptr 是要调整的内存地址
- size 是调整之后新大小
- 返回值为调整之后的内存起始位置
- 这个函数在调整原内存空间大小的基础上,还会将原来内存中的数据移动到的空间
- realloc在调整内存空间的时候存在两种情况
情况1:原有空间之后有足够大的空间
情况2:原有空间之后没有足够大的空间
当是情况1
的时候,要扩展内存就直接在原有内存之后直接追加空间,原来空间的数据不发生变化。
当是情况2
的时候,原有空间之后没有足够多的空间时,扩展的方法是:在堆空间上另找一个合适大小的连续空间来使用。这样函数返回的是一个新的内存地址。
free
用来释放动态开辟的内存
void free(void* ptr);
- 如果参数 ptr 指向的空间不是动态开辟的,那free函数的行为是未定义的
- 如果参数 ptr 是NULL指针,则函数什么事都不做
欢迎点赞,关注,收藏
朋友们,你的支持和鼓励,是我坚持分享,提高质量的动力
好了,本次就到这里,下一次 后端纯干货面试题整理 I I ,
技术是开放的,我们的心态,更应是开放的。拥抱变化,向阳而生,努力向前行。
我是小魔童哪吒,欢迎点赞关注收藏,下次见~