C++学习笔记(三)

136 阅读50分钟

宏和模板

使用宏避免多次包含

通常在 .h 文件(头文件)中声明类和函数,并在 .cpp 文件中定义函数,因此需要在 .cpp 文件中使用预处理器编译指令 #include <header> 来包含头文件。

如果头文件 class2.h 中声明了一个类,而 class1.h 中有该类的成员,则 class1.h 需要包含 class2.h;如果 class2.中也有 class1.h 中声明的类的成员,则 class2.h 也需要包含 class1.h。如果头文件 class1.h 和 class2.h 互相包含,在预处理器看来,会导致递归问题。为了避免这种问题,可结合使用宏以及预处理器编译指令 #ifndef 和 #endif。

包含 header2.h 的 header1.h 类似于下面这样:

#ifndef HEADER1_H _//multiple inclusion guard:
#define HEADER1_H_ // preprocessor will read this and following lines once
#include <header2.h>

class Class1
{
    // class members
};
#endif // end of header1.h

header2.h 与此类似,但宏定义不同,且包含的是<header1.h>:

#ifndef HEADER2_H_//multiple inclusion guard
#define HEADER2_H_
#include <header1.h>

class Class2
{
    // class members
};
#endif // end of header2.h

#ifndef 可读作 if-not-defined,是一个条件处理命令,让预处理器仅在标识符未定义时才继续。 #endif 告诉预处理器,条件处理指令到此结束。

预处理器首次处理 header1.h,遇到 #ifndef 时,发现宏 HEADER1_H_还未定义,则继续往下处理。当再次处理 header1.h 时,遇到 #ifndef 时,其条件为 false,则直接走 #endif,处理结束。

从 C++ 11 开始可以使用 #pragma once 实现相同的效果。

使用 assert 宏验证表达式

编写程序后,立即单步执行以测试每条代码路径很不错,但对大型应用程序来说可能不现实。比较现实的做法是,插入检查语句,对表达式或变量的值进行验证。

assert 宏让您能够完成这项任务。要使用 assert 宏,需要包含<assert.h>。下面是一个示例,它使用 assert() 来验证指针的值:

#include <assert.h>

int main()
{
    char* sayHello = new char [25];
    assert(sayHello != NULL); // throws a message if pointer is NULL

    // other code
    delete [] sayHello;
    return 0;
}

如果将 sayHello 初始化为 NULL, 并在调试模式下执行,Visual Studio 将弹出如下所示的窗口:

image.png

在 Microsoft Visual Studio 中,assert() 让你能够单击 Retry 按钮返回应用程序,而调用栈将指出哪行代码没有通过断言测试。这让 assert() 成为一项方便的调试功能。例如,可使用 assert 对函数的输入参数进行验证。长期而言,assert 有助于改善代码的质量,强烈推荐使用它。

在大多数开发环境中,assert() 通常在发布模式下被禁用,因此它仅在调试模式下显示错误消息。另外,在有些开发环境中,assert() 被实现为函数,而不是宏。

模板

模板函数

假设要编写一个函数,它适用于不同类型的参数,可以使用模板函数。模板函数的声明格式如下:

template <typename objType>
const objType& TemplateFunctionx(const objType& value1, const objType& value2);

关键字 template 标志着模板声明的开始,接下来是模板参数列表。该参数列表包含关键字 typename,它定义了模板参数 objType,objType 是一个占位符,针对对象实例化模板时,将使用对象的类型替换它。

下面是一个模板函数 GetMax,它返回两个参数中较大的一个:

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

template <typename Type>
const Type& GetMax(const Type& value1, const Type& value2)
{
	if (value1 > value2)
		return value1;
	else
		return value2;
}

template <typename Type>
void DisplayComparison(const Type& value1, const Type& value2)
{
	cout << "GetMax(" << value1 << ", " << value2 << ") = ";
	cout << GetMax(value1, value2) << endl;
}

int main()
{
	int num1 = -101, num2 = 2011;
	DisplayComparison(num1, num2);
	
	double d1 = 3.14, d2 = 3.1416;
	DisplayComparison(d1, d2);
	
	string name1("Jack"), name2("John");
	DisplayComparison(name1, name2);
	
	return 0;
}

输出如下:

GetMax(-101, 2011) = 2011
GetMax(3.14, 3.1416) = 3.1416
GetMax(Jack, John) = John

可以看到,同一个模板函数可以用于不同类型的数据:int、double 和 std::string。模板函数不仅可以重用(就像宏函数一样),而且更容易编写和维护,还是类型安全的。

调用 DisplayComparison 时,也可显式地指定类型,比如第 30 行可以改成这样:

DisplayComparison<string>(name1, name2);

但是没有必要,因为编译器能够自动推断出类型;但使用模板类时需要显示地指定。

模板函数是类型安全的,这意味着不能像下面这样调用:

DisplayComparison(num1, name1);

这种调用将导致编译错误。

模板类

下面是一个简单的模板类,它只有一个模板参数 T,用于存储一个成员变量:

template <typename T>
class HoldVarTypeT
{
private:
    T value;

public:
    void SetValue (const T& newValue) { value = newValue; }
    T& GetValue() {return value;}
};

使用该模板类时,需要给 T 指定类型:

HoldVarTypeT <int> holdInt; // template instantiation for int
holdInt.SetValue(5);
cout << "The value stored is: " << holdInt.GetValue() << endl;

你甚至可以用 Human 类来实例化这个模板:

HoldVarTypeT<Human> holdHuman;
holdHuman.SetValue(firstMan);
holdHuman.GetValue().IntroduceSelf();

还可以指定模板参数的默认类型,代码如下:

#include <iostream> 
using namespace std;

// template with default params: int & double 
template <typename T1 = int, typename T2 = double>
class HoldsPair
{
private:
	T1 value1;
	T2 value2;
 public:
	HoldsPair(const T1& val1, const T2& val2) // constructor 
	    : value1(val1), value2(val2) {}
	
	// Accessor functions 
	const T1& GetFirstValue() const
	{
		return value1;
	}
	
	const T2& GetSecondValue() const
	{
		return value2;
	}
};

int main()
{
	HoldsPair<> pairIntDbl(300, 10.09);
	HoldsPair<short, const char*>pairShortStr(25, "Learn templates, love C++"); 

	cout << "The first object contains -" << endl;
	cout << "Value 1: " << pairIntDbl.GetFirstValue() << endl;
	cout << "Value 2: " << pairIntDbl.GetSecondValue() << endl;
	
	cout << "The second object contains -" << endl;
	cout << "Value 1: " << pairShortStr.GetFirstValue() << endl;
	cout << "Value 2: " << pairShortStr.GetSecondValue() << endl;
	
	return 0;
}

打印如下:

The first object contains -
Value 1: 300
Value 2: 10.09
The second object contains -
Value 1: 25
Value 2: Learn templates, love C++

第一次实例化模板类的时候没有显示地指定参数类型,编译器使用了默认类型。

模板的实例化和具体化

具体化是指:指定模板参数是某种特定的类型,这样编译器就会用这个特定类型的参数来实例化模板类。下面是一个模板具体化的示例:

#include <iostream>
using namespace std;

template <typename T1 = int, typename T2 = double>
class HoldsPair
{
private:
	T1 value1;
	T2 value2;
public:
	HoldsPair(const T1& val1, const T2& val2) // constructor 
		 : value1(val1), value2(val2) {}

	// Accessor functions 
	const T1 & GetFirstValue() const;
	const T2& GetSecondValue() const;
};

// specialization of HoldsPair for types int & int here 
template<> class HoldsPair<int, int>
{
private:
	int value1;
	int value2;
	string strFun;
public:
	HoldsPair(const int& val1, const int& val2) // constructor 
		: value1(val1), value2(val2) {}
	
	const int & GetFirstValue() const
	{
		cout << "Returning integer " << value1 << endl;
		return value1;
	}
};

int main()
{
	HoldsPair<int, int> pairIntInt(222, 333);
	pairIntInt.GetFirstValue();
	
	return 0;
}

这段代码跟上一段是有很大差别的,第 20~35 行对模板类进行了具体化,在第 25 行还另外声明了一个字符串成员,这个模板类甚至都没有提供函数 GetFirstValue()和 GetSecondValue() 的实现,但程序依然能够通过编译。这是因为编译器只需考虑针对 <int, int> 的模板的具体化,而在这个具体化中,我们提供了模板类的具体实现。

模板类和静态成员

如果将类成员声明为静态的,该成员将由类的所有实例共享。模板类的静态成员与此类似,但不同的是模板类的成员有可能具体化成 int,也有可能具体化成 double,这时候模板类的静态成员会分别由这两种具体化类型的实例各自共享。

看下面的代码:

#include <iostream> 
using namespace std;

template <typename T>
class TestStatic
{
public:
	static int staticVal;
};

// static member initialization 
template<typename T> int TestStatic<T>::staticVal;

