C++(标准库):28---STL迭代器之(迭代器适配器(插入迭代器、流迭代器、反向迭代器、移动迭代器))

  • 除了每个容器有自己的迭代器之外,标准库在头文件iterator中还定义了额外几种迭代器

  • 这些迭代器包括:
    • 插入迭代器(insert iterator):这些迭代器被绑定到一个容器上,可用来向容器插入元素
    • 流迭代器(stream iterator):这些迭代器被绑定到输入或输出流上,可用来遍历所有关联的IO流
    • 反向迭代器(reverse iterator):这些迭代器向后而不是向前移动。除了forward_list容器之外的标准库容器都有反向迭代器
    • 移动迭代器(move iterator):这些专门的迭代器不是拷贝其中的元素,而是移动它们

一、插入迭代器

  • 插入迭代器是一种迭代器适配器,它接受一个容器,生成一个迭代器,能实现向给定容器添加元素。当我们通过一个插入迭代器进行赋值时,该迭代器调用容器操作来向给定容器的指定位置插入一个元素
  • 下图列出了这些迭代器支持的操作:

  • 根据元素插入的位置不同,插入迭代器可分为:
    • back_inserter创建一个使用push_back的迭代器
    • front_inserter创建一个使用push_back的迭代器
    • inserter创建一个使用insert的迭代器。此函数接受第二个参数,这个参数必须是一个指向给定容器的迭代器。元素将被插入到给定迭代器所表示的元素之前

  • 一个重要的注意事项:这三个迭代器返回一个位置,可以对这些位置进行操作,但是进行解引用(*)无效,++也不会改变位置

back_inserter

  • 该参数接受一个容器,返回一个与该容器绑定的插入迭代器,对此迭代器赋值,赋值运算符会调用push_back将元素添加到容器的尾部
  • 例如下面我们在vector容器的尾部插入一个元素:
std::vector<int> vec;
vec.push_back(1);
vec.push_back(2);

auto it = std::back_inserter(vec);
it = 3;

for (auto val : vec) {
    std::cout << val << " ";
}

  • 我们也可以在算法中运用此迭代器,下面是对fill_n()算法的使用:
    • 参数:参数1为迭代器起始位置。参数2为要改变的元素个数。参数3为要设定的值
    • fill_n()参数2的注意事项:参数2不能超出容器的大小,也不能对空容器操作
std::vector<int> vec;
vec.push_back(1);
vec.push_back(2);

//在vec的尾部添加5个8
//如果不使用back_inster是错误的,因为vector的容量不足5
std::fill_n(std::back_inserter(vec), 5, 8);

for (auto val : vec) {	
    std::cout << val << " ";
}

  • 例如我们下面与copy()算法结合,使其将一个容器的元素全部插入到另一个容器中。例如:
std::list<int> lst = { 1,2,3 };
std::list<int> lst2;

//将lst的元素颠倒过来插入lst2中
//因为front_inserter每次都调用push_back,因此每次插入都插入在头部,最后结果是1、2、3
copy(lst.begin(), lst.end(), std::back_inserter(lst2));

for (auto val : lst2) {
    std::cout << val << " ";
}

front_inserter

  • 该参数接受一个容器,返回一个与该容器绑定的插入迭代器,对此迭代器赋值,赋值运算符会调用push_front将元素添加到容器的头部
  • 例如我们下面与copy()算法结合,使其将一个容器的元素全部插入到另一个容器中。例如:
std::list<int> lst = { 1,2,3 };
std::list<int> lst2;

//将lst的元素颠倒过来插入lst2中
//因为front_inserter每次都调用push_front,因此每次插入都插入在头部,最后结果是3、2、1
copy(lst.begin(), lst.end(), std::front_inserter(lst2));

for (auto val : lst2) {
    std::cout << val << " ";
}

inserter

  • 参数1接受一个容器,参数2是指向该容器的一个迭代器。然后返回指向于参数2所指的迭代器之前的位置,对该位置进行操作操作,就是对参数2所指的迭代器之前的位置进行操作
  • 例如下面是对vector容器进行操作:
std::vector<int> vec;
vec.push_back(1);
vec.push_back(2);
	
//iter迭代器此时所指之处为2
auto iter = vec.begin() + 1;

//我们获取iter迭代器所指的之前的位置
auto new_iter = std::inserter(vec, iter);
//对返回的位置之处进行赋值,那么这个值就插入进vec中
new_iter = 6;

