C++:C++11新特性详解(1)

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/linwh8/article/details/51569807

前言:

虽然目前没有编译器能够完全实现C++11,但这并不意味着我们不需要了解,学习它。深入学习C++11,你会发现这根本就是一门新的语言,它解决了c++98中许多遗留下来的问题。早晚会有一天,C++11便会普及大部分编译器。因此,提早做些准备也是应该的。

在此我想做一个关于C++11的专题,将C++11的新特性进行一一讲解,以通俗易懂的语言及例子帮助读者入门C++11。本文便是C++11新特性详解系列文章的第一篇, 即C++:C++11新特性详解(1)

不过我要强调的是,这些文章主要是介绍C++11的新特性,有些在C++11不能编译通过的语法在C++14甚至C++17中支持。所以,这种问题应当灵活处理

不过还有一点要强调,这些文章是我学习相关书籍以及博文而做的总结,而且对于书中和博文中许多没有解释清楚的细节性问题我大都做了补充。因此这写文章也算上是我个人的笔记,相信是一份不错的教程

C++11的简要介绍
(1)出于保证稳定性与兼容性增加了不少新特性,如long long整数类型、静态断言、外部模板等等 ;
(2)具有广泛应用性、能与其他已有的或者新增的特性结合起来使用的、具有普适性的一些新特性,如继承构造函数,委派构造函数,列表初始化等等;
(3)对原有一些语言特性的改进,如auto类型推导、追踪返回类型、基于范围的for循环,等等;
(4)在安全方面所做的改进,如枚举类型安全和指针安全等方面的内容;
(5)为了进一步提升和挖掘C++程序性能和让C++能更好地适应各种新硬件,如多核,多线程,并行编程等等;
(6)颠覆C++一贯设计思想的新特性,如lambda表达式等;
(7)C++11为了解决C++编程中各种典型实际问题而做出的有效改进,如对Unicode的深入支持等。

下面是C++11的主要新特性:
C++11
C++11


// 2016.06.09 补充了nullptr部分

看完了前言,相信你对c++11的背景有了初步的了解。本篇文章主要详解的C++特性如下:
(1)auto的类型推导;
(2)decltype的类型推导;
(3)auto 与 decltype 结合的追踪返回类型;
(4)基于范围的for循环;
(5)nullptr;
(6)折叠规则;
(7)经典面试题:int (*(*pf())()) () { return nullptr; }

1.auto类型推导

在早期版本中,关键字auto主要是用于声明具有自动存储期的局部变量。然而,它并没有被经常使用。原因是:除了static类型以外,其他变量(以“数据类型+变量名”的方式定义)都默认为具有自动存储期,所以auto关键字可有可无。

所以,在C++11的版本中,删除了auto原本的功能,并进行了重新定义了。即C++11中的auto具有类型推导的功能。在讲解auto之前,我们先来了解什么是静态类型,什么是动态类型

(1)静态类型,动态类型,类型推导

通俗的来讲,所谓的静态类型就是在使用变量前需要先定义变量的数据类型,而动态类型无需定义。

严格的来讲,静态类型是在编译时进行类型检查,而动态类型是在运行时进行类型检查。

如python:

a = "helloworld"; // 动态类型

而C++:

std::string a = "helloworld"; // 静态类型

如今c++11中重新定义了auto的功能,这便使得静态类型也能够实现类似于动态类型的类型推导功能,十分有趣~

下面是auto的基本用法:

double func();
auto a  = 1; // int, 尽管1时const int类型,但是auto会自动去const
auto b = func(); // double
auto c; // wrong, auto需要初始化的值进行类型推导,有点类似于引用

注意: 其实auto就相当于一个类型声明时的占位符,而不是一种“类型”的声明,在编译时期编译器会将auto替代成变量的实际类型。

(2)auto的优势

I. 拥有初始化表达式的复杂类型变量声明时的简化代码。

也就是说,auto能够节省代码量,使代码更加简洁, 增强可读性。

如:

std::vector<std::string> array;
std::vector<std::string>::iterator it = array.begin();
// auto
auto it = array.begin();

