面试中常见的C++面试题汇总,快来看看对你是否有帮助!
1.编写完整版的strcpy函数
char * strcpy( char *strDest, const char *strSrc )
断言( (strDest != NULL) && (strSrc != NULL) );
char *地址 = strDest;
while( (*strDest++ = * strSrc++) != '\0' );
退货地址;
关键点:
使用assert断言函数判断参数是否为NULL;
当遇到'\0'时,赋值停止;
返回新字符串的首地址。
2.指出代码错误
无效测试(无效)
char *str = (char *) malloc( 100 );
strcpy( str, "你好" );
自由(str);
... //其他语句省略
有两个错误:
使用malloc分配内存后,应该判断是否分配成功;
free后,str应设置为NULL,以防止其成为野指针。
PS:malloc函数
malloc函数是分配长度为num_bytes字节的内存块的函数。 它可以向系统申请分配指定大小字节的内存空间。 malloc的全称是内存分配,中文称为动态内存分配。 当无法知道内存的具体位置时,如果想要绑定真实的内存空间,就需要使用动态内存分配。
首先,malloc函数返回void *类型。
对于 C++,如果编写: p = malloc (sizeof(int)); 那么程序无法编译,并报错:“void*无法赋值给int *类型变量”。
所以它必须通过 (int *) 进行转换。 对于C来说,没有这样的要求,但是为了让C程序更方便的移植到C++上,建议养成强制转换的习惯。
其次,该函数的实际参数是sizeof(int),用于指定所需的整型数据的大小。
在 Linux 中,您可以使用:malloc(0)。 这是因为Linux中的malloc的下限是16Bytes。 注意malloc(-1)是被禁止的; 但是,某些系统不允许使用 malloc(0)。
malloc 只分配内存,并不能初始化得到的内存,因此获得的新内存中的值将是随机的。
3. 提供 if 语句,用于分别将 BOOL、int、float 和指针变量与“零值”进行比较。
BOOL类型变量:if(!var)
int类型变量:if(var==0)
浮点型变量:
常量浮点 EPSINON = 0.00001;
if ((x >= - EPSINON) && (xi:首先通过a和i的值0x000n找到指针a的地址0x000m,a类中的偏移量offset,得到地址0x000n + a->i的偏移量,继续*( 0x000n + offset) = 10 赋值操作,即内存0x000n + offset的值为10。
24.一个类包含static、virtual等,请介绍一下这个类的内存分配情况?
1.静态修饰符
1)静态修饰成员变量
对于非静态数据成员,每个类对象都有自己的副本。 静态数据成员被视为类的成员。 无论定义多少个类,都只有一份静态数据成员的副本,该副本由该类型的所有对象(包括其派生类)共享。 因此,静态数据成员的值对于每个对象都是相同的,并且其值可以更新。
由于静态数据成员在全局数据区分配内存,并被属于该类的所有对象共享,因此它们不属于特定的类对象,可以在生成类对象之前使用。
2)静态修饰成员函数
与普通成员函数相比,静态成员函数没有this指针,因为它们不与任何对象关联。 从这个意义上来说,它不能访问属于类对象的非静态数据成员,也不能访问非静态成员函数,只能调用其他静态成员函数。
静态修饰的成员函数在代码区分配内存。
2.C++继承和虚函数
C++多态分为静态多态和动态多态。 静态多态是通过重载和模板技术实现的,并在编译时确定。 动态多态是通过虚函数和继承关系实现的,动态绑定是在运行时执行和确定的。
动态多态实现有几个条件:
(1)虚函数;
(2) 基类的指针或引用指向派生类的对象;
当基类指针调用成员函数(虚函数)时,它会查找该对象的虚函数表。 虚函数表的地址位于每个对象的首地址。 在虚函数表中找到该函数的指针并调用它。
每个对象中保存的只是一个指向虚函数表的指针。 C++内部为每个类维护一个虚函数表,该类的对象都指向同一个虚函数表。
为什么我们可以在虚函数表中准确找到对应的函数指针呢? 因为在类设计时,虚函数表是直接继承自基类的。 如果其中一个虚函数被覆盖,虚函数表的指针就会被替换,这样就可以根据指针准确地找到要调用哪个函数。
3.虚拟修改器
如果一个类是局部变量,则该类的数据存储在栈区中。 如果通过new/malloc动态申请一个类,则该类的数据存储在堆区中。
如果该类是从virutal继承的子类,则该类的虚函数表指针与该类的其他成员存储在一起。 虚函数表指针指向只读数据段中的类虚函数表。 虚函数表一一存储函数指针,函数指针指向代码段中的具体函数。
如果类中的成员是虚拟属性,则父类对应的属性将被隐藏。
25. 静态变量什么时候初始化?
静态变量存储在虚拟地址空间的data段和bss段中。 在C语言中,它们是在代码执行之前初始化的,属于编译时初始化。 在C++中,由于对象的引入,对象的生成必须调用构造函数。 因此,C++规定,当且仅当第一次使用对象时,才应构造全局或局部静态对象。
26. TCP如何保证可靠性?
TCP保证可靠性:
(1) 序列号、确认响应、超时重传
当数据到达接收方时,接收方需要发送确认响应以表明该数据段已被接收,确认序列号将指示其接下来需要接收的数据序列号。 如果发送方长时间没有收到确认响应,则可能是发送的数据丢失或者确认响应丢失。 在这种情况下,发送方会等待一定时间后重新发送。 这个时间一般为2*RTT(分段往返时间)+偏差值。
(2)窗口控制和高速重发控制/快速重发(重复确认响应)
TCP会使用窗口控制来提高传输速度,这意味着在一个窗口大小内,不必等待响应就可以发送下一段数据。 窗口大小是无需等待确认即可继续发送数据的最大值。 如果不使用窗口控制,则必须重新发送未收到确认的每个数据。
使用窗口控制,如果1001-2000数据段丢失,则每次发送后续数据时,确认响应都会继续发送序号为1001的响应,表示我要接收从1001开始的数据。如果发送方收到相同的响应3次,将立即重传; 但也有一种情况,可能收到了所有数据,但丢失了一些响应。 这种情况下,不会进行重传,因为发送端知道,如果数据段丢失,接收端不会放手,疯狂提醒……
(3)拥塞控制
如果窗口设置的很大,发送方连续发送大量数据,可能会造成网络拥塞(大家都在使用互联网,而你在这里疯狂发送消息,吞吐量就只有那么大,所以当然会被阻塞),甚至造成网络拥塞。 麻痹。 因此,TCP进行拥塞控制来防止这种情况的发生。
慢启动:定义拥塞窗口,最初将窗口大小设置为1,然后每次收到确认响应时(在一个rtt之后)将拥塞窗口大小增加2。
拥塞避免:设置慢启动阈值,一开始一般设置为65536。 拥塞避免是指当拥塞窗口大小达到该阈值时,拥塞窗口的值不再按指数增加,而是加性增加(对于每个确认/每个RTT,拥塞窗口大小+1)以避免拥塞。
将报文段超时重传视为拥塞。 一旦发生超时重传,我们需要首先将阈值设置为当前窗口大小的一半,将窗口大小设置为初始值1,然后重新进入慢启动过程。
快速重传:当遇到3个重复确认(高速重传控制)时,表示已经收到3个报文段,但前面的报文段已经丢失,会立即重传。
然后,首先将阈值设置为当前窗口大小的一半,然后将拥塞窗口大小设置为慢启动阈值+3。
这样可以实现:TCP通信时,网络吞吐量逐渐增加,随着拥塞减少吞吐量,然后进入缓慢增加的过程,网络就不会轻易瘫痪。
27.红黑树与AVL树的定义、特点及区别
平衡二叉树(AVL树):
平衡二叉树又称AVL树,是一种特殊的二叉排序树。 左右子树都是平衡二叉树,左右子树的高度差的绝对值不超过1。一句话来说,左右子树的高度差的绝对值以树中所有节点为根的树不超过1。二叉树上某个节点的左子树深度减去右子树深度的值称为平衡因子BF。 那么平衡二叉树上所有节点的平衡因子只能是-1、0和1。只要二叉树中某个节点的平衡因子绝对值大于1,则该二叉树是不平衡的。
红黑树:
红黑树是二叉搜索树,只不过每个节点都增加了一个存储位来表示该节点的颜色,可以是红也可以是黑(要么红,要么黑)。 通过限制从根到叶子的任何路径上每个节点的着色方式,红黑树确保没有路径的长度是任何其他路径的两倍。 因此,红黑树是一种弱平衡二叉树,相对于要求严格的AVL树来说,它的旋转次数较少,所以当查找、插入、删除操作较多时,红黑树是通常使用。
自然:
1. 每个节点不是红色就是黑色
2、根节点为黑色;
3、每个叶子节点(叶子节点是NULL指针或者树尾的NULL节点)都是黑色的;
4. 如果一个节点是红色的,那么它的子节点必须是黑色的。
5、对于任意节点,到叶子点树的NULL指针的每条路径都包含相同数量的黑色节点;
区别:
AVL树是高度平衡的。 频繁的插入和删除会导致频繁的重新平衡,导致效率下降。 红黑树不是高度平衡的,这是一种妥协。 插入最多可以旋转两圈,删除最多可以旋转三圈。
28.map和unordered_map的优缺点
对于map来说,其底层是基于红黑树实现的。 优点如下:
1)有序性,这是地图结构的最大优点。 其元素的有序性将简化许多应用中的许多操作。
2)地图查找、删除、添加等一系列操作的时间复杂度稳定,为logn。
其缺点如下:
1)查找、删除、添加等操作的平均时间复杂度较慢且与n有关
对于unordered_map来说,它的底层是一个哈希表。 优点如下:
查找、删除、添加速度快,时间复杂度为常数级O(c)
其缺点如下:
由于unordered_map内部基于哈希表,以(key,value)对的形式存储,因此空间占用较高。
Unordered_map的查找、删除、添加的时间复杂度不稳定,平均为O(c),具体取决于哈希函数。 极端情况下可能是 O(n)
29. Top(K) 问题
1.直接全部排序(只有内存足够的情况下才适用)
当数据量较小时,可以将所有数据容纳在内存中。 最简单最容易想到的办法就是对所有的数据进行排序,然后取出排序后的数据中的前K条。
该方法对数据量敏感。 当数据量较大时,内存无法完全容纳所有数据,这种方法不适合。 即使内存能够满足要求,这种方法对所有数据进行排序,而题目只需要找到前K个数据,所以这种方法效率不是很高,不推荐。
2.快速排序的变种(仅在内存足够的情况下使用)
这是基于快速排序的变体,因为第一种方法说对所有元素进行排序效率不是很高,只需要找到前K个最大的即可。
这种方法类似于快速排序。 首先选择一个分割元素,将比这个分割元素大的元素放在它的前面,比这个分割元素小的元素放在它的后面。 此时,一次排序就完成了。 如果此时这个分区元素的序号索引正好等于K,那么这个分区元素和它左边的数字正好是前K个最大的元素; 如果index > K,则前K大的数据在index的左边,则从index-1号开始继续递归排序; 如果index < K,则从划分元素右侧继续排序,直到发现序号index正好等于K。排序完前K个数后,返回Top K个元素。 这种方法避免了对除Top K元素之外的数据进行排序所带来的不必要的开销。
3.最小堆法
这是一种部分消除法。 首先读取前K个数字并构建一个最小堆。 然后将剩余的所有数字依次与堆顶进行比较。 如果小于或等于堆顶,则继续与下一个比较; 否则,删除堆顶元素,向堆中插入新数据,并重新调整最小堆。 遍历完所有数据后,最小堆中的数据就是最大的K数。
4.分而治之
将所有数据分为N部分,假设每部分数据都可以读入内存进行处理,并找到每部分数据中最大的K个数。 此时还剩下N*K条数据。 如果内存无法容纳N*K条数据,则继续分治过程,将其分为M部分,并找到每条数据中最大的K数。 如果M*K数仍然无法读入内存,则分而治之处理将继续。 在将剩余数字读入内存之前,可以使用快速排序或合并排序的变体来处理这些数字。
5. 哈希方法
如果这个数据中有很多重复的数据,可以先用hash的方法去掉重复的数字。 这样,如果重复率高的话,就会减少很多内存的使用,从而减少计算空间。 如果处理后的数据可以读入内存,则可以直接排序; 否则,可以采用分而治之法或最小堆法来处理数据。
30. 栈和堆有什么区别,为什么栈更快?
堆和栈的区别:
堆从低地址向高地址扩展; 堆栈从高地址向低地址扩展。
堆中的内存需要手动申请和释放; 栈中的内存由操作系统自动申请和释放,用于存储参数、局部变量等内存。
频繁调用堆中的malloc和free会产生内存碎片,降低程序效率; 而堆栈由于其先进后出的特性,不会产生内存碎片。
堆分配效率低,栈分配效率高
堆栈效率高的原因:
栈是操作系统提供的一种数据结构。 计算机底层为栈提供了一系列的支持:分配专门的寄存器来存储栈的地址,有专门的压入栈和压入栈的指令; 而堆是由C/C++函数库提供的,机制复杂,需要分配内存、合并内存、释放内存等一系列算法,效率较低。
31、编写一个函数,在main函数执行之前运行。
__attribute((构造函数))void before()
printf("main 之前\n");
32. extern“C”的作用是什么?
C++需要extern C来调用C函数,因为C语言没有函数重载。
33.STL迭代器删除元素
1、对于序列容器vector和deque,使用erase(itertor)后,后续每个元素的迭代器都会失效,但后续每个元素都会向前移动一个位置,但erase会返回下一个有效迭代。 设备;
2、对于关联的容器映射集合,使用erase(iterator)后,当前元素的迭代器失效,但其结构是红黑树。 删除当前元素不会影响下一个元素的迭代器,所以在调用erase之前记录下一个元素的迭代器即可。
3、对于list来说,它使用不连续分配的内存,它的erase方法也会返回下一个有效的迭代器。
34.向量和列表的区别和应用是什么?
一、概念:
1)矢量
连续存储的容器、动态数组、在堆上分配空间
底层实现:数组
双倍容量增加:
向向量添加(插入)新元素时,如果没有超出当前容量且还有空间,则直接添加到末尾(在指定位置插入),然后调整迭代器。
如果没有剩余空间,则会重新配置两倍于原始元素数量的空间,然后将原始空间元素通过复制初始化到新空间,然后将元素添加到新空间中。 最终,原有的空间将被破坏并释放。 迭代器将无效。
表现:
访问:O(1)
插入:插入到最后(有足够的空间):非常快
末尾插入(空间不够):需要内存申请和释放,以及复制之前的数据。
中间插入(空间足够):内存复制
中间插入(空间不够):需要内存申请和释放,以及复制之前的数据。
删除:最后 删除:很快
中途删除:内存复制
适用场景:频繁随机访问、不频繁插入和删除非尾节点。
2. 清单
动态链表在堆上分配空间。 每次插入元素都会分配空间,每次删除元素都会释放空间。
底层:双向链表
表现:
访问:随机访问性能很差,只能快速访问头尾节点。
插入:速度快,开销通常恒定
删除:非常快,通常开销恒定
适用场景: 频繁插入、删除大量数据
2、区别:
1)vector的底层实现是数组; 列表是一个双向链表。
2)Vector支持随机访问,但List不支持。
3)向量是顺序内存,列表不是。
4)向量在中间节点的插入和删除会引起内存复制,但列表不会。
5)Vector一次性分配内存,不足时只扩展2倍; 每次插入新节点时,list都会申请内存。
6)Vector随机访问性能好,但插入和删除性能较差; list的随机访问性能较差,但插入和删除性能良好。
3. 申请
Vector拥有连续的内存空间,因此支持随机访问。 如果需要高效的随机访问,并且不关心插入和删除的效率,就使用vector。
List具有不连续的内存空间。 如果需要高效的插入和删除,并且不关心随机访问,则应该使用list。
35. STL中的resize和reserve有什么区别?
resize():改变当前容器(size())所包含的元素数量,eg:vectorv; v.调整大小(len); v 的大小变为 len。 如果v的原始大小小于len,则容器添加(len -size)个元素,该元素的值默认为0。 v.push_back(3);之后,3被放在v的末尾,即下标为len,此时容器大小为len+1;
Reserve():改变当前容器的最大容量(capacity)。 它不会生成元素,只是决定容器允许放入多少个对象。如果reserve(len)的值大于当前的capacity(),那么就会重新分配一块能量。 为len对象节省空间,然后通过复制构造函数复制之前的v.size()对象,并销毁之前的内存。
36. 将源代码转换为可执行文件的过程是什么?
1)预编译
主要处理源代码文件中以“#”开头的预编译指令。处理规则见下文
1.删除所有#defines并展开所有宏定义。
2. 处理所有条件预编译指令,例如“#if”、“#endif”、“#ifdef”、“#elif”和“#else”。
3. 处理“#include”预编译指令并将文件内容替换到其位置。 这个过程是递归执行的,并且该文件包含其他文件。
4. 删除所有注释“//”和“/**/”。
5.保留所有#pragma编译器指令,编译器需要使用它们,如:#pragma Once是为了防止文件被重复引用。
6.添加行号和文件标识符,方便编译器在编译过程中生成调试用的行号信息,并在编译过程中出现编译错误或警告时显示行号。
2)编译
对预编译后生成的xxx.i或xxx.ii文件进行一系列词法分析、语法分析、语义分析和优化后,生成相应的汇编代码文件。
1.词法分析:使用类似于“有限状态机”的算法,将源代码程序输入到扫描器中,将字符序列划分为一系列标记。
2.语法分析:语法分析器对扫描器生成的token进行语法分析,生成语法树。 解析器输出的语法树是以表达式为节点的树。
3、语义分析:语法分析器只完成表达式的语法层面的分析,而语义分析器则判断表达式是否有意义。 它分析的语义是静态语义——可以在编译时暂存的语义。 相应的动态语义是只能在运行时确定的语义。
4.优化:源代码层面的优化过程。
5、目标代码生成:代码生成器将中间代码转换为目标机器代码,并生成一系列代码序列——汇编语言表示。
6.目标代码优化:目标代码优化器对上述目标机器代码进行优化:寻找合适的寻址方式、用位移代替乘法运算、删除冗余指令等。
3) 组装
将汇编代码转换为机器可以执行的指令(机器代码文件)。 汇编器的汇编过程比编译器简单。 没有复杂的语法,没有语义,也不需要指令优化。 只是根据汇编指令和机器指令的对照表一一翻译出来的。 组装过程有一个汇编器。 已完成。 编译后生成目标文件(与可执行文件格式几乎相同)xxx.o(Windows下)和xxx.obj(Linux下)。
4) 链接
将不同源文件生成的目标文件链接起来形成可执行程序。 链接分为静态链接和动态链接:
1.静态链接:
函数和数据被编译成二进制文件。 在使用静态库的情况下,当编译和链接可执行文件时,链接器从库中复制这些函数和数据,并将它们与应用程序的其他模块组合以创建最终的可执行文件。
浪费空间:因为每个可执行程序都必须拥有所有所需目标文件的副本,如果多个程序依赖同一个目标文件,那么内存中就会存在同一个目标文件的多个副本;
更新困难:每当修改库函数的代码时,都需要重新编译链接形成可执行程序。
运行速度快:但是静态链接的优点是可执行程序已经具备了执行程序所需的一切,并且在执行过程中运行速度很快。
2.动态链接:
动态链接的基本思想是将程序按照模块分割成相对独立的部分,并在程序运行时将它们链接在一起形成完整的程序,而不是像静态链接那样将所有程序模块链接成单个程序。 可执行文件。
共享库:即使每个程序需要依赖同一个库,该库也不会像静态链接那样在内存中拥有多个副本,而是这些多个程序在执行过程中会共享同一个副本;
更新方便:更新时只需替换原来的目标文件即可,无需重新链接所有程序。 当程序下次运行时,新版本的目标文件将自动加载到内存中并链接,程序将完成升级目标。
性能损失:由于链接被推迟到程序运行时,每次执行程序时都需要进行链接,所以在性能上会有一定的损失。
37. TCP为什么不能握手两次? 为什么不使用四次呢?
两次是不可能的:TCP 是全双工通信。 两次握手只能确认单向数据链路可以通信,但不能保证反向通信正常。
不需要四次:
本来握手应该和挥手一样,需要确认两个方向都可以连接。 原来的模型应该是:
1、客户端发送syn0到服务器
2、服务器收到syn0并回复ack(syn0+1)
3.服务器发送syn1
4. 客户端收到syn1并回复ack(syn1+1)
因为tcp是全双工的,所以上面四步确认了数据在两个方向都能正确到达,但是步骤2和3没有上下连接。 它们可以合并以加快握手效率,一切都变成3步握手。
面试找工作并不是一朝一夕就能完成的事情,失败的面试经历也不一定是坏事。 积累面试经验也是一种进步。 我希望这可以帮助你。
要了解IT相关内容,请查找“工作坐标在线”
评论列表