上一篇完成了 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*)0 或 0),避免重载歧义。
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++ 里 class 和 struct 几乎一样,唯一默认差异:访问权限。
| 关键字 | 默认访问权限 | 默认继承方式 |
|---|---|---|
| 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 内部是连续数组,记录三个指针:start、finish(已用末尾)、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[] 在键不存在时会插入默认值 。只查询用 find 或 at。
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不真正删除元素,只把目标元素移到末尾并返回新逻辑结尾,需配合erase(erase-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_clock、duration)- 属性:
[[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;
}
条件变量使用铁律:
- 必须配合
unique_lock(wait 需要解锁-等待-重锁的能力) wait的谓词版本自动处理虚假唤醒(spurious wakeup)- 修改变量要在持锁状态下,通知可以解锁后做
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 意义:
- 契约:告诉调用方安全
- 优化:
std::vector扩容时,只有移动构造是noexcept才用它(否则退化为拷贝) - 若 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++ 工程师的稀缺价值。
本篇你应带走的核心资产:
- RAII 思维:资源生命周期绑定对象生命周期,智能指针是标准答案
- 多态原理:vtable 的动态分派机制
- 移动语义:性能优化的底层钥匙
- STL 三件套:容器 + 迭代器 + 算法,写代码先想"STL 有没有现成的"
- 并发基础:锁、原子、条件变量、future
下一篇进入 数据结构与算法,把 C/C++ 底子接上"解题能力"。我们到时候见。
本系列文章持续更新中,欢迎收藏追更。
