Skip to content

C/C++开发面试题

💡 核心要点

C/C++开发重点考察指针操作、内存管理、面向对象设计、STL使用,以及性能优化。面试官通常会从基础语法和指针入手,逐步深入到复杂的模板元编程和并发编程。

基础语法

1. C vs C++

❓ C和C++有什么区别?为什么还要学C?

面试官翻了翻简历:"你会C也懂C++,那我问你,C和C++的本质区别是什么?"

💡 点击查看满分回答

C是过程式语言,C++是多范式语言,支持面向对象。

C语言特点:

  • 编程范式:纯过程式编程
  • 抽象层次:接近硬件,指针操作灵活
  • 标准库:stdlib.h、string.h等基础库
  • 编译方式:直接编译为机器码
  • 应用领域:系统编程、嵌入式、驱动开发

C++语言特点:

  • 编程范式:多范式(过程式、面向对象、泛型、函数式)
  • 抽象层次:更高层次抽象,STL、模板等
  • 标准库:STL容器、算法、智能指针
  • 编译方式:先编译为C,再编译为机器码
  • 应用领域:应用软件、游戏、服务器、高性能计算

为什么还要学C:

  • 底层理解:理解内存布局、指针运算、位操作
  • 性能优化:C代码通常比C++高效
  • 系统编程:操作系统、驱动、嵌入式必备
  • 兼容性:C代码可以无缝在C++中使用
  • 学习曲线:掌握C后再学C++更容易

选择原则:

  • 系统级开发用C:性能要求极高,资源受限
  • 应用级开发用C++:需要复杂抽象,快速开发
  • 混合使用:C实现核心算法,C++封装接口

2. 指针 vs 引用

❓ C++中指针和引用有什么区别?什么时候用哪个?

面试官点了点头:"指针和引用你能区分吗?举个例子说明?"

💡 点击查看满分回答

指针是变量存储地址,引用是变量的别名。

指针(Pointer):

  • 本质:存储变量内存地址的变量
  • 语法int* p = &x;
  • 可操作性:可以重新赋值,指向不同对象
  • 空值:可以为nullptr
  • 运算:支持指针算术运算

引用(Reference):

  • 本质:变量的别名,必须在定义时初始化
  • 语法int& r = x;
  • 可操作性:一旦绑定不能改变
  • 空值:不能为null,必须绑定有效对象
  • 运算:不支持算术运算

使用场景:

cpp
// 指针使用场景
void swap(int* a, int* b) {
    int temp = *a;
    *a = *b;
    *b = temp;
}

// 引用使用场景
void swap(int& a, int& b) {
    int temp = a;
    a = b;
    b = temp;
}

// 函数参数:引用避免拷贝
void process(const std::vector<int>& data) {
    // 不拷贝vector,只传递引用
}

// 返回值:引用避免拷贝
const std::string& get_name() {
    return name_;  // 返回引用
}

选择原则:

  • 可能为空用指针:如可选参数
  • 必须有效用引用:如函数参数返回值
  • 指针算术用指针:如数组遍历
  • 多态用指针/引用:基类指针引用
  • STL风格用引用:符合C++标准库惯例

内存管理

3. 堆 vs 栈

❓ 堆和栈有什么区别?内存泄漏怎么避免?

面试官敲了敲桌子:"堆和栈的区别你知道吗?内存泄漏怎么检测?"

💡 点击查看满分回答

栈自动管理,堆需要手动管理。

栈(Stack)内存:

  • 分配方式:自动分配和释放
  • 生命周期:函数调用期间
  • 大小限制:通常1-8MB,可配置
  • 访问速度:最快,硬件支持
  • 用途:局部变量、函数参数、返回地址

堆(Heap)内存:

  • 分配方式:程序员手动管理(new/delete)
  • 生命周期:程序员控制
  • 大小限制:受虚拟内存限制,可很大
  • 访问速度:较慢,需要系统调用
  • 用途:动态数据结构,大对象

内存泄漏避免:

RAII原则:

cpp
class Resource {
public:
    Resource() { data_ = new int[100]; }
    ~Resource() { delete[] data_; }
private:
    int* data_;
};

