C++常见面试题/笔试收录(一)
==========================================
真实面试环境中,被问到的相关问题,感兴趣的可以看下
==========================================
1. c和c++的联系和区别?
联系:
C++ 是基于 C 语言发展的,基本兼容 C 语言代码;
两者都支持过程式编程,都能操作底层硬件资源;
语法上很多关键字和结构相似。
区别
- C语言是面向过程的,而C++是面向对象的
- C语言的标准库有限,但C++的标准库丰富
- C语言不支持函数重载,而C++支持函数重载
- C语言需要手动管理内存资源,而C++可以通过RALL管理资源,还支持对象的构造析构
- C语言不支持异常处理,而C++中有异常捕获处理机制
2. 说一下c中malloc和c++ new的区别?
- 相同点:都是从堆上申请空间,并且需要用户手动释放。
- 不同点:
malloc和free是函数,new和delete是操作符
malloc申请的空间不会初始化,new申请的空间会初始化,即调用构造函数
malloc申请空间时,需要手动计算空间大小并传递,new只需在其后跟上空间的类型即可
malloc的返回值是void*,在使用时必须强转,new不需要,因为new后跟的是空间的类型malloc申请失败时,返回的是NULL,因此使用时必须判空,而new不需要,但是new需要捕获异常
3. 说一下c中的static和c++ static的联系和区别?
- C 中主要用于限制作用域和延长生命周期,
- C++ 中除了这些,还用于定义类的静态成员变量和函数。
4. C++ 面向对象的三大特征是?
封装: 把数据和操作数据的函数绑定在一起,隐藏内部实现,只暴露必要接口,提升安全性和可维护性。
继承:子类可以复用父类的成员,实现代码复用和结构扩展。
多态:通过基类指针或引用调用派生类的函数,实现“同一接口多种实现”。
5. 什么是重载、重写(覆盖)、重定义(隐藏)?
- 重载 : a. 两个函数在同一作用域,b.函数名相同,参数不同(类型,顺序,个数)
- 重写(覆盖):
两个函数分别在基类和派生类的作用域
函数名/参数/返回值都必须相同(协变除外) -> 简称三同
特殊点1: 两个函数都必须是虚函数(其实子类可以不用加virtual)
特殊点2: 如果返回值不同,则必须是父类的指针或引用 - 重定义(隐藏)
两个函数分别在基类和派生类的作用域
函数名相同
两个基类和派生类的同名函数不构成重写,那么就是重定义(隐藏)
6. 虚函数和纯虚函数的联系和区别?
联系:
都定义在基类中,允许派生类重写(override)。
都通过基类指针或引用调用派生类的函数,实现运行时多态。
都必须使用关键字
virtual
声明。
虚函数是可选重写,能实例化,纯虚函数是必须重写,不能实例化;
包含纯虚函数的类是抽象类,不能直接创建对象。
7. STL/用过哪些c++库哪些?
我用过 STL、Boost、Qt、OpenCV,还有 SQLite 等库,主要用于界面开发、数据处理和通信相关功能
8. -1的源码 反码 补码
- 源码:1000 0000 0000 0000 0000 0000 0000 0001
- 反码:1111 1111 1111 1111 1111 1111 1111 1110
- 补码:1111 1111 1111 1111 1111 1111 1111 1111
9. static关键字,在函数中声明静态变量和在类中声明静态变量的区别
- 在函数中时,改变了变量的生命周期和作用域,使其只能在当前文件中访问
- 在类中时:只能在类外进行定义和初始化,且不需要使用private、public、protected 访问规则
被类的所有对象所共享
3. 声明和定义的区别
声明是告诉编译器名字的存在
而定义是为名字分配内存并实现其功能
在使用变量或函数之前,必须先进行声明后定义
4. 数组和链表的区别
- 数组支持随机访问,链表不支持
- 但是链表的插入删除操作效率高,数组的插入删除效率低
5. 最小堆的插入删除查找的时间复杂度
- 插入O(logN),删除O(logN),查找O(n)
6. 请实现归并排序
void my_sort(vector<int>& nums,int l,int r){if(l==r){return;}int mid = (l+r)>>1;my_sort(nums,l,mid);my_sort(nums,mid+1,r);int begin1 = l,end1 = mid;int begin2 = mid+1,end2 = r;vector<int> tmp;while(begin1 <= end1 && begin2 <= end2){if(nums[begin1] < nums[begin2]){tmp.push_back(nums[begin1++]);}else{tmp.push_back(nums[begin2++]);}}while(begin1 <= end1){tmp.push_back(nums[begin1++]);}while(begin2 <= end2){tmp.push_back(nums[begin2++]);}// 拷贝回去for(int i = 0;i < tmp.size();i++){nums[l+i] = tmp[i];}}
7. 说一下const关键字
- const 修饰成员变量,定义成 const 常量,相较于宏常量,可进行类型检查,节省内存空间,提高了效率。
- const 修饰函数参数,使得传递过来的函数参数的值不能改变。
- const 修饰成员函数,使得成员函数不能修改任何类型的成员变量(mutable 修饰的变量除外),也不能调用非 const 成员函数,因为非 const 成员函数可能会修改成员变量。
8. 关于malloc的几个常见没有释放内存的错误
对空指针进行解引用操作
对开辟出来的空间的越界访问
内存使用完,没有进行释放(内存泄漏)
释放内存时,指针没有指向原来空间的首地址(释放开辟的空间的部分内存)
对非动态开辟的内存进行释放
对同一块内存的多次释放
9. 说一下extern 关键字
extern外部变量: 叫做外部声明,被extern修饰的变量,会告诉编译器这个变量的定义需要再其他文件中查找
10. 实现strcpy
char* my_strcpy(char* des, const char* sou)
{assert(des && sou);char* ret = des;while (*des++ = *sou++){;}return ret;
}
11. 找出不大于n的最大质数
sqrt优化被除数
bool isPrimeNum(int i)
{// 使用sqrt缩短范围int j = 0;for (j = 2; j < sqrt(i) + 1; j++) {if (i % j == 0) {return false;}}return true;
}
12. 1000个数范围是[0,999],有2个相同的数,请设计算法找出来
关键点:使用容器bitset,使用test函数,set函数
#include <bitset>
int function(vector<int>&v)
{bitset<1024*8>bt;for (auto e : v) {if (bt.test(e)) {return e;}bt.set(e);}return 0;
}
13. 如何解决hash冲突
链地址法: 对于相同的哈希值,使用链表进行挂起
再哈希法:提供多个哈希函数,如果第一个哈希函数计算出来的key的哈希值冲突了,则使用第二个哈希函数计算key的哈希值
建立公共溢出区: 将哈希表分为基本表和溢出表两部分,凡是和基本表发生冲突的元素,一律填入溢出表
开放定址法:将冲突的hash值不停的映射成新的哈希值,直到不冲突为止
- 线性探测再散列(向后面不断遍历)
- 平方探测再散列(平方跳动)
- 伪随机探测再散列(随机跳动)
15. map底层实现
- map是关联式容器,底层使用红黑树作为底层数据结构实现,而红黑树是一种自平衡的二叉搜索树,可以保持元素的有序,并且插入,删除,查找等操作的时间复杂度都是O(logN)
16. 哈希底层是什么
- 哈希表使用哈希函数将元素映射到一个桶中,每个桶中存储一个链表或者红黑树,哈希表的插入,删除,查找等操作的平均时间复杂度是O(1),但是最坏情况下可能达到O(N);
17. 各大排序时间复杂度
- 插入希尔都是O(N^2)
- 堆是O(N*logN),选择O(N^2)
- 冒泡是O(N^2),快速排序O(N*logN)
- 归并是O(N*logN);
18. 各大排序应用场景
- 插入排序:小规模,部分有序的数据
- 希尔排序: 中小规模数据
- 堆排序:任意规模的数据
- 选择排序: 小规模数据,实际效率较低
- 冒泡排序:小规模数据,实际效率较低
- 快速排序:任意规模的数据
19. 各个容器应用场景
- vector:常用于需要频繁添加或删除元素尾部的情况
- list: 适用于频繁插入或删除元素
- map 和 unordered_map: 常用于字典或配置项存储。
- set 和 unordered_set: 适用于去重或集合运算
- stack和queue: 只允许在一端进行插入和删除
20. 什么是野指针,以及如何避免
- 野指针: 不确定指向地址空间的指针,和未进行初始化的指针
- 指针定义时进行初始化,动态分配内存后,必须及时手动释放内存
21. 说一下结构体对齐
编译器将程序中的每个“数据单元”安排在字的整数倍的地址指向的内存之中
内存对齐的原则:
- 结构体变量的首地址 = min(最宽基本类型大小,对齐基数)的整除
- 其他成员的地址偏移量 = min(成员大小,对齐基数)的整数倍 + 填充字节
- 总大小 = min(最宽基本类型大小,对齐基数) + 填充字节
22. 代码编译过程
- 预处理:头文件的展开,宏替换,去注释
- 编译:把C/C++语言变成汇编代码;
- 汇编:通过汇编变成以.o结尾的目标二进制文件(不可执行)
- 链接:多个目标文件和连接库进行链接的,从而生成可执行的程序 .exe 文件。
23. 代码如何判断大小端
int main() {int a = 1;char* p = (char*) & a;//char* -- 访问一个字节(第一个地址)if (*p == 1){printf("小端\n");}else{printf("大端\n");}return 0;
}
- 小端:低地址在低地址
- 大端:低地址在高地址
24. 用2个栈实现队列
class Solution
{
public:void push(int value) {in_st.push(value);}int pop() {int tmp = 0;if (!out_st.empty()) {tmp = out_st.top();out_st.pop();}else {// 转移数据while (!in_st.empty()) {out_st.push(in_st.top());in_st.pop();}tmp = out_st.top();out_st.pop();}return tmp;}
private:stack<int>in_st;stack<int>out_st;
};
设计思路:
- 一个in_stack,一个out_statck
- push数据,直接push进in_stack;
- pop数据,如果out_stack有数据之间pop,没有就数据转移将in_stack中的数据转换到out_stack中,再pop
25. 用2个队列实现栈
class Solution
{
public:void push(int value) {if (q1.empty()) {q1.push(value);}else {q2.push(value);}}void pop() {if (q1.empty() && q2.empty()) {cout << "没有数据" << endl;}else if (q1.empty()) {while (q2.size() != 1) {q1.push(q2.front());q2.pop();}cout << q2.front() << endl;}else {while (q1.size() != 1) {q2.push(q1.front());q1.pop();}cout << q1.front() << endl;}}
private:queue<int>q1;queue<int>q2;
};
设计思路:
- 一个q1,一个q2
- push数据,直接找一个空队列,直接push
- pop数据,找一个非空队列,只留一个数据在非空队列中,其他数据转移到另一个空队列中
最后再将这个数据pop出来
26. 说一下C++读文件和写文件
28. 什么是多态
就是多种形态,具体点就是去完成某个行为,当不同的对象去完成时会,产生出不同的状态
- 派生类对象的地址可以赋值给基类指针。对于通过基类指针调用基类和派生类中都有的同名、同参数表的虚函数的语句,编译时并不确定要执行的是基类还是派生类的虚函数;
- 而当程序运行到该语句时,如果基类指针指向的是一个基类对象,则基类的虚函数被调用,
- 如果基类指针指向的是一个派生类对象,则派生类的虚函数被调用。这种机制就叫作“多态(polymorphism)
静态多态(编译阶段确定)
- 函数重载 和 函数模板 的使用
动态多态(运行阶段确定)
派生类 和 虚函数 的使用
29. 继承和多态区别与联系?
- 不同点: 继承是子类使用父类的方法,而多态则是父类使用子类的方法。
- 继承是为了重用代码,有效实现代码重用,减少代码冗余
- 多态是一种接口继承,增强接口的扩展性
31. 多态的实现原理?
- 每一个类中都有一个vfptr,它是一张虚函数的表,本质就是一个函数指针数组
- 多态调用:运行时去指向对象的虚表中找到函数地址,并进行调用(在符合多态的两个条件时,)
32. inline函数可以是虚函数吗?
- 不可以,但是inline只是一个建议,当一个函数是虚函数以后,多态调用中,inline会直接失效
33. 静态成员可以是虚函数吗?
- 不可以 ,因为 静态成员函数没有this指针 ,使用类型::成员函数的调用方式无法访问虚函数表,所以静态成员函数无法放进虚函数表
34. 构造函数可以是虚函数吗
- 不可以,virtual函数为了实现多态,运行时去虚表中找对应虚函数进行调用
- 而对象中虚表指针都是构造函数初始化列表阶段才初始化的
- 所以构造函数的虚函数是没有意义的
35. 析构函数可以是虚函数吗
- 可以,建议基类的析构函数定义成虚函数
- 析构函数名都会被处理成destructor,所以这里析构函数完成了虚函数重写
- 当基类的指针指向派生类对象的时候,发生多态,然后基类和派生类就会统一析构
如果不将基类的析构函数定义为虚函数的话,那么派生类的析构函数就无法执行。
36. 拷贝构造 和 operator=可以是虚函数?
- 拷贝构造不可以,拷贝构造也是构造函数,同上
- operator=可以但是没有什么实际价值
37. 对象访问普通函数快还是虚函数更快?
- 不构成多态, 是一样快的。
- 构成多态,则调用的普通函数快,
- 因为构成多态,运行时调用虚函数需要到虚函数表中去查找
38. 虚函数是在什么阶段生成的,存在哪的?
编译阶段就生成好的,存在代码段(常量区)
39. 什么是抽象类?抽象类的作用?
- 在虚函数的后面写上 =0 ,则这个函数为纯虚函数。包含纯虚函数的类叫做抽象类
- 抽象类强制重写了虚函数,另外抽象类体现出了 接口继承关系 。
- 统一接口、实现多态
40. 什么是菱形继承?菱形继承的问题是什么?
- 菱形继承是多继承的一种特殊情况,两个子类继承同一个父类,而又有子类同时继承这两个子类,我们称这种继承关系为菱形继承
- 菱形继承因为子类对象当中会有两份父类的成员,因此会导致数据冗余和二义性的问题
41. 进程的虚拟内存分配模型
- 栈区:由编译器自动分配和释放,存放为运行函数分配的局部变量,函数参数,返回数据,返回地址等,其操作类似于数据结构中的栈。
- 堆区:一般由程序员自动分配,如果程序员没有释放,程序结束时可能有OS回收。其分配类似于链表。
- 全局区(静态区static):存放全局变量,静态变量,常量。结束后由系统释放。
- 常量区(字符串常量区):存放常量字符串,程序结束后由系统释放。
- 代码区:存放函数体(类成员函数和全局区)的二进制代码
42. 堆和栈的区别
- 存储位置:栈存储局部变量和函数调用,位于内存的栈区;堆用于动态分配内存,位于堆区
- 分配方式:栈由系统自动管理,分配和释放速度快;堆需要程序员手动分配和释放,速度较慢
- 内存管理:栈内存自动回收;堆内存需要显式释放,否则可能发生内存泄漏
- 大小限制:栈的内存较小,容易溢出;堆的内存较大,但可能导致碎片化。
43. final(使该类不可被子类继承) -> 加在父类上
说明一下:
- 在一个父类的成员函数的后面+final使其变成不可被子类继承
44. override(判读子类是否发生了虚函数的重写) ->加在子类上
#include <iostream>
using namespace std;class Car{
public:virtual void Drive1() {}void Drive2(){}
};class Benz :public Car {
public:// 检查子类虚函数是否完成重写virtual void Drive1() override { cout << "Benz-舒适" << endl; }virtual void Drive2() override { cout << "Benz-舒适" << endl; }
};int main()
{Benz A;return 0;
}
- 在子类后面+override,判断这个子类的虚函数是否发生了虚函数的重写
45. 什么是内存泄漏
- 由于疏忽或错误导致的程序未能释放已经不再使用的内存
46. 智能指针有哪几种?智能指针的实现原理?
unique_ptr: 禁掉了拷贝构造和赋值重载
shared_ptr: 共同管理一段空间,引用计数 记录管理者
weak_ptr: 为了解决shared_ptr循环引用的问题,使它的next和prev不增加计数
47. C++11 引入了什么新增加的内容
智能指针, auto类型推导, lambda表达式,右值引用
48. sizeof 和 strlen 的区别
strlen
是头文件 中的函数,sizeof
是 C++ 中的运算符。strlen
测量的是字符串的实际长度(其源代码如下),以\0
结束。
而sizeof
测量的是字符数组的分配大小。- sizeof会计算\0
49. explicit 的作用
#include <iostream>
#include <cstring>
using namespace std;class A
{
public:int var;explicit A(int tmp){var = tmp;cout << var << endl;}
};
int main()
{A ex(100);A ex1 = 10; // error: conversion from 'int' to non-scalar type 'A' requestedreturn 0;
}
- 禁掉隐式类型转换
50. static修饰静态成员变量
静态成员变量是在类内进行声明,在类外进行定义和初始化,在类外进行定义和初始化的时候不要出现 static 关键字和private、public、protected 访问规则。
静态成员变量相当于类域中的全局变量,被类的所有对象所共享,包括派生类的对象。
51. static修饰静态成员函数
- 静态成员函数不能调用非静态成员变量或者非静态成员函数,因为静态成员函数没有 this 指针。
- 静态成员函数能做为类作用域的全局函数。
- 静态成员函数不能声明成虚函数(virtual)、const 函数和 volatile 函数。
52. define 和 typedef 的区别
- define不进行类型安全的检查,而typedef会进行类型安全的检查
- define没有作用域的限制,而typedef有作用域的限制
54. malloc 的原理?malloc 的底层实现?
- 当开辟的空间小于 128K 时,调用 brk() 函数,通过移动 _enddata 来实现;
- 当开辟空间大于 128K 时,调用 mmap() 函数,通过在虚拟地址空间中开辟一块内存空间来实现
55. C 和 C++ struct 的区别?
- 一个是自定义数据类型,而另一个是抽象数据类型
- C语言中的struct不能包含成员函数,C++中的struct可以包含成员函数,区别在于访问控制
55. volatile 的作用?是否具有原子性,对编译器有什么影响?
- volatile 的作用:
当对象的值可能在程序的控制或检测之外被改变时,应该将该对象声明为 violatile,
告知编译器不应对这样的对象进行优化
volatile不具有原子性。 - volatile 对编译器的影响:
使用该关键字后,编译器不会对相应的对象进行优化,应该在真实的物理地址空间中拿数据
即不会将变量从内存缓存到寄存器中 - 防止多个线程有可能使用内存中的变量,也有可能使用寄存器中的变量,从而导致程序错误。
56. extern C 的作用?
当 C++ 程序 需要调用 C 语言编写的函数,
57. 为什么用成员初始化列表会快一些?
- C++ 规定,对象的成员变量的初始化动作发生在进入构造函数本体之前
- 所以初始化列表在没有进入函数体之前,就将成员变量设为初始值了
- 总之就是一个进入了函数体,另一个没有进入函数体
58. 友元函数的作用
- 作用: 让普通的成员函数可以访问类中的私有成员和保护成员
60. 左值和右值的区别?两者之前如何转换
#include <iostream>
#include <utility>
using namespace std;int main()
{// 左值: 能取地址int* p = new int(0);int b = 1;const int c = 2;// 对左值的左值引用int*& rp = p;int& rb = b;const int& rc = c;int& pvalue = *p;// 右值:不能取地址10;1.1 + 2.2;// 对右值的右值引用int&& rr1 = 10;double&& rr2 = 1.1 + 2.2;// 对右值的左值引用// double& r1 = 1.1 + 2.2;// errorconst double& r1 = 1.1 + 2.2;// 对左值的右值引用//int&& rr5 = b;// errorint&& rr5 = move(b);return 0;
}
- 能取地址的叫左值,不能取地址的叫右值
- 如果对右值使用左值引用,需要加上const
- 如果对左值使用右值引用,需要是使用move函数
61. 什么是野指针和悬空指针?
- 野指针: 不确定指向地址空间的指针,和未进行初始化的指针
- 悬空指针: 这个指针指向的地址空间已经被释放了,但这个指针还是指向原来的那段地址空间
62. 指针和引用的区别
从使用场景来说
- 引用和指针使用场景基本一样,但是链表的链式结构是引用无法代替的,只能使用指针
从语法特性来说
- 引用在定义时必须初始化,指针没有要求。
- 引用在初始化时引用一个实体后,就不能再引用其他实体,
而指针可以在任何时候指向任何一个同类型实体。 - 没有NULL引用,但有NULL指针。
- 在sizeof中的含义不同:引用的结果为引用类型的大小,
但指针始终是4或者8字节 - 引用进行自增操作就相当于实体增加1,
而指针进行自增操作是指针向后偏移一个类型的大小。 - 有多级指针,但是没有多级引用。
- 访问实体的方式不同,引用是编译器自己处理,而指针需要显示解引用,
- 引用比指针使用起来相对更安全
63. 说一说c++的强制类型转换
static_cast关键字 -> 隐式类型转换
reinterpret_cast关键字 -> 强制类型转换
const_cast关键字->取消变量的const属性
dynamic_cast关键字->父类指针 转换 子类指针(保证安全性)
64. 写一下单例模式
class Singleton
{
public:static Singleton* GetInstance(){return &m_instance;}
private:// 构造函数私有Singleton() {};// C++11 : 防拷贝Singleton(Singleton const&) = delete;Singleton& operator=(Singleton const&) = delete;static Singleton m_instance;// 声明
};Singleton Singleton::m_instance;// 定义
多线程下
#include<iostream>
#include<thread>
#include<mutex>
using namespace std;
class Singleton
{
public:static Singleton* GetInstance(){// 保护第一次,后续不需要加锁// 双检查加锁if (_pInstance == nullptr){unique_lock<mutex> lock(_mtx);if (_pInstance == nullptr){_pInstance = new Singleton;}}return _pInstance;}private:// 构造函数私有Singleton(){};// C++11Singleton(Singleton const&) = delete;Singleton& operator=(Singleton const&) = delete;static Singleton* _pInstance;static mutex _mtx;
};Singleton* Singleton::_pInstance = nullptr;
mutex Singleton::_mtx; int main()
{Singleton::GetInstance();Singleton::GetInstance();return 0;
}
65. 用过哪些c++的库
标准库 STL, Boost , Qt ,OpenCV , MySQL C++ API
67. c++什么时候用智能指针
当需要自动管理动态内存,避免手动 delete
时,用智能指针。
68. 简述指针常量与常量指针区别?
指针常量,不可以改变指针指向,能解引用
常量指针,可以改变指针指向,但不能解引用
69. 说一下c++面向对象的几个特征
- 封装保护数据,继承复用代码,多态提高扩展性。
70. 如何让一个类不能实例化?
- 删除构造函数
- 抽象基类中的纯虚函数
- 私有构造 + 无友元
71. C++的空类有哪些成员函数
构造函数 析构函数 拷贝构造 赋值重载
移动构造函数(C++11 起)移动赋值运算符(C++11 起)
72. 动态绑定是如何实现的?
动态绑定主要通过虚函数表(vtable)和虚指针(vptr)实现。
- 每一个类中都有一个vfptr,它是一张虚函数的表,本质就是一个函数指针数组
- 多态调用:运行时去指向对象的虚表中找到函数地址,并进行调用(在符合多态的两个条件时,)
73. 如果析构函数里面抛出异常会怎么样?
- 调用
std::terminate()
,程序直接崩溃,异常终止
74. new 失败会怎样,不想抛出异常该怎么办 ?
默认情况下,
new
申请内存失败会抛出std::bad_alloc
异常。如果不想抛异常,可以用nothrow 版本的 new:
75. c++中是如何管理内存空间的
C++ 通过栈自动管理局部变量,堆由程序员手动申请释放,静态区存放全局和静态变量,代码区存放程序指令
76. 宏函数和内联函数的区别?
- 宏函数是简单替换,不安全;内联函数由编译器优化,更安全、更推荐。
77. 栈的原理你有了解过吗?
- 栈通过先进后出方式管理局部变量和函数调用,自动分配释放,底层靠栈帧和栈指针实现,效率高但空间有限
78. 用过哪些算法?
- 快速、归并、堆排、二分、哈希表等等
79. 迭代器是怎么用的?
- 迭代器像“智能指针”:
begin()
取起点,end()
取终点,++
走、*
解引用, - 更多的使用场景是:用范围 for 高效、安全地遍历和操作容器。
82. 实现快速排序
void QuickSort(int* a, int left, int right)
{if (left >= right)return;// ... 单趟排序int keyi = left;int prev = left, cur = left + 1;while (cur <= right){if (a[cur] < a[keyi]&& ++prev != cur){Swap(&a[prev], &a[cur]);}++cur;}Swap(&a[keyi], &a[prev]);keyi = prev;// [left, right]// [left, pivot-1] pivot [pivot+1, right]// 左子区间和右子区间有序,我们就有序了,如果让他们有序呢? 分治递归QuickSort(a, left, keyi - 1);QuickSort(a, keyi + 1, right);
}
83. 直接插入排序
void InsertSort(int* a, int n)
{for (int i = 0; i < n - 1; ++i){//一个数的插入int end = i;int tmp = a[end + 1];//记录需要插入的值while (end >= 0){if (a[end] > tmp){//向后移动a[end + 1] = a[end];--end;}else{break;}}//end有可能会减到-1a[end + 1] = tmp;// [0, end]有序 end+1位置的值插入[0, end],让[0, end+1]有序}
}
84. 红黑树五大性质
- 每个结点不是红色就是黑色
- 根结点是黑色的
- 如果一个结点是红色的,则它的两个孩子结点是黑色的(红色的俩俩不能相见)
- 对于每个结点,从该结点到其所有后代叶子结点的简单路径上,均包含相同数目的黑色结点
- 每个叶子结点都是黑色的(哨兵节点为黑色)
85. 请简要描述一下什么是回调函数,有什么用途
函数作为参数传入另一函数中的函数,在特定事件发生时被调用。
可以用在事件响应比如如按钮点击回调,异步处理比如网络请求返回时触发回调
再比如:排序算法时传入比较函数
86. 请描述C语言的头文件有那些作用和用途
函数声明:告诉编译器某个函数的存在(如
printf()
)宏定义:提供常量、简化代码(如
#define PI 3.14
)数据类型定义:自定义结构体、枚举等
外部变量声明和代码模块化
87. 模拟实现memmove
void* my_memmove(void* des,const void* cou, size_t num)
{assert(des && cou);//断言void* ret = des;//记录地址//后面的向开始if (des >= cou){while (num--){*((char*)des + num) = *((char*)cou + num);//从后开始访问}}//前面的先开始else{while (num--){*(char*)des = *(char*)cou;des = (char*)des + 1;cou = (char*)cou + 1;}}return ret;
}
88. 写String类的构造,析构,拷贝,赋值函数
namespace cl
{// 模拟实现string类class string{public:typedef char *iterator;typedef const char *const_iterator;// 默认成员函数// 构造函数string(const char *str = ""){_size = strlen(str); // 初始时,字符串大小设置为字符串长度_capacity = _size; // 初始时,字符串容量设置为字符串长度_str = new char[_capacity + 1]; // 为存储字符串开辟空间(多开一个用于存放'\0')strcpy(_str, str); // 将C字符串拷贝到已开好的空间}// 拷贝构造string(const string &s): _str(nullptr), _size(0), _capacity(0){string tmp(s._str); // 调用构造函数,构造出一个C字符串为s._str的对象swap(tmp); // 交换这两个对象}// 赋值重载string &operator=(string s) // 编译器接收右值的时候自动调用拷贝构造函数{swap(s); // 交换这两个对象return *this; // 返回左值(支持连续赋值)}// 析构函数~string(){delete[] _str; // 释放_str指向的空间_str = nullptr; // 及时置空,防止非法访问_size = 0; // 大小置0_capacity = 0; // 容量置0}private:char *_str; // 存储字符串size_t _size; // 记录字符串当前的有效长度size_t _capacity; // 记录字符串当前的容量};
}
89. 给一个a.txt的文本,从文本中获取数据,计算其中’C++''出现的次数
#include <fstream>
#include <iostream>
#include <string>std::size_t count_substr(const std::string& s, const std::string& sub)
{if (sub.empty()) return 0;std::size_t cnt = 0, pos = 0;while ((pos = s.find(sub, pos)) != std::string::npos) {++cnt;++pos; // 允许重叠}return cnt;
}int main()
{std::ifstream fin("a.txt", std::ios::in );if (!fin) {std::cerr << "无法打开 a.txt\n";return 1;}std::size_t total = 0;std::string word;while (fin >> word) { // 每次读一个“单词”total += count_substr(word, "C++");}std::cout << "\"C++\" 出现次数: " << total << '\n';
}
90. char(*a[10])(char)代表什么
- 一个函数指针数组,数组有 10 个元素,每个元素是一个指向参数为
char
,返回值为char*
的函数的指针
91. 多线程怎么实现共享内存
- 在 全局/堆 上创建共享数据结构(如队列、计数器)
- 把指针或引用传给各线程
- 用
pthread_mutex
/std::mutex
、读写锁、条件变量等保护临界区;
或用std::atomic
实现无锁并发。
92. 同步I0 和异步10的区别?
- 同步 I/O:发起I/O后,必须等待操作完成才能继续执行。
- 异步 I/O:发起I/O后,不等待,继续执行,操作完成后通过回调、事件或通知机制处理结果。
96. 树的特点
- 层级结构 唯一根节点 每个节点只有一个父节点 可以有多个子节点
- 无环结构 递归定义
97. 图的特点
- 由顶点(节点)和边组成
- 分为:有向图和无向图,环图和无环图,连通图和非连通图,权重图和非权重图
- 结构复杂,可表示任意关系
98. C++多线程
C++ 多线程:通过 std::thread
创建多个线程并发执行任务,配合 mutex
、condition_variable
等实现线程同步与通信,提升程序效率。
0. 快排和堆排的原理
快速排序:
- 通过选一个基准值,把数组划分为左右两部分(小于基准、大于基准),递归排序左右部分。核心是分治+递归。
堆排序:
- 利用堆这种数据结构,先建堆,再将堆顶元素取出放到数组末尾,然后调整堆。核心是选择排序+堆结构维护。
1. C++11/14/17新特性你用过哪些?为什么用?
C++11:
auto
:简化类型声明,提升可读性。lambda
表达式:简洁定义函数对象,用于回调、算法move
语义:减少拷贝,提升性能。unique_ptr/shared_ptr
:智能指针管理资源,防止内存泄漏。thread
、mutex
:支持多线程开发
C++14:
make_unique
:更安全高效创建unique_ptr
。泛型 lambda:lambda 参数支持自动类型推导。
C++17:
if constexpr
:编译期条件判断,配合模板更灵活。结构化绑定(
auto [a, b] = pair;
):解构赋值,提高可读性。std::optional
:表示可有可无的返回值,替代nullptr/null
。std::variant
:类型安全的联合体。std::filesystem
:处理文件系统更方便。
2. 智能指针(unique_ptr / shared_ptr)的工作机制和区别?
工作机制:
unique_ptr
:独占式所有权,对象只能被一个指针管理,转移所有权用std::move
。shared_ptr
:共享所有权,内部有引用计数,最后一个引用释放时析构对象。底层通过析构器自动释放资源,避免手动
delete
。
区别:
特性 | unique_ptr | shared_ptr |
---|---|---|
所有权 | 唯一 | 多个指针可共享 |
拷贝 | 不可拷贝(可移动) | 可拷贝 |
内存开销 | 小 | 有引用计数器,开销大一些 |
用途 | 独占资源(RAII) | 多方共享资源 |
总结:能用 unique_ptr
就不用 shared_ptr
,性能更好。
3. RAII 是什么?你在项目中怎么用的?
RAII(Resource Acquisition Is Initialization):是一种资源权限转移的思想,资源获取即初始化。对象生命周期控制资源生命周期,在构造函数中申请资源,在析构中释放,避免泄漏
4. 拷贝构造 vs 移动构造
构造方式 | 含义 | 特点 |
---|---|---|
拷贝构造函数 | 用已有对象复制一个新对象 | 深拷贝,资源复制,开销大 |
移动构造函数 | 把资源从旧对象“搬”到新对象 | 不复制资源,快,旧对象置空 |
5.虚函数表机制、虚析构函数为什么必须写?
虚函数表机制(vtable):
类中有虚函数 → 编译器生成虚函数表(vtable)。
每个对象有指向 vtable 的指针(vptr)。
通过 vptr 实现运行时多态,调用正确的派生类函数。
为什么要写虚析构函数:
父类指针指向子类对象时,删除对象只调用父类析构 → 资源泄漏。
虚析构能确保先调用子类析构,再调用父类析构,释放完整资源。
总结:只要类有虚函数,析构函数必须为虚,否则可能导致内存泄漏。
6. TCP 三次握手流程、TIME_WAIT 出现的原因?
TCP 三次握手流程:
客户端 → 服务端:发送 SYN,进入 SYN_SENT 状态。
服务端 → 客户端:回复 SYN+ACK,进入 SYN_RCVD。
客户端 → 服务端:发送 ACK,进入 ESTABLISHED,服务端接收后也进入 ESTABLISHED。
TIME_WAIT 出现原因:
主动关闭连接的一方进入
TIME_WAIT
状态。等待 2 倍最大报文生存时间(MSL),确保对方收到最后 ACK。
防止旧连接的延迟报文影响后续新连接。
总结:TIME_WAIT
是为连接安全清理而设计的机制。
7. select 和 epoll 区别?
select
vs epoll
对比项 | select | epoll |
---|---|---|
监视上限 | FD 索引最多 1024/2048(受 FD_SETSIZE ) | 理论无限(取决于内核和内存) |
事件通知方式 | 每次线性扫描 FD 集合 | 内核通过事件列表直接返回就绪 FD |
工作模式 | 始终水平触发(Level‑Triggered) | 支持 LT 与 ET(Edge‑Triggered) |
内核/用户态拷贝 | 每次调用需复制 FD 集合 | 就绪列表一次返回,无额外拷贝 |
性能随连接数变化 | O(N) | O(1),与连接数弱相关 |
适用场景 | 低并发、移植性好 | 高并发、高性能 Linux 服务器 |
8. 多线程同步机制?mutex、condition_variable用法?
常见多线程同步机制:
std::mutex
:互斥锁,保护临界区。std::recursive_mutex
:允许同一线程多次加锁。std::lock_guard
/std::unique_lock
:RAII 自动加解锁。std::condition_variable
:线程间通信/等待。std::atomic
:原子变量,锁-free 方案。std::future/promise
:线程结果传递。
9. 线程安全怎么保证?
互斥锁(std::mutex
):防止多个线程同时访问共享资源。
原子操作(std::atomic
):适合简单变量如计数器,无需加锁。
线程局部存储(thread_local
):每个线程独立变量,避免共享。
条件变量(std::condition_variable
):配合锁,实现线程间同步等待。
避免共享:设计时尽量避免多个线程同时访问同一资源(如消息队列、线程池中工作隔离)
10. C++观察者模式 和 工厂模式
- 观察者模式:一种订阅-通知机制,一个对象状态变化后,自动通知所有依赖它的对象(观察者)。
- 工厂模式:用一个工厂类/函数封装对象的创建逻辑,通过传参决定创建哪种子类对象,实现解耦和扩展性。
11. TCP沾包问题怎么解决
常见原因
- 发送端发送速度太快,连续发送多个小包
- 接收端读取不及时或读取缓冲区较大
固定长度:每条消息规定长度
特殊分隔符:在每条消息结尾添加特定字符,如 \n
、\r\n
消息头加长度字段:每条消息前添加一个固定长度的头部字段
高级协议封装:使用其他协议封装TCP