鹅厂cpp linux后台服务器开发岗面试总结(含答案)

码农天地 -
鹅厂cpp linux后台服务器开发岗面试总结(含答案)
1、堆和栈的区别

堆空间的内存是动态分配的,一般存放对象,并且需要手动释放内存malloc()/free()或new/delete。

栈空间的内存是由系统自动分配,一般存放局部变量,比如对象的地址等值,不需要程序员对这块内存进行管理,比如,函数中的局部变量的作用范围(生命周期)就是在调完这个函数之后就结束了。

2、free/delete与malloc/free区别new分配内存按照数据类型进行分配,malloc分配内存按照指定的大小分配;new不仅分配一段内存,而且会调用构造函数,malloc不会。(面试官强调)new分配的内存要用delete销毁,malloc要用free来销毁;delete销毁的时候会调用对象的析构函数,而free则不会。(面试官强调)new返回的是指定对象的指针,而malloc返回的是void*,因此malloc的返回值一般都需要进行类型转化。new是一个操作符可以重载,malloc是一个库函数。malloc分配的内存不够的时候,可以用realloc扩容。扩容的原理?new没用这样操作。new如果分配失败了会抛出bad_malloc的异常,而malloc失败了会返回NULL。申请数组时: new[]一次分配所有内存,多次调用构造函数,搭配使用delete[],delete[]多次调用析构函数,销毁数组中的每个对象。而malloc则只能sizeof(int) * n。3、指针常量与常量指针

指针常量——指针类型的常量(int *const p)。本质上一个常量,指针用来说明常量的类型,表示该常量是一个指针类型的常量。在指针常量中,指针自身的值是一个常量,不可改变,始终指向同一个地址。在定义的同时必须初始化。

常量指针——指向“常量”的指针(const int p, int const p)。常量指针本质上是一个指针,常量表示指针指向的内容,说明该指针指向一个“常量”。在常量指针中,指针指向的内容是不可改变的,指针看起来好像指向了一个常量。

需要C/C++ Linux高级服务器架构师学习资料后台加群812855908(包括C/C++,Linux,golang技术,Nginx,ZeroMQ,MySQL,Redis,fastdfs,MongoDB,ZK,流媒体,CDN,P2P,K8S,Docker,TCP/IP,协程,DPDK,ffmpeg等)

4、指针与引用的区别指针有自己的一块空间,而引用只是一个别名;使用sizeof看一个指针的大小是4,而引用则是被引用对象的大小;指针可以被初始化为NULL,而引用必须被初始化且必须是一个已有对象 的引用;作为参数传递时,指针需要被解引用才可以对对象进行操作,而直接对引 用的修改都会改变引用所指向的对象;可以有const指针,但是没有const引用;指针在使用中可以指向其它对象,但是引用只能是一个对象的引用,不能被改变;(面试官强调)指针可以有多级指针(**p),而引用至于一级;指针和引用使用++运算符的意义不一样;如果返回动态内存分配的对象或者内存,必须使用指针,引用可能引起内存泄露。5、struct与union数据内存对齐,内存对齐的作用

结构体struct内存对齐的3大规则:

对于结构体的各个成员,第一个成员的偏移量是0,排列在后面的成员其当前偏移量必须是当前成员类型的整数倍;结构体内所有数据成员各自内存对齐后,结构体本身还要进行一次内存对齐,保证整个结构体占用内存大小是结构体内最大数据成员的最小整数倍;如程序中有#pragma pack(n)预编译指令,则所有成员对齐以n字节为准(即偏移量是n的整数倍),不再考虑当前类型以及最大结构体内类型。
struct CAT_s
{
    int ld;
    char Color;
    unsigned short Age;
    char *Name;
    void(*Jump)(void);
}Garfield;

按照上面的3大规则直接来进行分析:

使用32位编译,int占4, char 占1, unsigned short 占2,char* 占4,函数指针占4个,由于是32位编译是4字节对齐,所以该结构体占16个字节。(说明:按几字节对齐,是根据结构体的最长类型决定的,这里是int是最长的字节,所以按4字节对齐);使用64位编译 ,int占4, char 占1, unsigned short 占2,char* 占8,函数指针占8个,由于是64位编译是8字节对齐,(说明:按几字节对齐,是根据结构体的最长类型决定的,这里是函数指针是最长的字节,所以按8字节对齐)所以该结构体占24个字节。

联合体union内存对齐的2大规则:

找到占用字节最多的成员;union的字节数必须是占用字节最多的成员的字节的倍数,而且需要能够容纳其他的成员.
//x64
typedef union {
    long i;
    int k[5];
    char c;
}D

