C++面试题常问

文章目录

零、面试大纲与策略

1.面试官的提问策略

先验证简历真伪(深挖细节),
再考察技术深度(追问为什么),
最后评估软技能(沟通与应变)。

项目深挖(50%)> 技术深度(30%)> 软技能(20%)。


(1)系统设计与工程思维(判断成长潜力)

1.“你现在做的两个项目,如果让你重新设计一次,你觉得哪些地方可以做得更好?”
考察点:复盘能力和自我批判意识。

2.“你在现场调试时,遇到过最诡异的 Bug 是什么?怎么定位的?”
考察点:调试方法论(是加打印乱试还是用逻辑推理+排除法)。这是区分优秀与普通的关键题。


(2)软技能与职业稳定性(隐性风险排查)

1.“你上一份工作做了刚好两年,为什么现在看机会?”
考察点:离职动机是否理性(如“想接触更复杂的架构” > “工资低”)。

2.“你的简历提到‘善用 AI 工具’,你能举例说明 AI 在哪个具体环节帮到了你吗?”
考察点:AI 是辅助还是依赖。如果他回答“让 AI 写驱动,我直接复制”,那是红牌。

3.“你对未来三年的职业规划是什么?更想深钻 Linux 底层,还是偏上层 Qt/应用?”
考察点:稳定性与岗位匹配度(看你招的是什么方向)。


2.C++基础提问大纲

第一类:基础与语法(const, static, 引用vs指针, 类型转换)。
第二类:OOP与多态(虚函数、vtable、构造/析构顺序、钻石问题)。
第三类:内存管理(RAII、智能指针、new/delete与malloc/free、内存对齐、内存布局)。
第四类:STL(vector/deque/map底层、迭代器失效、自定义分配器)。
第五类:现代C++(C++11及以后)(移动语义、完美转发、lambda、constexpr、concepts、协程——根据级别调整深度)。
第六类:并发(线程、互斥锁、条件变量、原子操作、无锁编程基础)。
第七类:Linux/系统编程(进程与线程、IPC、网络编程、gdb/valgrind)。
第八类:编译/链接(cmake、静态vs动态库、符号查找)。
第九类:手撕代码/算法(字符串处理、链表、二叉树、LRU缓存、设计模式)。
第十类:开放/系统设计(设计定时器、线程池、内存池)。


一、C语言基础

1.const 关键字

C/C++关键字:const

问:const修饰指针类型,const int * pint * const p 有什么区别?


2.static 关键字

C/C++关键字:static


3.编译的过程

编译过程:预处理、编译、汇编、链接

1.编译过程有几个步骤?
2.这4步分别做了什么?



二、C++基础

1.指针和引用

问题1:指针的定义是什么?
指针的定义:指针是一个变量,该变量存储的是一个地址,指向内存的一个存储单元。【指针就是存储内存地址的变量。】


问题2:引用的定义和本质是什么?
(1)引用的定义:引用是变量的别名,引用一经初始化,就和目标变量绑定,代表同一内存单元。
(2)引用本质:引用底层本质是常量指针:被编译器自动封装的常量指针 Type *const

int *const r = &a;

问题3:指针和引用的区别是什么?各自适合什么场景?
1.区别
(1)表达符号上:指针是T *t,引用是T &t
(2)指针可以为空,可以重新指向;引用必须初始化,且不能为空,不能重新绑定(换绑)!
(3)指针可以有多级,引用只能是一级

int num = 100;
int *p = #  //指针
int &ref = num; //引用
cout << *p << endl; //*p = num, p == &num
cout << ref << endl;

*p = 200; //修改num的值
p = nullptr; //指针可以重新指向别的地址

ref = 300; //修改num的值
//ref一旦绑定变量,永远不能再绑定其他变量

2.使用场景
(1)原则:能使用引用就优先用引用;必须用指针的时候再用指针。
<1>引用语法更接近变量本身,底层实现是指针地址。函数传参时引用更安全,指针适合需要动态改变指向或可能为空的场景
(2)必须用指针不能用引用的情况:
<1>需要代表 “空”(不存在对象),对象可能不存在,允许 “空”:函数入参,对象可能不存在;函数返回,有可能找不到对象;
<2>需要中途更换指向目标,需要运行时改变指向的目标 (引用不能重定向,指针可以随时重新赋值指向别的对象)
<3> 动态内存管理(new /delete)堆上创建对象天然返回指针
<4>容器存储支持指针,容器里面不能存引用(vector<Node*> nodes 合法;vector<int&> 非法)

现代 C++ 补充重要升级:工程开发中尽量少用裸指针 T*
需要所有权:std::unique_ptr
需要共享所有权:std::shared_ptr