auto在STL中应用非常广泛,如果在代码中需要多次使用迭代器,用auto便大大减少了代码量,使得代码更加简洁,增强了可读性

II.免除程序员在一些类型声明时的麻烦,或者避免一些在类型声明时的错误。

如:

class PI {
public:
    double operator *(float v) {
        return (double)val*v;
    }
    const float val = 3.1415927f;
};
int main(void) {
    float radius = 5.23f;
    PI pi;
    auto circumference = 2*( pi*radius);
    return 0;
}

设计PI类的作者将PI的*运算符进行了重载,使两个float类型的数相乘返回double类型。这样做的原因便是避免数据上溢以及精度降低。假如用户将circumference定义为float类,就白白浪费了PI类作者的一番好意,用auto便不会出现这样的问题。

但是auto并不能解决所有的精度问题,如:

unsigned int a = 4294967295; // unsigned int 能够存储的最大数据
unsigned int b = 1;
auto c = a+b;
cout << c << endl; // 输出c为0

a+b显然超出了unsigned int 能够存储的数据范围,但是auto不能自动将其匹配为能存储这一数据而不造成溢出的类型如unsigned long类型。所以在精度问题上自己还是要多留一点心,分析数据是否会溢出

III.“自适应”性能在一定程度上支持泛型编程。

如上面提到PI类,假如原作者要修改重载*返回的数据类型,即将double换成其他类型如long double,则它可以直接修改而无需修改main函数中的值。

再如这种“适应性”还能体现在模板的定义中:

template <typename T1, typename T2>
double func(const T1& a, const T2& b) {
    auto c = a + b;
    return c;
}
// 其实直接return a+b;也是可以的,这里只是举个例子,同时点出auto不能用于声明函数形参这一易错点

但是有一点要注意:不能将auto用于声明函数形参,所以不能用auto替代T1,T2。

然而,因为func()只能返回double值,所以func()还可以进一步泛化,那就需要decltype的使用了,在后面会详细讲解。

现此处有一段有趣的宏定义:

# define Max1(a, b) ((a) > (b)) ? (a) : (b)
# define Max2(a, b) ({ \
            auto _a = (a);
            auto _b = (b);
            (_a > _b) ? _a : _b;})

用Max2的宏定义效率更高。

(3)auto 使用细则

int x;
int* y = &x;
double foo();
int& bar();
auto* a = &x; // a:int*
auto& b = x; // b:int&
auto c = y; // c:int*
auto* d = y; // d:int*
auto* e = &foo(); // wrong, 指针不能指向临时变量
auto &f = foo(); // wrong, 左值引用不能存储右值
auto g = bar(); // int
auto &h = bar(); // int&

其实,对于指针而言, auto* a = &x <=> auto a = &x
但是对于引用而言,上面的情况就不遵循了,如果是引用, 要在auto后加&。

double foo();
float* bar();
const auto a = foo(); // a:const double
const auto &b = foo(); // b:const double&
volatile auto* c = bar(); // c:volatile float*

auto d = a; // d:double
auto &e = a; // e:const double
auto f = c; // f:float*
volatile auto& g = c; // g:volatile float*&

auto 会自动删除const(常量性),volatile(易失性)

对于引用和指针,即auto*, auto&仍会保持const与volatile

auto x = 1, y = 2; // (1) correct
const auto* m = &x, n = 1; // (2)correct
auto i = 1, j = 3.14f; // (3) wrong
auto o = 1, &p = 0, *q = &p; // (4)correct

auto有规定,当定义多个变量时,所有变量的类型都要一致,且为第一个变量的类型,否则编译出错。

对于(1): x, y都是int类型,符合auto定义多变量的机制, 编译通过;
对于(2):我们发现,m、n的类型不同,那为什么不报错?变量类型一致是指auto一致。m为const int*, 则auto匹配的是int,而n恰好为int类型,所以编译通过;
对于(3): i的类型是int, j的类型是float,类型不相同,编译出错;
对于(4): o的类型是int, p前有&,其实就是auto&, 即p为int&,而q前有,相当于auto,即q为int*,不难发现o, p, q三者auto匹配都为int,所以符合auto定义多变量的机制,编译通过。

(4)局限性

