Important

以下示例部分测试于 x86-64 Windows10/11 mingw G++ 编译器,其中写明的特殊情况在不同平台上会有所区别,对于跨平台程序请尽量避免触及特殊情况。

C++ 的历史

早期 C++

在 C++ 正式产生,组建标准委员会之前,C++ 的雏形就经历过数次更迭。

  • 1979 年,含类功能的 C 被实现 加入特性:类、成员函数、继承类、分离编译、访问控制、朋友功能、实参类型检查、形参默认值、内联函数、重载赋值运算符、构造与析构函数、空实参表调用和带 void 实参表调用等效、函数的调用与返回
  • 1985 年,Cfront 1.0 发布 加入特性:虚函数、函数与运算符重载、引用、newdelete 堆内存分配运算符、常量关键字、作用域解析运算符
  • 1985 年,C++ 第1个版本发布
  • 1989 年,Cfront 2.0 发布 加入特性:多重继承、成员指针、protected 访问控制、类型安全链接、抽象类、静态与 const 修饰成员函数、指定类的堆内存分配和释放
  • 1990 年,Annotated C++ 参考手册发布,指导 C++ 发展的标准化,直到 ISO 标准开始出现 加入特性:名称空间、异常处理、嵌套类、模板(aka. 泛型)
  • 1991 年,C++ 第2个版本发布

标准 C++

  • 1990 年,美国国家标准学会 C++ 委员会成立
  • 1991 年,国际标准化组织 C++ 委员会成立

C++标准概览

Note

此节内容来自于 C++ 参考手册网站 https://www.en.cppreference.com/;

可访问 https://www.en.cppreference.com/w/cpp/language/history.html 查看详细的 C++ 标准历史更替,这里只提及主要标准和关键性的标准变化。

C++98

在 1998 年发布的第 1 个 C++ 标准 (ISO/IEC 14882:1998)。C++ 标准也继承了部分 C 标准的内容。

  • 加入特性: 运行时类型识别 RTTI(动态类型转换)、typeid()、自定义类运算符函数、mutablebool、条件中声明、模板实例化、成员模板、异常处理、名称空间、类对象的动态内存分配、引用、const 关键字标识成员函数
  • 新增 C++ 库: auto_ptr, iostream, complex, etc.
  • 加入标准模板库 (Standard Template Library): 容器、算法、迭代器、函数对象

C++11