在这里插入图片描述


2.智能指针:用过哪些智能指针?举例说明

C++11提出智能指针,是为了实现自动化的内存管理,防止内存泄露。(因为C++没有垃圾回收机制)

(1)unique_ptr

unique_ptr是独占型智能指针


unique_ptr没有额外开销

#include <memory>

unique_ptr<int> p make_unique<int>(100);

(2)shared_ptr:如何实现的?

shared_ptr是共享型智能指针


指向对象有 引用计数,引用计数为0时自动delete (类似 操作系统-文件管理-索引结点-引用计数)。但引用计数会增加一些内存开销,而unique_ptr就没有内存开销。

shared_ptr<int> p = make_shared<int>(); //括号内不填数值,默认初始化为0

在这里插入图片描述


(3)weak_ptr

weak_ptr是一种不控制对象生命周期的指针,专门与shared_ptr配合使用。它不会增加引用计数,主要用于解决循环引用问题。weak_ptr可以用来检查指向的资源是否还存在,避免悬挂指针的风险。


3.运算符

1.·->
成员访问运算符 和 箭头成员访问运算符

对象.成员
指针->成员
ptr->member
// 等价于
(*ptr).member

4.虚函数原理:虚函数是什么?工作机制是什么?

1.概念
虚函数是在基类中使用virtual关键字声明的成员函数
它允许派生类 重写/覆盖(override) 基类的同形虚函数(函数名、参数列表、返回值都相同的虚函数)。


2.作用/目的/干什么的/为什么要用到虚函数
(1)提出虚函数的目的,就是为了实现动态多态 (运行时多态)
(2)基类指针指向派生类对象(或基类引用绑定派生类对象),就可调用派生类的虚函数。

父类指针 / 引用指向子类对象时,自动调用子类重写的函数,而不是父类版本。


3.原理
虚函数的实现依赖于 虚函数表 vtable 和 虚函数指针 vfptr
总结:对象中的虚函数指针,指向虚函数表。虚函数表中存放的是函数的入口地址,指向程序代码区中虚函数的存储位置

(1)虚函数指针虚函数指针,指向虚表的首地址。
vptr占8个字节。vptr是类的隐藏变量,在构造函数中被初始化为虚函数表的入口地址。


(2)虚函数表(虚表):存放虚函数的入口地址
①每个包含虚函数的类都有一个独立的虚函数表(vtable),同一个类的所有对象共享一个虚函数表。
②虚函数表包含该类的所有虚函数的入口地址。
③虚函数表是一个函数指针数组,数组的每个元素都是一个指向虚函数的指针(地址),存放虚函数的入口地址。
④每个类有一个虚函数表,类的所有实例化对象共享这一张虚表。


(3)调用虚函数
通过指针调用虚函数,通过虚函数指针找到虚表。(动态绑定)
通过对象调用虚函数,直接去程序代码区中找,不通过虚表。


(4)存放规则
虚函数表中,虚函数的排列顺序,是按照虚函数在基类中的声明顺序排布的。
②派生类继承时,会创建一张虚表,并复制基类虚表中的所有内容。
若派生类自己也有虚函数,则在虚表中往后排放。
若派生类中发生覆盖,则覆盖虚表中虚函数的入口地址。(动态多态,基类指针指向派生类对象,多态时,查看的是派生类的虚表,若发生覆盖,就调用派生类覆盖的虚函数)
③在带虚函数的多继承的结构下,虚表有多张:
<1>第一张虚表进行覆盖,后面几张虚表应该发生覆盖的对应位置存放go to指令,跳转到第一张虚表 (vs的存放规则)。
<2>派生类中内存布局,基类子对象按照继承的先后顺序排放。若有定义虚函数的类,会优先排放其虚函数指针和基类子对象。


//虚函数与多态

#include <iostream>
using std::cout;
using std::endl;

//基类
class Animal
{
public:
    virtual void makeSound() const{
        cout << "Animal makes a sound." << endl;
    }
};

//派生类:Dog
class Dog :public Animal
{
public:
    virtual void makeSound() const override{
        cout << "Dog barks." << endl;
    }
};

//派生类:Cat
class Cat :public Animal
{
public:
    virtual void makeSound() const override{
        cout << "Cat meows." << endl; 
    }
};

int main()
{
    //基类指针指向派生类对象
    Animal* animal1 = new Dog();
    Animal* animal2 = new Cat();

    //通过基类指针调用派生类的虚函数
    animal1->makeSound();
    animal2->makeSound();

    return 0;
}

更详细内容:C++(week12): C++基础 第八章:多态


5.多态

1.多态的含义:
多态,意为“多种形态”,即“一个接口,多种实现”。
用一个统一的接口(指针或引用)来操作不同的对象,并产生不同的行为


2.多态的分类
①编译时多态(静态多态),是通过函数重载、运算符重载实现的。
②运行时多态(动态多态),是基类指针指向派生类对象(或基类引用绑定派生类对象),从而能调用派生类覆盖的虚函数。通过继承和虚函数实现。


3.动态多态激活的5个条件
①基类定义了虚函数或声明了纯虚函数
②派生类中覆盖虚函数 (覆盖的是派生类的虚函数表中的虚函数的入口地址)
③创建派生类对象
④基类的指针指向派生类对象,或基类引用绑定派生类对象
⑤通过基类指针或引用调用虚函数


4.多态的效果?
基类指针可以调用到派生类的重写的虚函数

效果:基类指针指向派生类对象(或基类引用绑定派生类对象),那么基类指针就可以调用派生类的重写(覆盖/override)的虚函数。

这样你就可以用基类指针去使用别人定制的虚函数。如接口基类是你写的,你可以用基类指针调用同事写的派生类具体功能虚函数

Animal* animal1 = new Dog();
Animal* animal2 = new Cat();

animal1->makeSound();
animal2->makeSound();

5.动态多态的实现是怎样的?原理和过程


4.为什么析构函数建议是虚函数?(虚析构函数)
(1)如果派生类中有指针数据成员申请了堆空间,则基类的析构函数必须设为虚函数。
(2)不设为虚析构函数,则delete pbase 时只调用基类的虚构函数,而不会调用派生类的析构函数。造成派生类中指针申请的堆空间没有释放,造成内存泄露。


6.为什么构造函数不能是虚函数?
派生类在创建对象时,派生类的构造函数的初始化列表中会先调用基类的构造函数去创建基类子对象。
若基类的构造函数是虚函数,则此处调用会被覆盖,导致基类子对象无法创建,也就无法创建派生类对象。派生类对象都创建不了,就无法满足动态多态的5个条件,无法实现动态多态了。


7.基类和派生类的构造顺序、析构顺序?
先构造基类子对象,再构造派生类对象。
先析构派生类对象,再析构基类子对象。

//只创建一个派生类对象,观察基类和派生类的构造、析构顺序                       
Derived d;
Base()
Derived()
~Derived()
~Base()

6.STL

1.vector、list、array的对比
vector的扩容机制是怎样的?为什么频繁扩容影响性能?
考察点:容器底层原理
参考答案:vector默认扩容为原容量的2倍(或1.5倍,取决于编译器实现)。扩容时重新分配内存、拷贝/移动元素,时间复杂度O(n)。频繁扩容导致多次内存分配和拷贝,影响性能。建议使用reserve()预设容量减少扩容次数。


2.map、unordermap的使用区别、底层实现的区别
map与unordered_map的底层实现与性能差异?

考察点:容器选择能力

参考答案:map基于红黑树,有序,增删查O(log n);unordered_map基于哈希表,无序,增删查平均O(1),最坏O(n)。需要有序遍历用map,追求平均性能用unordered_map


7.设计模式 2种

(1)单例模式


(2)工厂模式


8.现代C++:11/14/17

(1)C++11有哪些新特性,请举例说明?


9.多线程


10.拷贝构造函数的三种调用时机

1.用已经存在的对象,创建并初始化一个同类型的新对象

Point pt2 = pt1;

2.对象作为参数进行传递时,实参初始化形参

void func(Point pt){
	
}

func(pt);

3.函数返回时,返回值是对象

return pt1;

11.友元的三种形式

1.将普通函数声明为友元函数

2.将其他类的成员函数声明为友元函数 (繁琐,不推荐)

3.友元类


12.运算符重载的三种形式

1.普通函数实现,但声明为友元函数

2.普通函数实现,调用公有接口以访问私有成员,如get函数

3.成员函数实现


13.初始化顺序、赋值顺序

1.初始化列表的初始化顺序:数据成员的声明顺序

2.参数的赋值顺序:从右向左


14.重载、隐藏、覆盖

1.重载 (overload):同一作用域下函数名相同参数不同 (个数、类型、顺序任一不同即可)。如函数重载、运算符重载。

2.隐藏 (oversee):发生在基类和派生类之间的数据成员和成员函数,同名。【派生类的同名函数隐藏基类的同名函数。只要求函数名相同。数据成员也只要名字相同。】

