跳转到内容

C++ 编程/STL

来自维基教科书,开放的书籍,面向开放的世界

标准模板库 (STL)

[编辑 | 编辑源代码]

标准模板库 (STL) 是 C++ 标准库 的一部分,它提供了一组算法、容器、迭代器和其他基本组件,这些组件以模板、类和函数的形式实现,对于扩展 C++ 的功能和标准化至关重要。 STL 的主要目标是通过强调性能和正确性来提供改进的实现标准化。

无需担心您的数组是否需要容纳 257 条记录,或因字符串缓冲区溢出而噩梦连连,您可以享受 vectorstring,它们可以自动扩展以容纳更多记录或字符。例如,vector 就像一个数组,区别在于 vector 的大小可以扩展以容纳更多单元格,或者在需要时缩减。必须记住,STL 不会与 OOP 冲突,但本身不是面向对象的;特别是它不使用运行时多态性(即没有虚拟函数)。

STL 的真正力量不在于其 容器 类,而在于它是一个框架,它结合了算法和数据结构,使用迭代器进行间接操作,从而允许更高阶算法的通用实现有效地作用于各种形式的数据。举个简单的例子,相同的 std::copy 函数可用于将元素从一个数组复制到另一个数组,或复制文件的字节,或将 "text like this" 中以空格分隔的单词复制到 std::vector<std::string> 等容器中。

 // std::copy from array a to array b
 int a[10] = { 3,1,4,1,5,9,2,6,5,4 };
 int b[10];
 std::copy(&a[0], &a[9], b);

 // std::copy from input stream a to an arbitrary OutputIterator
 template <typename OutputIterator>
 void f(std::istream &a, OutputIterator destination) {
   std::copy(std::istreambuf_iterator<char>(a),
             std::istreambuf_iterator<char>(),
             destination);
 }

 // std::copy from a buffer containing text, inserting items in
 // order at the back of the container called words.
 std::istringstream buffer("text like this");
 std::vector<std::string> words;
 std::copy(std::istream_iterator<std::string>(buffer),
           std::istream_iterator<std::string>(),
           std::back_inserter(words));
 assert(words[0] == "text");
 assert(words[1] == "like");
 assert(words[2] == "this");
Alexander Stepanov
Alexander Stepanov

C++ 标准库整合了 STL 的一部分(由 SGI/惠普公司发布为软件库)。C++ 标准模板库的主要实现者是 Alexander Stepanov

如今,我们称被采用到 C++ 标准中的部分为 STL。ISO C++ 没有指定头文件内容,允许在头文件中或真库中实现 STL。

注意
Alexander Stepanov 在一次采访中表示,他最初希望 STL 中的所有辅助函数都可见,但这在政治上不可行,尤其是堆函数。Bjarne 确实将 STL 中的组件数量减少了一半,以便于将其采用到标准中。

编译器将已经包含一个实现作为 C++ 标准的一部分(例如,MS Visual Studio 使用 Dinkum STL)。所有实现都必须符合标准关于功能和行为的要求,但程序在所有主要硬件实现、操作系统和编译器中的一致性也取决于 STL 实现的可移植性。它们还可能提供扩展功能或针对不同的设置进行优化。

有许多不同的 STL 实现,它们都基于语言标准,但仍彼此不同,这使得程序员对它透明,能够实现代码库的专业化和快速演进。有多种开源实现可用,可以参考这些实现。

STL 实现列表。

注意
将功能细分化有其优势,一些开发人员出于多种原因积极避免使用某些语言特性。C++ 允许程序员选择如何表达自己,控制开发范式,而不受更高抽象级别的约束。

我们将在本书的这一部分中讨论的容器是标准命名空间 (std::) 的一部分。它们都起源于 STL 的原始 SGI 实现。

注意
在选择容器时,您应该牢记它们的差异,这将有助于您生成更高效的代码。另请参阅本书的 优化部分,了解 在正确的容器中使用正确的数据

序列容器

[编辑 | 编辑源代码]
序列 - 比数组更容易

序列类似于 C 数组,但它们更易于使用。vector 通常是学习的第一个序列。其他序列 list 和双端队列与 vector 类似,但在某些特殊情况下效率更高。(它们的行为在迭代器在容器更改时保持有效性方面也存在重要差异;迭代器有效性是使用 C++ 中的容器时一个重要的,尽管有点高级的概念。)

  • vector - "易于使用的数组"
  • list - 实际上是一个双向链表
  • deque - 双端队列(正确发音为 "deck",常被误读为 "dee-queue")

