C/C++标准新特性简介

参考文档

C语言的起源发展

C语言诞生于1972年,美国贝尔实验室。作者为:Dennis MacAlistair Ritchie(丹尼斯·里奇) & Kenneth Lane Thompson(肯·汤普森)。
之所以叫C语言,是从B语言(肯·汤普森创建)基础上发展而来。 1978年,他们编写了《The C Programming Language》一书,成为C程序员的”圣经”,编程爱好者称为 “K&R”.

随着C语言迅速普及, 1983年,美国国家标准协会(ANSI)开始推动制订C语言标准。

1989年,ANSI发布了第一个完整的C语言标准——ANSI X3.159—1989,简称”C89″,也被称其为”ANSI C”。C89在1990年被国际标准组织ISO(International Organization for Standardization)一字不改地采纳,ISO官方给予的名称为:ISO/IEC 9899。

扩展很少, 主要是技术勘误,bugfix. 主要的扩展为: 扩充了宽和多字节字符支持, 包含 wctype.h'、'wchar.h

引入的新特性,常见的包含:

  • _Bool类型 C语言中的布尔类型, C语言中中的bool 类型为 _Bool类型的typedef.

  • long long 类型: 至少为64位。

  • restrict 关键字 该关键字仅有指向对象类型的指针,它告诉编译器,所有修改所指向内存的操作都必须通过该指针来修改,而不会通过其它途径。 该关键字的目的是为了促进编译器的优化,生成更有效率的汇编代码。
  • // 注释
  • stdint.h头文件 类型: int8_t, int16_t, int32_t, uint8_t, uint16_t, uint32_t, intptr_t(可以保存void*类型指针), uintptr_t 等。 宏定义:INT8_MIN, INT16_MIN, INT32_MIN, INT64_MIN, ​ INT8_MAX, INT16_MAX, INT32_MAX, INT64_MAX, ​ UINT8_MAX, UINT16_MAX, UINT32_MAX, UINT64_MAX等。
  • inttypes.h 头文件
  • 变长数组VLA 变量长度的数组,可以在栈上使用变量定义数组长度。 如下所示:

// C99引入的VLA
int main() {
    int a = 10;
    int array[a];  // 数组的大小为变量
    for (int i = 0; i < a; ++i) {
        array[i] = i;
    }
    return 0;
}

该特性在C11标准中退化为编译器的可选功能, 微软的MSVC一直不支持该特性,原因见:https://devblogs.microsoft.com/cppblog/c11-and-c17-standard-support-arriving-in-msvc/。 C++标准中也不支持VLA特性(很困惑的是:我使用g++的c++98标准编译类似的代码可以编译通过,不清楚为什么!)
* 声明与代码混合 大家有时候看老代码,它们的写法就是上来就声明一堆变量,随后才业务逻辑代码, 就是因为 C89&#x6807;&#x51C6;规定变量必须在函数一开始进行初始化,到了 C99&#x6807;&#x51C6;中约束就放开了,代码可读性就好多了。
* for 循环的 init 子句中的声明 下面这种写法,在 C99&#x6807;&#x51C6;之前是非法的,而 C99&#x6807;&#x51C6;允许这种方便的写法。

int sum = 0;
for (int i = 0; i < 100; ++i) {
    sum += i;
}
  • 复合字面量 就地构造一个指定类型的无名对象,在只需要一次数组、结构体或联合体变量时使用。 复合字面量的值类别是左值(能取其地址)。 若复合字面量出现于文件作用域,则其求值所得的无名对象拥有静态存储期,若复合字面量出现于块作用域,则该对象拥有自动存储期(此情况下对象的生存期结束于外围块的结尾)。
struct Info {
    int age;
    char* name;
};

int main() {
    // 数组类型
    int* array = NULL;
    array = (int[]){1, 4, 6, 7, 9};     // 复合字面量
    array[1] = 10;      // 该语句体现了左值特性,在C++中是不允许的, C++中不允许对临时变量取地址。

    // 结构体
    struct Info a = {10, "xiaohong"};
    a = (struct Info) {15, "xiaoming"};  // 复合字面量
}
// 业务代码中常见的所谓变长数组
struct CellInfo {
    uint8_t cellNum;
    int info[];    // 定义空的数组长度
};
int cellNum = 100;
cellInfo* pCellInfo = (cellInfo*)malloc(sizeof(CellInfo) + cellNum * sizeof(int));

int array[5] = {[3] = 100, [1] = 200};  // [0, 200, 0, 100, 0]

struct Info {
    int a;
    int b;
    int c;
}
struct Info value = {.b = 100, .a = 200, .c = 2899};
  • 变参数宏