void func() {
    Resource res;  // 自动管理
} // res析构,内存自动释放

智能指针:

cpp
#include <memory>

void func() {
    std::unique_ptr<int> p1(new int(42));
    std::shared_ptr<int> p2 = std::make_shared<int>(42);
    std::weak_ptr<int> p3 = p2;  // 避免循环引用
} // 自动释放

检测工具:

  • Valgrind:动态检测内存泄漏
  • AddressSanitizer:编译时插桩检测
  • Visual Studio CRT:Windows下内存检测

最佳实践:

  • 优先栈分配:小对象用栈
  • 智能指针管理堆内存:避免手动new/delete
  • 容器管理动态数组:用vector而非数组
  • 异常安全:确保异常时内存正确释放

4. new vs malloc

❓ new和malloc有什么区别?delete和free可以混用吗?

面试官推了推眼镜:"new和malloc的区别是什么?为什么不能混用delete和free?"

💡 点击查看满分回答

new是运算符,malloc是函数;new调用构造函数,malloc不调用。

new vs malloc:

特性newmalloc
类型运算符函数
内存位置自由存储区
大小自动计算手动指定
构造函数调用不调用
失败处理抛异常返回nullptr
重载可重载不可重载
类型安全类型安全void*需要转换

代码示例:

cpp
// new使用
class Object {
public:
    Object() { std::cout << "Constructor\n"; }
    ~Object() { std::cout << "Destructor\n"; }
};

Object* obj = new Object();  // 调用构造函数
delete obj;  // 调用析构函数

// malloc使用
Object* obj = (Object*)malloc(sizeof(Object));  // 不调用构造函数
free(obj);  // 不调用析构函数

为什么不能混用:

  • 内存布局不同:new可能在对象前后添加管理信息
  • 构造函数/析构函数:new/delete会调用,malloc/free不会
  • 异常安全:new失败抛异常,malloc返回null
  • 重载:new/delete可被类重载

最佳实践:

  • C++代码用new/delete:保证构造函数调用
  • C代码用malloc/free:纯内存分配
  • 智能指针:避免手动内存管理
  • 容器类:用vector等自动管理内存

面向对象

5. 继承 vs 组合

❓ 继承和组合有什么区别?什么时候用哪个?

面试官笑了笑:"继承和组合你怎么选择?举个例子?"

💡 点击查看满分回答

继承是"is-a"关系,组合是"has-a"关系。

继承(Inheritance):

  • 关系类型:"is-a"关系,子类是父类的特化
  • 耦合度:紧耦合,子类依赖父类实现
  • 灵活性:编译时确定,运行时不可变
  • 适用场景:多态行为,接口一致性

组合(Composition):

  • 关系类型:"has-a"关系,类包含其他类对象
  • 耦合度:松耦合,可动态改变
  • 灵活性:运行时可变,依赖注入
  • 适用场景:组件复用,策略模式

代码示例:

cpp
// 继承示例
class Animal {
public:
    virtual void speak() = 0;
};

class Dog : public Animal {
public:
    void speak() override { std::cout << "Woof\n"; }
};

// 组合示例
class Speaker {
public:
    virtual void speak() = 0;
};

class DogSpeaker : public Speaker {
public:
    void speak() override { std::cout << "Woof\n"; }
};

class Robot {
private:
    std::unique_ptr<Speaker> speaker_;
public:
    Robot(std::unique_ptr<Speaker> s) : speaker_(std::move(s)) {}
    void makeSound() { speaker_->speak(); }
};

设计原则:

  • 优先组合:组合比继承更灵活
  • 接口继承:纯虚函数接口继承
  • 实现组合:数据成员组合
  • 多态组合:策略模式等

选择标准:

  • "is-a"关系用继承:如Dog is an Animal
  • "has-a"关系用组合:如Car has an Engine
  • 运行时变化用组合:如不同策略
  • 编译时确定用继承:如固定层次结构

6. 虚函数机制

❓ 虚函数是怎么实现的?虚函数表在哪里?

面试官皱了皱眉:"虚函数的实现机制你了解吗?为什么有性能开销?"

