精通-C++-编程(一)

169 阅读23分钟

精通 C++ 编程(一)

原文:annas-archive.org/md5/0E32826EC8D4CA7BCD89E795AD6CBF05

译者:飞龙

协议:CC BY-NC-SA 4.0

前言

C++是一种有趣的编程语言,已经存在了将近三十年。它用于开发复杂的桌面应用程序、Web 应用程序、网络应用程序、设备驱动程序、内核模块、嵌入式应用程序以及使用第三方小部件框架的 GUI 应用程序;可以说,C++可以在任何领域使用。

自 1993 年开始编程以来,我一直珍惜与许多同事和行业专家进行的良好的技术讨论。在所有的技术讨论中,有一个话题一再重复,那就是,“你认为 C++今天还是一个相关的编程语言吗?我应该继续在 C++上工作,还是应该转向其他现代编程语言,比如 Java、C#、Scala 或 Angular/Node.js?”

我一直觉得应该开放学习其他技术,但这并不意味着要放弃 C++。然而,好消息是,有了新的 C++17 特性,C++已经重生,并将在未来几十年内继续存在和发展,这是我写这本书的动力。

人们一直认为 Java 会取代 C++,但它一直存在。当 C#进入行业时,同样的讨论再次开始,今天当 Angular/Node.js 和 Scala 似乎更具吸引力时,同样的讨论再次开始。然而,C++有自己的位置,迄今为止没有一种编程语言能够取代 C++的位置。

已经有很多 C++书籍帮助你理解这种语言,但很少有书籍涉及在 C++中开发 GUI 应用程序、使用 C++进行 TDD 和 BDD。

C++已经走过了很长的路,现在已经在多个领域得到了应用。它的主要优势在于其软件基础设施和资源受限的应用程序。C++ 17 的发布将改变开发人员编写代码的方式,而本书将帮助您掌握 C++的开发技能。

通过真实世界的实际示例解释每个概念,本书将首先介绍您 C++ 17 的最新特性。它将鼓励在 C++中采用清晰的代码实践,并演示 C++中的 GUI 应用程序开发选项。您将深入了解如何使用智能指针避免内存泄漏。接下来,您将学习多线程编程如何帮助您实现应用程序的并发性。

接下来,您还将深入了解 C++标准模板库。我们将解释在 C++程序中实现 TDD 和 BDD 的概念,以及基于模板的通用编程,为您提供构建强大应用程序的专业知识。最后,我们将以调试技术和最佳实践结束本书。当您读完本书时,您将对语言及其各个方面有深入的了解。

本书涵盖的内容

第一章,“C++17 特性”,解释了 C++17 的新特性以及已被移除的特性。它还通过易于理解的示例演示了关键的 C++17 特性。

第二章,“标准模板库”,概述了 STL,演示了各种容器和迭代器,并解释了如何在容器上应用有用的算法。本章还涉及了使用的内部数据结构及其运行效率。

第三章,“模板编程”,概述了通用编程及其优点。它演示了编写函数模板和类模板,以及重载函数模板。它还涉及了编写通用类、显式类特化和部分特化。

第四章,“智能指针”,解释了使用原始指针的问题,并推动了智能指针的使用。逐渐地,这一章介绍了 auto_ptr,unique_ptr,shared_ptr 和 weak_ptr 的使用,并解释了解决循环依赖问题的方法。

第五章,“在 C++中开发 GUI 应用程序”,概述了 Qt,并为您提供了在 Linux 和 Windows 上安装 Qt 的逐步说明。本章逐渐帮助您开发具有有趣的小部件和各种布局的令人印象深刻的 GUI 应用程序。

第六章,“多线程编程和进程间通信”,介绍了 POSIX pthreads 库,并讨论了本地 C++线程库。它还讨论了使用 C++线程库的好处。随后,它帮助您编写多线程应用程序,探讨了管理线程的方法,并解释了同步机制的使用。本章讨论了死锁和可能的解决方案。在本章末尾,它向您介绍了并发库。

第七章,“测试驱动开发”,简要概述了 TDD,并澄清了 TDD 的常见问题。本章为您提供了逐步说明,以安装 Google 测试框架并将其与 Linux 和 Windows 平台集成。它帮助您以易于理解的教程风格使用 TDD 开发应用程序。

第八章,“行为驱动开发”,概述了 BDD,并指导您在 Linux 平台上安装,集成和配置黄瓜框架。它还解释了 Gherkin,并帮助您编写 BDD 测试用例。

第九章,“调试技术”,讨论了行业中用于调试应用程序问题的各种策略和技术。随后,它帮助您了解使用 GDB 和 Valgrind 工具进行逐步调试,监视变量,修复各种与内存相关的问题,包括内存泄漏。

第十章,“代码异味和清洁代码实践”,讨论了各种代码异味和重构技术。

您需要为本书准备以下工具

在开始阅读本书之前,您需要准备以下工具:

  • g++编译器版本 5.4.0 20160609 或更高版本

  • GDB 7.11.1

  • Valgrind 3.11.0

  • Cucumber-cpp Git 2.7.4

  • Google 测试框架(gtest 1.6 或更高版本)

  • CMake 3.5.1

  • Ruby 2.5.1

  • Qt 5.7.0

  • Bundler v 1.14.6

所需的操作系统是 Ubuntu 16.04 64 位或更高版本。硬件配置至少应为 1GB RAM 和 20GB ROM。具有这种配置的虚拟机也应该足够。

这本书是为谁准备的

这本书是为有经验的 C++开发人员准备的。如果您是初学者 C++开发人员,那么强烈建议您在阅读本书之前对 C++语言有扎实的了解。

约定

在本书中,您将找到许多文本样式,用于区分不同类型的信息。以下是一些这些样式的示例及其含义解释。文本中的代码词,数据库表名,文件夹名,文件名,文件扩展名,路径名,虚拟 URL,用户输入和 Twitter 句柄显示如下:“initialize()方法将deque迭代器pos初始化为存储在deque中的第一个数据元素。”

一块代码设置如下:

#include <iostream>

int main ( ) {

        const int x = 5, y = 5;

        static_assert ( 1 == 0, "Assertion failed" );
        static_assert ( 1 == 0 );
        static_assert ( x == y );

        return 0;
}

当我们希望引起您对代码块的特定部分的注意时,相关的行或项目将以粗体显示:

#include <iostream>
#include <thread>
#include <mutex>
#include "Account.h"
using namespace std;

enum ThreadType {
  DEPOSITOR,
  WITHDRAWER
};

mutex locker;

任何命令行输入或输出都将按以下方式编写:

g++ main.cpp -std=c++17
./a.out

新术语重要单词以粗体显示。您在屏幕上看到的单词,例如菜单或对话框中的单词,会以这种方式出现在文本中:“您需要通过导航到新项目| Visual Studio | Windows | Win32 | Win32 控制台应用程序来创建名为 MathApp 的新项目。”

警告或重要说明会出现在这样。

提示和技巧会出现在这样。

C++17 功能

在本章中,您将学习以下概念:

  • C++17 背景

  • C++17 中有什么新功能?

  • C++17 中弃用或移除的功能是什么?

  • C++17 的关键特性

C++17 背景

如您所知,C++语言是 Bjarne Stroustrup 的心血结晶,他于 1979 年开发了 C++。C++编程语言由国际标准化组织(ISO)标准化。

最初的标准化于 1998 年发布,通常称为 C++98,接下来的标准化 C++03 于 2003 年发布,这主要是一个修复错误的版本,只有一个语言特性用于值初始化。2011 年 8 月,C++11 标准发布,核心语言增加了一些内容,包括对标准模板库STL)的一些重大有趣的更改;C++11 基本上取代了 C++03 标准。C++14 于 2014 年 12 月发布,带有一些新功能,后来,C++17 标准于 2017 年 7 月 31 日发布。