#define 标识符( 形参, ... ) 替换列表
#define 标识符( ... ) 替换列表

能用 __VA_ARGS__ 标识符访问额外实参,然后该标识符被实参替换。
* _ _func__ 在每一个函数体内,都预定义了一个变量 __func__, 表示该函数的名字,该变量具有块作用域涉及静态存储期。等价于:

static const char __func__[] = "function name";
  • 枚举的尾逗号 下面枚举值的定义,在c89标准中是错误的,c99标准允许这么定义:
enum Color {
    RED,
    BLUE,
    BLACK,    // 此处有一个逗号
};
  • 函数宏的空参数
  • inline声明符

引入的新特性,常用的有:

  • _Alignas 和 _Alignof 的字节对齐 _Alignas用于作为修改声明对象对齐要求的说明符, _Alignof 用于查询其运算数类型的对齐要求。
int main() {
    _Alignas(8) int a;
    _Alignas(int) char b;
    size_t offset = _Alignof(double);   // 获取double类型的对齐的字节大小。
    return 0;
}
  • _Noreturn 声明符 _Noreturn函数说明符用于告知编译器该函数将不返回任何内容。关于该关键字有什么作用的问题: 可能对阅读代码的人涉及编译器有些帮助吧。
  • _Static_assert编译期静态检查 这个很有用,可以把一下安全检查放到编译期进行!例如: _Static_assert(sizeof(int) == 4, "error: type INT is not 4 bytes.")
  • 匿名结构体和联合体 在 C 语言中,可以在结构体中声明某个联合体(或结构体)而不用指出它的名字,如此之后就可以像使用结构体成员一样直接使用其中联合体(或结构体)的成员。 GCC对该特性的描述: https://gcc.gnu.org/onlinedocs/gcc/Unnamed-Fields.html#Unnamed-Fields
struct Info {
    int a;
    char b;
    struct {
        double x;
        double y;
    };
} Object;
Object.x = 100.2;   // 直接使用
  • 泛型函数(_Generic关键字) _Generic关键字提供了一种在 编译时 根据 赋值表达式 的类型在 泛型关联表 中选择一个表达式的方法,因此可以将一组不同类型却功能相同的函数抽象为一个统一的接口,以此来实现泛型。 说明1: 关联列表中的表达式仅仅是普通的宏替换。 说明2: default列表不是必须的。
void intFunc(int x) {
    printf("this is int func.\n");
}
void doubleFunc(double x) {
    printf("this is double func.\n");
}
void otherFunc(void x) {
    printf("this is other func.\n");
}

#define MYFUNC(x) _Generic((x), int: intFunc(x), double: doubleFunc(), default: otherFunc(x))

int main() {
    int a = 10;
    double b = 20;
    char c = 'h';
    MYFUNC(a);
    MYFUNC(b);
    MYFUNC(c);
}

输出为:

yin@yin-VirtualBox:~$ ./a.out
this is int func.

this is double func.

this is other func.

  • 线程的内存模型、涉及 stdatomic.hthreads.h头文件 引入了 _Thread_local关键字, 表示线程存储类限定符。存储期是创建对象的线程的整个执行过程,在启动线程时初始化存储于对象的值。每个线程拥有其自身的相异对象。

bugFix, 无新特性。

下个主要 C 语言标准版本…

auto、break、case、char、const、continue、default、do、double、else、enum、extern、float、for、goto、if、int、long、register、return、short、signed、sizeof、static、struct、switch、typedef、union、unsigned、void、volatile、while, 共计32个关键字。

补充说明:

  • auto关键字,是存储类说明符,表示自动存储, 与C++标准引入的 auto的含义不同。
  • 掌握extern、static和votatile关键字的各种作用。

restrict、inline 、_Bool、_Complex、_Imaginary

_Alignas、_Alignof、_Atomic、_Generic、_Noreturn、_Static_assert、_Thread_local

  • char 类型 表示字符类型, 大多数编译器认为是有符号的, 自己可以验证一下:把0xFF的char类型赋值给int类型, 看看结果为255不是-1。gcc的结果为-1。
  • 布尔类型 _Bool, C99标准引入的。 对应的取值:true与false为宏定义。
  • 有符号类型 signed char, short, int, long, long long(C99标准引入的)
  • 无符号类型 unsigned char, unsigned short, unsigned int , unsigned long , unsigned long long(C99标准引入)
  • 浮点类型 float 、 double 、 long double, 复数、虚数

  • enum

  • 数组类型

  • 结构体类型
  • 联合体类型
  • 函数类型
  • 指针类型
  • 原子类型 _Atomic 关键字, C11标准引入的。