vector 本身就是一个模板类,它是一个 序列容器,允许您轻松创建 动态数组,用于存储程序中几乎任何数据类型或对象的元素(每个实例一种类型)。vector 类为您处理大多数内存管理。

由于 vector 包含连续的元素,因此它是用作旧式 C 样式数组的理想选择,在这种情况下您需要存储数据,并且是需要存储动态数据作为数组(在程序执行期间大小会发生变化)的理想选择(旧式 C 样式数组无法做到这一点)。但是,与静态数组相比,vector 会产生非常小的开销(取决于编译器的质量),并且不能通过初始化列表进行初始化。

注意

众所周知,vector 在使用 MSVC 编译器时速度较慢,这是因为 SECURE_SCL 标志默认情况下强制执行边界检查,即使在优化构建中也是如此。

访问 vector 的成员或追加元素所需的时间是固定的,与 vector 的大小无关,而查找 vector 元素中的特定值或将元素插入 vector 所需的时间与其位置成正比(与大小相关)。

注意

如果您创建了一个 vector,您可以使用连续的指针访问其数据

  std::vector<type> myvector(8);
  type * ptr = &myvector[0];
  ptr[0], ptr[7]; // access the first and last objects in myvector

此信息存在于 INCITS/ISO/IEC 14882-2003 中,但在 1998 年版本的 C++ 标准中没有得到正确记录。
请注意,ptr[i] 比 myvector.at(i) 快,因为没有执行错误检查。注意该指针的有效性。vector 的连续性在与 C 代码进行交互时最常很重要。

您还应该记住,std::vector<T>::iterator 可能不是指针;使用迭代器是访问容器的最安全模式,但安全性总是会影响性能。

示例
Clipboard

要完成的事情
是否应将其拆分为两个示例,一个 "旧式 C 样式数组" 示例和一个 "新的 C++ STL vector" 示例?


/*
David Cary 2009-03-04
quick demo for wikibooks
*/

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

vector<int> pick_vector_with_biggest_fifth_element(vector<int> left,vector<int> right)
{
    if(left[5] < right[5])
    {
        return( right );
    }
    // else
    return left ;
}

int* pick_array_with_biggest_fifth_element(int * left,int * right)
{
    if(left[5] < right[5])
    {
        return( right );
    }
    // else 
    return left ;
}

int vector_demo(void)
{
    cout << "vector demo" << endl;
    vector<int> left(7);
    vector<int> right(7);

    left[5] = 7;
    right[5] = 8;
    cout << left[5] << endl;
    cout << right[5] << endl;
    vector<int> biggest(pick_vector_with_biggest_fifth_element( left, right ) );
    cout << biggest[5] << endl;

    return 0;
}

int array_demo(void)
{
    cout << "array demo" << endl;
    int left[7];
    int right[7];

    left[5] = 7;
    right[5] = 8;
    cout << left[5] << endl;
    cout << right[5] << endl;
    int * biggest =
        pick_array_with_biggest_fifth_element( left, right );
    cout << biggest[5] << endl;

    return 0;
}

int main(void)
{
    vector_demo();
    array_demo();
}
成员函数

vector 类模拟了 Container 概念,这意味着它具有 begin()end()size()max_size()empty()swap() 方法。

注意
由于大多数 vector(或 deque)实现通常会为将来的增长预留一些额外的内部存储空间。当内存资源成为一个因素时,在更改标准 vector 大小(或释放使用的内存)时,首选 swap() 方法。

  • 信息性
    • vector::front - 返回 vector 的第一个元素的引用。
    • vector::back - 返回 vector 的最后一个元素的引用。
    • vector::size - 返回向量中元素的数量。
    • vector::empty - 如果向量中没有元素,则返回 true。
  • 标准操作
    • vector::insert - 将元素插入向量(单个和范围),将后面的元素向上移动。效率低下。
    • vector::push_back - 将元素追加(插入)到向量的末尾,如果需要,则为其分配内存。摊销 O(1) 时间。
    • vector::erase - 从向量中删除元素(单个和范围),将后面的元素向下移动。效率低下。
    • vector::pop_back - 删除向量的最后一个元素,(可能减少容量 - 通常不会减少,但这取决于特定的 STL 实现)。摊销 O(1) 时间。
    • vector::clear - 删除所有元素。但是请注意,如果数据元素是指向动态创建的内存的指针(例如,使用 new 运算符),则不会释放该内存。
  • 分配/大小修改
    • vector::assign - 用于删除 向量 并将指定元素复制到空的 目标 向量 中。
    • vector::reserve - 如果需要,更改向量的容量(分配更多内存)。在许多 STL 实现中,容量只能增长,而不能减少。
    • vector::capacity - 返回向量的当前容量(分配的内存)。
    • vector::resize - 更改向量的大小。
  • 迭代
    • vector::begin - 返回一个指向向量开始遍历的迭代器。
    • vector::end - 返回一个指向向量末尾之后的迭代器。
    • vector::at - 返回对 向量 中指定位置的数据元素的引用,并进行边界检查。

