C++11 新特性及原理(一、基础篇)
引言
一转眼的功夫国庆7天假期已快结束,开始倒计时准备整理心情开始新的征程,几天的颓废终于开始清醒,整理下很久前的知识,打算把他们梳理成文章分享给大家,当然网上有很多类似的文章,希望我的角度能给大家带来新的理解。
开篇
C++11新增了100多个新特性,修补了C++98/03中的600多个缺陷,使C++11编写代码更加便捷更加高效和优雅。GCC从4.8.1才完全支持C++11标准,使用GCC4.8以上编译器编译时需要添加-std=c++11选项,才能开启编译器对C++11标准的支持。下文选取了一些C++11新特性,主要针对我们在实际开发场景中,以编码规范为基准,选择这些特征的原则如下:
1、优雅性:适当的使用该特性便于代码的阅读及维护(减少代码冗余)。
2、谨慎原则:该特性比较“安全”,没有太多的“坑”。
3、健壮及约束性:使用该特性,能让代码更加的健壮,提升编码质量。
4、高效性:使用该特性能提高代码执行效率,节省系统资源。
注意:以下代码均使用g++ (GCC) 4.8.5 20150623 (Red Hat 4.8.5-4) 版本进行的编译
一、auto 初始化类型推导(优雅性,修补缺陷)
早在C++98标准中就存在了auto关键字,那时的auto用于声明变量为自动变量,自动变量意为拥有自动的生命期,这是多余且极少使用的,因为就算不使用auto声明,变量依旧拥有自动的生命期。
C++11取而代之的是全新的auto,变量的初始化类型推导。
C++11 auto 举例:
auto nVal = 1; // int
auto fVal = 1.3284 + 6; // double auto会做隐式类型转换进行类型提升
auto strVal = "Hello World"; // string
const auto& strTmp = strVal; // const string&
std::unordered_multimap<std::string, std::string>::iterator dictIter = dict.begin();
auto dictIter = dict.begin(); // 等同于上面语句,推荐
auto使用时的注意事项:
//初始化表达式是引用,则去除引用语义
int a = 10;
int& b = a;
auto c = b; //c的类型为int
auto& d = b; //此时d的类型才为int&
//初始化表达式为const,则除去const语义
const int a1 = 10;
auto b1 = a1; //b1的类型为int而非const int
//auto关键字带上&号,则不去除const语义
const int a2 = 10;
auto& b2 = a2; //因为auto带上&,故不去除const,b2类型为const int
b2 = 10; //非法,编译错误
//初始化表达式为数组时,auto关键字推导类型为指针
int a3[3] = { 1, 2, 3 };
auto b3 = a3;
cout << typeid(b3).name() << endl; //Pi
//表达式为数组且auto带上&,则推导类型为数组类型,其对数组指针进行引用
int a4[3] = { 1, 2, 3 };
auto& b4 = a4;
cout << typeid(b4).name() << endl; //A3_i
int* a5 = &a;
auto b5 = a5; //等同int* b5 = a5;
*b5 = 1234;
auto推导的是等号右边变量或表达式类型,我们只能对变量进行引用,当加入引用后编译器直接把推导的变量定义赋予auto变量。
C++11标准中auto不能作为函数参数使用以及函数返回值使用,前者是因为这么使用会和模板函数有相同的功能,后者在C++11中也可实现,但其编写风格破坏了原则一和原则四(当函数返回值是引用时auto会移除表达式类型reference属性)这里不再阐述,该功能在C++14标准中完全实现。
auto的自动类型推断发生在编译期,是否会造成编译期的时间消耗,我认为是没有消耗的,编译器在语法分析过程中需要得知右操作数的类型,再与左操作数的类型进行比较,检查是否可以发生相应的转化,是否需要进行隐式类型转换。
注意:auto关键字是通过初始化表达式进行类型推导,如果没有初始化表达式,就无法确定左值类型,编译器会报错误。参考原则一,该特性不可泛滥使用,大量的auto会导致代码可读性变差,适当的auto会减少代码冗余,提高可读性及维护性。
二、nullptr(安全性,修补缺陷)
C++11引入nullptr总的来说是因为:我们不喜欢不确定的,模糊的东西,计算机更不喜欢。我们知道c++98/03定义一个空指针时我们用的是NULL,而NULL的定义如下:
/* Define NULL pointer value */
#ifndef NULL
#ifdef __cplusplus
#define NULL 0 // C++
#else
#define NULL ((void *)0) // C
#endif
#endif /* NULL */
NULL是一个整数0,而0就有了多重含义:0既是一个常量整数,又是一个常量空指针。 这导致了很多问题,特别是以下这点:
#include <stddef.h>
void function(int) {} // #1
void function(char*) {} // #2
int main() {
function(NULL); // 调用#1还是#2?
int* ptr = 0; // 类型不安全,没有类型检查功能。
}
希望空指针有一个type-safe的名字,这样不仅可以顺带解决以上问题,而且还对检测错误有帮助。然后在C++11中就有了以下nullptr的实现:
const class mynullptr_t
{
public:
template<class T>
inline operator T*() const{ //mynullptr_t对象可以转成T*类型对象
cout << "T* is called" << endl;
return 0;
}
template<class C, class T>
inline operator T C::*() const{ //mynullptr_t对象可以转成T::C*类型对象
cout << "T C::* is called" << endl;
return 0;
}
private:
void operator&() const;
} mynullptr = {};
class A
{
public:
int *a;
};
void function(int){printf("call int\n");}
void function(char*){printf("call char*\n");}
int main()
{
int *p = mynullptr; //T* is called
int A::*a = mynullptr; //T C::* is called
function(0); //call int
function(mynullptr); //call char*
}
这里主要用到了operator隐式类型转换的功能,operator算子的隐式类型转换,是使用当前对象去生成另一个类型的对象,mynullptr先生成一个临时对象,进行赋值时会进行该对象的隐式转换,触发了指针操作符重载函数的调用,这种转换必须有operator算子的支持。经过20年,c++终于把自己造的坑填上,回到“正常”的范围。
三、基于范围的 for loop(优雅性)
C++11这次更新了基于范围的for loop,支持了现代编程语言中都有的遍历方式, for语句可以简单地遍历列表或容器中的元素,可以用在C风格数组,初始化列表和那些带有能返回迭代器的begin()和end()函数的类型上。所有提供了begin/end的标准容器都可以使用基于范围的for语句。
int array[5] = { 1, 2, 3, 4, 5 };
for (int& x : array) {
x *= 2;
cout << x << endl;
}
// C++11新特性 初始化列表
std::map<int, std::string> hashMap = {{1, "c++"}, {2, "java"}, {3, "python"}};
// item是一个pair,如果不是引用的话, 会变成赋值操作
for (const auto& item : hashMap) {
cout << "num:" << item.first << " language:" << item.second << endl;
}
上述for语法实现原理等价于下列内容:
{
//右值引用,后面会详细讲解
auto&& __range = range_expression;
for (auto __begin = begin_expr, __end = end_expr;
__begin != __end; ++__begin){
range_declaration = *__begin;
loop_statement
}
}
从以上实现来看,如果一个类想要支持这样遍历,使数据结构能够迭代,至少得有以下方法:begin()和end() 。这个迭代器必须实现三种操作符重载:!=,前缀++,解引用。
四、右值引用及移动构造函数(提高性能)
为了方便用户阅读及保证文章长度,此章内容较多固作为单独一篇来详细描述,具体请见:
C++11 新特性及原理(二、右值引用及移动构造函数)