申请内存的四大金刚函数:

  • malloc函数 最常用的内存分配函数,线程完全的。意味着会加锁,多线程有效率问题。另外,它是不可重入的,原因是:malloc通常为它所分配的存储区维护一个链接表和一些锁,在malloc执行过程中,如果遇到信号中断处理函数时, 并再一次调用malloc时,会破坏它维护的全局变量的信息。这也侧面说明了信号处理函数必须是可重入的。
  • calloc函数 与malloc的区别是:它分配内存之后, 会执行清零操作。 线程安全
  • realloc函数 该函数实现的功能不单一,不建议使用它,不介绍。
  • aligned_alloc函数(C11标准引入) 它可以分配对齐的内存空间, 线程安全。
  • free函数

内联汇编(常由 __asm__关键词引入)给予在 C 程序中嵌入汇编语言源代码的能力。

不同于 C++ 中,内联汇编在 C 中被当作扩展。它是条件性支持及由实现定义的,意思是它可以不存在,而且即使实现有所提供,它也并不拥有固定的含义。

因为不是标准,不同的 C 编译器拥有差异巨大的汇编声明规则,和与周围的 C 代码之间交互的不同约定.

C++语言的起源与发展

1979年,Bjame Sgoustrup到了Bell实验室,开始从事将C改良为带类的C(C with classes)的工作。1983年该语言被正式命名为C++。

  • 1979年, 支持的特性,包括:类,成员函数,派生类,公开与私有访问控制,友元,默认实参,内联函数,重载赋值运算符,构造函数,析构函数等。
  • 1985年,Cfront 1.0编译器, 新增加特性:虚函数,函数与运算符的重载,引用, new 和 delete 运算符, const 关键词,作用域解析运算符。
  • 1989年,Cfront 2.0编译器, 新增加特性:多继承,成员指针,受保护访问,类型安全的连接,抽象类,静态和 const 成员函数,类特有的 new 和 delete
  • 1990年,新增加特性:命名空间,异常处理,嵌套类,模板。

  • RIIT(运行时类型识别)

  • dynamic_cast类型转引说明符: 只能用于转换多态的类对象。 当由 基类的指针或引用转换到 子类型的指针或引用时,会进行 运行时检查。 当转换失败时,如果是指针类型,返回空指针,如果是引用类型,抛出异常。
  • typeId运算符, 查询类型的信息, 使用时需要包含 <typeinfo></typeinfo>头文件。当应用于多态类型的表达式时,typeid 表达式的求值可能涉及运行时开销(虚表查找),其他情况下 typeid 表达式都在编译时解决。 typeId运算符返回的类型为 std::type_info类型。
  • 转型运算符 允许从类类型到其他类型的隐式转换或显式转换(C++11引入的explicit关键字,指定必须进行显示转换), 隐式转换函数没有参数或显式返回类型。
class A {
public:
    // 从A类型转换为int类型
    operator int() const {
        return value;
    }
private:
    int value;
}
  • 协变返回类型 虚函数重载时,要求子类与基类函数的返回类型必须相同或者协变。 例如:
class Base {
public:
    virtual Base* Get() {
        return this;
    }
};

// 虚函数的返回值为协变场景。
class Derived : public Base {
public:
    Derived* Get() override {
        return this;
    }
};

具体的协变返回类型的定义如下:
1. 两个类型均为到类的指针或引用(左值或右值)。不允许多级指针或引用。
2. Base::f() 的返回类型中被引用/指向的类,必须是 Derived::f() 的返回类型中被引用/指向的类的无歧义且可访问的直接或间接基类。
3. Derived::f() 的返回类型必须有相对于 Base::f() 的返回类型的相等或较少的 cv 限定。
* mutable关键字 用于修饰类或结构体内的非const非引用的成员变量, 当类或结构体被修饰为const时, 也可以修改这些被mutable修饰的成员变量。 例如:

struct Info {
    int a;
    mutable int b;
};
const Info i;
i.b = 100;
  • 成员模板 模板声明可以定义在任何非局部类内部, 例如:成员函数模块、类内的类模板、using声明等。
class Demo {
public:
    template
    void Get();

    template
    struct A;

    template
    using Func = Set;
};
  • export (C++11之后,不使用了, 在c++20中,引入模块之后又了新的含义)
  • bitset库
  • auto_ptr (应该是在C++17标准中删除掉,太危险!)
  • iostream库
  • complex库

  • 值初始化 使用()或者{}构造一个对象时,执行值初始化。 标准中介绍的比较绕,见(). 自己总结如下:

  • 内置类型,执行零初始化。
  • 类对象, 如果是聚合类型,使用{}时,执行聚合初始化(如果此时类的构造函数声明为删除的(=delete), 类对象也是可以被构造出来)。
  • 类对象,如果 编译器隐式提供 或者 在类内部声明的同时使用=default显示让编译器提供,先对成员变量进行清零,再调用成员变量的构造函数进行初始化。
  • 类对象,有用户自定义的构造函数(在类外部使用=dafault定义的构造函数属于用户自定义),执行该构造函数进行初始化。
  • 数组成员,以每一个变量执行值初始化。

