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 新特性及原理(二、右值引用及移动构造函数)

 

 

 

 

10-06 11:39