C++ 面试核心知识点

4 阅读33分钟

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_mapO(1) 更快
自定义类型无哈希map只需 operator<
对内存敏感map哈希表有额外开销
需要范围查询maplower_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 可以是:                                     │
│      - 基本类型转换(intdouble)                        │
│      - 类层次上行/下行转换                                    │
│      - const 添加/移除                                        │
│      - 指针重新解释                                           │
│      一种语法,多种含义,难以区分                            │
│                                                             │
│   2. 难以搜索                                                │
│      在大型代码库中搜索类型转换很困难                        │
│                                                             │
│   3. 太强大,容易隐藏错误                                    │
│      编译器几乎不做检查,运行时可能崩溃                      │
│                                                             │
└─────────────────────────────────────────────────────────────┘

4.2 设计思路:分类管理,明确意图

┌─────────────────────────────────────────────────────────────┐
│                    C++ 类型转换设计                          │
├─────────────────────────────────────────────────────────────┤
│                                                             │
│   将转换按用途分类,每种转换有明确的语法和检查               │
│                                                             │
│   static_cast:编译期已知类型的转换                          │
│   - 基本类型转换                                             │
│   - 类层次上行转换(安全)                                   │
│   - 类层次下行转换(不安全,不检查)                         │
│   - void* 转换                                               │
│                                                             │
│   dynamic_cast:运行时类型检查                               │
│   - 类层次下行转换(安全,运行时检查)                       │
│   - 需要虚函数表(RTTI)                                     │
│                                                             │
│   const_castconst 属性转换                                 │
│   - 添加或移除 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. NULL0nullptr 是空指针

最后提醒:理解原理比记忆语法更重要,面试时能够解释"为什么"比知道"怎么做"更有价值。