类型的自动推导, auto的自动类型推断发生在编译期,所以使用auto并不会造成程序运行时效率的降低。使用场景举例:

  • 函数尾置返回类型中的auto 占位符(c++14标准允许了使用auto用于推导函数返回类型)
// 例子一:
auto GetFunc() -> int(*)() {  // 这样写,代码更清晰
    return nullptr;
}
// 例子二:函数模板中返回值依赖模板参数
template
auto Accumulate(T x, U y) -> decltype(x * y) {
    return x * y;
}
  • 变量的类型推导, 有以下几点细节要多多注意:
  • 在进行变量类型推导过程中, 会自动去除 引用语义、 顶层const语义、 volatile语义,保留 底层const
  • 在推导过程中,变量会隐式执行数组名到指针类型转换,如果带了引用号, 肯定还会保留数组类型。
  • 类似 auto *p的写法个人感觉没有太大意义,反而让代码阅读者更疑惑, auto本身就会推导出 指针类型
int a = 100;
const int* pA = &a;
const int& refA = a;

auto b1 = a;        // 类型为int
auto b2 = refA;     // 类型为int

const auto b3 = a;  // 类型为const int
auto& b4 = a;       // 类型为int&
auto& b5 = refA;    // 类型为const int&
auto b6 = pA;       // 类型为const int*

int array[4] = {1, 2, 3, 4};
auto c1 = array;    // 类型为int*
auto& c2 = array    // 类型为int(&)[4];

decltype操作符用于查询表达式或者实体变量的数据类型。在难以或不可能以标准写法进行声明的类型时, decltype 很有用,例如 lambda 相关类型或依赖于模板形参的类型。并不会实际计算表达式的值,编译器分析表达式并得到它的类型。 另一个应用就是:使用尾置返回时,推断返回值的类型。

对于表达式,当表达式的值类别为将亡值时, delctype的结果为右值引用T&&;当值类别为左值时,产生左值引用T&;当值为纯右值时,产生类型T。

对于变量,decltype会完整的返回变量的类型,包含顶层的const以及引用(auto做不到这一点)。 如果对象的名字带有括号,则它被当做通常的左值表达式,从而 decltype(x) 和 decltype((x)) 通常是不同的类型。

注意点:decltype对数组进行操作时,它的返回结果仍然为数组类型,而非指针。

int& a = 10;
const int& b = 100;
int c = 1;
int *p = c;
int array[10] = {0};

decltype(a) a1 = a;    // 推导类型为:int&
decltype(b) b1 = b;    // 推导类型为:const int& b
decltype(c) c1 = 10;   // 推导类型为:int
decltype((c)) c2 = c;  // 推导类型为:int&
decltype(*p) p1 = c;   // 推导类型为:int&
decltype(array) A = {0}; // 推导类型为:int[10]

auto f = [] () -> int {return 0;}
decltype(f) ff = f;     // 推导类型为:lambda表达式的函数类型

可以强制编译器自动自成以下特殊成员函数, 即可以在声明的时候使用,也可以在类外部使用,当在类外面使用时,认为是用户自定义的,而非编译器自动生成的。 = default 也只能对下面的特殊成员函数使用。

  • 默认构造函数
  • 复制构造函数
  • 移动构造函数
  • 复制赋值运算符
  • 移动赋值运算符
  • 析构函数

它的作用是把函数被定义为 弃置的(deleted)。任何弃置函数的使用都是非良构的(程序无法编译)。这包含调用,包括显式(以函数调用运算符)及隐式(对弃置的重载运算符、特殊成员函数、分配函数等的调用),构成指向弃置函数的指针或成员指针,甚或是在不求值表达式中使用弃置函数。 = delete只能出现在函数声明时,若函数被重载,则首先进行重载决议,且仅当选择了弃置函数时程序才非良构。

可以用于对除了 析构函数之外的任何的函数进行指定 = delete,不仅仅是类成员函数,还可以是普通函数。 尤其是在引导函数匹配过程中, 删除的函数非常有用。

template
void Get() = delete;

template<>
void Get() {
    cout << "int" << endl;
};
template<>
void Get() {
    cout << "double" << endl;
};

int main() {
    Get();
    Get();
    Get();   // 该行编译报错
    return 0;
}

首先说一点, final不是C++中的关键字, 只 是在成员函数声明或类头部中使用时有特殊含义的标识符。其他语境中它未被保留,而且可用于命名对象或函数。

final用于指定某个 虚函数不能在子类中被覆盖,或者某个类不能被子类继承,。 只能用在类的成员函数声明时使用, 并且只能用于虚函数。

指定一个虚函数覆盖另一个虚函数。 override 是在成员函数声明符之后使用时拥有特殊含义的标识符:其他情况下它不是保留的关键词。

// 例子一
auto func(int i) -> int(*)[10];
// 例子二, lambda表达式
auto func = []() ->int {return 10;}
// 例子三:模板
template
auto func(It beg, It end) -> decltype(*beg) {
    return *beg;
}
Class A {
public:
    A(A&& rhs) {}
    A& operator=(A&& rhs) {}
};

C++11之前,声明枚举类型,其底层类型不固定。底层类型是某个能表示所有枚举项值的整型类型,此类型不大于 int。 C++11及之后,可以指定枚举类型的底层类型。

// C++11 之前
enum Color {
    RED,
    GREEN,
    YELLOW,
};
// C++11 之后
enum Color : uint8_t {
    RED,
    GREEN,
    YELLOW,
};

标准C++中,枚举类型不是类型安全的, 枚举值可以隐式地转换为整数类型。C++11引入枚举类,即有作用域的枚值类型,为类型安全,枚举类型不可以隐式转换为整数类型,否则,编译器会报错。另外,声明有作用域枚举类型时,底层类型默认为 int.

enum class Color {
    RED,
    GREEN,
    YELLOW,
};
// 使用是必须指定作用域
Color value = Color::RED;

用于修饰编译器就可以确定下来的值。让代码开发人员很开心, 让编译器开发人员很恶心!

编译期的静态检查!

从花括号初始化器列表初始化对象。 具体地,根据不同的使用场景,执行不同的初始化效果。

c++新标准规定,函数可以返回花括号包围的值的列表。即使用列表对函数返回的临时变量进行初始化。

它是一个类模板, 定义于头文件

  • 花括号初始化器列表 列表初始化一个对象,其中对应构造函数接受一个 std::initializer_list 参数
  • 以 _花括号初始化器列表_为赋值的右运算数,或函数调用参数,而对应的赋值运算符/函数接受 std::initializer_list 参数
  • 绑定 _花括号初始化器列表_到 auto,包括在范围 for 循环中。

特别注意: 复制一个 std::initializer_list 不会复制其底层对象,即 浅拷贝

源代码实现大致如下:

  /// initializer_list
  template
    class initializer_list
    {
    public:
      typedef _E        value_type;
      typedef const _E&     reference;
      typedef const _E&     const_reference;
      typedef size_t        size_type;
      typedef const _E*     iterator;
      typedef const _E*     const_iterator;

    private:
      iterator          _M_array;
      size_type         _M_len;

      // The compiler can call a private constructor.

      constexpr initializer_list(const_iterator __a, size_type __l)
      : _M_array(__a), _M_len(__l) { }

    public:
      constexpr initializer_list() noexcept
      : _M_array(0), _M_len(0) { }

      // Number of elements.

      constexpr size_type
      size() const noexcept { return _M_len; }

      // First element.

      constexpr const_iterator
      begin() const noexcept { return _M_array; }

      // One past the last element.

      constexpr const_iterator
      end() const noexcept { return begin() + size(); }
    };

在c++11之后,内置类型与类对象都可以使用{}进行初始化,即做到了统一初始化。

一句话总结,当一个类有多个构造函数时,其中一个构造函数的工作可以委托给另一个来做。举例:

class A {
public:
    A(int a, int b, int c) {
        a_ = a;
        b_ = b;
        c_ = c;
    }
    A(int b) : A(10, b, 100) {}   // 它的工作委托给第一个构造函数来做。
private:
    a_ = 0;
    b_ = 0;
    c_ = 0;
};

一句话总结,当子类仅仅是为了透传参数,为了构造基类时,子类可以使用 using &#x57FA;&#x7C7B;&#x540D;&#x5B57;继承基类的构造函数,避免重复写了。

具体来说,using 声明语句只是令某个名字在当作用域内可见,而当作用于构造函数时,using声明语句将令编译器代码。

和普通成员的using 声明不同的是:一个构造函数的using声明不会改变该构造函数的访问级别。

它的作用是替换typedef, 它能做到typedef做不到的事情。例如:

using otherName = int;

template
using Array = vector;    // typedef做不到吧。

关于该特性,最重要的是理解两点: 参数包包展开, 在此基础上,就可以很轻松学会使用递归或继承的手法对具体的参数进行操作了。