在撰写本书时,C++17 是 C++编程语言的 ISO/IEC 标准的最新修订版。

本章需要支持 C++17 功能的编译器:gcc 版本 7 或更高版本。由于 gcc 版本 7 是撰写本书时的最新版本,因此在本章中我将使用 gcc 版本 7.1.0。

如果您还没有安装支持 C++17 功能的 g++ 7,可以使用以下命令安装:

`sudo add-apt-repository ppa:jonathonf/gcc-7.1

sudo apt-get update

sudo apt-get install gcc-7 g++-7`

C++17 中有什么新功能?

完整的 C++17 功能列表可以在en.cppreference.com/w/cpp/compiler_support#C.2B.2B17_features找到。

为了给出一个高层次的概念,以下是一些新的 C++17 功能:

  • 直接列表初始化的新汽车规则

  • 没有消息的static_assert

  • 嵌套命名空间定义

  • 内联变量

  • 命名空间和枚举器的属性

  • C++异常规范是类型系统的一部分

  • 改进的 lambda 功能,可在服务器上提供性能优势

  • NUMA 架构

  • 使用属性命名空间

  • 用于超对齐数据的动态内存分配

  • 类模板的模板参数推导

  • 具有自动类型的非类型模板参数

  • 保证的拷贝省略

  • 继承构造函数的新规范

  • 枚举的直接列表初始化

  • 更严格的表达式评估顺序

  • shared_mutex

  • 字符串转换

否则,核心 C++语言中添加了许多有趣的新功能:STL、lambda 等。新功能为 C++带来了面貌更新,从C++17开始,作为 C++开发人员,您会感到自己正在使用现代编程语言,如 Java 或 C#。

C++17 中弃用或移除的功能是什么?

以下功能现在已在 C++17 中移除:

  • register关键字在 C++11 中已被弃用,并在 C++17 中被移除

  • ++运算符对bool在 C++98 中已被弃用,并在 C++17 中被移除

  • 动态异常规范在 C++11 中已被弃用,并在 C++17 中被移除

C++17 的关键特性

让我们逐个探讨以下 C++17 的关键功能:

  • 更简单的嵌套命名空间

  • 从大括号初始化列表中检测类型的新规则

  • 简化的static_assert

  • std::invoke

  • 结构化绑定

  • ifswitch局部作用域变量

  • 类模板的模板类型自动检测

  • 内联变量

更简单的嵌套命名空间语法

直到 C++14 标准,C++中支持的嵌套命名空间的语法如下:

#include <iostream>
using namespace std;

namespace org {
    namespace tektutor {
        namespace application {
             namespace internals {
                  int x;
             }
        }
    }
}

int main ( ) {
    org::tektutor::application::internals::x = 100;
    cout << "\nValue of x is " << org::tektutor::application::internals::x << endl;

    return 0;
}

上述代码可以使用以下命令编译,并且可以查看输出:

g++-7 main.cpp -std=c++17
./a.out

上述程序的输出如下:

Value of x is 100

每个命名空间级别都以大括号开始和结束,这使得在大型应用程序中使用嵌套命名空间变得困难。C++17 嵌套命名空间语法真的很酷;只需看看下面的代码,你就会很容易同意我的观点:

#include <iostream>
using namespace std;

namespace org::tektutor::application::internals {
    int x;
}

int main ( ) {
    org::tektutor::application::internals::x = 100;
    cout << "\nValue of x is " << org::tektutor::application::internals::x << endl;

    return 0;
}

上述代码可以编译,并且可以使用以下命令查看输出:

g++-7 main.cpp -std=c++17
./a.out

输出与上一个程序相同:

Value of x is 100

来自大括号初始化列表的类型自动检测的新规则

C++17 引入了对初始化列表的自动检测的新规则,这补充了 C++14 的规则。C++17 规则坚持认为,如果声明了std::initializer_list的显式或部分特化,则程序是非法的:

#include <iostream>
using namespace std;

template <typename T1, typename T2>
class MyClass {
     private:
          T1 t1;
          T2 t2;
     public:
          MyClass( T1 t1 = T1(), T2 t2 = T2() ) { }

          void printSizeOfDataTypes() {
               cout << "\nSize of t1 is " << sizeof ( t1 ) << " bytes." << endl;
               cout << "\nSize of t2 is " << sizeof ( t2 ) << " bytes." << endl;
     }
};

int main ( ) {

    //Until C++14
    MyClass<int, double> obj1;
    obj1.printSizeOfDataTypes( );

    //New syntax in C++17
    MyClass obj2( 1, 10.56 );

    return 0;
}

上述代码可以编译,并且可以使用以下命令查看输出:

g++-7 main.cpp -std=c++17
./a.out

上述程序的输出如下:

Values in integer vectors are ...
1 2 3 4 5 

Values in double vectors are ...
1.5 2.5 3.5 

简化的 static_assert

static_assert宏有助于在编译时识别断言失败。这个特性自 C++11 以来就得到了支持;然而,在 C++17 中,static_assert宏在之前是需要一个强制的断言失败消息的,现在已经变成了可选的。

以下示例演示了使用static_assert的方法,包括消息和不包括消息:

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

int main ( ) {

        const int x = 5, y = 5;

        static_assert ( 1 == 0, "Assertion failed" );
        static_assert ( 1 == 0 );
        static_assert ( x == y );

        return 0;
}

上述程序的输出如下:

g++-7 staticassert.cpp -std=c++17
staticassert.cpp: In function ‘int main()’:
staticassert.cpp:7:2: error: static assertion failed: Assertion failed
 static_assert ( 1 == 0, "Assertion failed" );

staticassert.cpp:8:2: error: static assertion failed
 static_assert ( 1 == 0 );

从上面的输出中,您可以看到消息Assertion failed作为编译错误的一部分出现,而在第二次编译中,由于我们没有提供断言失败消息,出现了默认的编译器错误消息。当没有断言失败时,断言错误消息不会出现,如static_assert(x==y)所示。这个特性受到了 BOOST C++库中 C++社区的启发。

std::invoke()方法

std::invoke()方法可以用相同的语法调用函数、函数指针和成员指针:

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

void globalFunction( ) {
     cout << "globalFunction ..." << endl;
}

class MyClass {
    public:
        void memberFunction ( int data ) {
             std::cout << "\nMyClass memberFunction ..." << std::endl;
        }

        static void staticFunction ( int data ) {
             std::cout << "MyClass staticFunction ..." << std::endl;
        }
};

int main ( ) {

    MyClass obj;

    std::invoke ( &MyClass::memberFunction, obj, 100 );
    std::invoke ( &MyClass::staticFunction, 200 );
    std::invoke ( globalFunction );

    return 0;
}

上述代码可以编译,并且可以使用以下命令查看输出:

g++-7 main.cpp -std=c++17
./a.out

上述程序的输出如下:

MyClass memberFunction ...
MyClass staticFunction ...
globalFunction ...

std::invoke()方法是一个模板函数,可以帮助您无缝地调用可调用对象,无论是内置的还是用户定义的。

结构化绑定

现在您可以使用一个非常酷的语法初始化多个变量,并返回一个值,如下面的示例代码所示:

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

int main ( ) {

    tuple<string,int> student("Sriram", 10);
    auto [name, age] = student;

    cout << "\nName of the student is " << name << endl;
    cout << "Age of the student is " << age << endl;

    return 0;
}

在上述程序中,用粗体突出显示的代码是 C++17 引入的结构化绑定特性。有趣的是,我们没有声明string nameint age变量。这些都是由 C++编译器自动推断为stringint,这使得 C++的语法就像任何现代编程语言一样,而不会失去其性能和系统编程的好处。