注意

在处理容器时,务必记住 capacity()、size() 和 empty() 的区别。

vector<int> v;
for (vector<int>::iterator it = v.begin(); it!=v.end(); ++it/* increment operand is used to move to next element*/) {
    cout << *it << endl;
}
vector::迭代器
[编辑 | 编辑源代码]

std::vector<T> 提供随机访问迭代器;与所有容器一样,对迭代器的主要访问是通过 begin() 和 end() 成员函数。这些函数针对常量和非常量容器进行了重载,分别返回类型为 std::vector<T>::const_iterator 和 std::vector<T>::iterator 的迭代器。


Clipboard

要完成的事情
添加缺失数据


vector 示例
[编辑 | 编辑源代码]
 /* Vector sort example */
 #include <iostream>
 #include <vector>
 
 int main()
 {
         using namespace std;
  
         cout << "Sorting STL vector, \"the easier array\"... " << endl;
         cout << "Enter numbers, one per line.  Press ctrl-D to quit." << endl;
 
         vector<int> vec; 
         int tmp;
         while (cin>>tmp) {
                 vec.push_back(tmp);
         }
 
         cout << "Sorted: " << endl;
         sort(vec.begin(), vec.end());   
         int i = 0;
         for (i=0; i<vec.size(); i++) {
                 cout << vec[i] << endl;;
         }
 
         return 0;
 }

sort的调用实际上调用了函数模板std::sort的实例化,它将对由两个随机访问迭代器指定的任何半开范围起作用。

如果您想使上面的代码更“STLish”,您可以用以下方式编写此程序

 #include <iostream>
 #include <vector>
 #include <algorithm>
 #include <iterator>
 
 int main()
 {
        using namespace std;
 
        cout << "Sorting STL vector, \"the easier array\"... " << endl;
        cout << "Enter numbers, one per line.  Press ctrl-D to quit." << endl;
 
        istream_iterator<int> first(cin);
        istream_iterator<int> last;
        vector<int> vec(first, last);
 
        sort(vec.begin(), vec.end());
 
        cout << "Sorted: " << endl;
 
        copy(vec.begin(), vec.end(), ostream_iterator<int>(cout, "\n"));
 
        return 0;
 }

STL 提供了一个名为 list 的类模板(是标准命名空间 (std::) 的一部分),它实现了一个非侵入式双向链表。链表可以在恒定时间内在中间插入或删除元素,但没有随机访问。std::list 的一个有用特性是,只要该项目保留在列表中,对插入到列表中的项目的引用、指针和迭代器就会保持有效。

注意
考虑使用 vector 而不是 list 来获得更好的缓存一致性并避免“交换死亡”,请参阅优化部分,了解有关在正确的容器中使用正确的数据的信息。

list 示例
[编辑 | 编辑源代码]
 /* List example - insertion in a list */
 #include <iostream>
 #include <algorithm>
 #include <iterator>
 #include <list>

 void print_list(std::list<int> const& a_filled_list)
 {
        using namespace std;

        ostream_iterator<int> out(cout, "  ");
        copy(a_filled_list.begin(), a_filled_list.end(), out);
 }

 int main()
 {
	 std::list<int> my_list;

	 my_list.push_back(1);
	 my_list.push_back(10);
	 print_list(my_list); //print : 1 10

	 std::cout << std::endl;

	 my_list.push_front(45);
	 print_list(my_list); //print : 45 1 10

	 return 0;
 }
Clipboard

要完成的事情
添加缺失数据


关联容器(键和值)

[编辑 | 编辑源代码]

这种类型的容器使用键值指向容器中的每个元素,从而简化了程序员搜索容器的过程。与其遍历数组或向量元素来查找特定元素,您可以简单地请求 people["tero"]。与向量和其他容器一样,关联容器可以扩展以容纳任意数量的元素。

映射和多映射
[编辑 | 编辑源代码]