参数包,分为模板参数包和函数参数包, 其它模板参数包又可以分为类型模板参数包和非类型模板参数包。举例说明:

template    // Args为类型模板参数包
void Func(Args... t)          // t为函数参数包
{}

template      // indexs为非类型模板参数包
void Func() {
    int array[] = {indexs...};
}

包展开, 格式为: &#x6A21;&#x5F0F; ..., 看着比较抽象,举例说明:

/************   函数实参列表时的包展开    ***************/
template
Func(Args... args)     // 这里其实是对Args形参包的展开
{
    f(args...);  // 展开模式为参数本身T, 展开后为:arg0, arg1, arg2, arg3, ....

    f(&args...);  // 展开模式为&T,  展开后为:&arg0, &arg1, &arg2, &arg3, ...

    f(++args...); // 展开模式为++T, 展开后为: ++arg0, ++arg1, ++arg2, ++arg3, ...

    f(2 + args...); // 情节模式为2+T, 展开后为:2+arg0, 2+arg1, 2+arg2, 2+arg3, ...

    f(sum(5+args)...);  // 展开模式为:sum(5+T), 展开后为:5+arg0, 5+arg1, 5+arg2, 5+arg3, ...

    f(std::forward(100+args)...);   // 类型与参数一起展开,类型展开为Arg0, Arg1, Arg2, ....,
                                          // 参数展开为:10+arg0, 100+arg1, 100+arg2, ...

    f(const_cast(&args)...)  // 展开模式与上面类似。

    // 借用列表初始化,对args中的每一个参数进行打印输出。 展开后的样子为:
    // int dummy[] = {(cout << arg0, 0), {cout << arg1, 0}, ...., };
    int dummy[] = {(cout << args, 0)... };
}

/************   类型模板实参的包展开    ***************/
template
class MyData {
    tuple t1;    // 类型展开为: arg0, arg1, arg2, ...

    tuple t2;  // 类型展开为:int, arg0, arg1, arg2, ..., double
}

/************   更复杂的包展开    ***************/
f(h(args...) + args...); // 展开成: f(h(E1,E2,E3) + E1, h(E1,E2,E3) + E2, h(E1,E2,E3) + E3)

template
void g(Ts (&...arr)[N]) {}  // Ts (&...)[N] 不被允许,因为 C++11 语法要求带括号的省略号形参拥有名字
int n[1];
g("a", n); // Ts (&...arr)[N] 展开成 const char (&)[2], int(&)[1]

// 包展开可以用于指定类声明中的基类列表。典型情况下,这也意味着其构造函数也需要在成员初始化器列表中使用包展开,以调用这些基类的构造函数
template
class X : public Mixins... {
 public:
    X(const Mixins&... mixins) : Mixins(mixins)... { }
};

// 包展开可以出现于 lambda 表达式的捕获子句中
template
void f(Args... args) {
    auto lm = [&, args...] { return g(args...); };
    lm();
}

sizeof… 运算符, 它用于获取参数包的数目,而非 参数包的sizeof的总和哦。使用举例:

template
Func(Args... args)
{
    cout << sizeof...(Args) << endl;
    cout << sizeof... (args) << endl;
}
Func(10, 15.6, 'a', "hello");   // 输出结果都为4.

使用递归或继承对具体的每一个参数进行操作

// 递归式展开
template
void MyPrint(T t) {
    cout << t << endl;
}

template
void MyPrint(Head first, Tail... others) {
    cout << first << endl;
    MyPrint(others...);
}

int main() {
    MyPrint(10, 20.4, 'a', "sting");
    return 0;
}

// 继承式展开
template
class Manager;

template
class Manager {        // 偏特化
public:
    Manager(T t) {
        cout << t << endl;
    }
};

template
class Manager : public Manager {
public:
    Manager(Head first, Tail... others) : Manager(others...) {
        cout << first << endl;
    }
};

int main() {
    // 下面的输出,是反着的, v~^~v
    Manager my(10, 12.4, 'a', "hello");
    return 0;
}

说明以下几点:

格式为: template <形参列表> &#x53D8;&#x91CF;&#x58F0;&#x660E;<!--形参列表-->。使用举例:

template
string Type{"I do not know."};

int main() {
    Type = "int";
    Type = "long";
    Type = "char";
    Type = "double";
    cout << Type << " " << Type << " " << Type << " " << Type << " " << Type << " " << endl;
    return 0;
}

特别注意以下几点:

  • 在c++14标准中,使用 auto可以推导函数的return 语句中推导出它的返回类型。