3.覆盖/重写 (override):发生在基类和派生类之间的虚函数。
派生类的函数与基类的虚函数,函数名相同、参数的个数和类型相同、返回值类型均相同,则派生类可以覆盖基类的虚函数。覆盖的是虚函数表中,虚函数的入口地址。
【经覆盖,指向派生类对象的基类指针,可以调用派生类重写的虚函数。】


15.构造函数

1.类的构造函数与类的普通函数一样,都需要携带隐藏的参数:this指针

2.派生类的构造函数,会先调用基类的构造函数(汇编层面会添加三行代码)

3.如果存在虚函数,构造函数会用虚函数表的地址来初始化虚函数指针,并保存在对象实例里面。


16.函数指针与指针函数

1.函数指针:指向函数的指针

int add(int a, int b) {
    return a + b;
}

int (*func_ptr)(int, int);

func_ptr = add;

int result = func_ptr(2, 3);

2.指针函数:返回值类型为指针类型的函数

int * fuc(){
	int num = 100;
	int * p = &num;
	return p;
}



三、Linux命令

1.Linux命令 场景题

1.如何创建一个名为server.c 空文件?
答:touch server.c

2:如何查找当前目录及其子目录下的所有扩展名为.log的文件?
答案:find . -name “*.log”

3.vim操作
(1)一个文件有一千多行,如何快速到最后一行?
答:G
(2)vim里默认没显示行数,如何操作可以让其显示行数?
答:set :nu

4.有一个名为blt.log的文件,如何快速统计出总共有多少行?
答:wc -l blt.log

原理:统计的是 换行符\n的个数,如果最后一行没加换行符,则会比实际少一行

5.如何在终端只看最前面10行?最后面10行呢?
答:head -10 blt.log;tail -10 blt.log

6.如何从burnin.log中过滤出所有包含[error]关键词的行,并将这些错误行保存到另一个文件error.log中? (考察grep和重定向>)
答:grep “[error]” burnin.log > error.log

7.如何压缩和解压缩?zip 和 tar.gz
答:zip/unzip,tar -czvf/ tar -xzvf

8.如何修改一个文件的权限?仅仅只给文件blt.py增加执行权限怎么做?
答案:chmod +x blt.py

9.如何查看一个目录下各子目录和文件的大小?
答案:du -sh *


2.Linux命令一览表

Linux基础:命令行
Linux常用命令行

1.文件与目录操作:ls, cd , pwd, mkdir, rm ,cp ,mv, find
2.文件内容查看与编辑:vim/nano, cat, head/tail, less/more
3.文本处理与过滤:grep, awk, sed, wc
4.系统与进程管理:ps, top/htop, kill, df, du, free
5.权限与所有权:chmod, chown, sudo
6.网络与连接:ping, curl/wget, ssh, scp, netstat
7.压缩与归档:tar, zip/unzip
8.内存外存监控命令:
监控内存情况:(-h 是以人类可读方式,将默认的KB单位换为MB或GB)
(1)free:free -h 查看内存使用情况,总内存、已使用内存、空闲内存
(2)top:查看各项资源使用情况,类似Windows的任务管理器
(3)du:du -h 查看目录所占磁盘空间
①du -sh:显示指定目录的总和,不递归显示每个文件或子目录的大小。
②du -ah:查看所有文件和目录所占磁盘空间
(4)df:查看文件系统(分区)的磁盘使用情况
9.ls后发现当前目录下都是白色,如何显示颜色?
答案1:本次命令临时显示颜色:ls --color=auto
答案2:长久保存显示颜色:
(1)vim ~/.bashrc
(2)alias ls=‘ls --color=auto’
(3)source ~/.bashrc



三、Git命令

1.上传:add、commit、push

1.添加:添加文件到暂存区

git add <file-name>       #添加单个文件
git add .                 #添加所有更改的文件

2.提交:提交文件到本地仓库

git commit -m "提交信息(姓名: 操作内容)"

3.推送:把操作推送到远程仓库

git push origin master  #gitee的默认分支名为master 
git push origin main    #github的默认分支名为main

2.下拉:git pull

git pull origin <branch-name>   #从远程分支拉取最新代码并合并到本地
git fetch origin <branch-name>  #仅下载远程分支的最新更新,但不合并(后续自己手动合并)

3.查看分支:git branch

git branch     #查看本地分支
git branch -v  #查看分支的最近提交信息
git branch -a  #查看所有分支(包括远程分支)
git branch -r  #查看远程分支
git status     #查看当前分支的详细信息
git branch <branch-name>     #创建分支
git branch -d <branch-name>  #删除本地分支
git push origin --delete <branch-name>  #删除远程分支

4.下载仓库