mapmultimap 是关联容器,它们管理键/值对作为元素,如上所示。每个容器的元素将使用实际的键作为排序标准自动排序。两者之间的区别在于映射不允许重复项,而多映射则允许。

  • map - 唯一的键
  • multimap - 可以多次使用相同的键
  • set - 唯一的键是值
  • multiset - 键是值,可以多次使用相同的键
  /* Map example - character distribution  */
  #include <iostream>
  #include <map>
  #include <string>
  #include <cctype>
 
  using namespace std;
 
  int main()
  {
          /* Character counts are stored in a map, so that 
           * character is the key.
           * Count of char a is chars['a']. */
          map<char, long> chars;
 
          cout << "chardist - Count character distributions" << endl;
          cout << "Type some text. Press ctrl-D to quit." << endl;
          char c;
          while (cin.get(c)) {
                  // Upper A and lower a are considered the same 
                  c=tolower(static_cast<unsigned char>(c));
                  chars[c]=chars[c]+1; // Could be written as ++chars[c];
          }
 
          cout << "Character distribution: " << endl;
            
          string alphabet("abcdefghijklmnopqrstuvwxyz");
          for (string::iterator letter_index=alphabet.begin(); letter_index != alphabet.end(); letter_index++) {
                  if (chars[*letter_index] != 0) {
                          cout << char(toupper(*letter_index))
                               << ":" << chars[*letter_index]
                               << "\t" << endl; 
                  }
          }
          return 0;
  }

容器适配器

[编辑 | 编辑源代码]
  • stack - 后进先出 (LIFO)
  • queue - 先进先出 (FIFO)
  • 优先级队列

迭代器

[编辑 | 编辑源代码]

C++ 的迭代器是 STL 的基础之一。其他语言中也存在迭代器,但 C++ 使用了不同形式的迭代器,有优点也有缺点。

在 C++ 中,迭代器是概念而不是特定类型,它们是指针的泛化,作为容器使用的抽象。迭代器根据其属性(如遍历属性)进一步细分。

迭代器的基本思想是提供一种方法来遍历一些对象集合的概念。

一些(重叠的)迭代器类别包括

  • 单迭代器
  • 无效迭代器
  • 随机访问迭代器
  • 双向迭代器
  • 正向迭代器
  • 输入迭代器
  • 输出迭代器
  • 可变迭代器

一对迭代器 [begin, end) 用于定义一个半开范围,其中包括从 beginend 标识的元素,但 end 标识的元素除外。作为特殊情况,对于任何有效的迭代器 x,半开范围 [x, x) 为空。