void func(auto x = 1) {} // (1)wrong
struct Node {
    auto value = 10; // (2)wrong
};
int main(void) {
    char x[3];
    auto y = x;
    auto z[3] = x; // (3)wrong
    vector<auto> v = {1}; // (4)wrong
}

I.auto不能作为函数参数,否则无法通过编译;
II.auto不能推导非静态成员变量的类型,因为auto是在编译时期进行推导;
III.auto 不能用于声明数组,否则无法通过编译;
IV.auto不能作为模板参数(实例化时), 否则无法通过编译。

2.decltype 类型推导

类型推导是随着模板和泛型编程的广泛使用而引入的。在非泛型编程中,类型是明确的,而在模板与泛型编程中,类型是不明确的,它取决于传入的参数类型。

decltype与我前面讲到的auto还是有一些共同点的,如二者都是通过推导获得的类型来定义另外一个变量,再如二者都是在编译时进行类型推导。不过他们类型推导的方式有所不同,auto是通过初始化表达式推导出类型,而decltype是通过普通表达式的返回值推导出类型。

不过在讲解decltype之前,我们先来了解一下typeid

(1)typeid 与 decltype

对于C语言,是完全不支持动态类型的;
对于C++,与C不同的是,C++98标准中已经有部分支持动态类型了,便是运行时类型识别(RTTI)。

RTTI机制:为每个类型产生一个type_info类型数据,程序员可以在程序中使用typeid随时查询一个变量的类型,typeid就会返回变量相应的type_info数据,type_info的name成员可以返回类型的名字。在C++11中,增加了hash_code这个成员函数,返回该类型唯一的哈希值以供程序员对变量类型随时进行比较

也许你会有这样一个想法:我直接对type_info.name进行字符串比较不就可以了么,为什么还要给每个类型一个哈希值?我认为,字符串比较的开销也是比较大的,如果用每个类型来对于一个哈希值,通过比较哈希值确定类型是否相同的方法,会比使用字符串比较的效率要高得多。

下面一段代码是对typeid()type_info.name(), type_info.hash_code的应用:

# include <iostream>
# include <typeinfo>
using namespace std;
class white {};
class Black {};
int main(void) {
    white a;
    Black b;
    // white 与 black 前的数字会因编译器的不同而不同,如g++打印的是5
    cout << typeid(a).name() << endl; // 5 white
    cout << typeid(b).name() << endl; // 5 Black
    white c;
    bool a_b_sametype = (typeid(a).hash_code() == typeid(b).hash_code());
    bool a_c_sametype = (typeid(a).hash_code() == typeid(c).hash_code());
    cout << "Same type?" << endl;
    cout << "A and B" << (int)a_b_sametype << endl; // 0
    cout << "A and C" << (int)a_c_sametype << endl; // 1
    return 0;
}

然而,RTTI无法满足程序员的需求:因为RTTI在运行时才确定出类型,而更多的需求是在编译时确定类型。并且,通常的程序是要使用推导出来的这种类型而不是单纯地识别它

(2)decltypr的应用

I.decltype 与 using / typedef 连用
在头文件,常常看到如下定义:

using size_t = decltype(sizeof(0));
using ptrdiff_t = decltype((int *)0-(int*)0);
using nullptr_t = decltype(nullptr);

II.增加代码的可读性

std::vector<std::string> vec;
typedef decltype(vec.begin()) iterator; // (1)
decltype(vec)::iterator it; // (2)

III.重用匿名类型

enum class {K1, K2, K3} anon_e;  // 匿名的强类型枚举
union {
    decltype (anon_e) key;
    char* name;
} anon_u; // 匿名的union联合体
struct {
    int d;
    decltype(anon_u) id; 
} anon_s[100]; // 匿名的struct数组
int main(void) {
    decltype(anon_s) as;
    as[0].id.key = decltype(anon_e)::K1; // 引用匿名强类型枚举中的值
    return 0;
}

注:对于强类型枚举,也是C++11中的新特性,这个放到以后的文章里讲。

一般来说,使用匿名便是表明只想使用一次,不想被重用,此处只是表明decltype能实现这种功能。

IV.decltype 可以适当扩大模板泛型编程的能力。