git clone

5.查看分支

答:git branch -av


6.切换分支

答:git checkout 分支名


7.更新子仓库

答:git submodule update --init –recursive



四、C++构建工具链(Build Toolchain)、构建命令

1.如何查看camke的版本?
答:cmake --version

2.cmake+ninja构建命令
答:cmake -S 源码路径 -B build -G Ninja



五、Python和Shell

1.用Python实现一个函数:
传入a+b,返回a+b之和

# 1. 无参数无返回值(相当于 C++ 的 void 函数)
def say_hello():
    print("你好!这是无参函数。")

# 2. 有参数有返回值(相当于 C++ 的普通函数)
def add(a, b):
    result = a + b
    return result

2.Python和C++有何异同点?
有什么是Python独有的?有什么是C++独有的?有什么是Python和C++共有的?
Python与C++:异同点对比



六、操作系统

1.32位系统的内存布局

(1)内核态:3-4G高地址
(2)用户态:0-3G低地址
栈区:操作系统分配的内存
堆区:程序员手动申请的内存,malloc、new
全局/静态区:存全局变量、静态变量
文字常量区:存常量
程序代码区:存代码

链接:C++内存布局


2.死锁

1.死锁的概念:
两个或多个线程或进程在执行过程中为争夺资源而造成的僵局。

2.死锁发生的4个必要条件:
①互斥
②不剥夺
③请求与保持
④循环等待

3.死锁的3种处理:
①死锁预防:破坏死锁发生的四个必要条件之一
②死锁避免:银行家算法
③死锁的检测与解除:资源分配图


3.用户态和内核态、宏内核与微内核

1.用户态与内核态
内核态可以执行特权指令。用户态只能执行用户指令。

特权指令:I/O指令、系统调用、开/关中断指令、修改PSW


2.宏内核与微内核
①宏内核是指内核空间包含了大多数的操作系统服务,如进程管理、内存管理、文件系统、设备驱动等。
这样切换服务时上下文切换成本低,开销小,性能高。
缺点是不稳定、不安全,一个服务崩溃可能导致整个内核崩溃。
②微内核的设计理念是尽可能地将系统服务和系统核心功能分离,微内核系统中只包含最基础的服务,如低级的地址空间管理、进程调度和通信机制。
优点是安全稳定,一个服务故障不会影响到内核。
缺点是切换服务上下文开销大,性能低。


七、网络编程 / 计算机网络

1.TCP和UDP有什么区别? —— TCP 比 UDP 可靠但慢

初级工程师:
在这里插入图片描述


中级工程师:
1.TCP粘包和拆包问题是什么?如何解决?【“粘包处理(自建协议头)】
在这里插入图片描述


2.TCP和UDP的应用场景选择【UDP比TCP不可靠但是快】
在这里插入图片描述


3.TCP的面向连接与UDP的无连接在epoll中的差异
在这里插入图片描述


4.UDP发送数据>MTU时被IP层分片,结果会怎样?【MTU 与分片、UDP 丢包处理策略】
在这里插入图片描述


2.Socket通信过程

Socket网络编程


TCP Socket编程的核心流程 (经典C/S架构)
TCP 编程遵循“服务器被动等待,客户端主动连接”的模式。流程图如下:
服务器端:socket() → bind() → listen() → accept()(阻塞)→ recv()/send() → close()
客户端:socket() → connect()(发起握手)→ send()/recv() → close()

在这里插入图片描述


3.三次握手

在这里插入图片描述

1.三次握手的过程
①第一次握手:客户端发送SYN报文 (SYN=1,seq=x),然后进入SYN-SENT状态
②第二次握手:服务器先是处于LISTEN状态,收到SYN报文后,回复SYN + ACK报文(SYN=1,ACK=1,seq=y,ack=x+1),然后进入SYN-RCVD状态
③第三次握手:客户端回复ACK报文(ACK=1,seq=x+1,ack=y+1),然后进入ESTABLISHED状态。服务器收到后,也进入ESTABLISHED状态。


2.为什么要三次握手?两次握手行不行,四次握手行不行?
(1)两次握手:
若没有第三次握手,相当于服务器没有SYN-RCVD状态,直接进入ESTABLISHED状态,会存在问题:
如果没有第三次握手,若客户端发送的SYN报文在网络中延迟,并在客户端的超时时间内未收到服务器的ACK,则客户端会重新发送SYN报文。没有第三次握手进行确认而只有两次握手,服务器就会错误地认为这是一个新的连接请求,回复的ACK也会被客户端抛弃。会占用服务器一个端口,而不发送任何数据,造成资源浪费。

