C++ 面试核心知识点
采用问题驱动方式,从"为什么"到"怎么做",深入理解 C++ 核心机制
一、模板(Templates)
1.1 问题背景:我们遇到了什么问题?
场景:需要实现一个通用的 max 函数
// 方案1:为每种类型写重载函数
int max(int a, int b) { return a > b ? a : b; }
double max(double a, double b) { return a > b ? a : b; }
float max(float a, float b) { return a > b ? a : b; }
// ... 还有 char, short, long, unsigned ...
// 代码重复!维护困难!
问题分析:
┌─────────────────────────────────────────────────────────────┐
│ 核心矛盾 │
├─────────────────────────────────────────────────────────────┤
│ │
│ 需求:一段逻辑,适用于多种类型 │
│ │
│ C 语言方案:宏定义 │
│ #define MAX(a, b) ((a) > (b) ? (a) : (b)) │
│ 问题:无类型检查,副作用(a++ 会被执行两次) │
│ │
│ 早期 C++ 方案:函数重载 │
│ 问题:代码重复,类型无限扩展 │
│ │
│ 需要的:类型安全的"代码生成器" │
│ │
└─────────────────────────────────────────────────────────────┘
更多场景:
// 场景2:通用容器
// vector<int> 和 vector<double> 逻辑完全一样,只是存储类型不同
// 难道要为每种类型写一份 vector 代码?
// 场景3:通用算法
// sort 对 int 数组和 string 数组的逻辑一样,只是比较方式不同
// 如何做到一份代码,适用于所有类型?
1.2 设计思路:如何解决"一份代码,多种类型"?
思路1:宏(C 语言方案)
#define MAX(a, b) ((a) > (b) ? (a) : (b))
int x = 5, y = 3;
int m = MAX(x++, y++); // 展开:((x++) > (y++) ? (x++) : (y++))
// x 和 y 被自增了两次!
评价:文本替换,无类型检查,副作用问题。
思路2:void 泛型(C 语言方案)*
void* max(void* a, void* b, int (*cmp)(void*, void*)) {
return cmp(a, b) > 0 ? a : b;
}
// 使用麻烦,类型不安全,需要强制转换
评价:失去类型信息,使用繁琐,容易出错。
思路3:代码生成(C++ 模板方案)
┌─────────────────────────────────────────────────────────────┐
│ 模板的核心思想 │
├─────────────────────────────────────────────────────────────┤
│ │
│ 不是写死代码,而是写"代码的蓝图" │
│ │
│ 编译器根据实际使用的类型,自动生成对应的代码 │
│ │
│ 模板本身不是代码,是编译器生成代码的指令 │
│ │
│ ┌──────────────┐ 编译期 ┌──────────────┐ │
│ │ 模板(蓝图) │ ─────────────→ │ 具体类型代码 │ │
│ └──────────────┘ └──────────────┘ │
│ │
│ max<T>(a, b) max<int>(int, int) │
│ max<double>(double, ...) │
│ │
└─────────────────────────────────────────────────────────────┘
1.3 具体实现:模板的工作原理
函数模板:
// 模板定义:T 是类型参数
template<typename T>
T max(T a, T b) {
return a > b ? a : b;
}
// 编译器推导和实例化过程:
max(3, 5); // T 推导为 int,生成 max<int>
max(3.14, 2.71); // T 推导为 double,生成 max<double>
max('a', 'b'); // T 推导为 char,生成 max<char>
// 显式指定:
max<double>(3, 5); // 强制 T 为 double,3 和 5 被转换为 double
编译器实际生成的代码:
// 编译器为 max(3, 5) 生成:
int max<int>(int a, int b) {
return a > b ? a : b;
}
// 编译器为 max(3.14, 2.71) 生成:
double max<double>(double a, double b) {
return a > b ? a : b;
}
类模板:
template<typename T, size_t N>
class Array {
T data[N]; // N 是编译期常量
public:
T& operator[](size_t i) { return data[i]; }
size_t size() const { return N; }
};
// 使用
Array<int, 100> arr1; // 编译器生成 Array<int, 100>
Array<double, 50> arr2; // 编译器生成 Array<double, 50>
// 两个完全不同的类型!
// sizeof(arr1) = 400 字节
// sizeof(arr2) = 400 字节(50 * 8)
模板实例化的底层机制:
编译过程:
1. 模板定义阶段
template<typename T>
T max(T a, T b) { ... }
↓
不进行代码生成,只是语法检查
2. 模板使用阶段
max(3, 5);
↓
编译器推导 T = int
↓
生成具体函数代码
↓
int max(int a, int b) { ... }
↓
编译为机器码
3. 链接阶段
多个翻译单元使用相同模板实例 → 链接器去重
1.4 模板特化:处理特殊类型
问题:通用模板对某些类型不适用
template<typename T>
T max(T a, T b) {
return a > b ? a : b; // 对于指针类型,比较的是地址!
}
const char* s1 = "hello";
const char* s2 = "world";
max(s1, s2); // 比较的是指针地址,不是字符串内容!
全特化:为特定类型提供完全不同的实现
// 通用版本
template<typename T>
class Container {
public:
void print() { cout << "通用版本\n"; }
};
// 全特化:针对 const char* 的特殊处理
template<>
class Container<const char*> {
const char* data;
public:
void print() { cout << "字符串: " << data << "\n"; }
size_t length() const { return strlen(data); }
};
Container<int> c1; // 使用通用版本
Container<const char*> c2; // 使用特化版本
偏特化:部分参数特化
template<typename T, typename U>
class Pair {
public:
void print() { cout << "通用 Pair\n"; }
};
// 偏特化1:第二个参数是 int
template<typename T>
class Pair<T, int> {
public:
void print() { cout << "Pair<T, int>\n"; }
};
// 偏特化2:两个参数相同
template<typename T>
class Pair<T, T> {
public:
void print() { cout << "Pair<T, T>\n"; }
};
// 匹配优先级:
Pair<double, string> p1; // 通用版本
Pair<double, int> p2; // 偏特化1
Pair<int, int> p3; // 偏特化2(比偏特化1更特化)
1.5 SFINAE:编译期条件编程
问题:如何根据类型特性选择不同实现?
// 需求:只对整数类型提供 check 函数
template<typename T>
void check(T t) {
// 如何限制 T 只能是整数类型?
}
check(10); // OK
check(3.14); // 应该编译错误
设计思路:让不匹配的模板在替换时"安静地失败"
// enable_if 实现原理
template<bool B, typename T = void>
struct enable_if {};
template<typename T>
struct enable_if<true, T> { using type = T; };
// 使用
#include <type_traits>
template<typename T>
typename std::enable_if<std::is_integral<T>::value, T>::type
check(T t) {
return t;
}
template<typename T>
typename std::enable_if<std::is_floating_point<T>::value, T>::type
check(T t) {
return t * 2;
}
check(10); // 匹配第一个,返回 10
check(3.14); // 匹配第二个,返回 6.28
// check("hello"); // 编译错误,没有匹配的版本
SFINAE 原理图解:
尝试实例化 check("hello"):
候选1: enable_if<is_integral<const char*>::value, const char*>::type
is_integral<const char*>::value = false
enable_if<false, ...> 没有 type 成员
→ 替换失败,但不是错误(SFINAE)
→ 这个候选被排除
候选2: enable_if<is_floating_point<const char*>::value, const char*>::type
is_floating_point<const char*>::value = false
→ 替换失败,排除
没有可用候选 → 编译错误
1.6 变参模板:处理任意数量参数
问题:如何实现接受任意数量参数的函数?
// printf 的实现:接受任意数量和类型的参数
printf("%d %s %f", 10, "hello", 3.14);
// C 语言用 va_list,类型不安全
// C++ 如何用模板实现类型安全的版本?
递归展开实现:
// 递归终止:只有一个参数
template<typename T>
void print(T t) {
cout << t << endl;
}
// 变参模板:head + tail
template<typename T, typename... Args>
void print(T t, Args... args) {
cout << t << " ";
print(args...); // 递归调用,args 数量逐渐减少
}
print(1, 2.5, "hello", 'a');
// 展开过程:
// print(1, 2.5, "hello", 'a') → cout << 1, print(2.5, "hello", 'a')
// print(2.5, "hello", 'a') → cout << 2.5, print("hello", 'a')
// print("hello", 'a') → cout << "hello", print('a')
// print('a') → cout << 'a' << endl
C++17 折叠表达式:
template<typename... Args>
void print(Args... args) {
(cout << ... << args) << endl; // 一元右折叠
}
// 展开为:(((cout << arg1) << arg2) << arg3) << arg4
1.7 类型萃取:编译期类型计算
问题:如何在编译期获取类型信息?
// 需求:编写一个算法,对指针类型和非指针类型做不同处理
template<typename T>
void process(T t) {
// 如果 T 是指针,解引用
// 如果 T 不是指针,直接使用
// 如何实现?
}
实现原理:模板特化 + 继承
// 通用版本:不是指针
template<typename T>
struct is_pointer : std::false_type {};
// 特化版本:是指针
template<typename T>
struct is_pointer<T*> : std::true_type {};
// 使用
is_pointer<int>::value; // false
is_pointer<int*>::value; // true
is_pointer<int**>::value; // true
// 配合 if constexpr(C++17)
template<typename T>
void process(T t) {
if constexpr (is_pointer<T>::value) {
cout << "指针值: " << *t << endl;
} else {
cout << "值: " << t << endl;
}
}
1.8 常见陷阱与面试题
陷阱1:模板代码必须放在头文件中
// 错误做法:模板定义在 .cpp 中
// MyTemplate.h
template<typename T>
void foo(T t);
// MyTemplate.cpp
#include "MyTemplate.h"
template<typename T>
void foo(T t) { /* ... */ }
// main.cpp
#include "MyTemplate.h"
foo(10); // 链接错误!找不到 foo<int> 的定义
// 原因:模板实例化在编译期,编译 main.cpp 时看不到定义
// 正确做法1:定义放在头文件中
// 正确做法2:显式实例化(不推荐)
template void foo<int>(int); // 在 .cpp 中显式实例化
陷阱2:typename 关键字
template<typename T>
void foo() {
T::iterator it; // 编译错误!
// 原因:编译器不知道 T::iterator 是类型还是静态成员
// 需要显式告诉编译器这是类型
typename T::iterator it; // OK
}
陷阱3:两阶段查找
template<typename T>
void foo(T x) {
bar(x); // 依赖名,第二阶段查找
}
void bar(int x) { /* ... */ }
foo(10); // 可能找不到 bar!
// 解决:
template<typename T>
void foo(T x) {
using std::bar; // 或显式指定
bar(x);
}
面试题1:模板特化和函数重载的区别?
// 函数重载:基于参数类型选择
template<typename T> void foo(T t); // 通用版本
template<> void foo<int>(int t); // 特化版本
void foo(int t); // 普通函数
foo(10); // 调用普通函数(优先级最高)
foo<>(10); // 强制使用模板,调用特化版本
面试题2:如何实现编译期阶乘?
// C++11 递归实现
template<int N>
struct Factorial {
static constexpr int value = N * Factorial<N-1>::value;
};
template<>
struct Factorial<0> {
static constexpr int value = 1;
};
// C++14 函数版本
constexpr int factorial(int n) {
int result = 1;
for (int i = 1; i <= n; ++i) {
result *= i;
}
return result;
}
static_assert(Factorial<5>::value == 120, "");
static_assert(factorial(5) == 120, "");
面试题3:如何实现 is_same?
template<typename T, typename U>
struct is_same : std::false_type {};
template<typename T>
struct is_same<T, T> : std::true_type {};
is_same<int, int>::value; // true
is_same<int, double>::value; // false
二、右值引用与移动语义
2.1 问题背景:拷贝的代价有多大?
场景:函数返回大对象
vector<int> createVector() {
vector<int> v(1000000); // 100万个元素
for (int i = 0; i < 1000000; i++) {
v[i] = i;
}
return v; // 返回时发生什么?
}
vector<int> v = createVector(); // 拷贝?还是?
传统方案的问题:
// C++98/03:必须拷贝
vector<int> v = createVector();
// 1. createVector 中创建 v(分配 4MB 内存)
// 2. 返回时拷贝构造(再分配 4MB,复制 100 万个 int)
// 3. 销毁局部 v
// 4MB 内存复制!性能灾难!
// 优化方案1:传引用参数(丑陋)
void createVector(vector<int>& out);
// 优化方案2:使用指针(手动管理内存)
vector<int>* createVector();
// 需要手动 delete,容易内存泄漏
更多场景:
// 场景2:插入大对象
vector<string> v;
string s = "一个非常长的字符串...";
v.push_back(s); // 拷贝还是移动?
// 场景3:资源管理类
class FileHandle {
FILE* fp;
public:
FileHandle(const char* name) { fp = fopen(name, "r"); }
~FileHandle() { if (fp) fclose(fp); }
// 拷贝?文件句柄不能拷贝!
};
2.2 设计思路:区分"需要保留"和"即将销毁"
核心洞察:
┌─────────────────────────────────────────────────────────────┐
│ 值的分类 │
├─────────────────────────────────────────────────────────────┤
│ │
│ 左值(Lvalue):有名字,有持久地址 │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ • 变量名:int x = 10; │ │
│ │ • 解引用:*ptr │ │
│ │ • 数组元素:arr[0] │ │
│ │ • 返回左值引用的函数:string::operator[] │ │
│ │ │ │
│ │ 特征:可以取地址,可以被赋值 │ │
│ └─────────────────────────────────────────────────────┘ │
│ │
│ 右值(Rvalue):临时对象,即将销毁 │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ • 字面量:10, 3.14, "hello" │ │
│ │ • 临时对象:x + y, string("hi") │ │
│ │ • 返回值的函数:createVector() │ │
│ │ • 即将销毁的变量(move 后) │ │
│ │ │ │
│ │ 特征:不能取地址,生命周期即将结束 │ │
│ │ 关键洞察:临时对象的资源可以被"偷走"! │ │
│ └─────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────┘
设计目标:
对于即将销毁的临时对象:
不要深拷贝它的资源(复制 4MB 数据)
直接转移它的资源(交换指针,O(1))
┌─────────────────────────────────────────────────────────────┐
│ 拷贝语义:复制资源(保守,安全) │
│ 移动语义:转移资源(激进,高效) │
│ │
│ 左值 → 拷贝(保留原对象) │
│ 右值 → 移动(原对象即将销毁) │
└─────────────────────────────────────────────────────────────┘
2.3 具体实现:右值引用与移动操作
右值引用类型:
int x = 10;
int& lref = x; // 左值引用,绑定到左值
// int& lref2 = 10; // 错误,不能绑定到右值
int&& rref = 10; // 右值引用,绑定到右值
// int&& rref2 = x; // 错误,不能绑定到左值
int&& rref3 = std::move(x); // OK,move 将左值转为右值
移动构造函数:
class String {
char* data;
size_t len;
public:
// 拷贝构造函数:深拷贝
String(const String& other) {
len = other.len;
data = new char[len + 1];
strcpy(data, other.data); // 复制数据
cout << "拷贝构造\n";
}
// 移动构造函数:转移资源
String(String&& other) noexcept {
data = other.data; // 偷走指针
len = other.len;
other.data = nullptr; // 置空源对象
other.len = 0;
cout << "移动构造\n";
}
~String() {
delete[] data;
}
};
// 使用
String s1 = "Hello World"; // 构造
String s2 = s1; // 拷贝构造(s1 是左值,需要保留)
String s3 = std::move(s1); // 移动构造(s1 变成右值,资源被转移)
// s1 现在处于"有效但未指定状态",不要再使用
移动过程图解:
移动前:
s1: [ data ──→ "Hello World" ]
[ len = 11 ]
s3: [ data ──→ ? ] // 未初始化
[ len = ? ]
移动后:
s1: [ data ──→ nullptr ] // 置空
[ len = 0 ]
s3: [ data ──→ "Hello World" ] // 接管资源
[ len = 11 ]
没有内存分配,没有数据复制!
std::move 的本质:
// move 的实现(简化)
template<typename T>
constexpr typename std::remove_reference<T>::type&& move(T&& t) noexcept {
return static_cast<typename std::remove_reference<T>::type&&>(t);
}
// 作用:将左值强制转换为右值引用
// 注意:move 只是类型转换,不移动任何东西!
String s1 = "hello";
String s2 = std::move(s1); // move 后 s1 变成右值
// 然后调用移动构造函数
2.4 完美转发:保持值类别
问题:模板函数如何保持参数的值类别?
template<typename T>
void wrapper(T t) {
process(t); // 无论传入左值还是右值,t 都是左值!
}
int x = 10;
wrapper(x); // 希望 process(x) - 左值
wrapper(20); // 希望 process(20) - 右值,但 t 变成左值
万能引用(Universal Reference):
template<typename T>
void wrapper(T&& t) { // 万能引用
// T 推导为 int& 时,T&& = int& && = int&(引用折叠)
// T 推导为 int 时,T&& = int&&
process(std::forward<T>(t)); // 完美转发
}
int x = 10;
wrapper(x); // T = int&, t = int&, forward 后保持左值
wrapper(20); // T = int, t = int&&, forward 后保持右值
引用折叠规则:
// 只有 && && = &&,其他都是 &
& + & = &
& + && = &
&& + & = &
&& + && = &&
// 应用
template<typename T>
void foo(T&& param);
int x = 10;
foo(x); // T = int&, param = int& && → int&
foo(10); // T = int, param = int&&
std::forward 实现:
template<typename T>
T&& forward(typename std::remove_reference<T>::type& t) noexcept {
return static_cast<T&&>(t);
}
// 如果 T = int&,返回 int& && = int&(保持左值)
// 如果 T = int,返回 int&&(保持右值)
2.5 编译器优化:返回值优化(RVO)
vector<int> create() {
vector<int> v(1000000);
return v; // 理论上需要移动或拷贝
}
vector<int> v = create(); // 实际上?
RVO(Return Value Optimization):
C++17 前:编译器可以选择 RVO,但不是必须的
C++17 起:强制 RVO( Guaranteed Copy Elision )
实际执行:
1. 在 v 的内存位置直接构造 vector
2. create 内部的 v 就是这个对象
3. 零拷贝,零移动!
编译器将:
vector<int> v = create();
优化为:
vector<int> v; // 在调用者栈帧分配
create(&v); // 传递指针,直接构造
2.6 常见陷阱与面试题
陷阱1:move 后继续使用原对象
vector<int> v1 = {1, 2, 3, 4, 5};
vector<int> v2 = std::move(v1);
// v1 现在处于"有效但未指定状态"
v1.size(); // 可能是 0
v1[0]; // 未定义行为!不要访问
v1.push_back(6); // 可能可以,但不推荐
// 正确做法:move 后不再使用 v1,或重新赋值
v1 = {7, 8, 9}; // 重新赋值后可以正常使用
陷阱2:返回局部变量的引用
// 危险!
String&& create() {
String s = "hello";
return std::move(s); // 返回悬垂引用!
} // s 在这里销毁
// 正确做法:返回值
String create() {
String s = "hello";
return s; // NRVO/RVO 优化,不需要 move
}
陷阱3:const 右值引用
// 无意义!
void foo(const String&& s);
// const 右值不能移动,只能拷贝
// 通常不需要这种签名
面试题1:下列代码输出什么?
class A {
public:
A() { cout << "构造 "; }
A(const A&) { cout << "拷贝 "; }
A(A&&) { cout << "移动 "; }
};
A create() { return A(); }
A a = create();
// C++17 前(无优化):构造 → 移动(或拷贝)
// C++17 起(强制 RVO):构造(只有一次)
面试题2:实现一个简易的 unique_ptr
template<typename T>
class unique_ptr {
T* ptr;
public:
explicit unique_ptr(T* p = nullptr) : ptr(p) {}
// 禁止拷贝
unique_ptr(const unique_ptr&) = delete;
unique_ptr& operator=(const unique_ptr&) = delete;
// 允许移动
unique_ptr(unique_ptr&& other) noexcept : ptr(other.ptr) {
other.ptr = nullptr;
}
unique_ptr& operator=(unique_ptr&& other) noexcept {
if (this != &other) {
delete ptr;
ptr = other.ptr;
other.ptr = nullptr;
}
return *this;
}
~unique_ptr() { delete ptr; }
T* get() const { return ptr; }
T& operator*() const { return *ptr; }
T* operator->() const { return ptr; }
};
面试题3:完美转发的常见错误
template<typename T>
void bad(T&& t) {
process(t); // 错误!t 是左值
process(std::move(t)); // 错误!总是移动
process(std::forward<T>(t)); // 正确!保持值类别
}
三、STL 标准库
3.1 问题背景:为什么需要标准库?
场景:实现一个动态数组
// C 语言方案:手动管理
int* arr = (int*)malloc(10 * sizeof(int));
// 需要手动扩容、缩容、插入、删除...
// 容易内存泄漏,容易越界
// C++ 早期:自己实现容器类
class MyArray {
int* data;
int size;
int capacity;
public:
MyArray() : data(nullptr), size(0), capacity(0) {}
~MyArray() { delete[] data; }
// 拷贝构造、移动构造、赋值运算符...
// push_back、pop_back、insert、erase...
// 迭代器支持...
};
// 重复造轮子,质量参差不齐
核心问题:
┌─────────────────────────────────────────────────────────────┐
│ STL 要解决的问题 │
├─────────────────────────────────────────────────────────────┤
│ │
│ 1. 代码复用 │
│ 不要每个人都自己写链表、写动态数组 │
│ │
│ 2. 算法与数据结构分离 │
│ sort 不应该只适用于数组,应该适用于任何序列 │
│ │
│ 3. 类型安全 │
│ 替代 void* 泛型,编译期类型检查 │
│ │
│ 4. 性能 │
│ 零开销抽象,不用的功能不付出代价 │
│ │
└─────────────────────────────────────────────────────────────┘
3.2 设计思路:六大组件与迭代器模式
STL 架构:
┌─────────────────────────────────────────────────────────────┐
│ STL 六大组件 │
├─────────────────────────────────────────────────────────────┤
│ │
│ 容器(Containers):存储数据 │
│ ├─ 序列:vector, deque, list, forward_list, array │
│ ├─ 关联:set, map, multiset, multimap │
│ └─ 无序:unordered_set, unordered_map... │
│ │
│ 算法(Algorithms):操作数据 │
│ ├─ 非修改:find, count, for_each... │
│ ├─ 修改:copy, transform, replace... │
│ ├─ 排序:sort, stable_sort, binary_search... │
│ └─ 数值:accumulate, inner_product... │
│ │
│ 迭代器(Iterators):容器与算法的桥梁 │
│ ├─ 输入、输出、前向、双向、随机访问 │
│ │
│ 仿函数(Functors):自定义操作 │
│ ├─ less, greater, plus... │
│ │
│ 适配器(Adapters):接口转换 │
│ ├─ 容器适配器:stack, queue, priority_queue │
│ ├─ 迭代器适配器:reverse_iterator, insert_iterator │
│ └─ 函数适配器:bind, not1, mem_fn... │
│ │
│ 空间配置器(Allocator):内存管理 │
│ └─ 默认 allocator,可自定义 │
│ │
└─────────────────────────────────────────────────────────────┘
迭代器模式的核心思想:
算法不直接操作容器,而是通过迭代器操作元素
┌─────────────┐ 迭代器 ┌─────────────┐
│ vector │ ←──────────────→ │ sort │
└─────────────┘ └─────────────┘
┌─────────────┐ 迭代器 │ │
│ list │ ←──────────────→ │ 同一份 │
└─────────────┘ │ 代码 │
┌─────────────┐ 迭代器 │ │
│ array │ ←──────────────→ │ 适用于 │
└─────────────┘ │ 所有容器 │
└─────────────┘
算法通过迭代器访问元素,不关心容器具体类型
容器提供迭代器,不暴露内部实现
3.3 具体实现:vector 深度剖析
vector 的内存布局:
template<typename T, typename Allocator = std::allocator<T>>
class vector {
T* start; // 数据起始
T* finish; // 数据结束(size)
T* end_of_storage; // 容量结束(capacity)
Allocator alloc; // 空间配置器
};
// 内存布局:
// [ start finish end_of_storage ]
// ↓ ↓ ↓
// [ elem0, elem1, elem2, ..., ?, ?, ?, ? ]
// <-------- size -------->
// <------------ capacity ------------>
扩容机制:
void push_back(const T& x) {
if (finish != end_of_storage) {
// 还有空间,直接构造
construct(finish, x);
++finish;
} else {
// 需要扩容
insert_aux(end(), x);
}
}
void insert_aux(iterator position, const T& x) {
// 1. 计算新容量(通常 1.5 或 2 倍)
size_type old_size = size();
size_type new_size = old_size == 0 ? 1 : old_size * 2;
// 2. 分配新内存
iterator new_start = alloc.allocate(new_size);
iterator new_finish = new_start;
// 3. 拷贝/移动旧元素到新内存
new_finish = uninitialized_copy(start, position, new_start);
construct(new_finish, x);
++new_finish;
new_finish = uninitialized_copy(position, finish, new_finish);
// 4. 销毁并释放旧内存
destroy(start, finish);
deallocate(start, end_of_storage - start);
// 5. 更新指针
start = new_start;
finish = new_finish;
end_of_storage = new_start + new_size;
}
为什么是 1.5 或 2 倍扩容?
┌─────────────────────────────────────────────────────────────┐
│ 扩容因子的权衡 │
├─────────────────────────────────────────────────────────────┤
│ │
│ 扩容因子 = 2(MSVC): │
│ 优点:均摊 O(1),实现简单 │
│ 缺点:内存利用率 50%,可能存在内存碎片 │
│ │
│ 扩容因子 = 1.5(GCC): │
│ 优点:内存利用率更高,可以复用之前释放的内存 │
│ 缺点:均摊复杂度略高,但仍是 O(1) │
│ │
│ 数学证明:均摊 O(1) 需要扩容因子 > 1 │
│ │
└─────────────────────────────────────────────────────────────┘
迭代器失效:
vector<int> v = {1, 2, 3, 4, 5};
auto it = v.begin() + 2; // 指向 3
auto it2 = v.begin() + 3; // 指向 4
v.push_back(6); // 可能导致扩容
// it 和 it2 现在失效!
// 原因:扩容后内存地址改变,原迭代器指向已释放的内存
// 安全做法:
auto it = v.begin() + 2;
it = v.insert(it, 10); // insert 返回新的有效迭代器
// 失效规则总结:
// - push_back/emplace_back:如果扩容,全部失效
// - insert:插入点之后失效
// - erase:被删除元素及之后失效
// - resize/reserve:可能全部失效
// - clear:全部失效
// - pop_back:end() 失效
3.4 map vs unordered_map 的实现差异
map(红黑树):
// 节点定义
template<typename Key, typename T>
struct __tree_node {
__tree_node* parent;
__tree_node* left;
__tree_node* right;
// 颜色信息(通常用指针的低位存储)
pair<const Key, T> value;
};
// 查找:O(log n)
// 插入:O(log n),需要旋转维护平衡
// 删除:O(log n),需要旋转维护平衡
// 遍历:中序遍历,有序
unordered_map(哈希表):
// 桶数组 + 链表/红黑树(C++11 起)
template<typename Key, typename T>
class unordered_map {
vector<__hash_node*> buckets; // 桶数组
size_t bucket_count;
size_t element_count;
float max_load_factor; // 默认 1.0
hasher hash; // 哈希函数
key_equal equal; // 比较函数
};
// 查找:O(1) 平均,O(n) 最坏
// 插入:O(1) 平均
// 删除:O(1) 平均
// 遍历:无序
// 负载因子 = element_count / bucket_count
// 超过 max_load_factor 时 rehash(重新分配桶数组)
选择指南:
| 场景 | 选择 | 原因 |
|---|---|---|
| 需要有序遍历 | map | 天然有序 |
| 只需要查找 | unordered_map | O(1) 更快 |
| 自定义类型无哈希 | map | 只需 operator< |
| 对内存敏感 | map | 哈希表有额外开销 |
| 需要范围查询 | map | lower_bound/upper_bound |
| 频繁插入删除 | unordered_map | 平均更快 |
3.5 迭代器类别与算法约束
// 迭代器标签(用于函数重载)
struct input_iterator_tag {};
struct output_iterator_tag {};
struct forward_iterator_tag : public input_iterator_tag {};
struct bidirectional_iterator_tag : public forward_iterator_tag {};
struct random_access_iterator_tag : public bidirectional_iterator_tag {};
// 算法根据迭代器类别选择最优实现
template<typename Iterator>
void advance_impl(Iterator& it, int n, random_access_iterator_tag) {
it += n; // O(1)
}
template<typename Iterator>
void advance_impl(Iterator& it, int n, bidirectional_iterator_tag) {
while (n-- > 0) ++it; // O(n)
while (n++ < 0) --it;
}
template<typename Iterator>
void advance(Iterator& it, int n) {
advance_impl(it, n, typename iterator_traits<Iterator>::iterator_category());
}
3.6 常见陷阱与面试题
陷阱1:vector 的特化
vector<bool> v(10);
v[0] = true;
// vector<bool> 不是真正的容器!
// 它用位压缩存储,每个 bool 只占 1 bit
// v[0] 返回的是代理对象,不是真正的 bool&
bool& ref = v[0]; // 编译错误!
auto val = v[0]; // val 是代理对象,不是 bool
// 如果需要真正的 bool 容器:
vector<char> v; // 或 deque<bool>
陷阱2:map 的 operator[] 会插入元素
map<string, int> m;
// m 是空的
if (m["key"] == 10) { // 错误!会插入 "key" -> 0
// ...
}
// 正确做法:
auto it = m.find("key");
if (it != m.end() && it->second == 10) {
// ...
}
// 或者 C++20:
if (m.contains("key") && m.at("key") == 10) {
// ...
}
陷阱3:unordered_map 的自定义哈希
struct Point {
int x, y;
bool operator==(const Point& other) const {
return x == other.x && y == other.y;
}
};
// 必须提供哈希函数
namespace std {
template<>
struct hash<Point> {
size_t operator()(const Point& p) const {
return hash<int>()(p.x) ^ (hash<int>()(p.y) << 1);
}
};
}
// 更好的哈希(减少冲突)
size_t operator()(const Point& p) const {
size_t h1 = hash<int>()(p.x);
size_t h2 = hash<int>()(p.y);
return h1 ^ (h2 + 0x9e3779b9 + (h1 << 6) + (h1 >> 2));
}
面试题1:vector 的 size 和 capacity 区别?
vector<int> v;
v.reserve(100); // capacity = 100, size = 0
v.resize(50); // capacity = 100, size = 50
v.shrink_to_fit(); // capacity = 50(请求,不保证)
// size:实际元素数量
// capacity:不重新分配内存的前提下可以容纳的元素数量
面试题2:deque 的实现原理?
// deque:分段连续数组
template<typename T>
class deque {
T** map; // 指向各个缓冲区的指针数组
size_t map_size; // map 大小
iterator start; // 指向第一个元素
iterator finish; // 指向最后一个元素的下一个位置
};
// 每个缓冲区固定大小(通常 512 bytes)
// 插入删除:
// - 两端:O(1),可能分配/释放缓冲区
// - 中间:O(n),需要移动元素
// 与 vector 对比:
// - vector:整体连续,扩容代价大
// - deque:分段连续,扩容代价小
面试题3:list 的 sort 为什么是成员函数?
list<int> l = {3, 1, 4, 1, 5};
// 错误:
sort(l.begin(), l.end()); // 需要随机访问迭代器
// 正确:
l.sort(); // list 提供成员函数,使用归并排序
// 原因:std::sort 需要随机访问迭代器(QuickSort/IntroSort)
// list 只有双向迭代器,无法使用 std::sort
四、类型转换
4.1 问题背景:C 风格转换的问题
场景:需要各种类型转换
// 基本类型转换
double d = 3.14;
int i = (int)d; // C 风格
// 类层次转换
Base* b = new Derived();
Derived* d = (Derived*)b; // 危险!不检查
// const 转换
const int x = 10;
int* p = (int*)&x; // 去掉 const,危险!
// 指针类型转换
int* ip = new int(10);
void* vp = (void*)ip;
char* cp = (char*)vp;
C 风格转换的问题:
┌─────────────────────────────────────────────────────────────┐
│ C 风格转换的问题 │
├─────────────────────────────────────────────────────────────┤
│ │
│ 1. 含义不明确 │
│ (Type)value 可以是: │
│ - 基本类型转换(int 转 double) │
│ - 类层次上行/下行转换 │
│ - const 添加/移除 │
│ - 指针重新解释 │
│ 一种语法,多种含义,难以区分 │
│ │
│ 2. 难以搜索 │
│ 在大型代码库中搜索类型转换很困难 │
│ │
│ 3. 太强大,容易隐藏错误 │
│ 编译器几乎不做检查,运行时可能崩溃 │
│ │
└─────────────────────────────────────────────────────────────┘
4.2 设计思路:分类管理,明确意图
┌─────────────────────────────────────────────────────────────┐
│ C++ 类型转换设计 │
├─────────────────────────────────────────────────────────────┤
│ │
│ 将转换按用途分类,每种转换有明确的语法和检查 │
│ │
│ static_cast:编译期已知类型的转换 │
│ - 基本类型转换 │
│ - 类层次上行转换(安全) │
│ - 类层次下行转换(不安全,不检查) │
│ - void* 转换 │
│ │
│ dynamic_cast:运行时类型检查 │
│ - 类层次下行转换(安全,运行时检查) │
│ - 需要虚函数表(RTTI) │
│ │
│ const_cast:const 属性转换 │
│ - 添加或移除 const/volatile │
│ │
│ reinterpret_cast:重新解释比特位 │
│ - 任意指针类型转换 │
│ - 指针和整数互转 │
│ 最危险,几乎不做检查 │
│ │
└─────────────────────────────────────────────────────────────┘
4.3 具体实现:四种转换的底层机制
static_cast:
// 1. 基本类型转换(编译器生成转换代码)
double d = 3.14;
int i = static_cast<int>(d); // 截断小数部分
// 2. 类层次上行转换(编译期确定偏移)
class Base { virtual void foo() {} };
class Derived : public Base {};
Derived d;
Base* b = static_cast<Base*>(&d); // 安全,编译期计算偏移
// 3. 类层次下行转换(不检查,危险)
Base* b = new Base();
Derived* d = static_cast<Derived*>(b); // 编译通过,运行时崩溃!
// 4. void* 转换
int* ip = new int(10);
void* vp = static_cast<void*>(ip);
int* ip2 = static_cast<int*>(vp);
dynamic_cast:
// 需要虚函数表(RTTI 信息存储在 vtable 中)
class Base {
public:
virtual ~Base() {} // 必须有虚函数
};
class Derived : public Base {};
// 下行转换,运行时检查
Base* b = new Derived();
Derived* d = dynamic_cast<Derived*>(b); // 成功,返回有效指针
Base* b2 = new Base();
Derived* d2 = dynamic_cast<Derived*>(b2); // 失败,返回 nullptr
// 引用转换失败抛异常
try {
Derived& d = dynamic_cast<Derived&>(*b2); // 抛出 bad_cast
} catch (const std::bad_cast& e) {
// 处理错误
}
// 底层实现:通过 vptr 找到 type_info,运行时比较类型
const_cast:
// 添加或移除 const/volatile
const int x = 10;
int* p = const_cast<int*>(&x);
// 危险:修改 const 对象是未定义行为
*p = 20; // 可能崩溃,可能无效,取决于编译器优化
// 正确使用场景1:函数返回的 const 需要修改
const char* getName() { return "name"; }
char* name = const_cast<char*>(getName());
// 正确使用场景2:重载函数中复用代码
class String {
public:
const char& operator[](size_t i) const {
// 复杂边界检查...
return data[i];
}
char& operator[](size_t i) {
return const_cast<char&>(
static_cast<const String&>(*this)[i]
);
}
};
reinterpret_cast:
// 最危险的转换,直接重新解释比特位
// 1. 不相关类型指针转换
class A {};
class B {};
A* a = new A();
B* b = reinterpret_cast<B*>(a); // 危险!
// 2. 指针和整数互转
void* p = malloc(100);
uintptr_t addr = reinterpret_cast<uintptr_t>(p);
void* p2 = reinterpret_cast<void*>(addr);
// 3. 函数指针转换
typedef void (*FuncPtr)();
FuncPtr f = reinterpret_cast<FuncPtr>(&some_function);
// 用途:底层编程、硬件访问、序列化
4.4 常见陷阱与面试题
陷阱1:dynamic_cast 需要虚函数
class Base {}; // 没有虚函数
class Derived : public Base {};
Base* b = new Derived();
Derived* d = dynamic_cast<Derived*>(b); // 编译错误!
// 解决:添加虚函数(通常是虚析构)
class Base {
public:
virtual ~Base() = default;
};
陷阱2:static_cast 下行转换不检查
class Base { virtual void foo() {} };
class Derived : public Base {
public:
void derivedOnly() {}
};
Base* b = new Base();
Derived* d = static_cast<Derived*>(b); // 编译通过!
d->derivedOnly(); // 未定义行为,可能崩溃
// 安全做法:
if (Derived* d = dynamic_cast<Derived*>(b)) {
d->derivedOnly();
}
陷阱3:reinterpret_cast 的别名规则
// 严格别名规则:通过不相关类型的指针访问对象是未定义行为
int x = 10;
float* f = reinterpret_cast<float*>(&x);
cout << *f; // 未定义行为!
// 正确做法:使用 memcpy 或 bit_cast(C++20)
float f;
static_assert(sizeof(f) == sizeof(x));
memcpy(&f, &x, sizeof(x));
// C++20 bit_cast
float f = std::bit_cast<float>(x);
面试题1:四种转换的使用场景?
// static_cast:编译期已知类型
int i = static_cast<int>(3.14);
Base* b = static_cast<Base*>(derived_ptr);
// dynamic_cast:运行时类型检查
Derived* d = dynamic_cast<Derived*>(base_ptr);
// const_cast:const 属性
char* p = const_cast<char*>(const_p);
// reinterpret_cast:重新解释比特位
uintptr_t addr = reinterpret_cast<uintptr_t>(ptr);
面试题2:为什么 dynamic_cast 需要虚函数?
// dynamic_cast 依赖 RTTI(Run-Time Type Information)
// RTTI 信息存储在 vtable 中
// 对象内存布局:
// [ vptr ] [ 成员变量... ]
// ↓
// [ type_info ][ 虚函数指针... ]
// dynamic_cast 通过 vptr 找到 type_info
// 运行时比较类型关系
// 没有虚函数就没有 vtable,无法获取类型信息
五、Lambda 表达式
5.1 问题背景:函数对象的痛点
场景:STL 算法需要自定义比较逻辑
// 方案1:函数指针
bool greater_than(int a, int b) {
return a > b;
}
sort(v.begin(), v.end(), greater_than);
// 问题:不能携带状态
// 方案2:函数对象(仿函数)
class GreaterThan {
int threshold;
public:
GreaterThan(int t) : threshold(t) {}
bool operator()(int a, int b) const {
return a > b;
}
};
sort(v.begin(), v.end(), GreaterThan(10));
// 问题:需要写很多样板代码,定义在别处
// 方案3:局部类(C++98 不能用)
// C++98 不允许在函数内定义类用于模板参数
核心问题:
┌─────────────────────────────────────────────────────────────┐
│ 需要一种轻量级的函数对象 │
├─────────────────────────────────────────────────────────────┤
│ │
│ 要求: │
│ 1. 可以定义在调用处(局部、匿名) │
│ 2. 可以捕获局部变量(携带状态) │
│ 3. 类型安全,编译期检查 │
│ 4. 零开销(不劣于手写函数对象) │
│ │
└─────────────────────────────────────────────────────────────┘
5.2 设计思路:编译器生成匿名类
┌─────────────────────────────────────────────────────────────┐
│ Lambda 的核心思想 │
├─────────────────────────────────────────────────────────────┤
│ │
│ Lambda 不是魔法,是编译器帮你写函数对象 │
│ │
│ [捕获](参数) -> 返回类型 { 函数体 } │
│ ↓ ↓ ↓ ↓ │
│ 成员变量 operator() 返回值 函数体 │
│ │
│ 编译器生成: │
│ class Lambda_XXX { │
│ 捕获变量作为成员; │
│ public: │
│ 返回值 operator()(参数) const { 函数体 } │
│ }; │
│ │
└─────────────────────────────────────────────────────────────┘
5.3 具体实现:Lambda 的本质
编译器生成的代码:
// 源代码
int x = 10, y = 20;
auto lambda = [x, &y](int a, int b) -> int {
return x + y + a + b;
};
int result = lambda(1, 2);
// 编译器实际生成的代码(等价):
class Lambda_123 {
int x; // 值捕获的成员
int& y; // 引用捕获的成员
public:
Lambda_123(int x, int& y) : x(x), y(y) {}
int operator()(int a, int b) const {
return x + y + a + b;
}
};
int x = 10, y = 20;
Lambda_123 lambda(x, y);
int result = lambda(1, 2);
捕获列表详解:
int x = 10, y = 20;
// 值捕获:拷贝变量
auto f1 = [x, y]() { return x + y; };
// 等价于:
class Lambda1 {
int x, y;
public:
Lambda1(int x, int y) : x(x), y(y) {}
int operator()() const { return x + y; }
};
// 引用捕获:引用变量
auto f2 = [&x, &y]() { return x + y; };
// 等价于:
class Lambda2 {
int &x, &y;
public:
Lambda2(int& x, int& y) : x(x), y(y) {}
int operator()() const { return x + y; }
};
// 隐式捕获
auto f3 = [=]() { return x + y; }; // 全部值捕获
auto f4 = [&]() { return x + y; }; // 全部引用捕获
auto f5 = [=, &x]() { return x + y; }; // y 值捕获,x 引用捕获
auto f6 = [&, x]() { return x + y; }; // y 引用捕获,x 值捕获
// this 捕获
class MyClass {
int value = 10;
public:
void func() {
auto f = [this]() { return value; }; // 捕获 this 指针
auto g = [*this]() { return value; }; // C++17:拷贝整个对象
}
};
mutable:
int x = 10;
// 值捕获的变量默认是 const
auto f = [x]() { x++; }; // 编译错误
// mutable 允许修改捕获的变量
auto f = [x]() mutable {
x++; // 修改的是成员变量的拷贝
return x;
};
f(); // 返回 11
f(); // 返回 12(每次调用都是新的拷贝)
// x 本身还是 10
5.4 泛型 Lambda(C++14)
// C++14 起,参数可以是 auto
auto add = [](auto a, auto b) {
return a + b;
};
add(1, 2); // int
add(1.5, 2.5); // double
add(string("hello"), " world"); // string
// 编译器生成:
template<typename T, typename U>
auto add(T a, U b) {
return a + b;
}
// C++20 模板 Lambda
auto f = []<typename T>(T x, T y) {
return x + y;
};
5.5 常见陷阱与面试题
陷阱1:引用捕获的生命周期
function<int()> make_lambda() {
int x = 10;
return [&x]() { return x; }; // 危险!x 是局部变量
}
auto f = make_lambda();
f(); // 悬垂引用!x 已经销毁
// 正确做法:值捕获
return [x]() { return x; };
陷阱2:Lambda 不能默认构造
auto lambda = [](int x) { return x * 2; };
// 错误:
decltype(lambda) another; // 编译错误,没有默认构造
// 除非捕获列表为空
auto empty = []() {};
decltype(empty) another; // OK
陷阱3:std::function 的类型擦除开销
// Lambda 有具体类型(编译器生成)
auto lambda = [](int x) { return x * 2; };
// lambda 的类型是编译器生成的匿名类
// 存储在 std::function 中(类型擦除)
function<int(int)> f = lambda;
// 有虚函数调用开销
// 如果不需要类型擦除,不要用 std::function
// 直接用 auto 或模板
面试题1:Lambda 的大小是多少?
int x = 10, y = 20, z = 30;
auto f1 = []() {}; // 大小 = 1(空类优化)
auto f2 = [x]() {}; // 大小 = 4(一个 int)
auto f3 = [x, y]() {}; // 大小 = 8(两个 int)
auto f4 = [x, y, z]() {}; // 大小 = 12(三个 int)
auto f5 = [&x]() {}; // 大小 = 8(一个引用,64位)
auto f6 = [x]() mutable {}; // 大小 = 4(mutable 不影响大小)
// 注意:空 Lambda 大小为 1,是 C++ 标准规定(对象必须有唯一地址)
面试题2:实现一个简易的 delegate
template<typename... Args>
class Delegate {
using FuncType = function<void(Args...)>;
vector<FuncType> handlers;
public:
void add(FuncType f) {
handlers.push_back(f);
}
void operator()(Args... args) {
for (auto& h : handlers) {
h(args...);
}
}
};
// 使用
Delegate<int> onClick;
onClick.add([](int x) { cout << "A: " << x << endl; });
onClick.add([](int x) { cout << "B: " << x << endl; });
onClick(10); // 调用所有 handler
六、C++11/14/17/20 新特性
6.1 问题背景:C++98 的痛点
场景:现代编程需求的演进
// 1. 类型推导繁琐
map<string, vector<pair<int, string>>>::iterator it = m.begin();
// 太长!
// 2. NULL 的歧义
void foo(int);
void foo(char*);
foo(NULL); // 调用 foo(int)!
// 3. 遍历容器麻烦
for (vector<int>::iterator it = v.begin(); it != v.end(); ++it) {
cout << *it << endl;
}
// 4. 初始化不统一
int arr[] = {1, 2, 3};
vector<int> v;
for (int i = 0; i < 3; i++) v.push_back(arr[i]);
// 5. 编译期计算能力弱
// 想要编译期计算阶乘,只能用模板元编程,极其复杂
6.2 auto 与 decltype:自动类型推导
设计目标:
┌─────────────────────────────────────────────────────────────┐
│ auto 的设计目标 │
├─────────────────────────────────────────────────────────────┤
│ │
│ 1. 简化代码,减少类型重复 │
│ 2. 处理"类型难以书写"的情况(迭代器、lambda) │
│ 3. 类型安全,编译期推导 │
│ 4. 不影响性能(编译期完成) │
│ │
│ 注意:auto 不是动态类型,类型仍然是静态确定的 │
│ │
└─────────────────────────────────────────────────────────────┘
实现原理:
// auto 推导规则(类似模板参数推导)
auto x = expr;
// 等价于:
template<typename T>
void f(T x);
f(expr);
// 具体例子
auto i = 10; // int
auto& ri = i; // int&
const auto ci = 10; // const int
auto* p = &i; // int*
// 注意:auto 会去掉引用和 const
const int& cref = 10;
auto x = cref; // int(不是 const int&)
const auto y = cref; // const int
auto& z = cref; // const int&
decltype:
// decltype 获取表达式的精确类型
int x = 10;
int& rx = x;
const int cx = 10;
decltype(x) a; // int
decltype(rx) b = x; // int&(必须初始化)
decltype(cx) c = 0; // const int
decltype(x + 0.0) d; // double
// decltype(auto):C++14
int& func();
decltype(auto) x = func(); // int&(保留引用)
auto y = func(); // int(去掉引用)
6.3 nullptr:解决 NULL 的歧义
问题:
void foo(int x) { cout << "int\n"; }
void foo(char* p) { cout << "pointer\n"; }
void foo(double d) { cout << "double\n"; }
foo(NULL); // 调用 foo(int)!
// 原因:NULL 是宏,通常定义为 0 或 0L
nullptr 的实现:
// nullptr 是 std::nullptr_t 类型的常量
// std::nullptr_t 可以隐式转换为任意指针类型
// 但不能转换为整数类型
foo(nullptr); // 明确调用 foo(char*)
// 实现原理(简化):
struct nullptr_t {
template<typename T>
operator T*() const { return 0; } // 转换为任意指针
template<typename C, typename T>
operator T C::*() const { return 0; } // 转换为成员指针
};
nullptr_t nullptr;
6.4 范围 for 循环:简化遍历
实现原理:
// 源代码
for (const auto& x : container) {
cout << x << endl;
}
// 编译器展开(近似):
{
auto&& __range = container;
auto __begin = __range.begin();
auto __end = __range.end();
for (; __begin != __end; ++__begin) {
const auto& x = *__begin;
cout << x << endl;
}
}
自定义类型支持范围 for:
class MyContainer {
int data[10];
public:
int* begin() { return data; }
int* end() { return data + 10; }
const int* begin() const { return data; }
const int* end() const { return data + 10; }
};
MyContainer c;
for (auto& x : c) { // 调用 c.begin() 和 c.end()
x *= 2;
}
6.5 constexpr:编译期计算
演进:
// C++11:限制很多
constexpr int square(int x) {
return x * x; // 只能有一条 return
}
// C++14:放宽限制
constexpr int factorial(int n) {
int result = 1; // 可以声明变量
for (int i = 1; i <= n; ++i) { // 可以用循环
result *= i;
}
return result;
}
// C++17:constexpr if
template<typename T>
constexpr auto get_value(T t) {
if constexpr (std::is_pointer_v<T>) // 编译期条件
return *t;
else
return t;
}
// C++20:constexpr 虚函数、try-catch、new/delete
实现原理:
编译期计算 vs 运行时计算:
constexpr int x = factorial(5); // 编译期计算
// 编译器在编译时执行 factorial(5),直接生成 x = 120
const int y = factorial(5); // 可能运行时计算
// const 只保证只读,不保证编译期计算
int z = factorial(5); // 运行时计算
6.6 默认/删除函数:精确控制
问题:编译器自动生成函数的问题
class Resource {
int* data;
public:
Resource() : data(new int[100]) {}
~Resource() { delete[] data; }
// 编译器生成的拷贝构造函数:浅拷贝!
// 导致双重释放!
};
Resource r1;
Resource r2 = r1; // 浅拷贝,两个指针指向同一内存
// r1 和 r2 析构时都会 delete[],崩溃!
解决方案:
class Resource {
int* data;
public:
Resource() : data(new int[100]) {}
~Resource() { delete[] data; }
// 显式默认:让编译器生成默认版本
Resource(const Resource&) = default; // 但这里有问题!
// 显式删除:禁止某些操作
Resource(const Resource&) = delete; // 禁止拷贝
Resource& operator=(const Resource&) = delete; // 禁止赋值
// 移动操作
Resource(Resource&& other) noexcept : data(other.data) {
other.data = nullptr;
}
};
// 应用:单例模式
class Singleton {
public:
static Singleton& getInstance() {
static Singleton instance;
return instance;
}
Singleton(const Singleton&) = delete;
Singleton& operator=(const Singleton&) = delete;
private:
Singleton() = default;
};
6.7 结构化绑定(C++17):解构对象
// 绑定 pair
map<string, int> m;
for (const auto& [key, value] : m) {
cout << key << ": " << value << endl;
}
// 绑定 tuple
tuple<int, string, double> t = {1, "hello", 3.14};
auto [id, name, score] = t;
// 绑定数组
int arr[3] = {1, 2, 3};
auto [a, b, c] = arr;
// 绑定结构体(C++17 起)
struct Point { int x, y; };
Point p = {10, 20};
auto [px, py] = p;
// 实现原理:编译器生成匿名变量和解构代码
auto __t = t;
int id = get<0>(__t);
string name = get<1>(__t);
double score = get<2>(__t);
6.8 常见陷阱与面试题
陷阱1:auto 推导陷阱
vector<bool> v = {true, false, true};
auto x = v[0]; // x 是 vector<bool>::reference,不是 bool!
// 正确做法:
bool x = v[0];
auto x = static_cast<bool>(v[0]);
陷阱2:constexpr 函数可能运行时执行
constexpr int square(int x) {
return x * x;
}
int y = 10;
int r = square(y); // 运行时计算,y 不是常量
constexpr int z = 10;
constexpr int r2 = square(z); // 编译期计算
面试题1:auto 和 decltype 的区别?
int x = 10;
int& rx = x;
const int cx = 10;
auto a = x; // int(去掉引用和 const)
decltype(x) b; // int
auto c = rx; // int(去掉引用)
decltype(rx) d = x; // int&(保留引用)
auto e = cx; // int(去掉 const)
decltype(cx) f = 0; // const int(保留 const)
面试题2:constexpr 和 const 的区别?
const int a = 10; // 只读,可能在运行时初始化
constexpr int b = 10; // 编译期常量
const int c = getValue(); // OK,运行时初始化
// constexpr int d = getValue(); // 错误,getValue 必须是 constexpr
int arr[a]; // C++11 前可能错误(VLA)
int arr2[b]; // 总是 OK
附录:面试速查表
高频问题速答
| 问题 | 答案要点 |
|---|---|
| 模板实例化时机 | 编译期,根据使用类型生成代码 |
| 模板代码放哪里 | 头文件(或显式实例化) |
| SFINAE 原理 | 替换失败不是错误,用于编译期条件 |
| 右值引用作用 | 实现移动语义,避免深拷贝 |
| move 做了什么 | 类型转换,将左值转为右值 |
| 完美转发 | std::forward 保持参数的值类别 |
| vector 扩容 | 通常 1.5 或 2 倍,均摊 O(1) |
| vector 迭代器失效 | 扩容时全部失效,insert/erase 部分失效 |
| map vs unordered_map | 红黑树 O(log n) 有序 vs 哈希表 O(1) 无序 |
| 四种 cast 区别 | static/dynamic/const/reinterpret,各有用途 |
| Lambda 本质 | 编译器生成的匿名函数对象类 |
| Lambda 捕获方式 | 值捕获、引用捕获、隐式捕获 |
| auto 推导规则 | 去掉引用和 const,类似模板推导 |
| constexpr 作用 | 编译期计算,可用于数组大小等 |
| = default/delete | 控制编译器自动生成函数 |
易错点总结
1. 模板 typename 关键字(依赖名)
2. 右值引用是左值(T&& 参数在函数体内是左值)
3. move 后不要使用原对象
4. vector<bool> 不是容器
5. map[] 会插入元素
6. dynamic_cast 需要虚函数
7. Lambda 引用捕获的生命周期
8. auto 推导去掉引用和 const
9. constexpr 函数可能运行时执行
10. NULL 是 0,nullptr 是空指针
最后提醒:理解原理比记忆语法更重要,面试时能够解释"为什么"比知道"怎么做"更有价值。