c++11新特性
编译环境:Windows 10 + elclipse +gcc-8.1.0
1、类型推导
C++11 引入了 auto 和 decltype 这两个关键字实现了类型推导,由编译器在编译过程中完成类型推断过程。区别:
1)语法格式区别
auto varname = value; // auto 根据=右边的初始值 value 推导出变量的类型,且varname必须初始化。 decltype(exp) varname = value; // decltype 根据 exp 表达式推导出变量的类型,跟=右边的 value 没有关系。
原则上,exp 就是一个普通的表达式,它可以是任意复杂的形式,但是必须保证 exp 的结果是有类型的,不能是 void,否则导致编译错误。用例 :
auto i = 5; // i 被推导为 int auto arr = new auto(10) // arr 被推导为 int * int a = 0; decltype(a) b = 1; //b 被推导成了 int decltype(10.8) x = 5.5; //x 被推导成了 double decltype(x + 100) y; //y 被推导成了 double
2)对CV限制的处理
「cv 限定符」是 const 和 volatile 关键字的统称:
- const 关键字用来表示数据是只读的,也就是不能被修改;
- volatile 和 const 是相反的,它用来表示数据是可变的、易变的,目的是不让 CPU 将数据缓存到寄存器,而是从原始的内存中读取。
在推导变量类型时,auto 和 decltype 对 cv 限制符的处理是不一样的。decltype 会保留 cv 限定符,而 auto 有可能会去掉 cv 限定符。
以下是 auto 关键字对 cv 限定符的推导规则:
- 如果表达式的类型不是指针或者引用,auto 会把 cv 限定符直接抛弃,推导成 non-const 或者 non-volatile 类型。
- 如果表达式的类型是指针或者引用,auto 将保留 cv 限定符。
//非指针非引用类型 const int n1 = 0; auto n2 = 10; n2 = 99; // n2 赋值成功,说明不带 const,也就是 const 被 auto 抛弃了,验证 auto 的第一条推导规则。 decltype(n1) n3 = 20; n3 = 5; //赋值报错 n3 和 p3 都赋值失败,说明 decltype 不会去掉表达式的 const 属性。 //指针类型 const int *p1 = &n1; auto p2 = p1; *p2 = 66; // p2 赋值失败,说明是带 const 的,也就是 const 没有被 auto 抛弃,验证 auto 的第二条推导规则。 decltype(p1) p3 = p1; *p3 = 19; //赋值报错 n3 和 p3 都赋值失败,说明 decltype 不会去掉表达式的 const 属性。
3)对引用的处理
当表达式的类型为引用时,auto 和 decltype 的推导规则也不一样;decltype 会保留引用类型,而 auto 会抛弃引用类型,直接推导出它的原始类型。
int n = 10; int &r1 = n; //auto推导 auto r2 = r1; r2 = 20; cout << n << ", " << r1 << ", " << r2 << endl; //decltype推导 decltype(r1) r3 = n; r3 = 99; cout << n << ", " << r1 << ", " << r3 << endl; 运行结果: 10, 10, 20 99, 99, 99
从运行结果可以发现,给 r2 赋值并没有改变 n 的值,这说明 r2 没有指向 n,而是自立门户,单独拥有单独一块内存,这就证明 r 2不再是引用类型,它的引用类型被 auto 抛弃了。
给 r3 赋值,n 的值也跟着改变了,这说明 r3 仍然指向 n,它的引用类型被 decltype 保留了。
2、返回类型后置
如下两段代码,使用将 decltype 和 auto 结合起来完成返回值类型的推导。
template<typename T, typename U> auto add(T x, U y) -> decltype(x+y) { return x+y; }
int& foo(int& i); float foo(float& f); template <typename T> auto func(T& val) -> decltype(foo(val)) { return foo(val); }
从 C++14 开始可以直接让普通函数具备返回值推导,因此下面的写法变得合法(但C++11下非法):
template<typename T, typename U> auto add(T x, U y) { return x+y; }
3、模板势力连续右括号改进
在 C++98/03 的泛型编程中,模板实例化时连续两个右尖括号(>>)会被编译器解释成右移操作符,而不是模板参数表的结束。
template <typename T>
struct Foo
{
typedef T type;
};
template <typename T>
class A
{
// ...
};
int main(void)
{
Foo<A<int>>::type xx; //编译出错。须写成Foo<A<int> >
(注意两个右尖括号之间的空格)。
return 0;
}
但这种空格限制没有必要。在 C++11 标准中取消了这种限制,要求编译器对模板的右尖括号做单独处理,使编译器能够正确判断出>>
是一个右移操作符还是模板参数表的结束标记(delimiter,界定符)。然而,这种处理在某些时候会与老标准不兼容。如下所示:
template <int N> struct Foo { // ... }; int main(void) { Foo<100 >> 2> xx; return 0; }
在 C++98/03 的编译器中编译是没问题的,但 C++11 的编译器会显示:
error: expected unqualif?ied-id before '>' token Foo<100 >> 2> xx;
正确的写法是这样:
Foo<(100 >> 2)> xx; // 注意括号
这种加括号的写法其实也是一个良好的编程习惯,使得在书写时倾向于写出无二义性的代码。
4、使用using定义别名(替换typedef)
在C++中,可以通过typdef重定义一个类型:
typedef unsigned int uint_t;
但是重定义的类型不是一个新的类型,而是原有类型的一个新名字(别名)。因此下面语句是不合法的函数重载:
void func(unsigned int); void func(uint_t); // error: redefinition
使用typedef重定义类型很方便,但也有限制,比如无法重定义一个模板。比如下列类型定义:
typedef std::map<std::string, int> map_int_t; // ... typedef std::map<std::string, std::string> map_str_t; // ...
根据上面定义,如果需要定义一个以string为固定key类型map,value可同时映射到int和string的模板时,在C++98/03中,仅通过typedef确做不到,此时只能另写一个模板。
template <typename Val> struct str_map { typedef std::map<std::string, Val> type; }; // ... str_map<int>::type map1; // ...
在C++11中,可以重新定义一个模板。如下所示:
template <typename Val> using str_map_t = std::map<std::string, Val>; // ... str_map_t<int> map1;
上述代码使用 using 定义 std::map 的模板别名 str_map_t。比起前面使用外敷模板加 typedef 构建的 str_map,它完全就像是一个新的 map 类模板,因此,简洁很多。
实际上,using 的别名语法能覆盖 typedef 的全部功能。在重定义普通类型上,两种使用方法的效果是等价的,唯一不同的是定义语法。
// 重定义unsigned int typedef unsigned int uint_t; using uint_t = unsigned int; // 重定义std::map typedef std::map<std::string, int> map_int_t; using map_int_t = std::map<std::string, int>;
定义方法时,二者功能类似,但using语法清晰:
typedef void (*func_t)(int, int); using func_t = void (*)(int, int);
using 语法和 typedef 功能是等价的,所以:
void foo(void (*func_call)(int, int)); //error: redefinition,不能重载,func_t<int> 与 void(*)(int, int) 类型等价。 void foo(func_t<int> func_call);
通过 using 可以轻松定义任意类型的模板表达方式:
template <typename T> using type_t = T; // ... type_t<int> i;
type_t实例化后的类型和它的模板参数类型等价。这里,type_t<int>将等价于int。
5、增加支持函数模板的默认模板参数
类模板:通用的类描述(使用泛型来定义类),进行实例化时,其中的泛型再用具体的类型替换。
函数模板:通用的函数描述(使用泛型来定义函数),进行实例化时,其中的泛型再用具体的类型替换。
1)在类模板声明时,C++98/C++11标准都允许其有默认模板参数。但在声明函数模板时,C++98不支持函数模板默认参数,而C++11取消了这一限制。
void DefParm(int m = 3) {} // c++98编译通过,c++11编译通过(普通函数支持默认参数) template <typename T = int> class DefClass {privare: T val;public: T get(){ return T;}}; // c++98编译通过,c++11编译通过(c++98/11都支持类模板默认参数) template <typename T = int> T DefTempParm(T t) { return t = t+t;} // c++98编译失败,c++11编译通过(c++98不支持函数模板默认参数,c++11开始支持)
2) C++11对类模板和函数模板默认参数的区别:类模板在为默认模板参数指定默认值时,必须按照“从右到左”的规则进行指定。而这个规则对于函数模板来说不是必须的。
template <typename T1, typename T2 = int> class DefClass1 {}; template <typename T1 = int, typename T2> class DefClass2 {}; // ERROR: 无法通过编译:因为模板参数的默认值没有遵循“由右往左”的规则 template <typename T, int i = 0> class DefClass3 {}; template <int i = 0, typename T> class DefClass4 {}; // ERROR: 无法通过编译:因为模板参数的默认值没有遵循“由右往左”的规则 template <typename T1 = int, typename T2> void DefFunc1(T1 a, T2 b) {}; // OK 函数模板不用遵循“由右往左”的规则 template <int i = 0, typename T> void DefFunc2(T a) {}; // OK 函数模板不用遵循“由右往左”的规则
总之,模板函数优先从函数实参中推导类型,其次才是使用默认模板参数类型。
template <class T, class U = double> void f(T t = 0, U u = 0) {}; void g() { f(1, 'c'); // f<int, char>(1, 'c') f(1); // f<int, double>(1, 0), 使用了默认模板参数double f(); // 错误: T无法被推导出来,所以编译不通过。 f<int>(); // f<int, double>(0, 0), 使用了默认模板参数double f<int, char>(); // f<int, char>(0, 0) }
6、统一列表初始化方式
c++98/03中对象初始化方法有很多种:
//初始化列表 int i_arr[3] = { 1, 2, 3 }; //普通数组 struct A { int x; struct B { int i; int j; } b; } a = { 1, { 2, 3 } }; //POD类型 。即, plain old data 类型,也就是是可以直接使用 memcpy 复制的对象。 //拷贝初始化(copy-initialization) int i = 0; class Foo { public: Foo(int) {} } foo = 123; //需要拷贝构造函数 //直接初始化(direct-initialization) int j(0); Foo bar(123);
在c++98/03中,只可以使用初始化列表(initializer list)对普通数组和POD类型进行初始化。c++11增加了初始化列表的适用性,可以用于任何类型对象初始化。
class Foo { public: Foo(int) {} private: Foo(const Foo &); }; int main(void) { Foo a1(123); //直接初始化 Foo a2 = 123; //error: 'Foo::Foo(const Foo &)' is private 拷贝构造初始化 Foo a3 = { 123 }; Foo a4 { 123 }; int a5 = { 3 }; int a6 { 3 }; return 0; }
上例中,a1 直接初始化;a2需要调用拷贝构造函数进行初始化,由于拷贝构造函数被声明私有类型,所以编译错误。
a3、a4使用新的初始化方式初始化对象,效果如同a1(a3 虽然使用了等于号,但它仍然是列表初始化,因此,私有的拷贝构造并不会影响到它。)。
a5、a6 则是基本数据类型的列表初始化方式。
a4 和 a6 的写法(不使用=号),是 C++98/03 所不具备的。在 C++11 中,可以直接在变量名后面跟上初始化列表,来进行对象的初始化。
此外,new操作符等可以使用()进行初始化的地方,也可以使用初始化列表:
int* a = new int { 123 };//c++11之前 使用int* a = new int(123); double b = double { 12.12 }; //对匿名对象使用列表初始化后,再进行拷贝初始化。 int* arr = new int[3] { 1, 2, 3 };//堆上动态分配的数组使用初始化列表进行初始化。
初始化列表也可以直接用在函数返回值上:
struct Foo { Foo(int, double) {} }; Foo func(void) { return { 123, 321.0 }; }
7、lambda匿名函数
语法格式:
[外部变量访问说明符](参数)mutable noexcept/throw()->返回值类型 { //函数体; };
最简单的匿名函数:
[]{}; //该匿名函数未引入任何外部变量([]为空),也没有传递任何参数,没有指定mutable、noexcept等关键字,没有返回值和函数体。
1)[] 外部变量访问方式说明符:
[]方括号用于向编译器表明当前是一个lambda表达式,这个符号不能忽略。在方括号内可以注明当前lambda函数体中可以使用哪些外部变量。[外部变量]使用方法如下:
外部变量格式 | 功能 |
[] | 空括号表示当前lambda匿名函数中不导入任何外部变量。 |
[=] | 表示以值传递方式导入所有外部变量。 |
[&] | 表示以引用传递方式导入所有外部变量。 |
[var1,var2,...] | 表示以值传递方式导入var1,var2等指定的外部变量。 |
[&var1,&var2,...] | 表示以引用传递方式导入var1,var2等指定的外部变量。 |
[var1,&var2,...] | 混合使用两种传递方式导入指定的外部变量。 |
[=,&var1,...] | 表示除var1以引用方式导入外,其余外部变量都以值传递方式导入。 |
[this] | 表示以值传递方式导入当前的this指针。 |
注意:单个外部变量不允许以相同的传递方式导入多次,例如[=,var1]{}中,var1先后被以值传递的方式导入了2次,因此是非法的。
2)()参数列表:
和普通函数的定义一样,lambda匿名函数可以接收外部传递的多个函数。和普通函数不同的是,当不需要传递参数是,()可以省略。
3)mutable关键字:
默认情况下,lambda表达式不能修改以值传递方式引入的外部变量值。如果要修改它们的值,则需要添加mutable关键字,使用该关键字时,前面的()不能省略。
注意:对于以值传递方式引入的外部变量,lambda 表达式修改的是拷贝的那一份,并不会修改真正的外部变量;
4)noexcept/throw():
默认情况下,lambda 函数的函数体中可以抛出任何类型的异常。标注 noexcept 关键字,则表示函数体内不会抛出任何异常;使用 throw() 可以指定 lambda 函数内部可以抛出的异常类型。如果 lambda 函数标有 noexcept 而函数体内抛出了异常,或者函数体内抛出了使用throw()限定的其它非指定类型的异常,这些异常无法使用 try-catch 捕获,会导致程序执行失败。
5)->返回值类型
指明 lambda 匿名函数的返回值类型。如果 lambda 函数体内只有一个 return 语句,或者该函数返回 void,则编译器可以自行推断出返回值类型,此情况下可以直接省略-> 返回值类型
。
6)函数体
和普通函数一样,lambda 匿名函数包含的内部代码都放置在函数体中。函数体内除了可以使用指定传递进来的参数之外,还可以使用指定的外部变量以及全局范围内的所有全局变量。外部变量会受到以值传递还是以引用传递方式引入的影响,而全局变量则不会。也就是说在 lambda 表达式内可以使用任意一个全局变量,必要时还可以直接修改它们的值。
lambda表达式定义和使用:
使用lambda匿名函数协助排序:
#include <iostream> #include <algorithm> using namespace std; int main() { int num[4] = {4, 2, 3, 1}; //对 a 数组中的元素进行排序 sort(num, num+4, [=](int x, int y) -> bool{ return x < y; } ); for(int n : num){ cout << n << " "; } return 0; }
使用传统方式协助排序:
#include <iostream> #include <algorithm> using namespace std; //自定义的升序排序规则 bool sort_up(int x,int y){ return x < y; } int main() { int num[4] = {4, 2, 3, 1}; //对 a 数组中的元素进行排序 sort(num, num+4, sort_up); for(int n : num){ cout << n << " "; } return 0; }
上述代码表明,使用lambda匿名函数时,代码更简洁。
虽然lambda匿名函数没有名称,但可以手动设置一个名称。
#include <iostream> using namespace std; int main() { //display 即为 lambda 匿名函数的函数名 auto display = [](int a,int b) -> void{cout << a << " " << b;}; //调用 lambda 函数 display(10,20); return 0; }
值传递和引用传递的区别
#include <iostream> using namespace std; //全局变量 int all_num = 0; int main() { //局部变量 int num_1 = 1; int num_2 = 2; int num_3 = 3; cout << "lambda1:\n"; auto lambda1 = [=]{ //全局变量可以访问甚至修改 all_num = 10; //函数体内只能使用按值传递的外部变量,而无法对它们进行修改 cout << num_1 << " " << num_2 << " " << num_3 << endl; }; lambda1(); cout << all_num <<endl; cout << "lambda2:\n"; auto lambda2 = [&]{ all_num = 100; //函数体内可以使用和修改按引用传递的外部变量 num_1 = 10; num_2 = 20; num_3 = 30; cout << num_1 << " " << num_2 << " " << num_3 << endl; }; lambda2(); cout << all_num << endl; return 0; }
借助 mutable 关键字可以修改按值传递的外部变量的值,但修改的是拷贝的那一份的值,真正外部变量的值并不会发生改变。
#include<iostream> using namespace std; int main(){ int num_1 = 1; int num_2 = 2; int num_3 = 3; auto lambda1 = [=]() mutable{ num_1 = 10; num_2 = 20; num_3 = 30; //输出10 20 30 cout << num_1 << " "<< num_2 << " "<< num_3 << endl; }; lambda1(); //输出1 2 3 lambda函数体内只能使用按值传递的外部变量,而无法对它们进行修改。 cout << num_1 << " "<< num_2 << " "<< num_3 << endl; return 0; }
执行抛出异常类型。如果不使用 noexcept 或者 throw(),则 lambda 匿名函数的函数体中允许发生任何类型的异常。
#include<iostream> using namespace std; int main(){ auto except = []()throw(int) {throw 10;}; try { except(); } catch (int) { cout << "捕获到了整形异常1"; } auto except1 = []()noexcept{ //不抛出异常,但函数内部抛出了异常 导致程序直接奔溃 throw 100; }; auto except2 = []()throw(char){ //值捕获了char类型异常,没捕获int异常导致程序直接奔溃 throw 10; }; try{ except1(); except2(); }catch(int){ // cout << "捕获到了整形异常2"<< endl; } return 0; }
8、 非受限联合体
联合体(Union)是一种构造数据类型,联合体内成员共享同一块内存空间。
c++11标准规定,任何非引用类型都可以成为联合体的数据成员,这种联合体也被称为非受限联合体。
c++98不允许非POD类型作为联合体成员,但c++11取消了这个限制。
class Student{ public: Student(bool g, int a): gender(g), age(a) {} private: bool gender; int age; }; union T{ Student s; // Student类含有自定义构造函数,为非POD类型的成员,因此c++98下编译报错,c++11正常。 char name[10]; }; int main(){ return 0; }
c++11允许联合体有静态成员,静态成员变量只能在联合体内定义,却不能在联合体外使用,这使得该规则很没用:
union U { static int func() { int n = 3; return n; } };
非受限联合体赋值注意事项:
c++11规定,如果非受限联合体内有一个非POD的数据成员,而该成员拥有自定义构造函数,那么这个非受限联合体的默认构造函数和其它特殊成员函数(默认的拷贝构造函数,拷贝赋值操作符以及析构函数等)都将被编译器删除。这可能导致对象构造失败。
union U { string s; int n; }; int main() { U u; // s拥有自定义构造函数,为非POD数据类型,因此U的构造函数被删除;U的类型变量u需要调用默认的构造失败。 return 0; }
当然可以用placement new来解决这个问题:
union U { string s; int n; public: U() { new(&s) string; } ~U() { s.~string(); } }; int main() { U u; return 0; }
构造时,采用 placement new 将 s 构造在其地址 &s 上,这里 placement new 的唯一作用只是调用了一下 string 类的构造函数。注意,在析构时还需要调用 string 类的析构函数。
非受限联合体的匿名声明和“枚举式类”
匿名联合体是指不具名的联合体(也即没有名字的联合体),一般定义如下:
union U{ union { int x; }; //此联合体为匿名联合体 };
联合体 U 内定义了一个不具名的联合体,该联合体包含一个 int 类型的成员变量,我们称这个联合体为匿名联合体。
同样的,非受限联合体也可以匿名,而当非受限的匿名联合体运用于类的声明时,这样的类被称为“枚举式类”。示例如下:
#include <cstring> using namespace std; class Student{ public: Student(bool g, int a): gender(g), age(a){} bool gender; int age; }; class Singer { public: enum Type { STUDENT, NATIVE, FOREIGNER }; Singer(bool g, int a) : s(g, a) { t = STUDENT; } Singer(int i) : id(i) { t = NATIVE; } Singer(const char* n, int s) { int size = (s > 9) ? 9 : s; memcpy(name , n, size); name[s] = '\0'; t = FOREIGENR; } ~Singer(){} private: Type t; union { Student s; int id; char name[10]; }; }; int main() { Singer(true, 13); Singer(310217); Singer("J Michael", 9); return 0; }
上面的代码中使用了一个匿名非受限联合体,它作为类 Singer 的“变长成员”来使用,这样的变长成员给类的编写带来了更大的灵活性,这是 C++98标准中无法达到的(编译器会报member 'Student Singer::<anonymous union>::s' with constructor not allowed in union错误)。
9、 for循环
C++ 11标准之前(C++ 98/03 标准),如果要用 for 循环语句遍历一个数组或者容器,只能套用如下结构:
for(表达式 1; 表达式 2; 表达式 3){ //循环体 }
用例:
int main() { char arc[] = "https://www.cnblogs.com/zhongqifeng/"; size_t i; //for循环遍历普通数组 for (i = 0; i < strlen(arc); i++) { cout << arc[i]; } cout << endl; vector<char>myvector(arc,arc+23); vector<char>::iterator iter; //for循环遍历 vector 容器 for (iter = myvector.begin(); iter != myvector.end(); ++iter) { cout << *iter; } return 0; }
C++ 11 标准中,除了可以沿用前面介绍的用法外,还为 for 循环添加了一种全新的语法格式:
for (declaration : expression){ //循环体 }
其中,两个参数各自的含义如下:
- declaration:表示此处要定义一个变量,该变量的类型为要遍历序列中存储元素的类型。需要注意的是,C++ 11 标准中,declaration参数处定义的变量类型可以用 auto 关键字表示,该关键字可以使编译器自行推导该变量的数据类型。
- expression:表示要遍历的序列,常见的可以为事先定义好的普通数组或者容器,还可以是用 {} 大括号初始化的序列。
用例:
int main() { char arc[] = "https://www.cnblogs.com/zhongqifeng/"; for (char ch : arc) { cout << ch; } cout << "!"<<endl; vector<char>myvector(arc, arc + 23); //for循环遍历 vector 容器 for (auto ch : myvector) { cout << ch; } cout <<"!"<< endl; return 0; }
说明:
- 程序中在遍历 myvector 容器时,定义了 auto 类型的 ch 变量,当编译器编译程序时,会通过 myvector 容器中存储的元素类型自动推导出 ch 为 char 类型。注意,这里的 ch 不是迭代器类型,而表示的是 myvector 容器中存储的每个元素。
- 仔细观察程序的输出结果,其中第一行输出的字符串和 "!" 之间还输出有一个空格,这是因为新格式的 for 循环在遍历字符串序列时,不只是遍历到最后一个字符,还会遍历位于该字符串末尾的 '\0'(字符串的结束标志)。之所以第二行输出的字符串和 "!" 之间没有空格,是因为 myvector 容器中没有存储 '\0'。
C++11 for 循环支持遍历用{ }大括号初始化的列表:
for (int num : {1, 2, 3, 4, 5}) { cout << num << " "; }
C++11 for 循环遍历某个序列时,如果需要遍历的同时修改序列中元素的值,实现方案是在 declaration 参数处定义引用形式的变量:
int main() { char arc[] = "abcde"; vector<char>myvector(arc, arc + 5); //for循环遍历并修改容器中各个字符的值 for (auto &ch : myvector) { ch++; } //for循环遍历输出容器中各个字符 for (auto ch : myvector) { cout << ch; } return 0; } /* 输出: * bcdef * */
declaration 参数既可以定义普通形式的变量,也可以定义引用形式的变量。如果需要在遍历序列的过程中修改器内部元素的值,就必须定义引用形式的变量;反之,建议定义const &(常引用)形式的变量(避免了底层复制变量的过程,效率更高),也可以定义普通变量。
注意事项:
1)使用 for 循环遍历某个序列时,无论该序列是普通数组、容器还是用{ }大括号包裹的初始化列表,遍历序列的变量都表示的是当前序列中的各个元素。
//for循环遍历初始化列表 for (int ch : { 1, 2, 3, 4, 5 }) { cout << ch; } cout << "@" << endl; //for循环遍历普通数组 char arc[] = "https://www.cnblogs.com/zhongqifeng/"; for (char ch : arc) { cout << ch; } cout << "@" << endl; //for循环遍历 vector 容器 vector<char> myvector(arc, arc + 23); for (auto ch : myvector) { cout << ch; } cout << "@" << endl;
map<string, string> mymap { { "C++11", "C++11" }, { "Python", "Python" }, { "Java", "Java" } }; for (pair<string, string> ch : mymap) { cout << ch.first << " " << ch.second << endl; }
2) 总的来说,基于范围的 for 循环可以遍历普通数组、string字符串、容器以及初始化列表。除此之外,for 循环冒号后还可以放置返回 string 字符串以及容器对象的函数。
string str = "https://www.cnblogs.com/zhongqifeng/"; vector<int> myvector = { 1, 2, 3, 4, 5 }; string retStr() { return str; } vector<int> retVector() { return myvector; } int main() { //遍历函数返回的 string 字符串 for (char ch : retStr()) { cout << ch; } cout << endl; //遍历函数返回的 vector 容器 for (int num : retVector()) { cout << num << " "; } return 0; }
3) for 循环不支持遍历函数返回的以指针形式表示的数组。
string str = "https://www.cnblogs.com/zhongqifeng/"; vector<int> myvector = { 1, 2, 3, 4, 5 }; char* retStr() { return str; } int main() { //遍历函数返回的 string 字符串 for (char ch : retStr()) { //error cout << ch; } return 0; }
4) 基于范围的 for 循环遍历的是某函数返回的 string 对象或者容器时,整个遍历过程中,函数只会执行一次。
string str = "https://www.cnblogs.com/zhongqifeng/"; string retStr() { cout << "retStr:" << endl; return str; } int main() { //遍历函数返回的 string 字符串 for (char ch : retStr()) { cout << ch; } return 0; } /** 输出 * retStr: * https://www.cnblogs.com/zhongqifeng/ * */
5) 当使用基于范围的 for 循环遍历此类型容器时,切勿修改容器中不允许被修改的数据部分。
- 不允许修改 map、unordered_map、multimap 以及 unordered_multimap 容器存储的键的值;
- 不允许修改 set、unordered_set、multiset 以及 unordered_multiset 容器中存储的元素的值。
6) 基于范围的 for 循环完成对容器的遍历,其底层也是借助容器的迭代器实现的,在使用基于范围的 for 循环遍历容器时,应避免在循环体中修改容器存储元素的个数。
int main() { std::vector<int> arr = { 1, 2, 3, 4, 5 }; for (auto val : arr) { std::cout << val << std::endl; arr.push_back(10); //向容器中添加元素 } return 0; } /** * 输出结果错误 * 1 * 0 * 131408 * 0 * 5 * */
10 、constexpr(验证是否为常量表达式)
实际开发中,我们经常会用到常量表达式。以定义数组为例,数组的长度就必须是一个常量表达式:
int url[10];//正确 int url[6 + 4];//正确 int length = 6; int url[length];//错误,length是变量
C++ 程序的执行过程大致要经历编译、链接、运行这 3 个阶段,常量表达式只能在程序运行阶段计算出结果,而常量表达式的计算往往发生在程序的编译阶段。因为表达式只需要在编译阶段计算一次,进而极大提高程序的执行效率,节省了每次程序运行时都需要计算一次的时间。
constexpr 关键字的功能是使指定的常量表达式获得在程序编译阶段计算出结果的能力,而不必等到程序运行阶段。C++ 11 标准中,constexpr 可用于修饰普通变量、函数(包括模板函数)以及类的构造函数。
注意,获得在编译阶段计算出结果的能力,并不代表 constexpr 修饰的表达式一定会在程序编译阶段被执行,具体的计算时机还是编译器说了算。
1) constexpr 修饰普通变量
C++11 标准中,定义变量时可以用 constexpr 修饰,从而使该变量获得在编译阶段即可计算出结果的能力。
constexpr int num = 1 + 2 + 3; //编译器在编译期间计算num结果
int url[num] = { 1, 2, 3, 4, 5, 6 };
cout << url[1] << endl;
/**
* 输出结果
* 2
* */
当常量表达式中包含浮点数时,考虑到程序编译和运行所在的系统环境可能不同,常量表达式在编译阶段和运行阶段计算出的结果精度很可能会受到影响,因此 C++11 标准规定,浮点常量表达式在编译阶段计算的精度要至少等于(或者高于)运行阶段计算出的精度。
2)constexpr修饰函数
constexpr 可用于修饰函数的返回值,这样的函数又称为“常量表达式函数”。
constexpr 并非可以修饰任意函数的返回值,一个函数要想成为常量表达式函数,必须满足如下 4 个条件:
- 整个函数的函数体中,除了可以包含 using 指令、typedef 语句以及 static_assert 断言外,只能包含一条 return 返回语句。
constexpr int display(int x) { //int ret = 1 + 2 + x; //c++11 编译不通过,因为函数体只能包含一条return语句;c++ 17 编译通过,函数体中可以包含多条语句。 //return ret; //可以添加 using 执行、typedef 语句以及 static_assert 断言 return 1 + 2 + x; } int main() { cout << display(3) << endl; return 0; }
可以看到,display() 函数的返回值是用 constexpr 修饰的 int 类型值,且该函数的函数体中只包含一个 return 语句。
- 该函数必须有返回值。
//非法 constexpr void display() { //函数体 }
- 函数在使用之前,必须先定义函数(通常情况下,函数的使用分为“声明”和“定义”两部分,普通的函数调用只需要提前写好该函数的声明部分即可(函数的定义部分可以放在调用位置之后甚至其它文件中),但常量表达式函数在使用前,必须要有该函数的定义。)。
//普通函数的声明 int noconst_dis(int x); //常量表达式函数的声明 constexpr int display(int x); //常量表达式函数的定义 constexpr int display(int x) { return 1 + 2 + x; } int main() { //调用常量表达式函数 int a[display(3)] = { 1, 2, 3, 4 }; cout << a[2] << endl; //调用普通函数 cout << noconst_dis(3) << endl; return 0; } //普通函数的定义 int noconst_dis(int x) { return 1 + 2 + x; }
- return 返回的表达式必须是常量表达式。
constexpr int num = 3; constexpr int display(int x) { return num + x; } int main() { //调用常量表达式函数 int a[display(3)] = { 1, 2, 3, 4 }; //constexpr int num = 3; 前去掉constexpr时,编译通不过,因为int num=3需要运行时才能确定num的值。 return 0; }
3)constexpr修饰构造函数
constexpr可以直接修饰 C++ 内置类型的数据,但不能直接修饰自定义的数据类型(struct 或者 class d等)。
//自定义类型的定义 constexpr struct myType { //error 编译错误,不能修饰自定义类型 const char* name; int age; //其它结构体成员 }; int main() { constexpr struct myType mt { "zhangsan", 10 }; return 0; }
当我们想自定义一个可产生常量的类型时,正确的做法是在该类型的内部添加一个常量构造函数。
//自定义类型的定义 struct myType { //编译错误,不能修饰自定义类型 constexpr myType(char *name,int age):name(name),age(age){}; const char* name; int age; //其它结构体成员 }; int main() { constexpr struct myType mt { "zhangsan", 10 }; cout << mt.name << " " << mt.age << endl; return 0; }
可以看到,在 myType 结构体中自定义有一个构造函数,借助此函数,用 constexpr 修饰的 myType 类型的 my 常量即可通过编译。
注意,constexpr 修饰类的构造函数时,要求该构造函数的函数体必须为空,且采用初始化列表的方式为各个成员赋值时,必须使用常量表达式。
前面提到,constexpr 可用于修饰函数,而类中的成员方法完全可以看做是“位于类这个命名空间中的函数”,所以 constexpr 也可以修饰类中的成员函数,只不过此函数必须满足前面提到的 4个条件。
//自定义类型的定义 class myType { public: constexpr myType(const char *name, int age) : name(name), age(age) { } ; constexpr const char* getname() { return name; } constexpr int getage() { return age; } private: const char *name; int age; //其它结构体成员 }; int main() { constexpr struct myType mt {"zhangsan", 10}; constexpr const char * name = mt.getname(); constexpr int age = mt.getage(); cout << name << " " << age << endl; return 0; }
注意,C++11 标准中,不支持用 constexpr 修饰带有 virtual 的成员方法。
4)constexpr修饰模板函数
C++11 语法中,constexpr 可以修饰模板函数,但由于模板中类型的不确定性,因此模板函数实例化后的函数是否符合常量表达式函数的要求也是不确定的。
针对这种情况下,C++11 标准规定,如果 constexpr 修饰的模板函数实例化结果不满足常量表达式函数的要求,则 constexpr 会被自动忽略,即该函数就等同于一个普通函数。
//自定义类型的定义 struct myType { const char *name; int age; //其它结构体成员 }; //模板函数 template<typename T> constexpr T dispaly(T t) { return t; } int main() { struct myType stu { "zhangsan", 10 }; //普通函数 struct myType ret = dispaly(stu); cout << ret.name << " " << ret.age << endl; //常量表达式函数 constexpr int ret1 = dispaly(10); cout << ret1 << endl; return 0; }
可以看到,示例程序中定义了一个模板函数 display(),但由于其返回值类型未定,因此在实例化之前无法判断其是否符合常量表达式函数的要求:
- 当模板函数中以自定义结构体 myType 类型进行实例化时,由于该结构体中没有定义常量表达式构造函数,所以实例化后的函数不是常量表达式函数,此时 constexpr 是无效的;
- 模板函数的类型 T 为 int 类型,实例化后的函数符合常量表达式函数的要求,所以该函数的返回值就是一个常量表达式。
11、constexpr和const的区别
constexpr 是 C++ 11 标准新添加的关键字,在此之前(C++ 98/03标准)只有 const 关键字,其在实际使用中经常会表现出两种不同的语义。
void dis_1(const int x){ //错误,x是只读的变量 array<int,x> myarr{1,2,3,4,5}; cout << myarr[1] << endl; } void dis_2(){ const int x = 5; //x 为只读变量,并为一个值为5的常量,所以可以用来初始化array容器 array <int,x> myarr{1,2,3,4,5}; cout << myarr[1] << endl; } int main() { dis_1(5); dis_2(); return 0; }
C++11 标准中,建议将 const 和 constexpr 的功能区分开,即凡是表达“只读”语义的场景都使用 const,表达“常量”语义的场景都使用 constexpr。
另外,“只读”和“不允许被修改”之间并没有必然的联系。
int main() { int a = 10; const int &con_b = a; cout << con_b << endl; a = 20; cout << con_b << endl; return 0; } /** * 输出 * 10 * 20 * */
程序中用 const 修饰了 con_b 变量,表示该变量“只读”,即无法通过变量自身去修改自己的值。但这并不意味着 con_b 的值不能借助其它变量间接改变,通过改变 a 的值就可以使 con_b 的值发生变化。
在大部分实际场景中,const 和 constexpr 是可以混用,它们完全等价,都可以在程序的编译阶段计算出结果。:
const int a = 5 + 4; constexpr int a = 5 + 4;
但在某些场景中,必须明确使用 constexpr:
constexpr int sqr1(int arg) { return arg * arg; } const int sqr2(int arg) { return arg * arg; } int main() { array<int, sqr1(10)> mylist1; //可以,因为sqr1时constexpr函数 array<int, sqr2(10)> mylist1; //不可以,因为sqr2不是constexpr函数 return 0; }
其中,因为 sqr2() 函数的返回值仅有 const 修饰,而没有用更明确的 constexpr 修饰,导致其无法用于初始化 array 容器(只有常量才能初始化 array 容器)。
总之,C++ 11 标准中,const 用于为修饰的变量添加“只读”属性;而 constexpr 关键字则用于指明其后是一个常量(或者常量表达式),编译器在编译程序时可以顺带将其结果计算出来,而无需等到程序运行阶段,这样的优化极大地提高了程序的执行效率。
12、C++11 标准中所有整型数据类型
C++ 11 标准中,基于整数大小的考虑,共提供了如表 1 所示的这些数据类型。与此同时,标准中还明确限定了各个数据类型最少占用的位数。
整型类型 | 等价类型 |
C++11标准规定占用最少位数 |
short | short int(有符号短整型) | 至少16位(2个字节) |
signed short | ||
signed short int | ||
unsigned short | unsigned short int(有符号短整型) | |
unsigned short int | ||
int | int(有符号整型) | 至少16位(2个字节) |
signed | ||
signed int | ||
unsigned | unsigned int(无符号整型) | |
unsigned int | ||
long | long int(有符号长整型) | 至少32位(4个字节) |
long int | ||
signed long int | ||
unsigned long | unsigned long int(无符号长整形) | |
unsigned long int | ||
long long(C++11) | long long int(有符号超长整形) | 至少 64 位(8 个字节) |
long long int(C++11) | ||
signed long long(C++11) | ||
signed long long int (C++11) | ||
unsigned long long(C++11) | unsigned long long int(无符号超长整型) | |
unsigned long long int(C++11) |
C++11 标准规定,每种整数类型必须同时具备有符号(signed)和无符号(unsigned)两种类型,且每种具体的有符号整形和无符号整形所占用的存储空间(也就是位数)必须相同。注意,C++11 标准中只限定了每种类型最少占用多少存储空间,不同的平台可以占用不同的存储空间。
在前文表格罗列的这些数据类型中,long long 超长整型是 C++ 11 标准新添加的。
如同 long 类型整数需明确标注 "L" 或者 "l" 后缀一样,要使用 long long 类型的整数,也必须标注对应的后缀:
- 对于有符号 long long 整形,后缀用 "LL" 或者 "ll" 标识。例如,"10LL" 就表示有符号超长整数 10;
- 对于无符号 long long 整形,后缀用 "ULL"、"ull"、"Ull" 或者 "uLL" 标识。例如,"10ULL" 就表示无符号超长整数 10;
如果不添加任何标识,则所有的整数都会默认为 int 类型。
对于任意一种数据类型,读者可能更关心的是此类型的取值范围。对于 long long 类型来说,如果想了解当前平台上 long long 整形的取值范围,可以使用<climits>头文件中与 long long 整形相关的 3 个宏,分别为 LLONG_MIN、LLONG_MAX 和 ULLONG_MIN:
- LLONG_MIN:代表当前平台上最小的 long long 类型整数;
- LLONG_MAX:代表当前平台上最大的 long long 类型整数;
- ULLONG_MIN:代表当前平台上最大的 unsigned long long 类型整数(无符号超长整型的最小值为 0);
int main() { cout << "long long最大值:" << LLONG_MIN << " " << hex << LLONG_MIN << "\n"; cout << dec << "long long最小值:" << LLONG_MAX << " " << hex << LLONG_MAX << "\n"; cout << dec << "unsigned long long最大值:" << ULLONG_MAX << " " << hex << ULLONG_MAX; return 0; } /** * 当前平台(Windows10 64位操作系统)上输出显示long long 超长整型占用 64 位(也就是 16 个字节)的存储空间 * long long最大值:-9223372036854775808 8000000000000000 * long long最小值:9223372036854775807 7fffffffffffffff * unsigned long long最大值:18446744073709551615 ffffffffffffffff * */
13、右值引用
右值引用主要用于实现移动(move)语义和完美转发。
1)C++左值和右值
左值:lvalue,即loactor value的缩写,意为存储再内存中、有明确存储地址(可寻址)的数据。
右值:rvalue,即read value的缩写,是指可以提供数据值的数据(不一定可以寻址,例如存储于寄存器中的数据)
左值与右值判断方法:
- 位于赋值号左侧的表达式为左值,位于赋值号右侧的表达式为右值(C++中,左值也可以当作右值使用)。
int a = 5; //a 为左值,5为右值 5 = a; //错误,5 不能为左值 int b = 10; a = b; //a、b都是左值,只不过b可以当右值用
- 有名称、可获取到存储地址的表达式为左值,反之为右值。(上述变量a、b是变量名,可以通过&a、&b获得其地址,因此都是左值;字面量5、10既没有名称,也无法获取其地址,因为字面量通常存储在寄存器或与代码存储在一起,因此都是右值)
2)C++ 右值引用
c++98/03 标准中使用“&”来表示引用。但是这种引用方式有缺陷,即正常情况下只能操作c++中的左值,无法对右值添加引用。
int num = 10; int &b = num; //正确 int &c = 10; //错误
如上所示,编译器允许我们为num左值建立一个引用,但不能为10这个右值建立引用。因此,c++98/03标准中的引用又称为左值引用。
虽然 c++98/03 标准不支持为右值建立非常量左值引用,但允许使用常量左值引用操作右值。
int num = 10; const int &b = num; const int &c = 10;
右值往往是没有名称的,因此要使用它只能借助引用的方式。这就产生一个问题,实际开发中我们可能需要对右值进行修改(实现移动语义时就需要),显然左值引用的方式是行不通的。因此,c++11 标准新引入了另一种引用方式,称为右值引用,用 "&&" 表示。
右值引用使用规则:
- 右值引用必须立即进行初始化操作,且只能使用右值进行初始化。
int num = 10; //int && a = num; //右值引用不能初始化为左值 int && a = 10;
- 右值引用还可以对右值进行修改。
int && a = 10; a = 100; cout << a << endl;
14、C++移动构造函数
1)c++11移动语义
在c++11之前,如果想要对其它对象初始化一个同类的新对象,只能借助类的拷贝构造函数,然而在拷贝构造(特别是深拷贝)操作时效率太低,导致程序执行效率下降。下面代码中定义为demo类自定义一个深拷贝构造函数,避免只拷贝指针成员,导致多个对象中的指针成员指向同一块堆空间,进而在对象析构时对该空间释放多次,这是不允许的。
class demo{ public: demo():num(new int(0)){ cout<<"construct!"<<endl; } //拷贝构造函数 demo(const demo &d):num(new int(*d.num)){ cout<<"copy construct!"<<endl; } ~demo(){ cout<<"class destruct!"<<endl; delete num; } private: int *num; }; demo get_demo(){ return demo(); } int main() { demo a = get_demo(); return 0; }
main函数中调用get_demo函数返回一个demo对象并赋值给变量a时,整个流程有如下几个阶段:
- 执行get_dome函数内部demo()语句,即调用demo类的默认构造函数生成一个匿名对象;
- 执行get_dome函数内部 return demo(),此时调用拷贝构造函数赋值一份之前生成的匿名对象,将其作为get_demo()函数的返回值(函数体执行完毕之前,匿名对象会被析构函数销毁);
- 执行a=get_demo()语句,再调用一次拷贝构造函数,将之前拷贝得到的临时对象赋值给a(此行代码执行完毕,get_demo()函数返回的对象会被析构销毁);
- main函数执行结束前,会自行调用demo类的析构函数销毁a。
注意:目前大多编译器都会对程序中发生的的拷贝进行优化。所有在程序中,我们往往只能看到优化后的输出:
construct! class destruct!
而实际完整输出时这样的:
construct! <-- 执行 demo() copy construct! <-- 执行 return demo() class destruct! <-- 销毁 demo() 产生的匿名对象 copy construct! <-- 执行 a = get_demo() class destruct! <-- 销毁 get_demo() 返回的临时对象 class destruct! <-- 销毁 a
如上所示,利用拷贝构造函数实现对a对象初始化,底层实际上进行了2次拷贝(而且是深拷贝)操作。这对于申请少量堆空间的临时对象来说,深拷贝执行效率依旧可以接受,但如果临时对象的指针成员申请了大量堆空间,那么2次深拷贝操作势必会影响a对象初始化的执行效率。
以此,c++11 标准引入了右值引用的语法,借助它可以实现移动语义,解决当类中包含指针成员变量初始化同类对象时,避免深度拷贝导致的效率问题。
2)C++移动构造函数
移动语义是指以移动而非深拷贝的方式初始化含有指针成员的类对象。简单理解,移动语义指的是将其他对象(通常时临时对象)拥有的内存资源“移为己用”。
以前面程序中的 demo 类为例,该类的成员都包含一个整形的指针成员,其默认指向的是容纳一个整形变量的堆空间。当使用 get_demo() 函数返回的临时对象初始化 a 时,我们只需要将临时对象的 num 指针直接浅拷贝给 a.num,然后修改该临时对象中 num 指针的指向(通常另其指向 nullptr),这样就完成了 a.num 的初始化。
事实上,对于程序执行过程中产生的临时对象,往往只用于传递数据(没有其它的用处),并且会很快会被销毁。因此在使用临时对象初始化新对象时,我们可以将其包含的指针成员指向的内存资源直接移给新对象所有,无需再新拷贝一份,这大大提高了初始化的执行效率。
class demo{ public: demo():num(new int(0)){ cout<<"construct!"<<endl; } //拷贝构造函数 demo(const demo &d):num(new int(*d.num)){ cout<<"copy construct!"<<endl; } //添加移动构造函数 demo(demo &&d):num(d.num){ d.num = nullptr; cout<<"move construct!"<<endl; } ~demo(){ cout<<"class destruct!"<<endl; delete num; } private: int *num; }; demo get_demo(){ return demo(); } int main() { demo a = get_demo(); return 0; }
实际输出:
construct! move construct! class destruct! move construct! class destruct! class destruct!执行结果说明,当demo添加移动构造函数指挥,使用临时对象初始化操作过程中产生的两次拷贝操作都由转移构造函数完成。
上述代码中实现移动构造函数,此构造函数使用右值引用形式的参数,称为移动构造函数。在此构造函数中,num指针变量采用的是浅拷贝的复制方式,同时在函数内部重置了d.num,有效避免“同一块内存空间被释放多次”的情况发生。
我们知道,非const右值引用只能操作右值,程序执行结果中产生的临时对象(例如函数返回值、lambda表达式等)既无名称也无法获取存储地址,所以属于右值。当类中同时含有拷贝构造函数和移动构造函数时,如果使用临时对象初始化当前类对象,编译器会有效调用移动构造函数来完成此操作。只有当类中没有合适的移动构造函数时,编译器才会退而求其次,调用拷贝构造函数。
在实际开发中,通常在类中自定义移动构造函数的同时,会再为其定义一个适当的拷贝构造函数,由此当用户利用右值初始化类对象时,会调用移动构造函数;使用左值(非右值)初始化类对象时,会调用拷贝构造函数
如何在左值初始化同类对象使用移动构造函数来完成?
一般情况下,左值初始化同类对象只能通过拷贝构造函数完成,如果想用移动构造函数,则必须使用右值进行初始化。c++11标准中引入std::move()函数,它可以将左值强制转换成对应的右值,由此变可以使用移动构造函数。
15、std::move函数
c++11标准中,使用std::move()函数将左值强制转换为右值。通常,借助右值引用可以为指定的类添加移动构造函数,这样当使用该类的右值对象(可以理解为临时对象)初始化同类对象时,编译器会优先选择移动构造函数。
注意:移动构造函数的调用时机是:用同类的右值对象初始化新对象。那么,用当前类的左值对象(有名称,能获取其存储地址的实例对象)初始化同类对象时,是否就无法调用移动构造函数呢?我们可以使用std::move()函数。
std::move函数用法:
class demo{ public: demo():num(new int(0)){ cout<<"construct!"<<endl; } //拷贝构造函数 demo(const demo &d):num(new int(*d.num)){ cout<<"copy construct!"<<endl; } //添加移动构造函数 demo(demo &&d):num(d.num){ d.num = nullptr; cout<<"move construct!"<<endl; } ~demo(){ cout<<"class destruct!"<<endl; delete num; } public: int *num; }; int main() { demo a; demo b = a; //调用拷贝构造函数 cout<<*b.num<<endl; //可以执行 cout<<*a.num<<endl; //可以执行 demo c = std::move(a); cout<<*c.num<<endl; //可以执行 //cout<<*a.num<<endl; //a.num = nullptr,代码运行时报错。 return 0; } /*输出: construct! copy construct! 0 0 move construct! 0 class destruct! class destruct! class destruct! */
通过观察结果,以及对a、b、c初始化操作可知,a对象为左值,直接用于初始化b对象,底层调用的是拷贝构造函数;而通过std::move函数得到a对象的右值形式,用其初始化c对象,编译器会优先调用移动构造函数。
注意:调用拷贝构造函数时,不影响a对象。调用移动构造函数时,由于内部函数将a.num指针重置为nullptr,所以程序cout<<*a.num<<endl; 执行时发生错误。
std::move()函数的灵活使用:
class first { public: first() :num(new int(0)) { cout << "construct!" << endl; } //移动构造函数 first(first &&d) :num(d.num) { d.num = nullptr; cout << "first move construct!" << endl; } ~first() { delete num; // cout << "first move desconstruct!" << endl; } public: int *num; }; class second { public: second() :fir() {} //用 first 类的移动构造函数初始化 fir second(second && sec) :fir(move(sec.fir)) { cout << "second move construct" << endl; } ~second() { // cout << "second move desconstruct" << endl; } public: first fir; }; int main() { second oth; second oth2 = move(oth); //cout << *oth.fir.num << endl; //程序报运行时错误 return 0; } /* construct! first move construct! second move construct */
上述代码避免两次拷贝构造过程,有利提升程序执行效率。
16、完美转发
完美转发是指函数模板可以将自己的参数“完美”地转发给内部调用的其它函数,所谓完美,即不仅能够准确地转发参数的值,还能保证被转发参数的左、右值属性不变。
示例:
template<typename T> void function(T t) { otherdef(t); }
function() 函数模板中调用了 otherdef() 函数。在此基础上,完美转发指的是:如果 function() 函数接收到的参数 t 为左值,那么该函数传递给 otherdef() 的参数 t 也是左值;反之如果 function() 函数接收到的参数 t 为右值,那么传递给 otherdef() 函数的参数 t 也必须为右值。
显然,function() 函数模板并没有实现完美转发。一方面,参数 t 为非引用类型,这意味着在调用 function() 函数时,实参将值传递给形参的过程就需要额外进行一次拷贝操作;另一方面,无论调用 function() 函数模板时传递给参数 t 的是左值还是右值,对于函数内部的参数 t 来说,它有自己的名称,也可以获取它的存储地址,因此它永远都是左值,也就是说,传递给 otherdef() 函数的参数 t 永远都是左值。总之,无论从那个角度看,function() 函数的定义都不“完美”。
完美转发这样严苛的参数传递机制,C++98/03 标准中几乎不会用到,但 C++11 标准为 C++ 引入了右值引用和移动语义,因此很多场景中是否实现完美转发,直接决定了该参数的传递过程使用的是拷贝语义(调用拷贝构造函数)还是移动语义(调用移动构造函数)。
function() 函数为例,在 C++11 标准中实现完美转发,只需要编写如下一个模板函数即可:
template <typename T> void function(T&& t) { otherdef(t); }
此模板函数的参数 t 既可以接收左值,也可以接收右值。但仅仅使用右值引用作为函数模板的参数是远远不够的,还有一个问题继续解决,即如果调用 function() 函数时为其传递一个左值引用或者右值引用的实参,如下所示:
int n = 10; int & num = n; function(num); // T 为 int& int && num2 = 11; function(num2); // T 为 int &&
由 function(num) 实例化的函数底层就变成了 function(int & & t),同样由 function(num2) 实例化的函数底层则变成了 function(int && && t)。要知道,C++98/03 标准是不支持这种用法的,而 C++ 11标准为了更好地实现完美转发,特意为其指定了新的类型匹配规则,又称为引用折叠规则(假设用 A 表示实际传递参数的类型):
- 当实参为左值或者左值引用(A&)时,函数模板中 T&& 将转变为 A&(A& && = A&);
- 当实参为右值或者右值引用(A&&)时,函数模板中 T&& 将转变为 A&&(A&& && = A&&)。
通过将函数模板的形参类型设置为 T&&,我们可以很好地解决接收左、右值的问题。但除此之外,还需要解决一个问题,即无论传入的形参是左值还是右值,对于函数模板内部来说,形参既有名称又能寻址,因此它都是左值。那么如何才能将函数模板接收到的形参连同其左、右值属性,一起传递给被调用的函数呢?
C++11 标准的开发者已经帮我们想好的解决方案,该新标准还引入了一个模板函数 forword<T>(),我们只需要调用该函数,就可以很方便地解决此问题。仍以 function 模板函数为例,如下演示了该函数模板的用法:
#include <iostream> using namespace std; //重载被调用函数,查看完美转发的效果 void otherdef(int & t) { cout << "lvalue\n"; } void otherdef(const int & t) { cout << "rvalue\n"; } //实现完美转发的函数模板 template <typename T> void function(T&& t) { otherdef(forward<T>(t)); } int main() { function(5); int x = 1; function(x); return 0; } /* rvalue lvalue */
此 function() 模板函数才是实现完美转发的最终版本。可以看到,forword() 函数模板用于修饰被调用函数中需要维持参数左、右值属性的参数。
总的来说,在定义模板函数时,我们采用右值引用的语法格式定义参数类型,由此该函数既可以接收外界传入的左值,也可以接收右值;其次,还需要使用 C++11 标准库提供的 forword() 模板函数修饰被调用函数中需要维持左、右值属性的参数。由此即可轻松实现函数模板中参数的完美转发。
17、nullptr
c++98/03标准中,将一个指针初始化为空指针有2种方式:
int *p = 0; int *p = NULL; //推荐使用
将指针明确指向 0(0x0000 0000)这个内存空间。一方面,明确指针的指向可以避免其成为野指针;另一方面,大多数操作系统都不允许用户对地址为 0 的内存空间执行写操作,若用户在程序中尝试修改其内容,则程序运行会直接报错。
相比第一种方式,我们更习惯将指针初始化为 NULL。值得一提的是,NULL 并不是 C++ 的关键字,它是 C++ 为我们事先定义好的一个宏,并且它的值往往就是字面量 0(#define NULL 0)。
nullptr 是 nullptr_t 类型的右值常量,专用于初始化空类型指针。nullptr_t 是 C++11 新增加的数据类型,可称为“指针空值类型”。也就是说,nullpter 仅是该类型的一个实例对象(已经定义好,可以直接使用),如果需要我们完全定义出多个同 nullptr 完全一样的实例对象。
int * a1 = nullptr; char * a2 = nullptr; double * a3 = nullptr;
通过将指针初始化为 nullptr,可以很好地解决 NULL 遗留的问题。
#include <iostream> using namespace std; void isnull(void *c){ cout << "void*c" << endl; } void isnull(int n){ cout << "int n" << endl; } int main() { isnull(NULL);
isnull(0) isnull(nullptr); return 0; } /* int n
int n void*c */
通过结果,我们可以看出,由于 nullptr 无法隐式转换为整形,而可以隐式匹配指针类型。
所以,C++11 标准下,相比 NULL 和 0,使用 nullptr 初始化空指针可以令我们编写的程序更加健壮。
18、智能指针
请参考C++智能指针使用说明一文。