Skip to content

Latest commit

 

History

History
329 lines (280 loc) · 13 KB

l_19.md

File metadata and controls

329 lines (280 loc) · 13 KB

Lesson19 STL数组 & 基于范围的for循环

1. 动态数组 std::vector

概述

std::vector 被定义为一个带有2个类型参数的类模板:一个参数为存放数据类型,另一个参数为分配器类型

template<class T, class Allocator = allocator<T>> class vector;

vector提供了重载的 operator[] 以便访问和修改其中的元素,通过 operator[] 访问vector边界之外的元素时,该行为是未定义的。

除了使用 operator[] 运算符外,还可以通过 at(),front(),back() 访问vector中的元素。at() 方法等同于 operator[] 运算符,区别是 at() 会执行边界检查,如果索引超出边界,会报出out_of_range异常。 front()back() 分别返回vector第一个元素和最后一个元素的引用。在空容器中调用它们会发生未定义行为。

动态长度

vector真正强大之处在于动态增长的能力,它可以随插入数据的增多而适时扩大容量。并且其数据全部创建在自由存储区。

构造函数和析构函数

默认的构造函数创建一个不含元素的vector

std::vector<int> intVector;

可以指定元素个数,并指定这些元素的值

std::vector<int> intVector1(10);
// creates vector of 10 elements
std::vector<int> intVector3(10, 100);
// creates vector of 10 int with value 100

如果没有提供默认值,那么对新对象进行0初始化,指针类型将初始化为 nullptr

可以使用包含初始元素的初始化列表构建vector

std::vector<int> intVector({1, 2, 3, 4, 5, 6});

统一初始化可用于包括vector在内的大部分标准库容器

std::vector<int> intVector1 = {1, 2, 3, 4, 5, 6};
std::vector<int> intVector2{1, 2, 3, 4, 5, 6};

复制与赋值

vector内部已经实现深拷贝。因此,出于效率方面的考虑,应该通过引用而不是值传递向函数传递vector

除了普通的复制与赋值外,vector还提供 assign 方法,用于删除所有现有元素,并添加指定的新元素。这个方法特别适合vector的重用。
下面是一个简单的例子。intVector包含10个默认值为0的元素,然后通过 assign 方法删除所有10个元素,并填充5个值为100的元素代之。

std::vector<int> intVector(10);
...
intVector.assign(5, 100);

assign 还可接收初始化列表

intVector.assign({1, 2, 3, 4});

vector还提供 swap 方法,这个方法可以交换两个vector的内容。

std::vector<int> vectorOne(10);
std::vector<int> vectorTwo(5, 100);
vectorOne.swap(vectorTwo);

比较

标准库提供了6个重载比较运算符:==,!=,<,>,<=,>=。如果两个vector的元素数量相等,而且对应元素都相等,那么两个vector相等。两个vector的比较采用字典顺序,比较其中同位第一个不同的元素的大小,其结果就是两个vector的大小关系。如果出现两者的size不同,则size取较小者的大小,比较前size个元素的大小。

std::vector<int> vectorOne(10);
std::vector<int> vectorTwo(10);

if(vectorOne == vectorTwo) {
  std::cout << "equal" << std::endl;
} else {
  std::cout << "not equal" << std::endl;
}

vectorOne[3] = 50;

if(vectorOne < vectorTwo) {
  std::cout << "vectorOne is less than vectorTwo" << std::endl;
} else {
  std::cout << "vectorOne is not less than vectorTwo" << std::endl;
}****

output

equal
vectorOne is not less than vectorTwo

迭代器

上节中我们使用了for循环遍历vector中每个元素

std::vector<double> doubleVector;
...
for(std::vector<double>::iterator iter{begin(doubleVector)}; 
iter != end(doubleVector); ++iter) {
  std::cout << *iter << std::endl;
}

首先,看一下for循环的初始化语句

std::vector<double>::iterator iter{begin(doubleVector)};

前面提到,每个容器都定义了一种名为iterator的类型,以表示那个容器类型的迭代器。 begin() 返回引用容器中第一个元素的相应类型的迭代器。因此,这条初始化语句在iter变量中获得了引用doubleVector中第一个元素的迭代器。下面看一下for循环的比较语句。

