第 1 章 迈向现代 C++
1.1 被弃用的特性
在学习现代 C++ 之前,我们先了解一下从 C++11 开始,被弃用的主要特性:
注意:弃用并非彻底不能用,只是用于暗示程序员这些特性将从未来的标准中消失,应该尽量避免使用。但是,已弃用的特性依然是标准库的一部分,并且出于兼容性的考虑,大部分特性其实会『永久』保留。
-
不再允许字符串字面值常量赋值给一个 char *。如果需要用字符串字面值常量赋值和初始化一个 char *,应该使用 **const char *** 或者 auto。
char *str = "hello world!"; // 将出现弃用警告
-
C++98 异常说明、 unexpected_handler、set_unexpected() 等相关特性被弃用,应该使用 noexcept。
-
auto_ptr 被弃用,应使用 unique_ptr。
-
register 关键字被弃用,可以使用但不再具备任何实际含义。
-
bool 类型的 ++ 操作被弃用。
-
如果一个类有析构函数,为其生成拷贝构造函数和拷贝赋值运算符的特性被弃用了。
-
C 语言风格的类型转换被弃用(即在变量前使用 (convert_type)),应该使用 static_cast、reinterpret_cast、const_cast 来进行类型转换。
-
特别地,在最新的 C++17 标准中弃用了一些可以使用的 C 标准库,例如 、、 与 等
第2章 语言可用性的强化
2.1 常量
nullptr
C++11 引入了 nullptr 关键字,专门用来区分空指针、0。而 nullptr 的类型为 nullptr_t,能够隐式的转换为任何指针或成员指针的类型,也能和他们进行相等或者不等的比较。
constexpr
C++ 本身已经具备了常量表达式的概念,比如 1+2, 3*4 这种表达式总是会产生相同的结果并且没有任何副作用。如果编译器能够在编译时就把这些表达式直接优化并植入到程序运行时,将能增加程序的性能。一个非常明显的例子就是在数组的定义阶段:
#include <iostream>
#define LEN 10
int len_foo() {
int i = 2;
return i;
}
constexpr int len_foo_constexpr() {
return 5;
}
constexpr int fibonacci(const int n) {
return n == 1 || n == 2 ? 1 : fibonacci(n-1)+fibonacci(n-2);
}
int main() {
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]; // 合法
// char arr_5[len_foo()+5]; // 非法
char arr_6[len_foo_constexpr() + 1]; // 合法
std::cout << fibonacci(10) << std::endl;
// 1, 1, 2, 3, 5, 8, 13, 21, 34, 55
std::cout << fibonacci(10) << std::endl;
return 0;
}
上面的例子中,char arr_4[len_2] 可能比较令人困惑,因为 len_2 已经被定义为了常量。为什么 char arr_4[len_2] 仍然是非法的呢?这是因为 C++ 标准中数组的长度必须是一个常量表达式,而对于 len_2 而言,这是一个 const 常数,而不是一个常量表达式,因此(即便这种行为在大部分编译器中都支持,但是)它是一个非法的行为,我们需要使用接下来即将介绍的 C++11 引入的 constexpr 特性来解决这个问题;而对于 arr_5 来说,C++98 之前的编译器无法得知 len_foo() 在运行期实际上是返回一个常数,这也就导致了非法的产生。
注意,现在大部分编译器其实都带有自身编译优化,很多非法行为在编译器优化的加持下会变得合法,若需重现编译报错的现象需要使用老版本的编译器。
2.2 变量及其初始化
2.2.1 if/switch 变量声明强化
在传统 C++ 中,变量的声明虽然能够位于任何位置,甚至于 for 语句内能够声明一个临时变量 int,但始终没有办法在 if 和 switch 语句中声明一个临时的变量。例如:
#include <iostream>
#include <vector>
#include <algorithm>
int main() {
std::vector<int> vec = {1, 2, 3, 4};
// 在 c++17 之前
const std::vector<int>::iterator itr = std::find(vec.begin(), vec.end(), 2);
if (itr != vec.end()) {
*itr = 3;
}
// 需要重新定义一个新的变量
const std::vector<int>::iterator itr2 = std::find(vec.begin(), vec.end(), 3);
if (itr2 != vec.end()) {
*itr2 = 4;
}
// 将输出 1, 4, 3, 4
for (std::vector<int>::iterator element = vec.begin(); element != vec.end();
++element)
std::cout << *element << std::endl;
}
在上面的代码中,我们可以看到 itr 这一变量是定义在整个 main() 的作用域内的,这导致当我们需要再次遍历整个 std::vector 时,需要重新命名另一个变量。C++17 消除了这一限制,使得我们可以在 if(或 switch)中完成这一操作:
// 将临时变量放到 if 语句内
if (const std::vector<int>::iterator itr = std::find(vec.begin(), vec.end(), 3);
itr != vec.end()) {
*itr = 4;
}
2.2.2 初始化列表
c++11后基本不变:
- C++11 首先把初始化列表的概念绑定到类型上,称其为 std::initializer_list,允许构造函数或其他函数像参数一样使用初始化列表,这就为类对象的初始化与普通数组和 POD 的初始化方法提供了统一的桥梁
2.2.3 结构化绑定
tuple
定义与初始化
tuple可以使用初始化列表进行赋值。
tuple<int,double,string> t3 = {1, 2.0, "3"};
访问
可以使用get<常量表达式>(tuple_name)来访问或修改tuple的元素(返回引用)
get<0>(t3) = 4;
cout << get<1>(t3) << endl; //输出2
批量赋值
std::tie会将变量的引用整合成一个tuple,从而实现批量赋值。
int i; double d; string s;
tie(i, d, s) = t3;
cout << i << " " << d << " " << s << endl;
还可以使用std::ignore忽略某些tuple中的某些返回值,如
tie(i, ignore, s) = t3;
结构化绑定提供了类似其他语言中提供的多返回值的功能。在容器一章中,我们会学到 C++11 新增了 std::tuple 容器用于构造一个元组,进而囊括多个返回值。但缺陷是,C++11/14 并没有提供一种简单的方法直接从元组中拿到并定义元组中的元素,尽管我们可以使用 std::tie 对元组进行拆包,但我们依然必须非常清楚这个元组包含多少个对象,各个对象是什么类型,非常麻烦。
C++17 完善了这一设定,给出的结构化绑定可以让我们写出这样的代码:
#include <iostream>
#include <tuple>
std::tuple<int, double, std::string> f() {
return std::make_tuple(1, 2.3, "456");
}
int main() {
auto [x, y, z] = f();
std::cout << x << ", " << y << ", " << z << std::endl;
return 0;
}