高频面试题精讲
定义与作用
C++ 面试通常聚焦语言核心机制的深度理解。本章精讲六大高频考点:虚函数机制、智能指针、移动语义、STL 容器底层、const 体系和 static 用法。
核心原理
六大考点框架
完整示例
考点一:虚函数与 vtable
面试原题:"请解释虚函数表(vtable)的工作机制。"
#include <iostream>
#include <string>
class Logger {
public:
virtual ~Logger() = default;
virtual void log(const std::string& msg) {
std::cout << "[LOG] " << msg << "\n";
}
};
class FileLogger : public Logger {
public:
void log(const std::string& msg) override {
std::cout << "[FILE] " << msg << "\n";
}
};
class JsonLogger : public Logger {
public:
void log(const std::string& msg) override {
std::cout << "[JSON] " << msg << "\n";
}
};
int main() {
std::cout << "=== 虚函数分派 ===\n";
Logger* loggers[3] = {
new Logger(),
new FileLogger(),
new JsonLogger(),
};
// 动态分派:根据实际对象类型调用
for (auto* l : loggers) {
l->log("用户登录"); // 虚函数调用 = 指针解引用 → vtable → 函数地址
}
for (auto* l : loggers) delete l;
}
vtable 内存模型:
答题要点:
- 每个含虚函数的类有一个 vtable(虚函数表),存储函数指针
- 每个对象隐藏一个 vptr(虚表指针)指向其类的 vtable
- 虚函数调用 =
*(vptr + offset)(),即间接函数指针调用 - 构造函数中虚函数不表现多态(vptr 尚未设置)
- 虚函数额外开销:对象多 8 字节(64 位)vptr + 间接调用
考点二:智能指针选择与实现
面试原题:"什么时候用 unique_ptr,什么时候用 shared_ptr?"
#include <iostream>
#include <memory>
struct DatabaseConnection {
std::string name;
DatabaseConnection(std::string n) : name(std::move(n)) {
std::cout << " [连接] " << name << "\n";
}
~DatabaseConnection() {
std::cout << " [断开] " << name << "\n";
}
};
int main() {
std::cout << "=== 智能指针场景 ===\n\n";
// 场景一:独占所有权 → unique_ptr
{
auto conn = std::make_unique<DatabaseConnection>("DB-Master");
// conn 独占,离开作用域自动释放
}
std::cout << " (DB-Master 已释放)\n\n";
// 场景二:共享所有权 → shared_ptr
{
auto sharedConn = std::make_shared<DatabaseConnection>("DB-Pool");
{
auto copy1 = sharedConn; // 引用计数 +1
auto copy2 = sharedConn; // 引用计数 +1
std::cout << " 引用计数: " << sharedConn.use_count() << "\n";
// copy1, copy2 离开作用域,引用计数 -2
}
std::cout << " 引用计数: " << sharedConn.use_count() << "\n";
// 只有 sharedConn 还活着
}
std::cout << " (DB-Pool 已释放)\n\n";
// 场景三:避免循环引用 → weak_ptr
struct Node {
std::shared_ptr<Node> next;
std::weak_ptr<Node> prev; // 不增加引用计数
int id;
Node(int i) : id(i) {}
~Node() { std::cout << " 释放节点 " << id << "\n"; }
};
{
auto n1 = std::make_shared<Node>(1);
auto n2 = std::make_shared<Node>(2);
n1->next = n2;
n2->prev = n1; // weak_ptr 不增加计数
// 无循环引用,正常释放
}
std::cout << " 两个节点均正常释放\n";
}
答题要点:
| 智能指针 | 所有权 | 拷贝 | 使用场景 |
|---|---|---|---|
unique_ptr | 独占 | 不可拷贝,可移动 | 工厂函数、容器元素、PIMPL |
shared_ptr | 共享 | 引用计数 | 共享资源、图结构 |
weak_ptr | 观察 | 不增加计数 | 打破循环引用、缓存 |
考点三:移动语义
// "什么情况下会自动移动?"
// 1. 返回局部变量(NRVO/隐式移动)
std::vector<int> createVector() {
std::vector<int> v{1, 2, 3, 4, 5};
return v; // 编译器自动移动(或 NRVO 省略)
}
// 2. 返回 std::move 局部变量 → 禁用 NRVO
std::vector<int> createVectorBad() {
std::vector<int> v{1, 2, 3};
return std::move(v); // ❌ 阻止 NRVO,性能更差
}
答题要点:
- 用
std::move表示"这个值不再需要",而非真正移动 - 返回局部变量不要
std::move,会阻止 NRVO - 移动后源对象处于"有效但未指定"状态(通常可安全析构/赋值)
- 标记
noexcept的移动才被 vector 扩容等场景使用
考点四:STL 容器底层
面试原题:"vector 扩容机制是怎样的?"
答题要点:
- 动态数组,连续内存,末尾 O(1) 插入
- 扩容策略:通常是 1.5x (GCC) 或 2x (MSVC)
- 扩容 = 分配新内存 + 移动/拷贝元素 + 释放旧内存
- 若移动构造是
noexcept,扩容时 move;否则 copy shrink_to_fit是请求,不保证回收
// map 底层:红黑树
// - 插入/删除/查找一律 O(log n)
// - 元素始终有序(依据 key)
// - 迭代器是双向的
// - lower_bound / upper_bound 面试高频
考点五:const 体系
// const 的四种位置
const int x = 10; // 常量
const int* p1 = &x; // 指向常量的指针
int* const p2 = &y; // 常量指针
const int* const p3 = &x; // 指向常量的常量指针
// const 成员函数
class Counter {
int value_ = 0;
mutable int readCount_ = 0; // 可在 const 函数中修改
public:
int value() const {
readCount_++; // OK, mutable
return value_;
}
};
答题要点:
const修饰离它最近的左侧类型(无左侧则右侧)- const 成员函数不能修改非 mutable 成员,this 是
const T* mutable允许在 const 函数中修改特定成员- const 对象只能调用 const 成员函数
考点六:static 四种语义
| 位置 | 语义 | 示例 |
|---|---|---|
| 全局变量/函数 | 内部链接(本翻译单元可见) | static int globalId; |
| 局部变量 | 静态存储期(首次调用初始化,程序结束析构) | static int counter = 0; |
| 类静态成员 | 类级别共享 | static int instanceCount; |
| 类静态方法 | 无 this 指针,只能访问静态成员 | static int count(); |
易错场景
1. 虚函数不能是模板
class Base {
template<typename T>
virtual void f(T t); // ❌ 编译错误!虚函数不能是模板
};
2. shared_ptr 循环引用
struct A { std::shared_ptr<B> b; };
struct B { std::shared_ptr<A> a; };
auto a = std::make_shared<A>(), b = std::make_shared<B>();
a->b = b; b->a = a; // ❌ 循环引用,永不释放
// 解决:一方改用 weak_ptr
3. 虚析构函数缺失
class Base { ~Base() {} }; // ❌ 非虚析构
class Derived : public Base {};
Base* p = new Derived;
delete p; // 未定义行为:只调用 ~Base(),不调用 ~Derived()