auto Get() {
    return 10.5;
}
  • decltype(auto):实际推导出来的类型就是 decltype(expr)的类型, 推导结果比auto 更准确! 举例说明:
int a = 10;
int& refA = a;
auto b = refA;    // b的类型为int
decltype(auto) c = refA;    // c的类型为int&

当lambda表达式中的参数类型为auto时, 它就是一个泛型状态,对外表现类型于函数模板。例如:

int main() {
    auto sumFunc = [](auto a, auto b){ return a + b;};    // 泛型lambda表达式
    double c1 = sumFunc(1.5, 2.3);
    int c2 = sumFunc(1, 2);
    return 0;
}

// 上面的泛型lambda表达式与下面的函数模板是等价的
template
auto sumFunc(T a, Y b) {
    return a + b;
}

在c++11的版本,lambda的捕获列表只能捕获lambda表达式所在作用域的局部变量或全局变量,无法初始化自己的局部变量。c++14允许在捕获列表中初始化自定义的局部变量,与函数的参数初始化参数类似。举例:

int x = 10;
auto Func = [a = 100, b = x](){ return a + b;};

unique_ptr myPtr = make_unique(100);
auto func = [ptr = std::move(myPtr)](){cout << *ptr << endl;};

具体放松到什么程度,记不住,反正比c++11的约束条件少了一些。 constexpr函数平时几乎不使用,不多研究。

对于该特性,我一直不太明白, c++14之前,也支持这样的写法啊 int a = 0b1010101,怎么就变成c++14的新特性了呢。

这个特性,有时候使用起来还不错,比如: long long num = 1000'000'000'000'000.

指示一个实体已经被弃用,虽然还可以使用但是不鼓励。 它可以修饰类、变量、函数、命名空间枚举、模板特化等。具体的使用原则为:

使用举例:

[[deprecated("本函数已经弃用")]]
int myFunc() {
    return 10;
}

[[deperacated]]
int global = 100;

int main() {
    myFunc();
    global = 10;
    return 0;
}

// 编译时的输出为:
D:\programs\msys2\mingw64\bin\g++.exe -g D:\myCode\main.cpp -o D:\myCode\main.exe
D:\myCode\main.cpp:16:5: warning: 'deperacated' attribute directive ignored [-Wattributes]
   16 | int global = 100;
      |     ^~~~~~
D:\myCode\main.cpp: In function 'int main()':
D:\myCode\main.cpp:19:12: warning: 'int myFunc()' is deprecated: 本函数已经弃用 [-Wdeprecated-declarations]
   19 |     myFunc();
      |            ^
