吉比特 C++开发 一面
1. 自我介绍
2. 进程和线程的区别,线程共享哪些资源,不共享哪些资源
答案:进程是资源分配的基本单位,线程是 CPU 调度的基本单位。一个进程有独立的虚拟地址空间、文件描述符表、信号处理信息等;同一个进程内的多个线程共享地址空间、全局变量、堆、打开的文件描述符和代码段。
线程之间不共享的是各自的栈、寄存器上下文、线程局部存储和调度状态。所以线程切换通常比进程切换轻,因为不需要切换完整地址空间;但线程共享内存也意味着更容易出现数据竞争、死锁和内存可见性问题。
在车载边缘诊断数据采集与规则告警平台里,接入线程、解析线程、规则线程会共享任务队列和配置快照,这些地方必须明确加锁、无锁队列或者只读快照,否则并发问题很难排查。
3. 进程里一定有线程吗,主线程退出后其他线程会怎样
答案:进程至少有一个执行流,也就是主线程。传统说法里,一个进程至少包含一个线程,否则没有代码能被调度执行。在 Linux 下,线程本质上是通过 clone 创建的轻量级任务,它们共享同一个进程地址空间。主线程如果从 main 返回,相当于调用 exit,通常会终止整个进程,其他线程也会被结束。
如果只想结束当前线程,应该调用 pthread_exit 或让线程函数返回,而不是让整个进程退出。工程里常见问题是主线程启动了后台工作线程后直接返回,导致服务刚启动就退出。正确做法是主线程进入事件循环,或者等待工作线程结束。
代码:
#include <pthread.h>
#include <iostream>
#include <unistd.h>
using namespace std;
void* worker(void*) {
while (true) {
cout << "worker running\n";
sleep(1);
}
return nullptr;
}
int main() {
pthread_t tid;
pthread_create(&tid, nullptr, worker, nullptr);
// 如果 main 直接 return,整个进程会退出
pthread_join(tid, nullptr);
return 0;
}
4. 进程状态有哪些,僵尸进程和孤儿进程分别是什么
答案:Linux 进程常见状态有运行态、可中断睡眠、不可中断睡眠、停止态、僵尸态等。运行态表示正在运行或在运行队列中等待 CPU;可中断睡眠一般是在等待 IO、锁、定时器等事件;不可中断睡眠通常是在等待内核态资源,不能被普通信号打断;停止态一般是被调试或信号暂停。
僵尸进程是子进程已经退出,但父进程没有调用 wait 或 waitpid 回收它的退出状态,进程表里还保留一条记录。大量僵尸进程会占用 pid 和进程表资源。孤儿进程是父进程先退出,子进程还在运行,它会被 init/systemd 接管,一般不算问题。
代码:
#include <sys/wait.h>
#include <unistd.h>
#include <iostream>
using namespace std;
int main() {
pid_t pid = fork();
if (pid == 0) {
_exit(0);
}
int status = 0;
waitpid(pid, &status, 0); // 回收子进程,避免僵尸进程
cout << "child recycled\n";
return 0;
}
5. 进程间通信方式有哪些,管道、消息队列、共享内存和 socket 怎么选
答案:常见进程间通信方式有管道、命名管道、消息队列、共享内存、信号、信号量、Unix Domain Socket、TCP/UDP socket、mmap 文件映射等。管道适合父子进程之间简单的字节流通信;消息队列适合传递结构化消息;共享内存速度最快,因为数据不需要在内核和用户态之间反复拷贝,但同步要自己处理;socket 最通用,可以跨机器,也可以本机进程间通信。
如果是高吞吐的大块数据交换,可以考虑共享内存加同步机制。如果是模块解耦和可维护性优先,Unix Domain Socket 或 TCP 更容易调试。实际项目里不能只看性能,还要看容错、权限、协议演进、故障隔离和排查成本。
6. C++ 多态的底层实现,虚函数表放在哪里,什么时候形成
答案:C++ 运行时多态通常通过虚函数表和虚表指针实现。类中只要有虚函数,编译器一般会为这个类生成虚函数表,表里存放虚函数入口地址。对象里会有虚表指针,指向它真实类型对应的虚函数表。
虚函数表属于编译器生成的静态数据,通常放在只读数据段附近,不是每个对象一份。每个对象里通常只保存虚表指针。对象构造过程中,vptr 会随着构造层级变化。先构造基类部分,此时 vptr 指向基类虚表;再构造派生类部分,vptr 才指向派生类虚表。所以构造函数里调用虚函数不会表现出派生类多态。
代码:
#include <iostream>
using namespace std;
class Base {
public:
virtual void run() {
cout << "Base\n";
}
virtual ~Base() = default;
};
class Derived : public Base {
public:
void run() override {
cout << "Derived\n";
}
};
int main() {
Base* p = new Derived();
p->run();
delete p;
}
7. C++ 和 Java 的多态有什么本质差异
答案:C++ 的多态分为静态多态和运行时多态。模板、函数重载属于编译期确定;虚函数属于运行时动态绑定。C++ 只有被声明为 virtual 的成员函数才会走动态分派,普通函数调用默认是静态绑定。Java 中普通实例方法默认具有动态分派特性,除了 static、private、final 等特殊情况。
C++ 的多态更贴近对象内存布局,开发者需要关心虚析构、对象切片、指针转换、构造析构阶段虚函数行为。Java 由 JVM 管理对象和方法分派,运行时还有 JIT、内联缓存、逃逸分析等优化。它少了手动内存释放问题,但多了 GC 和 JVM 运行时行为。
在工程上,C++ 多态性能可控但容易踩生命周期坑,Java 多态语义统一但运行时环境更复杂。
8. 如果不用 C++ 语法,用 C 语言怎么模拟多态
答案:C 语言可以通过函数指针表模拟虚函数表。做法是定义一张操作表,里面放函数指针;每个对象结构体里放一个指向操作表的指针。不同类型对象初始化时绑定不同的操作表,调用时通过函数指针间接调用。
这种方式在驱动、网络协议栈、组件插件系统里很常见。它的缺点是编译器不会帮你做类型检查和生命周期管理,函数签名、对象转换、析构释放都要自己保证。
代码:
#include <stdio.h>
typedef struct Device Device;
typedef struct {
void (*start)(Device*);
void (*stop)(Device*);
} DeviceOps;
struct Device {
DeviceOps* ops;
int id;
};
void sensorStart(Device* d) {
printf("sensor %d start\n", d->id);
}
void sensorStop(Device* d) {
printf("sensor %d stop\n", d->id);
}
DeviceOps sensorOps = {
sensorStart,
sensorStop
};
int main() {
Device dev;
dev.id = 1001;
dev.ops = &sensorOps;
dev.ops->start(&dev);
dev.ops->stop(&dev);
return 0;
}
9. 带有虚函数的类会有什么额外问题,为什么基类析构函数通常要写成 virtual
答案:带虚函数的类会引入虚表指针,对象大小会增加,函数调用也可能多一次间接跳转。更重要的是,它意味着这个类可能被当作基类使用,这时析构函数必须特别注意。
如果通过基类指针删除派生类对象,而基类析构函数不是虚函数,只会调用基类析构,派生类资源不会释放,导致资源泄漏甚至未定义行为。所以只要一个类准备作为多态基类使用,析构函数通常就应该声明为 virtual。
另外带虚函数的类还要注意对象切片。按值传递基类对象会截掉派生类部分,导致多态失效。工程里多态对象一般通过引用、裸指针观察,或者用 unique_ptr<Base> / shared_ptr<Base> 管理。
代码:
#include <iostream>
using namespace std;
class Handler {
public:
virtual void handle() = 0;
virtual ~Handler() {
cout << "Handler dtor\n";
}
};
class AlarmHandler : public Handler {
public:
~AlarmHandler() {
cout << "AlarmHandler dtor\n";
}
void handle() override {
cout << "handle alarm\n";
}
};
int main() {
Handler* h = new AlarmHandler();
delete h;
}
10. map 和 unordered_map 的底层结构、复杂度和迭代器失效规则有什么区别
答案:map 通常基于红黑树实现,key 有序,查找、插入、删除复杂度是 O(logN)。它适合需要有序遍历、范围查询、找前驱后继、按 key 顺序输出的场景。unordered_map 基于哈希表实现,平均查找、插入、删除是 O(1),但不保证顺序,哈希冲突严重时会退化。
迭代器失效上,map 插入一般不会导致已有迭代器失效,删除某个元素只会让被删除元素的迭代器失效。unordered_map 插入如果触发 rehash,会导致所有迭代器失效;删除元素会让被删除元素的迭代器失效。所以在遍历 unordered_map 的同时插入元素要特别小心,尤其是数据量增长触发扩容时。
代码:
#include <unordered_map>
#include <iostream>
using namespace std;
int main() {
unordered_map<int, int> mp;
mp.reserve(10000);
mp.max_load_factor(0.7);
for (int i = 0; i < 1000; ++i) {
mp[i] = i * 10;
}
auto it = mp.find(10);
if (it != mp.end()) {
cout << it->second << endl;
}
}
11. map 和 unordered_map 常见操作的时间复杂度分别是多少,为什么最坏情况不一样
答案:map 的查找、插入、删除一般都是 O(logN),因为红黑树高度近似平衡。它的复杂度比较稳定,不太受 key 分布影响。unordered_map 平均情况下查找、插入、删除是 O(1),因为通过 hash 直接定位桶。但如果大量 key 落到同一个桶里,链表或桶内结构会变长,最坏可能退化到 O(N)。
unordered_map 的性能受 hash 函数、负载因子、桶数量、rehash 时机影响很大。如果 key 是自定义类型,必须提供质量较好的 hash,否则表面上用了哈希表,实际性能可能非常差。
代码:
#include <unordered_ma
剩余60%内容,订阅专栏后可继续查看/也可单篇购买
本专栏系统梳理C++方向, 大中厂高频高频面试考点 , 内容皆来自真实面试经历,从基础语法、内存管理、STL与设计模式,到操作系统与项目实战,结合真实面试题深度解析,帮助开发者高效查漏补缺,提升技术理解与面试通过率,打造扎实的C++工程能力.