共计 5161 个字符,预计需要花费 13 分钟才能阅读完成。
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<int> val = {1,2,3,4,5,6};
vector<int>::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<int> valset = {1,2,3,4,5,6};
set<int>::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 <stdlib.h>
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 ,
技术是凋谢的,咱们的心态,更应是凋谢的。拥抱变动,背阴而生,致力向前行。
我是 小魔童哪吒,欢送点赞关注珍藏,下次见~