要计算union的大小,首先要找到占用字节最多的成员,本例中是long,占用8个字节,int k[5]中都是int类型,仍然是占用4个字节的,然后union的字节数必须是占用字节最多的成员的字节的倍数,而且需要能够容纳其他的成员,为了要容纳k(20个字节),就必须要保证是8的倍数的同时还要大于20个字节,所以是24个字节。

内存对齐作用:

平台原因(移植原因):不是所有的硬件平台都能访问任意地址上的任意数据的;某些硬件平台只能在某些地址处取某些特定类型的数据,否则抛出硬件异常。性能原因:数据结构(尤其是栈)应该尽可能地在自然边界上对齐。原因在于,为了访问未对齐的内存,处理器需要作两次内存访问;而对齐的内存访问仅需要一次访问。6、TCP与UDP协议区别,具体的底层协议,属于哪一层,TCP握手协议的实现

TCP与UDP属于传输层

7、说出自己常用的容器,并给出常用的一些标准库用法

vector stack queue set

8、map容器(面试官强调)

C++ map用法参考链接

9、map与hush_map区别

hash_map和map的区别参考链接

10、给你一亿个数据如何统计IP地址出现的次数

由于电脑内存不够,所以不能一次将数据全部读取,只能实行分而治之的方法,才能满足空间的需求。可以创建一个hash表,将数据按一定的Key value分类,可以使用了将IP转化成长整形,然后取其后三位转化成字符串作为Key value,这样就能将所有IP按后三位分类。

11、vector与list的区别,以及各种操作的复杂度

1.概念

1)vector:

连续存储的容器,动态数组,在堆上分配空间。

底层实现:数组。

两倍容量增长:vector 增加(插入)新元素时,如果未超过当时的容量,则还有剩余空间,那么直接添加到最后(插入指定位置),然后调整迭代器。如果没有剩余空间了,则会重新配置原有元素个数的两倍空间,然后将原空间元素通过复制的方式初始化新空间,再向新空间增加元素,最后析构并释放原空间,之前的迭代器会失效。

性能:

访问:时间复杂度O(1)

插入:在最后插入(空间够):很快。时间复杂度O(1)。

在最后插入(空间不够):需要内存申请和释放,以及对之前数据进行拷贝。时间复杂度O(n)。

在中间插入(空间够):内存拷贝

在中间插入(空间不够):需要内存申请和释放,以及对之前数据进行拷贝。

删除:在最后删除:很快。

在中间删除:内存拷贝。

适用场景:经常随机访问,且不经常对非尾节点进行插入删除。

2)list:

动态链表,在堆上分配空间,每插入一个元数都会分配空间,每删除一个元素都会释放空间。

底层:双向链表。

性能:

访问:随机访问性能很差,只能快速访问头尾节点。

插入:很快,一般是常数开销。

删除:很快,一般是常数开销。

适用场景:经常插入删除大量数据。

2.区别

vector底层实现是数组;list是双向 链表。vector支持随机访问,list不支持。vector是顺序内存,list不是。vector在中间节点进行插入删除会导致内存拷贝,list不会。vector一次性分配好内存,不够时才进行2倍扩容;list每次插入新节点都会进行内存申请。vector随机访问性能好,插入删除性能差;list随机访问性能差,插入删除性能好。

3.应用

vector拥有一段连续的内存空间,因此支持随机访问,如果需要高效的随即访问,而不在乎插入和删除的效率,使用vector。

list拥有一段不连续的内存空间,如果需要高效的插入和删除,而不关心随机访问,则应使用list。

12、排序,以及排序代码实现逻辑,以及复杂度。

13、覆盖与重载的区别。

重载:两个函数名相同,但是参数列表不同(个数,类型),返回值类型没有要求,在同一作用域中。

重载的实现是:编译器根据函数不同的参数表,对同名函数的名称做修饰,然后这些同名函数就成了不同的函数(至少对于编译器来说是这样的)。如,有两个同名函数:function func(p:integer):integer;和function func(p:string):integer;。那么编译器做过修饰后的函数名称可能是这样的:int_func、str_func。对于这两个函数的调用,在编译器间就已经确定了,是静态的(记住:是静态)。也就是说,它们的地址在编译期就绑定了(早绑定),因此,重载和多态无关!

覆盖:子类继承了父类,父类中的函数是虚函数,在子类中重新定义了这个虚函数