💡 点击查看满分回答

虚函数通过虚函数表(vtable)实现运行时多态。

虚函数表(Virtual Table):

  • 存储位置:每个类有独立的虚函数表
  • 内容:指向虚函数实现的指针数组
  • 对象布局:对象前4/8字节存储vtable指针
  • 继承关系:子类vtable覆盖父类对应条目

动态绑定过程:

  1. 对象创建:构造函数设置vptr指向类vtable
  2. 函数调用:通过vptr找到vtable,再找到函数指针
  3. 间接调用:jmp到实际函数实现

代码示例:

cpp
class Base {
public:
    virtual void func() { std::cout << "Base\n"; }
};

class Derived : public Base {
public:
    void func() override { std::cout << "Derived\n"; }
};

// 内存布局
/*
Base object:
+-----------+
| vptr      |  --> Base vtable: [Base::func]
+-----------+
| data...   |
+-----------+

Derived object:
+-----------+
| vptr      |  --> Derived vtable: [Derived::func]
+-----------+
| data...   |
+-----------+
*/

性能开销:

  • 额外内存:vtable占用空间
  • 间接调用:无法内联,分支预测失效
  • 缓存影响:vptr访问影响缓存局部性
  • 构造开销:构造函数设置vptr

优化技巧:

  • 非虚函数:不需要多态的函数不要设为虚函数
  • final关键字:阻止进一步继承优化
  • 模板方法:编译时多态避免运行时开销
  • 内联优化:虚函数很少内联

最佳实践:

  • 基类析构函数为虚:防止内存泄漏
  • 虚函数声明为override:编译时检查
  • 避免在构造函数调用虚函数:未完成构造
  • 接口类用纯虚函数:定义契约

STL使用

7. vector vs list vs deque

❓ STL容器那么多,vector、list、deque有什么区别?

面试官点了点头:"STL容器你最常用哪些?它们的时间复杂度是怎样的?"

💡 点击查看满分回答

不同容器针对不同使用模式优化。

vector:

  • 底层实现:动态数组,连续内存
  • 优点:随机访问快,内存局部性好
  • 缺点:插入删除慢(需要移动元素)
  • 时间复杂度:访问O(1),插入/删除O(n)
  • 适用场景:随机访问多,尾部插入删除

list:

  • 底层实现:双向链表,节点分散
  • 优点:任意位置插入删除快
  • 缺点:随机访问慢,内存开销大
  • 时间复杂度:访问O(n),插入/删除O(1)
  • 适用场景:频繁插入删除,顺序访问

deque:

  • 底层实现:分段连续内存,双端队列
  • 优点:两端操作快,随机访问较好
  • 缺点:中间操作慢,内存稍多
  • 时间复杂度:访问O(1),两端操作O(1),中间O(n)
  • 适用场景:队列应用,两端操作

选择原则:

  • 默认选择vector:性能好,应用广
  • 频繁中间插入用list:如链表操作
  • 队列应用用deque:FIFO需求
  • 内存受限用vector:连续内存效率高

实际性能对比:

  • 遍历速度:vector > deque > list
  • 随机访问:vector ≈ deque > list
  • 插入删除:list > deque > vector
  • 内存效率:vector > deque > list

8. map vs unordered_map

❓ map和unordered_map有什么区别?时间复杂度是多少?

面试官翻了翻笔记:"map和unordered_map你怎么选择?红黑树和哈希表哪个快?"

💡 点击查看满分回答

map基于红黑树,unordered_map基于哈希表。

map:

  • 底层实现:红黑树,有序存储
  • 优点:有序,查找稳定
  • 缺点:查找慢,插入慢
  • 时间复杂度:查找/插入/删除O(log n)
  • 适用场景:需要有序,范围查询

unordered_map:

  • 底层实现:哈希表,无序存储
  • 优点:查找快,平均性能好
  • 缺点:最坏情况退化,无序
  • 时间复杂度:平均O(1),最坏O(n)
  • 适用场景:快速查找,无序需求

