参考资料:
C++11新特性之基本范围的For循环(range-based-for)
C++:auto关键字(C++11)、基于范围的for循环(C++11)
在早期C/C++中auto的含义是:使用auto修饰的变量,是具有自动存储器的局部变量,但遗憾的是一直没有人去使用它。
- 这是因为在C语言中使用auto关键字声明一个变量为自动变量,是C语言中应用最广泛的一种类型,在函数内定义变量时,如果没有被声明为其他类型的变量都是自动变量,也就是说,省去类型说明符auto的都是自动变量。这里的其他类型指的是变量的存储类型即:静态类型变量(static )、寄存器类型变量(register)和外部类型变量(extern)。
C++11中,auto被赋予了全新的含义即:auto不再是一个存储类型指示符,而是作为一个新的类型指示符来指示编译器,auto声明的变量必须由编译器在编译时期推导而得。
int TestAuto()
{
return 10;
}
int main() {
int a = 10;
auto b = a;
auto c = 'a';
auto d = TestAuto();
cout << typeid(b).name() << endl;//int
cout << typeid(c).name() << endl;//char
cout << typeid(d).name() << endl;//int
//auto e; 无法通过编译,使用auto定义变量时必须对其进行初始化
return 0;
}
注意:
使用auto定义变量时必须对其进行初始化,在编译阶段编译器需要根据初始化表达式来推导auto的实际类型。因此auto并非是一种“类型”的声明,而是一个类型声明时的“占位符”,编译器在编译期会将auto替换为变量实际的类型。
auto的使用细则
- auto与指针和引用结合起来使用
用auto声明指针类型时,用auto和auto*没有任何区别,但用auto声明引用类型时则必须加&
int main() {
int x = 10;
auto a = &x;
auto* b = &x;
auto& c = x;
cout << typeid(a).name() << endl;//int *
cout << typeid(b).name() << endl;//int *
cout << typeid(c).name() << endl;//int
*a = 20;//x == *a == *b == c == 20
*b = 30;//x == *a == *b == c == 30
c = 40; //x == *a == *b == c == 40
return 0;
}
- 在同一行定义多个变量
当在同一行声明多个变量时,这些变量必须是相同的类型,否则编译器将会报错,因为编译器实际只对第一个类型进行推导,然后用推导出来的类型定义其他变量。
void TestAuto()
{
auto a = 1, b = 2;
auto c = 3, d = 4.0; // 该行代码会编译失败,因为c和d的初始化表达式类型不同
}
auto不能推导的场景
- auto不能作为函数的参数
// 此处代码编译失败,auto不能作为形参类型,因为编译器无法对a的实际类型进行推导
void TestAuto(auto a)
{}
- auto不能直接用来声明数组
void TestAuto()
{
int a[] = {1,2,3};
//auto b[] = { 4,5,6 };//auto不能直接用来声明数组
}
- 为了避免与C++98中的auto发生混淆,C++11只保留了auto作为类型指示符的用法
- auto在实际中最常见的优势用法就是C++11提供的新式for循环,还有lambda表达式等进行配合使用。
- auto不能定义类的非静态成员变量
- 实例化模板时不能使用auto作为模板参数
范围for的语法
在C++98中如果要遍历一个数组,可以按照以下方式进行:
#include <iostream>
using namespace std;
void TestFor()
{
int array[] = { 1, 2, 3, 4, 5 };
for (int i = 0; i < sizeof(array) / sizeof(array[0]); ++i)
array[i] *= 2;
for (int* p = array; p < array + sizeof(array) / sizeof(array[0]); ++p)
cout << *p << " "; //2 4 6 8 10
}
int main(){
TestFor();
return 0;
}
对于一个有范围的集合而言,由程序员来说明循环的范围是多余的,有时候还会容易犯错误。因此C++11中引入了基于范围的for循环。for循环后的括号由冒号“ :”分为两部分:第一部分是范围内用于迭代的变量,第二部分则表示被迭代的范围。
void TestFor()
{
int array[] = { 1, 2, 3, 4, 5 };
for (auto& e : array)
e *= 2;
for (auto& e : array)
cout << e << " ";//2 4 6 8 10
return 0;
}
与普通循环类似,可以用continue来结束本次循环,也可以用break来跳出整个循环。
但如果容器里存的是类类型,就可能带来巨大的拷贝开销,因为每次做循环都需要创建容器元素的局部副本,这种情况下,应该用auto & for (auto& elem : container)
如果是只读的,还应该给 auto 加上 const 限定符for (const auto& elem : container)
基于 range 的 for 循环允许我们用更简单易读的形式遍历容器中的所有元素
vector<int> v{1, 2, 3};
for (std::vector<int>::iterator itr = vec.begin(); itr != vec.end(); itr++)
std::cout << *itr;
for (int i : v)
cout << i << endl;
for (auto& i : v)
cout << i << endl;
范围for的使用条件
- for循环迭代的范围必须是确定的
对于数组而言,就是数组中第一个元素和最后一个元素的范围;
注意:以下代码就有问题,因为for的范围不确定
void TestFor(int array[])
{
for(auto& e : array)
cout<< e <<endl;
}
- 迭代的对象要实现++和==的操作。
虽然基于范围的For循环使用起来非常的方便,我们不用再去关注for的开始条件和结束条件等问题了,但是还是有一些细节问题在使用的时候需要注意,来看下基于范围的for对于容器map的遍历:
std::map<string, int> map = { { "a", 1 }, { "b", 2 }, { "c", 3 } };
for (auto &val : map)
cout << val.first << "->" << val.second << endl;
为什么是使用val.first val.second而不是直接输出value呢?
在遍历容器的时候,auto自动推导的类型是容器的value_type类型,而不是迭代器,而map中的value_type是std::pair,也就是说val的类型是std::pair类型的,因此需要使用val.first,val.second来访问数据。
此外,使用基于范围的for循环还要注意一些容器类本身的约束,比如set的容器内的元素本身有容器的特性就决定了其元素是只读的,哪怕的使用了引用类型来遍历set元素,也是不能修改器元素的,看下面例子:
set<int> ss = { 1, 2, 3, 4, 5, 6 };
for (auto& n : ss)
cout << n++ << endl;
上述代码定义了一个set,使用引用类型遍历set中的元素,然后对元素的值进行修改,该段代码编译失败
自定义的类实现基于范围的for
上面说了这么多的基于范围的For的用法和使用细节,但是这些用法都用来遍历C++提供的一些数组,容器类,是否可以遍历自定义的类呢?
答案是肯定的,下面来通过这个Range类的实现看下如果为自定义的类实现基于范围的For。
由于基本范围的For不需要明确指定遍历的开始和结束范围,但是在内部实现上依赖于自定义类提供的begin和end方法,此外还需要一个自定义的迭代器对象来负责范围的取值。看下面的例子:
class Myiterator
{
public:
Myiterator(int val) :_value(val){}
bool operator!=(const Myiterator& other) const
{
return this->_value != other._value;
}
const int & operator*()
{
return _value;
}
int& operator++()
{
return ++_value;
}
private:
int _value;
};
class Range
{
public:
Range(int begin, int end) :__begin(begin), __end(end){}
Myiterator& begin()
{
return Myiterator(__begin);
}
Myiterator& end()
{
return Myiterator(__end);
}
private:
int __begin;
int __end;
};
int main()
{
for (auto i : Range(1, 10))
cout << i << " ";
}
输出为:1 2 3 4 5 6 7 8 9 请按任意键继续. . .,
可见,对于自定义的Range类我们可以用估计与范围的For循环来遍历,如果要实现Range(1,10,2)也就是带步长的Range的话只需要将其迭代器的++操作符改写,_value+=2即可。
因此归纳总结,为了给自定义的类实现基于范围的For的步骤如下:
1、定义自定义类的迭代器 //这里的迭代器是广义的迭代器,指针也属于该范畴。
2、该类型拥有begin() 和 end() 成员方法,返回值为迭代器(或者重载全局的begin() 和 end() 函数也可以) 。
3、自定义迭代器的 != 比较操作 。
4、自定义迭代器的++ 前置自增操作,显然该操作要是迭代器对象指向该容器的下一个元素 。
5、自定义迭代器* 解引用操作,显然解引用操作必须容器对应元素的引用,否则引用遍历时将会出错