(2)四次握手:
三次就够了,四次是多余的。

三次握手已经足够确保双方通信的可靠性和同步性:
第一次握手:客户端通知服务器“我想建立连接”。
第二次握手:服务器通知客户端“我收到了你的请求,并准备好建立连接”。
第三次握手:客户端通知服务器“我收到了你的确认,现在我们可以通信了”。


3.三次握手的过程图
在这里插入图片描述


4.SYN泛洪攻击 (SYN Flood 攻击):
客户端只发送一次握手,不进行第三次握手。
大量客户端发送SYN报文,使得服务器接受后就处于等待ACK的SYN-RCVD状态。此时不回复ACK,就会让服务器一直处于该状态,除非服务器主动断开连接。大量占用服务器,直至崩溃。


4.四次挥手

1.过程:
客户端与服务器刚开始都处于ESTABLISHED状态。
①第一次挥手:客户端发送FIN报文(FIN=1,seq=u),然后进入FIN-WAIT-1状态
②第二次挥手:服务器接受到FIN报文,并回复ACK报文(ACK=1,seq=v,ack=u+1),然后进入CLOSE-WAIT状态
③第三次挥手:服务器发送FIN报文(FIN=1,ACK=1,seq=w,ack=u+1),然后进入LAST-ACK状态
④第四次挥手:客户端发送ACK报文(ACK=1,seq=u+1,ack=w+1),然后进入TIME-WAIT状态,若2MSL内未收到服务器重发的第三次挥手,则CLOSED。服务器若成功收到第四次挥手,则CLOSED。


2.过程图
在这里插入图片描述


3.第一次、第二次挥手:关闭客户端的写操作、服务器的读操作。
第三次、第四次挥手:关闭服务器的写操作、客户端的读操作。


5.TCP粘包问题如何解决?

(1)TCP粘包/拆包问题是什么?—— TCP消息边界问题

TCP是面向流的协议(流式传输),没有消息边界。

  • TCP 是“流”协议:它只管把数据像水管里的水一样,源源不断地从 A 流到 B。它不关心你把数据分成了几次 send,只关心字节的顺序是否正确。
  • 粘包(Sticky Packet):发送方调用了 3 次 send(分别发了 “ABC”、“DEF”、“GHI”),接收方因为缓冲区读取时机,调用 1 次 recv 就读到了 “ABCDEFGHI”(粘在一起)。
  • 拆包(Half Packet):发送方发了 1 个 10KB 的大包,接收方调用 1 次 recv 只读到了 2KB(内核缓冲区暂时只有这么多,被拆开了)。
  • 核心结论:在 TCP 编程中,接收方永远无法保证“一次 recv 正好对应发送方的一次 send”。这就是粘包和拆包的本质。

面向流的协议在接收时会产生三种情况:完整包、粘包、拆包(以及拆粘混合)。
关键点:明确指出这是TCP的特性,而非bug。


(2)为什么会发生粘包和拆包?

  • 发送方原因(Nagle 算法):为了提高网络利用率,TCP 默认开启了 Nagle 算法。它会将多个小的、连续的数据包合并成一个大的 TCP 段发送,导致“粘包”。
  • 接收方原因(缓冲区读取):接收方内核的 TCP 缓冲区里积压了多个数据包,而用户进程调用 recv 时,一次性把缓冲区里的所有数据全取走了,导致“粘包”。
  • 链路层限制(MSS/MTU):网络链路层限制每次能传输的最大数据量(MTU 通常 1500 字节)。如果发送的包超过这个大小,IP 层会自动分片,在接收端重新组装,这会导致应用层感知到的“拆包”。

(3)解决方案:方案三,小火车协议:协议包头+协议包体

既然 TCP 不提供边界,边界必须由应用层自己定义。工业界通用的解决方案只有以下三种:


方案一:固定长度(Fixed Length)
做法:通信双方约定好每个报文固定为 N 个字节(比如 1024 字节)。如果数据不够,用 \0 或空格补足。
接收方:每次 recv 累计读取直到凑满 1024 字节,才交给业务逻辑处理。
优缺点:实现极简单,但非常浪费带宽(传输大量无意义填充字节),极少用于高性能场景。


方案二:特殊分隔符(Delimiter)
做法:在数据结尾添加特殊的标志符,比如 HTTP/1.1 使用 \r\n\r\n 作为头部结束标志,或者自定义 \n。
接收方:每收到一个字节就扫描缓冲区,一旦读到分隔符,就把之前的数据作为一个完整包取出。
优缺点:实现简单,但需要转义。如果数据本身内容里恰好包含了 \r\n\r\n,会导致提前截断,必须引入复杂的转义逻辑(如 C 语言字符串中的 \)。且遍历扫描消耗 CPU。


