当前位置: 首页>后端>正文

C++中的auto和for循环

参考资料:
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的使用细则

  1. 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;
}

  1. 在同一行定义多个变量

当在同一行声明多个变量时,这些变量必须是相同的类型,否则编译器将会报错,因为编译器实际只对第一个类型进行推导,然后用推导出来的类型定义其他变量。

void TestAuto()
{
     auto a = 1, b = 2;
     auto c = 3, d = 4.0; // 该行代码会编译失败,因为c和d的初始化表达式类型不同 
}

auto不能推导的场景

  1. auto不能作为函数的参数
// 此处代码编译失败,auto不能作为形参类型,因为编译器无法对a的实际类型进行推导 
void TestAuto(auto a)
{}
  1. auto不能直接用来声明数组
void TestAuto()
{
    int a[] = {1,2,3};
    //auto b[] = { 4,5,6 };//auto不能直接用来声明数组
}
  1. 为了避免与C++98中的auto发生混淆,C++11只保留了auto作为类型指示符的用法
  2. auto在实际中最常见的优势用法就是C++11提供的新式for循环,还有lambda表达式等进行配合使用。
  3. auto不能定义类的非静态成员变量
  4. 实例化模板时不能使用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的使用条件

  1. for循环迭代的范围必须是确定的

对于数组而言,就是数组中第一个元素和最后一个元素的范围;
注意:以下代码就有问题,因为for的范围不确定

void TestFor(int array[])
{
    for(auto& e : array)
        cout<< e <<endl;
}
  1. 迭代的对象要实现++和==的操作。

虽然基于范围的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、自定义迭代器* 解引用操作,显然解引用操作必须容器对应元素的引用,否则引用遍历时将会出错


https://www.xamrdz.com/backend/39x1942642.html

相关文章: