现代C++特性
C++11
统一的列表初始化 {}
-
基本用法
-
C++11 扩大了用大括号 {} 括起的列表的使用范围,使其可用于所有内置类型和用户自定义类型。使用时可以添加 =,也可以不添加。
-
内置类型与数组:
- int x = {1};
int y{2}; // 建议用法
int array1[]{1, 2, 3};
int* pa = new int[3]{0}; // 动态分配初始化
- int x = {1};
-
自定义对象:
- class Date {
public:
Date(int y, int m, int d) :_y(y), _m(m), _d(d) {}
private:
int _y, _m, _d;
};
Date d1(2024, 1, 1); // 旧风格
Date d2{2024, 1, 1}; // C++11 列表初始化
- class Date {
-
-
-
std::initializer_list
-
这是实现"容器支持任意数量元素初始化"的核心。编译器会将 {} 括起来的同类型序列识别为 initializer_list 类型
-
模拟实现 vector 支持 {} 初始化:
- namespace bit {
template
class vector {
public:
typedef T* iterator;
// 构造函数支持 initializer_list
vector(std::initializer_list l) {
_start = new T[l.size()];
_finish = _start + l.size();
_endofstorage = _start + l.size();
iterator vit = _start;
// 迭代器遍历列表
for (auto e : l) {
*vit++ = e;
}
}
// 赋值重载支持 {}
vector& operator=(std::initializer_list l) {
vector tmp(l); // 调用上面的构造函数
std::swap(_start, tmp._start);
return *this;
}
private:
iterator _start = nullptr;
iterator _finish = nullptr;
iterator _endofstorage = nullptr;
};
}
- namespace bit {
-
-
变量类型推导与声明
-
auto
-
功能:在编译阶段根据初始值自动推导变量类型。
-
注意:必须显示初始化。
-
场景:简化复杂迭代器声明(如 map<string, string>::iterator)。
-
-
decltype
-
功能:将变量的类型声明为表达式指定的类型。
-
与auto区别:auto 是根据变量的值推导,decltype 是根据表达式的类型推导(表达式不运行)。
-
int x = 1;
double y = 2.0;
decltype(x * y) ret; // ret 此时是 double 类型
vector<decltype(x * y)> v; // 可以作为模板参数
-
-
nullptr
-
起因:C++中 NULL 被定义为 0,在重载函数匹配时(如 func(int) 和 func(int*))会产生歧义。
-
解决:nullptr 是一个字面量,其类型为指针类型,能够明确表示空指针。
-
右值引用与移动语义
-
左值 vs 右值
-
左值(Lvalue):可以取地址的表达式,通常是变量名或解引用的指针。左值可以出现在赋值号左边。
-
右值(Rvalue):不能取地址的表达式,通常是字面常量、函数临时返回值、表达式结果(如 x + y)。右值只能出现在赋值号右边。
-
-
右值引用 (T&&)
-
右值引用就是给右值起别名。
-
核心意义:传统的左值引用在函数返回局部对象时,只能进行深拷贝。右值引用配合移动语义可以"窃取"临时对象的资源。
-
-
移动构造与移动赋值(模拟实现)
-
这是 C++11 效率提升的关键。以 string 为例:
- namespace bit {
class string {
public:
// 传统拷贝构造:深拷贝
string(const string& s) : _str(nullptr) {
string tmp(s._str);
swap(tmp);
}
// 移动构造:直接剥夺临时对象(右值)的资源
string(string&& s) : _str(nullptr), _size(0), _capacity(0) {
std::cout << "string(string&& s) -- 资源转移" << std::endl;
swap(s); // 交换资源,s 现在指向 nullptr
}
// 移动赋值
string& operator=(string&& s) {
std::cout << "operator=(string&& s) -- 移动赋值" << std::endl;
swap(s);
return *this;
}
void swap(string& s) {
std::swap(_str, s._str);
std::swap(_size, s._size);
}
private:
char* _str;
size_t _size;
size_t _capacity;
};
}
- namespace bit {
-
-
std::move
-
作用:将左值强制转换为右值引用,从而触发移动语义。
-
注意:被 move 后的左值,其内部资源可能已被转移,后续应谨慎使用。
-
万能引用与完美转发
-
万能引用 (Universal Reference)
-
在模板参数中,T&& 不一定代表右值引用,它代表"万能引用"。
-
如果传入左值,推导为左值引用。
-
如果传入右值,推导为右值引用。
-
-
-
完美转发 (std::forward)
-
问题:右值引用在被作为参数传递给下一层函数时,属性会退化成左值(因为它有名字了,可以取地址了)。
-
解决:std::forward(t) 可以在传参过程中保持对象原有的左值或右值属性。
-
template
void PerfectForward(T&& t) {
// 如果不加 forward,t 永远被当做左值传给下层
Fun(std::forward(t));
}
-
新的类功能
-
默认成员函数
-
C++11 增加了:移动构造函数和移动赋值运算符重载。
- 生成条件:如果你没有自己写析构函数、拷贝构造、拷贝赋值中的任何一个,编译器才会自动生成。
-
-
控制函数生成
-
default:强制生成默认函数(例如写了拷贝构造后还想要默认构造)。
- Person() = default;
-
delete:禁止生成某函数(C++98 是通过设为私有且不实现来达成)。
- Person(const Person& p) = delete; // 禁止拷贝构造
-
Lambda 表达式(匿名函数)
-
为什么需要 Lambda?
- 在 C++98 中,若需定义一个局部的比较逻辑(如 std::sort 的自定义排序),必须写一个仿函数(类对象),这会导致逻辑分散、代码冗余且命名困难。Lambda 允许在调用点直接编写逻辑。
-
语法结构
-
capture-list\] (parameters) mutable -\> return-type { statement } * 捕捉列表 \[ \]:捕捉上下文变量。 * \[a\]:传值捕捉变量 a。 * \[\&a\]:引用捕捉变量 a。 * \[=\]:全值捕捉(捕捉父作用域所有变量)。 * \[\&\]:全引用捕捉。 * 参数列表 ( ):同普通函数参数。 * mutable:默认传值捕捉是 const 的,加上此关键字可修改副本。 * 返回值 -\> type:可推导时通常省略。
-
编译器处理 Lambda 的方式与仿函数完全一致。
-
编译器会自动生成一个名为 lambda_uuid 的类。
-
该类重载了 operator()。
-
捕捉列表中的变量会变成该类的成员变量。
-
-
可变参数模板 (Variadic Templates)
-
允许模板接收 0 到任意多个模板参数,核心符号是省略号 ...。
-
参数包的展开方案
-
不能通过索引访问参数包,通常有两种展开方式:
-
递归方式:
- // 递归终止函数
void ShowList() { cout << endl; }
// 展开函数
template <class T, class ...Args>
void ShowList(T value, Args... args) {
cout << value << " ";
ShowList(args...); // 递归调用
}
- // 递归终止函数
-
逗号表达式展开(更高效):
-
利用初始化列表的特性,强制展开参数包。
-
template <class ...Args>
void ShowList(Args... args) {
int arr[] = { (PrintArg(args), 0)... }; // PrintArg 是处理单个参数的函数
}
-
-
-
-
emplace_back 的优势
-
vector::emplace_back 利用了可变参数模板和完美转发。
-
区别:push_back 接收一个构造好的对象(产生拷贝或移动);emplace_back 直接接收构造参数,在容器底层内存中就地构造,省去了一次临时对象的创建过程。
-
-
包装器:std::function 与 std::bind
-
std::function
-
C++ 中可调用对象种类繁多(函数指针、仿函数、Lambda)。这会导致模板在实例化时产生多份代码(模板膨胀)。std::function 是一种通用的类模板包装器,可以统一这些对象的类型。
-
语法:function<返回值类型(参数类型列表)>
- // 包装 Lambda
function<int(int, int)> f = [](int x, int y){ return x + y; };
// 包装成员函数
function<double(Plus, double, double)> f2 = &Plus::plusd;
- // 包装 Lambda
-
-
std::bind (适配器)
-
用于调整可调用对象的参数:改变参数顺序、固定某些参数的值。
-
占位符:std::placeholders::_1 代表第一个参数。
- // 固定第一个参数为 100
auto newFunc = bind(Plus, 100, placeholders::_1);
newFunc(20); // 实际调用 Plus(100, 20)
- // 固定第一个参数为 100
-
C++11 多线程库
-
C++11 以前依赖操作系统 API(pthread/WinAPI),现在有了跨平台的标准库 。
-
thread 类
-
线程对象创建即启动。
-
注意:thread 对象在析构前必须显式调用 join()(等待结束)或 detach()(后台运行),否则程序会崩溃。
-
参数传递:默认是值拷贝,若需传递引用,必须使用 std::ref()。
-
-
原子性操作 std::atomic
-
解决多线程自增(count++)非原子性的问题。
-
优点:不需要加锁,底层利用 CPU 的 CAS(Compare And Swap)指令实现,效率远高于 mutex。
- atomic sum = 0;
sum++; // 线程安全
- atomic sum = 0;
-
-
互斥锁与 RAII
-
std::mutex:手动 lock() / unlock()。
-
std::lock_guard:RAII 风格。构造时加锁,生命周期结束(析构)时自动解锁。
-
std::unique_lock:比 lock_guard 更灵活,支持中途手动释放或延迟加锁。
-
-
条件变量 condition_variable
-
配合 unique_lock 使用,用于线程间的同步通信(如生产者-消费者模型)。
-
wait(lock, predicate):释放锁并阻塞,直到满足断言 predicate。
-
notify_one() / notify_all():唤醒等待的线程。
-
-
智能指针
为什么需要智能指针?
-
内存泄漏与异常安全问题
-
痛点:在传统的C++编程中,手动管理内存(new/delete)极易出错。
-
典型场景:
-
忘记释放:申请后忘记调用 delete。
-
逻辑跳过:代码中间逻辑出现 return 或 goto,导致释放语句未执行。
-
异常安全:在申请资源后、释放资源前,如果中间函数抛出异常,执行流会直接跳转到 catch 块,导致 delete 被跳过。
-
-
代码示例(异常导致泄漏):
- void Func() {
int* p1 = new int;
int* p2 = new int; // 如果此处抛异常,p1就泄露了
cout << div() << endl; // 如果div()抛异常,p1和p2都泄露了
delete p1;
delete p2;
}
- void Func() {
-
内存泄漏 (Memory Leak)
-
定义与危害
-
定义:指因为疏忽或错误造成程序未能释放已经不再使用的内存。内存泄漏是指失去了对该段内存的控制。
-
危害:长期运行的程序(如服务器)响应变慢,最终导致系统崩溃(OOM)。
-
-
分类
-
堆内存泄漏 (Heap leak):通过 malloc/new 申请的内存未处理。
-
系统资源泄漏:套接字 (Socket)、文件描述符 (FD)、管道等未释放。
-
-
检测与避免
-
检测工具:Linux下的 Valgrind,Windows下的 VLD (Visual Leak Detector)。
-
预防措施:养成良好习惯,但最根本的解决方法是采用 RAII 思想和 智能指针。
-
智能指针的核心原理
-
RAII (资源获取即初始化)
-
定义:利用对象的生命周期来控制资源。
-
机制:
-
构造函数:获取资源(申请内存)。
-
析构函数:释放资源(释放内存)。
-
-
好处:对象出作用域会自动调用析构函数,保证资源一定会被释放。
-
-
像指针一样的行为
- 原理:通过运算符重载实现 operator* 和 operator->。
-
基础框架模拟实现
- template
class SmartPtr {
public:
SmartPtr(T* ptr = nullptr) : _ptr(ptr) {}
~SmartPtr() { if(_ptr) delete _ptr; }
// 像指针一样使用
T& operator*() { return _ptr; }
T operator->() { return _ptr; }private:
T* _ptr;
};
- template
C++ 标准库智能指针
-
std::auto_ptr (C++98) ------ 管理权转移
-
原理:拷贝时将原指针置空,将管理权交给新指针。
-
缺点:极其危险。拷贝后原对象悬空(NULL),访问会崩溃。
-
模拟实现关键代码:
- auto_ptr(auto_ptr& sp) : _ptr(sp._ptr) {
sp._ptr = nullptr; // 管理权转移,原对象变空
}
- auto_ptr(auto_ptr& sp) : _ptr(sp._ptr) {
-
-
std::unique_ptr (C++11) ------ 防拷贝
-
原理:简单粗暴,直接禁止拷贝和赋值。
-
适用场景:独占一份资源。
-
实现方式:使用 = delete 禁用拷贝构造和赋值。
- unique_ptr(const unique_ptr& sp) = delete;
unique_ptr& operator=(const unique_ptr& sp) = delete;
- unique_ptr(const unique_ptr& sp) = delete;
-
-
std::shared_ptr (C++11) ------ 引用计数(最常用)
-
原理:多个指针共享同一份资源,通过内部的引用计数记录使用者数量。
-
机制:
-
拷贝/赋值时:计数 +1。
-
析构时:计数 -1。
-
计数减为 0 时:释放资源。
-
-
线程安全问题:
-
计数本身是安全的:标准库通过加锁(或原子操作)保证了引用计数的加减是线程安全的。
-
指向的资源不一定安全:智能指针指向的对象并发访问仍需手动加锁。
-
-
shared_ptr 的核心模拟实现(含线程安全)
-
shared_ptr 的核心在于引用计数,但由于引用计数是多个对象共享的,在多线程环境下必须保证计数的原子性。
- #include
#include
- #include
-
-
namespace bit {
template
class shared_ptr {
public:
// 构造函数
shared_ptr(T* ptr = nullptr)
: _ptr(ptr)
, _pRefCount(new int(1))
, _pmtx(new std::mutex)
{}
// 释放逻辑:封装成私有函数
void Release() {
bool flag = false;
_pmtx->lock();
if (--(*_pRefCount) == 0) {
if (_ptr) {
// std::cout << "delete: " << _ptr << std::endl;
delete _ptr;
}
delete _pRefCount;
flag = true; // 标记需要释放锁
}
_pmtx->unlock();
if (flag) delete _pmtx; // 只有当计数为0时才销毁锁
}
// 拷贝构造:增加计数(需加锁)
shared_ptr(const shared_ptr<T>& sp)
: _ptr(sp._ptr)
, _pRefCount(sp._pRefCount)
, _pmtx(sp._pmtx)
{
_pmtx->lock();
++(*_pRefCount);
_pmtx->unlock();
}
// 赋值重载:左减右加
shared_ptr<T>& operator=(const shared_ptr<T>& sp) {
if (_ptr != sp._ptr) { // 防止指向同一资源的指针赋值
Release(); // 释放旧资源
_ptr = sp._ptr;
_pRefCount = sp._pRefCount;
_pmtx = sp._pmtx;
_pmtx->lock();
++(*_pRefCount);
_pmtx->unlock();
}
return *this;
}
~shared_ptr() { Release(); }
// 指针行为重载
T& operator*() { return *_ptr; }
T* operator->() { return _ptr; }
T* get() const { return _ptr; }
int use_count() { return *_pRefCount; }
private:
T* _ptr;
int* _pRefCount; // 堆上的引用计数
std::mutex* _pmtx; // 互斥锁,保证计数操作原子性
};
}
- 智能指针对象的安全:引用计数的 ++ 和 -- 在内部通过加锁保证了安全。即:多个线程同时拷贝/销毁同一个 shared_ptr 对象,计数不会错乱。
- 管理资源的安全:智能指针不保证其指向的堆空间内容的线程安全。如果多个线程通过智能指针同时修改 *ptr,开发者仍需额外加锁。
-
循环引用 (Circular Reference) 深度剖析
-
现象描述
-
当两个对象内部各持有一个 shared_ptr 且互相指向对方时,会形成闭环。
- struct ListNode {
int _data;
std::shared_ptr _next;
std::shared_ptr _prev;
~ListNode() { std::cout << "~ListNode()" << std::endl; }
};
- struct ListNode {
-
-
void Test() {
auto n1 = std::make_shared();
auto n2 = std::make_shared();
n1->_next = n2; // n2计数变2
n2->_prev = n1; // n1计数变2
} // 函数结束,n1/n2析构,计数分别降为1,资源均不释放 -> 内存泄漏
- 原理解析
- n1 释放的前提是 n2->_prev 销毁。
- n2->_prev 销毁的前提是 n2 释放。
- n2 释放的前提是 n1->_next 销毁。
- 相互死等,引用计数永远无法降为 0。
- 解决方案:weak_ptr
- 特性:weak_ptr 并不参与资源的生命周期管理,它不增加引用计数。
- 修复:将 ListNode 中的 _next 和 _prev 改为 std::weak_ptr,环路被打破。
-
std::weak_ptr (C++11) ------ 解决循环引用
-
原理:弱引用,不增加引用计数。
-
痛点:循环引用:
-
场景:两个节点(如双向链表)互相指向对方。
-
后果:两个 shared_ptr 计数永远为1,导致资源永远无法释放。
-
-
解决方案:将结构内部的指针定义为 weak_ptr。
- struct ListNode {
weak_ptr _next; // 不增加引用计数
weak_ptr _prev;
};
- struct ListNode {
-
定制删除器 (Custom Deleter)
-
场景:智能指针默认使用 delete 释放资源。但如果资源是 new[] 申请的、或是 malloc 申请的、亦或是文件句柄,则需要自定义处理方式。
-
用法:在构造时传入一个仿函数或 Lambda 表达式。
- // 示例:管理文件资源
std::shared_ptr sp(fopen("test.txt", "w"), [](FILE* p){
fclose§;
});
- // 示例:管理文件资源
// 示例:管理数组
std::shared_ptr sp2(new int[10], [](int* p){
delete[] p;
});
C++14 核心特性
函数返回值类型推导 (Return Type Deduction)
-
知识点详述
- 在 C++11 中,如果函数返回值依赖于模板参数,必须使用"追踪返回值类型"(Trailing return type)。C++14 进一步放宽了限制,允许直接使用 auto 让编译器根据函数体中的 return 语句自动推导类型。
-
// C++11 方式
template<typename T, typename U>
auto Add_C11(T x, U y) -> decltype(x + y) {
return x + y;
}
// C++14 方式:直接推导
template<typename T, typename U>
auto Add_C14(T x, U y) {
return x + y; // 编译器自动推导返回类型
}
-
使用规则与限制
-
如果函数内有多个 return 语句,它们的推导类型必须完全一致。
-
如果没有 return 语句,推导为 void。
-
如果函数是虚函数,则不能使用返回值推导。
-
递归函数只有在递归调用前有至少一个 return 语句能确定类型时,才能使用推导。
-
-
优缺点
-
优点:简化代码,尤其是处理复杂的模板嵌套(如迭代器类型)时,不需要写冗长的 decltype。
-
缺点:函数实现必须放在头文件中(编译器需要看到函数体才能推导);降低了接口的可读性(用户不看代码不知道确切返回什么)。
-
泛型 Lambda 表达式 (Generic Lambdas)
-
知识点详述
- C++11 的 Lambda 参数必须指定具体类型。C++14 允许在 Lambda 参数中使用 auto 关键字,这使得 Lambda 具有了类似函数模板的能力。
-
auto sum = [](auto a, auto b) {
return a + b;
};
cout << sum(1, 2) << endl; // 整数加法
cout << sum(1.1, 2.2) << endl; // 浮点数加法
cout << sum(string("A"), "B") << endl; // 字符串拼接
-
底层模拟实现原理
-
编译器实际上将泛型 Lambda 转换为一个带有模板成员函数 operator() 的匿名仿函数类。
- // 模拟编译器生成的结构
class Lambda_UUID {
public:
template<typename T1, typename T2>
auto operator()(T1 a, T2 b) const {
return a + b;
}
};
- // 模拟编译器生成的结构
-
Lambda 捕获表达式 / 初始化捕获 (Initialized Lambda Captures)
-
知识点详述
- C++11 的 Lambda 捕获只能捕获作用域内的变量,且不能对捕获后的变量进行初始化。C++14 引入了捕获表达式,允许在捕获列表中定义新变量并初始化。
-
核心应用场景:捕获移动对象
- C++11 无法直接捕获一个"只能移动"的对象(如 unique_ptr)并保持其移动语义。C++14 完美解决了这个问题。
-
#include
#include
void TestCapture() {
auto ptr = std::make_unique(10);
// 将 ptr 移动到 Lambda 内部的变量 p 中
auto func = [p = std::move(ptr)]() {
std::cout << "Inside Lambda: " << *p << std::endl;
};
// 此时外部 ptr 已为空
if (!ptr) std::cout << "Outer ptr is null" << std::endl;
func();
}
- 优点:极大地增强了 Lambda 的灵活性,支持在捕获列表中重命名变量,支持移动语义捕获。
变量模板 (Variable Templates)
-
知识点详述
- 在 C++14 之前,模板只能用于类或函数。C++14 允许定义变量模板,这对于定义数学常量或属性常量非常有用。
-
// 定义变量模板
template
constexpr T pi = T(3.1415926535897932385);
void TestVarTemplate() {
float f_pi = pi; // 3.14159...
double d_pi = pi; // 更高精度的 pi
cout << f_pi << endl;
cout << d_pi << endl;
}
放宽 constexpr 限制 (Relaxed constexpr)
-
相似点区别 (C++11 vs C++14)
-
C++11 的 constexpr:非常苛刻。函数体只能包含一条 return 语句,不能有循环、不能有 if 分支。
-
C++14 的 constexpr:更加通用。允许在函数内部使用局部变量、条件分支(if/switch)、循环(for/while)。
-
-
// C++14 允许循环和局部变量在 constexpr 函数中
constexpr int Factorial(int n) {
int res = 1;
for (int i = 1; i <= n; ++i) {
res *= i;
}
return res;
}
int main() {
constexpr int val = Factorial(5); // 编译期计算出 120
int arr[val]; // 合法
}
-
优点:更多的计算可以从运行期移至编译期,显著提高程序运行效率,且编写编译期逻辑像写普通逻辑一样自然。
-
注意:constexpr 函数内部依然不能调用非 constexpr 函数,且不能有 static 变量或 thread_local 变量。
二进制字面量与数字分隔符
-
知识点详述
-
二进制字面量:使用 0b 或 0B 前缀表示。
-
数字分隔符:使用单引号 ' 作为分隔符,不影响数值,仅增加可读性。
-
-
int b = 0b1010'1111; // 二进制
long long big_num = 1'000'000'000; // 十亿,清晰易读
float f = 3.141'592f;
智能指针:std::make_unique
-
知识点详述
- 在 C++11 中,标准库提供了 std::make_shared,但由于疏忽遗漏了 std::make_unique。C++14 终于补全了这个短板。它用于创建一个 std::unique_ptr,而无需显式调用 new。
-
#include
struct Widget {
Widget(int x, double y) {}
};
void TestMakeUnique() {
// C++11 方式(繁琐且存在潜在异常风险)
std::unique_ptr p1(new Widget(10, 2.5));
// C++14 方式(简洁且安全)
auto p2 = std::make_unique<Widget>(10, 2.5);
// 创建数组
auto pArr = std::make_unique<int[]>(5);
}
-
为什么需要它?(优缺点与区别)
-
异常安全性:考虑函数调用 process(std::unique_ptr(new T()), func())。如果在 new T() 之后但在构造指针前 func() 抛出异常,会导致内存泄漏。make_unique 保证了分配和构造的原子性。
-
优点:代码更简洁(符合 RAII 原则),避免显式 new,提高安全性。
-
缺点:无法指定自定义删除器(Deleter)。如果需要自定义删除器,仍需手动构造 unique_ptr。
-
共享锁:std::shared_timed_mutex 与 std::shared_lock
-
知识点详述
- C++14 引入了读写锁(Read-Write Lock)的基础。它允许多个线程同时进行读操作,但写操作是互斥的。
-
#include <shared_mutex>
#include
#include
class SafeCounter {
private:
mutable std::shared_timed_mutex _mtx;
int _value = 0;
public:
// 读操作:使用 shared_lock,允许多人同时读
int Get() const {
std::shared_lockstd::shared_timed_mutex lock(_mtx);
return _value;
}
// 写操作:使用 unique_lock,同一时刻只能一人写
void Increment() {
std::unique_lock<std::shared_timed_mutex> lock(_mtx);
_value++;
}
};
-
优点:对于"读多写少"的并发场景,能显著提高程序吞吐量。
-
注意点:C++14 提供的是 shared_timed_mutex,它支持超时。如果不需要超时功能,C++17 引入了性能略高的 std::shared_mutex。
关联容器的异构查找 (Heterogeneous Lookup)
-
在 C++11 中,如果你有一个 std::map<std::string, int>,使用 const char* 查找时,会强制创建一个临时的 std::string 对象,造成性能损耗。C++14 允许直接使用不同类型进行查找。
-
#include
#include
void TestLookup() {
// 关键点:使用 std::less<> (空模板) 开启异构查找
std::map<std::string, int, std::less<>> myMap;
myMap["Alice"] = 1;
// C++14 查找:直接传入字符串常量,不再创建临时 std::string 对象
auto it = myMap.find("Alice");
}
- 性能提升:减少了不必要的临时对象内存分配和析构开销,特别是在高性能 Server 端开发中非常有用。
时间库增强:std::chrono 字面量
-
知识点详述
- C++14 增加了内置的时间单位字面量,让代码看起来更符合人类直觉。
-
#include
#include
using namespace std::chrono_literals; // 必须引入命名空间
void TestChrono() {
auto lesson_time = 45min; // 表示 45 分钟
auto sleep_time = 2s; // 表示 2 秒
auto latency = 100ms; // 表示 100 毫秒
std::this_thread::sleep_for(1s); // 极佳的可读性
}
std::integer_sequence (元编程利器)
-
知识点详述
- 这是一个编译期工具,表示一个整数序列。它最大的作用是将 std::tuple(元组)中的元素展开并作为参数传递给函数。
-
#include
#include
#include
// 处理解包后的逻辑
template<typename... Args>
void RealFunc(Args... args) {
(std::cout << ... << args) << std::endl; // C++17 折叠表达式简化打印
}
// 辅助函数,利用 index_sequence 展开元组
template<typename Tuple, std::size_t... Is>
void UnpackTuple(const Tuple& t, std::index_sequence<Is...>) {
RealFunc(std::get(t)...);
}
int main() {
auto t = std::make_tuple(1, 3.14, "Hello");
// 创建 0, 1, 2 的索引序列
UnpackTuple(t, std::make_index_sequence<3>{});
}
std::exchange
-
知识点详述
- 将一个新值赋给对象,并返回该对象的旧值。
-
#include
// 模拟实现
template<class T, class U = T>
T my_exchange(T& obj, U&& new_val) {
T old_val = std::move(obj);
obj = std::forward++(new_val);
return old_val;
}++
// 使用场景:移动构造函数
class MyClass {
int* ptr;
public:
MyClass(MyClass&& other)
: ptr(std::exchange(other.ptr, nullptr)) {} // 拿走旧值,置空原指针
};
std::quoted (字符串转义)
-
知识点详述
- 解决带空格和引号的字符串在 IO 流中的格式问题。
-
#include
#include
#include
void TestQuoted() {
std::stringstream ss;
std::string s = "Hello "World"";
ss << std::quoted(s);
// 输出到流的内容变为: "Hello \"World\"" (带外层引号且内部转义)
std::string output;
ss >> std::quoted(output); // 读回原字符串
std::cout << output; // Hello "World"
}
C++17 核心特性
核心语法
-
结构化绑定 (Structured Bindings)
-
知识点详述
- 允许使用一个声明同时从数组、结构体或元组中提取多个变量。这是对 C++11 std::tie 的重大升级。
-
#include
#include
-
void TestStructuredBindings() {
std::map<std::string, int> scores = {{"Alice", 90}, {"Bob", 85}};
// C++17 方式:直接解构 map 的 pair
for (const auto& [name, score] : scores) {
std::cout << name << ": " << score << std::endl;
}
// 解构结构体
struct Point { double x, y; };
Point p = {1.2, 3.4};
auto [px, py] = p;
}
- 相似点区别 (vs std::tie)
- std::tie:变量必须提前声明,且不支持直接解构结构体。
- 结构化绑定:一行代码完成声明+初始化,代码更简洁,变量作用域更紧凑。
- 优缺点
- 优点:大幅提升可读性,减少冗余代码。
- 缺点:无法部分忽略某些成员(C++20 才引入 [_] 忽略),且绑定变量的类型必须一致推导。
-
带初始化的条件分支 (Selection statements with initializer)
-
知识点详述
- 在 if 或 switch 语句中允许定义一个初始化语句。该变量的生命周期仅限于该分支内部。
-
#include
-
void TestIfInit() {
std::set s = {1, 2, 3};
// 在 if 中初始化迭代器并判断
if (auto it = s.find(2); it != s.end()) {
std::cout << "Found: " << *it << std::endl;
}
// it 在此处已失效,避免了命名空间污染
}
- 优缺点
- 优点:限制变量作用域,增强异常安全性,防止变量在逻辑外被误用。
- 缺点:如果初始化语句过长,会降低首行的可读性。
-
折叠表达式 (Fold Expressions)
-
知识点详述
- 专门为可变参数模板设计。在 C++11/14 中展开参数包需要递归或黑科技,C++17 允许通过一行表达式完成整个参数包的二元运算。
-
// C++17 折叠表达式实现全加器
template<typename... Args>
auto Sum(Args... args) {
return (... + args); // 一元右折叠:(arg1 + (arg2 + arg3))
}
-
// 打印所有参数
template<typename... Args>
void Printer(Args... args) {
(std::cout << ... << args) << std::endl;
}
int main() {
std::cout << Sum(1, 2, 3, 4); // 10
Printer(1, " hello ", 3.14);
}
- 模拟实现(对比 C++11 的繁琐)
- // C++11 必须通过递归实现 Sum
template
T Sum11(T t) { return t; }
template<typename T, typename... Args>
T Sum11(T first, Args... args) {
return first + Sum11(args...);
}
-
constexpr if (编译期分支)
-
知识点详述
- 允许在模板中根据编译期条件选择性地编译代码块。不满足条件的逻辑分支不会被编译入二进制文件,这解决了 SFINAE(替换失败并非错误)过于复杂的问题。
-
#include <type_traits>
-
template
auto GetValue(T t) {
if constexpr (std::is_pointer_v) {
return *t; // 如果是指针,解引用
} else {
return t; // 如果不是指针,直接返回
}
}
- 核心优势
- 取代标签分发:不再需要写多个重载函数或 std::enable_if。
- 更清晰的逻辑:在一个函数内处理多种类型逻辑,维护性极高。
-
类模板参数推导 (CTAD, Class Template Argument Deduction)
-
知识点详述
- 允许在实例化类模板时省略显式的模板参数,编译器会根据构造函数的参数自动推导。
-
#include
#include
-
void TestCTAD() {
// C++11
std::pair<int, double> p1(1, 2.2);
// C++17
std::pair p2(1, 2.2); // 自动推导为 pair<int, double>
std::vector v = {1, 2, 3}; // 推导为 vector<int>
}
-
内联变量 (Inline Variables)
-
知识点详述
- 允许在头文件中直接定义并初始化静态成员变量或全局变量,而不会导致"多重定义"错误。
-
// MyHeader.h
class MyConfig {
public:
static inline int global_val = 100; // C++17 支持
};
-
inline MyConfig g_config; // 头文件中定义全局对象也没问题
- 核心作用
- Header-only 库:这是实现"全头文件库"的最后一块拼图,开发者不再需要专门创建一个 .cpp 来初始化静态变量。
-
嵌套命名空间简化
- // C++11
namespace A { namespace B { namespace C { ... }}}
- // C++11
// C++17
namespace A::B::C {
// 逻辑
}
标准库
-
std::string_view(极致的只读字符串优化)
-
知识点详述
- string_view 是一个非拥有式的字符串视图。它内部仅包含一个指向现有字符数组的指针和长度。
-
相似点区别(vs const std::string&)
-
const std::string&:如果传入的是字符串字面量(如 "hello"),会触发一次动态内存分配来创建一个临时的 std::string。
-
std::string_view:直接指向字面量地址,零拷贝,零内存分配。
-
-
#include <string_view>
-
void PrintView(std::string_view sv) {
std::cout << sv << std::endl;
}
int main() {
std::string s = "A very long string...";
PrintView(s); // OK,无拷贝
PrintView("Hello"); // OK,直接引用字面量,无临时对象生成
// 甚至可以高效截取子串
std::string_view sub = s.substr(0, 5); // 仅修改偏移量和长度,不产生新字符串
}
- 优缺点
- 优点:显著减少高性能场景下的内存分配。
- 缺点:生命周期风险。因为不拥有数据,如果原始 string 销毁,string_view 就会变成野指针。
-
std::optional(优雅地处理空值)
-
知识点详述
- 用于表示"可能存在,也可能不存在"的值。它可以替代"魔法值"(如返回 -1 表示失败)或指针。
-
#include
-
std::optional ToInt(std::string s) {
try {
return std::stoi(s);
} catch (...) {
return std::nullopt; // 返回"无内容"
}
}
int main() {
auto res = ToInt("123");
if (res) { // 判断是否有值
std::cout << "Value: " << *res << std::endl;
}
// 默认值方案
int val = ToInt("abc").value_or(0); // 如果转换失败返回 0
}
- 优缺点
- 优点:语义明确,强制调用者处理为空的情况,比返回指针更安全。
- 缺点:比原始类型多占用一点点内存(存储状态位)。
-
std::variant(类型安全的 union)
-
知识点详述
- 类型安全的联合体。它知道当前存储的是哪种类型,并且在访问错误类型时抛出异常。
-
#include
-
void TestVariant() {
std::variant<int, std::string, double> v;
v = 10;
v = "Hello";
// 访问方式 1:get
try {
std::cout << std::get<std::string>(v);
} catch (const std::bad_variant_access&) {}
// 访问方式 2:std::visit (模式匹配)
std::visit([](auto&& arg) {
std::cout << arg;
}, v);
}
- 相似点区别(vs union)
- union:无法存储 std::string 等具有构造函数的复杂类型,不安全。
- std::variant:支持任何类型,自动管理生命周期。
-
std::any(类型安全的万能容器)
-
知识点详述
- 可以存储任意类型的单个值。
-
#include
-
void TestAny() {
std::any a = 1;
a = std::string("Universal");
if (a.has_value()) {
// 必须通过 any_cast 提取
try {
std::string s = std::any_cast<std::string>(a);
} catch (const std::bad_any_cast&) {}
}
}
- 相似点区别(vs void*)
- void*:不记录类型,无法在运行时检查。
- std::any:记录类型(RTTI),不匹配时报错。缺点是性能较差(涉及动态分配)。
-
std::filesystem(跨平台文件系统操作)
-
知识点详述
- 终于将操作系统的文件操作标准化。不再需要区分 Windows 的 _mkdir 或 Linux 的 mkdir。
-
#include
namespace fs = std::filesystem;
-
void TestFS() {
fs::path p = "C:/temp/test.txt";
if (fs::exists(p)) {
std::cout << "File size: " << fs::file_size(p);
}
// 遍历目录
for (auto& entry : fs::directory_iterator(".")) {
std::cout << entry.path() << std::endl;
}
fs::create_directories("a/b/c"); // 递归创建目录
}
-
并行算法(多核加速一键开启)
-
知识点详述
- 为 60 多个 STL 算法(如 sort, find, transform)增加了执行策略参数。
-
#include
#include // 关键头文件
-
void TestParallel() {
std::vector v(1000000, 1);
// 串行执行 (默认)
std::sort(v.begin(), v.end());
// 并行执行 (多核加速)
std::sort(std::execution::par, v.begin(), v.end());
// 矢量化并行执行
std::sort(std::execution::par_unseq, v.begin(), v.end());
}
- 优缺点
- 优点:一行代码利用多核性能。
- 注意:编译器和库的支持程度不同(如 GCC 需要 tbb 库),且对于小规模数据,并行的开销反而更大。
-
std::byte
-
知识点详述
-
C++17 引入了 std::byte,它既不是字符类型也不是整数,而是真正的"位集合"。
-
定义:enum class byte : unsigned char {};
-
优点:避免了 char 或 unsigned char 在处理二进制数据时被误认为字符或数字的问题。它仅支持位运算,不支持算术运算。
-
-
-
std::void_t 模拟实现(元编程神器)
-
void_t 是探测类成员存在性的利器。虽然 C++17 已经内置,但理解其原理对元编程至关重要。
-
// 核心逻辑:无论传入什么类型,最终都映射为 void
template<typename... Args>
using my_void_t = void;
-
// 实际应用:探测类是否有 type 成员
template<typename T, typename = void>
struct has_type_member : std::false_type {};
template
struct has_type_member<T, my_void_t> : std::true_type {};
C++20 核心特性
核心语法
-
概念 (Concepts) ------ 模板编程的革命
-
知识点详述
- Concepts 是对模板参数的约束(Constraints)。在 C++20 之前,如果模板参数不符合要求,编译器会报出成百上千行难以阅读的错误(SFINAE 机制);有了 Concepts,我们可以直接定义模板参数必须满足的条件。
-
#include
#include
-
// 定义一个 Concept:必须是整型
template
concept Integral = std::is_integral_v;
// 使用方式 1:requires 子句
template
requires Integral
T Add(T a, T b) {
return a + b;
}
// 使用方式 2:直接作为类型前缀(最简洁)
void PrintInt(Integral auto n) {
std::cout << n << std::endl;
}
int main() {
Add(10, 20); // OK
// Add(1.1, 2.2); // 编译直接报错:不再报错在函数内部,而是报错在调用处类型不匹配
}
- 相似点区别(vs SFINAE / std::enable_if)
- SFINAE (enable_if):语法极其晦涩,报错信息在模板深处,编译慢。
- Concepts:语法自然(类似自然语言),报错信息极其清晰(明确指出哪个约束未满足),提高编译速度。
- 优缺点
- 优点:代码可读性极高;调试模板极其轻松;支持重载(根据不同约束选择最合适的函数)。
- 缺点:增加了新的关键字和语法负担。
-
三路比较运算符 (<=>, Spaceship Operator)
-
知识点详述
- 又称"航天飞机运算符"。只需定义一个 <=>,编译器就能自动生成 <、>、<=、>= 以及 ==、!=。
-
#include
-
struct Point {
int x, y;
// 自动生成所有比较逻辑
auto operator<=>(const Point&) const = default;
};
int main() {
Point p1{1, 2}, p2{1, 3};
if (p1 < p2) { /* 自动工作 */ }
}
- 核心返回值:排序类别 (Ordering)
- std::strong_ordering:强顺序(如整数,1就是1,严格相等)。
- std::partial_ordering:部分顺序(如浮点数,存在 NaN 无法比较的情况)。
- 优缺点
- 优点:极大减少冗余代码(Boilerplate code),以前写 6 个比较运算符,现在只需 1 行。
- 注意:= default 会按照成员定义的顺序进行逐个比较。
-
协程 (Coroutines) ------ 高并发利器
-
知识点详述
-
协程是能暂停执行并稍后恢复的函数。它在单个线程内实现多任务并发,非常适合网络 IO 和异步编程。C++20 提供的是协程底层框架,包含三个新关键字:
-
co_await:挂起当前协程,等待任务完成。
-
co_yield:挂起并返回一个值(常用于生成器)。
-
co_return:协程结束并返回结果。
-
-
-
模拟实现结构(极简模型)
-
协程的完整实现非常复杂,必须包含 promise_type 和 handle。以下是结构演示:
- #include
#include
- #include
-
-
struct Generator {
struct promise_type {
int current_value;
auto get_return_object() { return Generator{std::coroutine_handle<promise_type>::from_promise(*this)}; }
auto initial_suspend() { return std::suspend_always{}; }
auto final_suspend() noexcept { return std::suspend_always{}; }
auto yield_value(int value) { current_value = value; return std::suspend_always{}; }
void unhandled_exception() {}
void return_void() {}
};
std::coroutine_handle<promise_type> h;
};
Generator Counter() {
for (int i = 0; i < 3; ++i) co_yield i; // 每次 yield 都会暂停
}
- 优缺点
- 优点:相比回调函数(Callback Hell),协程可以用同步的代码逻辑写异步程序。
- 缺点:极其难用。C++20 只给了底层协议,没有给高层库。普通开发者建议等待 C++23/26 完善后的 std::execution 或使用第三方库(如 cppcoro)。
-
模块 (Modules) ------ 告别编译缓慢
-
知识点详述
- 取代了沿用数十年的 #include 预处理机制。
-
相似点区别(vs 传统头文件)
-
头文件:每次 #include 都是物理上的文本拷贝。如果 100 个文件包含同一个头文件,该头文件就被解析 100 次(导致编译慢)。
-
模块:只编译一次,生成二进制接口文件。其他文件通过 import 导入,不再重复解析。
-
-
代码举例
- // math.ixx (模块定义)
export module math;
export int add(int a, int b) { return a + b; }
- // math.ixx (模块定义)
-
// main.cpp
import math;
int main() { return add(1, 2); }
- 优缺点
- 优点:大幅缩短编译时间;彻底解决宏冲突(模块内的宏不会污染外部);逻辑隔离。
- 缺点:目前各大编译器(MSVC/GCC/Clang)和构建系统(CMake)的支持还存在微小差异。
-
指定初始化 (Designated Initializers)
-
知识点详述
- 借鉴自 C 语言,允许在初始化结构体时明确指定成员名称,增强代码自解释性。
-
struct Config {
int width;
int height;
bool fullscreen;
};
-
void Test() {
// 语法清晰,不易填错位
Config c = {
.width = 1920,
.height = 1080,
.fullscreen = true
};
}
- 限制(与 C 的区别)
- C++ 中必须严格按照成员定义的顺序进行初始化(C 语言可以乱序)。
-
consteval 与 constinit
-
区别对比
-
constexpr:可能是编译期,也可能是运行期(看参数)。
-
consteval:必须是编译期执行。如果无法在编译期算出结果,直接报错(称为立即函数)。
-
constinit:强制变量必须在编译期初始化,但变量本身不一定是 const 的。解决"全局变量初始化顺序(Static Init Order Fiasco)"问题的利器。
-
-
标准库与框架
-
Ranges 库 () ------ 容器操作的逻辑革命
-
知识点详述
- Ranges(范围)库是 C++20 最大的库更新。它允许我们通过管道操作符 | 将各种算法(过滤、转换、截取)组合在一起。它的核心思想是延迟计算(Lazy Evaluation):只有在真正迭代数据时,计算才会发生。
-
#include
#include
#include
#include
-
int main() {
std::vector nums = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
// 需求:取偶数 -> 平方 -> 取前 3 个结果
auto result = nums | std::views::filter([](int n) { return n % 2 == 0; })
| std::views::transform([](int n) { return n * n; })
| std::views::take(3);
for (int v : result) {
std::cout << v << " "; // 输出:4 16 36
}
}
- 相似点区别 (vs 传统 STL 算法)
- 传统 STL:如 std::transform。必须传入迭代器 begin/end,且通常需要创建一个临时容器来存储中间结果,代码冗长且性能有浪费。
- Ranges:直接传入容器名;支持组件组合;不产生中间临时容器。
- 优缺点
- 优点:代码极度简洁,逻辑高度组合化;延迟计算节省内存和 CPU。
- 缺点:编译时间显著增加;调试时的调用栈极其复杂。
-
std::span () ------ 连续内存的通用视图
-
知识点详述
- std::span 是一个轻量级的非拥有式视图,它代表一段连续的内存。它可以包装 C 风格数组、std::vector、std::array。
-
相似点区别 (vs std::string_view)
-
string_view:专门针对只读字符串。
-
span:针对任何连续类型的数据,且支持通过视图修改原始数据(除非定义为 std::span)。
-
-
#include
#include
#include
-
// 函数不再关心传入的是 vector 还是数组,只要是连续内存即可
void FillZero(std::span data) {
for (auto& x : data) x = 0;
}
int main() {
int arr[] = {1, 2, 3};
std::vector v = {4, 5, 6};
FillZero(arr); // 包装 C 数组
FillZero(v); // 包装 vector
}
- 优缺点
- 优点:替代"指针+长度"的危险写法;提高接口通用性;性能等同于原始指针。
- 缺点:不拥有数据,需开发者保证原始数据生命周期(与 string_view 一致)。
-
std::format () ------ 现代格式化
-
知识点详述
- 结合了 printf 的简洁和 cout 的类型安全。使用 {} 作为占位符,支持复杂的格式化控制。
-
#include
#include
#include
-
int main() {
std::string s = std::format("Hello, {}! You have {:0>5d} messages.", "Alice", 42);
std::cout << s << std::endl;
// 输出:Hello, Alice! You have 00042 messages.
}
- 优缺点
- 优点:语法非常现代(类似 Python);比 ostream 性能更高;支持自定义类型的格式化扩展。
- 缺点:目前某些旧版编译器支持不全。
-
std::jthread () ------ 协同中断线程
-
知识点详述
-
jthread (Joining Thread) 是对 std::thread 的改良。
-
RAII 自动合并:析构时如果线程还在运行,会自动调用 join(),避免崩溃。
-
停止令牌 (Stop Token):原生支持从外部请求线程停止。
-
-
// 简化的原理演示
class SimpleJThread {
std::thread _t;
public:
template<typename F, typename... Args>
SimpleJThread(F&& f, Args&&... args) : _t(std::forward(f), std::forward(args)...) {}
~SimpleJThread() {
if (_t.joinable()) {
_t.join(); // 析构自动 join,这就是 jthread 的核心行为
}
}
};
-
代码举例 (停止令牌)
- #include
-
void Worker(std::stop_token st) {
while (!st.stop_requested()) {
// 执行任务...
}
}
int main() {
std::jthread jt(Worker);
// 无需手动 join,jt 析构时会自动 join
// 也可以手动发送停止信号:
jt.request_stop();
}
-
数学常量与 std::numbers
-
知识点详述
- C++20 终于在 头文件中提供了官方的数学常量(π, e, sqrt2 等),不再需要自己定义宏。
-
#include
#include
-
int main() {
std::cout << std::numbers::pi << std::endl; // 3.14159...
std::cout << std::numbers::sqrt2 << std::endl;
}
-
std::atomic_ref ()
-
知识点详述
- 允许对一个非原子变量进行临时的原子操作。
-
场景说明
-
你有一个大型结构体,平时在单线程下处理。但在某个特定算法中,需要多个线程并发累加其中的某个成员。
-
优点:不需要将整个变量定义为 atomic(atomic 变量通常有额外的内存开销和对齐要求),仅在需要时赋予原子属性。
-
-