写在前面:C++是我学编程以来觉得"又爱又恨"的一门语言。爱它是因为它性能无敌------操作系统、游戏引擎、高频交易、数据库内核、自动驾驶,这些对性能要求极致的领域几乎都是C++的天下。恨它是因为它太复杂了------指针、引用、模板、智能指针、移动语义、多线程......每一个都能让你debug到凌晨三点。这篇文章把C++的核心知识体系完整梳理了一遍,从基础语法到现代C++20,从面向对象到模板元编程,从STL到多线程并发,面试常问的、工作中真正用到的、初学者最容易踩坑的地方,我都尽量讲透了。全文大概两万多字,建议先收藏再看,当字典用也行。

文章目录
-
- 一、C++到底学什么?先给你一张全景图
- 二、基础语法:C++比C多了什么?
-
- [2.1 引用:C++独有的语法糖](#2.1 引用:C++独有的语法糖)
- [2.2 const的四种用法](#2.2 const的四种用法)
- [2.3 auto与decltype(C++11)](#2.3 auto与decltype(C++11))
- 三、面向对象:C++的核心竞争力
-
- [3.1 类与对象:封装](#3.1 类与对象:封装)
- [3.2 继承与多态:虚函数的奥秘](#3.2 继承与多态:虚函数的奥秘)
- [3.3 虚函数表(vtable):多态的底层原理](#3.3 虚函数表(vtable):多态的底层原理)
- [3.4 多重继承与菱形问题](#3.4 多重继承与菱形问题)
- 四、模板与泛型编程
-
- [4.1 函数模板](#4.1 函数模板)
- [4.2 类模板](#4.2 类模板)
- 五、STL标准模板库:C++的瑞士军刀
-
- [5.1 容器](#5.1 容器)
- [5.2 迭代器与常用算法](#5.2 迭代器与常用算法)
- 六、智能指针:告别内存泄漏
-
- [6.1 为什么需要智能指针](#6.1 为什么需要智能指针)
- [6.2 三种智能指针](#6.2 三种智能指针)
- 七、现代C++特性(C++11/14/17/20)
-
- [7.1 Lambda表达式](#7.1 Lambda表达式)
- [7.2 移动语义与右值引用](#7.2 移动语义与右值引用)
- [7.3 C++17/20 新特性](#7.3 C++17/20 新特性)
- 八、多线程与并发
-
- [8.1 std::thread基础](#8.1 std::thread基础)
- [8.2 async与future](#8.2 async与future)
- [8.3 原子操作](#8.3 原子操作)
- 九、RAII:C++的核心编程范式
- 十、面试高频考点汇总
-
- Q1:C++的虚函数是怎么实现的?
- Q2:智能指针的选择原则?
- Q3:new/delete和malloc/free的区别?
- Q4:移动语义解决了什么问题?
- [Q5:STL vector扩容机制?](#Q5:STL vector扩容机制?)
- Q6:C++中的RAII是什么?
- 十一、C++学习路线与资源推荐
-
- [11.1 推荐学习路线](#11.1 推荐学习路线)
- [11.2 推荐学习资源](#11.2 推荐学习资源)
- 写在最后
- 参考资料
一、C++到底学什么?先给你一张全景图
C++的知识体系非常庞大,这也是它被戏称为"大而全"语言的原因。但核心知识可以归纳为以下八大板块:
| 板块 | 核心内容 | 对应文章章节 | 难度 |
|---|---|---|---|
| 基础语法与类型系统 | 基本类型、auto、decltype、引用、const、constexpr、作用域与命名空间 | 第二章 | ⭐⭐ |
| 指针、引用与内存管理 | 指针与数组、智能指针(unique/shared/weak)、new/delete、内存模型 | 第六章 | ⭐⭐⭐ |
| 面向对象编程 | 封装、继承、多态、虚函数与vtable、运算符重载、多重继承 | 第三章 | ⭐⭐⭐ |
| 模板与泛型编程 | 函数模板、类模板、模板特化、SFINAE、C++20 Concepts | 第四章 | ⭐⭐⭐⭐ |
| STL标准模板库 | 容器(vector/map/set)、迭代器、算法、string/string_view | 第五章 | ⭐⭐⭐ |
| 现代C++特性(11/14/17/20) | Lambda表达式、移动语义、optional/variant/any、结构化绑定、协程 | 第七章 | ⭐⭐⭐⭐ |
| 多线程与并发编程 | thread、mutex、async/future、atomic、无锁编程 | 第八章 | ⭐⭐⭐⭐⭐ |
| 异常处理与RAII | 异常机制、RAII编程范式、资源管理、智能指针原理 | 第九章 | ⭐⭐⭐ |
学习建议:建议按表格顺序学习,前面的板块是后面的基础。标⭐⭐的先掌握,标⭐⭐⭐⭐⭐的可以放到后面深入研究。
二、基础语法:C++比C多了什么?
2.1 引用:C++独有的语法糖
引用是C++最重要的特性之一,很多人搞不清引用和指针的区别。
cpp
#include <iostream>
// 值传递:修改不影响原变量
void swap_by_value(int a, int b) {
int temp = a; a = b; b = temp;
// 函数结束后,外面的a和b不受影响
}
// 指针传递:通过地址修改原变量
void swap_by_ptr(int *a, int *b) {
int temp = *a; *a = *b; *b = temp;
}
// 引用传递:最简洁的方式
void swap_by_ref(int &a, int &b) {
int temp = a; a = b; b = temp;
}
int main() {
int x = 3, y = 5;
swap_by_value(x, y);
std::cout << "值传递后: x=" << x << ", y=" << y << std::endl; // x=3, y=5(没变)
swap_by_ptr(&x, &y);
std::cout << "指针传递后: x=" << x << ", y=" << y << std::endl; // x=5, y=3
swap_by_ref(x, y);
std::cout << "引用传递后: x=" << x << ", y=" << y << std::endl; // x=3, y=5
return 0;
}
引用 vs 指针对比:
| 特性 | 引用(&) | 指针(*) |
|---|---|---|
| 必须初始化 | ✅ 声明时必须绑定 | ❌ 可以先声明后赋值 |
| 可以为空 | ❌ 不存在空引用 | ✅ 可以是nullptr |
| 可以重新绑定 | ❌ 一生只绑定一个对象 | ✅ 可以指向不同对象 |
| 语法 | 使用时像普通变量 | 需要解引用*p |
| sizeof | 绑定对象的大小 | 指针本身的大小(8字节) |
| 底层实现 | 实际上是指针 | 就是指针 |
踩坑提醒 :引用必须在声明时初始化,且一旦绑定就不能更改。但很多人不知道,引用在底层其实也是用指针实现的,编译器帮你做了自动解引用。所以引用传递本质上还是传地址,只是语法更干净。面试官经常问"引用和指针的区别",记住上面这张表就够了。
2.2 const的四种用法
cpp
#include <iostream>
int main() {
int val = 42;
// 1. const变量
const int MAX = 100;
// MAX = 200; // 编译错误
// 2. const指针(4种组合,面试必问)
const int *p1 = &val; // 指向const int的指针(值不能改,指针能改)
// *p1 = 43; // 编译错误
p1 = nullptr; // 可以
int *const p2 = &val; // const指针(值能改,指针不能改)
*p2 = 43; // 可以
// p2 = nullptr; // 编译错误
const int *const p3 = &val; // 都不能改
// 3. const引用(常用于函数参数,避免拷贝同时不允许修改)
// void print(const std::string &s); // 传引用避免拷贝,const保证不修改
// 4. const成员函数(承诺不修改对象状态)
// class MyClass { int getValue() const; };
// 5. constexpr(C++11,编译期常量)
constexpr int SIZE = 10;
int arr[SIZE]; // 合法,SIZE是编译期常量
// const int SIZE2 = 10; // 也是编译期常量(初始化为字面量时)
return 0;
}
2.3 auto与decltype(C++11)
cpp
#include <iostream>
#include <vector>
#include <type_traits>
int main() {
// auto:编译器自动推导类型
auto x = 42; // int
auto pi = 3.14; // double
auto name = "hello"; // const char*
auto list = {1, 2, 3}; // std::initializer_list<int>
// auto与引用
int a = 10;
auto &ref = a; // int&(auto推导为int,加上&就是引用)
auto ptr = &a; // int*(auto推导为int*)
// decltype:获取表达式的类型
int b = 20;
decltype(a) c = 30; // c的类型和a一样,都是int
decltype(a + b) d = 50; // d的类型是int(a+b的结果类型)
decltype(a * 1.5) e = 0; // e的类型是double
// auto vs decltype的区别
const int ci = 10;
auto f = ci; // f是int(auto忽略const和引用)
decltype(ci) g = 20; // g是const int(decltype保留const)
// C++14: auto返回类型
auto add = [](int a, int b) -> auto { return a + b; }; // C++14可省略->auto
std::cout << "x=" << x << ", pi=" << pi << std::endl;
return 0;
}
三、面向对象:C++的核心竞争力
3.1 类与对象:封装
cpp
#include <iostream>
#include <string>
#include <stdexcept>
class BankAccount {
private:
std::string owner;
double balance;
public:
// 构造函数(初始化列表,比在函数体内赋值更高效)
BankAccount(const std::string& name, double initial)
: owner(name), balance(initial) {
if (initial < 0) throw std::invalid_argument("初始余额不能为负");
}
// 析构函数(virtual析构函数很重要!)
virtual ~BankAccount() = default;
// 存款
void deposit(double amount) {
if (amount <= 0) throw std::invalid_argument("存款金额必须大于0");
balance += amount;
}
// 取款
bool withdraw(double amount) {
if (amount <= 0 || amount > balance) return false;
balance -= amount;
return true;
}
// const成员函数:不修改对象状态
double getBalance() const { return balance; }
const std::string& getOwner() const { return owner; }
// 友元函数(可以访问私有成员)
friend std::ostream& operator<<(std::ostream& os, const BankAccount& acc);
};
// 运算符重载:输出流
std::ostream& operator<<(std::ostream& os, const BankAccount& acc) {
os << acc.owner << "的余额: " << acc.balance;
return os;
}
int main() {
BankAccount acc("张三", 1000.0);
acc.deposit(500.0);
acc.withdraw(200.0);
std::cout << acc << std::endl; // 张三的余额: 1300
return 0;
}
3.2 继承与多态:虚函数的奥秘
cpp
#include <iostream>
#include <vector>
#include <memory>
// 基类:虚析构函数!
class Animal {
protected:
std::string name;
public:
Animal(const std::string& n) : name(n) {}
// 虚函数:运行时多态
virtual void speak() const {
std::cout << name << "发出声音" << std::endl;
}
// 纯虚函数:使Animal成为抽象类
virtual void move() const = 0;
// 虚析构函数:确保正确释放派生类对象(面试必问!)
virtual ~Animal() { std::cout << "~Animal()" << std::endl; }
};
class Dog : public Animal {
public:
Dog(const std::string& n) : Animal(n) {}
// override关键字:显式重写(C++11),编译器检查是否真的重写了
void speak() const override {
std::cout << name << ": 汪汪汪!" << std::endl;
}
void move() const override {
std::cout << name << "在跑" << std::endl;
}
// final关键字:禁止进一步重写
void fetch() const final {
std::cout << name << "在捡球" << std::endl;
}
~Dog() override { std::cout << "~Dog()" << std::endl; }
};
class Cat : public Animal {
public:
Cat(const std::string& n) : Animal(n) {}
void speak() const override {
std::cout << name << ": 喵喵喵!" << std::endl;
}
void move() const override {
std::cout << name << "在跳" << std::endl;
}
~Cat() override { std::cout << "~Cat()" << std::endl; }
};
int main() {
// 多态:基类指针指向派生类对象
std::vector<std::unique_ptr<Animal>> zoo;
zoo.push_back(std::make_unique<Dog>("旺财"));
zoo.push_back(std::make_unique<Cat>("咪咪"));
zoo.push_back(std::make_unique<Dog>("大黄"));
for (const auto& animal : zoo) {
animal->speak(); // 运行时决定调用哪个版本
animal->move();
}
// 输出:
// 旺财: 汪汪汪!
// 旺财在跑
// 咪咪: 喵喵喵!
// 咪咪在跳
// 大黄: 汪汪汪!
// 大黄在跑
// unique_ptr自动释放,不需要手动delete
return 0;
}
面试高频 :面试官几乎必问"为什么析构函数要声明为virtual?"如果不声明为virtual,通过基类指针delete派生类对象时,只会调用基类的析构函数,派生类的资源不会被释放,导致内存泄漏 。这条规则:只要类中有虚函数,析构函数就必须是virtual的。
3.3 虚函数表(vtable):多态的底层原理
虚函数表原理:
每个有虚函数的类都有一个虚函数表(vtable):
┌─────────────────────────────────────┐
│ Dog的虚函数表 (vtable) │
├─────────────────────────────────────┤
│ [0] Dog::speak() 的地址 │
│ [1] Dog::move() 的地址 │
│ [2] ~Dog() 的地址 │
└─────────────────────────────────────┘
每个对象有一个指向虚函数表的指针(vptr):
┌─────────────────────────────────────┐
│ Dog对象 │
├─────────────────────────────────────┤
│ vptr ─────────────→ vtable │
│ name: "旺财" │
│ (Animal的成员) │
└─────────────────────────────────────┘
调用 animal->speak() 的过程:
1. 通过对象的 vptr 找到 vtable
2. 在 vtable 中找到 speak() 的地址
3. 调用对应的函数
虚函数调用的开销:
- 多一次间接寻址(查vtable)
- 通常影响很小(< 5%)
- 但在极端性能敏感的场景下需要考虑
3.4 多重继承与菱形问题
cpp
#include <iostream>
// 菱形继承问题
class Animal {
public:
int age;
Animal() : age(0) { std::cout << "Animal()" << std::endl; }
};
// 虚继承:解决菱形问题
class Mammal : virtual public Animal {
public:
Mammal() { std::cout << "Mammal()" << std::endl; }
};
class Bird : virtual public Animal {
public:
Bird() { std::cout << "Bird()" << std::endl; }
};
class Bat : public Mammal, public Bird {
public:
Bat() { std::cout << "Bat()" << std::endl; }
};
int main() {
Bat bat;
bat.age = 5; // 不使用virtual继承时,这里会报错:age不明确
// 使用virtual继承后,只有一个Animal子对象,age是唯一的
return 0;
}
// 输出:
// Animal()
// Mammal()
// Animal() ← 不用virtual只调用一次
// Bird()
// Bat()
经验之谈 :多重继承在实际项目中用得很少,容易引入复杂性。C++核心指南的建议是:优先使用组合,谨慎使用继承,避免多重继承。如果确实需要多重继承,一定要用虚继承来避免菱形问题。
四、模板与泛型编程
4.1 函数模板
cpp
#include <iostream>
#include <string>
// 函数模板:一个定义,多种类型
template <typename T>
T getMax(T a, T b) {
return (a > b) ? a : b;
}
// 多个模板参数
template <typename T, typename U>
auto add(T a, U b) -> decltype(a + b) {
return a + b;
}
// 模板特化:为特定类型提供特殊实现
template <>
const char* getMax<const char*>(const char* a, const char* b) {
return (std::strcmp(a, b) > 0) ? a : b;
}
// C++11: 可变参数模板
void print() { std::cout << std::endl; }
template <typename T, typename... Args>
void print(T first, Args... rest) {
std::cout << first << " ";
print(rest...); // 递归展开
}
int main() {
std::cout << getMax(3, 5) << std::endl; // 5
std::cout << getMax(3.14, 2.72) << std::endl; // 3.14
std::cout << getMax("abc", "def") << std::endl; // def(使用特化版本)
std::cout << add(3, 1.5) << std::endl; // 4.5
print(1, "hello", 3.14, "world"); // 1 hello 3.14 world
return 0;
}
4.2 类模板
cpp
#include <iostream>
#include <stdexcept>
// 类模板:实现一个简单的动态数组
template <typename T>
class MyVector {
private:
T* data;
size_t size_;
size_t capacity_;
void resize(size_t newCapacity) {
T* newData = new T[newCapacity];
for (size_t i = 0; i < size_; i++) {
newData[i] = data[i];
}
delete[] data;
data = newData;
capacity_ = newCapacity;
}
public:
explicit MyVector(size_t initCapacity = 4)
: size_(0), capacity_(initCapacity) {
data = new T[capacity_];
}
~MyVector() { delete[] data; }
// 禁止拷贝(简化示例)
MyVector(const MyVector&) = delete;
MyVector& operator=(const MyVector&) = delete;
// 移动构造函数
MyVector(MyVector&& other) noexcept
: data(other.data), size_(other.size_), capacity_(other.capacity_) {
other.data = nullptr;
other.size_ = 0;
other.capacity_ = 0;
}
void push_back(const T& value) {
if (size_ >= capacity_) resize(capacity_ * 2);
data[size_++] = value;
}
// 移动语义版本的push_back
void push_back(T&& value) {
if (size_ >= capacity_) resize(capacity_ * 2);
data[size_++] = std::move(value);
}
T& operator[](size_t index) {
if (index >= size_) throw std::out_of_range("Index out of range");
return data[index];
}
const T& operator[](size_t index) const {
if (index >= size_) throw std::out_of_range("Index out of range");
return data[index];
}
size_t size() const { return size_; }
};
int main() {
MyVector<int> intVec;
intVec.push_back(10);
intVec.push_back(20);
intVec.push_back(30);
for (size_t i = 0; i < intVec.size(); i++) {
std::cout << intVec[i] << " "; // 10 20 30
}
std::cout << std::endl;
MyVector<std::string> strVec;
strVec.push_back("Hello");
strVec.push_back("C++");
strVec.push_back("Templates");
for (size_t i = 0; i < strVec.size(); i++) {
std::cout << strVec[i] << " "; // Hello C++ Templates
}
std::cout << std::endl;
return 0;
}
经验之谈:模板是C++泛型编程的基石,STL的全部容器和算法都是基于模板实现的。但模板的错误信息出了名的难读------一个简单的模板实例化错误可能输出几十行甚至上百行的编译错误。C++20引入的Concepts可以大幅改善这个问题。
五、STL标准模板库:C++的瑞士军刀
5.1 容器
cpp
#include <iostream>
#include <vector>
#include <map>
#include <unordered_map>
#include <set>
#include <deque>
#include <stack>
#include <queue>
#include <algorithm>
int main() {
// 1. vector:动态数组(最常用)
std::vector<int> vec = {3, 1, 4, 1, 5, 9};
vec.push_back(2);
vec.emplace_back(6); // C++11:原地构造,比push_back更高效
// 排序 + 去重
std::sort(vec.begin(), vec.end());
auto last = std::unique(vec.begin(), vec.end());
vec.erase(last, vec.end());
std::cout << "vector: ";
for (int v : vec) std::cout << v << " "; // 1 2 3 4 5 6 9
std::cout << std::endl;
// 2. map:有序键值对(红黑树)
std::map<std::string, int> ages;
ages["Alice"] = 20;
ages["Bob"] = 25;
ages["Charlie"] = 22;
// C++17结构化绑定
for (const auto& [name, age] : ages) {
std::cout << name << ": " << age << std::endl;
}
// 3. unordered_map:哈希表(O(1)查找)
std::unordered_map<std::string, int> scores;
scores["math"] = 95;
scores["english"] = 88;
// 4. set:自动排序且不重复
std::set<int> s = {5, 3, 1, 4, 2, 3}; // 自动去重+排序
std::cout << "set: ";
for (int v : s) std::cout << v << " "; // 1 2 3 4 5
std::cout << std::endl;
// 5. deque:双端队列
std::deque<int> dq = {1, 2, 3};
dq.push_front(0); // 头部插入
dq.push_back(4); // 尾部插入
// 6. priority_queue:优先队列(默认大顶堆)
std::priority_queue<int> pq;
pq.push(3); pq.push(1); pq.push(5); pq.push(2);
while (!pq.empty()) {
std::cout << pq.top() << " "; // 5 3 2 1
pq.pop();
}
std::cout << std::endl;
return 0;
}
STL容器选择指南:
| 场景 | 推荐容器 | 时间复杂度 | 说明 |
|---|---|---|---|
| 随机访问 | vector |
O(1) | 默认首选 |
| 频繁头尾操作 | deque |
O(1) | 双端队列 |
| 有序键值对 | map |
O(log n) | 红黑树 |
| 哈希键值对 | unordered_map |
O(1) | 最常用 |
| 有序不重复 | set |
O(log n) | 自动排序去重 |
| 优先队列 | priority_queue |
O(log n) | 堆实现 |
| 栈 | stack |
O(1) | LIFO |
| 队列 | queue |
O(1) | FIFO |
5.2 迭代器与常用算法
cpp
#include <iostream>
#include <vector>
#include <numeric>
#include <algorithm>
#include <functional>
int main() {
std::vector<int> vec = {5, 2, 8, 1, 9, 3, 7, 4, 6};
// 排序
std::sort(vec.begin(), vec.end()); // 升序
std::sort(vec.begin(), vec.end(), std::greater<int>()); // 降序
// 查找
auto it = std::find(vec.begin(), vec.end(), 5);
if (it != vec.end()) {
std::cout << "找到5,位置: " << std::distance(vec.begin(), it) << std::endl;
}
// 二分查找(必须先排序)
bool found = std::binary_search(vec.begin(), vec.end(), 7);
std::cout << "二分查找7: " << (found ? "找到" : "未找到") << std::endl;
// 统计
int count = std::count(vec.begin(), vec.end(), 3);
// 累加
int sum = std::accumulate(vec.begin(), vec.end(), 0);
std::cout << "总和: " << sum << std::endl;
// 最大值/最小值
auto [minIt, maxIt] = std::minmax_element(vec.begin(), vec.end());
std::cout << "最小: " << *minIt << ", 最大: " << *maxIt << std::endl;
// transform:对每个元素做变换
std::vector<int> doubled;
std::transform(vec.begin(), vec.end(), std::back_inserter(doubled),
[](int x) { return x * 2; });
// remove_if + erase(删除满足条件的元素)
vec.erase(
std::remove_if(vec.begin(), vec.end(), [](int x) { return x % 2 == 0; }),
vec.end()
);
// for_each
std::for_each(vec.begin(), vec.end(), [](int x) {
std::cout << x << " ";
});
std::cout << std::endl;
return 0;
}
六、智能指针:告别内存泄漏
6.1 为什么需要智能指针
cpp
// 传统方式:手动管理内存,容易泄漏
void traditional_way() {
int* p = new int(42);
// ... 如果这里抛异常或提前return,p就泄漏了
delete p;
}
// 智能指针:自动释放
void smart_way() {
std::unique_ptr<int> p = std::make_unique<int>(42);
// 不管怎么退出,p都会自动释放
}
6.2 三种智能指针
| 智能指针 | 所有权 | 可拷贝 | 使用场景 |
|---|---|---|---|
unique_ptr |
独占 | ❌ 不可拷贝,可移动 | 默认首选 |
shared_ptr |
共享(引用计数) | ✅ 可拷贝 | 多个对象共享同一资源 |
weak_ptr |
不拥有 | ✅ 可拷贝 | 打破shared_ptr循环引用 |
cpp
#include <iostream>
#include <memory>
class Node {
public:
int value;
std::shared_ptr<Node> next;
std::weak_ptr<Node> prev; // 用weak_ptr避免循环引用
Node(int v) : value(v) {
std::cout << "Node(" << value << ") 创建" << std::endl;
}
~Node() {
std::cout << "~Node(" << value << ") 销毁" << std::endl;
}
};
int main() {
// unique_ptr:独占所有权
{
auto p1 = std::make_unique<int>(42);
std::cout << "*p1 = " << *p1 << std::endl; // 42
auto p2 = std::move(p1); // 可以移动,不能拷贝
std::cout << "*p2 = " << *p2 << std::endl; // 42
} // p2自动释放
// shared_ptr:共享所有权
{
auto sp1 = std::make_shared<int>(100);
std::cout << "use_count: " << sp1.use_count() << std::endl; // 1
auto sp2 = sp1; // 引用计数+1
std::cout << "use_count: " << sp1.use_count() << std::endl; // 2
{
auto sp3 = sp1;
std::cout << "use_count: " << sp1.use_count() << std::endl; // 3
} // sp3离开作用域,引用计数-1
std::cout << "use_count: " << sp1.use_count() << std::endl; // 2
} // 所有shared_ptr离开作用域,内存释放
// weak_ptr:打破循环引用
{
auto n1 = std::make_shared<Node>(1);
auto n2 = std::make_shared<Node>(2);
n1->next = n2;
n2->prev = n1; // weak_ptr不增加引用计数
} // 正常释放,不会内存泄漏
return 0;
}
踩坑提醒 :
shared_ptr的循环引用是C++最常见的内存泄漏场景之一。比如双向链表、树结构的父子节点、观察者模式中的互相引用。解决方案:一方用shared_ptr,另一方用weak_ptr。另外,make_shared比直接new更高效(一次分配对象和控制块),推荐使用。
七、现代C++特性(C++11/14/17/20)
7.1 Lambda表达式
cpp
#include <iostream>
#include <vector>
#include <algorithm>
int main() {
std::vector<int> vec = {5, 2, 8, 1, 9, 3};
// Lambda基本语法:[捕获列表](参数) -> 返回类型 { 函数体 }
// 1. 最简Lambda
auto greet = []() { std::cout << "Hello!" << std::endl; };
greet();
// 2. 带参数
auto add = [](int a, int b) { return a + b; };
std::cout << "3 + 4 = " << add(3, 4) << std::endl;
// 3. 捕获外部变量
int threshold = 5;
auto count = std::count_if(vec.begin(), vec.end(),
[threshold](int x) { return x > threshold; });
std::cout << "大于" << threshold << "的个数: " << count << std::endl;
// 4. 捕获方式对比
int a = 10;
auto f1 = [a]() { return a; }; // 值捕获(拷贝)
auto f2 = [&a]() { return a; }; // 引用捕获
auto f3 = [=]() { return a; }; // 全部值捕获
auto f4 = [&]() { return a; }; // 全部引用捕获
auto f5 = [=, &a]() { return a; }; // 默认值捕获,a引用捕获
// 5. 泛型Lambda(C++20)
// auto print = [](const auto& x) { std::cout << x << " "; };
// print(42); // int
// print("hi"); // const char*
return 0;
}
7.2 移动语义与右值引用
cpp
#include <iostream>
#include <vector>
#include <string>
#include <utility>
class Buffer {
private:
int* data;
size_t size_;
public:
// 构造函数
explicit Buffer(size_t size) : size_(size), data(new int[size]) {
std::cout << "构造(size=" << size << ")" << std::endl;
}
// 析构函数
~Buffer() { delete[] data; std::cout << "析构" << std::endl; }
// 拷贝构造(深拷贝)
Buffer(const Buffer& other) : size_(other.size_), data(new int[other.size_]) {
std::memcpy(data, other.data, size_ * sizeof(int));
std::cout << "拷贝构造" << std::endl;
}
// 移动构造(转移资源,零拷贝)
Buffer(Buffer&& other) noexcept : data(other.data), size_(other.size_) {
other.data = nullptr;
other.size_ = 0;
std::cout << "移动构造(零拷贝!)" << std::endl;
}
// 移动赋值
Buffer& operator=(Buffer&& other) noexcept {
if (this != &other) {
delete[] data;
data = other.data;
size_ = other.size_;
other.data = nullptr;
other.size_ = 0;
}
std::cout << "移动赋值" << std::endl;
return *this;
}
size_t size() const { return size_; }
};
Buffer createBuffer() {
Buffer buf(1024);
return buf; // 触发移动构造(或NRVO优化)
}
int main() {
// std::move:将左值转换为右值引用
std::string str1 = "Hello, C++!";
std::string str2 = std::move(str1); // str1的资源转移给str2
std::cout << "str2: " << str2 << std::endl; // Hello, C++!
// str1现在是空字符串(已移动)
// 移动语义的实际应用
std::vector<Buffer> buffers;
buffers.push_back(createBuffer()); // 移动构造
buffers.emplace_back(2048); // 原地构造,更高效
return 0;
}
经验之谈 :移动语义是C++11最重要的特性之一。简单来说,拷贝是"复制一份",移动是"把资源偷过来"。对于管理大量资源的对象(大字符串、容器、文件句柄),移动比拷贝快几个数量级。
std::move本身不做任何移动,它只是把左值转换成右值引用,真正的移动由移动构造函数/移动赋值运算符完成。
7.3 C++17/20 新特性
cpp
#include <iostream>
#include <optional>
#include <variant>
#include <tuple>
#include <any>
int main() {
// C++17: optional(可能没有值)
std::optional<int> maybeInt;
std::cout << "has_value: " << maybeInt.has_value() << std::endl; // false
maybeInt = 42;
std::cout << "value: " << maybeInt.value() << std::endl; // 42
std::cout << "or: " << maybeInt.value_or(0) << std::endl; // 42
// C++17: variant(类型安全的union)
std::variant<int, double, std::string> v = "Hello";
std::cout << "variant: " << std::get<std::string>(v) << std::endl;
v = 3.14;
std::cout << "index: " << v.index() << std::endl; // 1(double)
// C++17: 结构化绑定
auto [key, value] = std::pair(1, "one");
std::cout << key << ": " << value << std::endl;
auto [x, y, z] = std::make_tuple(1, 2.0, "three");
std::cout << x << ", " << y << ", " << z << std::endl;
// C++17: if constexpr(编译时条件)
template<typename T>
void process(T value) {
if constexpr (std::is_integral_v<T>) {
std::cout << "整数: " << value << std::endl;
} else if constexpr (std::is_floating_point_v<T>) {
std::cout << "浮点数: " << value << std::endl;
} else {
std::cout << "其他类型" << std::endl;
}
}
process(42); // 整数: 42
process(3.14); // 浮点数: 3.14
process("hi"); // 其他类型
// C++17: std::filesystem(文件系统操作)
// #include <filesystem>
// namespace fs = std::filesystem;
// fs::path p = "/tmp/test.txt";
// if (fs::exists(p)) { fs::remove(p); }
return 0;
}
八、多线程与并发
8.1 std::thread基础
cpp
#include <iostream>
#include <thread>
#include <mutex>
#include <atomic>
#include <vector>
#include <chrono>
std::mutex mtx;
std::atomic<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() {
const int TIMES = 100000;
counter = 0;
std::thread t1(increment, TIMES);
std::thread t2(increment, TIMES);
t1.join();
t2.join();
std::cout << "counter = " << counter << std::endl; // 200000
// 使用lambda创建线程
std::vector<std::thread> threads;
for (int i = 0; i < 5; i++) {
threads.emplace_back([i]() {
std::cout << "线程 " << i << " 运行" << std::endl;
});
}
for (auto& t : threads) t.join();
return 0;
}
8.2 async与future
cpp
#include <iostream>
#include <future>
#include <chrono>
int heavyComputation(int n) {
std::this_thread::sleep_for(std::chrono::seconds(2));
return n * n;
}
int main() {
// async:异步执行任务
std::future<int> result = std::async(std::launch::async, heavyComputation, 42);
std::cout << "任务已提交,主线程继续执行..." << std::endl;
int value = result.get(); // 阻塞等待结果
std::cout << "计算结果: " << value << std::endl; // 1764
// 并行执行多个任务
auto f1 = std::async(std::launch::async, heavyComputation, 10);
auto f2 = std::async(std::launch::async, heavyComputation, 20);
auto f3 = std::async(std::launch::async, heavyComputation, 30);
std::cout << "10^2=" << f1.get() << ", 20^2=" << f2.get()
<< ", 30^2=" << f3.get() << std::endl;
return 0;
}
8.3 原子操作
cpp
#include <iostream>
#include <atomic>
#include <thread>
#include <vector>
// atomic:无锁编程的基础
std::atomic<int> atomicCounter{0};
int normalCounter = 0;
void atomicIncrement() {
atomicCounter.fetch_add(1, std::memory_order_relaxed);
}
void normalIncrement() {
normalCounter++; // 非原子操作,结果不正确
}
int main() {
const int THREADS = 10;
const int TIMES = 100000;
// 原子操作测试
std::vector<std::thread> threads;
for (int i = 0; i < THREADS; i++) {
threads.emplace_back([&]() {
for (int j = 0; j < TIMES; j++) {
atomicIncrement();
}
});
}
for (auto& t : threads) t.join();
std::cout << "atomic counter: " << atomicCounter << std::endl; // 1000000
// 普通操作测试(结果不正确)
normalCounter = 0;
threads.clear();
for (int i = 0; i < THREADS; i++) {
threads.emplace_back([&]() {
for (int j = 0; j < TIMES; j++) {
normalIncrement();
}
});
}
for (auto& t : threads) t.join();
std::cout << "normal counter: " << normalCounter << std::endl; // 不等于1000000
return 0;
}
九、RAII:C++的核心编程范式
RAII(Resource Acquisition Is Initialization)是C++最重要的编程范式之一。
cpp
#include <iostream>
#include <fstream>
#include <mutex>
// RAII示例1:文件操作
class FileGuard {
std::ofstream file;
public:
FileGuard(const std::string& path) {
file.open(path);
if (!file.is_open()) throw std::runtime_error("无法打开文件");
}
~FileGuard() {
if (file.is_open()) file.close();
}
void write(const std::string& content) {
file << content << std::endl;
}
};
// RAII示例2:锁守卫(std::lock_guard就是RAII)
class ScopedLock {
std::mutex& mtx;
public:
explicit ScopedLock(std::mutex& m) : mtx(m) { mtx.lock(); }
~ScopedLock() { mtx.unlock(); }
};
int main() {
// 文件RAII:不管是否抛异常,文件都会被正确关闭
{
FileGuard fg("test.txt");
fg.write("Hello, RAII!");
// 即使这里抛异常,析构函数也会关闭文件
}
// 锁RAII:std::lock_guard
std::mutex mtx;
{
std::lock_guard<std::mutex> lock(mtx);
// 临界区操作...
} // 自动释放锁
return 0;
}
面试高频 :RAII是C++面试的高频考点。核心思想:将资源的生命周期绑定到对象的生命周期 。对象构造时获取资源,析构时释放资源。无论正常退出还是异常退出,析构函数都会被调用,从而保证资源不泄漏。智能指针、
std::lock_guard、std::fstream都是RAII的典型应用。
十、面试高频考点汇总
Q1:C++的虚函数是怎么实现的?
A :编译器为每个有虚函数的类生成一个虚函数表(vtable),表中存储了该类所有虚函数的地址。每个对象有一个指向vtable的指针(vptr)。调用虚函数时,通过vptr找到vtable,再从vtable中找到对应的函数地址进行调用。开销:多一次间接寻址,性能影响通常< 5%。
Q2:智能指针的选择原则?
A :默认用unique_ptr,它开销最小(和裸指针一样),语义最清晰------独占所有权。只有当确实需要多个对象共享同一资源时才用shared_ptr。shared_ptr有引用计数开销,且可能产生循环引用问题。weak_ptr不拥有对象,主要用于打破shared_ptr的循环引用。
Q3:new/delete和malloc/free的区别?
A :new/delete是C++运算符,malloc/free是C库函数。new会调用构造函数,delete会调用析构函数,malloc/free不会。new自动计算大小,malloc需要手动指定。new失败抛异常,malloc失败返回NULL。C++中推荐使用new/delete(或更好的智能指针)。
Q4:移动语义解决了什么问题?
A:移动语义解决了不必要的深拷贝问题。在C++11之前,从函数返回大对象、容器扩容等场景都会触发深拷贝,即使原来的对象马上就要被销毁。移动语义允许"窃取"临时对象的资源(指针转移),避免了昂贵的内存分配和数据复制。
Q5:STL vector扩容机制?
A :vector容量不够时,通常分配当前容量的2倍 (或1.5倍,取决于实现)的新内存,将旧数据拷贝到新内存,然后释放旧内存。扩容时所有迭代器、指针、引用都会失效。可以用reserve()提前分配足够的容量避免频繁扩容。
Q6:C++中的RAII是什么?
A :RAII(Resource Acquisition Is Initialization)是C++的核心编程范式。核心思想:将资源的生命周期绑定到对象的生命周期 。对象构造时获取资源,析构时释放资源。无论正常退出还是异常退出,析构函数都会被调用。智能指针、std::lock_guard、std::fstream都是RAII的典型应用。
十一、C++学习路线与资源推荐
11.1 推荐学习路线
第1周:基础语法(变量、引用、const、命名空间)
第2周:面向对象(类、继承、多态、虚函数、运算符重载)
第3周:模板与STL(函数模板、类模板、vector/map/set、迭代器)
第4周:内存管理(new/delete、智能指针、RAII)
第5周:现代C++(Lambda、移动语义、auto/decltype)
第6周:多线程(thread、mutex、async/future、atomic)
第7周:进阶特性(模板元编程、C++17/20新特性)
第8周:综合项目(简易HTTP服务器/JSON解析器/线程池/简易数据库)
11.2 推荐学习资源
| 资源 | 说明 |
|---|---|
| 《C++ Primer》 | C++入门的"圣经",系统全面 |
| 《Effective C++》 | 进阶必读,55条改善代码的建议 |
| 《Effective Modern C++》 | Scott Meyers的现代C++最佳实践 |
| 《STL源码剖析》 | 侯捷著,深入理解STL底层实现 |
| 《C++ Concurrency in Action》 | C++并发编程权威指南 |
| cppreference.com | 最权威的在线参考文档 |
写在最后
C++是一门"学无止境"的语言。从C++98到C++20,标准不断演进,新特性层出不穷。但不管标准怎么变,核心的东西始终不变:理解内存、理解对象模型、理解模板、理解RAII。
我见过太多人,C++语法背得很熟,但不知道虚函数表长什么样,不知道std::move到底做了什么,不知道shared_ptr的循环引用怎么解决。语法只是工具,理解原理才是王道。
这篇文章涵盖了C++的核心知识点,但学C++最重要的是动手实践。建议你今天就把代码编译运行一遍,遇到编译错误就查,遇到段错误就用gdb调试,这比看十篇文章都有用。
互动话题:你在学C++的过程中,哪个知识点让你印象最深或者踩过最深的坑?是指针和引用搞混了,还是模板的编译错误看不懂?欢迎在评论区分享你的经历,大家一起交流!
如果这篇文章对你有帮助,欢迎点赞、收藏、关注三连支持!后续我会持续更新C++进阶系列文章,包括模板元编程、C++20协程、现代C++设计模式等。
本文为C++全面教学系列的第一篇,后续文章正在撰写中,关注我不迷路 👇