真正和多态相关的是“覆盖”。当子类重新定义了父类的虚函数后,父类指针根据赋给它的不同的子类指针,动态(记住:是动态!)的调用属于子类的该函数,这样的函数调用在编译期间是无法确定的(调用的子类的虚函数的地址无法给出)。因此,这样的函数地址是在运行期绑定的(晚邦定)。结论就是:重载只是一种语言特性,与多态无关,与面向对象也无关!引用一句Bruce Eckel的话:“不要犯傻,如果它不是晚邦定,它就不是多态。”

14、进程与线程的区别。

1.基本概念:

进程是对运行时程序的封装,是系统进行资源调度和分配的的基本单位,实现了操作系统的并发;

线程是进程的子任务,是CPU调度和分派的基本单位,用于保证程序的实时性,实现进程内部的并发;线程是操作系统可识别的最小执行和调度单位。每个线程都独自占用一个虚拟处理器:独自的寄存器组,指令计数器和处理器状态。每个线程完成不同的任务,但是共享同一地址空间(也就是同样的动态内存,映射文件,目标代码等等),打开的文件队列和其他内核资源。

2.区别:

一个线程只能属于一个进程,而一个进程可以有多个线程,但至少有一个线程。线程依赖于进程而存在。进程在执行过程中拥有独立的内存单元,而多个线程共享进程的内存。(资源分配给进程,同一进程的所有线程共享该进程的所有资源。同一进程中的多个线程共享代码段(代码和常量),数据段(全局变量和静态变量),扩展段(堆存储)。但是每个线程拥有自己的栈段,栈段又叫运行时段,用来存放所有局部变量和临时变量。)进程是资源分配的最小单位,线程是CPU调度的最小单位;系统开销: 由于在创建或撤消进程时,系统都要为之分配或回收资源,如内存空间、I/O设备等。因此,操作系统所付出的开销将显著地大于在创建或撤消线程时的开销。类似地,在进行进程切换时,涉及到整个当前进程CPU环境的保存以及新被调度运行的进程的CPU环境的设置。而线程切换只须保存和设置少量寄存器的内容,并不涉及存储器管理方面的操作。可见,进程切换的开销也远大于线程切换的开销。通信:由于同一进程中的多个线程具有相同的地址空间,致使它们之间的同步和通信的实现,也变得比较容易。进程间通信IPC,线程间可以直接读写进程数据段(如全局变量)来进行通信——需要进程同步和互斥手段的辅助,以保证数据的一致性。在有的系统中,线程的切换、同步和通信都无须操作系统内核的干预进程编程调试简单可靠性高,但是创建销毁开销大;线程正相反,开销小,切换速度快,但是编程调试相对复杂。进程间不会相互影响 ;线程一个线程挂掉将导致整个进程挂掉进程适应于多核、多机分布;线程适用于多核15、进程间的通信方式。

进程间通信主要包括管道、系统IPC(包括消息队列、信号量、信号、共享内存等)、以及套接字socket(面试官强调)。

16、读写锁、自旋锁与死锁。

读写锁实际是一种特殊的自旋锁,它把对共享资源的访问者划分成读者和写者,读者只对共享资源进行读访问,写者则需要对共享资源进行写操作。这种锁相对于自旋锁而言,能提高并发性,因为在多处理器系统中,它允许同时有多个读者来访问共享资源,最大可能的读者数为实际的逻辑CPU数。写者是排他性的,一个读写锁同时只能有一个写者或多个读者(与CPU数相关),但不能同时既有读者又有写者。

有了原子操作,就可以了制作控制临界区的锁机制了。自旋锁就是其中的一个代表。自旋锁机制可以用门和锁的例子来比喻。进程执行到某个临界区,相当于要进入一栋房子,这是进程会检查屋内是否有人(进程),如果屋内没有人,则直接拿起钥匙进入并把门锁上(进入临界区);如果屋内有人(进程),则在门口等待(忙等待)屋内的进程出来再进去。可以看出,自旋锁最多只能被一个进程持有,如果有新的进程希望获取自旋锁,它将会一直忙等待直到前一个持有自旋锁的进程释放锁。

在多道程序系统中,由于多个进程的并发执行,改善了系统资源的利用率并提高了系统 的处理能力。然而,多个进程的并发执行也带来了新的问题——死锁。所谓死锁是指多个进程因竞争资源而造成的一种僵局(互相等待),若无外力作用,这些进程都将无法向前推进。

特别申明:本文内容来源网络,版权归原作者所有,如有侵权请立即与我们联系(cy198701067573@163.com),我们将及时处理。

Tags 标签

加个好友,技术交流

1628738909466805.jpg