for (auto val : vec) {
    std::cout << val << " ";
}

  • 例如我们下面与copy()算法结合,使其将一个容器的元素全部插入到另一个容器中。例如:
    • 为什么lst2中的元素顺序也是顺序的,因为:
      • 第一次插入begin()为空,那么1被插入在空之前,此时lst2中只有1
      • 第二次插入时,此时的lst2.begin()不是指向1,仍然指向于第一次插入时的空,因此还是在1和空之间插入,此时容器中为1、2
      • 第三次插入时,此时的st2.begin()也不是指向1,仍然指向于第一次插入时的空,因此此时是在1、2和空之间进行插入,此时为1、2、3
std::list<int> lst = { 1,2,3 };
std::list<int> lst2;

//将lst的元素全部拷贝进lst2中
copy(lst.begin(), lst.end(), std::inserter(lst2, lst2.begin()));

for (auto val : lst2) {
    std::cout << val << " ";
}

二、流迭代器

  • 虽然iostream类型不是容器,但标准库定义了可以用于这些IO类型对象的迭代器:
    • istream_iterator:读取输入流
    • ostream_iterator:读取输出流
  • 这些迭代器将它们对应的流当做一个特定类型的元素序列来处理。通过使用流迭代器,我们可以用泛型算法从流对象读取以及向其写入数据

istream_iterator

  • 创建与初始化:
    • 创建一个输入流迭代器时,必须指定迭代器将要读取的数据的类型
    • 因为istream_iterator使用>>读取流,因此创建istream_iterator对象时必须使用输入流对象初始化它。当然,我们也可以默认初始化迭代器,这样就创建了一个可以当做尾后值使用的迭代器
  • 例如,下面是一些输入流迭代器的创建:
//读取的数据类型为int,使用cin输入流对象初始化它,代表从标准输入读取数据
std::istream_iterator<int> int_it(cin);

//读取的数据类型为int,默认初始化(尾后迭代器)
std::istream_iterator<int> int_eof;

std::ifstream in("afile.txt");
//读取的数据类型为string,使用文件输入流初始化它,代表从文件中读取字符串
std::istream_iterator<std::string> str_it(in);
  • 例如,下面从标准输入读取数据,存到一个vector中:
    • 我们循环从cin中读取数据,然后保存到vec中
    • 每次循环时检查in_iter是否等于eof。eof被定义为空的istream_iterator,从而可以当做尾后迭代器来使用(一旦其关联的流遇到文件尾或遇到IO错误,迭代器的值就与尾后迭代器相等)
    • in_iter也相当于一个容器,其中保存了我们所输入的所有数据,因此在push_back中我们解引用得到in_iter中我们输入的值,并且使用++使迭代器前后推进,读取下一个值
std::vector<int> vec;

//阻塞在此处,从cin中读取数据
std::istream_iterator<int> in_iter(std::cin);
std::istream_iterator<int> int_eof;

//遍历in_iter中的每个读取的元素(读取不是在此处,在上面)
while (in_iter != int_eof) {
    vec.push_back(*in_iter++);
}

std::cout << "result: ";
for (auto val : vec) {
    std::cout << val << " ";
}

  • 因为流迭代器类似于一个容器,其中保存了我们所输入的数据,因此我们也可以使用下面的方式对一个vec进行构造。例如:
//阻塞在此处,从cin中读取数据
std::istream_iterator<int> in_iter(std::cin);
std::istream_iterator<int> eof;

//从迭代器范围构造vec
std::vector<int> vec(in_iter, eof);

for (auto val : vec) {
    std::cout << val << " ";
}

  • 我们也可以在算法中使用istream_iterator迭代器,例如:
    • accumulate()算法的参数1和参数2表示一个迭代器范围,参数3为初始值,此算法将迭代器所指的范围内的元素与初始值加在一起返回总和
//阻塞在此处,从cin中读取数据
std::istream_iterator<int> in_iter(std::cin);
std::istream_iterator<int> eof;

将in_iter, eof内的元素做总和,以0为初始值
std::cout << std::accumulate(in_iter, eof, 0) << std::endl;

ostream_iterator

  • 创建与初始化:
    • 创建一个输出流迭代器时,必须指定迭代器将要输出的数据的类型
    • 因为ostream_iterator使用<<输出数据,因此创建ostream_iterator对象时其参数1必须指定一个输出流对象,另外其还有一个参数2,这个参数是一个字符串类型(必须是C风格的字符串),在输出每个元素后都会打印此字符串
    • 另外,不允许有类似于istream_iterator的尾后迭代器,因此不允许有默认初始化
  • 例如,下面使用输出流迭代器,输出中的vector中全部元素,并且在没输出一个元素之后加上一个空格:
std::vector<int> vec{ 1,2,3 };

//输出流迭代器操作的元素类型为int,使用cout作为初始化
std::ostream_iterator<int> out_iter(std::cout, " ");

//for循环每执行一次,从vec中取出一个元素赋值给out_iter,然后out_iter进行输出
for (auto val : vec) {
    *out_iter++ = val;
}
std::cout << std::endl;

  • 对于ostream_iterator来说,*、++、--这些操作符都是无效的,因此上面的for循环还可以改为:
    • 虽然两者都是相同的,但是我们希望使用上面的方式,因此这样具有可读性,并且与其他迭代器的使用格式保持一致
//与上面的是等效的
for (auto val : vec) {
    out_iter = val;
}
  • 我们也可以在算法中使用,例如我们使用copy()算法将vector的元素拷贝进输出流迭代器对象中进行输出。例如:
std::vector<int> vec{ 1,2,3 };

std::ostream_iterator<int> out_iter(std::cout, " ");

std::copy(vec.begin(), vec.end(), out_iter);
std::cout << std::endl;

使用流迭代器处理类类型

  • 如果我们自己定义的类型支持输入运算符(>>)或输出运算符(<<),那么我们就可以对该类型创建istream_iterator或ostream_iterator对象
  • 例如,下面我们的Sales_item对象定义了operator >>和operator<<运算符
struct Sales_data {
    std::string isbn()const { return bookNo; }
    double avg_price()const {
        if (units_sold)
            return revenue / units_sold;
        else
            return 0;
    }

    Sales_data& operator+=(const Sales_data& rhs)
    {
        units_sold += rhs.units_sold;
        revenue += rhs.revenue;
        return *this;
    }

    std::string bookNo;      //图书编号
    unsigned units_sold = 0; //销量
    double revenue = 0.0;    //收入
};

std::ostream& operator<<(std::ostream& os,const Sales_data& item)
{
    //输出图书编号、销量、收入、平均价格
    os << item.isbn() << " " << item.units_sold << " "
        << item.revenue << " " << item.avg_price();
    return os;
}

std::istream& operator>>(std::istream& is, Sales_data& item)
{
    double price;
    //输入图书编号、销量、价格
    is >> item.bookNo >> item.units_sold >> price;

    if (is) //定义收入
        item.revenue = item.units_sold*price;
    else    //输入失败,对象赋予默认状态
        item =Sales_data();

    return is;
}

int main()
{
    std::istream_iterator<Sales_data> item_iter(cin), eof;
    std::ostream_iterator<Sales_data> out_iter(cout, "\n");

    //保存第一笔读取的数据
    Sales_data sum = *item_iter++;

    //从item_iter中读取数据,每次循环返回的都是一个Sales_data对象
    while (item_iter != eof) {
        //如果当前Sales_data对象与sum的图书编号一致,说明是同一本书
        //那么就将两个Sales_data对象相加
        if (item_iter->isbn() == sum.isbn())
            sum += *item_iter++;
        //如果不是同一本树,那么就输出sum
        //然后将下一个对象赋值给sum
        else {
            out_iter = sum;     //输出
            sum = *item_iter++;
        }
    }
    return 0;
}
  • 下面前两条是输入结果,当我们输入第三条之后,第四条是输出结果(因为遇到图书编号不一致的了)。同理,最后一条也是输出结果

三、反向迭代器

  • 反向迭代器就是在容器中从尾元素向首元素反向移动的迭代器。对于反向迭代器来说,其递增就是向前移动一个元素,其减低就会向后移动一个元素
  • 除了forward_list容器之外,其他容器都支持反向迭代器,我们可以调用容器的rbegin()、rend()、crbegin()、crend()成员函数来获得反向迭代器
  • 下图显示了vector的所有迭代器(正向的和反向的):

演示案例

  • 例如,下面我们使用反向迭代器,逆序打印vector中的元素:
std::vector<int> vec{ 1,2,3 };

//r_iter的类型为const_reverse_iterator
for (auto r_iter = vec.crbegin(); r_iter != vec.crend(); r_iter++)
{
    std::cout << *r_iter << " ";
}

  • 例如,我们在算法中使用反向迭代器,例如下面可以对容器进行逆序排序:
std::vector<int> vec{ 1,2,3 };

//默认排序方式,从小到大排序
std::sort(vec.begin(), vec.end());
for (auto val : vec){
    std::cout << val << " ";
}
std::cout << std::endl;

//逆序方式,从大到小排序
std::sort(vec.rbegin(), vec.rend());
for (auto val : vec){
    std::cout << val << " ";
}