int main()
{
	TestStatic<int> intInstance1;
	intInstance1.staticVal = 2011;

	TestStatic<int> intInstance2;
	intInstance2.staticVal = 2012;
	
	TestStatic<double> dblnstance1;
	dblnstance1.staticVal = 1011;

	TestStatic<double> dblnstance2;
	dblnstance2.staticVal = 1012;
	
	cout << "intInstance1.staticVal = " << intInstance1.staticVal << endl;
	cout << "intInstance2.staticVal = " << intInstance2.staticVal << endl;
	cout << "dblnstance1.staticVal = " << dblnstance1.staticVal << endl;
	cout << "dblnstance2.staticVal = " << dblnstance2.staticVal << endl;
	
	return 0;
}

打印如下:

intInstance1.staticVal = 2012
intInstance2.staticVal = 2012
dblnstance1.staticVal = 1012
dblnstance2.staticVal = 1012

通过打印可以发现,具体化为 int 的两个实例中的静态成员是共享的,但是具体化为 int 和具体化为 double 的两个实例中的静态成员不是共享的。

这里第 12 行不可或缺,它用于初始化模板类的静态成员:

template<typename T> int TestStatic<T>::staticVal;

对于模板类的静态成员,通用的初始化语法如下:

template<template parameters> StaticType
ClassName<Template Arguments>::StaticVarName;

参数数量可变的模板

参数数量可变的模板是 2014 年发布的 C++14 新增的,如下是一个可以计算任意个参数的和的代码:

#include <iostream>
using namespace std;

template <typename Res, typename ValType>
void Sum(Res& result, ValType& val)
{
	result = result + val;
}

template <typename Res, typename First, typename... Rest>
void Sum(Res& result, First val1, Rest... valN)
{
	result = result + val1;
	return Sum(result, valN ...);
}

int main()
{
	double dResult = 0;
	Sum(dResult, 3.14, 4.56, 1.1111);
	cout << "dResult = " << dResult << endl;
	
	string strResult;
	Sum(strResult, "Hello ", "World");
	cout << "strResult = " << strResult.c_str() << endl;
	
	return 0;
}

运行后打印如下:

dResult = 8.8111
strResult = Hello World

可以看到,使用参数数量可变的模板定义的函数 Sum() 不仅能够处理不同类型的参数,还能够处理不同数量的参数。编译期间,编译器将根据调用 Sum() 的情况创建正确的代码,并反复处理提供的参数,直到将所有的参数都处理完毕。

在 C++中,模板中的省略号告诉编译器,默认类或模板函数可接受任意数量的模板参数,且这些参数可为任何类型。

C++14 提供了一个运算符,可用于确定调用参数数量可变的模板时,提供了多少个模板参数。在上面的代码中,可像下面这样在函数 Sum()中使用这个运算符:

int arrNums[sizeof...(Rest)];
// length of array evaluated using sizeof...() at compile time

千万不要将 sizeof…() 和 sizeof(Type) 混为一谈。后者返回类型的长度,而前者指出向参数数量可变的模板传递了多少个参数。

使用 static_assert 执行编译阶段检查

static_assert 是 C++11 新增的一项功能,让您能够在不满足指定条件时禁止编译。这好像不可思议,但对模板类来说很有用。例如,您可能想禁止针对 int 实例化模板类,为此可使用 static_assert,它是一种编译阶段断言,可用于在开发环境(或控制台中)显示一条自定义消息:

static_assert(expression being validated, "Error message when check fails");

要禁止针对类型 int 实例化模板类,可使用 static_assert( ),并将 sizeof(T) 与 sizeof(int) 进行比较,如果它们相等,就显示一条错误消息:

static_assert(sizeof(T) != sizeof(int), "No int please!");

代码如下:

template <typename T>

class EverythingButInt
{
public:
    EverythingButInt()
    {
        static_assert(sizeof(T) != sizeof(int), "No int please!");
    }
 };

int main()
{
    EverythingButInt<int> test; // template instantiation with int.
    return 0;
}

运行后没有输出,因为这个程序不能通过编译,它显示一条错误消息,指出您指定的类型不正确:

error: No int please!

标准模板库

简单地说,标准模板库(STL)是一组模板类和函数,向程序员提供了:

  • 用于存储信息的容器;
  • 用于访问容器存储的信息的迭代器;
  • 用于操作容器内容的算法。

STL容器

STL 顺序容器如下所示:

  • std::vector:操作与动态数组一样,在最后插入数据。
  • std::deque:与 std::vector 类似,但允许在开头插入或删除元素。
  • std::list:操作与双向链表一样。可将它视为链条,对象被连接在一起,您可在任何位置添加或删除对象。
  • std::forward_list:类似于 std::list,但是单向链表,只能沿一个方向遍历。

关联容器按指定的顺序存储数据,就像词典一样。这将降低插入数据的速度,但在查询方面有很大的优势。

STL 提供的关联容器如下所示:

  • std::set:存储各不相同的值,在插入时进行排序;容器的复杂度为对数。
  • std::unordered_set:存储各不相同的值,在插入时进行排序;容器的复杂度为常数。这种容器是 C++11 新增的。
  • std::map:存储键-值对,并根据唯一的键排序;容器的复杂度为对数。
  • std::unordered_map:存储键-值对,并根据唯一的键排序;容器的复杂度为对数。这种容器是 C++11 新增的。
  • std::multiset:与 set 类似,但允许存储多个值相同的项,即值不需要是唯一的。
  • std::unordered_multiset:与 unordered_set 类似,但允许存储多个值相同的项,即值不需要是唯 一的。这种容器是 C++11 新增的。
  • std::multimap:与 map 类似,但不要求键是唯一的。
  • std::unordered_multimap:与 unordered_map 类似,但不要求键是唯一的。这种容器是 C++11 新增的。

复杂度是一种指标,指出了容器的性能与其包含的元素个数之间的关系。复杂度为常量时,意思是说这种容器的性能不受其包含的元素个数的影响。换句话说,在这种容器包含 1000 个元素和 1000000 个元素时,处理时间相同。复杂度为对数,则表示性能与元素个数的对数成反比。换句话说,这种容器包含 1000000 个元素时,处理时间为包含 1000 个元素时的两倍。线性复杂度意味着性能与元素个数成反比。换而言之,这种容器包含 1000000 个元素时,处理时间为包含 1000 个元素时的 1000 倍。对于给定的容器,复杂度可能随要执行的操作而异。也就是说,插入元素的复杂度可能为常量,而搜索复杂度为线性。

容器适配器(Container Adapter)是顺序容器和关联容器的变种,其功能有限,用于满足特定的需求。主要的适配器类如下所示:

  • std::stack:以 LIFO(后进先出)的方式存储元素,让您能够在栈顶插入(压入)和删除(弹出)元素。
  • std::queue:以 FIFO(先进先出)的方式存储元素,让您能够删除最先插入的元素。
  • std::priority_queue:以特定顺序存储元素,因为优先级最高的元素总是位于队列开头。

STL 迭代器

STL 中的迭代器是模板类,从某种程度上说,它们是泛型指针。这些模板类让程序员能够对 STL 容器进行操作。注意,操作也可以是以模板函数的方式提供的 STL 算法,迭代器是一座桥梁,让这些模板函数能够以一致而无缝的方式处理容器,而容器是模板类。

STL 提供的迭代器分两大类。

  • 输入迭代器:通过对输入迭代器解除引用,它将引用对象,而对象可能位于集合中。最严格的输入迭代器确保只能以只读的方式访问对象。
  • 输出迭代器:输出迭代器让程序员对集合执行写入操作。最严格的输出迭代器确保只能执行写入操作。 前向迭代器:这是输入迭代器和输出迭代器的一种细化,它允许输入与输出。

上述两种基本迭代器可进一步分为三类。

  • 前向迭代器可以是 const 的,只能读取它指向的对象;也可以改变对象,即可读写对象。前向迭代器通常用于单向链表。
  • 双向迭代器:这是前向迭代器的一种细化,可对其执行递减操作,从而向后移动。双向迭代器通常用于双向链表。
  • 随机访问迭代器:这是对双向迭代器的一种细化,可将其加减一个偏移量,还可将两个迭代器相减以得到集合中两个元素的相对距离。随机访问迭代器通常用于数组。

从实现层面说,可将“细化”视为继承或具体化。

STL 算法

查找、排序和反转等都是标准的编程需求,不应让程序员重复实现这样的功能。因此 STL 以 STL 算法的方式提供这些函数,通过结合使用这些函数和迭代器,程序员可对容器执行一些最常见的操作。

最常用的 STL 算法如下所示。

  • std::find:在集合中查找值。
  • std::find_if:根据用户指定的谓词在集合中查找值。
  • std::reverse:反转集合中元素的排列顺序。
  • std::remove_if:根据用户定义的谓词将元素从集合中删除。
  • std::transform:使用用户定义的变换函数对容器中的元素进行变换。

这些算法都是 std 命名空间中的模板函数,要使用它们,必须包含标准头文件 <algorithm>。

下面通过一个示例阐述迭代器如何无缝地将容器和 STL 算法连接起来:

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

int main()
{
	// A dynamic array of integers 
	vector <int> intArray;
	
	// Insert sample integers into the array 
	intArray.push_back(50);
	intArray.push_back(2991);
	intArray.push_back(23);
	intArray.push_back(9999);
	
	cout << "The contents of the vector are: " << endl;
	
	// Walk the vector and read values using an iterator 
	vector <int>::iterator arrIterator = intArray.begin();
	
	while (arrIterator != intArray.end())
	{
		// Write the value to the screen 
		cout << *arrIterator << endl;
		
		// Increment the iterator to access the next element 
		 ++arrIterator;
	}

	// Find an element (say 2991) using the 'find' algorithm 
	vector <int>::iterator elFound = find(intArray.begin(), intArray.end(), 2991);

	// Check if value was found 
	if (elFound != intArray.end())
	{
		// Determine position of element using std::distance 
		int elPos = distance(intArray.begin(), elFound);
		cout << "Value " << *elFound;
		cout << " found in the vector at position: " << elPos << endl;
	}

	return 0;
}

打印如下:

The contents of the vector are:
50
2991
23
9999
Value 2991 found in the vector at position: 1

第 20 行声明了迭代器对象 arrIterator,并将其为指向容器开头。第 22~29 行演示了如何在循环中使用该迭代器遍历 vector,跟遍历静态数组非常像。迭代器的用法在所有 STL 容器中都相同。所有容器都提供了 begin() 函数和 end() 函数,其中前者指向第一个元素,后者指向容器中最后一个元素的后面。这就是第 22 行的 while 循环在 end()前面而不是 end() 处结束的原因。第 32 行演示了如何使用 find 在 vector 中查找值。find 操作的结果也是一个迭代器,通过将该迭代器与容器末尾进行比较,可判断 find 是否成功,如第 35 行所示。如果找到了元素,便可对该迭代器解除引用(就像对指针解除引用一样)以显示该元素。算法 distance 计算找到的元素的所处位置的偏移量。

如果将上面代码中所有的 vector 都替换为 deque,代码仍能通过编译并完美地运行。这表明迭代器让您能够轻松地使用算法和容器。

STL String 类

STL 提供了一个专门为操纵字符串而设计的模板类: std::basic_string,该模板类的两个常用具体化如下所示。

  • std::string:基于 char 的 std::basic_string 具体化,用于操纵简单字符串。
  • std::wstring:基于 wchar_t 的 std::basic_string 具体化,用于操纵宽字符串,通常用于存储支持各种语言中符号的 Unicode 字符。

使用 STL string 类

最常用的字符串函数包括:

  • 复制;
  • 连接;
  • 查找字符和子字符串;
  • 截短;
  • 使用标准模板库提供的算法实现字符串反转和大小写转换。

要使用 STL string 类,必须包含头文件 <string>。

string 类提供了很多重载的构造函数,因此可以多种方式进行实例化和初始化。例如,可使用常量字符串初始化 STL string 对象或将常量字符串赋给 STL std::string 对象:

const char* constCStyleString = "Hello String!";
std::string strFromConst (constCStyleString);

std::string strFromConst = constCStyleString;

上述代码与下面的代码类似:

std::string str2 ("Hello String!");

可让 string 的构造函数只接受输入字符串的前 n 个字符:

// Initialize a string to the first 5 characters of another
std::string strPartialCopy (constCStyleString, 5);

还可这样初始化 string 对象,即使其包含指定数量的特定字符:

// Initialize a string object to contain 10 'a's
std::string strRepeatChars (10, 'a');

要访问 STL string 的字符内容,可使用迭代器,也可采用类似于数组的语法并使用下标运算符([])提供偏移量。要获得 string 对象的 C 风格表示,可使用成员函数 c_str(),代码如下:

#include <string>
#include <iostream>

int main()
{
	using namespace std;
	
	string stlString("Hello String"); // sample 
	
	// Access the contents of the string using array syntax 
	cout << "Display elements in string using array-syntax: " << endl;
	for (size_t charCounter = 0;
		charCounter < stlString.length();
		++charCounter)
	{
		cout << "Character [" << charCounter << "] is: ";
		cout << stlString[charCounter] << endl;
	}
	cout << endl;
	
	// Access the contents of a string using iterators 
	cout << "Display elements in string using iterators: " << endl;
	int charOffset = 0;
	string::const_iterator charLocator;
	for (auto charLocator = stlString.cbegin();
		charLocator != stlString.cend();
		++charLocator)
	{
		cout << "Character [" << charOffset++ << "] is: ";
		cout << *charLocator << endl;
    }
	cout << endl;
	
	// Access contents as a const char* 
	cout << "The char* representation of the string is: ";
	cout << stlString.c_str() << endl;
	
	return 0;
}

第 24 行可以删除,可以使用关键字 auto 让编译器根据 std::string::cbegin() 的返回值推断 charLocator 的类型。

拼接字符串

要拼接字符串,可使用运算符 +=,也可使用成员函数 append():

string sampleStr1 ("Hello");
string sampleStr2 (" String! ");
sampleStr1 += sampleStr2; // use std::string::operator+=
// alternatively use std::string::append()
sampleStr1.append (sampleStr2); // (overloaded for char* too)

在 string 中查找字符或子字符串

STL string 类提供了成员函数 find(),该函数有多个重载版本,可在给定 string 对象中查找字符或子字符串。

// Find substring "day" in sampleStr, starting at position 0
size_t charPos = sampleStr.find ("day", 0);
// Check if the substring was found, compare against string::npos
if (charPos != string::npos)
    cout << "First instance of \"day\" was found at position " << charPos;
else
    cout << "Substring not found." << endl;

通过将 find() 操作的结果与 std::string::npos(实际值为−1)进行比较,std::string::npos 表示没有找到要搜索的元素。如果 find() 函数没有返回 npos,它将返回一个偏移量,指出子字符串或字符在 string 中的位置。

STL string 还有一些与 find() 类似的函数,如 find_first_of()、find_first_not_of()、 find_last_of()和 find_last_not_of(),这些函数可帮助程序员处理字符串。

截短 STL string

STL string 类提供了 erase()函数,具有以下用途。

  • 在给定偏移位置和字符数时删除指定数目的字符。
string sampleStr ("Hello String! Wake up to a beautiful day!");
sampleStr.erase (13, 28); // Hello String!
  • 在给定指向字符的迭代器时删除该字符。
sampleStr.erase (iCharS); // iterator points to a specific character
  • 在给定由两个迭代器指定的范围时删除该范围内的字符。
sampleStr.erase (sampleStr.begin (), sampleStr.end ()); // erase from begin

字符串的反转

反转 STL string 很容易,只需使用泛型算法 std::reverse():

string sampleStr ("Hello String! We will reverse you!");
reverse (sampleStr.begin (), sampleStr.end ());    

字符串的大小写转换

要对字符串进行大小写转换,可使用算法 std::transform(),它对集合中的每个元素执行一个用户指定的函数。在这里,集合是 string 对象本身。如下代码演示了如何对 string 中的字符进行大小写转换。

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

int main()
{
	using namespace std;
	
	cout << "Please enter a string for case-convertion:" << endl;
	cout << "> ";
	
	string inStr;
	getline(cin, inStr);
	cout << endl;
	
	transform(inStr.begin(), inStr.end(), inStr.begin(), ::toupper);
	cout << "The string converted to upper case is: " << endl;
	cout << inStr << endl << endl;
	
	transform(inStr.begin(), inStr.end(), inStr.begin(), ::tolower);
	cout << "The string converted to lower case is: " << endl;
	cout << inStr << endl << endl;
	
	return 0;
}

打印如下:

Please enter a string for case-convertion:
> It's a sunny day!

The string converted to upper case is:
IT'S A SUNNY DAY!

The string converted to lower case is:
it's a sunny day!

基于模板的 STL string 实现

前面说过,std::string 类实际上是 STL 模板类 std::basic_string 的具体化。容器类 basic_string 的模板声明如下:

template<class _Elem,
    class _Traits,
    class _Ax>
    class basic_string    

在该模板定义中,最重要的参数是第一个:_Elem,它指定了 basic_string 对象将存储的数据类型。std::string 使用 _Elem=char 具体化模板 basic_string 的结果,而 wstring 使用 _Elem= wchar 具体化模板 basic_string 的结果。

换句话说,STL string 类的定义如下:

typedef basic_string<char, char_traits<char>, allocator<char> > string;   

而 STL wstring 类的定义如下:

typedef basic_string<wchar_t, char_traits<wchar_t>, allocator<wchar_t> > string;

如果编写的应用程序需要更好地支持非拉丁字符,如中文和日文,应使用 std::wstring。

C++14 标准库支持将用引号扩起的字符串转换为 std::basic_string<t>的 operator “”s,这让有些字符串操作直观而简单,如下所示:

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

