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
{
// do something
}
(2) 初始化列表
C++11 把初始化列表的概念绑定到了类型上,并将其称之为std::initializer_list, 它允许构造函数或其他函数像参数一样使用初始化列表,这就为类对象的初始化与普通数组和POD(Plain Old Data,即没有构造、析构和虚函数的类或结构体)
的初始化方法提供了统一的桥梁。
class MagicFoo {
public:
std::vector
MagicFoo(std::initializer_list
for (std::initializer_list
vec.push_back(*it);
}
void foo(std::initializer_list
for (std::initializer_list
vec.push_back(*it);
}
};
MagicFoo magicFoo = {1, 2, 3, 4, 5}; //像初始化数组一样初始化MagicFoo
magicFoo.foo({6,7,8,9}); //初始化列表除了用在对象构造上,还能将其作为普通函数的形参
3. 类型推导
// 从C++11 起, 使用auto 关键字进行类型推导
std::vector
MagicFoo(std::initializer_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
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::vectorfor (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)。