反向迭代器需要递减运算符

  • 反向迭代器即需要递增运算符,也需要递减运算符。因为其可以在容器中进行双向移动
  • 因此,由于forward_list只支持单向顺序访问,因此不能使用反向迭代器
  • 同理,流迭代器不支持递减运算符,因此也不能对流迭代器使用反向迭代器

反向迭代器和其他迭代器间的关系

  • 例如,现在我们有一个string对象中保存着使用逗号分隔的单词列表
  • 我们可以用正向迭代器查找出该string对象中的第一个单词,并且打印,代码如下:
std::string line = "FIRST,MIDDLE,LAST";

auto comma = std::find(line.cbegin(), line.cend(), ',');
std::cout << std::string(line.cbegin(), comma) << std::endl;

  • 我们还可以使用反向迭代器查找出该string对象中的最后一个单词,并且打印,代码如下:
    • 打印的结果是最后一个单词“LAST”被逆序打印为“TSAL”
    • 原因是,使用反向迭代器处理string,但是我们打印
std::string line = "FIRST,MIDDLE,LAST";

auto rcomma = std::find(line.crbegin(), line.crend(), ',');
std::cout << std::string(line.crbegin(), rcomma) << std::endl;

  • 我们可以调用const_reverse_iterator反向迭代器对象的base()成员,该成员会将反向迭代器变为正向迭代器。代码如下:
std::string line = "FIRST,MIDDLE,LAST";

auto rcomma = std::find(line.crbegin(), line.crend(), ',');
std::cout << std::string(rcomma.base(), line.cend()) << std::endl;

  • 从技术上将,普通迭代器和反向迭代器的关系反映了左闭合区间的特性。关键点在于[line.crbegin(),rcomma)和[rcomma.base(),line.cend())指向line中相同的元素范围。为了实现这一点,rcomma和rcomma.base()必须生成相邻位置而不是相同位置,crbegin()和cend()也是如此

四、移动迭代器

  • 这些专门的迭代器不是拷贝其中的元素,而是移动它们
  • 我们可以通过标准库的make_move_iterator()函数将一个普通迭代器转换为一个移动迭代器
  • 此函数接受一个迭代器参数,返回一个迭代器。原迭代器的所有其他操作在移动迭代器中都照常工作
  • 关于对象移动和右值引用,可以参阅:https://blog.csdn.net/qq_41453285/article/details/104419356

演示案例

  • 下面是一个动态内存管理类:
//动态内存管理类
class StrVec {
public:
    size_t size()const { return first_free - elements; }
    std::string *begin()const { return elements; }
    std::string *end()const { return first_free; }
private:
    void reallocate(); //获得更多内存并拷贝已有元素
    void free();       //销毁元素并释放内存
private:
    static std::allocator<std::string> alloc; //分配元素
    std::string *elements;   //指向数组首元素的指针
    std::string *first_free; //指向数组第一个空闲元素的指针
    std::string *cap;        //指向数组尾后位置的指针
};

  • 其中有一个reallocate()成员函数,其:
    • 该函数的功能是分配更多的内存,并将原来的元素都移动到新内存中
    • 由于移动迭代器支持正常的迭代器操作,我们可以将一对移动迭代器传递给算法
    • 其中我们使用一个名为uninitialized_copy()的算法:
      • 该算法对参数1和参数2所指的输入序列中的每个元素调用construct拷贝到从first所开始的相对应的位置上
      • 此算法使用迭代器的解引用运算符从输入序列中提取元素,但是此时我们使用make_move_iterator将其迭代器类型改为移动迭代器,因此解引用运算符生成的是一个右值引用,这意味着construct将使用移动构造函数来移动元素,从而避免了元素的拷贝
void StrVec::reallocate()
{
    //分配大小两倍于当前规模的内存空间
    auto newCapacity = size() ? 2 * size() : 1;
    auto first = alloc.allocate(newCapacity);

    //移动元素
    auto last = uninitialized_copy(make_move_iterator(begin()),
        make_move_iterator(end()), first);

    //释放旧内存,更新其他内容
    free();
    elements = first;  
    first_free = last;
    cap = elements + newCapacity;
}
  • 值得注意的是,标准库不保证哪些算法适用移动迭代器,哪些不适用。由于移动一个对象可能销毁掉原对象,因此你只有在确信算法在为一个元素赋值或将其传递给一个用户定义的函数后不再访问它时,才能将移动迭代器传递给算法

发布了1594 篇原创文章 · 获赞 1190 · 访问量 57万+

猜你喜欢

转载自blog.csdn.net/qq_41453285/article/details/105486208
今日推荐