上述代码可以编译,并且可以使用以下命令查看输出:

g++-7 main.cpp -std=c++17
./a.out

上述程序的输出如下:

Name of the student is Sriram
Age of the student is 10

If 和 Switch 局部作用域变量

有一个有趣的新功能,允许您声明一个绑定到ifswitch语句代码块的局部变量。在ifswitch语句中使用的变量的作用域将在各自的块之外失效。可以通过以下易于理解的示例更好地理解,如下所示:

#include <iostream>
using namespace std;

bool isGoodToProceed( ) {
    return true;
}

bool isGood( ) {
     return true;
}

void functionWithSwitchStatement( ) {

     switch ( auto status = isGood( ) ) {
          case true:
                 cout << "\nAll good!" << endl;
          break;

          case false:
                 cout << "\nSomething gone bad" << endl;
          break;
     } 

}

int main ( ) {

    if ( auto flag = isGoodToProceed( ) ) {
         cout << "flag is a local variable and it loses its scope outside the if block" << endl;
    }

     functionWithSwitchStatement();

     return 0;
}

上述代码可以编译,并且可以使用以下命令查看输出:

g++-7 main.cpp -std=c++17
./a.out

上述程序的输出如下:

flag is a local variable and it loses its scope outside the if block
All good!

类模板的模板类型自动推断

我相信你会喜欢你即将在示例代码中看到的内容。虽然模板非常有用,但很多人不喜欢它,因为它的语法很难和奇怪。但是你不用担心了;看看下面的代码片段:

#include <iostream>
using namespace std;

template <typename T1, typename T2>
class MyClass {
     private:
          T1 t1;
          T2 t2;
     public:
          MyClass( T1 t1 = T1(), T2 t2 = T2() ) { }

          void printSizeOfDataTypes() {
               cout << "\nSize of t1 is " << sizeof ( t1 ) << " bytes." << endl;
               cout << "\nSize of t2 is " << sizeof ( t2 ) << " bytes." << endl;
     }
};

int main ( ) {

    //Until C++14
    MyClass<int, double> obj1;
    obj1.printSizeOfDataTypes( );

    //New syntax in C++17
    MyClass obj2( 1, 10.56 );

    return 0;
}

上述代码可以编译,并且可以使用以下命令查看输出:

g++-7 main.cpp -std=c++17
./a.out

程序的输出如下:

Size of t1 is 4 bytes.
Size of t2 is 8 bytes.

内联变量

就像 C++中的内联函数一样,现在您可以使用内联变量定义。这对初始化静态变量非常方便,如下面的示例代码所示:

#include <iostream>
using namespace std;

class MyClass {
    private:
        static inline int count = 0;
    public:
        MyClass() { 
              ++count;
        }

    public:
         void printCount( ) {
              cout << "\nCount value is " << count << endl;
         } 
};

int main ( ) {

    MyClass obj;

    obj.printCount( ) ;

    return 0;
}

上述代码可以编译,并且可以使用以下命令查看输出:

g++-7 main.cpp -std=c++17
./a.out

上述代码的输出如下:

Count value is 1

总结

在本章中,您了解了 C++17 引入的有趣的新特性。您学会了超级简单的 C++17 嵌套命名空间语法。您还学会了使用大括号初始化列表进行数据类型检测以及 C++17 标准中引入的新规则。

您还注意到,static_assert可以在没有断言失败消息的情况下完成。此外,使用std::invoke(),您现在可以调用全局函数、函数指针、成员函数和静态类成员函数。并且,使用结构化绑定,您现在可以用返回值初始化多个变量。

您还学到了ifswitch语句可以在if条件和switch语句之前有一个局部作用域的变量。您了解了类模板的自动类型检测。最后,您使用了inline变量。

C++17 有许多更多的特性,但本章试图涵盖大多数开发人员可能需要的最有用的特性。在下一章中,您将学习标准模板库。

标准模板库

本章将涵盖以下主题:

  • STL 概述

  • STL 架构

  • 容器

  • 迭代器

  • 算法

  • 函数对象

  • STL 容器

  • 序列

  • 关联

  • 无序

  • 适配器

让我们在以下各节中逐个查看 STL 主题。

标准模板库架构

C++ 标准模板库STL)提供了现成的通用容器、可应用于容器的算法以及用于导航容器的迭代器。STL 是用 C++模板实现的,模板允许在 C++中进行通用编程。

STL 鼓励 C++开发人员专注于手头的任务,通过摆脱编写低级数据结构和算法的开发人员。STL 是一个经过时间考验的库,可以实现快速应用程序开发。

STL 是一项有趣的工作和架构。它的秘密公式是编译时多态性。为了获得更好的性能,STL 避免了动态多态性,告别了虚函数。广义上来说,STL 有以下四个组件:

  • 算法

  • 函数对象

  • 迭代器

  • 容器

STL 架构将所有上述四个组件都连接在一起。它具有许多常用的算法,并提供性能保证。有趣的是,STL 算法可以在不了解包含数据的容器的情况下无缝工作。这是由于迭代器的高级遍历 API,它完全抽象了容器中使用的底层数据结构。STL 广泛使用运算符重载。让我们逐个了解 STL 的主要组件,以便对 STL 的概念有一个很好的理解。

算法

STL 算法由 C++模板支持;因此,相同的算法可以处理不同的数据类型,或者独立于容器中数据的组织方式。有趣的是,STL 算法足够通用,可以使用模板支持内置和用户定义的数据类型。事实上,算法通过迭代器与容器进行交互。因此,对算法来说重要的是容器支持的迭代器。话虽如此,算法的性能取决于容器中使用的底层数据结构。因此,某些算法仅适用于选择性的容器,因为 STL 支持的每个算法都期望某种类型的迭代器。

迭代器

迭代器是一种设计模式,但有趣的是,STL 的工作开始得早得多

四人帮将他们与设计模式相关的工作发布给了软件社区。迭代器本身是允许遍历容器以访问、修改和操作容器中存储的数据的对象。迭代器以如此神奇的方式进行操作,以至于我们不会意识到或需要知道数据存储在何处以及如何检索。

以下图像直观地表示了一个迭代器:

从上图中,您可以了解到每个迭代器都支持begin() API,它返回第一个元素的位置,end() API 返回容器中最后一个元素的下一个位置。

STL 广泛支持以下五种类型的迭代器:

  • 输入迭代器

  • 输出迭代器

  • 前向迭代器

  • 双向迭代器

  • 随机访问迭代器

容器实现了迭代器,让我们可以轻松地检索和操作数据,而不需要深入了解容器的技术细节。

以下表格解释了五种迭代器中的每一种:

迭代器的类型描述
输入迭代器
  • 用于从指向的元素中读取

  • 它适用于单次导航,一旦到达容器的末尾,迭代器将失效

  • 它支持前增量和后增量运算符

  • 它不支持递减运算符

  • 它支持解引用

  • 它支持==!=运算符来与其他迭代器进行比较

  • istream_iterator迭代器是输入迭代器

  • 所有容器都支持这个迭代器

|

输出迭代器
  • 它用于修改指向的元素

  • 它对于单次导航是有效的,一旦到达容器的末尾,迭代器将无效

  • 它支持前增量和后增量运算符

  • 它不支持减量运算符

  • 它支持解引用

  • 它不支持==!=运算符

  • ostream_iteratorback_inserterfront_inserter迭代器是输出迭代器的示例

  • 所有容器都支持这个迭代器

|

前向迭代器
  • 它支持输入迭代器和输出迭代器的功能

  • 它允许多次导航

  • 它支持前增量和后增量运算符

  • 它支持解引用

  • forward_list容器支持前向迭代器

|

双向迭代器
  • 它是一个支持双向导航的前向迭代器

  • 它允许多次导航

  • 它支持前增量和后增量运算符

  • 它支持前减量和后减量运算符

  • 它支持解引用

  • 它支持[]运算符

  • listsetmapmultisetmultimap容器支持双向迭代器

|

随机访问迭代器
  • 元素可以使用任意偏移位置进行访问

  • 它支持前增量和后增量运算符

  • 它支持前减量和后减量运算符

  • 它支持解引用

  • 它是功能上最完整的迭代器,因为它支持先前列出的其他类型迭代器的所有功能

  • arrayvectordeque容器支持随机访问迭代器

  • 支持随机访问的容器自然也支持双向和其他类型的迭代器

|

容器

STL 容器通常是动态增长和收缩的对象。容器在内部使用复杂的数据结构存储数据,并提供高级函数来访问数据,而不需要我们深入了解数据结构的复杂内部实现细节。STL 容器非常高效且经过时间考验。

每个容器使用不同类型的数据结构以有效的方式存储、组织和操作数据。尽管许多容器可能看起来相似,但在内部它们的行为是不同的。因此,选择错误的容器会导致应用程序性能问题和不必要的复杂性。

容器有以下几种类型:

  • 顺序

  • 关联

  • 容器适配器

容器中存储的对象是复制或移动的,而不是引用。我们将在接下来的部分中使用简单而有趣的示例来探索每种类型的容器。

函数对象

函数对象是行为类似于常规函数的对象。美妙之处在于函数对象可以替代函数指针的位置。函数对象是方便的对象,让您在不损害面向对象编码原则的情况下扩展或补充 STL 函数的行为。

函数对象很容易实现;你只需要重载函数运算符。函数对象也被称为函数对象。

以下代码将演示如何实现一个简单的函数对象:

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

template <typename T>
class Printer {
public:
  void operator() ( const T& element ) {
    cout << element << "\t";
  }
};

int main () {
  vector<int> v = { 10, 20, 30, 40, 50 };

  cout << "\nPrint the vector entries using Functor" << endl;

  for_each ( v.begin(), v.end(), Printer<int>() );

  cout << endl;

  return 0;
}

让我们快速编译程序,使用以下命令:

g++ main.cpp -std=c++17
./a.out

让我们来检查程序的输出:

Print the vector entries using Functor
10  20  30  40  50

我们希望你意识到函数对象是多么简单和酷。

序列容器

STL 支持一系列非常有趣的序列容器。序列容器以线性方式存储同质数据类型,可以按顺序访问。STL 支持以下序列容器:

  • 数组

  • 向量

  • 列表

  • forward_list

  • 双端队列

由于存储在 STL 容器中的对象只是值的副本,STL 从用户定义的数据类型中期望满足一些基本要求,以便将这些对象存储在容器中。存储在 STL 容器中的每个对象都必须提供以下最低要求:

  • 一个默认构造函数

  • 一个复制构造函数

  • 一个赋值运算符

让我们逐个探索序列容器在以下子节中。

数组

STL 数组容器是一个固定大小的序列容器,就像 C/C++内置数组一样,只是 STL 数组是大小感知的,比内置的 C/C++数组要聪明一点。让我们通过一个例子来了解 STL 数组:

#include <iostream>
#include <array>
using namespace std;
int main () {
  array<int,5> a = { 1, 5, 2, 4, 3 };

  cout << "\nSize of array is " << a.size() << endl;

  auto pos = a.begin();

  cout << endl;
  while ( pos != a.end() ) 
    cout << *pos++ << "\t";
  cout << endl;

  return 0;
}

可以使用以下命令编译前面的代码并查看输出:

g++ main.cpp -std=c++17
./a.out 

程序的输出如下:

Size of array is 5
1     5     2     4     3

代码演示

以下行声明了一个固定大小(5)的数组,并用五个元素初始化了数组:

array<int,5> a = { 1, 5, 2, 4, 3 };

一旦声明,大小就不能更改,就像 C/C++内置数组一样。array::size()方法返回数组的大小,不管在初始化列表中初始化了多少个整数。auto pos = a.begin()方法声明了一个array<int,5>的迭代器,并分配了数组的起始位置。array::end()方法指向数组中最后一个元素的下一个位置。迭代器的行为类似于或模拟 C++指针,对迭代器进行解引用会返回迭代器指向的值。迭代器位置可以通过++pos--pos向前和向后移动。

数组中常用的 API

以下表格显示了一些常用的数组 API:

API描述
at( int index )这返回存储在由索引引用的位置的值。索引是从零开始的索引。如果索引超出数组的索引范围,此 API 将抛出std::out_of_range异常。
operator [ int index ]这是一个不安全的方法,如果索引超出数组的有效范围,它不会抛出任何异常。这比at稍微快一点,因为此 API 不执行边界检查。
front()这返回数组中的第一个元素。
back()这返回数组中的最后一个元素。
begin()这返回数组中第一个元素的位置
end()这返回数组中最后一个元素的下一个位置
rbegin()这返回反向开始位置,即返回数组中的最后一个元素的位置
rend()这返回反向结束位置,即返回数组中第一个元素之前的一个位置
size()这返回数组的大小

数组容器支持随机访问;因此,给定一个索引,数组容器可以在*O(1)*或常量时间内获取一个值。

可以使用反向迭代器以反向方式访问数组容器元素:

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

int main () {

    array<int, 6> a;
    int size = a.size();
    for (int index=0; index < size; ++index)
         a[index] = (index+1) * 100;   

    cout << "\nPrint values in original order ..." << endl;

    auto pos = a.begin();
    while ( pos != a.end() )
        cout << *pos++ << "\t";
    cout << endl;

    cout << "\nPrint values in reverse order ..." << endl;

    auto rpos = a.rbegin();
    while ( rpos != a.rend() )
    cout << *rpos++ << "\t";
    cout << endl;

    return 0;
}

我们将使用以下命令来获取输出:

./a.out

输出如下:

Print values in original order ...
100   200   300   400   500   600

Print values in reverse order ...
600   500   400   300   200   100

Vector

Vector 是一个非常有用的序列容器,它的工作原理与数组完全相同,只是向量可以在运行时增长和缩小,而数组的大小是固定的。但是,在数组和向量下面使用的数据结构是一个简单的内置 C/C++样式数组。

让我们看下面的例子更好地理解向量:

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

int main () {
  vector<int> v = { 1, 5, 2, 4, 3 };

  cout << "\nSize of vector is " << v.size() << endl;

  auto pos = v.begin();

  cout << "\nPrint vector elements before sorting" << endl;
  while ( pos != v.end() )
    cout << *pos++ << "\t";
  cout << endl;

  sort( v.begin(), v.end() );

  pos = v.begin();

  cout << "\nPrint vector elements after sorting" << endl;

  while ( pos != v.end() )
    cout << *pos++ << "\t";
  cout << endl;

  return 0;
}

可以使用以下命令编译前面的代码并查看输出:

g++ main.cpp -std=c++17
./a.out

程序的输出如下:

Size of vector is 5

Print vector elements before sorting
1     5     2     4     3

Print vector elements after sorting
1     2     3     4     5

代码演示

以下行声明了一个向量,并用五个元素初始化了向量:

vector<int> v = { 1, 5, 2, 4, 3 };

然而,向量还允许使用vector::push_back<data_type>( value ) API 将值附加到向量的末尾。sort()算法接受两个表示必须排序的数据范围的随机访问迭代器。由于向量内部使用内置的 C/C++数组,就像 STL 数组容器一样,向量也支持随机访问迭代器;因此,sort()函数是一个高效的算法,其运行时复杂度是对数的,即O(N log2 (N))

常用的向量 API

以下表格显示了一些常用的向量 API:

API描述
at ( int index )这返回索引位置存储的值。如果索引无效,则会抛出std::out_of_range异常。
operator [ int index ]这返回索引位置存储的值。它比at( int index )更快,因为该函数不执行边界检查。
front()这返回向量中存储的第一个值。
back() 这返回向量中存储的最后一个值。
empty()如果向量为空,则返回 true,否则返回 false。
size() 这返回向量中存储的值的数量。
reserve( int size ) 这保留向量的初始大小。当向量大小达到容量时,插入新值需要重新调整向量大小。这使得插入消耗*O(N)*的运行时复杂度。reserve()方法是对描述的问题的一种解决方法。
capacity()这返回向量的总容量,而大小是向量中实际存储的值。
clear() 这清除所有值。
push_back<data_type>( value )这在向量末尾添加一个新值。

使用istream_iteratorostream_iterator从/向向量读取和打印会非常有趣和方便。以下代码演示了向量的使用:

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

int main () {
    vector<int> v;

    cout << "\nType empty string to end the input once you are done feeding the vector" << endl;
    cout << "\nEnter some numbers to feed the vector ..." << endl;

    istream_iterator<int> start_input(cin);
    istream_iterator<int> end_input;

    copy ( start_input, end_input, back_inserter( v ) );

    cout << "\nPrint the vector ..." << endl;
    copy ( v.begin(), v.end(), ostream_iterator<int>(cout, "\t") );
    cout << endl;

    return 0;
}

请注意,程序的输出被跳过,因为输出取决于您输入的输入。请随时尝试在命令行上执行这些指令。

代码演示

基本上,复制算法接受一系列迭代器,其中前两个参数表示源,第三个参数表示目标,这恰好是向量:

istream_iterator<int> start_input(cin);
istream_iterator<int> end_input;

copy ( start_input, end_input, back_inserter( v ) );

start_input迭代器实例定义了一个从istreamcin接收输入的istream_iterator迭代器,而end_input迭代器实例定义了一个文件结束符,它默认为空字符串("")。因此,输入可以通过在命令行输入终端中键入""来终止。

同样,让我们了解以下代码片段:

cout << "\nPrint the vector ..." << endl;
copy ( v.begin(), v.end(), ostream_iterator<int>(cout, "\t") );
cout << endl;

复制算法用于将向量中的值逐个复制到ostream中,并用制表符(\t)分隔输出。

向量的陷阱

每个 STL 容器都有其优点和缺点。没有单个 STL 容器在所有情况下都表现更好。向量内部使用数组数据结构,而在 C/C++中数组的大小是固定的。因此,当您尝试在向量大小已经达到最大容量时向向量添加新值时,向量将分配新的连续位置,以容纳旧值和新值在一个连续的位置。然后开始将旧值复制到新位置。一旦所有数据元素都被复制,向量将使旧位置无效。

每当这种情况发生时,向量插入将需要*O(N)*的运行时复杂度。随着向量大小随时间增长,按需,*O(N)*的运行时复杂度将表现出相当糟糕的性能。如果你知道所需的最大大小,你可以预留足够的初始大小来克服这个问题。然而,并不是在所有情况下你都需要使用向量。当然,向量支持动态大小和随机访问,在某些情况下具有性能优势,但你可能真的不需要随机访问,这种情况下列表、双端队列或其他一些容器可能更适合你。

列表

列表 STL 容器在内部使用双向链表数据结构。因此,列表只支持顺序访问,在最坏的情况下,在列表中搜索随机值可能需要*O(N)的运行时复杂度。然而,如果你确定只需要顺序访问,列表确实提供了自己的好处。列表 STL 容器允许你以常数时间复杂度在最佳、平均和最坏的情况下在末尾、前面或中间插入数据元素,即O(1)*的运行时复杂度。

以下图片展示了列表 STL 使用的内部数据结构:

让我们编写一个简单的程序,亲身体验使用列表 STL:

#include <iostream>
#include <list>
#include <iterator>
#include <algorithm>
using namespace std;

int main () {

  list<int> l;

  for (int count=0; count<5; ++count)
    l.push_back( (count+1) * 100 );

  auto pos = l.begin();

  cout << "\nPrint the list ..." << endl;
  while ( pos != l.end() )
    cout << *pos++ << "-->";
  cout << " X" << endl;

  return 0;
}

我相信到现在为止你已经对 C++ STL 有了一些了解,它的优雅和强大。观察到语法在所有 STL 容器中保持不变是不是很酷?你可能已经注意到,无论你使用数组、向量还是列表,语法都保持不变。相信我,当你探索其他 STL 容器时,你会得到同样的印象。

话虽如此,前面的代码是不言自明的,因为我们在其他容器中做了差不多的事情。

让我们尝试对列表进行排序,如下所示的代码:

#include <iostream>
#include <list>
#include <iterator>
#include <algorithm>
using namespace std;

int main () {

    list<int> l = { 100, 20, 80, 50, 60, 5 };

    auto pos = l.begin();

    cout << "\nPrint the list before sorting ..." << endl;
    copy ( l.begin(), l.end(), ostream_iterator<int>( cout, "-->" ));
    cout << "X" << endl;

    l.sort();

    cout << "\nPrint the list after sorting ..." << endl;
    copy ( l.begin(), l.end(), ostream_iterator<int>( cout, "-->" ));
    cout << "X" << endl; 

    return 0;
}

你注意到了sort()方法吗?是的,列表容器有自己的排序算法。列表容器支持自己版本的排序算法的原因是,通用的sort()算法需要一个随机访问迭代器,而列表容器不支持随机访问。在这种情况下,相应的容器将提供自己的高效算法来克服这个缺点。

有趣的是,列表支持的sort算法的运行时复杂度是O(N log2 N)

列表中常用的 API

以下表格显示了 STL 列表的最常用 API:

API描述
front()返回列表中存储的第一个值
back() 返回列表中存储的最后一个值
size()返回列表中存储的值的数量
empty()当列表为空时返回true,否则返回false
clear()清除列表中存储的所有值
push_back<data_type>( value )这在列表末尾添加一个值
push_front<data_type>( value )在列表的前面添加一个值
merge( list )这将两个相同类型值的排序列表合并
reverse()这将列表反转
unique()从列表中删除重复的值
sort()这对存储在列表中的值进行排序

前向列表

STL 的forward_list容器建立在单向链表数据结构之上;因此,它只支持向前导航。由于forward_list在内存和运行时方面每个节点消耗一个更少的指针,因此与列表容器相比,它被认为更有效。然而,作为性能优势的额外边缘的代价,forward_list不得不放弃一些功能。

以下图表显示了forward_list中使用的内部数据结构:

让我们来探索以下示例代码:

#include <iostream>
#include <forward_list>
#include <iterator>
#include <algorithm>
using namespace std;

int main ( ) {

  forward_list<int> l = { 10, 10, 20, 30, 45, 45, 50 };

  cout << "\nlist with all values ..." << endl;
  copy ( l.begin(), l.end(), ostream_iterator<int>(cout, "\t") );

  cout << "\nSize of list with duplicates is " << distance( l.begin(), l.end() ) << endl;

  l.unique();

  cout << "\nSize of list without duplicates is " << distance( l.begin(), l.end() ) << endl;

  l.resize( distance( l.begin(), l.end() ) );

  cout << "\nlist after removing duplicates ..." << endl;
  copy ( l.begin(), l.end(), ostream_iterator<int>(cout, "\t") );
  cout << endl;

  return 0;

}

可以使用以下命令查看输出:

./a.out

输出如下:

list with all values ...
10    10    20    30    45    45    50
Size of list with duplicates is 7

Size of list without duplicates is 5

list after removing duplicates ...
10    20   30   45   50

代码演示

以下代码声明并初始化了forward_list容器,其中包含一些唯一值和一些重复值:

forward_list<int> l = { 10, 10, 20, 30, 45, 45, 50 };

由于forward_list容器不支持size()函数,我们使用distance()函数来找到列表的大小:

cout << "\nSize of list with duplicates is " << distance( l.begin(), l.end() ) << endl;

以下forward_list<int>::unique()函数删除重复的整数,保留唯一的值:

l.unique();

forward_list容器中常用的 API

下表显示了常用的forward_list API:

API描述
front()返回forward_list容器中存储的第一个值
empty()forward_list容器为空时返回 true,否则返回 false
clear()清除forward_list中存储的所有值
push_front<data_type>( value )forward_list的前面添加一个值
merge( list )将两个排序的forward_list容器合并为相同类型的值
reverse()反转forward_list容器
unique()forward_list容器中删除重复的值
sort()forward_list中存储的值进行排序

让我们再探索一个例子,以更好地理解forward_list容器:

#include <iostream>
#include <forward_list>
#include <iterator>
#include <algorithm>
using namespace std;

int main () {

    forward_list<int> list1 = { 10, 20, 10, 45, 45, 50, 25 };
    forward_list<int> list2 = { 20, 35, 27, 15, 100, 85, 12, 15 };

    cout << "\nFirst list before sorting ..." << endl;
    copy ( list1.begin(), list1.end(), ostream_iterator<int>(cout, "\t") );
    cout << endl; 

    cout << "\nSecond list before sorting ..." << endl;
    copy ( list2.begin(), list2.end(), ostream_iterator<int>(cout, "\t") );
    cout << endl;

    list1.sort();
    list2.sort();

    cout << "\nFirst list after sorting ..." << endl;
    copy ( list1.begin(), list1.end(), ostream_iterator<int>(cout, "\t") );
    cout << endl; 

    cout << "\nSecond list after sorting ..." << endl;
    copy ( list2.begin(), list2.end(), ostream_iterator<int>(cout, "\t") );
    cout << endl;    

    list1.merge ( list2 );

    cout << "\nMerged list ..." << endl;
    copy ( list1.begin(), list1.end(), ostream_iterator<int>(cout, "\t") );

    cout << "\nMerged list after removing duplicates ..." << endl;
    list1.unique(); 
    copy ( list1.begin(), list1.end(), ostream_iterator<int>(cout, "\t") );

    return 0;
}

前面的代码片段是一个有趣的例子,演示了sort()merge()unique() STL 算法的实际用法。

可以使用以下命令查看输出:

./a.out

程序的输出如下:

First list before sorting ...
10   20   10   45   45   50   25
Second list before sorting ...
20   35   27   15   100  85   12   15

First list after sorting ...
10   10   20   25   45   45   50
Second list after sorting ...
12   15   15   20   27   35   85   100

Merged list ...
10   10   12   15   15   20   20   25   27   35   45   45  50   85  100
Merged list after removing duplicates ...
10   12   15   20   25   27   35   45   50   85  100

输出和程序都很容易理解。

Deque

deque 容器是一个双端队列,其使用的数据结构可以是动态数组或向量。在 deque 中,可以在前面和后面插入元素,时间复杂度为O(1),而在向量中,插入元素在后面的时间复杂度为O(1),而在前面的时间复杂度为O(N)。deque 不会遭受向量遭受的重新分配问题。然而,deque 具有向量的所有优点,除了在性能方面稍微优于向量,因为每一行都有几行动态数组或向量。

以下图表显示了 deque 容器中使用的内部数据结构:

让我们编写一个简单的程序来尝试 deque 容器:

#include <iostream>
#include <deque>
#include <algorithm>
#include <iterator>
using namespace std;

int main () {
  deque<int> d = { 10, 20, 30, 40, 50 };

  cout << "\nInitial size of deque is " << d.size() << endl;

  d.push_back( 60 );
  d.push_front( 5 );

  cout << "\nSize of deque after push back and front is " << d.size() << endl;

  copy ( d.begin(), d.end(), ostream_iterator<int>( cout, "\t" ) );
  d.clear();

  cout << "\nSize of deque after clearing all values is " << d.size() <<
endl;

  cout << "\nIs the deque empty after clearing values ? " << ( d.empty()
? "true" : "false" ) << endl;

return 0;
}

可以使用以下命令查看输出:

./a.out

程序的输出如下:

Intitial size of deque is 5

Size of deque after push back and front is 7

Print the deque ...
5  10  20  30  40  50  60
Size of deque after clearing all values is 0

Is the deque empty after clearing values ? true

deque 中常用的 API

下表显示了常用的 deque API:

API描述
at ( int index )返回索引位置存储的值。如果索引无效,则抛出std::out_of_range异常。
operator [ int index ]返回索引位置存储的值。这个函数比at( int index )更快,因为它不执行边界检查。
front()返回 deque 中存储的第一个值。
back() 返回 deque 中存储的最后一个值。
empty()如果 deque 为空则返回true,否则返回false
size() 返回 deque 中存储的值的数量。
capacity()返回 deque 的总容量,而size()返回 deque 中实际存储的值的数量。
clear() 清除所有的值。
push_back<data_type>( value )在 deque 的末尾添加一个新值。

关联容器

关联容器以有序方式存储数据,与序列容器不同。因此,关联容器不会保留插入数据的顺序。关联容器在搜索值时非常高效,具有*O(log n)*的运行时复杂度。每次向容器添加新值时,如果需要,容器将重新排序内部存储的值。

STL 支持以下类型的关联容器:

  • 集合

  • 映射

  • 多重集合

  • 多重映射

  • 无序集合

  • 无序多重集合

  • 无序映射

  • 无序多重映射

关联容器将数据组织为键值对。数据将根据键进行排序,以实现随机和更快的访问。关联容器有两种类型:

  • 有序

  • 无序

以下关联容器属于有序容器,因为它们按特定顺序/排序。有序关联容器通常使用某种形式的二叉搜索树BST);通常使用红黑树来存储数据:

  • 集合

  • 映射

  • 多重集合

  • 多重映射

以下关联容器属于无序容器,因为它们没有按特定顺序排序,并且它们使用哈希表:

  • 无序集合

  • 无序映射

  • 无序多重集合

  • 无序多重映射

让我们在以下子节中通过示例了解先前提到的容器。

集合

集合容器仅以有序方式存储唯一值。集合使用值作为键来组织值。集合容器是不可变的,也就是说,存储在集合中的值不能被修改;但是可以删除值。集合通常使用红黑树数据结构,这是一种平衡二叉搜索树。集合操作的时间复杂度保证为O(log N)

让我们使用一个集合编写一个简单的程序:

#include <iostream>
#include <set>
#include <vector>
#include <iterator>
#include <algorithm>
using namespace std;

int main( ) {
    set<int> s1 = { 1, 3, 5, 7, 9 };
    set<int> s2 = { 2, 3, 7, 8, 10 };

    vector<int> v( s1.size() + s2.size() );

    cout << "\nFirst set values are ..." << endl;
    copy ( s1.begin(), s1.end(), ostream_iterator<int> ( cout, "\t" ) );
    cout << endl;

    cout << "\nSecond set values are ..." << endl;
    copy ( s2.begin(), s2.end(), ostream_iterator<int> ( cout, "\t" ) );
    cout << endl;

    auto pos = set_difference ( s1.begin(), s1.end(), s2.begin(), s2.end(), v.begin() ); 
    v.resize ( pos - v.begin() );

    cout << "\nValues present in set one but not in set two are ..." << endl;
    copy ( v.begin(), v.end(), ostream_iterator<int> ( cout, "\t" ) );
    cout << endl; 

    v.clear();

    v.resize ( s1.size() + s2.size() );

    pos = set_union ( s1.begin(), s1.end(), s2.begin(), s2.end(), v.begin() );

    v.resize ( pos - v.begin() );

    cout << "\nMerged set values in vector are ..." << endl;
    copy ( v.begin(), v.end(), ostream_iterator<int> ( cout, "\t" ) );
    cout << endl; 

    return 0;
}