性能对比:

  • 小数据量:unordered_map稍快
  • 大数据量:unordered_map明显快
  • 有序需求:只能用map
  • 内存使用:unordered_map稍多(哈希表开销)

代码示例:

cpp
#include <map>
#include <unordered_map>

std::map<std::string, int> ordered_map;
std::unordered_map<std::string, int> hash_map;

// 有序遍历
for (const auto& pair : ordered_map) {
    // 按key有序访问
}

// 快速查找
auto it = hash_map.find("key");
if (it != hash_map.end()) {
    // 找到了
}

选择原则:

  • 需要有序用map:如排序输出,范围查找
  • 性能优先用unordered_map:如缓存,配置存储
  • 数据量大用unordered_map:O(log n) vs O(1)
  • 键类型复杂用map:自定义比较函数

优化技巧:

  • 自定义哈希函数:改善unordered_map性能
  • 负载因子控制:rehash时机
  • 预留空间:减少rehash开销

模板编程

9. 模板 vs 宏

❓ 模板和宏有什么区别?为什么模板更好?

面试官看了眼手表:"模板和宏你怎么选?模板的特化是什么?"

💡 点击查看满分回答

模板是类型安全的编译时多态,宏是文本替换。

宏(Macro):

  • 处理时机:预处理时文本替换
  • 类型安全:无类型检查,可能错误
  • 调试困难:错误信息指向展开后代码
  • 副作用:可能重复计算参数

模板(Template):

  • 处理时机:编译时实例化
  • 类型安全:编译时类型检查
  • 调试友好:错误信息清晰
  • 重载支持:函数重载,运算符重载

代码对比:

cpp
// 宏定义
#define MAX(a, b) ((a) > (b) ? (a) : (b))
#define SQUARE(x) ((x) * (x))

// 模板定义
template<typename T>
T max(T a, T b) {
    return a > b ? a : b;
}

template<typename T>
T square(T x) {
    return x * x;
}

// 使用
int result1 = MAX(1, 2);  // 宏
int result2 = max(1, 2);  // 模板

double d = SQUARE(3.14);  // 可能重复计算
double d2 = square(3.14); // 不会重复计算

模板特化:

cpp
// 通用模板
template<typename T>
class Container {
public:
    void process(T value) { /* 通用处理 */ }
};

// 特化版本
template<>
class Container<bool> {
public:
    void process(bool value) { /* 布尔特化处理 */ }
};

为什么模板更好:

  • 类型安全:编译时检查类型错误
  • 调试友好:错误信息指向模板定义
  • 内联优化:编译器可内联展开
  • 重载支持:支持函数重载

最佳实践:

  • 简单替换用宏:如常量定义
  • 类型相关用模板:泛型编程
  • 避免宏复杂逻辑:用模板或函数
  • 模板特化谨慎:只在必要时使用

10. SFINAE和enable_if

❓ SFINAE是什么?enable_if怎么用?

面试官笑了笑:"模板元编程你了解吗?SFINAE有什么用?"

💡 点击查看满分回答

SFINAE是模板编译失败不报错的机制,用于模板特化选择。

SFINAE原理:

  • Substitution Failure Is Not An Error:替换失败不是错误
  • 编译过程:模板实例化时,如果替换导致无效代码,跳过该特化
  • 应用场景:函数重载决议,traits类设计

enable_if使用:

cpp
#include <type_traits>

template<typename T>
typename std::enable_if<std::is_integral<T>::value, void>::type
process(T value) {
    // 只对整数类型实例化
    std::cout << "Processing integer: " << value << std::endl;
}

template<typename T>
typename std::enable_if<std::is_floating_point<T>::value, void>::type
process(T value) {
    // 只对浮点类型实例化
    std::cout << "Processing float: " << value << std::endl;
}

// C++14简化语法
template<typename T>
std::enable_if_t<std::is_integral_v<T>, void>
process_v14(T value) {
    // 使用_v后缀和_t别名
}

实际应用:

类型traits:

cpp
template<typename T>
struct is_pointer : std::false_type {};

template<typename T>
struct is_pointer<T*> : std::true_type {};

template<typename T>
std::enable_if_t<is_pointer<T>::value, void>
print_pointer(T ptr) {
    std::cout << "Pointer value: " << *ptr << std::endl;
}

函数重载选择:

cpp
// 容器类型检测
template<typename T>
struct is_container : std::false_type {};

template<typename T, typename Alloc>
struct is_container<std::vector<T, Alloc>> : std::true_type {};

// 不同处理函数
template<typename T>
std::enable_if_t<!is_container<T>::value, void>
serialize(T value) {
    // 基本类型序列化
}

template<typename T>
std::enable_if_t<is_container<T>::value, void>
serialize(const T& container) {
    // 容器类型序列化
}

最佳实践:

  • 条件编译:用enable_if控制模板实例化
  • traits类:定义类型特征
  • 组合使用:enable_if与其他traits结合
  • C++14/17:使用简化的_v和_t后缀

并发编程

11. 线程 vs 进程

❓ 多线程和多进程有什么区别?C++11线程库怎么用?

面试官推了推眼镜:"多线程和多进程你怎么选?std::thread的坑有哪些?"

💡 点击查看满分回答

线程共享内存,进程独立内存空间。

进程(Process):

  • 资源拥有:独立地址空间、文件描述符
  • 通信方式:管道、消息队列、共享内存
  • 创建开销:大,需要复制页表等
  • 隔离性:完全隔离,一个崩溃不影响其他
  • 适用场景:需要强隔离,运行不同程序

线程(Thread):

  • 资源拥有:共享地址空间,与进程共享资源
  • 通信方式:直接访问共享内存
  • 创建开销:小,只需创建栈和上下文
  • 隔离性:不完全隔离,线程崩溃可能影响进程
  • 适用场景:共享数据,并发计算

C++11线程使用:

cpp
#include <thread>
#include <iostream>

void worker(int id) {
    std::cout << "Worker " << id << " started\n";
    // 工作内容
}

int main() {
    std::thread t1(worker, 1);
    std::thread t2(worker, 2);
    
    t1.join();  // 等待线程结束
    t2.join();
    
    return 0;
}

std::thread注意事项:

  • join或detach:必须调用,否则程序终止
  • 异常安全:用RAII包装join
  • 线程局部存储:thread_local关键字
  • 硬件并发数:std:🧵:hardware_concurrency()

选择原则:

  • 数据共享用线程:共享内存通信
  • 强隔离用进程:安全性要求高
  • I/O密集用线程:减少上下文切换
  • CPU密集用进程:利用多核,避免GIL

12. 互斥锁 vs 原子操作

❓ std::mutex和std::atomic有什么区别?什么时候用哪个?

面试官皱了皱眉:"锁和原子操作你怎么选?原子操作怎么保证线程安全?"

💡 点击查看满分回答

原子操作是硬件级线程安全,互斥锁是软件级同步。

互斥锁(std::mutex):

  • 保护粒度:代码块级别
  • 实现方式:操作系统互斥原语
  • 性能开销:较大,上下文切换
  • 死锁风险:不当使用导致死锁
  • 适用场景:保护复杂操作,临界区大

原子操作(std::atomic):

  • 保护粒度:单个变量级别
  • 实现方式:硬件原子指令
  • 性能开销:最小,无上下文切换
  • 死锁风险:无死锁风险
  • 适用场景:简单变量操作,计数器等

代码示例:

cpp
#include <mutex>
#include <atomic>
#include <thread>

class Counter {
private:
    std::mutex mutex_;
    std::atomic<int> atomic_count_{0};
    int normal_count_{0};
    
public:
    // 互斥锁保护
    void increment_mutex() {
        std::lock_guard<std::mutex> lock(mutex_);
        ++normal_count_;
    }
    
    // 原子操作
    void increment_atomic() {
        ++atomic_count_;  // 线程安全,无需锁
    }
    
    int get_mutex() {
        std::lock_guard<std::mutex> lock(mutex_);
        return normal_count_;
    }
    
    int get_atomic() {
        return atomic_count_.load();  // 原子读取
    }
};

原子操作类型:

  • load/store:原子读写
  • fetch_add:原子加法返回旧值
  • compare_exchange:CAS操作
  • memory_order:内存序控制