template <typename T1, typename T2>
void Sum(T1& t1, T2 & t2, decltype(t1+t2)& s) {
    s = t1+t2
}
int main(void) {
    int a = 3;
    long b = 5;
    float c = 1.0f, f = 2.3f;
    long e;
    float f;
    Sum(a, b, e);
    Sum(c, d, f);
    return 0;
}

通过将返回类型改为void,并将原来的返回值s定义于形参之中,利用decltype(t1+t2)& 对其声明,使t1 + t2的返回值不会受到限制。如此显然在一定程度上扩大了模板泛型编程的能力。

但是此处还是有个缺陷,程序员仍然需要提前设定返回值的类型,如变量e与变量f,还是不能实现真正意义上的模板泛型编程。为使其更加泛化,通常采用追踪返回类型来实现,这个在后边会讲到。

V. 实例化模板

int hash(char* );
map<char*, decltype(hash)> dict_key; // wrong, decltype需要利用函数返回值进行推导
map<char*, decltype(hash(nullptr))> dict_key1;

(3)decltype 推导的四规则

在了解这四个规则之前,我们先来了解标记符表达式(id-expression)的概念。

标记符表达式(id-expression):所有除去关键字和字面量等编译器需要使用的标记以外的程序员自定义的标记(token)都可以是标记符(identifier), 而单个标记符对应的表达式就是标记符表达式

int arr[4], int i, arr与i就是标记符表达式。对于前者,去除关键字int与字面量[4]后剩下的arr便是标记符表达式。

还有一点,C++11中对值的类型分类与C++98有所不同。在C++98中,值可分左值与右值。通俗地来讲, 所谓的左值便是含有变量名的数值,所谓的右值就是没有变量名的数值,即为临时变量, 以及包含右值引用。而在C++11中,就将右值更进一层地分类:分为纯右值与将亡值,纯右值即为没有变量名的数值,将亡值即为右值引用,且左值与将亡值合称为泛左值

decltype推导的四规则如下:
(1)如果e是一个没有带括号的标记符表达式或者类成员访问表达式,那么decltype(e)就是e所命名的实体的类型。此外,如果e是一个被重载的函数,可能会导致编译错误;
(2)否则,假设e的类型是T,如果e是一个将亡值(xvalue), 那么decltype(e)为T&&;
(3)否则,假设e的类型是T,如果e是一个左值,则decltype(e)为T&;
(4)否则,假设e的类型是个T, 则decltype(e)为T。

下面通过代码分别对四规则进行举例:

int arr[5] = {0};
int *ptr = arr;
struct S {
    double d;
} s;
void overload(int);
void overload(char);
int&& RvalRef();
const bool Func(int);

// 规则1
decltype(arr) var1; // int [5]
decltype(ptr) var2; // int*
decltype(s.d) var4; // double
decltype(Overloaded) var5; // wrong

// 规则2
decltype(RvalRef()) val6 = 1; // int&&

// 规则3
decltype(true? i : i) var7 = i; // int&
decltype((i)) var8 = i; // int&
decltype(++i) var9 = i; // int&
decltype(arr[3]) var10 = i; // int&
decltype(*ptr) var11 = i; //int&
decltype("lval") var2 = "lval"; // (const char*)&

// 规则4
decltype(1) var13; // int
decltype(i++) var14; // int
decltype(Func(1)) var15; // const bool

上面的代码中,需要重点注意的是:
(1)++i 与 i++: ++i返回的是左值引用,i++返回的是右值。
(2)字符串的字面常量为左值,其它字符串字面量为右值。
(3)对于 decltype((i)) val8 = idecltype((i)) val8 = 1;前者能通过编译,后者不可以,提示的错误信息如下:

aaa.cpp: In functionint main()’:
aaa.cpp:6:26: error: invalid initialization of non-const reference of type ‘int&’ from an rvalue of type ‘int’
     decltype((i)) val8 = 1;

原因是:i是标记符表达式,而(i)不是标记符表达式,所以它遵循的应当是规则3,decltype推导的类型为int&,即左值引用。i是左值,将i赋给val8是可以的,但是讲1赋给val8却是不可以的。1是右值编译器不允许将一个右值赋给一个左值引用,所以编译不通过,这点要注意!