int main()
{
	string str1("Traditional string \0 initialization");
	cout << "Str1: " << str1 << " Length: " << str1.length() << endl;
	
	string str2("C++14 \0 initialization using literals"s);
	cout << "Str2: " << str2 << " Length: " << str2.length() << endl;
	
	return 0;
}    

输出:

Str1: Traditional string Length: 19
Str2: C++14 initialization using literals Length: 37

第 10 行使用了 C++14 引入的 operator “”s,这让实例 str2 能够包含并操作含有空字符的字符缓冲区。

总之,STL string 类是标准模板库提供的一个容器,可满足程序员众多的字符串操作需求。使用这个类的优点是,原本由程序员负责的内存管理、字符串比较和字符串操作都将由 STL 框架提供的一个容器类完成。

STL 动态数组类

vector 是一个模板类,提供了动态数组的通用功能,具有如下特点:

  • 在数组末尾添加元素所需的时间是固定的,即在末尾插入元素的所需时间不随数组大小而异,在末尾删除元素也如此;
  • 在数组中间添加或删除元素所需的时间与该元素后面的元素个数成正比;
  • 存储的元素数是动态的,而 vector 类负责管理内存。

要使用 std::vector 类,需要包含头文件:#include <vector>

要声明指向 list 中元素的迭代器,可以这样做:

std::vector<int>::const_iterator elementInVec;

如果需要可用于修改值或调用非 const 函数的迭代器,可使用 iterator 代替 const_iterator。

鉴于 std::vector 有多个重载的构造函数,您可在实例化 vector 时指定它开始应包含的元素数以及这些元素的初始值,还可使用 vector 的一部分来实例化另一个 vector。

下面是各种实例化 std::vector 的方式,包括指定长度和初始值以及复制另一个 vector 中的值:

#include <vector> 

int main()
{
	// vector of integers 
	std::vector<int> integers;
	
	// vector initialized using C++11 list initialization 
	std::vector<int> initVector{ 202, 2017, -1 };
	
	// Instantiate a vector with 10 elements (it can still grow) 
	std::vector<int> tenElements(10);
	
	// Instantiate a vector with 10 elements, each initialized to 90 
	std::vector<int> tenElemInit(10, 90);
	
	// Initialize vector to the contents of another 
	std::vector<int> copyVector(tenElemInit);
	
	// Vector initialized to 5 elements from another using iterators 
	std::vector<int> partialCopy(tenElements.cbegin(),
	tenElements.cbegin() + 5);

	return 0;
}

第 6 行使用了默认构造函数,在不知道容器最小需要多大,即不知道要存储多少个整数时,默认构造函数很有用。后,第 18 和 21 行演示了如何使用一个 vector 实例化另一个 vector 的内容,即复制 vector 对象或其一部分。这是所有 STL 容器都支持的构造方式。最后一种形式使用了迭代器,partialCopy 包含 tenElements 的前 5 个元素。

在 vector 中插入元素时,元素将插入到数组末尾,这是使用成员函数 push_back() 完成的:

vector <int> integers; // declare a vector of type int

// Insert sample integers into the vector:
integers.push_back (50);
integers.push_back (1);    

C++11 通过 std::initialize_list<>支持列表初始化,让您能够像处理静态数组那样,在实例化 vector 同时初始化其元素。与大多数容器一样,std::vector 也支持列表初始化,让您能够在实例化 vector 的同时指定其元素:

vector<int> integers = {50, 1, 987, 1001};
// alternatively:
vector<int> vecMoreIntegers {50, 1, 987, 1001}; 

如果要在中间插入元素,该如何办呢?很多 STL 容器(包括 std::vector)都包含 insert( )函数,且有多个重载版本。其中一个版本让您能够指定插入位置:

// insert an element at the beginning
integers.insert (integers.begin(), 25); 

另一个版本让您能够指定插入位置、要插入的元素数以及这些元素的值(都相同):

// Insert 2 elements of value 45 at the end
integers.insert (integers.end(), 2, 45);   

还可将另一个 vector 的内容插入到指定位置:

// Another vector containing 2 elements of value 30
vector <int> another (2, 30);

// Insert two elements from another container in position [1]
integers.insert (integers.begin() + 1, another.begin(), another.end());

可使用迭代器(通常是由 begin() 或 end() 返回的)告诉 insert() 您想将新元素插入到什么位置。

也可将该迭代器设置为 STL 算法(如 std::find() 函数)的返回值。std::find() 可用于查找元素,然后在这个位置插入另一个元素(这将导致找到的元素向后移)。

给 vector 添加元素时,应首选 push_back(),insert()可能是效率最低的(插入位置不是末尾时),因为在开头或中间插入元素时,将导致 vector 类将后面的所有元素后移(为要插入的元素腾出空间)。根据容器中包含的对象类型,这种移动操作可能需要调用拷贝构造函数或赋值运算符,因此开销可能很大。

可使用下列方法访问 vector 的元素:

  • 使用下标运算符([])以数组语法方式访问;
  • 使用成员函数at();
  • 使用迭代器;

使用[]访问 vector 的元素时,面临的风险与访问数组元素相同,即不能超出容器的边界。

使用下标运算符([])访问 vector 的元素时,如果指定的位置超出了边界,结果将是不确定的(什么情况都可能发生,很可能是访问违规)。更安全的方法是使用成员函数 at():

// gets element at position 2
cout < < integers.at (2);

at() 函数在运行阶段检查容器的大小,如果索引超出边界(无论如何都不能这样做),将引发异常。

还可以使用迭代器以类似于指针的语法访问 vector 中的元素:

#include <iostream> 
#include <vector>

int main()
{
	using namespace std;
	vector <int> integers{ 50, 1, 987, 1001 };

	vector <int>::const_iterator element = integers.cbegin();
	// auto element = integers.cbegin (); // auto type deduction 

	while (element != integers.end())
	{
		size_t index = distance(integers.cbegin(), element);

		cout << "Element at position ";
		cout << index << " is: " << *element << endl;

		// move to the next element 
		++element;
	}

	return 0;
}

打印如下:

Element at position 0 is: 50
Element at position 1 is: 1
Element at position 2 is: 987
Element at position 3 is: 1001    

vector 还支持使用 pop_back() 函数将末尾的元素删除。使用 pop_back() 将元素从 vector 中删除所需的时间是固定的,即不随 vector 存储的元素个数而异。如下代码演示了如何使用函数 pop_back() 删除 vector 末尾的元素:

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

template <typename T>
void DisplayVector(const vector<T>& inVec)
{
	for (auto element = inVec.cbegin(); // auto and cbegin(): C++11 
            element != inVec.cend(); // cend() is new in C++11 
            ++element)
	cout << *element << ' ';
	
	cout << endl;
}

int main()
{
	vector <int> integers;
	
	// Insert sample integers into the vector: 
	integers.push_back(50);
	integers.push_back(1);
	integers.push_back(987);
	integers.push_back(1001);
	
	cout << "Vector contains " << integers.size() << " elements: ";
	DisplayVector(integers);

	// Erase one element at the end 
	integers.pop_back();
	
	cout << "After a call to pop_back()" << endl;
	cout << "Vector contains " << integers.size() << " elements: ";
	DisplayVector(integers);
	
	return 0;
}   

打印如下:

Vector contains 4 elements: 50 1 987 1001
After a call to pop_back()
Vector contains 3 elements: 50 1 987    

vector 的大小指的是实际存储的元素数,而 vector 的容量指vector 能够存储的元素数,vector 的大小小于或等于容量。

要查询 vector 当前存储的元素数,可调用 size():

cout << "Size: " << integers.size ();    

要查询 vector 的容量,可调用 capacity():

cout << "Capacity: " << integers.capacity () << endl;    

如果 vector 需要频繁地给其内部动态数组重新分配内存,将对性能造成一定的影响。在很大程度上说,这种问题可以通过使用成员函数 reserve(number) 来解决。reserve 函数的功能基本上是增加分配给内部数组的内存,以免频繁地重新分配内存。通过减少重新分配内存的次数,还可减少复制对象的时间,从而提高性能,这取决于存储在 vector 中的对象类型。

下面的代码演示了 size() 和 capacity():

#include <iostream> 
#include <vector>

int main()
{
	using namespace std;

	// instantiate a vector object that holds 5 integers of default value 
	vector <int> integers(5);
	
	cout << "Vector of integers was instantiated with " << endl;
	cout << "Size: " << integers.size();
	cout << ", Capacity: " << integers.capacity() << endl;
	
	// Inserting a 6th element in to the vector 
  integers.push_back(666);
	
	cout << "After inserting an additional element... " << endl;
	cout << "Size: " << integers.size();
	cout << ", Capacity: " << integers.capacity() << endl;
	
	// Inserting another element 
	integers.push_back(777);
	
	cout << "After inserting yet another element... " << endl;
	cout << "Size: " << integers.size();
	cout << ", Capacity: " << integers.capacity() << endl;
	
	return 0;
}

打印如下:

Vector of integers was instantiated with
Size: 5, Capacity: 5
After inserting an additional element...
Size: 6, Capacity: 7
After inserting yet another element...
Size: 7, Capacity: 7    

第 9 行实例化了一个包含 5 个整型对象的 vector,这些整型对象使用默认值 0。第 16 行在 vector 中插入了第 6 个元素,鉴于在插入前 vector 的容量为 5,因此 vector 的内部缓冲区没有足够的内存来存储第 6 个元素。换句话说,vector 为扩大其容量以存储 6 个元素,需要重新分配内部缓冲区。重新分配的逻辑实现是智能的:为避免插入下一个元素时再次重新分配,提前分配了比当前需求更大的容量,这里将容量增大到了 7。第 23 行插入了第 7 个元素,这次没有扩大容量,因为已分配的内存足以满足需求。

在重新分配 vector 内部缓冲区时提前增加容量方面,C++标准没有做任何规定,因此性能优化程度取决于使用的 STL 实现。

deque 是一个 STL 动态数组类,与 vector 非常类似,但支持在数组开头和末尾插入或删除元素。实例化 deque 的代码如下:

// Define a deque of integers
std::deque <int> intDeque;

deque 与 vector 极其相似,也支持使用函数 push_back() 和 pop_back(),也使用运算符 [] 以数组语法访问其元素。deque 与 vector 的不同之处在于,它还允许您使用 push_front 和 pop_front 在开头插入和删除元素。

STL list 和 forward_list

标准模板库(STL)以模板类 std::list 的方式向程序员提供了双向链表。双向链表的主要优点是,插入和删除元素的速度快,且时间是固定的。从 C++11 起,您还可使用单向链表 std::forward_list,这种链表只能沿一个方向遍历。

链表是一系列节点,其中每个节点除包含对象或值外还指向下一个节点,即每个节点都链接到下一个节点和前一个节点,list 类的 STL 实现允许在开头、末尾和中间插入元素,且所需的时间固定。要使用 std::list 类,需要包含头文件 <list>:#include <list>

std 命名空间中的模板类 list 是一种泛型实现,要使用其成员函数,必须实例化该模板。

与 deque 类似,要在 list 开头插入元素,可使用其成员方法 push_front()。要在末尾插入,可使用成员方法 push_back()。

下面的代码演示了如何使用 使用 push_front() 和 push_back() 在 list 中插入元素:

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

template <typename T>
void DisplayContents(const T& container)
{
	for (auto element = container.cbegin();
		element != container.cend();
		++element)
		cout << *element << ' ';

	cout << endl;
}

int main()
{
	std::list <int> linkInts{ -101, 42 };
	
	linkInts.push_front(10);
	linkInts.push_front(2011);
	linkInts.push_back(-1);
	linkInts.push_back(9999);
	
	DisplayContents(linkInts);
	
	return 0;
}    

打印如下:

2011 10 -101 42 -1 9999    

注意这里的 DisplayContents() 比前面的 DisplayVector() 更通用, DisplayVector() 可用于任何 vector,而不管其存储的元素类型如何,而 DisplayContents() 可用于任何容器。

std::list 的特点之一是,在其中间插入元素所需的时间是固定的,这项工作是由成员函数 insert() 完成的。

成员函数 list::insert()有 3 种版本:

  • iterator insert(iterator pos, const T& x), 其中第 1 个参数是插入位置,第 2 个参数是要插入的值。该函数返回一个迭代器,它指向刚插入到 list 中的元素。
  • void insert(iterator pos, size_type n, const T& x),比上面多了一个参数:是要插入的元素的个数。
  • template <class InputIterator> void insert(iterator pos, InputIterator f, InputIterator l),该重载版本是一个模板函数,除一个位置参数外,它还接受两个输入迭代器,指定要将集合中相应范围内的元素插入到 list 中。注意,输入类型 InputIterator 是一种模板参数化类型,因此可指定任何集合(数组、vector 或另一个 list)的边界。

下面的代码演示了在 list 中插入元素的各种方法:

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

template <typename T>
void DisplayContents(const T& container)
{
	for (auto element = container.cbegin();
		element != container.cend();
		++element)
		cout << *element << ' ';

	cout << endl;
}

int main()
{
	list <int> linkInts1;
	
	// Inserting elements at the beginning... 
	linkInts1.insert(linkInts1.begin(), 2);
	linkInts1.insert(linkInts1.begin(), 1);
	
	// Inserting an element at the end... 
	linkInts1.insert(linkInts1.end(), 3);

	cout << "The contents of list 1 after inserting elements:" << endl;
	DisplayContents(linkInts1);

	list <int> linkInts2;
	
	// Inserting 4 elements of the same value 0... 
	linkInts2.insert(linkInts2.begin(), 4, 0);
	
	cout << "The contents of list 2 after inserting '";
	cout << linkInts2.size() << "' elements of a value:" << endl;
	DisplayContents(linkInts2);
	
	list <int> linkInts3;
	
	// Inserting elements from another list at the beginning... 
	linkInts3.insert(linkInts3.begin(),
		linkInts1.begin(), linkInts1.end());
	
	cout << "The contents of list 3 after inserting the contents of ";
	cout << "list 1 at the beginning:" << endl;
	DisplayContents(linkInts3);
	
	// Inserting elements from another list at the end... 
	linkInts3.insert(linkInts3.end(),
		linkInts2.begin(), linkInts2.end());
	
	cout << "The contents of list 3 after inserting ";
	cout << "the contents of list 2 at the end:" << endl;
	DisplayContents(linkInts3);
	
	return 0;
}    

打印如下:

The contents of list 1 after inserting elements:
1 2 3
The contents of list 2 after inserting '4' elements of a value:
0 0 0 0
The contents of list 3 after inserting the contents of list 1 at the beginning:
1 2 3
The contents of list 3 after inserting the contents of list 2 at the end:
1 2 3 0 0 0 0    

可以通过函数 erase() 删除 list 中的元素, 一个接受一个迭代器参数并删除迭代器指向的元素;另一个接受两个迭代器参数并删除指定范围内的所有元素。代码如下:

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

template <typename T>
void DisplayContents(const T& container)
{
	for (auto element = container.cbegin();
		element != container.cend();
		++element)
		cout << *element << ' ';

	cout << endl;
}

int main()
{
	 std::list <int> linkInts{ 4, 3, 5, -1, 2017 };
	
	// Store an iterator obtained in using insert() 
	auto val2 = linkInts.insert(linkInts.begin(), 2);
	
	cout << "Initial contents of the list:" << endl;
	DisplayContents(linkInts);

	cout << "After erasing element '" << *val2 << "':" << endl;
	linkInts.erase(val2);
	DisplayContents(linkInts);

	linkInts.erase(linkInts.begin(), linkInts.end());
	cout << "Number of elements after erasing range: ";
	cout << linkInts.size() << endl;
	
	return 0;
}    

打印如下:

Initial contents of the list:
2 4 3 5 -1 2017
After erasing element '2':
4 3 5 -1 2017
Number of elements after erasing range: 0    

insert() 返回一个迭代器,该迭代器指向新插入的元素,如第 21 行所示。将指向值为 2 的元素的迭代器存储到了变量 val2 中,以便第 27 行使用它来调用 erease(),从而将该元素从list中删除。第 30 行演示了如何使用 erease() 来删除指定范围内的元素,这行删除了从 begin() 到 end() 之间的所有元素,这相当于清空整个 list。

list 的一个独特之处是,指向元素的迭代器在 list 的元素重新排列或插入元素后仍有效。

可以使用 list::reverse() 反转元素的排列顺序,reverse() 只是反转 list 中元素的排列顺序。它是一个没有参数的简单函数,确保 指向元素的迭代器在反转后仍有效—如果程序员保存了该迭代器。

list 的成员函数 sort()有两个版本,下面是没有参数的版本:

linkInts.sort(); // sort in ascending order 

另一个接受一个二元谓词函数作为参数,可以让你指定排序标准:

bool SortPredicate_Descending (const int& lhs, const int& rhs)
{
    // define criteria for list::sort: return true for desired order
    return (lhs > rhs);
}
    
// Use predicate to sort a list:
linkInts.sort(SortPredicate_Descending);    

如果 list 的元素类型为类,而不是 int 等简单内置类型,如何对其进行排序呢?假设有一个包含地址簿条目的 list,其中每个元素都是一个对象,包含姓名、地址等内容,如何确保按姓名对其进行排序?

方法有两种:

  • 在类中实现运算符 < ;
  • 提供一个排序二元谓词 ;

看下面的代码:

#include <list>
#include <string>
#include <iostream>
using namespace std;

template <typename T>
void displayAsContents(const T& container)
{
	for (auto element = container.cbegin();
		element != container.cend();
		++element)
	  cout << *element << endl;

	cout << endl;
}

struct ContactItem
{
	string name;
	string phone;
	string displayAs;

	ContactItem(const string& conName, const string & conNum)
	{
		name = conName;
		phone = conNum;
		displayAs = (name + ": " + phone);
	}

	// used by list::remove() given contact list item 
	bool operator == (const ContactItem& itemToCompare) const
	{
		return (itemToCompare.name == this->name);
	}
	
	 // used by list::sort() without parameters 
	 bool operator < (const ContactItem& itemToCompare) const
	 {
		return (this->name < itemToCompare.name);
	 }

	 // Used in displayAsContents via cout 
	 operator const char*() const
	 {
		return displayAs.c_str();
	 }
};

bool SortOnphoneNumber(const ContactItem& item1, const ContactItem& item2)
{
	return (item1.phone < item2.phone);
}

int main()
{
	list <ContactItem> contacts;
	contacts.push_back(ContactItem("Jack Welsch", "+1 7889879879"));
	contacts.push_back(ContactItem("Bill Gates", "+1 97789787998"));
	contacts.push_back(ContactItem("Angi Merkel", "+49 234565466"));
	contacts.push_back(ContactItem("Vlad Putin", "+7 66454564797"));
	contacts.push_back(ContactItem("Ben Affleck", "+1 745641314"));
	contacts.push_back(ContactItem("Dan Craig", "+44 123641976"));
	
	cout << "List in initial order: " << endl;
	displayAsContents(contacts);
	
	contacts.sort();
	cout << "Sorting in alphabetical order via operator<:" << endl;
	displayAsContents(contacts);
	
	contacts.sort(SortOnphoneNumber);
	cout << "Sorting in order of phone numbers via predicate:" << endl;

	displayAsContents(contacts);
	
	cout << "Erasing Putin from the list: " << endl;
	contacts.remove(ContactItem("Vlad Putin", ""));
	displayAsContents(contacts);
	
	return 0;
}              

打印如下:

Bill Gates: +1 97789787998
Angi Merkel: +49 234565466
Vlad Putin: +7 66454564797
Ben Affleck: +1 745641314
Dan Craig: +44 123641976

Sorting in alphabetical order via operator<:
Angi Merkel: +49 234565466
Ben Affleck: +1 745641314
Bill Gates: +1 97789787998
Dan Craig: +44 123641976
Jack Welsch: +1 7889879879
Vlad Putin: +7 66454564797

Sorting in order of phone numbers via predicate:
Ben Affleck: +1 745641314
Jack Welsch: +1 7889879879
Bill Gates: +1 97789787998
Dan Craig: +44 123641976
Angi Merkel: +49 234565466
Vlad Putin: +7 66454564797

Erasing Putin from the list:
Ben Affleck: +1 745641314
Jack Welsch: +1 7889879879
Bill Gates: +1 97789787998
Dan Craig: +44 123641976
Angi Merkel: +49 234565466            

第 67 行调用了 list::sort,但没有提供谓词函数。在没有提供谓词的情况下,函数 sort() 检查 ContactItem 是否定义了运算符<。 要根据电话号码进行排序,可在调用 list::sort() 提供二元谓词函数 SortOnPhoneNumber(),如第 71 行所示。第 77 使用 list::remove( ) 从 list 中删除,list::remove() 使用第 31~34 行实现的 ContactItem::operator== 将该对象与 list 中的元素进行比较。该运算符在姓名相同时返回 true,给 list::remove() 指出了匹配标准。

STL list 是一个模板类,可用于创建任何对象类型的列表。

forward_list 的用法与 list 很像,但只能沿一个方向移动迭代器,且插入元素时只能使用函数 push_front(),而不能使用 push_back()。当然,总是可以使用 insert() 及其重载版本在指定位置插入元素。

如下代码演示了 forward_list 类的一些函数:

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

template <typename T>
void DisplayContents(const T& container)
{
	for (auto element = container.cbegin();
		element != container.cend();
		++element)
	 cout << *element << ' ';
	
	cout << endl;
}

int main()
{
	forward_list<int> flistIntegers{ 3, 4, 2, 2, 0 };
	flistIntegers.push_front(1);
	
	cout << "Contents of forward_list: " << endl;
	DisplayContents(flistIntegers);

	flistIntegers.remove(2);
	flistIntegers.sort();
	cout << "Contents after removing 2 and sorting: " << endl;
	DisplayContents(flistIntegers);
	
	return 0;
}            

打印如下:

Contents of forward_list:
1 3 4 2 2 0
Contents after removing 2 and sorting:
0 1 3 4            

鉴于 forward_list 不支持双向迭代,因此只能对迭代器使用运算符 ++,而不能使用 --。第 24 行使用函数 remove(2) 删除了值为 2 的所有元素;第 25 行调用了 sort(),这将使用默认的排序谓词,即 std::less<T>。

forward_list 的优点在于,它是一种单向链表,占用的内存比 list 稍少,因为只需指向下一个元素,而无需指向前一个元素。

STL 集合类

STL 提供了方便在应用程序中进行频繁而快速的搜索的容器类。std::set 和 std::multiset 用于存储一组经过排序的元素,其查找元素的复杂度为对数,而 unordered 集合的插入和查找时间是固定的。

set 和 multiset 之间的区别在于,后者可存储重复的值,而前者只能存储唯一的值。

为了实现快速搜索,STL set 和 multiset 的内部结构像二叉树,这意味着将元素插入到 set 或 multiset 时将对其进行排序,以提高查找速度。这还意味着不像 vector 那样可以使用其他元素替换给定位置的元素,位于 set 中特定位置的元素不能替换为值不同的新元素,这是因为 set 将把新元素同内部树中的其他元素进行比较,进而将其放在其他位置。

STL set 和 multiset 都是模板类,要使用其成员函数,必须先实例化。

set 和 multiset 都是在插入时对元素进行排序的容器,如果您没有指定排序标准,它们将使用默认谓词 std::less,确保包含的元素按升序排列。也可以在实例化 set 和 multiset 时可以指定排序谓词:

// used as a template parameter in set / multiset instantiation
template <typename T>
struct SortDescending
{
    bool operator()(const T& lhs, const T& rhs) const
    {
        return (lhs > rhs);
    }
};
            
// a set and multiset of integers (using sort predicate)
set <int, SortDescending<int>> setInts;
multiset <int, SortDescending<int>> msetInts;            

要在 set 和 multiset 中插入元素,可使用 insert():

setInts.insert (-1);  // 参数是插入的值
msetInts.insert (setInts.begin (), setInts.end ());  // 参数是指定的容器范围         

如下代码演示了在 STL set 或 multiset 中插入元素:

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


template <typename T>
void DisplayContents(const T& container)
{
	for (auto element = container.cbegin();
		element != container.cend();
		++element)
	   cout << *element << ' ';
	
	cout << endl;
}

int main()
{
	set <int> setInts{ 202, 151, -999, -1 };
	setInts.insert(-1); // duplicate 
	cout << "Contents of the set: " << endl;
	DisplayContents(setInts);
	
	multiset <int> msetInts;
	msetInts.insert(setInts.begin(), setInts.end());
	msetInts.insert(-1); // duplicate 
	
	cout << "Contents of the multiset: " << endl;
	DisplayContents(msetInts);
	
	cout << "Number of instances of '-1' in the multiset are: '";
	cout << msetInts.count(-1) << "'" << endl;
	
	return 0;
}            

打印如下:

Contents of the set:
-999 -1 151 202
Contents of the multiset:
-999 -1 -1 151 202
Number of instances of '-1' in the multiset are: '2'            

从输出可知,multiset 能够存储多个相同的值,而 set 不能。第 31 行演示了成员函数 multiset::count() 的用法,它返回 multiset 中有多少个元素存储了指定的值。

set、multiset、map 和 multimap 等关联容器都提供了成员函数 find(),它让您能够根据给定的键来查找值:

auto elementFound = setInts.find (-1);
// Check if found...
if (elementFound != setInts.end ())
    cout << "Element " << *elementFound << " found!" << endl;
else
    cout << "Element not found in set!" << endl; 

将 find() 返回的迭代器与 end() 进行比较,以核实是否找到了指定的元素。如果该迭代器有效,便可使用 *elementFound 访问它指向的值。

鉴于 multiset 可能在相邻的位置存储多个值相同的元素,为了访问所有这些元素,可使用 find() 返回的迭代器,并将迭代器前移 count()-1 次。

set、multiset、map 和 multimap 等关联容器都提供了成员函数 erase(),它让你能够根据键删除值:

setObject.erase (key);            

erase()函数的另一个版本接受一个迭代器作为参数,并删除该迭代器指向的元素:

setObject.erase (element);            

通过使用迭代器指定的边界,可将指定范围内的所有元素都从 set 或 multiset 中删除:

setObject.erase (iLowerBound, iUpperBound);            

看下面的代码:

#include <set> 
#include <iostream> 
#include <string> 
using namespace std;

template <typename T>
void DisplayContents(const T& container)
{
	for (auto iElement = container.cbegin();
		iElement != container.cend();
		++iElement)
		cout << *iElement << endl;

	cout << endl;
}

