面试问题总结
[toc]
内卷环境之下,岗位的专业分工程度越来越高,综合研发能力比较强的人真的有点水土不服.表面上视野广阔一个顶两,实际上任一方向都有比较强的可替代性.
由于之前工作涉及的领域比较多,大多在项目期间的时候深入过,退出项目栈之后又容易忘,领域知识很难迁移,找工作的时候就很麻烦,一边需要复习语言细节,一边需要复习项目细节,一边需要复习理论相关内容,一边还要复习常用工具命令用法,还要刷算法题和手撕实现细节等等,短时间复习很难覆盖全面,长时间又很容易遗忘.
特别是面试问什么的都有,问八股文背诵记忆力,问API细节甚至入参是啥,简历上任何一个点都有可能是人家感兴趣的会深入问下去的.
普通面试官考察实现细节,技术大佬会试探性问技术边界,两者都很难应对,可能曾经深入了80%,过了很长时间也只能回答出10%.工作的时候非常投入,应聘时却很难体现,就非常吃亏.
然后匹配的方向也蛮难找的,螺丝钉岗有螺丝钉岗的烦恼,研发岗有研发岗的郁闷,之前的打法有点类似深度优先遍历,快速深入快速退出然后清空记忆栈,继续遍历,锻炼出了不少解决复杂问题的能力,唯独没锻炼出记忆力.
未来还是需要在专业领域方向有更多沉淀.考虑简历上更多地剪枝以提高匹配专业度和降低面试准备难度.
把曾经涉及过的相关问题做个记录,随遇而安吧.
小细节
- 一些平时不需要关心或者很少使用的用法
- 没测试过就没有印象的小细节
- 过于底层的内容
CPP
关于C++类空间大小的计算(以下以32位编译器为例|gcc)
#include <iostream>
using namespace std;
// 空类的大小
// 所谓类的实例化就是在内存中分配一块地址,每个实例在内存中都有独一无二的地址.同样空类也会被实例化, 所以编译器会给空类隐式地添加一个字节, 这样空类实例化之后就有了独一无二的地址了. =1
class Stu1{ };
// 非空类的存储空间以包含变量的存储大小为准 =1
class Stu2{ char a; };
// 非空类,包含函数不占用存储空间
// 类的大小与它的构造函数、析构函数和其他成员函数无关, 只已它的数据成员有关
// 数据成员会自动与最大宽度的成员对齐,4+2+2(补)+8+1+7(补)=24
class Stu3{
public:
~Stu3(){};
void fun(){};
int a; //4
short b; //2
double c; //8
char d; //1
};
// 非空类,包含虚函数, 会因为多一个虚函数指针(vptr)指向虚函数表
// 4(vptr指针)+4+2+6(补)+8+1+7(补)=32
class Stu4{
public:
virtual ~Stu4(){};//4
int a; //4
short b; //2
double c; //8
char d; //1
};
// 4(vptr指针) +4(int) +1(char)+3(补齐) =12
class Stu5{
public:
virtual ~Stu5(){};//4
int a; //4
char b; //1
};
// 有虚函数的继承, 子类的存储是父类的存储+子类的存储
// 所以存储是4(vptr指针)+4 +2+6(补) +8 +1+1(char)+6(补)=32
class Stu6 :public Stu4 {
public:
virtual ~Stu6(){};
char c;
};
// 存储是4(vptr指针)+4 +2+6(补) +8 +1+7(补) +4(int)+1(char)+3(补)=40
class Stu7 :public Stu4 {
public:
virtual ~Stu7(){};
int c;
char d;
};
// 存储是4(vptr指针)+4 +2+6(补) +8 +1(char)+1(char)+4(int)+2(补)=32
class Stu8 :public Stu4 {
public:
virtual ~Stu8(){};
char d;
int c;
};
// 静态变量不占用实例的存储空间 = 4(int)
class Stu9{
public:
int a;
static int b;
};
int main(){
std::cout <<"1:"<< sizeof(Stu1) << std::endl;//1
std::cout <<"2:"<< sizeof(Stu2) << std::endl;//1
std::cout <<"3:"<< sizeof(Stu3) << std::endl;//24
std::cout <<"4:"<< sizeof(Stu4) << std::endl;//32
std::cout <<"5:"<< sizeof(Stu5) << std::endl;//12
std::cout <<"6:"<< sizeof(Stu6) << std::endl;//32
std::cout <<"7:"<< sizeof(Stu7) << std::endl;//40
std::cout <<"8:"<< sizeof(Stu8) << std::endl;//32
std::cout <<"9:"<< sizeof(Stu9) << std::endl;//4
}
- 总结
- 类大小只跟成员变量有关,空类大小=1
- 静态变量空间不计算在实例空间内
- 继承时的空间大小: 计算子类和父类空间,虚函数指针只有一个
- 自动补齐:与最大宽度的数据成员依次对齐(注意stu7和stu8的特殊区别,有点不合常规,可能是编译器实现不一样)
继承关系中虚函数及其默认值
#include <iostream>
using namespace std;
struct A{
virtual int fun(int a = 10){return a;};
};
struct B : public A{
int fun(int a = 20)override{return a;};
};
struct C : public A{
int fun(int a = 30)override{return a+a;};
};
int main(){
B b;
C c;
A* a=&b;
cout<<a->fun()<<endl;//10
a=&c;
cout<<a->fun()<<endl;//20
}
- 总结
- 默认值在编译期就已经确定下来了,若调用时缺省参数,子类定义的默认值会被无视,然后才会调用子类方法
- 一般来说应该避免在虚函数中使用默认值
- 因为还有一个点是,声明时定义了默认值,实现函数的签名内不能包含默认值,否则编译报错.
- 规范的注释方式是在子类实现有默认值的虚方法时,在形参列表内如下注释上默认值.
struct B : public A{ int fun(int a/*=10*/)override{return a;}; };
在构造函数和析构函数中调用虚函数
#include "iostream"
using namespace std;
class Base{
public:
Base(){
cout << "Base::Base()\n";
fun();
}
virtual ~Base(){
cout << "Base::~Base()\n";
fun();
}
virtual void fun(){
cout << " Base::fun() virtual\n";
}
};
class Derive: public Base{
public:
Derive(){
cout << "Derive::Derive()\n";
fun();
}
~Derive(){
cout << "Derive::~Derive()\n";
fun();
}
void fun() override {
cout << " Derive::fun() virtual\n";
}
};
int main(){
cout << "-----------------------------------\n";
Derive *d = new Derive();
delete d;
cout << "-----------------------------------\n";
Base *bd = new Derive();
delete bd;
}
//输出相同
-----------------------------------
Base::Base()
Base::fun() virtual //派生类还未构造,调用基类自身方法
Derive::Derive()
Derive::fun() virtual //派生类正在构造,成功调用派生类自身方法
Derive::~Derive()
Derive::fun() virtual //派生类正在析构,成功调用派生类自身方法
Base::~Base()
Base::fun() virtual //派生类已被析构,调用基类自身方法
-----------------------------------
Base::Base()
Base::fun() virtual
Derive::Derive()
Derive::fun() virtual
Derive::~Derive()
Derive::fun() virtual //delete基类指针时能正确调用子类覆盖方法
Base::~Base()
Base::fun() virtual
在C++编码规范-实用增强细节版 这篇文章中有个建议是构造和析构函数应该只处理成员变量初始化相关的工作,为了减少记忆负担经常会有许多经验总结,但也因此会忽略掉背后的许多细节,会导致你大概知道是什么原因,但是又讲不完整.
总结: 看其他文章的理由说:由于构造/析构时对象是不完整/不安全的,故而无法完成动态联编会丧失多态性.
其实多态性并没有消失,依然会动态联编,只不过找不到还未构造或已经被析构的对象时调用了自身方法而已,基类/派生类在构造/析构函数中调用虚方法会调用自身实现,delete基类指针时也能正确调用子类的覆盖方法,行为一致性并未被破坏. 毕竟除非使用奇异模板递归模式,基类内是操作不到也不应该操作到派生类的. 如果清楚构造/析构顺序,其实这个问题并不存在.如果面试官以这个角度提问大概率只是想考察构造和析构顺序罢了.
int类型的-1 左移右移的值
#include <iostream>
#include <bitset>
#include <cmath>
using namespace std;
int main(){
// 32位及以上操作系统上, int型数据的十进制表示范围是: -2^31 到 2^31-1
cout << std::bitset<32>(-std::pow(2, 31)) << " ~ " << std::bitset<32>(std::pow(2, 31) - 1) << endl;
cout << std::bitset<32>(-std::pow(2, 31)).to_ulong() << "~" << std::bitset<32>(std::pow(2, 31) - 1).to_ulong() << endl;
cout << std::bitset<32>(1) << endl; //正数原,补,反码都相同
cout << std::bitset<32>(-1) << endl;//补码<=除标志位取反+1=>原码
cout << std::bitset<32>(-1 << 1) << " " << "-1 << 1 : " << (-1 << 1) << endl;//负数左移,补码补0
cout << std::bitset<32>(-1 >> 1) << " " << "-1 >> 1 : " << (-1 >> 1) << endl;//负数右移,补码补1
}
// 10000000000000000000000000000000 ~ 10000000000000000000000000000000
// 2147483648~2147483648
// 00000000000000000000000000000001
// 11111111111111111111111111111111
// 11111111111111111111111111111110 -1 << 1 : -2
// 11111111111111111111111111111111 -1 >> 1 : -1
总结
- int类型在计算机中是以补码存储的(4字节*8bit=32bit),首位标志位,范围-2^31 ~ 2^31-1
- 补码<=除标志位取反+1=>原码
- 负数左移补0,右移补1
引用和指针的区别
简单来说主要区别有:
- 初始化: 引用定义时必须初始化,指针没有要求
- 指向性: 引用是变量的别名,初始化后不能指向其他实体,指针可以任意指向
- 空值: 不存在空引用但是可以存在空指针
- sizeof: sizeof中引用就是变量的大小,指针就是指针占用的大小(4/8)
- 自加: 引用自加就是变量自加,指针自加会偏移类型的大小
- 多级: 有多级指针但没有多级引用
- 访问: 指针需要解引用显式取值,引用由编译器自己处理.
- 底层实现:引用是通过指针实现的,可以认为是固化版指针
- 引用具有指针的效率和变量的方便直观性
memcpy memmove区别
- 2者都是将N个字节的源地址的内容拷贝到目标地址中.
- 当源内存和目标内存存在重叠时,
memcpy
会出现错误, 而memmove
能正确地实施拷贝 memcpy
实现void * memcpy (void* dest,const void* src,size_t n){ char* d = (char*)dest; const char s = (const char*) src; while (n-–) *d++ = *s++; return dest; }
memmove
实现void* memmove (void* dest,const void* src,size_t n){ char* d = (char*)dest; const char* s = (const char*) src; if(s>d){//正向拷贝 与memcpy相同 while (n--) *d++ = *s++; } else if(s<d){//反向拷贝 d = d+n-1; s = s+n-1; while(n--) *d-- = *s--; } return dest; }
各种各样
大小端编码
- 网络字节序是大端编码
- 大端编码:高位数字存放在低地址字节中, 将0x1234转化为1字节的char, 高地址字节被丢弃, 剩余低地址字节, 即12.
- 小端编码:高位数字存放在高地址字节中, 如0x1234, 小端编码机器将12存放在高位地址字节, 34存放在低地址字节中, 将其转化为1字节的char时, 高地址字节被丢弃, 剩余低地址字节, 就是34.
grep
信号 | 值 | 描述 | 组合键/命令 |
---|---|---|---|
1 | SIGHUP | 挂起 | |
2 | SIGINT | 终止 | Ctrl+C |
3 | SIGQUIT | 停止 | |
9 | SIGKILL | 无条件终止 | kill -9 |
15 | SIGTERM | 尽可能终止 | |
17 | SIGSTOP | 无条件停止 | |
18 | SIGTSTP | 暂停 | CTRL+Z |
19 | SIGCONT | 继续 |
bash shell
默认忽略SIGQUIT
和SIGTERM
信号(必要时在脚本中捕获处理),会处理SIGHUP
和SIGINT
信号.
trap
捕获信号并处理(可用于.sh文件内)trap <cmd> <signals> trap "echo 'trepped ctrl+c'" SIGINT # 捕获命令 trap "echo 'goodbye'" EXIT # 捕获shell脚本的退出 trap -- SIGINT # 移除要捕获的信号
后台运行shell
- 在命令后加&符即可
./test.sh &
不同主机间传送文件
scp 远端用户名@IP地址:文件的绝对路径 指定本地保存的路径 (-r传送文件夹)
scp root@192.168.1.12:/tmp/test.txt ./temp/test.txt
gdb和gprof
gdb: GDB查看C++对象内存布局
gprof: gprof是GNU profile工具, 用于程序的性能优化以及程序瓶颈问题的查找和解决.可以得到每个函数的调用次数, 每个函数消耗的处理器时间, 也可以得到函数的调用关系图, 包括函数调用的层次关系, 每个函数调用花费了多少时间.
g++ -pg
编译器会自动在目标代码中插入用于性能测试的代码片断
gprof test.exe gmon.out > gprof_result.txt
重定向到文本文件以便于后续分析.
通识类
AI相关
- 池化层
- 实施池化的目的
- 降低信息冗余
- 提升模型的尺度不变性、旋转不变性
- 防止过拟合
- 常见操作
- 最大值池化
- 均值池化
- 随机池化
- 中值池化
- 组合池化(同时利用最大值与均值池化两种的优势)
- 实施池化的目的
GPU/CUDA相关
并行处理的类型
- 基于任务的并行处理
- 操作系统与进程
- 基于数据的并行处理
- SIMD,单指令多数据
- 基于任务的并行处理
常用并行模式
- 循环并行展开
- 图像处理算法中,沿X轴处理时内循环,沿Y轴处理时外循环.
- 循环并行化是OpenMP的基础
- 派生/汇集模式
- 比如OpenMP中,用编译指令语句定义可并行区,并行区中的代码被分成N个线程,随后再汇聚成单个线程
- MapReduce 大规模分布式并行计算框架
- 分条/分块
- 使用CUDA解决问题需要将问题分条分块(CUDA提供的是简单二维网格模型)
- 分而治之
- 循环并行展开
体系结构
- GPU设备包括一组SM, 每个SM由包括一组SP或CUDA核(并行)
- GPU性能由SP数量, 全局内存带宽, 程序员利用并行架构的充分程度
- 解决时空局部性问题-> 多级缓存:一级缓存 二级缓存 全局内存
- 缓存一致性 保证所有处理核的内存视图一样(CPU要求, GPU不要求)
- GPU通过PCI-E总线与CPU或其他GPU连接
- 纹理内存和常量内存是全局内存的特殊视图,每个SM都设置独立访问它们的总线,纹理内存用于存储插值计算所需的数据,常量内存缓存只读数据.
CUDA
- CUDA的SPMD模型:每个线程执行的代码一样但是数据不同
- CUDA结构类型 SIMT-单指令多线程
- CUDA将问题分解成线程块的网格,每块包含多个线程束,块可以按任意顺序执行
- 线程块(可以任意顺序执行,调度到SM上时执行不中断)
- 线程束 分块的数量一般为SM的8-16倍
- 全局内存 纹理内存 常量内存
__global__
内核函数 指示生成GPU代码__host__
主机函数__device__
设备函数 用于在设备上调用该设备函数__constand__
指示常量内存__shared__
指示共享内存- 调试工具 Nsight,cuda-gdb等
- 编译器 NVCC
threadIdx 线程索引 blockIdx 线程块索引 blockDim.x 每个线程块启动的线程数量 gridDim 线程网格上的线程块数量 const unsigned int idx = (blockIdx.x*blockDim.x)+threadIdx.x const unsigned int idy = (blockIdx.y*blockDim.y)+threadIdx.y 绝对线程索引 thraad_idx=((gridDim.x*blockDim.x)*idy)+idx;
网络相关
Git相关
小测验
线程间通信
两个进程中的线程间通信方式
- 信号量, socket, 共享内存 ,管道,共享文件
一个进程中的线程间通信方式
- 条件变量
condition_variable
配合互斥锁可以避免忙等- 比起普通方法bool值的轮询,通过唤醒来替代不必要的轮询
- 存在信号丢失和虚假唤醒的问题,通过增加条件判断来规避
- 条件变量为什么要和mutex搭配, 不能单独使用吗?
- 条件变量通信相当于操作公共变量,需要加锁
信号量(PV操作)
#include <iostream> #include <thread> #include <semaphore> using namespace std; std::counting_semaphore seamp(1); // 生产者信号量 std::counting_semaphore seams(0); // 消费者信号量 int num(0); void producer_thread(){ while (num < 10){ seamp.acquire(); //p num += 1; cout << "producer: " << num << endl; seams.release(); //v } } void consumer_thread(){ while (num < 10){ seams.acquire(); cout << "consumer: " << num << endl; seamp.release(); } } int main(){ thread tp1(producer_thread); thread ts1(consumer_thread); tp1.join(); ts1.join(); }
原子操作
atomic
可以取代mutex和lock
同步原语:future and async/packaged_task/promise
thread promise
低级接口#include <iostream> #include <future> #include <thread> #include <chrono> using namespace std; void thread_set_promise(std::promise<int>& pm) { cout << "1"; this_thread::sleep_for(100ms); pm.set_value(2); cout << "3"; } int main() { promise<int> pm; future<int> fu = pm.get_future(); thread t(&thread_set_promise, ref(pm)); cout << fu.get();//block t.join(); system("pause"); } //132
async() future
高级接口#include <future> #include <thread> #include <iostream> #include <random> #include <chrono> using namespace std; int work(char c){ default_random_engine e(c); uniform_int_distribution<int> id(10,100); for(int i=0;i<10;i++){ this_thread::sleep_for(chrono::milliseconds(id(e))); cout.put(c).flush(); } return c; } int f1(){ return work('.'); } int f2(){ return work('+'); } int main(){ future<int> r1(async(f1)); // async启动异步线程(不保证) //future<int> r1(async(launch::async, f1)); // async启动异步线程(保证) //future<int> r1(async(launch::deferred, f1)); // async启动异步线程(延迟到get) auto ret = r1.wait_for(10ms); switch (ret){ case future_status::ready:{ cout<<"ready"; r1.get()+ f2(); break; } case future_status::deferred:{ cout<<"deferred"; r1.get()+ f2(); break; } case future_status::timeout:{ cout<<"timeout"; r1.get()+ f2(); break; } } system("pause"); } //.+.+..+...+..++.++++ready //..timeout........++++++++++ // ++++++++++deferred..........
packaged_task, future
packaged_task
和function
的区别在于前者返回值以future返回#include <iostream> #include <future> #include <thread> using namespace std; int func (int x){ return x; } int main() { std::packaged_task<int(int)> t(func); std::thread t1(std::ref(t), 1); std::future<int> fu = t.get_future(); std::shared_future<int> fus = fu.share(); cout<< fus.get(); system("pause"); }
锁
互斥锁
mutex & Lock_guard | unique_lock
recursive_mutex
允许同一线程多次调用timed_mutex
可以try_lock_for
等待一段时间- 尝试/同时锁定多个锁
std::lock(m1,m2); std::try_lock(m1,m2); // 注意需要过继给lock_guard std::lock_guard<std::mutex> lg_m1(m1,std::adopt_lock); std::lock_guard<std::mutex> lg_m2(m2,std::adopt_lock);
读写锁
- 解决多线程同时读
- 共享锁
std::shared_lock<std::shared_mutex>
独占锁
std::unique_lock<std::shared_mutex>
#include <shared_mutex> //C++17 #include <thread> #include <vector> #include <iostream> using namespace std; shared_mutex mu; vector<int> vec; bool is_run = 1; void read(){ while(is_run){ shared_lock sl(mu); if(!vec.empty()){ cout << "read_id:" << this_thread::get_id() << " num:" << vec.front() << endl; vec.erase(vec.begin()); } sl.unlock(); // this_thread::sleep_for(100ms); } } void write(){ static int num; while(is_run){ if(vec.size()<10){ unique_lock ul(mu); cout << "write_id:" << this_thread::get_id() << " num:" << num << endl; vec.push_back(num++); ul.unlock(); }else{ // this_thread::sleep_for(100ms); } } } int main(){ thread t1(write); thread t2(read); thread t3(read); t1.detach(); t2.detach(); t3.detach(); this_thread::sleep_for(1s); is_run = false; system("pause"); }
多线程条件变量+互斥锁 生产者消费者模型
#include<iostream> #include<thread> #include<mutex> #include<condition_variable> #include<vector> using namespace std; mutex mu; condition_variable cv; vector<int> vec; bool is_run=true; void productor(){ static int num =1; while (is_run){ unique_lock<mutex> ul(mu); cv.wait(ul, []() { return (vec.size() < 10); }); // 满10等待 cout << "生产者:" << this_thread::get_id() << " 生产:" << num << endl; vec.push_back(num++); ul.unlock(); //只需对操作共享结构加锁 cv.notify_all(); //通知消费者 } } void consumer(){ while(is_run){ unique_lock<mutex> ul(mu); cv.wait(ul,[](){return (vec.size()>=1);});// 小于1等待 cout << "消费者:"<<this_thread::get_id() <<" 消费:"<<vec.front() << endl; vec.erase(vec.begin()); ul.unlock();//只需对操作共享结构加锁 cv.notify_all(); //通知生产者 } } int main(){ thread t1(productor); thread t2(productor); thread t3(consumer); thread t4(consumer); t1.detach(); t2.detach(); t3.detach(); t4.detach(); this_thread::sleep_for(10s); is_run=false; system("pause"); }
高并发
无锁编程CAS
list实现无锁栈(compare and swap) 对一致性要求不高时可用
#include <atomic>
#include <memory>
#include <iostream>
#include <exception>
template <class T>
struct list_t{
list_t(T&& data_, list_t<T>* next_):data(std::move(data_)),next(next_){}
list_t(const T& data_, list_t<T>* next_):data(data_),next(next_){}
T data;
list_t<T> *next = nullptr;
};
// 单链表实现无锁栈(compare and swap) 对一致性要求不高时可用
template <class T>
class cas_stack_t{
public:
cas_stack_t(){
_size.store(0,std::memory_order_relaxed);
_data_list.store(nullptr,std::memory_order_relaxed);
_data_pop_list.store(nullptr,std::memory_order_relaxed);
}
~cas_stack_t(){
while (!empty()){pop();};
}
void emplace(T&& data){
list_t<T>* new_head = new list_t<T>(data, _data_list.load(std::memory_order_relaxed));
//weak版本的CAS允许偶然出乎意料的返回,性能比strong好一些
while (!_data_list.compare_exchange_weak(new_head->next, new_head,
std::memory_order_relaxed,std::memory_order_relaxed));
_size++;
}
void push(const T& data){
list_t<T>* new_head = new list_t<T>(data, _data_list.load(std::memory_order_relaxed));
while (!_data_list.compare_exchange_weak(new_head->next, new_head,
std::memory_order_relaxed,std::memory_order_relaxed));
_size++;
};
T& top(){
if (_data_pop_list){
return _data_pop_list.load(std::memory_order_relaxed)->data;
}else if(_data_list){
return _data_list.load(std::memory_order_relaxed)->data;
}else{
throw std::logic_error("before call top() , make sure stack is not empty.");
}
}
void pop(){
// 为空则重新获取等待处理的数据
if (!_data_pop_list){
_data_pop_list = _data_list.exchange(nullptr,std::memory_order_relaxed);
}
list_t<T>* p = _data_pop_list.load(std::memory_order_relaxed);
if (!p) return;
while (!_data_pop_list.compare_exchange_weak(
p, p->next,std::memory_order_relaxed, std::memory_order_relaxed));
delete p;
_size--;
};
bool empty(){
return (!_data_pop_list) && (!_data_list);
}
size_t size(){
return _size.load(std::memory_order_relaxed);
}
private:
std::atomic<size_t> _size;
std::atomic<list_t<T>*> _data_list;
std::atomic<list_t<T>*> _data_pop_list;
};
#include <vector>
#include <thread>
using namespace std;
int main(){
cas_stack_t<vector<int>> s;
int n = 300;
thread t1([&](){
for (int i = 1;i < n/2; i++){
s.emplace({1,i});
}
});
thread t2([&](){
for (int i = n/2;i < n; i++){
s.push({2,i});
}
});
thread t3([&](){
while(s.empty());
while (!s.empty()){
if (s.size()%10==0)
cout<<"size:"<<s.size()<<endl;
auto vec = s.top();
for (auto i : vec){
cout<< i << " ";
}
cout<<endl;
s.pop();
}
});
t1.detach();
t2.detach();
t3.join();
return 0;
}