Modern C++ 语言可用性的强化


1. 常量

  (1) nullptr 空指针

    nullptr 出现的目的是为了替代NULL,专门用来区分空指针、0

  (2) constexpr 常量表达式

  在编译时就把这些表达式直接优化并植入到程序运行,增加程序的性能。

char arr_1[10]; // 合法
char arr_2[LEN]; // 合法
int len = 10;
// char arr_3[len]; // 非法
const int len_2 = len + 1;
constexpr int len_2_constexpr = 1 + 2 + 3;
// char arr_4[len_2]; // 非法
char arr_4[len_2_constexpr]; // 合法, len_2_constexp是常量表达式
// char arr_5[len_foo()+5]; // 非法
char arr_6[len_foo_constexpr() + 1]; // 合法

上面的例子中,char arr_4[len_2] 可能比较令人困惑,因为len_2 已经被定义为了常量。为什么char arr_4[len_2] 仍然是非法的呢?这是因为C++ 标准中数组的长度必须是一个常量表达式,而对于len_2 而言,这是一个const 常数,而不是一个常量表达式,因此(即便这种行为在大部分编译器中都支持,但是)它是一个非法的行为。

constexpr 让用户显式的声明函数或对象构造函数在编译期会成为常量表达式

2.变量及其初始化

   (1) 支持在if 和switch 语句中声明一个临时的变量。

    在传统C++ 中,可以在for 语句内能够声明一个临时变量int, 如 

    for(int i = 0; i < 100; i++)

    {

        // do something

    }

       但if(或switch)里不可以这样。

  C++17使得我们可以在if(或switch)中也可以完成这一操作:

// 将临时变量放到if 语句内
if (const std::vector::iterator itr = std::find(vec.begin(), vec.end(), 3);itr != vec.end())

{
    // do something
}

 

(2) 初始化列表

C++11 把初始化列表的概念绑定到了类型上,并将其称之为std::initializer_list, 它允许构造函数或其他函数像参数一样使用初始化列表,这就为类对象的初始化与普通数组和POD(Plain Old Data,即没有构造、析构和虚函数的类或结构体)

的初始化方法提供了统一的桥梁。

class MagicFoo {
public:
  std::vector vec;
  MagicFoo(std::initializer_list list) {  //MagicFoo构造函数带初始化列表
    for (std::initializer_list::iterator it = list.begin();it != list.end(); ++it)
      vec.push_back(*it);
  }

 

void foo(std::initializer_list list) {  //函数参数为初始化列表
  for (std::initializer_list::iterator it = list.begin(); it != list.end(); ++it)

    vec.push_back(*it);
}

};

  MagicFoo magicFoo = {1, 2, 3, 4, 5}; //像初始化数组一样初始化MagicFoo

  magicFoo.foo({6,7,8,9}); //初始化列表除了用在对象构造上,还能将其作为普通函数的形参

3. 类型推导

// 从C++11 起, 使用auto 关键字进行类型推导
std::vector vec;
MagicFoo(std::initializer_list list)
for (auto it = list.begin(); it != list.end(); ++it) {
  vec.push_back(*it);
}

注意:auto 不能用于函数传参,因此下面的做法是无法通过编译的(考虑重载的问题,我们应该使用模板):
int add(auto x, auto y);// 错误,无法通过编译的

此外,auto 还不能用于推导数组类型
auto auto_arr2[10] = {arr}; // 错误, 无法推导数组元素类型

 4. 控制流

 (1) if constexpr

  C++17 将constexpr 这个关键字引入到if 语句中,允许在代码中声明常量表达式的判断条件

template
auto print_type_info(const T& t) {
  if constexpr (std::is_integral::value) {
    return t + 1;
  } else {
    return t + 0.001;
  }
}

int main() {
  std::cout << print_type_info(5) << std::endl;
  std::cout << print_type_info(3.14) << std::endl;
}

在编译时,实际代码就会表现为如下:
int print_type_info(const int& t) {
  return t + 1;
}