注意
范围符号可能有所不同,其含义是表示范围限制的包含或排除。另一个常见的符号是 [begin, end[(表示 begin 是范围的一部分,而 end 不是)。

C++ 中最原始的迭代器示例(也可能是其语法的灵感来源)是内置指针,它们通常用于迭代数组中的元素。

遍历容器

[编辑 | 编辑源代码]

访问(但不能修改)容器中的每个元素类型C<T>使用迭代器。

 for (
      typename C<T>::const_iterator iter = group.begin();
      iter != group.end();
      ++iter
     )
 {
     T const &element = *iter;
 
     // access element here
 }

请注意 typename 的用法。它告知编译器 'const_iterator' 是一个类型,而不是一个静态成员变量。(它仅在模板代码中必要,实际上在 C++98 中,在常规的非模板代码中它是无效的。这可能会在 C++ 标准的下一个修订版中发生变化,从而始终允许上面的 typename。)

修改容器中的每个元素类型C<T>使用迭代器。

 for (
      typename C<T>::iterator iter = group.begin();
      iter != group.end();
      ++iter
     )
 {
     T &element = *iter;
 
     // modify element here
 }

在遍历容器时修改容器本身时,某些容器(如 vector)需要小心,以确保迭代器不会失效并最终指向无效元素。例如,而不是

  for (i = v.begin(); i != v.end(); ++i) {
    ...
    if (erase_required) {
      v.erase(i);
    }
  }

执行

  for (i = v.begin(); i != v.end(); ) {
    ...
    if (erase_required) {
        i = v.erase(i);
    } else {
        ++i;
    }
  }

erase()成员函数返回下一个有效的迭代器,或者end(),从而结束循环。请注意,++ierase()被调用时不会被执行。

仿函数

[编辑 | 编辑源代码]

仿函数或函数对象,是一个具有 operator () 的对象。仿函数的重要性在于它们可以在许多 C++ 函数可以使用的上下文中使用,同时还能维护状态信息。除了迭代器之外,仿函数是 STL 利用的最基本的概念之一。

STL 提供了许多预先构建的仿函数类;例如,std::less 通常用于为需要确定两个对象中哪一个在“前面”的算法指定默认比较函数。

 #include <vector>
 #include <algorithm>
 #include <iostream>

 // Define the Functor for AccumulateSquareValues
 template<typename T>
 struct AccumulateSquareValues
 {
     AccumulateSquareValues() : sumOfSquares()
     {
     }
     void operator()(const T& value)
     {
         sumOfSquares += value*value;
     }
     T Result() const
     {
         return sumOfSquares;
     }
     T sumOfSquares;
 };

 std::vector<int> intVec;
 intVec.reserve(10);
 for( int idx = 0; idx < 10; ++idx )
 {
     intVec.push_back(idx);
 }
 AccumulateSquareValues<int> sumOfSquare = std::for_each(intVec.begin(), 
                                                         intVec.end(), 
                                                         AccumulateSquareValues<int>() );
 std::cout << "The sum of squares for 1-10 is " << sumOfSquare.Result() << std::endl;

 // note: this problem can be solved in another, more clear way:
 // int sum_of_squares = std::inner_product(intVec.begin(), intVec.end(), intVec.begin(), 0);

STL 还提供了一些有用的算法,以 _模板函数_ 的形式提供,这些函数借助迭代器概念来操作 STL 容器(或派生类)。

STL 算法并不局限于 STL 容器,例如

#include <algorithm>

  int array[10] = { 2,3,4,5,6,7,1,9,8,0 };

  int* begin = &array[0];
  int* end = &array[0] + 10;

  std::sort(begin, end);// the sort algorithm will work on a C style array
带有 _if 后缀的算法
带有 _copy 后缀的算法
  • 非修改算法
  • 修改算法
  • 移除算法
  • 变异算法
  • 排序算法
  • 排序范围算法
  • 数值算法
Clipboard

要完成的事情
完整


[编辑 | 编辑源代码]
stable_sort
[编辑 | 编辑源代码]
partial_sort
[编辑 | 编辑源代码]
最小值和最大值
[编辑 | 编辑源代码]

标准库提供了函数模板 minmax,它们分别返回两个参数的最小值和最大值。每个模板都有一个重载版本,允许您自定义值的比较方式。

template<class T>
const T& min(const T& a, const T& b);

template<class T, class Compare>
const T& min(const T& a, const T& b, Compare c);

template<class T>
const T& max(const T& a, const T& b);

template<class T, class Compare>
const T& max(const T& a, const T& b, Compare c);

如何使用 Compare 类型参数的示例

 #include <iostream>
 #include <algorithm>
 #include <string>

 class Account
 {
	 private :
         std::string owner_name;
	 int credit;
	 int potential_credit_transfer;

	 public :
	 Account(){}
	 Account(std::string name, int initial_credit, int initial_credit_transfer) :
	 	 owner_name(name),
	         credit(initial_credit),
	         potential_credit_transfer(initial_credit_transfer)
	 {}

	 bool operator<(Account const& account) const { return credit < account.credit; }

	 int potential_credit() const { return credit + potential_credit_transfer; }

	 std::string const& owner() const { return owner_name; }
 };

 struct CompareAccountCredit
 {
	 bool operator()(Account const& account1, Account const& account2) const 
         { return account1 < account2; }
 };

 struct CompareAccountPotentialCredit
 {
	 bool operator()(Account const& account1, Account const& account2) const 
         { return account1.potential_credit() < account2.potential_credit(); }
 };

 int main()
 {
	 Account account1("Dennis Ritchie", 1000, 250), account2("Steeve Jobs", 500, 10000), 
         result_comparison;

	 result_comparison = std::min(account1, account2, CompareAccountCredit());
	 std::cout << "min credit of account is : " + result_comparison.owner() << std::endl;

	 result_comparison = std::min(account1, account2, CompareAccountPotentialCredit());
	 std::cout << "min potential credit of account is : " + result_comparison.owner() << std::endl;

	 return 0;
 }
Clipboard

要完成的事情
这需要一个关于如何使用 Compare 类型参数的示例。

分配器

[编辑 | 编辑源代码]

分配器由标准 C++ 库(特别是 STL)使用,以允许对内存分配策略进行参数化。

分配器的主题有些晦涩,大多数应用程序软件开发人员可以安全地忽略它。所有允许指定分配器的标准库构造都有一个默认分配器,如果用户没有提供,则使用该分配器。

如果代码的内存使用方式非常特殊,以至于在使用通用默认分配器时会导致性能问题,则自定义分配器会很有用。在其他情况下,默认分配器也不适用,例如在实现全局运算符 new 和 delete 的替换时使用标准容器。

华夏公益教科书