现代C++特性

现代C++特性

C++11

统一的列表初始化 {}

  • 基本用法

    • C++11 扩大了用大括号 {} 括起的列表的使用范围,使其可用于所有内置类型和用户自定义类型。使用时可以添加 =,也可以不添加。

      • 内置类型与数组:

        • int x = {1};
          int y{2}; // 建议用法
          int array1[]{1, 2, 3};
          int* pa = new int[3]{0}; // 动态分配初始化
      • 自定义对象:

        • 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 列表初始化
  • 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;

        };

        }

变量类型推导与声明

  • 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;

      };

      }

  • 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;
  • std::bind (适配器)

    • 用于调整可调用对象的参数:改变参数顺序、固定某些参数的值。

    • 占位符:std::placeholders::_1 代表第一个参数。

      • // 固定第一个参数为 100
        auto newFunc = bind(Plus, 100, placeholders::_1);
        newFunc(20); // 实际调用 Plus(100, 20)

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++; // 线程安全
    • 互斥锁与 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;
        }

内存泄漏 (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;

    };

C++ 标准库智能指针

  • std::auto_ptr (C++98) ------ 管理权转移

    • 原理:拷贝时将原指针置空,将管理权交给新指针。

    • 缺点:极其危险。拷贝后原对象悬空(NULL),访问会崩溃。

    • 模拟实现关键代码:

      • auto_ptr(auto_ptr& sp) : _ptr(sp._ptr) {
        sp._ptr = nullptr; // 管理权转移,原对象变空
        }
  • std::unique_ptr (C++11) ------ 防拷贝

    • 原理:简单粗暴,直接禁止拷贝和赋值。

    • 适用场景:独占一份资源。

    • 实现方式:使用 = delete 禁用拷贝构造和赋值。

      • unique_ptr(const unique_ptr& sp) = delete;
        unique_ptr& operator=(const unique_ptr& sp) = delete;
  • std::shared_ptr (C++11) ------ 引用计数(最常用)

    • 原理:多个指针共享同一份资源,通过内部的引用计数记录使用者数量。

    • 机制:

      • 拷贝/赋值时:计数 +1。

      • 析构时:计数 -1。

      • 计数减为 0 时:释放资源。

    • 线程安全问题:

      • 计数本身是安全的:标准库通过加锁(或原子操作)保证了引用计数的加减是线程安全的。

      • 指向的资源不一定安全:智能指针指向的对象并发访问仍需手动加锁。

    • shared_ptr 的核心模拟实现(含线程安全)

      • shared_ptr 的核心在于引用计数,但由于引用计数是多个对象共享的,在多线程环境下必须保证计数的原子性。

        • #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; }
          };

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;
        };

定制删除器 (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++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

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; }

// 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 变量通常有额外的内存开销和对齐要求),仅在需要时赋予原子属性。

相关推荐
MediaTea2 小时前
Python:比较协议
运维·服务器·开发语言·网络·python
tankeven2 小时前
自创小算法00:数据分组
c++·算法
wuqingshun3141592 小时前
说一下JVM内存结构
java·开发语言·jvm
苏宸啊2 小时前
OS环境变量
linux·c++
33三 三like2 小时前
高精度计算
开发语言·c++·算法
Hello.Reader2 小时前
Tauri 项目结构前端壳 + Rust 内核,怎么协作、怎么构建、怎么扩展
开发语言·前端·rust
sycmancia2 小时前
C++——二阶构造模式
c++
lsx2024062 小时前
SQLite 命令详解
开发语言
csbysj20202 小时前
CSS3 2D 转换
开发语言