C++ 学习补充
前面已有一些篇章,将常使用到的特性进行了简单的描述,这里针对 C++17以上的新特性进行补充,以及对一些案例做较为详细的解析。
1 C++11 新特性
这是一次大的版本更新,再次拔高了C++的生态,由于特性太多,记不常用特性。
1.1 容器 STL
新增了无序容器:其中 std::unordered_map/set 较为常用,主要介绍 std::unordered_multimap/set。
相比之下,multimap 主要差别在键可重复:
unordered_multimap<string, int> ummap;
ummap.insert({"apple", 10});
ummap.insert({"apple", 30}); // 重复 key,成功插入新键值对
// 不支持 operator[],以下代码编译错误
// cout << "apple: " << ummap["apple"] << endl;
// 查找元素:返回所有 key 为 "apple" 的键值对
auto range = ummap.equal_range("apple");
cout << "apple 的值有:";
for (auto it = range.first; it != range.second; ++it) {
cout << it->second << " "; // 输出 10 30
}
新增了列表容器 std::forward_list,使用方法同 std::list。但新容器使用单向链表实现,因此只支持头插法,当无需双向迭代时,会有更高的空间利用率。
新增容器 std::array,他保存在栈内存中,初始化时需要指定类型与大小:
std::array<int, 4> arr= {1,2,3,4}; // 初始化
void foo(int *p, int len) {}
foo(arr.data(), arr.size()); // 兼容 C 风格接口
std::sort(arr.begin(), arr.end()); // 使用 std::sort
下面针对常用 STL 进行一些对比,从而更好的理解使用场景:
| 容器 | 底层结构 | 随机访问 | 头插/尾插 | 中间插入/删除 | 内存占用 | 迭代器失效 | 一句话场景 |
|---|---|---|---|---|---|---|---|
| std::vector | 连续数组 | O(1) | 尾插O(1)均摊 | O(n) | 最小(仅元素) | 重分配全失效 | “默认就用它”,缓存友好、随机访问多 |
| std::deque | 分块链表(map+块) | O(1) | 头尾插O(1) | O(n) | 稍高(块+map) | 头尾不失效 | 头尾都要频繁增长,如调度队列、滑动窗口 |
| std::list | 双向链表 | ×O(n) | 头尾插O(1) | 定位后O(1) | 每节点两指针 | 仅当前节点 | 大对象且中间插入极多,指针已指向节点 |
容器 vector、list 的结构比较好理解,而 deque 可能更加陌生,下面是他的实现结构:
map (T**)
+-----+-----+-----+-----+
| * | * | * | * | → 指向一块连续存储
+--|--+--|--+--|--+--|--+
| | | |
[buf] [buf] [buf] [buf] 每个 buf 固定大小(如 512 B)
结构 deque 也能连续地址随机访问,其中第一级索引 map<T*> → O(1) 定位到块,第二级块内偏移 → O(1) 定位到元素。
1.2 反向迭代器
- rbegin():返回指向容器最后一个元素的反向迭代器;
- rend():返回指向容器第一个元素前位置的反向迭代器;
若迭代元素仅仅用于只读,那么可以使用 crbegin()、crend()
当使用迭代器时,auto it = v.rbegin(); 将自动推导为 std::vector<int>::reverse_iterator 成员。
1.3 正则表达式
C++11 提供的正则表达式库操作 std::string 对象,对模式 std::regex(本质是 std::basic_regex)进行初始化,通过 std::regex_match 进行匹配,从而产生 std::smatch(本质是 std::match_results 对象)。
std::regex base_regex("[a-z]+\\.txt");
bool fg = std::regex_match(fname, base_regex); // 仅判断是否匹配成功
std::smatch base_match;
std::regex_match(fname, base_match, base_regex); // 可获取匹配结果
base_match.size();
base_match[0].str();
1.4 内存对齐
相比 C语言时代依赖编译器扩展 __attribute__((aligned)) / __declspec(align) 只能对齐变量/结构体,而 C++是语言级关键字,支持 class、成员、局部变量、new 表达式、模板元编程 —— 功能和可移植性都提升。
-
查询对齐:
alignof与std::alignment_ofalignof(T)是运算符,返回类型T的对齐字节数;std::alignment_of<T>::value是trait 包装,结果与alignof(T)相同,但可用于模板元编程。在C+14后简化为std::alignment_of_v<T>;
-
指定对齐:
alignasalignas(N)写在类型/变量前,强制对齐到 N 字节;- 可用于类、结构体、成员、局部变量;
- 若
N不是 2 的幂或小于自然对齐,编译器报错;
-
获得“对齐且足够大”的原始存储:
std::aligned_storageC++23 起被标记为 deprecated,不会调用构造函数,必须配合 placement new 和 手动析构。
std::aligned_storage<sizeof(A), alignof(A)>::type data; // 声明对齐类型 A *attr = new (&data) A; // placement new 构造 attr->~A(); // 手动析构 -
分配并对齐:
aligned_alloc与::operator new在 C+17特性中,如果希望以C语言风格去
malloc内存:void* ptr = std::aligned_alloc(align, sizeof(MyStruct)); MyStruct* p = new (ptr) MyStruct; // 使用完后 p->~MyStruct(); std::free(ptr);通常以
pure C对外接口时会如案例中使用,以 C+的风格,更推荐:// C++ 17 风格 void* raw = ::operator new(sizeof(A), std::align_val_t{128}); A* p = new (raw) A{42}; // 只构造 p->~A(); // 必须手动析构 ::operator delete(raw, std::align_val_t{128}); // C++ 20 风格 A* p = new (std::align_val_t(128)) A{42}; delete p;
2 多线程与锁
在 C+11之前,还只能使用 POSIX 标准的 pthread 库来操作多线程,当前引入了 std::thread 来创建线程,支持对线程 join 或者 detach。
#include <thread>
auto func = []() { thread_local int count = 0; }; // 支持线程变量
std::thread t(func);
if (t.joinable()) {
t.detach();
# t.join();
}
t.get_id(); // 获取线程ID
为了保证多线程可以同时操作共享数据,引入了 std::mutex 等线程同步手段,大致可分为四种:
- std::mutex:独占的互斥量,不可重入,不带超时功能
- std::recursive_mutex:递归互斥量,可重入,不带超时功能
- std::timed_mutex:带超时的互斥量,不可重入
- std::recursive_timed_mutex:带超时的互斥量,可重入
可重入(reentrancy) 每次“再 lock”都会把内部计数器 +1,解锁时 -1,减到 0 才真正释放;而不可重入,在已经持有锁的线程若再次 lock,会立刻死锁
#include <mutex>
std::mutex mutex_;
mutex_.lock();
mutex_.unlock();
// 若指定超时时间
std::timed_mutex timed_mutex_;
timed_mutex_.try_lock_for(std::chrono::milliseconds(200));
timed_mutex_.unlock();
从 C++11奠基一直到 C++20 爆发,异步编程有非常多的特性,如:async、future、协程等等新概念,详见 Blog.
2.1 时钟
时间是一个较为复杂的概念,下面逐步进行讲解。首先明确时间单位的认知:
1 秒 = 1000 毫秒 = 1e6 微秒 = 1e9 纳秒
2.1.1 核心概念
-
时钟(Clock):提供当前时间的接口,包含时间点类型和分辨率。
时钟能够提供当前的时间,有 3种标准时钟:
-
system_clock:系统时间(可被修改),能用于获取当前时区的时间 -
steady_clock:单调时钟(不可修改,适合计时),相对于系统开机启动的时间 -
high_resolution_clock:高分辨率时钟(通常是steady_clock的别名)利用函数
noew()获取当前时间点,如:std::chrono::system_clock::now();
-
-
时间间隔(Duration):表示两个时间点的差值(如“500 毫秒”),由数值和单位组成。
template <class Rep, class Period = ratio<1> > class duration;Rep 表示一种数值类型,用来表示 Period 的数量,如 int、float…
Period 是 ratio 类型,用来表示【用秒表示的时间单位】,常用的定义有:
- ratio<60, 1>:minutes
- ratio<1 , 1>:seconds
- ratio<1 , 1000>:microseconds
- ….
template <intmax_t N, intmax_t D = 1> class ratio;N 代表分子,D 代表分母,故 ratio 表示一个分数(60*1s=1min,单位:1min)。
用户可以自定义 Period,如:ratio<2, 1>表示单位时间是2秒。
-
时间点(Time point):表示某个具体时刻(如 “2024-09-23 12:00:00”),基于时钟和时间间隔定义。
绑定到特定时钟的时刻,定义为:
time_point<Clock, Duration>
2.1.2 实际案例
-
时间间隔
#include <chrono> // 表示"2秒"和"500毫秒" std::chrono::seconds sec(2); // 2秒 std::chrono::milliseconds ms(500); // 500毫秒 // 时间间隔转换 auto sec_from_ms = std::chrono::duration_cast<std::chrono::seconds>(ms); std::cout << "500毫秒转换为秒:" << sec_from_ms.count() << "秒(截断)" << std::endl; -
时钟
系统时钟:
// system_clock:获取当前系统时间(可对应日历时间) auto sys_now = std::chrono::system_clock::now(); // 时间点 // 转换为 time_t(秒级),再格式化输出 std::time_t sys_time = std::chrono::system_clock::to_time_t(sys_now); std::cout << "当前系统时间:" << std::ctime(&sys_time); // 如:Mon Sep 23 12:00:00 2024单调时钟:
// steady_clock:适合计时(不受系统时间修改影响) auto start = std::chrono::steady_clock::now(); std::this_thread::sleep_for(std::chrono::seconds(1)); // 休眠1秒 auto end = std::chrono::steady_clock::now(); auto elapsed = end - start; // 时间点相减得到时间间隔 std::cout << "休眠时间(微秒):" << std::chrono::duration_cast<std::chrono::microseconds>(elapsed).count() << std::endl; // 约1e6微秒 -
时间点计算
// 时间点计算:3天后的系统时间 auto future = sys_now + std::chrono::hours(24 * 3); std::time_t future_time = std::chrono::system_clock::to_time_t(future); std::cout << "3天后的时间:" << std::ctime(&future_time);
上述大致是 C++11的时间篇章的内容,还不涉及时区,日历等概念,在 C++20后会得到扩展。
2.2 条件算法
-
all_of:检测表达式是否对范围[first, last)中所有元素都返回true,如果都满足,则返回true
std::vector<int> v(10, 2); if (std::all_of(v.cbegin(), v.cend(), [](int i) { return i % 2 == 0; })) { std::cout << "All numbers are even\n"; } -
any_of:检测表达式是否对范围[first, last)中至少一个元素返回true,如果满足,则返回true,否则返回false,用法和上面一样
-
none_of:检测表达式是否对范围[first, last)中所有元素都不返回true,如果都不满足,则返回true,否则返回false,用法和上面一样
-
find_if_not:找到第一个不符合要求的元素迭代器,和find_if相反
-
copy_if:复制满足条件的元素
-
itoa:对容器内的元素按序递增
std::vector<int> l(10); std::iota(l.begin(), l.end(), 19); // 19为初始值 for (auto n : l) std::cout << n << ' '; // 19 20 21 22 23 24 25 26 27 28 -
minmax_element:返回容器内最大元素和最小元素位置
std::vector<int> v = {3, 9, 1, 4, 2, 5, 9}; auto result = std::minmax_element(v.begin(), v.end()); std::cout << "min element at: " << *(result.first); // min element at: 1 std::cout << "max element at: " << *(result.second); // max element at: 9 -
is_sorted、is_sorted_until:返回容器内元素是否已经排好序。
3 C++14 新特性
相比 C11,它算是一个补丁,下面列出当前工作中有改善的部分功能:
3.1 变量模板
template<class T>
constexpr T pi = T(3.1415926535897932385L);
int main() {
cout << pi<int> << endl; // 3
cout << pi<double> << endl; // 3.14159
return 0;
}
在数学计算中常见,能支持多个不同精度的类型。如上述中,使用 int、double 的圆周率仅需定义一次。
3.2 字面量分隔符
该功能主要体现在书写可读性上,如:
int a = 0b0001'0011'1010;
double b = 3.14'1234'1234'1234;
引入了分隔符,方便阅读。
3.3 变参数模板
无比实用的特性之一,能对任意个变量进行读入,实际编程中有多种玩法:
-
递归展开
auto sum() { return 0; } template<typename Type, typename... Types> auto sum(Type& arg, Types&... args){ return arg + sum(args...); } // 模板参数包 typename... Types // 函数参数包 Types&... args案例利用“递归剥头”,进行求和。
能使用
sizeof获取实参的个数,且下面两种方法等价:sizeof...(args)和sizeof...(Types)。 -
折叠表达式
语法:
- 一元左折:( args + … )
- 一元右折:( … + args )
- 二元左折:( init + … + args )
- 二元右折:( args + … + init )
左折 / 右折都是编译期把包展开成一串运算符,但结合顺序不同,如:
- 左折
(val + ...):((a + b) + c) + d - 右折
(... + val):a + (b + (c + d))
左折求和
template <typename... Ts> auto sum(Ts... v) { return (v + ...); } // 空包时返回 0 // 相比案例 1. 的求和计算,递归会促使编译器生成 N 层函数调用; // 左折,编译器只有一条表达式:return (a + b + c + d);右折链式赋值
template <typename T, typename... Rest> void chain_set(T& head, Rest&... rest) { (head = ... = rest); // a = b = c = d }至于为什么会区分左、右,是由于运算符不是都可交换,如加法/乘法左右结果相同,而链式赋值、流插入等场景,顺序影响语义。
由于参数不确定,那么会出现空包行为,不同的运算符在空包时会有默认的空包值,假设使用了自定义运算符,则必须写成二元形式。
如逗号
,运算符的空包值为void()无值,若希望空包时函数有默认行为:template <typename... F> void call_all(F... f) { (f(), ...); // 一元左折,空包时 **无表达式** // 编译期通过,但运行时 **啥也不执行** (f(), ... , void()); // 二元右折,空包时返回 void() } -
结合完美转发
template <typename T, typename... Args> T* create(Args&&... args) { return new T(std::forward<Args>(args)...); }模板编程时,通常结合完美转发避免额外的拷贝,因此不要使用
new T(args...)的方式 → 会丢失右值性质 。 -
调试技巧
template <typename... Ts> void dbg(Ts&&... args) { auto t = std::forward_as_tuple(args...); std::apply([](auto&&... a) { ((std::cout << a << ' '), ...); }, t); }上述案例可全程完美转发,不丢失右值性。
4 C++17 新特性
介绍常用新特性
4.1 string_view
void fun(std::string_view str) { ... }
// fun("world")
string_view 直接指向字符串字面量的内存(.data() 指向 "world" 的首地址,.size() 为 5),不做任何拷贝,仅存储指针和长度,在 64位系统中,仅占 16字节(8 字节指针 + 8 字节长度)。
它的使用场景非常明确:① 字符串仅进行只读操作 ② 兼容多种字符串类型作为入参。
string_view 不管理原始字符串的生命周期 ,它的生命周期由外部决定,需要特别注意。
下面进行一些字符串的扩展:
void fun(std::string str) { ... }
void fun(std::string& str) { ... }
void fun(std::string&& str) { ... }
std::string s = "world";
fun(s); // 匹配 fun(std::string& str)
fun(std::string("world")); // 匹配 fun(std::string&& str)
fun("world"); // 字面量,const char* 隐式转换为 std::string,匹配 fun(std::string&& str)
// 通常为了避免歧义,也会引入
void fun(const char* str) { ... }
即需要了解,值类别与引用的基础匹配(无隐式转换时) :
- 左值 → 优先匹配 非 const 左值引用(T&),其次匹配 const 左值引用(const T&),最后匹配 值传递(T)(需拷贝);
- 右值 → 优先匹配 右值引用(T&&),其次匹配 const 左值引用(const T&),最后匹配 值传递(T)(需拷贝)。(不考虑 const T&&,无法移动的右值基本不使用)
隐式转换的影响:
如果实参类型与形参类型不直接匹配(如 const char* vs std::string),需要先进行隐式转换(如 const char* → std::string),此时 “转换后的实参值类别” 和 “转换成本” 会改变匹配优先级。
上述案例中,所表诉的歧义在于,若存在以下重载:
void fun(const std::string& str) { ... }
C++ 标准规定 const T& 可绑定左值、右值、临时对象(类似万能引用,但无法推导左右值),编译器通常会匹配更通用的 const std::string&,但部分编译器可能将此视为“歧义” 。
5 C++20 新特性
5.1 时钟
新特性增加了时区、日历等核心功能,在这之前进行基础知识的科普:
-
UTC(协调世界时,Coordinated Universal Time)
基于原子钟的物理时间,是全球通用的标准时间,不受时区、夏令时影响,是国际上统一的时间基准。
-
GMT(格林尼治标准时间,Greenwich Mean Time)
基于地球自转的天文时间,受地球自转不均匀,季节、潮汐等影响,于 1972 年起被 UTC 取代。
**在现阶段的开发中,所有时区的计算、时间戳的基准,都将 UTC 作为基准标识。**以 GMT 作为基准的的场景极度罕见,部分历史接口显示 GMT 内部实现也是 UTC 时间,只因历史习惯沿用该名称(如:HTTP 协议头、C 语言 gettime()函数等)。
Unix 时间戳的标准起点是 1970-01-01 00:00:00 UTC,当需要转换为“人类可理解的本地时间”时,则需要根据时区“加/减偏离时间”:
- 东时区(如 UTC+8、UTC+9):本地时间 = UTC 时间 + 偏移小时(因东时区比 UTC 快)
- 西时区(如 UTC-5、UTC-8):本地时间 = UTC 时间 - 偏移小时(因西时区比 UTC 慢)
举例:
-
存储:
用户 2024-09-23 08:00(UTC+8)提交订单,后端将其转换为 UTC 时间 2024-09-23 00:00,存储为时间戳
1726972800(秒级,10位)。 -
显示:
- 北京用户查看(UTC+8):UTC 时间
1726972800+ 8 小时 → 2024-09-23 08:00; - 纽约用户查看(UTC-5) :UTC 时间
1726972800- 5 小时 → 2024-09-22 19:00(纽约比北京晚 13 小时);
- 北京用户查看(UTC+8):UTC 时间
5.2 关键扩展
-
时间字面量
启用
chrono_literals后 ,可直接书写时间字面量:// 启用时间字面量(2h、30min等) using namespace std::chrono_literals; // 时间字面量与便捷操作 auto duration = 2h + 30min; // 2小时30分钟(字面量直接相加) std::cout << "2h30min = " << duration.count() << "分钟\n"; // 150分钟 -
日历类型
使用
year_month_day表示日期:// 日历类型:直接操作年、月、日 std::chrono::year y{2024}; std::chrono::month m{9}; // 9月(1-12) std::chrono::day d{23}; // 23日 std::chrono::year_month_day ymd{y, m, d}; // 2024-09-23使用
weekday获取星期:// 转换为星期 std::chrono::weekday wd{ymd}; // 星期几(0=周日,1=周一...6=周六) std::cout << "2024-09-23是星期" << wd.c_encoding() << "(1=周一)\n"; // 输出:1(周一)支持日期加减(如
ymd + 100d计算 100 天后的日期)#include <format> // C++20 格式化库(需编译器支持) // 日历计算:100天后的日期 auto future_ymd = ymd + 100d; // d是天数字面量(需chrono_literals) std::cout << "100天后是:" << std::format("{}-{:02}-{:02}\n", future_ymd.year(), future_ymd.month(), future_ymd.day()); // 2024-12-31可完全摆脱 C 风格函数。
-
时钟增强
-
utc_clock:明确以 UTC 时间为基准的时钟(避免
system_clock的本地时间歧义)。 -
file_clock:用于文件时间戳(如
std::filesystem中的文件创建时间)。主要是为了屏蔽不同操作系统的文件系统实现差异,如 Windows 的文件时间戳以 “1601-01-01 00:00:00 UTC” 为纪元(起点),单位是 100 纳秒;而 Linux 的文件时间戳以 “1970-01-01 00:00:00 UTC” 为纪元,单位是纳秒。
在上述背景下,操作文件的时间较为繁琐,
file_clock::time_point屏蔽了这些问题:namespace ch = std::chrono; namespace fs = std::filesystem; const fs::path file_path = "/path/to/file"; ch::file_clock::time_point file_modify_time = fs::last_write_time(file_path); ch::zoned_time local_modify_time {ch::current_zone(), file_modify_time}; std::cout << "文件最后修改时间:" << std::format("{0:%F %T}\n", local_modify_time);
-
-
时区支持
通过
locate_zone("Asia/Shanghai")获取时区规则(包含夏令时信息),zoned_time自动完成 UTC 与本地时间的转换,无需关心偏移细节。// 获取当前UTC时间 auto utc_now = ch::utc_clock::now(); std::cout << "当前UTC时间:" << std::format("{0:%F %T}\n", utc_now); // 如2024-09-23 00:00:00 // 转换为北京时区(UTC+8) auto beijing_tz = ch::locate_zone("Asia/Shanghai"); // 查找时区 ch::zoned_time beijing_time{beijing_tz, utc_now}; // 绑定时区和时间点 std::cout << "北京时区时间:" << std::format("{0:%F %T}\n", beijing_time); // 2024-09-23 08:00:00 // 转换为纽约时区(UTC-4/5,自动处理夏令时) auto nyc_tz = ch::locate_zone("America/New_York"); ch::zoned_time nyc_time{nyc_tz, utc_now}; std::cout << "纽约时区时间:" << std::format("{0:%F %T}\n", nyc_time); // 2024-09-22 20:00:00(夏令时) -
时间点操作
历史版本,时间点取整涉及单位转换、取模等复杂逻辑换算,当前提供更便捷的函数
ch::floor<ch::hours>(now),并支持hours/minutes/days等多种单位。// 时间点取整:将当前时间向下取整到最近的小时 auto now = ch::system_clock::now(); auto hour_aligned = ch::floor<ch::hours>(now); // 如14:35:22 → 14:00:00 std::cout << "当前时间向下取整到小时:" << std::format("{0:%T}\n", hour_aligned);