在 2011 年发布的 C++ 标准 (ISO/IEC 14882:2011)。

  • 匿名函数(aka. lambda 表达式);

  • 右值引用、移动构造函数和移动赋值运算符;

  • 范围型(遍历型)循环(for(typename an_item_in_list : list_array(traversable)));

  • 大括号与等号初始化器、初始化列表(std::initialize_list 类型,默认初始化为0,阻止窄向转换 (narrowing-conversion),减少和函数调用的结构互相混淆的可能,统一不同类型的初始化方式);

  • 空指针专用字面值 nullptr

  • 使用 64 位的超长整型 long long

  • [C++ Lib] 自动管理堆内存的智能指针;

  • 类型获取运算符 decltype(),初始化对象时自动类型判断 auto 关键字;

  • 成员函数使用的 defaultdelete 注记;

  • 禁用继承(对于类)与禁用重写(对于虚函数)限定符 final

  • 明确重写限定符 override

  • 新增函数声明方式:尾随型返回值(auto function() -> <return_type>);

  • 作用域限定枚举(enum struct|class ENUM_NAME {}),借助类型关键字,将枚举值当作成员常量处理,要将其赋值给整型变量时需要进行显式强制转换;

  • 字面类型。指的是那些在运行前就能确定其信息的对象,这意味着字面类型仅含运行前资源、不含运行时才能得到的资源。 包括基本数据类型、指针、基本数据类型数组、枚举、引用,以及符合以下条件的类:

    • 类中的非静态成员变量都属于上述字面类型;
    • [C++20 前] 至少 1 个带 constexpr 标记的构造函数;
    • 使用隐式或 =default 标记的默认析构函数; [C++20 及之后] 标记 constexpr 的析构函数。
    • 若是继承类,父类也是字面类型。

    [C++20] 允许动态内存分配的存在,但内存释放位置必须明确,不能在 constexpr 函数内返回堆内存。

  • 编译时常量表达式关键字 constexpr,使一些仅由常量、形参组成的表达式或使用这些常量的非虚函数在编译时就计算它们的值。 [C++11] 只允许此函数体有一条 return 语句; [C++11 之后] 允许创建局部变量、设置条件判断和多条 return 语句;

  • 委托构造函数(默认值通过一个构造函数给出,这个构造函数实际上是带参数去调用拥有更多形参的一个重载版本);继承构造函数(它也可以看作是一种委托构造函数);

  • 分别支持 UTF-16 和 UTF-32 编码字符存储的 char16_tchar32_t 类型;

  • 使用 using 创建类型别名(过去使用 typedef <original> <alias>;,现在可用 using <alias> = <original>;,对于函数指针,可用 using <alias> = <return> (*)(<argument_list>));

  • 类型别名模板,使得可以为模板(泛型)类型创建别名,而不影响模板功能;以下是创建一个图的别名例子:

    template<typename key_type, typename value_type>
    using CustomMap = std::map<key_type, value_type, hash_map<key_type>>;
     
    // Usage example
    CustomMap<string, int> sheet = {};
  • 可变参数模板(模板可用 ... 标识传入实参数量可变,例如 template <typename Wildcard, typename ... [name]>);以下示例中,函数将所有传入参数尝试连接到一个字符串上。

    template<typename Wildcard, typename ...Wildcard_rest>
    std::string connector(Wildcard first_arg, Wildcard_rest ...rest_args) {
        if (sizeof...(rest_args) != 0) {
            return std::string(first_arg) + connector(rest_args...);
        }
        else {
            return std::string(first_arg);
        }
    }
  • 多种字符串字面量类型

    • 普通型 "This is a string",受文件编码和不同平台编译器的影响;
    • 纯字符串 R"(This is a raw string)",不处理转义字符;
    • 宽字符串 L"This is a wide string",编码为 wchar_t,Windows / Linux 占 2 (UTF-16) / 4 (UTF-32) 字节每字符;
    • UTF-8 字符串 u8"This is an utf-8 string",编码为 char (C++11 ~ C++17) 或 char8_t (C++20);
    • UTF-16 字符串 u"This is an utf-16 string",编码为 char16_t
    • UTF-32 字符串 U"This is an utf-32 string",编码为 char32_t

    纯字符串可以组合其他编码字符串使用。

    Note

    若纯字符串中含有 )" 时,需要添加额外的字符序列区分字符串边界:

    char* str_1 = R"(An important "(TASK)")";         // ❌
    char* str_2 = R"123(An important "(TASK)")123";   // ✅

    这些额外的字符序列分别置于两侧圆括号外;字符序列不能使用非 ASCII 字符,和 ASCII 字符中的空白字符、重音符(反引号)、控制字符、反斜线 \、除尖括号以外的其他括号。

  • 自定义字面常值(aka. 硬编码值)后缀,通过预先定义后缀使得字面常值的类型确定且易读;后缀只能用于字面常值,不能用于变量;

    Note

    定义后缀需要通过实现操作符函数完成:

    <return_type> operator"" <suffix_name>(argument);
    

    可接受的参数:

    • 整型:unsigned long long | unsigned long long int
    • 浮点型:long double
    • 字符型:char | wchar_t | char16_t | char32_tchar8_t(自 C++20 起)
    • 字符串型:const char*, std::size_t (需要 2 个参数)

    后缀名应该在开头添加下划线 _,以便和标准库定义的后缀进行区分。

    基本用法示例

    unsigned int operator"" _u32(unsigned long long int value) {
        return value;
    }
     
    std::string operator"" _kmph(const char* str, size_t length) {
        return std::string(str) + "km/h";
    }
     
    // Usage
    unsigned int value = 233_u32;
    auto speed = "233"_kmph;
  • 使用两层中括号以设置属性 [[Attribute]]

  • noexcept 限定符和 noexcept() 运算符;noexcept() 接受一个表达式,根据表达式类型检查所有涉及的函数调用是否设置 noexcept;如果全部声明,返回 true

  • 多线程内存模型;

  • 垃圾回收接口;(没有编译器实装,C++23 移除)

  • 类型字节对齐检查运算符 alignof(typename/object),类型声明时字节对齐设置运算符 alignas(int)

  • 静态断言 static_assert(),在 C++17 前的函数结构为:static_assert(bool_expression, err_msg),自从 C++17 新增了一个重载版本:static_assert(bool_expression)

  • [C++ Lib] 新增许多算法,例如 std::shuffle()std::is_sorted()std::move()std::move_backward()

  • [C++ Lib] 新增库:分数型表示 ratio、高精度时间 chrono

  • [C++ Lib] 返回迭代器的函数:std::begin()std::end()std::next()std::prev()

  • [C++ Lib] 在线程间传递异常:std::exception_ptrstd::error_codestd::error_condition

