目录

C++ 学习补充

前面已有一些篇章,将常使用到的特性进行了简单的描述,这里针对 C++17以上的新特性进行补充,以及对一些案例做较为详细的解析。

这是一次大的版本更新,再次拔高了C++的生态,由于特性太多,记不常用特性。

新增了无序容器:其中 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) 定位到元素。

  • rbegin():返回指向容器最后一个元素的反向迭代器;
  • rend():返回指向容器第一个元素前位置的反向迭代器;

若迭代元素仅仅用于只读,那么可以使用 crbegin()、crend()

当使用迭代器时,auto it = v.rbegin(); 将自动推导为 std::vector<int>::reverse_iterator 成员。

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();

相比 C语言时代依赖编译器扩展 __attribute__((aligned)) / __declspec(align) 只能对齐变量/结构体,而 C++是语言级关键字,支持 class、成员、局部变量、new 表达式、模板元编程 —— 功能和可移植性都提升。

  1. 查询对齐:alignofstd::alignment_of

    • alignof(T)运算符,返回类型 T 的对齐字节数;
    • std::alignment_of<T>::valuetrait 包装,结果与 alignof(T) 相同,但可用于模板元编程。在C+14后简化为 std::alignment_of_v<T>
  2. 指定对齐:alignas

    • alignas(N) 写在类型/变量前,强制对齐到 N 字节
    • 可用于类、结构体、成员、局部变量
    • N 不是 2 的幂或小于自然对齐,编译器报错;
  3. 获得“对齐且足够大”的原始存储:std::aligned_storage

    C++23 起被标记为 deprecated不会调用构造函数,必须配合 placement new手动析构

    std::aligned_storage<sizeof(A), alignof(A)>::type data;    // 声明对齐类型
    A *attr = new (&data) A;    // placement new 构造
    attr->~A();                 // 手动析构
    
  4. 分配并对齐: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;

在 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.

时间是一个较为复杂的概念,下面逐步进行讲解。首先明确时间单位的认知:

1 秒 = 1000 毫秒 = 1e6 微秒 = 1e9 纳秒

  1. 时钟(Clock):提供当前时间的接口,包含时间点类型和分辨率。

    时钟能够提供当前的时间,有 3种标准时钟:

    • system_clock:系统时间(可被修改),能用于获取当前时区的时间

    • steady_clock:单调时钟(不可修改,适合计时),相对于系统开机启动的时间

    • high_resolution_clock:高分辨率时钟(通常是 steady_clock 的别名)

      利用函数 noew() 获取当前时间点,如:std::chrono::system_clock::now();

  2. 时间间隔(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秒。

  3. 时间点(Time point):表示某个具体时刻(如 “2024-09-23 12:00:00”),基于时钟和时间间隔定义。

    绑定到特定时钟的时刻,定义为:time_point<Clock, Duration>

  1. 时间间隔

    #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;
  2. 时钟

    系统时钟:

    // 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. 时间点计算

    // 时间点计算: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后会得到扩展。

  1. 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";
    }
  2. any_of:检测表达式是否对范围[first, last)中至少一个元素返回true,如果满足,则返回true,否则返回false,用法和上面一样

  3. none_of:检测表达式是否对范围[first, last)中所有元素都不返回true,如果都不满足,则返回true,否则返回false,用法和上面一样

  4. find_if_not:找到第一个不符合要求的元素迭代器,和find_if相反

  5. copy_if:复制满足条件的元素

  6. 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
    
  7. 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
    
  8. is_sorted、is_sorted_until:返回容器内元素是否已经排好序。

相比 C11,它算是一个补丁,下面列出当前工作中有改善的部分功能:

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 的圆周率仅需定义一次。

该功能主要体现在书写可读性上,如:

int a = 0b0001'0011'1010;
double b = 3.14'1234'1234'1234;

引入了分隔符,方便阅读。

无比实用的特性之一,能对任意个变量进行读入,实际编程中有多种玩法:

  1. 递归展开

    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)

  2. 折叠表达式

    语法:

    • 一元左折:( 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()
    }
  3. 结合完美转发

    template <typename T, typename... Args>
    T* create(Args&&... args)
    {
        return new T(std::forward<Args>(args)...);
    }

    模板编程时,通常结合完美转发避免额外的拷贝,因此不要使用 new T(args...) 的方式 → 会丢失右值性质 。

  4. 调试技巧

    template <typename... Ts>
    void dbg(Ts&&... args)
    {
        auto t = std::forward_as_tuple(args...);
        std::apply([](auto&&... a) { ((std::cout << a << ' '), ...); }, t);
    }

    上述案例可全程完美转发,不丢失右值性。

介绍常用新特性

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&,但部分编译器可能将此视为“歧义” 。

新特性增加了时区、日历等核心功能,在这之前进行基础知识的科普:

  1. UTC(协调世界时,Coordinated Universal Time)

    基于原子钟的物理时间,是全球通用的标准时间,不受时区、夏令时影响,是国际上统一的时间基准。

  2. 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 慢)

举例:

  1. 存储:

    用户 2024-09-23 08:00(UTC+8)提交订单,后端将其转换为 UTC 时间 2024-09-23 00:00,存储为时间戳 1726972800(秒级,10位)。

  2. 显示:

    • 北京用户查看(UTC+8):UTC 时间 1726972800 + 8 小时 → 2024-09-23 08:00;
    • 纽约用户查看(UTC-5) :UTC 时间 1726972800 - 5 小时 → 2024-09-22 19:00(纽约比北京晚 13 小时);
  1. 时间字面量

    启用 chrono_literals 后 ,可直接书写时间字面量:

    // 启用时间字面量(2h、30min等)
    using namespace std::chrono_literals;
    
    // 时间字面量与便捷操作
    auto duration = 2h + 30min;  // 2小时30分钟(字面量直接相加)
    std::cout << "2h30min = " << duration.count() << "分钟\n";  // 150分钟
    
  2. 日历类型

    使用 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 风格函数。

  3. 时钟增强

    • 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);
  4. 时区支持

    通过 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(夏令时)
    
  5. 时间点操作

    历史版本,时间点取整涉及单位转换、取模等复杂逻辑换算,当前提供更便捷的函数 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);