double print_type_info(const double& t) {
  return t + 0.001;
}

 (2)  区间for 迭代

  std::vector vec = {1, 2, 3, 4};
  for (auto element : vec)
    std::cout << element << std::endl; // read only
  for (auto &element : vec
    element += 1; // writeable

 4.面向对象

(1)委托构造

   构造函数可以在同一个类中一个构造函数调用另一个构造函数,从而达到简化代码的目的。

class Base {
public:
  int value1;
  int value2;
  Base() {
    value1 = 1;
  }
  Base(int value) : Base() { // 委托Base() 构造函数
    value2 = value;
  }
};


int main() {
Base b(2);  //在调用Base(int value) 的同时,也调用了Base(),所以value = 1。
std::cout << b.value1 << std::endl;
std::cout << b.value2 << std::endl;
}

(2)继承构造

   在传统C++ 中,构造函数如果需要继承是需要将参数一一传递的,这将导致效率低下。C++11 利用关键字using 引入了继承构造函数的概念:

class Base {
public:
  int value1;
  int value2;
  Base() {
    value1 = 1;
  }


  Base(int value) : Base() { // 委托Base() 构造函数
  value2 = value;
  }
};


class Subclass : public Base {
public:
  using Base::Base; // 继承构造: Subclass 将自动继承Base的两个构造函数:Base()和Base(int value), 不需手动声明。
};


int main() {
  Subclass s(3);  
  std::cout << s.value1 << std::endl;
  std::cout << s.value2 << std::endl;
}

(3)显示虚函数重载

   override 当重载虚函数时,引入override 关键字将显式的告知编译器进行重载,编译器将检查基函数是否存在这样的虚函数,否则将无法通过编译:

   struct Base {
    virtual void foo(int);
  };
  
  struct SubClass: Base {
    virtual void foo(int) override; // 合法
    virtual void foo(float) override; // 非法, 父类没有此虚函数
  };

  final final 则是为了防止类被继续继承以及终止虚函数继续重载引入的。

struct Base {
  virtual void foo() final;
};


struct SubClass1 final: Base { }; // 合法

struct SubClass2 : SubClass1 { }; // 非法, SubClass1 已final
struct SubClass3: Base { 

  void foo(); // 非法, foo 已final
};

 

(4)显示禁用默认函数

default

C++规定一旦客户程序代码中实现了这些函数的自定义版本,则编译器不再自动产生默认版本。不自动产生,但是客户程序中可以手动指定让编译器生成默认版本。例如:客户程序中定义了带参的构造函数后,可通过如下方式让编译器产生不带参的构造函数:

class MyClass
{
public:
    MyClass()=default;  //此语句显式让编译器产生不带参的构造函数,客户程序中不再定义该函数的函数体
    MyClass(int i):data(i){}
private:
    int data;
};

delete

定义删除函数,当某个原型的函数客户程序中明确禁止使用,或者明确希望编译器不要生成特定默认函数。编译器也禁止定义该函数。典型的是禁止使用拷贝构造函数,以往的做法是将拷贝构造函数访问权限设置为private并不提供实现,这样当拷贝对象时编译不能通过。现在使用delete关键词显示指示编译器不生成默认版本。可将拷贝构造函数在类体重如下声明:

class MyClass
{
public:
    MyClass()=default;
    MyClass(int i):data(i){}
 
    MyClass(const MyClass&)=delete;
 
private:
    int data;
}

//!!!禁止定义拷贝构造函数,这里会报错
MyClass::MyClass(const MyClass &myclass)
{
this->m_data = myclass.m_data;
}


对于非类内部的函数也可以声明为删除函数,如:
 void TestFunc(int i)
{
}
 
void TestFunc(char c)=delete;
int main()
{
    TestFun(1);
    TestFun('a');//报错:attempting to reference a deleted function。如果没有“=delete”再定义函数体就不会报错,或者没有“void TestFunc(char c)=delete;”这行删除函数的声明也不会报错,'a'会隐式转换成int类型后调用参数是int的TestFun函数
}

 (5)强类型枚举

在传统C++ 中,枚举类型并非类型安全,枚举类型会被视作整数,则会让两种完全不同的枚举类型可以进行直接的比较(虽然编译器给出了检查,但并非所有),甚至同一个命名空间中的不同枚举类型的枚举值名字不能相同,这通常不是我们希望看到的结果。
C++11 引入了枚举类(enumeration class),并使用enum class 的语法进行声明:
enum class new_enum : unsigned int {
  value1,
  value2,
  value3 = 100,
  value4 = 100
};
这样定义的枚举实现了类型安全,首先他不能够被隐式的转换为整数,同时也不能够将其与整数数字进行比较,更不可能对不同的枚举类型的枚举值进行比较。但相同枚举值之间如果指定的值相同,那么可以进行比较:
if (new_enum::value3 == new_enum::value4) {
  // 会输出
  std::cout << "new_enum::value3 == new_enum::value4" << std::endl;
}
在这个语法中,枚举类型后面使用了冒号及类型关键字来指定枚举中枚举值的类型,这使得我们能够为枚举赋值(未指定时将默认使用int)。

    •