补充一下,在C++11的标准库中提供了is_lvalue_reference<>与is_rvalue_reference<>, 用于判断方括号内的内容的类型是否为左值引用或右值引用。如果是,则返回1,如若不是,则返回0。所以可以利用他们来检验decltype推导出的类型是否为左值引用和右值引用。

(4)cv限制符的继承与冗余的符号

所谓的cv限制符是指:const和volatile。

与auto不同,decltype能够“带走”表达式的cv限制符。不过,如果对象的定义中有cv限制符时,其成员不会继承const或volatile限制符。

举例说明:

# include <iostream>
# include <type_traits>
using namespace std;

const int ic = 0;
volatile int iv;
struct S {
    int i;
};
const S a = {0};
volatile S b;
volatile S* p = &b;

int main(void) {
    cout << is_const<decltype(ic)>::value << endl; // 1
    cout << is_volatile<decltype(iv)>::value << endl; // 1
    cout << is_const<decltype(a)>::value << endl; // 1
    cout << is_volatile<decltype(b)>::value << endl; // 1
    cout << is_const<decltype(a.i)>::value << endl; // 0
    cout << is_volatile<decltype(p->i)>::value << endl; // 0
    return 0;
}

还有,使用decltype从表达式推导出类型后进行类型定义时,可能会出现一些冗余的符号:cv限制符,符号引用&。如果推导出的类型已经有了这些属性,冗余的符号将会被忽略,这种规则叫做折叠规则

下面用表格来概括一下折叠规则

typedef T& TR;  // T&的位置是对于TR的类型定义
TR v;  // TR的位置是声明变量v的类型

TR的类型定义 声明变量v的类型 v的实际类型
T& TR T&
T& TR& T&
T& TR&& T&
T&& TR T&&
T&& TR& T&
T&& TR&& T&&

规律:
当TR为T&时,无论定义类型v时有多少个&,最终v的类型都是T&;
当TR为T&&时,则v最终的类型与定义类型v时&的数量有关:
(1)如果&的总数量为奇数,则v的最终类型为T&;
(2)如果&的总数量为偶数,则v的最终类型为T&&。

上面主要是对引用符号&的冗余处理,那么对于指针符号* decltype该如何处理呢?

# include <iostream>
# include <type_traits>
using namespace std;
int i = 1;
int &j = i;
int *p = &i;
const int k = 1;
int main(void) {
    decltype(i)& val1 = i;
    decltype(i)& val2 = i;
    cout << is_lvalue_reference<decltype(val1)>::value << endl; // 1
    cout << is_rvalue_reference<decltype(val2)>::value << endl; // 0
    cout << is_lvalue_reference<decltype(val2)>::value << endl; // 1
    decltype(p)* val3 = &i; // 编译失败,val3为int**类型,等号右侧为int*类型
    decltype(p)* val4 = &p; // int**
    auto* val5 = p; // int*
    v3 = &i;
    const decltype(k) var4 = 1; // 冗余的const
    return 0;
}

由上面的代码可知,auto对于*的冗余编译器采取忽略的措施,而decltype对于*的冗余编译器采取不忽略的措施。

(5)追踪返回类型

在C++98中,如果一个函数模板的返回类型依赖于实际的入口参数类型,那么该返回类型在模板实例化之前可能都无法确定,这样的话我们在定义该函数模板时就会遇到麻烦。

我们很快就会想到可以用decltype来定义:

// 注:本段代码不能通过编译,只是帮助读者了解追踪返回类型的由来
template<typename T1, typename T2>
decltype(t1+t2) Sum(T1& t1, T2& t2) {
    return t1+t2;
}

不过这样有个问题。因为编译器是从左到右读取的,此时decltype内的t1, t2都未声明,而按照 C/C++ 编译器的规则,变量在使用前必须声明。

因此, 为了解决这一问题,C++11引进了新语法——追踪返回类型,来声明和定义这样的函数:

template<typename T1, typename T2>
auto Sum(T1& t1, T2& t2)->decltype(t1+t2) {
    return t1+t2;
}

auto**占位符**与->return_type是构成追踪返回类型函数的两个基本元素。

I.引进返回类型函数后返回类型无需写明作用域:
如:

class ABCDEFGH {
    struct B {};
};
// 原来版本
ABCDEFGH::B ABCDEFGH::func() {}
// c++11
auto ABCDEFGH::func()->B {}

这样再一定程度上能简化代码,增强可读性;

II.使模板更加泛化(如Sum函数)
III.简化函数的定义,提高代码的可读性。

在这里举一个很经典的例子:

# include <iostream>
# include <type_traits>
using namespace std;

int (*(*pf())()) () { return nullptr; }

// auto(*)()->int(*) ()  返回int(*)()类型的函数p
// auto pf1()->auto(*)()->int(*)() // 返回函数p的函数
auto pf1()->auto(*)()->int(*)() { return nullptr; }

int main(void) {
    cout << is_same<decltype(pf), decltype(pf1)>::value << endl; // 1
    return 0;
}

首先说明一下main函数:我认为应该是is_same<decltype(pf()), decltype(pf1())>::value, 即p1、p2后面要加上括号,但是不知道为什么两种方式都能通过编译,此处求指点~

我先来分析一下那个很复杂很复杂的函数:int (*(*pf())()) () { return nullptr; }
先介绍一下函数指针返回函数指针的函数的语法:

// function ptr
return_type(*func_pointer)(parameter_list)
// A function return func_pointer
return_type(*function(func_parameter_list))(parameter_list) {}

函数指针的变量名为func_pointer,指向的函数返回类型为return_type参数列表为parameter_list
返回函数指针的函数名称为function,参数列表为func_parameter_list,返回类型为return_type(*)(parameter_list)

对于int (*(*pf())()) () { return nullptr; }
(1)该函数的返回类型为int(*)(), 是一个指向返回类型为int,参数列表为空的函数的指针;
(2)该函数的参数列表为空;
(3)该函数的名称为*pf();
(4)说明pf()返回的也是一个函数指针,且这个函数指针指向该函数。

这种函数的定义方式使得代码的可读性大大降低,C++11中的追踪返回类型能大大改善这种情况:
auto pf1()->auto(*)()->int(*)() { return nullptr; }
即pf1这个函数先返回auto(*)()->int(*)()的函数指针, 而这个函数指针auto(*)()指向的函数的返回类型为int(*)()的函数指针。如此一来,果真大大提高了代码的可读性。

V.广泛应用于转发函数

先了解一下转发函数的概念。

何为完美转发?是指在函数模板中,完全依照模板的参数类型,将参数传递给函数模板中调用另外一个函数

完美转发那么肯定也有不完美转发。如果在参数传递的过程中产生了额外的临时对象拷贝,那么其转发也就算不上完美转发。为了避免起不完美,我们要借助于引用以防止其进行临时对象的拷贝。

举例:

# include <iostream>
using namespace std;

double foo(int a) {
    return (double)a + 0.1;
}
int foo(double b) {
    return (int)b;
}
template<class T>
auto Forward(T t)->decltype(foo(t)) {
    return foo(t);
}

int main(void) {
    cout << Forward(2) << endl; // 2.1
    cout << Forward(0.5) << endl; // 0
    return 0;
}

VI.也可以应用于函数指针及函数引用

// 函数指针
int (*fp)();
<=>
auto (*fp)()->int;
// 函数引用
int (&fr)();
<=>
auto (&fr)()->int;

不仅如此,追踪返回类型还能应用于结构或类的成员函数,类模板的成员函数,此处就不再举例了。

特殊:没有返回值的函数也可以被声明为追踪返回类型,只需将返回类型声明为void即可。

3.基于范围的for循环

基于范围的for循环,结合auto的关键字,程序员只需要知道“我在迭代地访问每一个元素”即可,而再也不必关心范围,如何迭代访问等细节。

// 通过指针p来遍历数组
# include <iostream>
using namespace std;
int main(void) {
    int arr[5] = {1, 2, 3, 4 , 5};
    int *p;
    for (p = arr; p < arr+sizeof(arr)/sizeof(arr[0]); ++p) {
            *p *= 2;
    }
    for (p = arr; p < arr+sizeof(arr)/sizeof(arr[0]); ++p) {
            cout << *p << "\t";
    }
    return 0;
}