选择原则:

  • 简单计数用原子:性能好,无死锁
  • 复杂操作用锁:保护多个变量
  • 性能敏感用原子:减少锁竞争
  • 数据结构用锁:原子操作难以扩展

最佳实践:

  • 优先原子操作:简单场景性能更好
  • 锁粒度最小化:减少锁持有时间
  • 读写锁优化:读多写少场景
  • 无锁数据结构:复杂并发场景

性能优化

13. 编译优化

❓ C++编译优化有哪些?O2和O3有什么区别?

面试官点了点头:"编译器优化你了解吗?为什么Release版本比Debug快那么多?"

💡 点击查看满分回答

编译器优化大幅提升程序性能,但可能影响调试。

优化级别:

  • O0:无优化,调试友好
  • O1:基本优化,减少代码大小
  • O2:全面优化,平衡速度和大小
  • O3:激进优化,最大化速度,可能增大代码

主要优化技术:

函数内联(Inlining):

cpp
// 内联函数
inline int square(int x) { return x * x; }

// 编译器可能内联展开
int result = square(5);  // 直接变成 result = 5 * 5;

循环优化:

  • 循环展开:减少循环开销
  • 循环不变代码外提:移出循环的计算
  • 强度削弱:用便宜操作替换昂贵操作

死代码消除:

cpp
int func(int x) {
    int unused = compute();  // 如果unused未使用,被消除
    if (false) {             // 常量条件,分支消除
        do_something();
    }
    return x * 2;
}

Debug vs Release差异:

  • 断言:Release中NDEBUG定义,assert失效
  • 调试信息:Release无调试符号
  • 内联:Release积极内联
  • 优化:Release启用各种优化

性能影响:

  • 速度提升:O2比O0快2-5倍,O3更快但不稳定
  • 代码大小:O3可能增大20-50%
  • 编译时间:O3编译慢2-3倍
  • 调试难度:优化后难以调试

最佳实践:

  • 开发用O0:便于调试
  • 发布用O2:平衡性能和大小
  • 性能测试用O3:追求极致性能
  • profile指导优化:基于运行数据优化

14. 性能分析和优化

❓ C++程序性能分析工具有哪些?怎么定位瓶颈?

面试官看了眼手表:"程序运行慢了,你会怎么分析?有什么工具?"

💡 点击查看满分回答

性能分析需要多层次工具和方法结合。

CPU分析:

  • gprof:GNU性能分析器,函数级分析
  • perf:Linux性能计数器,详细硬件事件
  • Valgrind Callgrind:缓存模拟,分支预测分析
  • Intel VTune:商业性能分析器

内存分析:

  • Valgrind Massif:堆内存分析
  • heaptrack:堆内存跟踪
  • tcmalloc:Google内存分配器,带分析
  • jemalloc:FreeBSD内存分配器

代码级优化:

热点函数识别:

cpp
// 使用perf定位热点
perf record ./program
perf report

// gprof使用
g++ -pg program.cpp
./program
gprof ./program

常见性能问题:

  • 缓存未命中:数据访问模式不友好
  • 分支预测失败:条件分支难以预测
  • 虚函数调用:间接调用开销
  • 内存分配:频繁new/delete

优化技巧:

数据结构优化:

cpp
// 缓存友好数据布局
struct CacheFriendly {
    int hot_data[64];  // 热数据连续
    int cold_data;     // 冷数据分离
};

// 预分配避免重复分配
std::vector<int> data;
data.reserve(1000);  // 预留空间

算法优化:

  • 时间复杂度:O(n²)→O(n log n)
  • 空间局部性:改善缓存命中率
  • 分支消除:用查表替代条件判断
  • SIMD指令:向量化计算

编译器优化:

  • LTO:链接时优化,全局优化
  • PGO:profile指导优化
  • 自动向量化:-ftree-vectorize
  • 函数克隆:基于调用上下文优化

最佳实践:

  • 性能基准:建立性能基线
  • 逐步优化:一次改一个因素
  • 测量验证:量化优化效果
  • 避免过度优化:保持代码可读性