C++ 14

在 2014 年发布的 C++ 标准。

  • 变量模板,用于定义常量并按需要设置为不同类型;

    template<typename Wildcard>
    Wildcard ConstantVal = 3.5;
     
    // Usage
    ConstantVal<int>;
  • 模板(泛型) lambda,使用 auto 作为参数类型,从而使匿名函数能接受任意类型;

  • 初始化 lambda 捕获,允许在 [] 中初始化变量;最重要的用处是移动捕获;

    auto ptr = std::make_unique<int>();
    auto lambda_function = [inner_ptr = std::move(ptr)] {};
  • 减少 constexpr 标注的函数限制,现在允许在 constexpr 函数内部使用条件判断语句和多条返回语句;

  • 自动推断返回类型,允许在函数返回值声明处使用 auto;内部任何返回语句必须能够被统一为相同类型;

  • 二进制字面值,允许用 0b 开头表示一个二进制整型;

  • 整型字面值分隔符,允许用 ' 单引号分隔以提高易读性;

  • 允许初始化时混用聚合体和默认成员初始化器;

  • [C++ Lib] 创建独占型智能指针函数 std::make_unique()

  • [C++ Lib] 互斥锁 std::shared_timed_mutex 和读写锁 std::shared_lock

  • [C++ Lib] 静态编译工具 std::integer_sequence

  • [C++ Lib] std::exchange(new, original)new 赋值至 original,然后返回 original;适用于移动对象;

  • [C++ Lib] std::quoted() 用于输出时添加引号,用于输入时去除最外层一对引号。

C++17

在 2017 年发布的 C++ 标准,是一个主要版本。

(部分内容已删去)

  • 像 Python 那样允许多变量接受返回值,返回值序列以列表形式提供,这个过程被称为解包 (decompose);以下是标准库函数 ret() 的定义及简单应用实例:

    // Definition of function
    std::pair<std::string, int> ret(const std::map<std::string, int>& inner_map, int position) {
        return {inner_map.begin()->first, inner_map.begin()->second};
    }
    // Usage
    auto [key, value] = ret(json_simulator, 0);
  • 允许在条件判断中初始化一个变量,然后进行条件判断;这使得函数返回值得以重复使用的前提下,又确保其作用域仅限于 if 语句内:

    if (int status = tcp_connect("localhost", 12333); status != -1)
  • 现在允许对类内静态成员变量标注内联 inline,从而允许直接对它们初始化而无需另写定义语句;

    // Before C++17
    // ERROR
    class Test {
        static int value = 5;	// ❌ Always get error
    };
    // CORRECT
    class Test {
        static int value;
    };
    int Test::value = 5;        // 🟡 Static member variable should
                                //    be defined and be assigned separately!
     
    // C++17 and after
    class Test {
        static inline int value = 5;  // ✅
    };
  • 在可变模板函数的基础上,进一步支持对二元运算的简化表达。前例请见 C++11 中的可变模板;

    template<typename ...Wildcard>
    std::string str_connector(Wildcard ...parameters) {
        return (std::string(parameters) + ...);
    }
     
    // Usage
    std::string after_connection = str_connector("Forza", "Horizon", " ", "5");
    std::cout << after_connection << std::endl;

    Warning

    此方法应该尽可能只用在简单对象间的操作与运算。

  • 现在能为 if 设置编译时检查常量表达式 constexpr,使其像 #ifdef#ifndef 那样由编译器根据编译代码内容决定应该生成哪个(哪些)分支的 if 语句;如果多个调用者分别满足不同条件,那么分别编译;

  • auto 现在能作为非类型的模板参数;

    template<auto value>
    constexpr auto ConstantValue = value;
     
    constexpr auto var = ConstantValue<16>;
  • [[nodiscard]] 属性:若调用处未接受返回值,发出编译警告;

  • [[maybe_unused]] 属性:停止对一个未使用对象的警告;

  • [C++ Lib] std::optional 返回一个“可能存在”值;使用此类型可以无需建立特殊的错误值;

  • [C++ Lib] std::variant C++ 特别改造的联合体类型;

  • [C++ Lib] std::any C++ 特别改造的无类型指针 void*;要转换为某个类型,使用 std::any_cast<>()

  • [C++ Lib] std::filesystem C++ 独立于任何平台的文件操作方法;

  • [C++ Lib] 许多标准库算法可用 std::execution::par 以启用多核工作流处理;

  • [C++ Lib] 字节数据类型:std::byte