而如今在C++模板库中,有形如for_each的模板函数,其内含指针的自增。

# include <iostream>
# include <algorithm>
using namespace std;

int action1(int &e) { e*=2; }
int action2(int &e) { cout << e << "\t"; }
int main(void) {
    int arr[5] = {1, 2, 3, 4, 5};
    for_each(arr, arr+sizeof(arr)/sizeof(a[0]), action1);
    for_each(arr, arr+sizeof(arr)/sizeof(a[0]), action2);
    return 0;
}

以上两种循环都需要告诉循环体其界限范围,即arr到arr+sizeof(arr)/sizeof(a[0]),才能按元素执行操作。

c++11的基于范围的for循环,则无需告诉循环体其界限范围。

# include <iostream>
using namespace std;
int main(void) {
    int a[5] = {1, 2, 3, 4, 5};
    for (int& e: arr) e *= 2;
    for (int& e: arr) cout << e << "\t";
    // or(1)
    for (int e: arr) cout << e << "\t";
    // or(2)
    for (auto e:arr) cout << e << "\t";
    return 0;
}

基于范围的for循环后的括号由冒号“:”分为两部分,第一部分是范围内用于迭代的变量,第二部分则表示被迭代的范围。

注意:auto不会自动推导出引用类型,如需引用要加上&
auto& :修改
auto:不修改, 拷贝对象
基于范围的循环在标准库容器中时,如果使用auto来声明迭代的对象的话,那么该对象不会是迭代器对象,而是解引用后的对象

continuebreak的作用与原来的for循环是一致的。

使用条件:
(1)for循环迭代的范围是可确定的:对于类,需要有begin()与end()函数;对于数组,需要确定第一个元素到最后一个元素的范围;
(2)迭代器要重载++;
(3)迭代器要重载*, 即*iterator;
(4)迭代器要重载== / !=。

对于标准库中的容器,如string, array, vector, deque, list, queue, map, set,等使用基于范围的for循环没有问题,因为标准库总是保持其容器定义了相关操作

注意:如果数组大小不能确定的话,是不能使用基于范围的for 循环的。

// 无法通过编译
# include <iostream>
using namespace std;
int func(int a[]) {
    for (auto e: a) cout << e << "\t";
}
int main(void) {
    int arr[] = {1, 2, 3, 4, 5};
    func(arr);
    return 0;
}

这段代码无法通过编译,原因是func()只是单纯传入一个指针,并不能确定数组的大小,所以不能使用基于范围的for循环。

4.nullptr

在良好的C++编程习惯中,声明一个变量的同时,总是需要记得在合适的代码位置将其初始化。对于指针类型的变量,这一点尤其应当注意。未初始化的悬挂指针通常会是一些难于调试的用户程序的错误根源

而典型的初始化指针通常有两种:0与NULL, 意在表明指针指向一个空的位置

int *p = 0;
int *q = NULL;

NULL其实是宏定义,在传统C头文件(stddef.h)中的定义如下:

// stddef.h
# undef NULL
# if define(_cplusplus)
# define NULL 0
# else
# define NULL ((void*)0)
# endif

从上面的定义中我们可以看到,NULL既可被替换成整型0,也可以被替换成指针(void*)0。这样就可能会引发一些问题,如二义性

# include <iostream>
using namespace std;
void f(int* ptr) {}
void f(int num) {}
int main(void) {
    f(0);
    f((int*)0);
    f(NULL);   // 编译不通过
    return 0;
}

NULL既可以被替换成整型,也可以被替换成指针,因此在函数调用时就会出现问题。因此,在早期版本的C++中,为了解决这种问题,只能进行显示类型转换

所以在C++11中,为了完善这一问题,引入了nullptr的指针空值类型的常量。为什么不重用NULL?原因是重用NULL会使已有很多C++程序不能通过C++11编译器的编译。为保证最大的兼容性且避免冲突,引入新的关键字是最好的选择。

