C++ 面试题总结
C++ 基础
Q1:C++ 和 C 的区别?
主要区别:
面向对象编程
- C++ 支持面向对象编程(封装、继承、多态)
- C 是过程式编程语言
函数重载
- C++ 支持函数重载
- C 不支持函数重载
命名空间
- C++ 有命名空间(namespace)
- C 没有命名空间
引用
- C++ 支持引用(reference)
- C 只有指针
异常处理
- C++ 支持异常处理(try-catch)
- C 没有异常处理机制
内存管理
- C++ 有 new/delete 操作符
- C 使用 malloc/free
类型检查
- C++ 类型检查更严格
- C 类型检查相对宽松
Q2:C++ 中 struct 和 class 的区别?
主要区别:
默认访问权限
struct默认成员是publicclass默认成员是private
默认继承方式
struct默认继承是publicclass默认继承是private
其他方面
- 功能基本相同,都可以有构造函数、析构函数、成员函数等
struct在 C++ 中主要用于兼容 C,或者用于简单的数据聚合
1 | // struct 示例 |
Q3:C++ 中的四种类型转换?
C++ 提供了四种类型转换操作符:
- static_cast
- 用于基本类型转换、父子类指针转换
- 编译时检查,不保证运行时安全
1 | int i = 10; |
- dynamic_cast
- 用于多态类型之间的转换
- 运行时检查,转换失败返回 nullptr(指针)或抛出异常(引用)
- 只能用于有虚函数的类
1 | Base* base = new Derived(); |
- const_cast
- 用于移除 const 或 volatile 属性
- 主要用于函数重载场景
1 | const int* p = &i; |
- reinterpret_cast
- 用于低级别的类型转换
- 非常危险,不保证可移植性
- 主要用于指针和整数之间的转换
1 | int* p = new int(10); |
内存管理
Q4:new/delete 和 malloc/free 的区别?
主要区别:
类型安全
new/delete是操作符,类型安全malloc/free是函数,需要手动计算大小
构造函数/析构函数
new会调用构造函数delete会调用析构函数malloc/free不会调用构造函数/析构函数
返回值
new返回具体类型指针malloc返回void*,需要强制转换
失败处理
new失败抛出std::bad_alloc异常malloc失败返回NULL
重载
new/delete可以重载malloc/free不能重载
1 | // new/delete 示例 |
Q5:什么是内存泄漏?如何避免?
内存泄漏:
程序在运行过程中,动态分配的内存没有被正确释放,导致内存无法被再次使用。
常见原因:
- 使用
new分配内存后忘记delete - 异常导致
delete未执行 - 循环引用(智能指针)
避免方法:
- 使用智能指针
std::unique_ptr:独占所有权std::shared_ptr:共享所有权std::weak_ptr:弱引用,解决循环引用
1 | // 使用 unique_ptr |
- RAII(Resource Acquisition Is Initialization)
- 资源获取即初始化
- 在对象构造时获取资源,在析构时释放资源
1 | class FileHandle { |
- 使用容器
- 优先使用 STL 容器(vector、string 等),自动管理内存
Q6:堆和栈的区别?
主要区别:
分配方式
- 栈:由编译器自动分配和释放
- 堆:由程序员手动分配和释放
速度
- 栈:速度快,直接操作栈指针
- 堆:速度慢,需要查找合适的内存块
大小限制
- 栈:大小有限(通常几MB)
- 堆:大小受限于系统可用内存
内存碎片
- 栈:不会产生内存碎片
- 堆:可能产生内存碎片
生命周期
- 栈:局部变量,函数结束时自动释放
- 堆:需要手动释放,否则可能内存泄漏
1 | void example() { |
指针和引用
Q7:指针和引用的区别?
主要区别:
初始化
- 引用必须在定义时初始化,且不能改变指向
- 指针可以先定义后初始化,可以改变指向
空值
- 引用不能为空,必须指向有效对象
- 指针可以为空(nullptr)
操作
- 引用是别名,使用起来像普通变量
- 指针需要解引用(*)才能访问指向的对象
内存
- 引用不占用额外内存(编译器优化)
- 指针占用内存(存储地址)
多级
- 引用没有多级引用
- 指针可以有多级指针
1 | int a = 10; |
Q8:什么是野指针和悬空指针?
野指针(Wild Pointer):
未初始化的指针,指向未知内存地址。
1 | int* p; // 野指针,未初始化 |
悬空指针(Dangling Pointer):
指向已经被释放的内存地址的指针。
1 | int* p = new int(10); |
避免方法:
- 初始化指针为
nullptr - 释放后立即置空
- 使用智能指针
面向对象
Q9:C++ 中的多态是如何实现的?
多态分为两种:
编译时多态(静态多态)
- 函数重载
- 模板
运行时多态(动态多态)
- 虚函数
- 通过虚函数表(vtable)实现
虚函数表机制:
1 | class Base { |
虚函数表原理:
- 每个有虚函数的类都有一个虚函数表
- 对象中包含一个指向虚函数表的指针(vptr)
- 调用虚函数时,通过 vptr 找到虚函数表,再调用对应的函数
Q10:虚函数和纯虚函数的区别?
虚函数(Virtual Function):
- 有函数体,可以被子类重写
- 子类可以不重写
1 | class Base { |
纯虚函数(Pure Virtual Function):
- 没有函数体,用
= 0表示 - 包含纯虚函数的类是抽象类,不能实例化
- 子类必须实现纯虚函数
1 | class Base { |
Q11:构造函数和析构函数可以是虚函数吗?
构造函数:
- 不能是虚函数
- 原因:对象在构造时,虚函数表还未建立,无法通过虚函数表调用
析构函数:
- 应该是虚函数(如果类会被继承)
- 原因:确保通过基类指针删除派生类对象时,能正确调用派生类的析构函数
1 | class Base { |
STL 容器
Q12:vector 的底层实现原理?
vector 底层实现:
- 动态数组,使用连续内存存储
- 当容量不足时,会重新分配更大的内存(通常是原来的 2 倍)
- 将旧数据拷贝到新内存,释放旧内存
关键点:
容量(capacity)和大小(size)
size():当前元素个数capacity():当前容量
扩容机制
- 扩容时,所有迭代器、指针、引用都会失效
- 扩容代价高,需要拷贝所有元素
1 | vector<int> v; |
- 时间复杂度
- 随机访问:O(1)
- 尾部插入:平均 O(1),最坏 O(n)(需要扩容)
- 中间插入:O(n)
Q13:map 和 unordered_map 的区别?
主要区别:
底层实现
map:红黑树(平衡二叉搜索树)unordered_map:哈希表
有序性
map:有序(按 key 排序)unordered_map:无序
时间复杂度
map:查找、插入、删除 O(log n)unordered_map:平均 O(1),最坏 O(n)
内存占用
map:相对较少unordered_map:相对较多(需要维护哈希表)
key 要求
map:key 需要支持<操作符unordered_map:key 需要支持哈希函数和==操作符
1 | // map:有序 |
Q14:迭代器失效的情况?
vector:
- 插入元素:可能导致所有迭代器失效(扩容时)
- 删除元素:被删除元素之后的迭代器失效
1 | vector<int> v = {1, 2, 3, 4, 5}; |
list/deque:
- 插入元素:不会导致迭代器失效
- 删除元素:只有被删除元素的迭代器失效
map/set:
- 插入元素:不会导致迭代器失效
- 删除元素:只有被删除元素的迭代器失效
1 | map<int, int> m = {{1, 1}, {2, 2}, {3, 3}}; |
模板和泛型编程
Q15:函数模板和类模板的区别?
函数模板:
- 用于生成函数
- 编译器根据调用时传入的参数类型自动推导
1 | template<typename T> |
类模板:
- 用于生成类
- 使用时需要显式指定类型参数
1 | template<typename T> |
Q16:typename 和 class 在模板中的区别?
在模板参数中:
typename和class可以互换使用typename更语义化,表示类型
1 | template<typename T> // 或 template<class T> |
在模板内部:
typename用于告诉编译器后面的标识符是类型class不能用于此场景
1 | template<typename T> |
多线程和并发
Q17:C++11 中的多线程支持?
主要特性:
- std::thread
- 创建和管理线程
1 |
|
- std::mutex
- 互斥锁,保护共享资源
1 |
|
- std::condition_variable
- 条件变量,用于线程间通信
1 |
|
- std::atomic
- 原子操作,无需加锁
1 |
|
Q18:什么是死锁?如何避免?
死锁:
多个线程互相等待对方释放锁,导致所有线程都无法继续执行。
死锁产生的四个必要条件:
- 互斥条件:资源不能被多个线程同时使用
- 请求与保持:线程持有资源的同时请求其他资源
- 不剥夺条件:资源不能被强制释放
- 循环等待:存在循环等待链
避免方法:
- 按顺序加锁
- 所有线程按相同顺序获取锁
1 | std::mutex mtx1, mtx2; |
- 使用超时锁
- 使用
try_lock_for或try_lock_until
- 使用
1 | std::timed_mutex mtx; |
- 避免嵌套锁
- 尽量减少锁的嵌套使用
常见算法题
Q19:实现一个单例模式
懒汉式(线程安全):
1 | class Singleton { |
C++11 线程安全的懒汉式(推荐):
1 | class Singleton { |
Q20:实现一个智能指针
简化版 unique_ptr:
1 | template<typename T> |
其他重要知识点
Q21:const 关键字的作用?
const 的多种用法:
- const 变量
- 常量,不能修改
1 | const int a = 10; |
- const 指针
const int* p:指向常量的指针(不能通过 p 修改指向的值)int* const p:常量指针(不能改变指向)const int* const p:指向常量的常量指针
1 | int a = 10, b = 20; |
- const 成员函数
- 不能修改成员变量(mutable 除外)
1 | class MyClass { |
- const 引用
- 不能通过引用修改对象
1 | const int& ref = a; |
Q22:static 关键字的作用?
static 的多种用法:
- static 局部变量
- 生命周期延长到程序结束
- 只初始化一次
1 | void func() { |
- static 成员变量
- 属于类,不属于对象
- 所有对象共享
1 | class MyClass { |
- static 成员函数
- 属于类,不属于对象
- 不能访问非静态成员
1 | class MyClass { |
- static 全局变量/函数
- 限制作用域为当前文件
Q23:volatile 关键字的作用?
volatile 的作用:
- 告诉编译器不要优化该变量
- 每次访问都从内存读取,不缓存到寄存器
- 主要用于多线程、中断处理、硬件寄存器访问
1 | volatile int flag = 0; |
Q24:inline 关键字的作用?
inline 的作用:
- 建议编译器将函数内联展开
- 减少函数调用开销
- 编译器可能忽略 inline 建议
使用场景:
- 短小的函数
- 频繁调用的函数
1 | inline int add(int a, int b) { |
注意:
- 定义在类内的成员函数默认是 inline
- 模板函数通常也是 inline
Q25:C++11 的新特性?
主要新特性:
- auto 和 decltype
- 自动类型推导
1 | auto i = 10; // int |
- 范围 for 循环
1 | vector<int> v = {1, 2, 3, 4, 5}; |
- lambda 表达式
1 | auto func = [](int a, int b) { return a + b; }; |
智能指针
unique_ptr、shared_ptr、weak_ptr
右值引用和移动语义
1 | class MyClass { |
- nullptr
- 替代 NULL
1 | int* p = nullptr; // 而不是 NULL |
- 统一初始化
1 | vector<int> v{1, 2, 3, 4, 5}; |
总结
C++ 是一门功能强大但复杂的语言,掌握这些基础知识对于面试和实际开发都非常重要。重点需要理解:
- 内存管理:理解堆栈、指针、引用、智能指针
- 面向对象:封装、继承、多态、虚函数
- STL:常用容器的底层实现和特性
- 模板:函数模板和类模板的使用
- 多线程:线程安全、锁、死锁避免
- C++11 新特性:现代 C++ 的编程方式
在实际开发中,应该优先使用现代 C++ 特性(智能指针、RAII、STL 容器等),避免手动管理内存,提高代码的安全性和可维护性。