D:\myCode\main.cpp:11:5: note: declared here
   11 | int myFunc() {
      |     ^~~~~~

生成已完成,但收到警告。
  • std::make_unique
  • std::integer_sequence 见源码:
// integer_sequence
template
struct integer_sequence
{
    typedef _Tp value_type;
    static constexpr size_t size() noexcept { return sizeof...(_Idx); }
};

// make_integer_sequence
using make_integer_sequence = integer_sequence;

// index_sequence
template
using index_sequence = integer_sequence;

// make_index_sequence
template
using make_index_sequence = make_integer_sequence;
  • std::exchange
  • std::quoted
  • std::shared_timed_mutex 与 std::shared_lock

  • auto_ptr

  • register关键字
  • 等等。 剩余的过时的那些特性,也没有使用过,不列出。

目的就是使对参数包的操作更加容易。使用递归和继承的方法展开参数包,确实有一些麻烦!折叠表达式可以分为四种形式:

怎么理解呢? 把 ... 理解成 括号括起来的一系列的表达式, ... 在形参包的右边就是右折叠,在左边就是左折叠。

支持的操作符包括: + - * / % ^ & | = < > << >> += -= *= /= %= ^= &= |= <<=>>= == != <=>= && || , .* ->*<!--=--><!--=-->。在二元折叠中,两个 op 必须相同。

注意事项:

c++17, 扩展了一些场景下,类模板的类型参数可以通过构造函数的参数进行推导出来。 我感觉,实在价值确实不太大吧,只是写代码时方便了一些,我不深入了解。

**if constexpr** 开始的语句被称为 constexpr if 语句。 特点是:编译期执行。

在c++17中,由于关键词 inline 对于函数的含义已经变为”容许多次定义”而不是”优先内联”,因此这个含义也扩展到了变量。inline 保证了 在多个翻译单元中定义,但最终只保留一个, 保证所有.cpp文件中的定义都是相同的

这个就厉害了, 可以定义一个变量放到头文件中,方便多了。

// 1.h
inline a = 10;

// a.cpp
void f1() {
    a = a - 100;
}

// b.cpp
void f2() {
    a = a + 100;
}

它的作用是使写代码更加方便。 可以绑定数组、绑定std:pair, std::tuple类型,还可以绑定public的类对象。使用方法:

&#x3010;c-v&#x9650;&#x5B9A;&#x7B26;&#x3011; auto &#x3010;&&#x3011; [&#x53C2;&#x6570;&#x540D;1&#xFF0C; &#x53C2;&#x6570;&#x540D;2] = &#x8868;&#x8FBE;&#x5F0F;.

里面可能会有很多小的语言成面的注意事项,通常写代码中,这些细节我们都不需要关心的,理解常用的方法就够了!

该新特性, 确实可以大大方便写代码,不还是非常有价值的。 举例说明:

// &#x4E3E;&#x4F8B;1: &#x7ED1;&#x5B9A;&#x6570;&#x7EC4;
int a[2] = {1, 2};
auto [x, y] = a;    // &#x4FEE;&#x6539;x,y &#x4E0D;&#x4F1A;&#x5F71;&#x54CD;&#x5230;&#x6570;&#x7EC4;a
auto &[x, y] = a;   // &#x4FEE;&#x6539;x,y &#x4F1A;&#x5F71;&#x54CD;&#x5230;&#x6570;&#x7EC4;a.

// &#x4E3E;&#x4F8B;2&#xFF1A; &#x7ED1;&#x5B9A;struct
struct Point {
    int x;
    int y;
};
Point p{100, 200};
auto[x0, y0] = p;
auto& [x0, y0] = p;

struct Point2 {
    const int x = 100;
    const int y = 200;
};
Point2 p2;
auto [x1, y1] = p2;
x1 = 10000;    // &#x7F16;&#x8BD1;&#x62A5;&#x9519;&#xFF0C;&#x53EA;&#x8BFB;&#x7C7B;&#x578B;&#xFF0C;&#x4E0D;&#x53EF;&#x4EE5;&#x4FEE;&#x6539;&#x3002;

// &#x4E3E;&#x4F8B;3&#xFF1A; &#x7ED1;&#x5B9A;std::pair
std::set<string> mySet;
.....

auto [iter, success] = m.insert("heloo");

std::map<string, int> myMap;
...

for (auto [name, age] : myMap) {
    cout << name << age << endl;
}
</string,></string>

在if语句 和switch 语句中,可以引入一个局部变量并进行初始化, 使用 分号进行隔离。 举例:

if (int i = 200; i > 20) {
    cout << "OK" << endl;
}

switch(int a = GetValue; a + 10) {
    case 1: ....

    case 2: ...

    break;
}

c++17,增加了lamba表达式的捕获列表中,可以显示捕获 *this, 表现为:当前对象的简单以复制捕获。

class Demo {
public:
  auto GetFunc() {
      auto f = [*this]() {      // c++17之前,这样写是错误的!1
          cout << a << endl;
      }
  }
private:
    int a = 100;
};

用于switch语句中,表示第上一个case语句,直落到下一个case语句,是有意而为之,编译器不需要给出警告。

一句话:用于修饰函数、类、或枚举值,调用声明为 nodiscard 的函数,或调用 按值返回声明为 nodiscard 的枚举或类的函数,并且忽略了这些函数返回值时(即充值表达式),让励编译器发布警告。

想要避免给出警告,把函数返回值使用 void转型一下就可以了, 即: (void)Func(10);

.cpp文件中,如果定义了未使用的变量或函数时, 编译器会告警。怎么办?那就使用该声明告诉编译器别发警告。

预处理器常量表达式,若 找到文件名则求值为 1,而若找不到则求值为 0。 通常用于检查一下要包含的头文件是否存在,如果存在,就include 它。

#if __has_include()
 include
#endif

又称为万能容器,可以存放任意类型的单一对象。

贴一些any类具体实现的一些源码, 加深理解:

c++
void (*_M_manager)(_Op, const any*, _Arg*);
_Storage _M_storage;
</code></pre>
<p>

const: 在未声明为 extern 的非局部非 volatile 非模板 (C++14 起)非 inline (C++17 起)变量声明上使用 const 限定符,会给予该变量内部连接。这有别于 C,其中 const 文件作用域对象拥有外部连接。

区别与联系

Original: https://www.cnblogs.com/yinheyi/p/15866859.html
Author: 殷大侠
Title: C/C++标准新特性简介

原创文章受到原创版权保护。转载请注明出处:https://www.johngo689.com/535236/

转载文章受原作者版权保护。转载请注明原作者出处!

(0)

大家都在看

亲爱的 Coder【最近整理,可免费获取】👉 最新必读书单  | 👏 面试题下载  | 🌎 免费的AI知识星球