方案三:长度字段 + 正文(Length + Body)—— 工业界最通用方案
做法:每个数据包由 “固定大小的头部(Header)” + “可变长的正文(Body)” 组成。头部里用一个固定字段(通常是 4 个字节)标明 Body 的长度。
封包结构:[ 4字节总长度 ] [ 可变长度的业务数据 ]
接收方处理逻辑(至关重要):这是解决粘包的核心流程。
先 recv 读取 4 个字节,解析出 Body 的长度(假设是 N)。
循环调用 recv,累计读取直到凑齐 N 个字节。
将这 N 个字节完整地交给上层业务解析。剩下的缓冲区数据,留给下一个循环处理下一个包。

拓展:王道的小火车协议,就是方案三。
“小火车”这个比喻很形象,一列火车由车头和车厢组成,这正好对应了解决粘包最常用的“包头+包体”方案:

  • 车头 = 协议包头Header):固定长度,包含描述信息,例如整个数据包的长度。
  • 车厢 = 协议包体Body):存放真正的业务数据,长度可变。

发送端:
在这里插入图片描述

接收端:
在这里插入图片描述
第四步的理解:
在这里插入图片描述


6.TCP网络断点续传如何实现?——TCP本身不支持断点续传,需要程序员在应用层实现

背景:在实际生产环境中(比如游戏服务器、金融交易系统),网络闪断、服务器重启是家常便饭,“断点重连”是保证服务高可用的基石。
TCP 协议本身没有“断点重连”的功能(连接断了就是断了,Socket fd 失效)。所谓的“断点重连”,是应用层自己实现的逻辑。


(1)方案一:发送总文件的MD5码,对比文件大小,从断点offset续传 —— 多线程传多个文件

1.王道方案:上传文件之前先发送总文件MD5码和文件大小,跟服务器上的临时文件进行对比。如果两边总文件大小的MD5码一致则继续;MD5不一致则让其从头重传。
MD5码一致,再对比文件数据大小。如果文件大小一致则“秒传”,不一致则fseek到断点处继续传输。
这个传了一半的数据是在recv那边,send方是完整的。


DeepSeek改进建议:客户端发送文件总 MD5 → 服务器比对/续传 → 服务器收完整文件后,重新计算一次 MD5 → 如果和客户端之前发来的一致,返回“上传成功”;如果不一致,返回“文件损坏,请全部重传”。

在这里插入图片描述
在这里插入图片描述

完整步骤
在这里插入图片描述


(2)方案二:每个数据块加ID —— 可以多线程加速传输同一个文件

DeepSeek方案(多线程):每个数据块(Chunk)加序列号(SeqID)或者偏移量(offset);每成功recv一块数据就向发送方回复确认包(ACK);发送方要有缓存窗口,发出的数据不能立即丢弃,要等到收到接收方的ACK
在这里插入图片描述


(3)方案三:Header:时间戳+SeqID

有候选人回答是:Header:时间戳+SeqID,来对比上次传输到哪里了。


7.为何还要使用裸TCP,而不考虑已经封装完备的?

有候选人的回答是:
不使用已经封装好的TCP的原因是:钢铁厂、焦化厂环境高温、电磁脉冲影响,对于封装好的TCP(ZeroMQ、MQTT)这种库延迟比较高,写裸TCP会降低延迟,并且自己写的TCP Socket方便排查Bug。而封装好的TCP库都是黑盒,增加了排障难度。


(1)ZMQ(ZeroMQ,跨平台消息中间件)

1.嵌入式:ZMQ了太重了。在STM32开发板上,内存只有几十 KB的MCU,运行不了ZMQ

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述


(2)MQTT

在资源紧张的MCU上,MQTT是事实标准;而在资源丰富的Linux网关或边缘设备上,ZMQ则能发挥其低延迟和高性能的优势。

都是消息中间件,它们核心区别在于设计目标:

  • ZMQ:为系统内部组件通信设计,目标是高性能、低延迟、灵活。适用于树莓派或Linux网关等较强设备间通信。
  • MQTT:为物联网设备与云通信设计,目标是轻量、省电、可靠。适用于STM32/ESP8266等低功耗设备上传数据。

MQTT是连接海量低功耗设备与云端的“标准语言”;而ZeroMQ则更适合作为Linux服务器内部或网关之间追求极致低延迟通信的“高速总线”。


MQTT之所以能成为嵌入式物联网的“母语”,主要得益于以下特点:
1.核心优势:轻量与高效,报文头部仅2字节
2.核心模式:发布/订阅(Pub/Sub):
(1)解耦三方:发布者(传感器)与订阅者(App/数据库)无需知道对方IP,通过中央代理(Broker)交换数据。
(2)一对多广播:一个设备的数据可以同时被多个接收端消费,扩展性极强

在这里插入图片描述

在这里插入图片描述

“MQTT解决的是‘生态互联’和‘海量路由’问题,而不是‘简单通信’问题。10个设备搞MQTT,属于杀鸡用牛刀;但如果这10只鸡将来要进阿里云超市,那必须穿MQTT这件标准外套。”



八、数据库

1.数据库的事务和索引



九、Qt

1.Qt信号与槽机制


2.Qt元对象


3.Qt 事件机制 或者 事件循环


4.GDB调试、你一般都用QtCreator调试嘛


十、手撕算法

1.二分查找


2.LRU Cache


3.滑动窗口


4.排序

(1)手撕快速排序

原理:筛选一个基准元素,将待排序序列分割成两个子序列。使左边子序列所有元素都小于等于基准元素,右边子序列所有元素都大于等于基准元素。然后再对两个子序列分别进行快速排序,直到整个序列有序。
在这里插入图片描述

若交换,另一个while(执行一次)负责移动下标
未交换的时候,本while负责移动下标

核心代码:

int Partition(int A[],int L,int R){
	int mid = A[L];
	while(L < R){
		while(A[R]>=mid && L<R) R--;
		A[L] = A[R];
		while(A[L]<=mid && L<R) L++;
		A[R] = A[L];
	}
	A[L] = mid;
	return L;
}

void QuickSort(int A[],int L,int R){
	if(L >= R) return;
	int M = Partition(A,L,R);
	QuickSort(A,L,M-1);
	QuickSort(A,M+1,R);
}

对5,3,2,6,7,9,1,4,8,0进行一轮快速排序,得到的序列应该是:0,3,2,4,1,5,9,7,8,6

完整代码:

#include <iostream>
using namespace std;

int Partition(int A[],int L,int R){
    int mid = A[L];
    while(L < R){
        while(A[R]>=mid && L<R) R--;
        A[L] = A[R];
        while(A[L]<=mid && L<R) L++;
        A[R] = A[L];
    }
    A[L] = mid;
    return L;
}

void QuickSort(int A[],int L,int R){
    if(L>=R) return;
    int M = Partition(A,L,R);
    QuickSort(A,L,M-1);
    QuickSort(A,M+1,R);
}

int main() {
    int A[10] = {5,3,2,6,7,9,1,4,8,0};
    //排序前
    for(int i = 0; i < 10; ++i)     cout << A[i] <<" ";
    cout << endl;
    //快排
    QuickSort(A,0,9);
    //排序后
    for(int i = 0; i < 10; ++i)     cout << A[i] <<" ";
    cout << endl;
    return 0;
}

(2)冒泡降序排序

2.降序排序,小值从前往后冒泡

void Bubble_des(int A[],int n){
    for(int i = 0; i < n-1; ++i){
        for(int j = 0; j < n-1-i; ++j){  //从前往后
            if(A[j] < A[j+1]){           //对于降序,条件改为小于
                int temp = A[j];
                A[j] = A[j+1];
                A[j+1] = temp;
            }
        }
    }
}

4.降序排序,大值从后往前冒泡

void BubbleSort_des(int A[],int n){
    for(int i = 0; i < n-1; ++i){
        for(int j = n-1; j > i; --j){  //从后往前
            if(A[j] > A[j-1]){         //对于降序,改为大于
                int temp = A[j];
                A[j] = A[j-1];
                A[j-1] = temp;
            }
        }
    }
}

5.四维图新面试算法题

算法题:
(1)视觉算法相关题目
①NMS算法
②均值滤波
③YOLO中reorg算子实现
④FocallLoss
⑤匈牙利匹配算法
(2)数据结构
①链表节点插入
②二叉树遍历,前中后序,层序
③二叉搜索树 BST
④哈希表
⑤岛屿问题(dfs,bfs),岛屿的数量
(3)通用算法
①LRU Cache
②合法括号匹配
③滑动窗口
④二分查找
⑤快速幂
⑥重复元素(哈希表)
⑦合并两个链表
⑧前k个高频元素 (Top K)
(4)编程能力
①string to float
②矩阵旋转
③内存拷贝
④最长回文子串
⑤生产者消费者模型 (C++实现一个生产者消费者的多线程模型)
⑥线程池 (C++实现)
⑦内存池 (C++实现)

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

程序员爱德华

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值