可以使用以下命令查看输出:

./a.out

程序的输出如下:

First set values are ...
1   3   5   7   9

Second set values are ...
2   3   7   8   10

Values present in set one but not in set two are ...
1   5   9

Merged values of first and second set are ...
1   2   3   5   7   8   9  10

代码演示

以下代码声明并初始化两个集合s1s2

set<int> s1 = { 1, 3, 5, 7, 9 };
set<int> s2 = { 2, 3, 7, 8, 10 };

以下行将确保向量有足够的空间来存储结果向量中的值:

vector<int> v( s1.size() + s2.size() );

以下代码将打印s1s2中的值:

cout << "\nFirst set values are ..." << endl;
copy ( s1.begin(), s1.end(), ostream_iterator<int> ( cout, "\t" ) );
cout << endl;

cout << "\nSecond set values are ..." << endl;
copy ( s2.begin(), s2.end(), ostream_iterator<int> ( cout, "\t" ) );
cout << endl;

set_difference()算法将向量v填充为仅存在于集合s1中而不在s2中的值。迭代器pos将指向向量中的最后一个元素;因此,向量resize将确保向量中的额外空间被移除:

auto pos = set_difference ( s1.begin(), s1.end(), s2.begin(), s2.end(), v.begin() ); 
v.resize ( pos - v.begin() );

以下代码将打印填充到向量v中的值:

cout << "\nValues present in set one but not in set two are ..." << endl;
copy ( v.begin(), v.end(), ostream_iterator<int> ( cout, "\t" ) );
cout << endl;

set_union()算法将合并集合s1s2的内容到向量中,然后向量被调整大小以适应合并后的值:

pos = set_union ( s1.begin(), s1.end(), s2.begin(), s2.end(), v.begin() );
v.resize ( pos - v.begin() );

以下代码将打印填充到向量v中的合并值:

cout << "\nMerged values of first and second set are ..." << endl;
copy ( v.begin(), v.end(), ostream_iterator<int> ( cout, "\t" ) );
cout << endl;

集合中常用的 API

以下表格描述了常用的集合 API:

API描述
insert( value )这将值插入到集合中
clear()这将清除集合中的所有值
size()这将返回集合中存在的条目总数
empty()如果集合为空,则打印true,否则返回false
find()这将查找具有指定键的元素并返回迭代器位置
equal_range()这将返回与特定键匹配的元素范围
lower_bound()这将返回指向第一个不小于给定键的元素的迭代器
upper_bound()这将返回指向第一个大于给定键的元素的迭代器

映射

映射按键组织值。与集合不同,映射每个值都有一个专用键。映射通常使用红黑树作为内部数据结构,这是一种平衡的 BST,可以保证在映射中搜索或定位值的*O(log N)*运行效率。映射中存储的值根据键使用红黑树进行排序。映射中使用的键必须是唯一的。映射不会保留输入的顺序,因为它根据键重新组织值,也就是说,红黑树将被旋转以平衡红黑树高度。

让我们编写一个简单的程序来理解映射的用法:

#include <iostream>
#include <map>
#include <iterator>
#include <algorithm>
using namespace std;

int main ( ) {

  map<string, long> contacts;

  contacts["Jegan"] = 123456789;
  contacts["Meena"] = 523456289;
  contacts["Nitesh"] = 623856729;
  contacts["Sriram"] = 993456789;

  auto pos = contacts.find( "Sriram" );

  if ( pos != contacts.end() )
    cout << pos->second << endl;

  return 0;
}

让我们编译并检查程序的输出:

g++ main.cpp -std=c++17
./a.out

输出如下:

Mobile number of Sriram is 8901122334

代码漫步

以下行声明了一个映射,其中string名称作为键,long移动号码作为映射中存储的值:

map< string, long > contacts;

以下代码片段按名称添加了四个联系人:

 contacts[ "Jegan" ] = 1234567890;
 contacts[ "Meena" ] = 5784433221;
 contacts[ "Nitesh" ] = 4567891234;
 contacts[ "Sriram" ] = 8901122334;

以下行将尝试在联系人映射中定位名称为Sriram的联系人;如果找到Sriram,则find()函数将返回指向键值对位置的迭代器;否则返回contacts.end()位置:

 auto pos = contacts.find( "Sriram" );

以下代码验证了迭代器pos是否已经到达contacts.end()并打印联系人号码。由于映射是一个关联容器,它存储一个key=>value对;因此,pos->first表示键,pos->second表示值:

 if ( pos != contacts.end() )
 cout << "\nMobile number of " << pos->first << " is " << pos->second << endl;
 else
 cout << "\nContact not found." << endl;

映射中常用的 API

以下表显示了常用的映射 API:

API描述
at ( key )如果找到键,则返回相应键的值;否则抛出std::out_of_range异常
operator[ key ]如果找到键,则更新相应键的现有值;否则,将添加一个具有提供的key=>value的新条目
empty()如果映射为空,则返回true,否则返回false
size()返回映射中存储的key=>value对的数量
clear()清除映射中存储的条目
count()返回与给定键匹配的元素数
find()查找具有指定键的元素

Multiset

多重集合容器的工作方式与集合容器类似,只是集合只允许存储唯一值,而多重集合允许存储重复值。在集合和多重集合容器的情况下,值本身被用作键来组织数据。多重集合容器就像集合一样;它不允许修改多重集合中存储的值。

让我们使用多重集合编写一个简单的程序:

#include <iostream>
#include <set>
#include <iterator>
#include <algorithm>
using namespace std;

int main() {
  multiset<int> s = { 10, 30, 10, 50, 70, 90 };

  cout << "\nMultiset values are ..." << endl;

  copy ( s.begin(), s.end(), ostream_iterator<int> ( cout, "\t" ) );
  cout << endl;

  return 0;
}

可以使用以下命令查看输出:

./a.out

程序的输出如下:

Multiset values are ...
10 30 10 50 70 90

有趣的是,在前面的输出中,您可以看到 multiset 包含重复的值。

Multimap

多重映射的工作方式与映射完全相同,只是多重映射容器允许使用相同的键存储多个值。

让我们用一个简单的例子来探索 multimap 容器:

#include <iostream>
#include <map>
#include <vector>
#include <iterator>
#include <algorithm>
using namespace std;

int main() {
  multimap< string, long > contacts = {
    { "Jegan", 2232342343 },
    { "Meena", 3243435343 },
    { "Nitesh", 6234324343 },
    { "Sriram", 8932443241 },
    { "Nitesh", 5534327346 }
  };

  auto pos = contacts.find ( "Nitesh" );
  int count = contacts.count( "Nitesh" );
  int index = 0;

  while ( pos != contacts.end() ) {
    cout << "\nMobile number of " << pos->first << " is " <<
    pos->second << endl;
    ++index;
    if ( index == count )
      break;
  }

  return 0;
}

可以编译程序并使用以下命令查看输出:

g++ main.cpp -std=c++17

./a.out

程序的输出如下:

Mobile number of Nitesh is 6234324343
Mobile number of Nitesh is 5534327346

无序集合

