目录
迈向现代c++
1.1被弃用的c++
注意∶弃用并非彻底不能用,只是用于暗示程序员这些特性将从未来的标准中消失,应该尽量避免使用。但是,已弃用的特性依然是标准库的一部分,并且出于兼容性的考虑,大部分特性其实会『永久』保留。
(1)不再允许字符串字面值常量赋值给一个char 。如果需要用字符串字面值常量赋值和初始化一个char ,应该使用const char 或者auto cpp,
char str = “hello world!”;//l将出现弃用警告
(2)C++98异常说明、unexpected_handler、 set_unexpected()等相关特性被弃用,应该使用noexcept;
(3)auto_ptr被弃用,应使用unique_ptr*;
(4)register关键宇被弃用,可以使用但不再具备任何实际含义。bool类型的+操作被弃用。;
(5)如果一个类有析构函数,为其生成拷贝构造函数和拷贝赋值运算符的特性被弃用了;
(6)C语言风格的类型转换被弃用(即在变量前使用(convert_type)),应该使用static_cast、reinterpret_cast、const_cast来进行类型转换。
(7)特别地,在最新的C++17标准中弃用了一些可以使用的C标准库,例如、. 与等
1.2与C的兼容性
从现在开始,你的脑子里应该树立『C++不是C的一个超集』这个观念(而且从一开始就不是,后面的进一步阅读的参考文献中给出了C++98和C99之间的区别)。在编写C++时,也应该尽可能的避免使用诸如void*之类的程序风格。而在不得不使用C时,应该注意使用extern "c”这种特性,将C语言的代码与C++代码进行分离编译,再统一链接这种做法,例如:
// foo.h
extern "C"
{
#endif
int add(int x,int y);
#ifdef __cplusplus
}
#endif
// foo.c
int add(int x,int y)
{
return x+y;
}
//1.2cpp
#include "foo.h"
#include <iostream>
#include <functional>
int main()
{
[out = std::ref(std::cout << "Result from C code: " << add(1, 2))](){
out.get()<<".\n";}()
return 0;
}
//跟我一起学习后续看
编译
gcc foo.c -o foo.o
g++ 1.2.cpp foo.o -std=c++2a -o 1.2
语言可用性
当我们声明、定义一个变量或者常量,对代码进行流程控制、面向对象的功能、模板编程等这些都是运行时之前,可能发生在编写代码或编译器编译代码时的行为。为此,我们通常谈及语言可用性,是指那些发生在运行时之前的语言行为。
2.1常量
nullptr(代替NULL)
nullptr 出现的目的是为了替代 NULL。在某种意义上来说,传统 C++ 会把 NULL、0 视为同一种东西,这取决于编译器如何定义 NULL,有些编译器会将 NULL 定义为 ((void)0),有些则会直接将其定义为0;
C++ 不允许直接将 void * 隐式转换到其他类型。但如果编译器尝试把 NULL 定义为 ((void)0),那么在下面这句代码中
const *char=NULL;
//没有了 void * 隐式转换的 C++ 只好将 NULL 定义为 0。而这依然会产生新的问题,将 NULL 定义成 0 将导致 C++ 中重载特性发生混乱。考虑下面这两个 foo 函数:
//
#include <iostream>
#include <type_traits>
void foo(char*);
void foo(int);
int main()
{
if(if (std::is_same<decltype(NULL), decltype(0)>::value)
std::cout << "NULL == 0" << std::endl;
if (std::is_same<decltype(NULL), decltype((void*)0)>::value)
std::cout << "NULL == (void *)0" << std::endl;
if (std::is_same<decltype(NULL), std::nullptr_t>::value))
std::cout << "NULL == nullptr" << std::endl;
foo(0); // 调用 foo(int)
// foo(NULL); // 该行不能通过编译
foo(nullptr); // 调用 foo(char*)
return 0;
}
void foo(char *) {
std::cout << "foo(char*) is called" << std::endl;
}
void foo(int i) {
std::cout << "foo(int) is called" << std::endl;
}
foo(int) is called
foo(char*) is called
此外,在上面的代码中,我们使用了 decltype 和 std::is_same 这两个属于现代 C++ 的语法,简单来说,decltype 用于类型推导,而 std::is_same 用于比较两个类型是否相等,
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];//在vscode支持这个是合法的
char arr_4[len_2_constexpr];
char arr_6[len_foo_constexpr()+1];
//char arr_6[len_foo()+1];//非法
std::cout << fibonacci(10) << std::endl;
// 1, 1, 2, 3, 5, 8, 13, 21, 34, 55
}
的例子中,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变量及其初始化
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, 2, 3, 4
for (std::vector<int>::iterator element = vec.begin(); element != vec.end(); ++element)
std::cout << *element << std::endl;
}
在上面的代码中,我们可以看到 itr 这一变量是定义在整个 main() 的作用域内的,这导致当我们需要再次遍历整个 std::vectors 时,需要重新命名另一个变量。C++17 消除了这一限制,使得我们可以在 if(或 switch)中完成这一操作
if (const std::vector<int>::iterator itr = std::find(vec.begin(), vec.end(), 3);
itr != vec.end()) {
*itr = 4; }
初始化列表
初始化是一个非常重要的语言特性,最常见的就是在对象进行初始化时进行使用。在传统 C++ 中,不同的对象有着不同的初始化方法,例如普通数组、POD (Plain Old Data,即没有构造、析构和虚函数的类或结构体)类型都可以使用 {} 进行初始化,也就是我们所说的初始化列表。而对于类对象的初始
化,要么需要通过拷贝构造、要么就需要使用 () 进行。这些不同法都针对各自对象,不能通用。
例如
#include <iostream>
#include <vector>
#include<iostream>
class Foo {
public:
int value_a;
int value_b;
Foo(int a, int b) : value_a(a), value_b(b) {}
};
int main() {
// before C++11
int arr[3] = {1, 2, 3};
Foo foo(1, 2);
std::vector<int> vec = {1, 2, 3, 4, 5};
std::cout << "arr[0]: " << arr[0] << std::endl;
std::cout << "foo:" << foo.value_a << ", " << foo.value_b << std::endl;
for (std::vector<int>::iterator it = vec.begin(); it != vec.end(); ++it) {
std::cout << *it << std::endl;
}
return 0; }
//C++ 新特性 Foo foo {2,3}
为了解决这个问题,C++11 首先把初始化列表的概念绑定到了类型上,并将其称之为 std::initializer_list,
允许构造函数或其他函数像参数一样使用初始化列表,这就为类对象的初始化与普通数组和 POD 的初
始化方法提供了统一的桥梁,例如:
#include<initializer_list>
#include<vector>
#include<iostream>
class MagicFoo
{
public:
std::vector<int>vec;
MagicFoo(std::initializer_list<int> list)
{
for(std::initializer_list<int>::iterator it=list.begin();
it!=list.end();++it)
vec.push_back(*it);
}
void foo(std::initializer_list<int> list)
{
std::cout<<"magicFoo:";
for(std::initializer_list<int>::iterator itr=list.begin();
itr!=list.end();itr++)
{
std::cout<<*itr<<",";
}
std::cout<<std::endl;
}
};
int main()
{
MagicFoo magicfoo={1,2,3,4,5};
std::cout<<"magicFoo:";
for(std::vector<int>::iterator itr=magicfoo.vec.begin();
itr!=magicfoo.vec.end();itr++)
{
std::cout<<*itr<<",";
}
std::cout<<std::endl;
magicfoo.foo({6,7,8,9});
return 0;
}
结构化绑定
结构化绑定提供了类似其他语言中提供的多返回值的功能。在容器一章中,我们会学到 C++11 新增了 std::tuple 容器用于构造一个元组,进而囊括多个返回值。但缺陷是,C++11/14 并没有提供一种简单的方法直接从元组中拿到并定义元组中的元素,尽管我们可以使用 std::tie 对元组进行拆包,但我们依然必须非常清楚这个元组包含多少个对象,各个对象是什么类型,非常麻烦。
#include<iostream>
#include<tuple>
#include<typeinfo>
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;
}
// 虽然有错误 但是能运行!!!(只需声明-std=c++11)
2.3类型推导
auto(只对变量)
auto 在很早以前就已经进入了 C++,但是他始终作为一个存储类型的指示符存在,与 register 并
存。在传统 C++ 中,如果一个变量没有声明为 register 变量,将自动被视为一个 auto 变量。而随着
register 被弃用(在 C++17 中作为保留关键字,以后使用,目前不具备实际意义),对 auto 的语义变
更也就非常自然了。
#include <initializer_list>
#include <vector>
#include <iostream>
class MagicFoo {
public:
std::vector<int> vec;
MagicFoo(std::initializer_list<int> list) {
// 从 C++11 起, 使用 auto 关键字进行类型推导
for (auto it = list.begin(); it != list.end(); ++it) {
vec.push_back(*it);
} }
};
int main() {
MagicFoo magicFoo = {1, 2, 3, 4, 5};
std::cout << "magicFoo: ";
for (auto it = magicFoo.vec.begin(); it != magicFoo.vec.end(); ++it) {
std::cout << *it << ", "; }
std::cout << std::endl;
return 0; }
*注意:auto 不能用于函数传参,因此下面的做法是无法通过编译的(考虑重载的问题,我们应该使用模板)
auto auto_arr2[10] = {arr}; // 错误, 无法推导数组元素类型
decltype
decltype 关键字是为了解决 auto 关键字只能对变量进行类型推导的缺陷而出现的。它的用法和ypeof 很相似:
decltype(表达式)
用例:
auto x = 1;
auto y = 2;
decltype(x+y) z;
你已经在前面的例子中看到 decltype 用于推断类型的用法,下面这个例子就是判断上面的变量 x,y, z 是否是同一类型:
if (std::is_same<decltype(x), int>::value)
std::cout << "type x == int" << std::endl;
if (std::is_same<decltype(x), float>::value)
std::cout << "type x == float" << std::endl;
if (std::is_same<decltype(x), decltype(z)>::value)
std::cout << "type z == type x" << std::endl;
尾返回类型推导
你可能会思考,在介绍 auto 时,我们已经提过 auto 不能用于函数形参进行类型推导,那么 auto能不能用于推导函数的返回类型呢?还是考虑一个加法函数的例子,在传统 C++ 中我们必须这么写:
template<typename T, typename U>
auto add(T x, U y) {
return x+y
}
auto q = add3<double, int>(1.0, 2);
std::cout << "q: " << q << std::endl;
2.4控制流
if_constexpr
正如本章开头出,我们知道了 C++11 引入了 constexpr 关键字,它将表达式或函数编译为常量结果。一个很自然的想法是,如果我们把这一特性引入到条件判断中去,让代码在编译时就完成分支判断,岂不是能让程序效率更高?C++17 将 constexpr 这个关键字引入到 if 语句中,允许在代码中声明常量
表达式的判断条件,考虑下面的代码:
#include<iostream>
#include<typeinfo>
#include <type_traits>
template<typename T>
auto print_type_info(const T&t)
{
if constexpr(std::is_integral<T>::value)
{
return t+1;
}
else
{
return t+0.0001;
}
}
int main()
{
std::cout<<print_type_info(5)<<std::endl;
std::cout<<print_type_info(3.14)<<std::endl;
return 0;
}
实际流程
int print_type_info(const int& t) {
return t + 1; }
double print_type_info(const double& t) {
return t + 0.001; }
区间for迭代
#include <iostream>
#include <vector>
#include <algorithm>
int main()
{
std::vector<int> vec={1,2,3,4};
for(auto element:vec)
{
std::cout << element << std::endl; // read only
}
for (auto &element : vec) {
element += 1; // writeable
}
for (auto element : vec)
std::cout << element << std::endl; // read only
}