iter != end(doubleVector);

这条语句检查迭代器是否超越了vector中元素序列的尾部。当到达这一点时,循环终止。递增语句 ++iter 递增迭代器,以便引用vector中的下一个元素。

for循环体包含一个输出语句

std::cout << *iter << std::endl;

通过 * 解引用 iter 从而获得iter所引用的元素,然后通过cout输出。

上述迭代器的for循环可通过 auto 关键字进行简化

for(auto iter{begin(doubleVector)};
    iter != end(doubleVector); ++iter) {
      std::cout << *iter << std::endl;
    }

有了 auto 编译器就会根据初始化语句右侧的内容自动推导变量iter的类型。在本例中,初始化语句右侧的内容是调用 begin() 得到的结果。

vector支持以下功能

  • (c)begin()(c)end() 返回指向第一个元素和最后一个元素的后一个元素的(const)迭代器。
  • (c)rbegin()(c)rend() 返回指向第一个元素的前一个元素和后一个元素的反向迭代器。

访问对象成员

如果容器中的元素是对象,那么可对迭代器调用 -> 运算符,调用对象的方法或访问对象成员。

const_iterator

普通的迭代器支持读和写。然而如果对const对象调用begin()和end(),或调用cbegin()和cend(),都将得到const_iterator。const_iterator是只读的,不能通过const_iterator修改它引用的元素。iterator始终可以转换为const_iterator,因此下面这种写法是安全的:

std::vector<type>::const_iterator it{begin(myVector)};

然而,const_iterator不能转换为iterator。如果myVector是const修饰的,那么下面这行代码无法通过编译:

std::vector<type>::iterator it{begin(myVector)};

增添和删除数据

可以通过 push_back() 方法可向vector追加元素。vector还提供了删除元素的对应方法 pop_back()

通过 insert() 方法可以在vector中任意位置插入元素,这个方法在迭代器指定位置添加1个或多个元素,并将所有元素向后移动,给新元素腾出空间。insert() 有5中不同的重载形式:

  • 插入单个元素
  • 插入单个元素的n个副本
  • 从某个迭代器范围插入元素
  • 使用移动语义将元素转移到vector中
  • 向vector中插入一列元素,这列元素是通过初始化列表指定的

通过 erase() 可在vector中的任意位置删除元素,通过 clear() 可删除所有元素。
erase() 有两种形式:一种接收单个迭代器,删除单个元素,另一种接受两个迭代器,删除迭代器指定的元素范围。

这个示例还演示了erase()的双参数版本和insert()的以下版本:

  • insert(const_iterator pos, const T& x);     // 将值x插入位置pos
  • insert(const_iterator pos, size_type n, const T& x);     // 将值x在位置pos插入n次
  • insert(const_iterator pos, InputIterator first, InputIterator last);     // 将范围[first,last)内的元素插入到位置pos
template <typename T>
void printVector(const std::vector<T>& v) {
   for (auto& element : v) {        // 这里使用基于范围的for循环
      std::cout << element << " ";
   }
   std::cout << std::endl;
}

int main() {
   std::vector<int> vectorOne{1, 2, 3, 5};
   std::vector<int> vectorTwo;

   // Oops, we forgot to add 4. Insert it in the correct place
   vectorOne.insert(cbegin(vectorOne) + 3, 4);

   // Add element 6 through 10 to vectorTwo
   for (int i{6}; i <= 10; i++) {
      vectorTwo.push_back((i));
   }
   printVector(vectorOne);
   printVector(vectorTwo);

   // Add all the elements from vectorTwo to the end of vectorOne
   vectorOne.insert(cend(vectorOne), cbegin(vectorTwo), cend(vectorOne) + 5);
   printVector(vectorOne);

   // Now erase the number 2 through 5 in vectorOne
   vectorOne.erase(cbegin(vectorOne) + 1, cbegin(vectorOne) + 5);
   printVector(vectorOne);

   // Clear vectorTwo entirely
   vectorTwo.clear();

   // Add 10 copies of the value 100
   vectorTwo.insert(cbegin(vectorTwo), 10, 100);

   // Decide we only want 9 elements
   vectorTwo.pop_back();
   printVector(vectorTwo);
}

