智协慧同 C++开发 一面
1. 自我介绍
2. 二分查找的边界条件怎么处理
答案:二分查找容易写错的地方主要是区间定义不清楚。常见写法有闭区间 [l, r] 和左闭右开 [l, r),只要前后一致就可以。闭区间写法里,循环条件通常是 l <= r,因为 l == r 时区间里还有一个元素。更新时如果 mid 不满足,就要移动到 mid - 1 或 mid + 1,否则可能死循环。
如果查找的是第一个大于等于目标值的位置,也就是 lower_bound,推荐使用左闭右开区间,写起来更自然。实际面试里,二分不只是查等值,还经常用于答案二分,比如最小可行值、最大满足值,这时候关键是判断函数必须具备单调性。
代码:
#include <vector>
using namespace std;
int lowerBound(const vector<int>& nums, int target) {
int l = 0;
int r = nums.size();
while (l < r) {
int mid = l + (r - l) / 2;
if (nums[mid] >= target) {
r = mid;
} else {
l = mid + 1;
}
}
return l;
}
3. 为什么有些场景不能用 vector,vector 的连续内存会带来什么问题
答案:vector 的优势是连续内存、随机访问快、缓存友好,但它不适合所有场景。如果需要频繁在中间插入和删除,vector 会移动大量元素,代价较高。如果元素对象很大,移动成本也会更明显。如果保存的是指向元素的指针或迭代器,vector 扩容时会重新分配内存,导致原来的指针、引用和迭代器全部失效。
另外,vector 需要一段连续内存。如果元素数量非常大,即使总内存还够,也可能因为找不到足够大的连续虚拟地址空间而分配失败。对于频繁头部插入删除,可以考虑 deque;对于稳定迭代器和频繁中间插入删除,可以考虑 list;对于查找为主,可以考虑哈希表或树结构。
代码:
#include <vector>
#include <iostream>
using namespace std;
int main() {
vector<int> v;
v.reserve(2);
v.push_back(1);
int* p = &v[0];
v.push_back(2);
v.push_back(3); // 可能触发扩容,p 失效
// cout << *p << endl; // 悬空指针,不能再使用
}
4. 菱形继承会产生什么问题,虚继承如何解决
答案:菱形继承指的是两个中间类继承同一个基类,最终派生类又同时继承这两个中间类。这样最终派生类中会有两份公共基类子对象,访问公共基类成员时可能产生二义性,也会造成数据冗余。
虚继承可以让最终派生类中只保留一份虚基类子对象,从而解决重复继承的问题。但虚继承不是没有代价。它会让对象布局变复杂,编译器需要维护虚基类偏移信息,访问虚基类成员可能需要额外的间接寻址。所以虚继承一般只在确实需要共享公共基类子对象时使用,不建议为了“看起来高级”随便用。
代码:
#include <iostream>
using namespace std;
struct A {
int value = 10;
};
struct B : virtual public A {};
struct C : virtual public A {};
struct D : public B, public C {};
int main() {
D d;
d.value = 20;
cout << d.value << endl;
}
5. 动态库和静态库有什么区别,修改函数体或函数参数后什么时候需要重新链接
答案:静态库在链接阶段会被拷贝进最终可执行文件,程序运行时不再依赖原来的 .a 文件。动态库是在运行时加载的,多个进程可以共享同一份 .so 的代码段,升级动态库也更方便。如果修改了静态库的函数实现,使用它的可执行文件必须重新链接,否则可执行文件里仍然是旧代码。如果修改了动态库的函数实现,而导出的符号名和 ABI 没变,通常替换 .so 后重启进程即可生效,不需要重新链接可执行文件。
如果修改了函数参数、返回值、类布局、虚函数顺序、结构体字段等,就涉及 ABI 变化。这时即使是动态库,也不能简单替换,因为旧程序仍然按旧 ABI 调用新库,可能导致栈破坏、参数错位、对象布局不匹配。C++ 动态库尤其要注意 ABI 稳定,导出接口最好使用 C 风格接口、PImpl 或版本化接口。
代码:
# 生成静态库 g++ -c math.cpp -o math.o ar rcs libmath.a math.o g++ main.cpp -L. -lmath -o app_static # 生成动态库 g++ -fPIC -shared math.cpp -o libmath.so g++ main.cpp -L. -lmath -Wl,-rpath=. -o app_dynamic # 查看动态依赖 ldd ./app_dynamic
6. new/delete 和 malloc/free 的区别,为什么不能混用
答案:malloc/free 是 C 标准库函数,只负责申请和释放原始内存,不会调用构造函数和析构函数。new/delete 是 C++ 运算符,new 会先申请内存再调用构造函数,delete 会先调用析构函数再释放内存。
如果用 malloc 给 C++ 对象分配内存,对象的构造函数不会执行;如果用 free 释放 new 出来的对象,析构函数不会执行。更严重的是,不同分配方式可能走不同的内存管理路径,混用属于未定义行为。数组也要注意,new[] 必须配 delete[]。
代码:
#include <cstdlib>
#include <iostream>
using namespace std;
class FileGuard {
public:
FileGuard() {
cout << "open resource\n";
}
~FileGuard() {
cout << "close resource\n";
}
};
int main() {
FileGuard* p1 = new FileGuard();
delete p1;
void* mem = malloc(sizeof(FileGuard));
FileGuard* p2 = new (mem) FileGuard(); // placement new
p2->~FileGuard();
free(mem);
}
7. NULL 和 nullptr 有什么区别,为什么 C++11 推荐使用 nullptr
答案:NULL 本质上通常是 0 或 0L 的宏,在函数重载场景中可能被当作整数,从而匹配到错误的重载函数。nullptr 是 C++11 引入的空指针字面量,类型是 std::nullptr_t,可以隐式转换为任意指针类型,但不会被当成普通整数。
所以在现代 C++ 中,空指针应该优先使用 nullptr。它能避免重载歧义,也让代码语义更明确。
代码:
#include <iostream>
using namespace std;
void f(int) {
cout << "int\n";
}
void f(char*) {
cout << "char*\n";
}
int main() {
// f(NULL); // 可能匹配 int
f(nullptr); // 匹配 char*
}
8. C++11 之后哪些特性对工程代码影响最大
答案:C++11 之后影响比较大的特性有右值引用、移动语义、智能指针、lambda、auto、范围 for、nullptr、override、线程库、原子操作、constexpr、委托构造和统一初始化。其中移动语义和智能指针对工程代码影响最大。移动语义能减少大对象复制,智能指针能让资源生命周期更清晰。
lambda 在回调、排序、异步任务里非常常用,但捕获方式必须谨慎。override 可以让编译器检查虚函数重写是否正确,避免因为参数写错导致本来想重写,实际变成新函数。现代 C++ 的核心不是堆新语法,而是用 RAII 和类型系统减少资源泄漏和未定义行为。
代码:
#include <memory>
#include <vector>
#include <string>
using namespace std;
class Buffer {
private:
vector<char> data_;
public:
explicit Buffer(vector<char> data) : data_(move(data)) {}
};
int main() {
auto p = make_unique<Buffer>(vector<char>{'a', 'b', 'c'});
}
9. gdb 如何查看栈帧、局部变量、线程和崩溃位置
答案:gdb 排查崩溃时,先看程序收到的信号和崩溃位置,然后查看调用栈。bt 可以查看当前线程调用栈,frame n 切换到指定栈帧,info locals 查看局部变量,p var 打印变量。多线程程序要用 info threads 查看线程列表,用 thread apply all bt 打印所有线程栈。
如果 core 文件里变量被优化掉,可以重新用 -g -O0 编译复现;线上通常不会完全关闭优化,所以需要结合寄存器、汇编、日志和代码版本定位。如果是内存越界或 use-after-free,gdb 只能看到崩溃现场,根因可能在更早的位置,这时要结合 ASan、Valgrind 或硬件 watchpoint。
代码:
ulimit -c unlimited gdb ./server core.xxx (gdb) bt (gdb) frame 2 (gdb) info locals (gdb) p some_var (gdb) info threads (gdb) thread apply all bt
10. Docker 是怎么隔离进程、网络和文件系统的
答案:Docker 本质上不是虚拟机,它主要依赖 Linux namespace 和 cgroups。namespace 负责隔离视图,比如 PID namespace 让容器看到自己的进程树,Network namespace 让容器有独立网卡和路由表,Mount namespace 让容器看到自己的文件系统挂载。cgroups 负责资源限制,比如限制 CPU、内存、IO、进程数。
容器镜像一般采用分层文件系统,多个镜像层只读,运行时再叠加一个可写层。所以容器启动快、资源开销小,但它共享宿主机内核,隔离性弱于完整虚拟机。线上用 Docker 部署 C++ 服务时,要注意动态库依赖、时区、ulimit、日志挂载、网络模式和 core 文件路径。
代码:
docker build -t cpp-server:latest . docker run -d \ --name cpp-server \ --ulimit core=-1 \ -p 8080:8080 \ -v /data/logs:/app/logs \ cpp-server:latest docker exec -it cpp-server bash docker logs -f cpp-server
11. CMake 如何组织一个带动态库、静态库和可执行文件的工程
答案:CMake 的核心是描述 target,而不是手动拼命令。一个工程里通常会把公共代码编译成库,再让可执行文件链接这个库。如果库需要被其他模块使用,就要明确 include 目录、链接依赖和编译选项。
PUBLIC、PRIVATE、INTERFACE 很重要。PRIVATE表示只
剩余60%内容,订阅专栏后可继续查看/也可单篇购买
本专栏系统梳理C++方向, 大中厂高频高频面试考点 , 内容皆来自真实面试经历,从基础语法、内存管理、STL与设计模式,到操作系统与项目实战,结合真实面试题深度解析,帮助开发者高效查漏补缺,提升技术理解与面试通过率,打造扎实的C++工程能力.
