智协慧同 C++开发 一面

1. 自我介绍

2. 二分查找的边界条件怎么处理

答案:二分查找容易写错的地方主要是区间定义不清楚。常见写法有闭区间 [l, r] 和左闭右开 [l, r),只要前后一致就可以。闭区间写法里,循环条件通常是 l <= r,因为 l == r 时区间里还有一个元素。更新时如果 mid 不满足,就要移动到 mid - 1mid + 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. 为什么有些场景不能用 vectorvector 的连续内存会带来什么问题

答案: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/deletemalloc/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. NULLnullptr 有什么区别,为什么 C++11 推荐使用 nullptr

答案:NULL 本质上通常是 00L 的宏,在函数重载场景中可能被当作整数,从而匹配到错误的重载函数。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、nullptroverride、线程库、原子操作、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 目录、链接依赖和编译选项。

PUBLICPRIVATEINTERFACE 很重要。PRIVATE表示只

剩余60%内容,订阅专栏后可继续查看/也可单篇购买

C++ 常考面试题总结 文章被收录于专栏

本专栏系统梳理C++方向, 大中厂高频高频面试考点 , 内容皆来自真实面试经历,从基础语法、内存管理、STL与设计模式,到操作系统与项目实战,结合真实面试题深度解析,帮助开发者高效查漏补缺,提升技术理解与面试通过率,打造扎实的C++工程能力.

全部评论

相关推荐

评论
点赞
收藏
分享

创作者周榜

更多
牛客网
牛客网在线编程
牛客网题解
牛客企业服务