大小和容量

vector提供两个可获得大小信息的方法:size()capacity()size() 方法返回vector中元素的个数,而 capacity() 返回的是vector在重新分配前可保存的元素个数。因此,在重新分配前还能插入的元素个数为 capacity()-size()

通过 empty() 方法可以查询vector是否为空。vector可以为空,但是容量不能为0。

还有非成员 std::size()std::empty() 全局函数,他们可用于所有容器,也可以用于静态分配的C风格数组以及初始化列表。

std::vector<int> vec{1, 2, 3};
std::cout << std::size(vec) << std::endl;
std::cout << std::empty(vec) << std::endl;

预留容量

如果希望程序尽可能高效,或要确保迭代器不会失效,就要强迫vector预先分配足够的空间,已保存所有元素。

一种预分配空间的方式时调用 reserve() 。这个方法负责保存指定数目的足够空间。

注意
为元素预留空间改变的是容量而不是大小,也就是说,这个过程不会创建真正的元素。不要越界访问。

另一种预分配空间的方法是在构造函数中,或者通过 resize()assign() 方法指定vector中要保存的元素数目。

2. 静态数组 std::array

std::array的大小是固定的,不能增加或收缩。该容器是为了让数据能分配到栈上。std::array 的行为实际上是和C风格数组相同的。

std::array 的声明需要有两个模板参数,第一个是存放数据类型,第二个是元素数量(数组长度)。

和vector一样,array支持随机访问迭代器,元素都保存在连续内存中。array支持 font()back()at()operator[],还支持使用 fill() 将特定元素填满array。由于array大小固定,所以不支持vector的大小与容量的方法。

example

std::array<int, 3> arr{9, 8, 7};

// size()
std::cout << "array size = " << arr.size() << std::endl;

// 迭代器遍历
for (std::array<int, 3>::iterator it = arr.begin(); it != arr.end(); ++it) {
   std::cout << *it << std::endl;
}

// fill()
arr.fill(3);

// const迭代器遍历
for (std::array<int, 3>::const_iterator it = arr.cbegin(); it != arr.cend(); ++it) {
   std::cout << *it << std::endl;
}

output

array size = 3
9
8
7
3
3
3

可使用 std::get<n>() 函数模板,从array中检索位于索引位置n的元素。索引必须是常量表达式。使用 std::get<n>() 的优势在于编译器在编译期会检查给定索引是有效的,否则将导致编译错误。

std::array<int, 3> myArray{11, 22, 33};
std::cout << std::get<1>(myArray) <<std::endl;
std::cout << std::get<10>(myArray) <<std::endl;     // error

3. 基于范围的 for 循环

你是否觉得,过去写的for循环太太太太不优雅了呢?

int array[10] = {1,2,3,4,5,6,7,8,9,10};
for(int i = 0; i < 10; i++) {
  std::cout << array[i] << std::endl;
}

那么隆重介绍,基于范围的for循环

for(int ele : array) {
  std::cout << ele << std::endl;
}

听说你还想对数组的值进行修改?那就取个引用吧

for(int& ele : array) {
  ele += 2;
  std::cout << ele << std::endl;
}

用这样的语法形式来写就自动的能够从数组中依次取出各个元素,用来初始化这个 ele

这么好用的东西,怎么只能用来给C风格数组用?

std::vector<int> vec{1, 2, 3, 4, 5};
for(auto i : vec) {
  std::cout << i << std::endl;
}

在上面的基于范围的 for 循环中, n 表示 arr 中的一个元素, auto 则是让编译器自动推导出 n 的类型。在这里, n 的类型将被自动推导为 vector 中的元素类型 int。在 n 的定义之后,紧跟一个冒号( :),之后直接写上需要遍历的表达式, for 循环将自动以表达式返回的容器为范围进行迭代。

提高访问效率且不需要修改元素可使用const引用

std::vector<int> vec{1, 2, 3, 4, 5};
for(const auto& i : vec) {
  std::cout << i << std::endl;
}

太优雅了


edit Serein