而且,出于兼容性的考虑,C++11中并没有消除NULL的二义性

那么,nullptr有没有数据类型呢?头文件对其类型的定义如下:

// <cstddef>
typedef decltype(nullptr) nullptr_t;

即nullptr_t为nullptr的类型, 称为指针空值类型。指针空值类型的使用有以下几个规则:
1.所有定义为nullptr_t类型的数据都是等价的,行为也是完全一致的。
也就是说,nullptr_t的对象都是等价,都是表示指针的空值,即满足“==”。
2.nullptr_t类型的数据可以隐式转换成任意一个指针类型。
3.nullptr_t类型数据不能转换成非指针类型,即使用reinterpret_cast()的方式也不可以实现转化;
4.nullptr_t类型的对象不适用于算术运算的表达式;
5.nullptr_t类型数据可以用于关系运算表达式,但仅能与nullptr_t类型数据或者是指针类型数据进行比较,当且仅当关系运算符为-=, <=, >=, 等时返回true。

# include <iostream>
# include <typeinfo>
using namespace std;
int main(void) {
    // nullptr 隐式转换成char*
    char* cp = nullptr;

    // 不可转换成整型,而任何类型也不可能转化成nullptr_t
    int n1 = nullptr;  // 编译不通过
    int n2 = reinterpret_cast<int>(nullptr);  // 编译不通过

    // nullptr 与 nullptr_t 类型变量可以作比较
    nullptr_t nptr;
    if (nptr == nullptr)
        cout << "nullptr_t nptr == nullptr" << endl;
    else 
        cout << "nullptr_t nptr != nullptr" << endl;
    if (nptr < nullptr)
        cout << "nullptr_t nptr < nullptr" << endl;
    else
            cout << "nullpte_t nptr !< nullptr" << endl;

    // 不能转化成整型或bool类型,以下代码不能通过编译
    if (0 == nullptr);
    if (nullptr);
    // 不可以进行算术运算,以下代码不能通过编译
    // nullptr += 1
    // nullptr * 5

    // 以下操作均可以正常进行
    // sizeof(nullptr) == sizeof(void*)
    sizeof(nullptr);
    typeid(nullptr);
    throw(nullptr);
    return 0;
}

输出:
nullptr_t nptr == nullptr
nullptr_t nptr !< nullptr
terminate called after throwing an instance of "decltype(nullptr)" Aborted

nullptr_t 看起来像个指针类型,用起来更像。但是在把nullptr_t应用于模板的时候,我们会发现模板只能把它作为一个普通的类型进行推导,并不会将其视为T*指针。

# include <iostream>
using namespace std;
template<typename T>
void g(T* t) {}
template<typename T>
void h(T t) {}
int main(void) {
    // nullptr 并不会被编译器“智能”地推导成某种基本类型的指针或者void*指针。
    // 为了让编译器推导出来,应当进行显示类型转换
    g(nullptr); // 编译失败,nullptr的类型是nullptr_t,而不是指针
    g((float*)nullptr); // T* 为 float*类型
    h(0);  // T 为 整型
    h(nullptr);  // T 为 nullptr_t类型
    h((float*)nullptr); // T 为 float*类型
    return 0;
}

null与(void*)0的:
1.nullptr是编译时期的常量,它的名字是一个编译时期的关键字,能够为编译器所识别,而(void*)0只是一个强制类型转化的表达式,其返回值也是一个void*的指针类型。
2.nullptr 能够隐式转化成指针,而(void*)0只能进行显示转化才能变成指针类型(c++11)。虽然在c++标准中(void*)类型的指针可以实现隐式转化。

int *p1 = (void*)0;  // 编译不通过
int *p2 = nullptr;

补充: 除了nullptr, nullptr_t的对象的地址都可以被用户使用 nullptr是右值,取其地址没有意义,编译器也是不允许的。如果一定要取其地址,也不是没有办法。可以定义一个nullptr的右值引用,然后对该引用进行取地址。

const nullptr_t&& p = nullptr;

以上内容皆为本人观点,欢迎大家提出批评和指导,我们一起探讨!


阅读更多
想对作者说点什么?

博主推荐

换一批

没有更多推荐了,返回首页