无序集合的工作方式类似于集合,只是这些容器的内部行为不同。集合使用红黑树,而无序集合使用哈希表。集合操作的时间复杂度为O(log N),而无序集合操作的时间复杂度为O(1);因此,无序集合往往比集合更快。

无序集合中存储的值没有特定的顺序,不像集合那样以排序的方式存储值。如果性能是标准,那么无序集合是一个不错的选择;然而,如果需要以排序的方式迭代值,那么集合是一个不错的选择。

无序映射

无序映射的工作方式类似于映射,只是这些容器的内部行为不同。映射使用红黑树,而无序映射使用哈希表。映射操作的时间复杂度为O(log N),而无序映射操作的时间复杂度为O(1);因此,无序映射比映射更快。

无序映射中存储的值没有特定的顺序,不像映射中的值按键排序。

无序多重集

无序多重集的工作方式类似于多重集,只是这些容器的内部行为不同。多重集使用红黑树,而无序多重集使用哈希表。多重集操作的时间复杂度为O(log N),而无序多重集操作的时间复杂度为O(1)。因此,无序多重集比多重集更快。

无序多重集中存储的值没有特定的顺序,不像多重集中的值以排序的方式存储。如果性能是标准,无序多重集是一个不错的选择;然而,如果需要以排序的方式迭代值,那么多重集是一个不错的选择。

无序多重映射

无序多重映射的工作方式类似于多重映射,只是这些容器的内部行为不同。多重映射使用红黑树,而无序多重映射使用哈希表。多重映射操作的时间复杂度为O(log N),而无序多重映射操作的时间复杂度为O(1);因此,无序多重映射比多重映射更快。

无序多重映射中存储的值没有特定的顺序,不像多重映射中的值按键排序。如果性能是标准,那么无序多重映射是一个不错的选择;然而,如果需要以排序的方式迭代值,那么多重映射是一个不错的选择。

容器适配器

容器适配器将现有容器适配为新容器。简单来说,STL 扩展是通过组合而不是继承完成的。

STL 容器不能通过继承进行扩展,因为它们的构造函数不是虚拟的。在整个 STL 中,您可以观察到静态多态性在操作符重载和模板方面都得到了使用,而出于性能原因,动态多态性被有意地避免使用。因此,通过对现有容器进行子类化来扩展 STL 不是一个好主意,因为这会导致内存泄漏,因为容器类不是设计成像基类一样行为的。

STL 支持以下容器适配器:

  • 队列

  • 优先队列

让我们在以下小节中探索容器适配器。

栈不是一个新的容器;它是一个模板适配器类。适配器容器包装现有容器并提供高级功能。栈适配器容器提供栈操作,同时隐藏对栈不相关的不必要功能。STL 栈默认使用 deque 容器;然而,我们可以在栈实例化期间指示栈使用满足栈要求的任何现有容器。

双端队列、列表和向量满足栈适配器的要求。

栈遵循后进先出LIFO)的原则。

栈中常用的 API

以下表格显示了常用的栈 API:

API描述
top()返回栈中的最顶部值,即最后添加的值
push<data_type>( value )这将把提供的值推到栈顶
pop()这将从栈中移除最顶部的值
size()返回栈中存在的值的数量
empty()如果栈为空则返回true;否则返回false

现在是时候动手写一个简单的程序来使用栈了:

#include <iostream>
#include <stack>
#include <iterator>
#include <algorithm>
using namespace std;

int main ( ) {

  stack<string> spoken_languages;

  spoken_languages.push ( "French" );
  spoken_languages.push ( "German" );
  spoken_languages.push ( "English" );
  spoken_languages.push ( "Hindi" );
  spoken_languages.push ( "Sanskrit" );
  spoken_languages.push ( "Tamil" );

  cout << "\nValues in Stack are ..." << endl;
  while ( ! spoken_languages.empty() ) {
              cout << spoken_languages.top() << endl;
        spoken_languages.pop();
  }
  cout << endl;

  return 0;
}

程序可以通过以下命令编译,并查看输出:

g++ main.cpp -std=c++17

./a.out

程序的输出如下:

Values in Stack are ...
Tamil
Kannada
Telugu
Sanskrit
Hindi
English
German
French

从前面的输出中,我们可以看到栈的 LIFO 行为。

队列

队列基于先进先出FIFO)原则工作。队列不是一个新的容器;它是一个模板化的适配器类,包装现有容器并提供队列操作所需的高级功能,同时隐藏对队列无关的不必要功能。STL 队列默认使用 deque 容器;但是,在队列实例化期间,我们可以指示队列使用满足队列要求的任何现有容器。

在队列中,新值可以添加到后面,并从前面移除。双端队列、列表和向量满足队列适配器的要求。

队列中常用的 API

以下表格显示了常用的队列 API:

API 描述
push() 在队列的后面添加一个新值
pop() 移除队列前面的值
front() 返回队列前面的值
back() 返回队列的后面的值
empty() 当队列为空时返回true;否则返回false
size() 返回队列中存储的值的数量

让我们在以下程序中使用队列:

#include <iostream>
#include <queue>
#include <iterator>
#include <algorithm>
using namespace std;

int main () {
  queue<int> q;

  q.push ( 100 );
  q.push ( 200 );
  q.push ( 300 );

  cout << "\nValues in Queue are ..." << endl;
  while ( ! q.empty() ) {
    cout << q.front() << endl;
    q.pop();
  }

  return 0;
}

程序可以通过以下命令编译,并查看输出:

g++ main.cpp -std=c++17

./a.out

程序的输出如下:

Values in Queue are ...
100
200
300

从前面的输出中,您可以观察到值以与它们被推入的相同顺序弹出,即 FIFO。

优先队列

优先队列不是一个新的容器;它是一个模板化的适配器类,包装现有容器并提供优先队列操作所需的高级功能,同时隐藏对优先队列无关的不必要功能。优先队列默认使用 vector 容器;但是,deque 容器也满足优先队列的要求。因此,在优先队列实例化期间,您可以指示优先队列也使用 deque。

优先队列以使最高优先级值首先出现的方式组织数据;换句话说,值按降序排序。

deque 和 vector 满足优先队列适配器的要求。

优先队列中常用的 API

以下表格显示了常用的优先队列 API:

API 描述
push() 在优先队列的后面添加一个新值
pop() 移除优先队列前面的值
empty() 当优先队列为空时返回true;否则返回false
size() 返回优先队列中存储的值的数量
top() 返回优先队列前面的值

让我们编写一个简单的程序来理解priority_queue

#include <iostream>
#include <queue>
#include <iterator>
#include <algorithm>
using namespace std;

int main () {
  priority_queue<int> q;

  q.push( 100 );
  q.push( 50 );
  q.push( 1000 );
  q.push( 800 );
  q.push( 300 );

  cout << "\nSequence in which value are inserted are ..." << endl;
  cout << "100\t50\t1000\t800\t300" << endl;
  cout << "Priority queue values are ..." << endl;

  while ( ! q.empty() ) {
    cout << q.top() << "\t";
    q.pop();
  }
  cout << endl;

  return 0;
}

程序可以通过以下命令编译,并查看输出:

g++ main.cpp -std=c++17

./a.out

程序的输出如下:

Sequence in which value are inserted are ...
100   50   1000  800   300

Priority queue values are ...
1000  800   300   100   50

从前面的输出中,您可以观察到priority_queue是一种特殊类型的队列,它重新排列输入,使最高值首先出现。

总结

在本章中,您学习了现成的通用容器、函数对象、迭代器和算法。您还学习了集合、映射、多重集和多重映射关联容器,它们的内部数据结构,以及可以应用于它们的常见算法。此外,您还学习了如何使用各种容器,并进行了实际的代码示例。

在下一章中,您将学习模板编程,这将帮助您掌握模板的基本知识。