struct ContactItem
{
	string name;
	string phoneNum;
	string displayAs;

	ContactItem(const string& nameInit, const string & phone)
	{
		name = nameInit;
		phoneNum = phone;
		displayAs = (name + ": " + phoneNum);
	}

	// used by set::find() given contact list item 
	bool operator == (const ContactItem& itemToCompare) const
	{
		return (itemToCompare.name == this->name);
	}

	// used to sort 
	bool operator < (const ContactItem& itemToCompare) const
	{
		return (this->name < itemToCompare.name);
	}

	// Used in DisplayContents via cout 
	operator const char*() const
	{
		return displayAs.c_str();
	}
};

int main()
{
	set<ContactItem> setContacts;
	setContacts.insert(ContactItem("Jack Welsch", "+1 7889 879 879"));
	setContacts.insert(ContactItem("Bill Gates", "+1 97 7897 8799 8"));
	setContacts.insert(ContactItem("Angi Merkel", "+49 23456 5466"));
	setContacts.insert(ContactItem("Vlad Putin", "+7 6645 4564 797"));
	setContacts.insert(ContactItem("John Travolta", "91 234 4564 789"));
	setContacts.insert(ContactItem("Ben Affleck", "+1 745 641 314"));
	DisplayContents(setContacts);

	cout << "Enter a name you wish to delete: ";
	string inputName;
	getline(cin, inputName);

	auto contactFound = setContacts.find(ContactItem(inputName, ""));
	if (contactFound != setContacts.end())
	{
		setContacts.erase(contactFound);
		cout << "Displaying contents after erasing " << inputName << endl;
		DisplayContents(setContacts);
	}
	else
		cout << "Contact not found" << endl;

	return 0;
}            

运行后打印如下:

Angi Merkel: +49 23456 5466
Ben Affleck: +1 745 641 314
Bill Gates: +1 97 7897 8799 8
Jack Welsch: +1 7889 879 879
John Travolta: 91 234 4564 789
Vlad Putin: +7 6645 4564 797

Enter a name you wish to delete: Jack Welsch
Displaying contents after erasing Jack Welsch
Angi Merkel: +49 23456 5466
Ben Affleck: +1 745 641 314
Bill Gates: +1 97 7897 8799 8
John Travolta: 91 234 4564 789
Vlad Putin: +7 6645 4564 797            

这段代码与之前按字母顺序对 std::list 进行排序的代码很像,差别在于 std::set 排序是在插入元素时进行的。输出表明,您不需要调用任何函数来对 set 中的元素进行排序,因为已经在插入元素时使用第 37~40 行实现的operator < 进行了排序。您让用户指定要删除的条目,然后第 64 行调用 find() 找到该条目,而第 67 行使用 erase() 删除该条目。

对需要频繁查找(使用 find()等函数)的应用程序来说,STL set 和 multiset 很有优势,因为其内容是经过排序的,因此查找速度更快。然而,为了提供这种优势,容器在插入元素时进行排序,因此插入元素时会有额外的开销。

find() 利用了内部的二叉树结构,这种有序的二叉树结构使得 set 和 multiset 跟顺序容器(如 vector)相比有一个缺点:在 vector 中,可以使用新值替换迭代器(如 std::find() 返回的迭代器)指向的元素;但 set 根据元素的值对其进行了排序,因此不能使用迭代器覆盖元素的值,虽然通过编程可实现这种功能。

但是 STL std::set 和 std::multiset 相比于未经排序的比如 vector 等容器,查找速度更快,所需的时间不是与元素数成正比,而是与元素数的对数成正比。相比于未经排序的容器(查找时间与元素数成正比),这极大地改善了性能,但有时候这还不够。程序员和数学家都喜欢探索插入和排序时间固定的方式,一种这样的方式是使用基于散列的实现,即使用散列函数来计算排序索引。将元素插入散列集合时,首先使用散列函数计算出一个唯一的索引,再根据该索引决定将元素放到哪个桶(bucket)中。

从 C++11 起,STL 提供的容器类 std::unordered_set 就是基于散列的 set。要使用 STL 容器 std::unordered_set 或 std::unordered_multiset,需要包含头文件<unordered_set>。

其用法与 std::set 差别不大,然而,unordered_set 的一个重要特征是,有一个负责确定排列顺序的散列函数:

unordered_set<int>::hasher HFn = usetInt.hash_function();           

下面的代码演示了 std::unordered_set 提供的一些常见方法的用法:

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

template <typename T>
void DisplayContents(const T& cont)
{
	cout << "Unordered set contains: ";
	for (auto element = cont.cbegin();
		element != cont.cend();
		++element)
		cout << *element << ' ';

	cout << endl;

	cout << "Number of elements, size() = " << cont.size() << endl;
	cout << "Bucket count = " << cont.bucket_count() << endl;
	cout << "Max load factor = " << cont.max_load_factor() << endl;
	cout << "Load factor: " << cont.load_factor() << endl << endl;
}

int main()
{
	unordered_set<int> usetInt{ 1, -3, 2017, 300, -1, 989, -300, 9 };
	DisplayContents(usetInt);
	usetInt.insert(999);
	DisplayContents(usetInt);

	cout << "Enter int you want to check for existence in set: ";
	int input = 0;
	cin >> input;
	auto elementFound = usetInt.find(input);

	if (elementFound != usetInt.end())
		cout << *elementFound << " found in set" << endl;
	else
		cout << input << " not available in set" << endl;

	return 0;
} 

打印如下:

Unordered set contains: 9 1 -3 989 -1 2017 300 -300
Number of elements, size() = 8
Bucket count = 8
Max load factor = 1
Load factor: 1

Unordered set contains: 9 1 -3 989 -1 2017 300 -300 999
Number of elements, size() = 9
Bucket count = 64
Max load factor = 1
Load factor: 0.140625

Enter int you want to check for existence in set: -300
-300 found in set           

插入第 9 个元素时,unordered_set 重新组织:创建 64 个桶并重新创建散列表,而负载系数降低了。main() 中的其他代码表明,在 unordered_set 中查找元素的语法与 set 类似。

STL 映射类

map 和 multimap 是键-值对容器,支持根据键进行查找。map 和 multimap 之间的区别在于,后者能够存储重复的键,而前者只能存储唯一的键。

为了实现快速查找,STL map 和 multimap 的内部结构看起来像棵二叉树。这意味着在 map 或 multimap 中插入元素时将进行排序;还意味着不像 vector 那样可以使用其他元素替换给定位置的元素,位于 map 中特定位置的元素不能替换为值不同的新元素,这是因为 map 将把新元素同二叉树中的其他元素进行比较,进而将它放在其他位置。

实例化模板类 map 时,需要指定键和值的类型以及可选的谓词(它帮助 map 类对插入的元素进行排序)。因此,典型的 map 实例化语法如下:

#include <map>
using namespace std;
...
map <keyType, valueType, Predicate=std::less <keyType>> mapObj;
multimap <keyType, valueType, Predicate=std::less <keyType>> mmapObj; 

第三个模板参数是可选的。如果您值指定了键和值的类型,而省略了第三个模板参数,std::map 和 std::multimap 将把 std::less<> 用作排序标准。

如下代码演示了如何实例化 STL map 和 multimap:

#include<map> 
#include<string> 

template<typename keyType>
struct ReverseSort
{
	bool operator()(const keyType& key1, const keyType& key2)
	{
		return (key1 > key2);
	}
};

int main()
{
	using namespace std;

	// map and multimap key of type int to value of type string 
	map<int, string> mapIntToStr1;
	multimap<int, string> mmapIntToStr1;

	// map and multimap constructed as a copy of another 
	map<int, string> mapIntToStr2(mapIntToStr1);
	multimap<int, string> mmapIntToStr2(mmapIntToStr1);

	// map and multimap constructed given a part of another map or multimap 
	map<int, string> mapIntToStr3(mapIntToStr1.cbegin(),
		mapIntToStr1.cend());

	multimap<int, string> mmapIntToStr3(mmapIntToStr1.cbegin(),
		mmapIntToStr1.cend());

	// map and multimap with a predicate that inverses sort order 
	map<int, string, ReverseSort<int>> mapIntToStr4
		(mapIntToStr1.cbegin(), mapIntToStr1.cend());

	multimap<int, string, ReverseSort<int>> mmapIntToStr4
		(mapIntToStr1.cbegin(), mapIntToStr1.cend());

	return 0;
}   

要在这两种容器中插入元素,都可使用成员函数 insert:

std::map<int, std::string> mapIntToStr1;
// insert pair of key and value using make_pair function
mapIntToStr.insert (make_pair (-1, "Minus One")); 

也可直接使用 std::pair 来指定要插入的键和值:

mapIntToStr.insert (pair <int, string>(1000, "One Thousand"));

另外,还可使用类似于数组的语法进行插入。这种方式对用户不太友好,是由下标运算符([])支持的:

mapIntToStr [1000000] = "One Million"; 

还可使用 map 来实例化 multimap:

