C/C++后端筑基系列(二):C++ 从基础到进阶

上一篇完成了 C 语言全景梳理。本篇进入C++------它不是"C 的升级包",而是一门重新定义了抽象能力的多范式语言。掌握 C++,意味着同时掌握:面向对象、泛型、资源管理、并发。这也是后端面试绕不开的核心。
阅读前提:已掌握 C 语言基础(指针、结构体、内存模型)。

约定:所有示例均按 C++17 标准编写(建议 g++ -std=c++17 编译;

文中 C++11/14 特性已在代码注释中标注),代码中出现的注释用于标注关键点。


系列总览

领域 状态
第01篇 C 语言 ✅ 已完成
第02篇 C++ 本篇
第03篇 数据结构与算法 规划中
第04篇 Linux 操作系统 规划中
第05篇 Git 版本控制 规划中

本篇目录

系列总览

本篇目录

[第1章 C++ 的历史与设计哲学](#第1章 C++ 的历史与设计哲学)

[1.1 一段简史](#1.1 一段简史)

[1.2 设计哲学:多范式](#1.2 设计哲学:多范式)

[1.3 零开销抽象(Zero-Overhead Abstraction)](#1.3 零开销抽象(Zero-Overhead Abstraction))

[1.4 与 C 的关系](#1.4 与 C 的关系)

[第2章 从 C 到 C++:语言层面的进阶](#第2章 从 C 到 C++:语言层面的进阶)

[2.1 引用(Reference):指针的"安全壳"](#2.1 引用(Reference):指针的"安全壳")

[2.2 函数重载:同名不同参](#2.2 函数重载:同名不同参)

[2.3 默认参数与内联函数](#2.3 默认参数与内联函数)

[2.4 命名空间:防止名字冲突](#2.4 命名空间:防止名字冲突)

[2.5 nullptr:真正的空指针](#2.5 nullptr:真正的空指针)

[2.6 bool 类型](#2.6 bool 类型)

[2.7 const 的进阶用法](#2.7 const 的进阶用法)

[2.8 字符串与动态数组:string 和 vector 初体验](#2.8 字符串与动态数组:string 和 vector 初体验)

[第3章 类与对象](#第3章 类与对象)

[3.1 class 与 struct](#3.1 class 与 struct)

[3.2 构造函数与初始化列表](#3.2 构造函数与初始化列表)

[3.3 析构函数](#3.3 析构函数)

[3.4 this 指针](#3.4 this 指针)

[3.5 const 成员函数](#3.5 const 成员函数)

[3.6 静态成员](#3.6 静态成员)

[第4章 继承与多态:面向对象三大特性](#第4章 继承与多态:面向对象三大特性)

[4.1 三种继承方式](#4.1 三种继承方式)

[4.2 重载、隐藏、重写:三兄弟区别](#4.2 重载、隐藏、重写:三兄弟区别)

[4.3 虚函数与动态多态](#4.3 虚函数与动态多态)

[4.4 虚函数表(vtable):多态的底层原理](#4.4 虚函数表(vtable):多态的底层原理)

[4.5 纯虚函数与抽象类](#4.5 纯虚函数与抽象类)

[4.6 虚析构:防止内存泄漏](#4.6 虚析构:防止内存泄漏)

[4.7 多态的代价](#4.7 多态的代价)

[第5章 运算符重载与友元](#第5章 运算符重载与友元)

[5.1 什么是运算符重载](#5.1 什么是运算符重载)

[5.2 流运算符:为什么不能是成员函数](#5.2 流运算符:为什么不能是成员函数)

[5.3 友元(friend)](#5.3 友元(friend))

[5.4 哪些运算符不能重载](#5.4 哪些运算符不能重载)

[第6章 内存管理与智能指针](#第6章 内存管理与智能指针)

[6.1 new/delete vs malloc/free](#6.1 new/delete vs malloc/free)

[6.2 进程内存布局](#6.2 进程内存布局)

[6.3 内存泄漏](#6.3 内存泄漏)

[6.4 智能指针:RAII 的标准实现](#6.4 智能指针:RAII 的标准实现)

[6.5 循环引用:shared_ptr 的陷阱](#6.5 循环引用:shared_ptr 的陷阱)

[第7章 拷贝控制与移动语义](#第7章 拷贝控制与移动语义)

[7.1 拷贝构造函数与拷贝赋值](#7.1 拷贝构造函数与拷贝赋值)

[7.2 深拷贝:自己实现拷贝控制](#7.2 深拷贝:自己实现拷贝控制)

[7.3 三法则 / 五法则](#7.3 三法则 / 五法则)

[7.4 右值引用与移动语义](#7.4 右值引用与移动语义)

[7.5 完美转发:std::forward](#7.5 完美转发:std::forward)

[第8章 STL 容器与迭代器](#第8章 STL 容器与迭代器)

[8.1 容器总览](#8.1 容器总览)

[8.2 vector 原理与扩容](#8.2 vector 原理与扩容)

[8.3 迭代器:统一的访问接口](#8.3 迭代器:统一的访问接口)

[8.4 迭代器失效:面试高频坑](#8.4 迭代器失效:面试高频坑)

[8.5 关联容器:map 与 unordered_map 的对比](#8.5 关联容器:map 与 unordered_map 的对比)

[8.6 使用建议](#8.6 使用建议)

[第9章 STL 算法、函数对象与 lambda](#第9章 STL 算法、函数对象与 lambda)

[9.1 常用算法速查](#9.1 常用算法速查)

[9.2 函数对象(仿函数)](#9.2 函数对象(仿函数))

[9.3 lambda 表达式:就地匿名函数对象](#9.3 lambda 表达式:就地匿名函数对象)

[9.4 算法与 lambda 配合是日常主力](#9.4 算法与 lambda 配合是日常主力)

[第10章 模板与泛型编程](#第10章 模板与泛型编程)

[10.1 函数模板](#10.1 函数模板)

[10.2 类模板](#10.2 类模板)

[10.3 特化与偏特化](#10.3 特化与偏特化)

[10.4 变参模板(C++11)](#10.4 变参模板(C++11))

[10.5 模板的常见坑](#10.5 模板的常见坑)

[第11章 C++11 现代特性实战](#第11章 C++11 现代特性实战)

[11.1 auto 与 decltype](#11.1 auto 与 decltype)

[11.2 范围 for:遍历的最简形态](#11.2 范围 for:遍历的最简形态)

[11.3 初始化列表](#11.3 初始化列表)

[11.4 结构化绑定(C++17)](#11.4 结构化绑定(C++17))

[11.5 enum class:强类型枚举](#11.5 enum class:强类型枚举)

[11.6 nullptr、override、final 小结](#11.6 nullptr、override、final 小结)

[11.7 std::optional、std::variant、std::any(C++17)](#11.7 std::optional、std::variant、std::any(C++17))

[11.8 其他高频特性速览](#11.8 其他高频特性速览)

[第12章 并发编程](#第12章 并发编程)

[12.1 线程基础:std::thread](#12.1 线程基础:std::thread)

[12.2 互斥锁:std::mutex 与 RAII 锁](#12.2 互斥锁:std::mutex 与 RAII 锁)

[12.3 条件变量:线程间通知](#12.3 条件变量:线程间通知)

[12.4 原子操作:std::atomic](#12.4 原子操作:std::atomic)

[12.5 async、future、promise:异步任务](#12.5 async、future、promise:异步任务)

[12.6 并发常见坑](#12.6 并发常见坑)

[第13章 异常处理与异常安全](#第13章 异常处理与异常安全)

[13.1 try/catch 基础](#13.1 try/catch 基础)

[13.2 异常规格与 noexcept](#13.2 异常规格与 noexcept)

[13.3 异常安全三级别](#13.3 异常安全三级别)

[13.4 异常 vs 错误码](#13.4 异常 vs 错误码)

[第14章 工程实践与常见坑](#第14章 工程实践与常见坑)

[14.1 头文件与编译](#14.1 头文件与编译)

[14.2 const 正确性](#14.2 const 正确性)

[14.3 常见坑清单](#14.3 常见坑清单)

[14.4 性能要点](#14.4 性能要点)

[14.5 编码规范建议](#14.5 编码规范建议)

[第15章 面试考点速查表](#第15章 面试考点速查表)

结语


第1章 C++ 的历史与设计哲学

1.1 一段简史

1979 年,Bjarne Stroustrup 在贝尔实验室开始研究"带类的 C"(C with Classes)。他的目标朴素而坚定:既要 C 的性能和底层控制力,又要 Simula 的抽象能力

年份 里程碑
1979 开始开发 "C with Classes"
1983 正式更名 C++(++ 是 C 的自增运算符)
1985 第一个商业版本发布
1998 C++98 首次标准化(沿用至 2011 年的"经典 C++")
2003 C++03(小修小补)
2011 C++11 里程碑式大改:移动语义、lambda、智能指针、并发
2014 C++14(C++11 的完善)
2017 C++17(if 初始化、结构化绑定、filesystem)
2020 C++20(概念、协程、range、模块初探)
2023 C++23(进一步演进)

关键认知 :面试和日常开发中,C++11 是"现代 C++"的分水岭。经典 C++ 靠裸指针和手工管理,现代 C++ 靠 RAII 和智能指针。后面所有章节都默认"现代 C++"视角。

1.2 设计哲学:多范式

C++ 不是纯面向对象语言,它同时支持四种范式:

范式 核心手段 代表语法
过程式 函数、结构体 与 C 相同的部分
面向对象 类(封装)、继承、多态 class、virtual
泛型 模板 template
函数式 lambda、算法 std::for_each

这是 C++ 强大的根源,也是复杂度的来源。没有银弹,只有选择------不同场景选不同范式。

1.3 零开销抽象(Zero-Overhead Abstraction)

C++ 的核心承诺:你为抽象付出的性能,应当趋近于零。虚函数调用、模板实例化,都在编译期或运行时以最小代价完成。

代价是:这些机制的原理必须被理解(比如虚函数表 vtable、模板实例化),否则容易写出"看起来面向对象、实际性能失控"的代码。这也是为什么 C++ 面试爱问底层原理。

1.4 与 C 的关系

维度 C C++
定位 系统级、贴近硬件 系统级 + 抽象层
抽象手段 结构体 + 函数指针 类、模板、继承
内存管理 malloc/free new/delete + RAII
默认范式 过程式 多范式
类型安全 相对更强(引用、cast 严格)

注意:C++ 基本兼容 C 语法,但不建议在 C++ 中继续写 C 风格代码。面试官问"你写了多久 C++",潜台词是"你会不会只写了个带 cout 的 C"。


第2章 从 C 到 C++:语言层面的进阶

这一章解决"用 C 的思维看 C++"时的第一个落差:C++ 在语言层面对 C 做了哪些增强

2.1 引用(Reference):指针的"安全壳"

引用是变量的别名,必须在定义时初始化,且不能改变指向

cpp 复制代码
#include <iostream>

int main() {
    int a = 10;
    int &r = a;          // r 是 a 的引用
    r = 20;              // 修改 a
    std::cout << a << std::endl;  // 输出 20

    int b = 30;
    r = b;               // 注意:这不是让 r 指向 b,而是把 b 的值赋给 a
    std::cout << a << std::endl;  // 输出 30,a 被改成 30

    // int &r2;          // 错误:引用必须初始化
    return 0;
}
对比项 指针 引用
是否可为空 可为 NULL 不能(必须绑定对象)
是否可改指向 可以 不可以
需要解引用 *p 直接用
传参语义 传地址(值拷贝指针) 传别名(不拷贝)
空指针风险 无(但可能悬垂)

传参最佳实践 :读参数用 const T&,改参数用 T&,需要可选/重绑定用 T*

cpp 复制代码
#include <string>
#include <iostream>

// 避免拷贝:const 引用
void print(const std::string &s) {
    std::cout << s << std::endl;
}

// 修改实参:引用
void addOne(int &n) {
    ++n;
}

int main() {
    std::string msg = "hello";
    print(msg);      // 不拷贝
    int x = 1;
    addOne(x);
    std::cout << x << std::endl;  // 2
    return 0;
}

2.2 函数重载:同名不同参

C++ 允许同名函数,只要参数列表不同(个数、类型、顺序)。返回类型不能区分重载。

cpp 复制代码
#include <iostream>

int add(int a, int b)        { return a + b; }
double add(double a, double b) { return a + b; }

int main() {
    std::cout << add(1, 2) << std::endl;       // 3
    std::cout << add(1.5, 2.5) << std::endl;   // 4
    return 0;
}

底层原理 :编译期做名字修饰 (name mangling),把参数类型编进符号名。这就是 C++ 函数能被重载而 C 不能的原因,也解释了为什么 C++ 调用 C 函数需要 extern "C"(关闭名字修饰)。

2.3 默认参数与内联函数

cpp 复制代码
#include <iostream>

void greet(const char *name = "world") {   // 默认参数
    std::cout << "Hello, " << name << std::endl;
}

inline int square(int x) { return x * x; }  // 内联:建议编译器展开

int main() {
    greet();        // Hello, world
    greet("C++");   // Hello, C++
    std::cout << square(5) << std::endl;     // 25
    return 0;
}

要点:

  • 默认参数必须从右往左连续提供 ,不能 void f(int a = 1, int b);
  • inline 只是建议,编译器可忽略;内联适合小函数,省去调用开销
  • C++17 起,inline 也可用于变量(内联变量)

2.4 命名空间:防止名字冲突

cpp 复制代码
#include <iostream>

namespace alice {
    int value = 1;
}
namespace bob {
    int value = 2;
}

int main() {
    std::cout << alice::value << std::endl;  // 1
    std::cout << bob::value << std::endl;    // 2
    using namespace bob;                     // 引入整个命名空间(慎用)
    std::cout << value << std::endl;         // 2
    return 0;
}

规范:头文件中禁止 using namespace std;,会污染全局命名空间。

2.5 nullptr:真正的空指针

C++11 引入 nullptr,类型是 std::nullptr_t,专门表示空指针。替代 C 的 NULL(本质是 (void*)00),避免重载歧义。

cpp 复制代码
#include <iostream>

void f(int)    { std::cout << "int\n"; }
void f(void*)  { std::cout << "ptr\n"; }

int main() {
    f(NULL);      // 有歧义,NULL 被当作 0 或 (void*)0?不推荐
    f(nullptr);   // 明确调用 f(void*)
    return 0;
}

2.6 bool 类型

C++ 内置 bool,取值 true/false,一般占 1 字节。

cpp 复制代码
#include <iostream>

int main() {
    bool ok = true;
    std::cout << ok << std::endl;        // 1
    std::cout << std::boolalpha << ok << std::endl;  // true
    return 0;
}

2.7 const 的进阶用法

C 语言里 const 只是"只读"约定。C++ 里 const 参与了类型系统,分两种:

形式 含义 示例
顶层 const 指针本身不可变 int *const p
底层 const 所指对象不可变 const int *p
cpp 复制代码
int main() {
    int a = 1;
    int *const p = &a;   // 顶层:p 不能再指向别处
    // p = nullptr;      // 错误
    *p = 2;              // 可以

    const int *q = &a;   // 底层:不能通过 q 修改 a
    // *q = 3;           // 错误
    a = 4;               // 可以(a 本身不是 const)
    return 0;
}

constexpr(C++11):编译期求值的常量表达式,比 const 更强。

cpp 复制代码
constexpr int kSize = 1024 * 1024;  // 编译期就确定

2.8 字符串与动态数组:string 和 vector 初体验

C++ 用 std::string 取代 char[],用 std::vector 取代"malloc + 手动扩容"。

cpp 复制代码
#include <iostream>
#include <string>
#include <vector>

int main() {
    std::string s = "hello";
    s += " world";                  // 拼接
    std::cout << s.size() << std::endl;  // 11

    std::vector<int> v = {1, 2, 3};
    v.push_back(4);                 // 自动扩容
    for (int x : v) {               // 范围 for(C++11)
        std::cout << x << ' ';
    }
    std::cout << std::endl;         // 1 2 3 4
    return 0;
}

到这里,你已经能在 C++ 里写出不自找麻烦的日常代码了。真正的重点在"类"------进入第3章。

第3章 类与对象

3.1 class 与 struct

C++ 里 classstruct 几乎一样,唯一默认差异:访问权限

关键字 默认访问权限 默认继承方式
struct public public
class private private
cpp 复制代码
#include <iostream>

struct Point {
    int x;            // 默认 public
    int y;
};

class Account {
private:
    double balance_;  // 默认 private,外部不可直接访问
public:
    void deposit(double n) { balance_ += n; }
    double balance() const { return balance_; }
};

int main() {
    Point p{1, 2};
    std::cout << p.x << std::endl;   // 1

    Account a;
    a.deposit(100);
    std::cout << a.balance() << std::endl;  // 100
    // a.balance_ = 0;               // 错误:private
    return 0;
}

工程规范:数据成员默认放 private,通过方法访问------这就是封装

3.2 构造函数与初始化列表

构造函数在对象创建时自动调用。初始化列表比函数体内赋值更高效、也更安全。

cpp 复制代码
#include <iostream>
#include <string>

class Student {
private:
    std::string name_;
    int age_;
public:
    // 初始化列表:真正"初始化",而非"先默认构造再赋值"
    Student(const std::string &name, int age)
        : name_(name), age_(age) {
        std::cout << "Student created\n";
    }

    void show() const {
        std::cout << name_ << ", " << age_ << std::endl;
    }
};

int main() {
    Student s("Tom", 18);
    s.show();   // Tom, 18
    return 0;
}

为什么必须用初始化列表const 成员、引用成员、没有默认构造函数的成员对象,只能在初始化列表中初始化。

cpp 复制代码
class Demo {
    const int id_;      // const 成员
    int &ref_;          // 引用成员
public:
    Demo(int id, int &r) : id_(id), ref_(r) {}   // 必须走初始化列表
};

3.3 析构函数

对象销毁时自动调用,用于释放资源。名字是"类名前加 "。

cpp 复制代码
#include <cstdio>
#include <iostream>

class File {
private:
    FILE *fp_;   // C 风格文件指针(示意)
public:
    //explicit 只用于单参数构造函数,作用:禁止隐式类型转换
    explicit File(const char *path) {
        fp_ = fopen(path, "w");
        std::cout << "open\n";
    }
    ~File() {
        if (fp_) fclose(fp_);
        std::cout << "close\n";
    }
};

int main() {
    File f("a.txt");
    return 0;   // 出作用域自动析构,输出 "close"
}

关键点:析构函数不能抛异常(否则 terminate);析构顺序与构造相反(成员逆序、派生先于基类)。

3.4 this 指针

成员函数内,this 指向调用该函数的对象本身。

cpp 复制代码
#include <iostream>

class Counter {
    int n_ = 0;
public:
    Counter &incr() {   // 返回引用,支持链式调用
        ++n_;
        return *this;   // 返回对象自身
    }
    void show() const { std::cout << n_ << std::endl; }
};

int main() {
    Counter c;
    c.incr().incr().incr();   // 链式调用
    c.show();                 // 3
    return 0;
}

3.5 const 成员函数

在函数声明后加 const,承诺"不修改对象状态"。const 对象只能调用 const 成员函数。

cpp 复制代码
class Widget {
    int value_ = 1;
public:
    int get() const { return value_; }   // 可被 const 对象调用
    void set(int v) { value_ = v; }      // 非 const
};

int main() {
    const Widget w;
    w.get();    // ok
    // w.set(2); // 错误:const 对象不能调用非 const 函数
    return 0;
}

规则const 成员函数内部不能修改成员变量(除非成员被 mutable 修饰)。

3.6 静态成员

静态成员属于类本身,不属于某个对象。所有对象共享一份。

cpp 复制代码
#include <iostream>

class Counter {
public:
    static int count;             // 声明
    Counter() { ++count; }
};

int Counter::count = 0;           // 定义(C++17 前必须类外定义)

int main() {
    Counter a, b, c;
    std::cout << Counter::count << std::endl;  // 3,通过类名访问
    return 0;
}

C++17 起可用 inline static int count = 0; 在类内直接初始化。

静态成员函数:没有 this,只能访问静态成员。


第4章 继承与多态:面向对象三大特性

面向对象三大特性:封装、继承、多态。封装已在第3章体现,本章聚焦继承与多态。

4.1 三种继承方式

继承方式 public 成员在派生类中 protected 成员 private 成员
public 继承 public protected 不可访问
protected 继承 protected protected 不可访问
private 继承 private private 不可访问

实际工程中 99% 用 public 继承(表达"is-a"关系)。

cpp 复制代码
class Base {
public:
    int pub = 1;
protected:
    int prot = 2;
private:
    int priv = 3;   // 派生类永远拿不到
};

class Derived : public Base {
public:
    void f() {
        std::cout << pub << prot;   // 可以访问 pub 和 prot
        // std::cout << priv;       // 错误
    }
};

4.2 重载、隐藏、重写:三兄弟区别

概念 条件 作用域 关键字
重载 overload 同名不同参 同一作用域
隐藏 hide 基类同名成员被派生类遮蔽 继承体系
重写 override 基类虚函数被派生类重新实现 继承体系 virtual/override
cpp 复制代码
class Base {
public:
    virtual void f() { std::cout << "Base::f\n"; }      // 虚函数
    void g()        { std::cout << "Base::g\n"; }
};

class Derived : public Base {
public:
    void f() override { std::cout << "Derived::f\n"; }  // 重写
    void g()        { std::cout << "Derived::g\n"; }    // 隐藏
};

隐藏 vs 重写:非虚函数同名即隐藏;虚函数同名且签名一致才是重写。

4.3 虚函数与动态多态

基类指针/引用调虚函数,实际调用运行时才确定的对象方法------这就是动态多态。

cpp 复制代码
#include <iostream>
#include <vector>
#include <memory>

class Shape {
public:
    virtual void draw() const { std::cout << "Shape\n"; }
    virtual ~Shape() = default;   // 重要:基类析构要 virtual(见4.5)
};

class Circle : public Shape {
public:
    void draw() const override { std::cout << "Circle\n"; }
};

class Square : public Shape {
public:
    void draw() const override { std::cout << "Square\n"; }
};

int main() {
    std::vector<std::unique_ptr<Shape>> shapes;
    shapes.push_back(std::make_unique<Circle>());
    shapes.push_back(std::make_unique<Square>());

    for (const auto &s : shapes) {
        s->draw();   // 输出 Circle / Square,运行期动态分派
    }
    return 0;
}

4.4 虚函数表(vtable):多态的底层原理

含虚函数的类,其每个对象头部隐含一个指针 vptr ,指向一张虚函数表 vtable,表中存储各虚函数的实际地址。

复制代码
对象内存布局(示意)
┌──────────────┐
│ vptr ──────┼────► vtable(属于类)
│ 数据成员     │       ├─ &Shape::draw()   ← 被派生类改写
│ 数据成员     │       └─ &Shape::~Shape()
└──────────────┘

调用 s->draw() 时,实际是:解引用 vptr → 查表 → 间接调用。这多了一次间接跳转,是虚函数比普通函数慢一点的来源。

常见面试题:含虚函数的对象 sizeof 会多一个指针大小(64 位下多 8 字节);每个类一张 vtable,所有该类的对象共享。

4.5 纯虚函数与抽象类

cpp 复制代码
class Animal {
public:
    virtual void speak() = 0;   // 纯虚函数:= 0
    virtual ~Animal() = default;
};

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

// Animal a;   // 错误:抽象类不能实例化

含至少一个纯虚函数的类叫抽象类,只能做基类。派生类必须实现所有纯虚函数,否则自身也是抽象类。

4.6 虚析构:防止内存泄漏

只要类会被当作基类使用,析构函数就要声明为 virtual

cpp 复制代码
#include <iostream>

class Base {
public:
    Base()  { std::cout << "Base()\n"; }
    virtual ~Base() { std::cout << "~Base()\n"; }   // 必须 virtual
};

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

int main() {
    Base *p = new Derived();
    delete p;   // 若析构非虚:只调用 ~Base(),派生类资源泄漏
    return 0;
}

为什么delete 基类指针时,编译器按静态类型(Base)决定调用哪个析构。非虚则走 ~Base(),派生类的资源不会被释放。虚析构保证按动态类型调用 ~Derived(),再自动调用 ~Base()

面试高频追问:析构为什么可以(应该)是虚的,构造函数为什么不能是虚的?

构造时虚表还没建立完整(先基类后派生),调用虚函数不会分派到派生类;析构时对象仍是完整对象,且必须按"派生→基类"顺序,所以需要虚析构。

4.7 多态的代价

代价 说明
内存 每个对象多一个 vptr
性能 每次调用多一次间接寻址(分支预测可能失效)
设计 对象切片、切片拷贝等问题需注意

对象切片(object slicing) :用基类接收派生类对象,派生类部分被"切掉"。

cpp 复制代码
Shape s = Circle();   // 切片:只有 Shape 部分被拷贝,draw 调用 Shape 版
Shape &r = circle;    // 引用:无切片,正确多态
Shape *p = &circle;   // 指针:无切片,正确多态

记住一句话:多态只通过指针或引用发生,值拷贝会切片。

第5章 运算符重载与友元

5.1 什么是运算符重载

让运算符(+ - * / << >> == 等)作用于自定义类型。本质是成员函数或友元函数的语法糖。

cpp 复制代码
#include <iostream>

class Vector {
public:
    int x, y;
    Vector(int x = 0, int y = 0) : x(x), y(y) {}

    // 成员函数形式:v1 + v2
    Vector operator+(const Vector &other) const {
        return Vector(x + other.x, y + other.y);
    }
};

int main() {
    Vector a(1, 2), b(3, 4);
    Vector c = a + b;            // 等价于 a.operator+(b)
    std::cout << c.x << ' ' << c.y << std::endl;   // 4 6
    return 0;
}

5.2 流运算符:为什么不能是成员函数

cout << v 中左操作数是 std::ostream,不是我们的类,所以必须写成友元函数

cpp 复制代码
#include <iostream>

class Point {
    int x_, y_;
public:
    Point(int x, int y) : x_(x), y_(y) {}
    friend std::ostream &operator<<(std::ostream &os, const Point &p);
};

std::ostream &operator<<(std::ostream &os, const Point &p) {
    os << "(" << p.x_ << ", " << p.y_ << ")";
    return os;
}

int main() {
    Point p(3, 5);
    std::cout << p << std::endl;   // (3, 5)
    return 0;
}

5.3 友元(friend)

友元可以访问类的 private 成员。破坏封装,谨慎使用

场景 是否建议用友元
重载 << / >> ✅ 必须
重载对称运算符(如 operator+ 两侧类型不同) ✅ 常用
一般业务逻辑 ❌ 尽量不用
cpp 复制代码
class A {
    int secret = 42;
    friend class B;          // B 可访问 A 的私有成员
    friend int hack(A &a);   // 全局函数可访问
};

5.4 哪些运算符不能重载

::

.*

?:

.

sizeof

typeid 不能重载。

重载规则 :不能发明新运算符;至少一个操作数是自定义类型;&&||, 重载后会丢失短路求值语义,不要重载


第6章 内存管理与智能指针

6.1 new/delete vs malloc/free

对比 malloc/free new/delete
语言 C 库函数 C++ 运算符
是否调用构造/析构
返回类型 void*,需强转 类型安全的指针
失败行为 返回 NULL std::bad_alloc
是否可重载 可(类级别重载)
数组 手动算大小 new[] / delete[]

注意new[] 必须配 delete[]new 必须配 delete,混用是未定义行为。

6.2 进程内存布局

bash 复制代码
高地址
┌─────────────────┐
│     栈 Stack     │  局部变量,向下增长,自动管理
├─────────────────┤
│      ↓ ↓ ↓      │
│   (未使用)      │
│      ↑ ↑ ↑      │
├─────────────────┤
│     堆 Heap      │  new/malloc,向上增长,手动管理
├─────────────────┤
│ 全局区/静态区     │  全局变量、static 变量
├─────────────────┤
│ 常量区(只读)    │  字符串字面量等
├─────────────────┤
│ 代码段 Code      │  指令
└─────────────────┘
低地址

6.3 内存泄漏

new 了没 delete,堆内存永久无法回收。长期运行的程序(服务器)会越来越占内存,最终 OOM。

现代 C++ 的答案:RAII------资源(内存、文件、锁)在构造时获得,析构时释放。栈对象自动析构,天然防泄漏。

6.4 智能指针:RAII 的标准实现

C++11 提供三种智能指针,都在 <memory>

智能指针 所有权 特点
unique_ptr 独占 不能拷贝,只能移动;零额外开销
shared_ptr 共享 引用计数;计数线程安全,对象访问不安全
weak_ptr 不拥有 观察者;解决循环引用,需要 lock()
cpp 复制代码
#include <iostream>
#include <memory>

struct Node {
    int val;
    Node(int v) : val(v) { std::cout << "Node " << v << " ctor\n"; }
    ~Node() { std::cout << "Node " << val << " dtor\n"; }
};

int main() {
    // unique_ptr:独占
    std::unique_ptr<Node> up = std::make_unique<Node>(1);
    // std::unique_ptr<Node> up2 = up;   // 错误:不能拷贝
    std::unique_ptr<Node> up3 = std::move(up);  // 移动转移所有权

    // shared_ptr:共享,引用计数
    std::shared_ptr<Node> sp1 = std::make_shared<Node>(2);
    std::shared_ptr<Node> sp2 = sp1;      // 计数 +1
    std::cout << sp1.use_count() << std::endl;  // 2

    // weak_ptr:不增加计数
    std::weak_ptr<Node> wp = sp1;
    if (auto locked = wp.lock()) {        // 需要 lock 才能访问
        std::cout << locked->val << std::endl;
    }
    return 0;
}

最佳实践

  • 优先 std::make_unique / std::make_shared(异常安全 + 一次分配)
  • 默认用 unique_ptr,确需共享才用 shared_ptr
  • 裸指针只用于"观察"(不拥有),不用于 delete

6.5 循环引用:shared_ptr 的陷阱

两个对象用 shared_ptr 互相指向,引用计数永远不为 0,内存泄漏。

cpp 复制代码
#include <iostream>
#include <memory>

struct B;  // 前置声明

struct A {
    std::shared_ptr<B> b;
    ~A() { std::cout << "~A\n"; }
};
struct B {
    std::shared_ptr<A> a;   // 改成 weak_ptr 可破解循环
    ~B() { std::cout << "~B\n"; }
};

int main() {
    std::shared_ptr<A> pa = std::make_shared<A>();
    std::shared_ptr<B> pb = std::make_shared<B>();
    pa->b = pb;
    pb->a = pa;             // 循环引用:A 和 B 都析构不掉
    std::cout << "before exit\n";
    return 0;               // 不输出 ~A / ~B(泄漏)
}

解法 :把其中一环改成 weak_ptr。weak_ptr 不增加引用计数,不阻碍析构。

面试必考:"shared_ptr 循环引用怎么解决?" → 用 weak_ptr。

第7章 拷贝控制与移动语义

7.1 拷贝构造函数与拷贝赋值

特殊成员函数 触发时机
拷贝构造 T(const T&) 用已有对象创建新对象、按值传参、按值返回
拷贝赋值 operator= 对已存在的对象赋值

默认行为是"逐成员拷贝"(浅拷贝)------对含指针的类就是灾难。

cpp 复制代码
#include <iostream>
#include <cstring>

class Str {
    char *buf_;
public:
    explicit Str(const char *s) {
        buf_ = new char[strlen(s) + 1];
        strcpy(buf_, s);
    }
    // 默认拷贝构造:逐成员拷贝 → 两个对象共享同一块内存 → 双重释放!

    ~Str() { delete[] buf_; }
};

int main() {
    Str a("hello");
    Str b = a;   // 浅拷贝:a.buf_ 和 b.buf_ 指向同一块内存
    // 函数结束时 a、b 各 delete 一次 → 双重释放 / 未定义行为
    return 0;
}

7.2 深拷贝:自己实现拷贝控制

cpp 复制代码
#include <iostream>
#include <cstring>

class Str {
    char *buf_;
public:
    explicit Str(const char *s) {
        buf_ = new char[strlen(s) + 1];
        strcpy(buf_, s);
    }
    // 拷贝构造:深拷贝
    Str(const Str &other) {
        buf_ = new char[strlen(other.buf_) + 1];
        strcpy(buf_, other.buf_);
    }
    // 拷贝赋值:先释放自己的,再拷贝(要处理自赋值)
    Str &operator=(const Str &other) {
        if (this == &other) return *this;   // 自赋值保护
        delete[] buf_;
        buf_ = new char[strlen(other.buf_) + 1];
        strcpy(buf_, other.buf_);
        return *this;
    }
    ~Str() { delete[] buf_; }

    const char *c_str() const { return buf_; }
};

int main() {
    Str a("hello");
    Str b = a;        // 深拷贝
    Str c("world");
    c = a;            // 拷贝赋值
    std::cout << b.c_str() << ' ' << c.c_str() << std::endl;  // hello hello
    return 0;
}

赋值重载要点 :返回 Str&(支持链式赋值 a = b = c)、处理自赋值、考虑异常安全(先 new 成功再 delete 更稳)。

7.3 三法则 / 五法则

如果一个类需要自定义析构 (说明管理着资源),那么拷贝构造、拷贝赋值也必须自定义------这叫三法则(Rule of Three)。

C++11 引入移动语义后扩展到五法则

特殊成员函数 签名
析构函数 ~T()
拷贝构造 T(const T&)
拷贝赋值 T& operator=(const T&)
移动构造 T(T&&)
移动赋值 T& operator=(T&&)

经验 :要么全实现,要么全默认(= default)。管理资源、需要深拷贝时,用智能指针成员往往能免写五法则。

7.4 右值引用与移动语义

问题std::vector 返回、字符串拼接,大量临时对象拷贝浪费性能。移动语义让"资源转移"代替"资源复制"。

cpp 复制代码
#include <iostream>
#include <utility>

class Buffer {
    int *data_;
    size_t size_;
public:
    explicit Buffer(size_t n) : size_(n), data_(new int[n]) {}
    ~Buffer() { delete[] data_; }

    // 移动构造:偷走 other 的资源,让 other 置空
    Buffer(Buffer &&other) noexcept
        : data_(other.data_), size_(other.size_) {
        other.data_ = nullptr;
        other.size_ = 0;
    }
    // 禁止拷贝(简化示例)
    // C++11 语法:= delete,显式删除函数。
    // 拷贝构造函数,被删除。
    // 作用:不能用一个对象去初始化另一个新对象。
    Buffer(const Buffer &) = delete;
    Buffer &operator=(const Buffer &) = delete;
};

int main() {
    Buffer b1(1000);
    Buffer b2 = std::move(b1);   // 移动:b1 的资源转给 b2,b1 变空
    return 0;
}

左值 vs 右值

  • 左值:有名字、能取地址。如变量、*p
  • 右值:临时值。如字面量、函数返回值 f()std::move(x)

std::move 不做任何移动,只是把左值转换成右值引用,好让移动构造被选中。

7.5 完美转发:std::forward

模板函数要把参数原样传给另一个函数,保持"左值仍是左值、右值仍是右值"------用 std::forward

cpp 复制代码
#include <utility>
#include <iostream>

void inner(int &)  { std::cout << "lvalue\n"; }
void inner(int &&) { std::cout << "rvalue\n"; }

template <typename T>
void outer(T &&x) {           // 万能引用(转发引用)
    inner(std::forward<T>(x));  // 保持 x 的左/右值属性
}

int main() {
    int a = 1;
    outer(a);       // lvalue
    outer(2);       // rvalue
    return 0;
}

移动语义是 C++11 最重要的变化之一。面试高频:"std::move 和 std::forward 的区别?"→ move 无条件转右值;forward 按模板参数类型条件转发。

第8章 STL 容器与迭代器

STL(Standard Template Library)是 C++ 最值钱的资产。容器 + 迭代器 + 算法三件套,配合模板,实现"数据与操作解耦"。

8.1 容器总览

容器 底层结构 特点 适用场景
vector 动态数组 连续内存、随机访问 O(1)、尾插 O(1) 默认选择
deque 分段连续 双端增删 O(1) 队列、双端操作
list 双向链表 任意位置插入删除 O(1)、无随机访问 频繁中间插入
forward_list 单向链表 更省内存 只需单向遍历
set / multiset 红黑树 有序、去重、O(log n) 去重 + 有序
map / multimap 红黑树 键值对、O(log n) 有序键值
unordered_set 哈希表 O(1) 平均、无序 快速查找
unordered_map 哈希表 O(1) 平均、无序 快速键值查找
array 定长数组 栈上、编译期定长 替代 C 数组
priority_queue 取最大/最小 O(1) 优先队列

经验法则 :不知道用什么容器就用 vector。它是缓存最友好的容器。

8.2 vector 原理与扩容

vector 内部是连续数组,记录三个指针:startfinish(已用末尾)、end_of_storage(容量末尾)。

扩容机制:容量不足时,分配一块新内存(常见为 1.5~2 倍),把旧元素移动过去,释放旧内存。

cpp 复制代码
#include <iostream>
#include <vector>

int main() {
    std::vector<int> v;
    for (int i = 0; i < 10; ++i) {
        v.push_back(i);
        std::cout << "size=" << v.size()
                  << " capacity=" << v.capacity() << "\n";
    }
    return 0;
}

面试要点

  • 扩容是昂贵的(拷贝/移动 + 分配 + 释放),预估大小用 reserve(n) 避免多次扩容
  • 扩容后,所有迭代器、指针、引用失效
  • 为什么是 1.5/2 倍?摊还分析保证 push_back 均摊 O(1)

8.3 迭代器:统一的访问接口

迭代器类似指针,是"泛型化的指针"。

cpp 复制代码
#include <iostream>
#include <vector>

int main() {
    std::vector<int> v = {1, 2, 3, 4};
    for (auto it = v.begin(); it != v.end(); ++it) {
        std::cout << *it << ' ';
    }
    std::cout << std::endl;
    // 范围 for 底层就是迭代器
    for (int x : v) std::cout << x << ' ';
    std::cout << std::endl;
    return 0;
}

迭代器种类与容器能力对应:

迭代器类型 能力 容器
随机访问 +n [n] -- vector、deque
双向 ++ -- list、set、map
前向 ++ forward_list、unordered_*
输入/输出 单次遍历 istream/ostream

8.4 迭代器失效:面试高频坑

修改容器可能导致迭代器失效。规则记牢:

操作 vector/deque 迭代器 list/set/map 迭代器
插入 可能全失效 不失效(list 插不影响其他)
删除 被删元素之后的失效 仅被删元素失效
扩容 全部失效 不适用
cpp 复制代码
#include <iostream>
#include <vector>

int main() {
    std::vector<int> v = {1, 2, 3, 4, 5};
    // 错误:erase 后 it 失效,再 ++ 是未定义行为
    // for (auto it = v.begin(); it != v.end(); ++it) {
    //     if (*it % 2 == 0) v.erase(it);
    // }

    // 正确:利用 erase 返回下一个有效迭代器
    auto it = v.begin();
    while (it != v.end()) {
        if (*it % 2 == 0)
            it = v.erase(it);
        else
            ++it;
    }
    for (int x : v) std::cout << x << ' ';   // 1 3 5
    std::cout << std::endl;
    return 0;
}

8.5 关联容器:map 与 unordered_map 的对比

对比 map unordered_map
底层 红黑树 哈希表
有序性 按键有序 无序
查找 O(log n) O(1) 平均
内存 较小 较大(桶 + 冲突链)
需要 自定义 < 自定义 hash + ==
稳定性 稳定 不稳定
cpp 复制代码
#include <iostream>
#include <map>
#include <string>

int main() {
    std::map<std::string, int> score;
    score["Alice"] = 90;
    score["Bob"] = 85;
    score["Alice"] = 95;   // 覆盖

    for (const auto &kv : score) {   // 按键升序遍历
        std::cout << kv.first << ": " << kv.second << std::endl;
    }
    // Alice: 95
    // Bob: 85

    auto it = score.find("Bob");
    if (it != score.end()) // 没找到 返回end
        std::cout << "found: " << it->second << std::endl;  // 85
    return 0;
}

注意operator[] 在键不存在时会插入默认值 。只查询用 findat

8.6 使用建议

  • 插入不破坏已有元素引用(除非扩容):vector 尾插安全
  • 需要中间删除 → list/set/map
  • 需要频繁头尾操作 → deque
  • 需要有序遍历 → map/set(红黑树)
  • 只要查找快 → unordered_*(哈希)

第9章 STL 算法、函数对象与 lambda

9.1 常用算法速查

STL 算法都在 <algorithm>,操作的是迭代器区间 [first, last)

算法 作用 复杂度
sort 排序(introsort) O(n log n)
stable_sort 稳定排序 O(n log n)
find 线性查找 O(n)
binary_search 二分查找(须有序) O(log n)
lower_bound 第一个 ≥ 值的位置 O(log n)
upper_bound 第一个 > 值的位置 O(log n)
count 统计出现次数 O(n)
accumulate 求和 O(n)
max_element 最大元素 O(n)
reverse 反转 O(n)
unique 去重相邻重复 O(n)
remove 逻辑删除(移到末尾) O(n)
cpp 复制代码
#include <algorithm>
#include <iostream>
#include <numeric>
#include <vector>

int main() {
    std::vector<int> v = {3, 1, 4, 1, 5, 9, 2, 6};

    std::sort(v.begin(), v.end());       // 1 1 2 3 4 5 6 9
    int sum = std::accumulate(v.begin(), v.end(), 0);
    std::cout << "sum=" << sum << std::endl;               // 31

    auto it = std::lower_bound(v.begin(), v.end(), 4);
    std::cout << *it << std::endl;                         // 4

    int n = std::count(v.begin(), v.end(), 1);
    std::cout << "count of 1 = " << n << std::endl;        // 2
    return 0;
}

易错点

  • remove 不真正删除元素,只把目标元素移到末尾并返回新逻辑结尾,需配合 eraseerase-remove 惯用法
  • sort 不稳定;需要稳定排序用 stable_sort
  • 算法要求迭代器类型匹配(sort 需要随机访问迭代器,不能用于 list)
cpp 复制代码
#include <algorithm>
#include <iostream>
#include <vector>

int main() {
    std::vector<int> v = {1, 2, 3, 2, 4, 2, 5};
    // erase-remove 惯用法:真正删除所有 2
    v.erase(std::remove(v.begin(), v.end(), 2), v.end());
    for (int x : v) std::cout << x << ' ';   // 1 3 4 5
    std::cout << std::endl;
    return 0;
}

9.2 函数对象(仿函数)

重载了 operator() 的类,可像函数一样调用,还能携带状态。

cpp 复制代码
#include <algorithm>
#include <iostream>
#include <vector>

class GreaterThan {
    int threshold_;
public:
    // explicit 禁止隐式类型转换,只允许显式调用
    explicit GreaterThan(int t) : threshold_(t) {}
    bool operator()(int x) const { return x > threshold_; }
};

int main() {
    std::vector<int> v = {5, 12, 3, 18, 7};
    int cnt = std::count_if(v.begin(), v.end(), GreaterThan(10));
    std::cout << ">10 count: " << cnt << std::endl;   // 2
    return 0;
}

9.3 lambda 表达式:就地匿名函数对象

C++11 引入,语法:[捕获列表](参数) -> 返回类型 { 函数体 }。本质是编译器生成的匿名函数对象。

cpp 复制代码
#include <algorithm>
#include <iostream>
#include <vector>

int main() {
    std::vector<int> v = {5, 12, 3, 18, 7};
    int threshold = 10;

    // [threshold]:按值捕获;[&threshold]:按引用捕获
    auto cnt = std::count_if(v.begin(), v.end(),
                             [threshold](int x) { return x > threshold; });
    std::cout << cnt << std::endl;   // 2

    // 排序时自定义规则
    std::sort(v.begin(), v.end(), [](int a, int b) { return a > b; });
    for (int x : v) std::cout << x << ' ';   // 18 12 7 5 3
    std::cout << std::endl;
    return 0;
}

捕获方式速查

写法 含义
[] 不捕获
[x] 按值捕获 x
[&x] 按引用捕获 x
[=] 按值捕获所有用到的外部变量
[&] 按引用捕获所有
[=, &x] 除 x 按引用外,其余按值

注意:按引用捕获的 lambda 在引用对象销毁后调用是悬垂引用;传给异步任务、回调时尤其小心,优先按值捕获。

9.4 算法与 lambda 配合是日常主力

cpp 复制代码
#include <algorithm>
#include <iostream>
#include <string>
#include <vector>

int main() {
    std::vector<std::string> names = {"alice", "bob", "carol"};
    std::sort(names.begin(), names.end(),
              [](const std::string &a, const std::string &b) {
                  return a.size() < b.size();   // 按长度排
              });
    for (const auto &n : names) std::cout << n << ' ';  // bob alice carol
    std::cout << std::endl;
    return 0;
}

第10章 模板与泛型编程

10.1 函数模板

让函数对"任何类型"生效,编译期由编译器生成具体版本。

cpp 复制代码
#include <iostream>
#include <string>

template <typename T>
T max_of(T a, T b) {
    return a > b ? a : b;
}

int main() {
    std::cout << max_of(3, 5) << std::endl;        // 5(推导为 int)
    std::cout << max_of(3.14, 2.71) << std::endl;  // 3.14(double)
    std::cout << max_of<std::string>("a", "b") << std::endl;  // 显式指定
    return 0;
}

编译期机制 :模板不是"函数",是"函数配方"。每次用新类型实例化,编译器生成一份新代码------这就是"零开销抽象"的来源,但也导致编译变慢、代码膨胀

10.2 类模板

cpp 复制代码
#include <iostream>
#include <string>

template <typename T>
class Box {
    T value_;
public:
    explicit Box(const T &v) : value_(v) {}
    T get() const { return value_; }
};

int main() {
    Box<int> bi(42);
    Box<std::string> bs("hi");
    std::cout << bi.get() << ' ' << bs.get() << std::endl;  // 42 hi
    return 0;
}

10.3 特化与偏特化

形式 示例 作用
全特化 template<> class Box<int> 对特定类型定制实现
偏特化 template<typename T> class Box<T*> 对"指针类型"定制
cpp 复制代码
#include <iostream>

template <typename T>
struct TypeTag { static const char *name() { return "generic"; } };

template <>
struct TypeTag<int> { static const char *name() { return "int"; } };  // 全特化

template <typename T>
struct TypeTag<T*> { static const char *name() { return "pointer"; } };  // 偏特化

int main() {
    std::cout << TypeTag<double>::name() << std::endl;  // generic
    std::cout << TypeTag<int>::name() << std::endl;     // int
    std::cout << TypeTag<int*>::name() << std::endl;    // pointer
    return 0;
}

10.4 变参模板(C++11)

接受任意数量、任意类型参数。

cpp 复制代码
#include <iostream>

void print() {}   // 终止条件

template <typename T, typename... Rest>
void print(T first, Rest... rest) {
    std::cout << first << ' ';
    print(rest...);   // 递归展开
}

int main() {
    print(1, "hello", 3.14, 'x');   // 1 hello 3.14 x
    std::cout << std::endl;
    return 0;
}

10.5 模板的常见坑

说明
两阶段查找 依赖参数的名称在实例化时才查找,行为可能出人意料
代码膨胀 实例化过多导致二进制变大
报错难读 模板错误信息又长又绕
声明与定义分离 模板定义放 .h 或 .hpp,不能只放 .cpp
重载/特化优先级 全特化 > 偏特化 > 主模板

工程经验:模板放头文件;减少模板嵌套;类型约束用 C++20 concept(或 SFINAE)。

模板是"编译期编程",很多面试题(如 type_traits、SFINAE)建立在此之上。

第11章 C++11 现代特性实战

C++11 是"新 C++"的开端。本章把零散的新特性整合成一套可上手的工具箱。

11.1 auto 与 decltype

cpp 复制代码
#include <iostream>
#include <vector>
#include <map>

int main() {
    auto x = 42;                       // int
    auto s = std::string("hi");        // std::string

    std::vector<std::map<std::string, int>> data;
    auto it = data.begin();            // 冗长类型让 auto 代劳

    decltype(x) y = 1;                 // y 的类型是 x 的类型(int)
    std::cout << y << std::endl;
    return 0;
}

建议 :能用 auto 就用,尤其迭代器、lambda、复杂模板类型。但别写 auto i = 0; 替代 int i(清晰性优先)。

11.2 范围 for:遍历的最简形态

cpp 复制代码
#include <iostream>
#include <vector>

int main() {
    std::vector<int> v = {1, 2, 3};
    for (int x : v) { std::cout << x; }       // 值拷贝
    for (const int &x : v) { std::cout << x; } // 只读引用,不拷贝
    for (int &x : v) { ++x; }                  // 可修改

    // 99% 场景用 const auto&
    for (const auto &x : v) { std::cout << x; }
    std::cout << std::endl;
    return 0;
}

11.3 初始化列表

cpp 复制代码
#include <initializer_list>
#include <iostream>
#include <vector>

class Foo {
    std::vector<int> vals_;
public:
    Foo(std::initializer_list<int> list) : vals_(list) {}
    void show() const {
        for (int v : vals_) std::cout << v << ' ';
        std::cout << std::endl;
    }
};

int main() {
    Foo f = {1, 2, 3, 4};   // 调用 initializer_list 构造
    f.show();               // 1 2 3 4
    std::vector<int> v{10, 20, 30};
    return 0;
}

11.4 结构化绑定(C++17)

cpp 复制代码
#include <iostream>
#include <map>
#include <string>

int main() {
    std::map<std::string, int> m = {{"a", 1}, {"b", 2}};
    for (const auto &[key, value] : m) {   // 结构化绑定
        std::cout << key << "=" << value << ' ';
    }
    std::cout << std::endl;   // a=1 b=2
    return 0;
}

11.5 enum class:强类型枚举

cpp 复制代码
#include <iostream>

enum class Color { Red, Green, Blue };   // 强类型
enum OldColor { Red, Green, Blue };      // 传统枚举(污染全局)

int main() {
    Color c = Color::Red;
    // if (c == 0) {}        // 错误:不能隐式转 int
    // if (c == OldColor::Red) {}  // 错误:不同类型

    int n = static_cast<int>(c);         // 需要显式转换
    std::cout << n << std::endl;         // 0
    return 0;
}

11.6 nullptr、override、final 小结

关键字 作用
nullptr 真正的空指针常量
override 显式声明重写基类虚函数,编译器校验
final 禁止类被继承 / 虚函数被重写
cpp 复制代码
class Base {
public:
    virtual void f() {}
};
class Derived : public Base {
public:
    void f() override {}   // 写错签名,编译器直接报错
};
class Final final : public Base {};   // 不能再被继承
// class X : public Final {};         // 错误

11.7 std::optional、std::variant、std::any(C++17)

类型 用途 类比
std::optional<T> 可能没有值 更安全的"空值"
std::variant<T1,T2> 类型安全的联合体 增强版 union
std::any 任意类型容器 void* 的类型安全版
cpp 复制代码
#include <cstdlib>
#include <iostream>
#include <optional>

std::optional<int> parse(const char *s) {
    char *end = nullptr;
    int v = std::strtol(s, &end, 10);
    if (end != s && *end == '\0') return v;   // 解析成功
    return std::nullopt;                       // 失败:无值
}

int main() {
    auto a = parse("123");
    if (a) std::cout << *a << std::endl;       // 123
    auto b = parse("abc");
    if (!b) std::cout << "parse failed\n";     // parse failed
    return 0;
}

11.8 其他高频特性速览

  • std::array<T,N>:定长数组,带边界检查(.at()
  • std::function:可保存任意可调用对象(函数、lambda、仿函数)
  • std::bind:绑定参数生成新可调用对象(C++11,lambda 通常更好用)
  • std::chrono:时间库(steady_clockduration
  • 属性:[[nodiscard]](C++17,忽略返回值告警)、[[maybe_unused]]
cpp 复制代码
#include <iostream>
#include <functional>

int add(int a, int b) { return a + b; }

int main() {
    std::function<int(int, int)> f = add;
    f = [](int a, int b) { return a * b; };   // 可重新赋值 lambda
    std::cout << f(3, 4) << std::endl;        // 12
    return 0;
}

第12章 并发编程

12.1 线程基础:std::thread

cpp 复制代码
#include <iostream>
#include <thread>

void worker(int id) {
    std::cout << "thread " << id << " running\n";
}

int main() {
    std::thread t(worker, 1);   // 启动线程
    t.join();                   // 等待线程结束(必须 join 或 detach)
    std::cout << "main done\n";
    return 0;
}

注意

  • 线程对象析构时若仍 joinable,程序 std::terminate------要么 join,要么 detach
  • 线程间默认不共享变量,共享需同步机制
  • std::cout 并发输出可能交错,示例仅为演示

12.2 互斥锁:std::mutex 与 RAII 锁

cpp 复制代码
#include <iostream>
#include <mutex>
#include <thread>
#include <vector>

std::mutex mtx;
int counter = 0;

void increment(int times) {
    for (int i = 0; i < times; ++i) {
        std::lock_guard<std::mutex> lock(mtx);   // 构造加锁,析构解锁(RAII)
        ++counter;
    }
}

int main() {
    std::vector<std::thread> threads;
    for (int i = 0; i < 4; ++i)
        threads.emplace_back(increment, 100000);

    for (auto &t : threads) t.join();
    std::cout << "counter = " << counter << std::endl;   // 400000
    return 0;
}
锁类型 特点
std::mutex 基础互斥锁
std::lock_guard RAII 封装,构造加锁、析构解锁,不可手动控制
std::unique_lock 更灵活,可手动 lock/unlock,可延迟加锁
std::recursive_mutex 同一线程可重复加锁
std::shared_mutex(C++17) 读写锁:多读单写

12.3 条件变量:线程间通知

cpp 复制代码
#include <chrono>
#include <condition_variable>
#include <iostream>
#include <mutex>
#include <thread>

std::mutex mtx;
std::condition_variable cv;
bool ready = false;

void waiter() {
    std::unique_lock<std::mutex> lock(mtx);
    cv.wait(lock, [] { return ready; });   // 循环检查条件,避免虚假唤醒
    std::cout << "waiter: notified\n";
}

void notifier() {
    std::this_thread::sleep_for(std::chrono::milliseconds(100));
    {
        std::lock_guard<std::mutex> lock(mtx);
        ready = true;
    }
    cv.notify_one();   // 唤醒一个等待线程
}

int main() {
    std::thread t1(waiter);
    std::thread t2(notifier);
    t1.join();
    t2.join();
    return 0;
}

条件变量使用铁律

  1. 必须配合 unique_lock(wait 需要解锁-等待-重锁的能力)
  2. wait 的谓词版本自动处理虚假唤醒(spurious wakeup)
  3. 修改变量要在持锁状态下,通知可以解锁后做

12.4 原子操作:std::atomic

cpp 复制代码
#include <atomic>
#include <iostream>
#include <thread>
#include <vector>

std::atomic<int> counter{0};   // 无锁递增(对整型通常编译为原子指令)

void inc(int times) {
    for (int i = 0; i < times; ++i)
        counter.fetch_add(1);   // 或 ++counter
}

int main() {
    std::vector<std::thread> threads;
    for (int i = 0; i < 8; ++i) threads.emplace_back(inc, 100000);
    for (auto &t : threads) t.join();
    std::cout << counter << std::endl;   // 800000
    return 0;
}

atomic vs mutex:计数、标志等简单操作用 atomic(无锁、更快);复杂临界区用 mutex。

12.5 async、future、promise:异步任务

cpp 复制代码
#include <future>
#include <iostream>

int compute(int n) {
    int sum = 0;
    for (int i = 1; i <= n; ++i) sum += i;
    return sum;
}

int main() {
    std::future<int> result = std::async(std::launch::async, compute, 100);
    std::cout << "doing other work...\n";
    std::cout << "sum = " << result.get() << std::endl;   // 5050,阻塞等待结果
    return 0;
}
组件 作用
std::async 异步执行函数,返回 future
std::future<T> 获取异步结果(.get() 阻塞等待)
std::promise<T> 手动设置 future 的值
std::packaged_task 把可调用对象包装成可获取结果的异步任务

12.6 并发常见坑

后果 对策
数据竞争(无锁共享变量) 未定义行为 加锁 / atomic
死锁(多锁加锁顺序不一致) 程序卡死 固定加锁顺序 / std::lock 一次锁多个
条件变量虚假唤醒 逻辑错误 wait 用谓词版本
在析构线程对象前没 join terminate RAII 封装或确保 join
锁粒度太大 性能下降 缩小临界区

死锁示例:两个线程各持一把锁,互相等对方的锁。

cpp 复制代码
#include <chrono>
#include <mutex>
#include <thread>

std::mutex m1, m2;

void f1() {
    std::lock_guard<std::mutex> a(m1);
    std::this_thread::sleep_for(std::chrono::milliseconds(1));
    std::lock_guard<std::mutex> b(m2);   // 等 m2
}
void f2() {
    std::lock_guard<std::mutex> a(m2);
    std::this_thread::sleep_for(std::chrono::milliseconds(1));
    std::lock_guard<std::mutex> b(m1);   // 等 m1 → 互相等待,死锁
}

解法 :所有线程按相同顺序加锁(先 m1 后 m2),或用 std::lock(m1, m2) 一次性获取。

第13章 异常处理与异常安全

13.1 try/catch 基础

cpp 复制代码
#include <iostream>
#include <stdexcept>

int divide(int a, int b) {
    if (b == 0)
        throw std::runtime_error("divide by zero");   // 抛出异常
    return a / b;
}

int main() {
    try {
        std::cout << divide(10, 2) << std::endl;      // 5
        std::cout << divide(10, 0) << std::endl;      // 抛异常
    } catch (const std::exception &e) {               // 捕获
        std::cout << "caught: " << e.what() << std::endl;
    }
    return 0;
}

异常传播 :抛出后在调用栈逐层向上找 catch,找不到则 std::terminate 终止程序。栈上对象在异常传播时会被自动析构------这是 RAII 能保证资源释放的根本。

13.2 异常规格与 noexcept

语法 含义
void f(); 可能抛异常
void f() noexcept; 承诺不抛异常
void f() noexcept(true/false); 条件式

noexcept 意义:

  1. 契约:告诉调用方安全
  2. 优化:std::vector 扩容时,只有移动构造是 noexcept 才用它(否则退化为拷贝)
  3. 若 noexcept 函数真的抛出异常 → std::terminate,程序终止
cpp 复制代码
#include <iostream>
#include <type_traits>
#include <utility>

class Data {
    int *p_;
public:
    Data() : p_(new int[100]) {}
    Data(Data &&other) noexcept : p_(other.p_) {   // 移动构造标记 noexcept
        other.p_ = nullptr;
    }
    ~Data() { delete[] p_; }
    Data(const Data &) = delete;
    Data &operator=(const Data &) = delete;
};

int main() {
    std::cout << std::boolalpha
              << std::is_nothrow_move_constructible<Data>::value
              << std::endl;   // true
    return 0;
}

13.3 异常安全三级别

级别 含义 例子
基本保证 不泄漏资源,但对象状态可能被修改 元素部分插入成功
强保证 操作要么全成功,要么回到原状态 vector 的 push_back(拷贝时)
不抛保证 绝不抛异常 简单 getter、int 运算

核心原则

  • 资源用 RAII 管理(智能指针、锁),异常发生时自动释放
  • 不在析构函数抛异常(析构默认 noexcept)
  • 构造函数中失败:对象不会被构造出来,已构造的成员会被正确析构

13.4 异常 vs 错误码

对比 异常 错误码
传播 自动沿栈传播 手动逐层传递
遗漏处理 程序终止(安全失败) 静默忽略(风险)
性能 无异常时零开销,抛出时慢 恒定
适用 构造失败、跨多层错误 频繁调用的轻量错误

工程建议 :关键路径避免大量 try/catch;预期内的可恢复错误考虑返回 std::optional/expected;异常用于真正异常的情况。


第14章 工程实践与常见坑

14.1 头文件与编译

  • 头文件卫士#pragma once(现代,非标准但全编译器支持)或 #ifndef XXX_H
  • 头文件放声明,源文件放定义;模板放头文件
  • 减少头文件依赖:用前置声明(class A;)、PImpl 惯用法
  • 避免在头文件 using namespace std;
cpp 复制代码
// example.h
#pragma once
class Example {
public:
    Example();
    int get() const;
private:
    int value_;
};

// example.cpp
#include "example.h"
Example::Example() : value_(42) {}
int Example::get() const { return value_; }

14.2 const 正确性

  • 能加 const 就加:入参 const T&、成员函数 const、局部变量 const
  • const 是自文档化的契约,也帮助编译器优化

14.3 常见坑清单

说明
浅拷贝/双重释放 含指针类不实现拷贝控制 → 用智能指针成员
悬垂引用 返回局部变量引用、引用已销毁对象
迭代器失效 修改容器后继续用旧迭代器
对象切片 基类值接收派生类对象
delete[]delete 混用 未定义行为
自赋值 拷贝赋值不检查 this == &other
除零 / 越界 不检查就访问
内存泄漏 new 了不 delete → 智能指针
虚析构缺失 多态删除派生类对象泄漏
死锁 / 数据竞争 并发共享无同步

14.4 性能要点

建议 原理
传参用 const T& 避免拷贝
返回用 std::move(或靠 NRVO) 避免多余拷贝
容器预留 reserve 减少扩容
emplace_back 而非 push_back 就地构造,少一次拷贝
循环遍历用引用 避免大对象拷贝
避免深拷贝大数据 用移动 / 共享
cpp 复制代码
#include <iostream>
#include <string>
#include <vector>

int main() {
    std::vector<std::string> v;
    v.reserve(3);                              // 预留容量
    v.emplace_back("hello");                   // 就地构造
    v.emplace_back("world");
    v.emplace_back("!");
    for (const auto &s : v)                    // const 引用遍历
        std::cout << s << ' ';
    std::cout << std::endl;
    return 0;
}

14.5 编码规范建议

  • 命名:类名大驼峰 StudentManager,变量/函数小驼峰或下划线 student_name/getName
  • 私有成员建议带后缀 _name_,一眼区分成员与局部变量
  • 一行一声明,函数职责单一
  • auto 但别滥用;类型明显时写具体类型更清晰
  • 提交前过一遍编译器告警(-Wall -Wextra

第15章 面试考点速查表

考点 一句话答案 对应章节
引用和指针区别 引用必须初始化、不能改绑、无空引用 2.1
为什么 C++ 能重载 C 不能 名字修饰(name mangling) 2.2
NULL vs nullptr nullptr 是类型安全的空指针常量 2.5
初始化列表为什么必须 const/引用成员只能初始化列表 3.2
析构函数为什么 virtual 基类指针 delete 派生类要调派生析构 4.6
构造/析构为什么不能虚构造 构造时虚表未完整,需先基类后派生 4.6
虚函数原理 vptr 指向 vtable,间接调用 4.4
对象切片 值拷贝派生类到基类只留基类部分 4.7
new 和 malloc 区别 new 调构造、类型安全、失败抛异常 6.1
内存泄漏怎么防 RAII + 智能指针 6.3/6.4
shared_ptr 循环引用 用 weak_ptr 打破 6.5
三/五法则 管资源就需自定义拷贝+析构 7.3
std::move vs forward move 转右值;forward 条件转发 7.5
vector 扩容原理 分配新内存→移动→释放,均摊 O(1) 8.2
迭代器失效 注意修改容器后迭代器失效规则 8.4
map vs unordered_map 红黑树 O(logn) vs 哈希 O(1) 8.5
erase-remove 惯用法 remove 逻辑删除 + erase 真正删 9.1
模板为何放头文件 模板按需实例化,需在编译单元可见定义 10.5
移动构造为何 noexcept vector 扩容用它才安全高效 13.2
死锁怎么解决 统一加锁顺序 / std::lock 12.6
条件变量为何用谓词 防虚假唤醒 12.3
异常安全级别 基本/强/不抛三级别 13.3

结语

C++ 从 C 而来,却远不止"C 的扩展"。它让你同时拥有底层控制力与高层抽象力,也要求你同时理解两者------这正是 C++ 工程师的稀缺价值。

本篇你应带走的核心资产:

  1. RAII 思维:资源生命周期绑定对象生命周期,智能指针是标准答案
  2. 多态原理:vtable 的动态分派机制
  3. 移动语义:性能优化的底层钥匙
  4. STL 三件套:容器 + 迭代器 + 算法,写代码先想"STL 有没有现成的"
  5. 并发基础:锁、原子、条件变量、future

下一篇进入 数据结构与算法,把 C/C++ 底子接上"解题能力"。我们到时候见。


本系列文章持续更新中,欢迎收藏追更。

相关推荐
kyle~19 分钟前
x86 汇编LOCK前缀 --- 硬件架构向软件提供的核心同步原语
汇编·c++·性能优化·硬件架构·实时系统
Escalating_xu2 小时前
【C++入门基础(下)】默认参数、函数重载、引用、inline 与 nullptr
android·c++·redis
艾莉丝努力练剑2 小时前
【AI大模型接入SDK】Provider分析与实现
c++·人工智能·学习·面试·大模型·llm
m0_5474866610 小时前
《JavaScript核心原理 》全套PPT课件2026
开发语言·javascript·ecmascript
老当益壮梁奶奶10 小时前
Linux软件编程学习笔记(七):线程分离与线程间通信详解
linux·c语言·c++·笔记·学习
MC皮蛋侠客10 小时前
Tauri 2.x 系列(五):调用系统 API——官方插件、Rust crate 与原生能力
开发语言·后端·rust
布莱克60511 小时前
软中断和硬中断的区别
开发语言·操作系统
whcyhhh11 小时前
头歌实践教学平台:数据科学与大数据技术导论(十二)
大数据·开发语言·python·数据清洗
lzhdim12 小时前
13、JavaScript事件循环机制 - JavaScript学习系列文章
开发语言·前端·javascript·学习·ecmascript