std::multimap<int, std::string> mmapIntToStr(mapIntToStr.cbegin(),mapIntToStr.cend()); 

下面的代码演示了在 STL map 或 multimap 中使用 insert() 以及数组语法(运算符[])插入元素

#include <map> 
#include <iostream> 
#include<string> 

using namespace std;

// Type-define the map and multimap definition for easy readability 
typedef map <int, string> MAP_INT_STRING;
typedef multimap <int, string> MMAP_INT_STRING;

template <typename T>
void DisplayContents(const T& cont)
{
	for (auto element = cont.cbegin();
		element != cont.cend();
		++element)
		cout << element->first << " -> " << element->second << endl;

	cout << endl;
}

int main()
{
	MAP_INT_STRING mapIntToStr;

	// Insert key-value pairs into the map using value_type 
	mapIntToStr.insert(MAP_INT_STRING::value_type(3, "Three"));

	// Insert a pair using function make_pair 
	mapIntToStr.insert(make_pair(-1, "Minus One"));

	// Insert a pair object directly 
	mapIntToStr.insert(pair <int, string>(1000, "One Thousand"));

	// Use an array-like syntax for inserting key-value pairs 
	mapIntToStr[1000000] = "One Million";

	cout << "The map contains " << mapIntToStr.size();
	cout << " key-value pairs. They are: " << endl;
	DisplayContents(mapIntToStr);

	// instantiate a multimap that is a copy of a map 
	MMAP_INT_STRING mmapIntToStr(mapIntToStr.cbegin(),
		mapIntToStr.cend());

	// The insert function works the same way for multimap too 
	// A multimap can store duplicates - insert a duplicate 
	mmapIntToStr.insert(make_pair(1000, "Thousand"));

	cout << endl << "The multimap contains " << mmapIntToStr.size();
	cout << " key-value pairs. They are: " << endl;
	cout << "The elements in the multimap are: " << endl;
	DisplayContents(mmapIntToStr);

	// The multimap can return number of pairs with same key 
	cout << "The number of pairs in the multimap with 1000 as their key: "
		<< mmapIntToStr.count(1000) << endl;

	return 0;
}

打印如下:

The map contains 4 key-value pairs. They are:
-1 -> Minus One
3 -> Three
1000 -> One Thousand
1000000 -> One Million


The multimap contains 5 key-value pairs. They are:
The elements in the multimap are:
-1 -> Minus One
3 -> Three
1000 -> One Thousand
1000 -> Thousand
1000000 -> One Million

The number of pairs in the multimap with 1000 as their key: 2    

map 和 multimap 等关联容器都提供了成员函数 find(),它让您能够根据给定的键查找值。find() 总是返回一个迭代器:

multimap <int, string>::const_iterator pairFound = mapIntToStr.find(key);

需要首先检查该迭代器,确保 find() 成功了,再使用它来访问找到的值:

if (pairFound != mapIntToStr.end())
{
    cout << "Key " << pairFound->first << " points to Value: ";
    cout << pairFound->second << endl;
}
else
    cout << "Sorry, pair with key " << key << " not in map" << endl;

multimap 中一个键可以对应多个值,可以使用 multimap::count() 确定有多少个值与这个键对应,再对迭代器递增,然后访问这些相邻的值:

auto pairFound = mmapIntToStr.find(key);
// Check if find() succeeded 
if (pairFound != mmapIntToStr.end())
{
	// Find the number of pairs that have the same supplied key 
	size_t numPairsInMap = mmapIntToStr.count(1000);
	for (size_t counter = 0;
		counter < numPairsInMap; // stay within bounds 
		++counter)
	{
		cout << "Key: " << pairFound->first; // key 
		cout << ", Value [" << counter << "] = ";
		cout << pairFound->second << endl; // value 
		++pairFound;
	}
}
else
	cout << "Element not found in the multimap";      

map 和 multimap 提供了成员函数 erase(),该函数删除容器中的元素。调用 erase 函数时将键作为参数,这将删除包含指定键的所有键-值对:

mapObject.erase (key);

函数 erase() 的另一种版本接受迭代器作为参数,并删除迭代器指向的元素:

mapObject.erase(element);

还可使用迭代器指定边界,从而将指定范围内的所有元素都从 map 或 multimap 中删除:

mapObject.erase (lowerBound, upperBound);    

可以在实例化 map 时提供一个排序谓词,代码如下:

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

template <typename T>
void DisplayContents(const T& cont)
{
	for (auto element = cont.cbegin();
		element != cont.cend();
		++element)
		cout << element->first << " -> " << element->second << endl;

	cout << endl;
}

struct PredIgnoreCase
{
	bool operator()(const string& str1, const string& str2) const
	{
		string str1NoCase(str1), str2NoCase(str2);
		transform(str1.begin(), str1.end(), str1NoCase.begin(), ::tolower);
		transform(str2.begin(), str2.end(), str2NoCase.begin(), ::tolower);

		return(str1NoCase< str2NoCase);
	};
};

typedef map<string, string> DIR_WITH_CASE;
typedef map<string, string, PredIgnoreCase> DIR_NOCASE;

int main()
{
	// Case-sensitive directorycase of string-key plays no role 
	DIR_WITH_CASE dirWithCase;

	dirWithCase.insert(make_pair("John", "2345764"));
	dirWithCase.insert(make_pair("JOHN", "2345764"));
	dirWithCase.insert(make_pair("Sara", "42367236"));
	dirWithCase.insert(make_pair("Jack", "32435348"));

	cout << "Displaying contents of the case-sensitive map:" << endl;
	DisplayContents(dirWithCase);

	// Case-insensitive mapcase of string-key affects insertion & search 
	DIR_NOCASE dirNoCase(dirWithCase.begin(), dirWithCase.end());

	cout << "Displaying contents of the case-insensitive map:" << endl;
	DisplayContents(dirNoCase);

	// Search for a name in the two maps and display result 
	cout << "Please enter a name to search" << endl << "> ";
	string name;
	cin >> name;

	auto pairWithCase = dirWithCase.find(name);
	if (pairWithCase != dirWithCase.end())
		cout << "Num in case-sens. dir: " << pairWithCase->second << endl;
	else
		cout << "Num not found in case-sensitive dir" << endl;

	auto pairNoCase = dirNoCase.find(name);
	if (pairNoCase != dirNoCase.end())
		cout << "Num found in CI dir: " << pairNoCase->second << endl;
	else
		cout << "Num not found in the case-insensitive directory" << endl;

	return 0;
}

打印如下:

Displaying contents of the case-sensitive map:
JOHN -> 2345764
Jack -> 32435348
John -> 2345764
Sara -> 42367236

Displaying contents of the case-insensitive map:
Jack -> 32435348
JOHN -> 2345764
Sara -> 42367236

Please enter a name to search
> jack
Num not found in case-sensitive dir
Num found in CI dir: 32435348 

第 30 行实例化电话簿时没有使用排序谓词,会默认使用谓词 std::less<T>,根据区分大小写的 std::string::operator< 进行排序和查找。第 31 行实例化电话簿时指定了谓词 PreIgnoreCase,它将两个字符串转换为小写后再进行比较,从而确保比较不区分大小写。从输出可知,在这两个 map 中查找 jack 时,在不区分大小写的 map 中能够找到 Jack,但在区分大小写的 map 中找不到。

也可将结构 PredIgnoreCase 声明为类,注意,声明为类时需要给 operator() 加上关键字 public。在 C++编译器看来,结构类似于类,但成员默认为公有的,继承方式也默认为公有的。

注意到这里使用的谓词是一个实现了运算符 () 的结构,这种也可作为函数的对象被称为函数对象(或 Functor),这个将在后面详细介绍。

在查找方面,map 的复杂度为对数,即所需的时间与 map 包含的元素数的对数成反比。虽然对数复杂度已相当不错,但别忘了,map、multimap、set 和 multiset 等容器在插入时对元素进行排序,因此其插入速度更慢。有没有插入和查找实际固定的容器呢?散列表就是这样的容器,其插入时间是固定的,根据键查找元素的时间也几乎是固定的(大多数情况下如此),而不受容器大小的影响。

从 C++11 起,STL 支持散列映射—std::unordered_map 类,要使用这个模板类,需要包含头文件 <unordered_map>:

#include<unordered_map>    

散列表与简单映射的区别在于,散列表将键-值对存储在桶中,每个桶都有索引,指出了它在散列表中的相对位置(类似于数组)。这种索引是使用散列函数根据键计算得到的:

Index = HashFunction(key, TableSize);    

unordered_map 和 unordered_multimap 实现散列表的容器是 C++11 引入的,其实例化、插入和查找的方法与 std::map 和 std::multimap 类似。然而,一个重要的特点是,unordered_map 包含一个散列函数,用于计算排列顺序:

unordered_map<int, string>::hasher hFn = umapIntToStr.hash_function();    

要获悉键对应的索引,可调用该散列函数,并将键传递给它:

size_t hashingVal = hFn(1000);