第13章 用STL简化编程

C++最酷的地方之一就是标准模板库(Standard Template Library,STL)。大多数编译器都支持。模板是可用来创建高级容器的泛化数据类型。例如,可用 list 模板创建整数、浮点数甚至自定义类型的链表。

虽然听起来很新奇,但不用担心。STL 是解决许多常见编程问题的有效手段。其出发点很简单。和函数、类和对象一样,既然一个编程问题已经解决,为什么要重新解决一遍?

目前,大多数C++编译器都提供了对STL的完整支持。C++14(甚至C++11)编译器肯定都是支持的。

13.1 列表模板

STL提供了对建立在其他类型基础上的集合(或容器)的广泛支持。指定好基类型,STL 就能构建基于该类型的高级容器。例如:

list<int> ilist;	//整数列表
list<string> strList;	//字符串列表
list<Fraction> bunchoFract;	//分数列表

各种列表随便建。基类型可以是任何基元类型。(比如int),也可以是你自己写的类型(比如Fraction)。用 list 模板创建的链表能特别高效地执行插入和删除操作。STL 支持其他泛化数据结构,包括 vector(可无限增长的数组)以及 set 和 map(基于二叉树构建)。

可以在代码中使用的最简单的构造就称为“基元”,其他构造都是它们复合而成的。

所有 STL 名称都是 std 命名空间的一部分,意味着要么在每个 STL 名称(比如 stack 或list)前添加 std::,要么在程序中添加 using namespace std::,就像本书的例子一样。

用C++写模板
C++ 最早的版本完全不支持模板。但在 C++ 问世后几年间,程序员(尤其是专业程序员)开始呼吁模板支持。代码最好能重用,不需要重新发明轮子。

我们用模板实现泛型算法。例如,一旦写好针对整数的某个算法,同样的代码应该能重用于其他数据类型,比如 double、字符串或其他任何类型的对象。感觉就像是掌握了一组容器类及其相关函数,并执行全局搜索和替换,将 int 的所有实例都替换成其他类型。

可以很简单地用 C++ 写自己的模板类和模板函数。例如,可以用 template 关键字声明名为 pair 的泛型容器:

template class<T>
class pair{
public:
	T first, last;
};

以后凡是涉及“一对元素”的容器类,都可以用该模板来声明。

pair<int> intPair;
pair<double> floatPair;
pair<string> full_name;

intPair.first = 12;

不过,写自己的模板类的主题超出了本书范围。若全面探讨,很容易就能多写几百页。模板作为一个高级主题令人着迷,市面上有许多不错的参考书。

虽然自己写模板有点超纲,但我鼓励即使是新入行的 C++ 程序员,也在理解了类和指针之后马上拥抱标准模板库(STL)。STL 提供的类不仅节省时间,还相当好用。好多工作别人已经做了一遍,没必要重复。

创建和使用列表类

使用列表模板前需开启对它的支持:

# include <list>
using namespace std;

然后就可以创建自己的链表类,用以下语法声明 STL 列表类:

list<类型> 列表名;

不添加 using namespace 语句,来自 STL 的项就必须附加 std:: 前缀。标准库的其他对象和模板同理。通过以下语法在没有using namespace语句的情况下使用模板:std::1ist<类型列表名;下面展示了更多例子:

#include <list>
using namespace std;
、、、
list<int> list_of_ints;
list<int> another_list;
list<double> list_of_floatingpt;
list<Point> list_of_pts;
list<string> LS;

创建好的列表最开始是空的,可用 push_back 函数在列表末端(back 的来历)添加元素。
例如:

list<string> LS;
LS.push_back("Able");
LS.push_back("Baker");
LS.push_back("Charlie");

push_front 成员函数则可以将元素添加到列表前端。效果和上个例子一样,只不过顺序相反。

Ls.push_front("Able");
LS.push_front("Baker");
LS.push_front("Charlie");

数值列表就添加数值元素:

list<int> list_of_ints;
list_of_ints.push_back(100);

如你所见,可创建任意基类型的链表并添加数据。如果要在此基础上做更多的事情,需要用到迭代器。

下一段限定C++14编译器。(事实上,C++11就引入了该功能,只是 Microsoft 等厂商花了些时间才正式支持。)

使用符合 C++14 规范的编译器,可以像数组那样,使用逗号分隔的列表来初始化包括列表在内的大多数STL容器。例如:

list<int> iList = {1,2,3,4,5};

创建和使用迭代器

STL 的许多模板都使用迭代器,从而一次访问一个列表元素(称为遍历),迭代器外观和使用感受都像指针,尤其是它们还使用 ++,-- 和 * 操作符(虽然有区别),用以下语法声明送代器:

list<类>::iterator 迭代器名

例如,以下语句声明一个列表和对应的迭代器:

list<string> LS;
list<string>::iterator iter;

现在就可以用 iter 遍历 LS 列表,因其基类型(string)一致.

STL 列表提供 begin 和 end 函数返回指向列表头尾的迭代器。用以下语句初始化选
.代器:

list<string>::iterator iter = LS.begin();

下面是含4个元素的字符串列表LS的操作示意图.
在这里插入图片描述

正确初始化的 iter 现在可像指针那样使用。递增操作符++使 iter 指向下一项。

++iter;//在列表中前进一个元素

如下图所示,递增迭代器即可遍历列表,和数组的指针操作一样。

在这里插入图片描述

和指针一样,用间接寻址操作符 (*) 访问迭代器指向的数据:

cout << *iter << endl;	//打印指向的字符串

配合这些操作,用一个循环就可打印所有列表元素。注意 end 成员函数生成的迭代器指向最后一个元素之后的位置,而非指向最后一个元素本身。所以可用它作为循环条件。只要没有抵达LS.end(),循环就继续。

iter = Ls.begin();	//从头开始
while(iter != LS.end()){	//抵达Ls.end()就结束循环
	cout << *iter << endl;	//打印字符串
	++iter;				//跳到下一个
}

用 for 循环更简单:

for (iter = LS.begin(); iter != LS.end(); ++iter){
	cout << *iter << endl;
}

C++11/C++14: for each

第4章提到基于范围的for,用它打印列表项更简单。迭代器都用不上。

下一段限定C++14编译器。(事实上,C++11就引入了该功能,只是Microsoft等厂高花了些时间才正式支持。)

下面介绍如何用基于范围的 for 打印列表,第17章会更深入介绍。以下代码适合任何 STL 容器(含所有列表容器),修改一下名称 LS 即可。

for (auto x:LS)
{
    cout << x << endl;
}

比较指针和迭代器
你现在知道为什么我说迭代器像送代器。STL 类的设计者故意使其外观和感觉像指针,以便和 C++ 语言的其他部分配合。重用前缀和后缀递增操作符(++)很方便,一看就知道作用是什么,用间接寻址操作符(*)也是这个意图。这一切都是基于 C++ 的操作符重载语法。

但选代器和普通指针本质上还是有所区别的,也可将后者理解成“原始”指针,后者不会制止无效内存访问,所以使用需谨慎。

迭代器则可放心使用,它是安全的,而且是故意设计成如此。程序可尝试将选代器移过容器边界,这不会产生什么严重后果,只是迭代器无法访问容器中的数据罢了。失去控制的指针可能覆盖和破坏整个系统的内存,但送代器碰不了它不该碰的任何东西。

例13.1:STL 有序列表

现已掌握了写一个有序列表程序所需的迭代器和列表语法。等下你看到程序有多短,就知道程序员有多爱 STL,开始之前注意,STL 列表类提供了内建的 sort 函数(还有其他许多函数):

LS.sort();	// 按字母顺序对列表排序

为支持列表的 sort 函数和其他成员函数,列表的基类型必须为小于操作待(<)、赋值操作待(=)和相等性测试操作符(==)定义合理行为。string 类自然已定义了这些行为。
如未定义这些操作符,某些列表成员函数就可能无法使用。

以下是完整程序。

// alphalist2.cpp
# include <iostream>
# include <list>
# include <string>
using namespace std;

int main()
{
	string s;
	list<string > LS;
	list<string>::iterator iter;
	while (true)
	{
		cout << "输入字符串(直接按Enter退出):";
		getline(cin, s);
		if (s.size() == 0)
		{
			break;
		}
		LS.push_back(s);
	}
	LS.sort();	// 排序

	for (iter = LS.begin(); iter != LS.end(); iter++)
	{
		cout << *iter << endl;
	}
	return 0;
}

这个短小精悍的程序允许用户输入任意大小的任意数量的字符串(仅受系统本身的物理限制),输入完毕后,程序按字母顺序打印所有字符串。例如,假定输入:

John
Paul 
George 
Ringo 
Brian Epstein

程序将打印这些名字排好序的结果:

Brian Epstein 
George 
John
Paul 
Ringo

大多数程序逻辑都是以前见过的。main 中的半数语句都在提醒用户输入,并用 LS.push_back()
在列表尾添加一个字符串。和往常一样,如用户不输入而直接按ENTER,会造成一个长度为零的字符串,表示“我结束了”

while(true){
	cout << "Enter string(ENTER to exit):";
    getline(cin, s);
    if(s.size() == 0){
		break;
    }
    LS.push_back(s);
}

类真正强大的地方在于对 sort 的调用,这是一个强大的成员函数。

LS.sort();

最后用迭代器(iter)打印所有成员。

可用 STL 迭代方便地写用于“打印所有成员”的函数。特别是,当迭代抵达 LS.end()
时,表明迭代已移过了列表最后一个元素,工作完成。

相反,如 iter != LS.end() 成立,表明列表尚未完全处理,所以工作应该继续。

for(iter = LS.begin(); iter != LS.end(); ++iter)
{
    cout << *iter << endl;
}

连续排序列表

上一节的方案唯一的问题在于,只在所有元素插入列表后才开始排序。小程序无所谓,你永远注意不到有什么区别。但对于相当长的列表(比如几百万个元素),排序时间会相当长。

大型数据库更好的方案是始终维持数据的排序状态。每个新元素都添加到它的正确排序位置。第12章创建的二叉树就是如此。

维持列表的连续排序状态不难。不是用这个语句添加字符串:

LS.push_back(s);

而是每次添加元素时都使用以下语句。这些语句首先判断正确的字母顺序位置。然后,insert 函数在迭代器指向的元素前插入一个新元素(本例是一个字符串)。

for(iter = LS.begin(); iter != LS.end() && s > *iter;)
{
    ++iter;
}
LS.insert(iter, s);

为什么这么简单?一个原因是和之前一样,iter != LS.end() 作为测试条件太好使了。
由于 LS.end() 对应最后一个元素之后的位置,所以可循环测试从头到尾(含)的每个元素。最后一个元素不必作为特殊情况处理。

使这个循环如此简单的另一个原因是 STL insert 函数非常健壮;它在情况不好的时候也具有正确的行为,这进一步避免了处理特殊情况的必要。以空列表为例,这时 insert 函数只是将 s 作为第一个元素添加。

如果迭代器(iter)都到末尾了还是没找到插入点怎么办?这时 insert 函数所做的事情正是你希望的:将 s 添加到列表末尾,正好在 end 之前。也就是说,正好在最后一个元素之后。

但有时就连这样的有序列表也不太理想。对于超大数据集合,程序经常检索包含上千万个元素的列表并不是什么好事。例如,如果平均要检索 500 万个元素才能定位正确的插入点,那么代价相当高昂。相反,第12章的二叉树例子能实现几乎瞬时的存取速度。那一章的Btree类创建了一个基本二叉树。更高级的二叉树在 STL 中通过 map 和 set 模板提供。(不过,建议试着写自己的二叉树,中间乐趣多多!)

练习

练习13.1.1,

修改例13.1,使用一个连续排序列表(如刚才所述)。

答案:

#include <iostream>
#include <list>
#include <string>

using namespace std;

int main()
{
	string s;
	list<string> LS;
	list<string>::iterator iter;

	while (true) {
		cout << "Enter string (ENTER to exit): ";
		getline(cin, s);
		if (s.size() == 0) {
			break;
		}
		for (iter = LS.begin(); iter != LS.end() && s > *iter;
			iter++) {
		}
		LS.insert(iter, s);    // <- Here is where insertion is made!
	}
	for (iter = LS.begin(); iter != LS.end(); iter++) {
		cout << *iter << endl;
	}

	return 0;
}

练习13.1.2.

修改例13.1,使用一个连续排序列表,但元素按相反的顺序。

答案:

#include <iostream>
#include <list>
#include <string>

using namespace std;

int main()
{
	string s;
	list<string> LS;
	list<string>::iterator iter;

	while (true) {
		cout << "Enter string (ENTER to exit): ";
		getline(cin, s);
		if (s.size() == 0) {
			break;
		}

		// Note that only change needed from last exercise
		// is to reverse the comparison here from > to <.

		for (iter = LS.begin(); iter != LS.end() && s < *iter;
			iter++) {
		}
		LS.insert(iter, s);    // <- Here is where insertion is made!
	}
	for (iter = LS.begin(); iter != LS.end(); iter++) {
		cout << *iter << endl;
	}

	return 0;
}


练习13.1.3,

修改例13.1来报告列表大小。可写代码统计迭代次数,也可调用模板类的 size 函数,语法是list.size()

答案:

#include <iostream>
#include <list>
#include <string>

using namespace std;

int main()
{
	string s;
	list<string> LS;
	list<string>::iterator iter;

	while (true) {
		cout << "Enter string (ENTER to exit): ";
		getline(cin, s);
		if (s.size() == 0) {
			break;
		}
		for (iter = LS.begin(); iter != LS.end() && s < *iter;
			iter++) {
		}
		LS.insert(iter, s);    // <- Here is where insertion is made!
	}
	for (iter = LS.begin(); iter != LS.end(); iter++) {
		cout << *iter << endl;
	}

	cout << "Size of the list is: " << LS.size() << endl;

	return 0;
}

练习13.1.4,

用列表模板写程序来获取任意数量的浮点数作为输入。都添加到列表,并通过遍历列表来报告以下信息:最小数:最大数:总和;平均数。不用列表或数组能否实现?归根结底,为什么想到用列表呢?

答案:

// 代码有问题
#include <iostream>
#include <list>
#include <string>

using namespace std;

int main()
{
	string s;   // Input string
	list<double> the_list;
	list<double>::iterator iter;
	double x = 0.0;
	double total = 0.0;
	double low = 0.0;
	double high = 0.0;

	while (true) {
		cout << "Enter string (ENTER to exit): ";
		getline(cin, s);
		if (s.size() == 0) {
			break;
		}
		x = stof(s);
		for (iter = the_list.begin(); iter != the_list.end()
			&& x < *iter; iter++) {

			the_list.insert(iter, x);
			total += x;
			if (x < low || the_list.size() == 1) {
				low = x;
			}
			if (x > high || the_list.size() == 1) {
				high = x;
			}
		}
	}
	cout << "Lowest value is: " << low << endl;
	cout << "Highest value is: " << high << endl;
	cout << "Total is: " << total << endl;
	cout << "Average is: " << (total / the_list.size()) << endl;
	return 0;
}

13.2 涉及RPN计算器

不,这不是 Registered Practicing Nurses(注册执业护士)计算器,而是出我的 Reverse Polish Notation(RPN,逆波兰记法)计算器。它获取任意复杂度的一个输入行,分析它,并执行所有指定的计算。

听起来好难,而且老实说一般只有大学 CS 课程才会关注该项目。但有了 STL 和来自标准 C++ 库的 strtok 函数,大多数工作其实已经完成了。

RPN 最优美的地方在于能无歧义地指定数学和逻辑表达式,避免了使用圆括号的必要。
它的语法只有两条规则:

​ 表达式 → 数值字面值
​ 表达式 → 表达式表达式操作符
该记法的意思是:要求值的每个表达式要么是一个简单数字(最简形式),要么是两个表达式后跟一个操作符。从中看到递归的优美吗?较小的表达式可在较大的表达式中重新合并,从而实现任意复杂度。

如果还没有 get 到这一点,不要慌张,稍后还会详述。最明显的是,RPN 记法可对下面这样的东西求值:
2 3 +
意思是"2和3相加”。结果是5,可完美地套入“表达式表达式操作符"。2 和 3 各自都是数值字面值,是有效表达式,且后跟一个操作符(+)。目前是不是一切顺利?再来看一个较复杂的表达式:

​ 2 3 + 17 10 - *

真正理解 RPN 后,这个表达式不在话下。一个表达式可由任意两个操作数后跟一个操作符构成。重点在于,操作数本身可以是表达式。换言之,可在表达式中的表达式中构造表达式、、、族套层数随意。

注意最靠近操作数的操作符具有最高优先级。2 3 + 是有效表达式,17 10- 也是。这两个表达式后跟一个乘法操作符(*),从而构成一个大表达式。

在这里插入图片描述

最终求值结果是 35。该 RPN 表达式等价于以下标准记法(也称为中缀记法)的输入行:
(2 + 3)*(17 - 10)
标准记法的缺点在于它严重依赖于圆括号,RPN 则无此问题。

顺便说一句,为了语法的完整性,下面列出了支持的操作符。

operator → +
operator → *
operator → -
operator → /

这意味着操作符可以是 +,*,-或 /。

还可在一行中用OR表示上述语法。注意OR没有加粗,意味着"OR"不是字面值。
operator+ OR * OR – OR /
下面列出了 RPN 的更多例子,都采用标准算术记法。

2  10  5  4  -  /  +	// ==>	2 + (10 / (5 - 4))
1  2  3 ** 10 9 - +		// ==> (1 * (2 * 3)) + (10 - 9)
5  3 - 15 * 			// ==>(5-3)* 15

波兰记法简史
波兰记法由著名哲学家、教授和逻辑学家扬·武卡谢维奇(Jan Eukasiewicz)发明。虽然在大多数国家都不太出名,他对于20世纪初的公理逻辑有着杰出的贡献。1920年,教授创建了一个方案从逻辑表达式中移除对圆括号的需要,使其更简洁。该方案同样适合数学。为了向自己的国籍致敬,他将其命名为波兰记法。在他的版本中(可以称为正波兰记法),操作符是前缀,例如:
+ 2 3
20世纪60年代初,计算机科学家鲍尔和迪克斯特拉(F.L.Bauer和E.W.Dijkstra)发明了一个类似的方案,但把操作符作为后缀而不是前缀。他们将其命名为逆波兰记法,以纪念它的创始人。

逆波兰记法(Reverse Polish Notation,RPN)在70年代和80年代逐渐普及,主要运用于手持科学计算器、如本章所述,RPN 在基于栈的计算系统上很容易实现.RPN 目前仍是一些编程语言(比如 PostFix)的基础。

|那么,计算机能实现正波兰记法吗?能,但要难得多,因为在读一个操作符的时候还不知道它应用于什么,要写一个正波兰解释器,最好的方案是将所有项(token)都读入一个列表,再反转列表!

为 RPN 使用栈

本书之前已讲到了栈(stack)的概念。最开始接触的是用于存储局部变量、实参和返回地址的“栈”。然后,第 12 章讲到了为汉诺塔问题设计的自定义栈。这是一个特殊栈,它做的事情比较专业:跟踪空白(盘),例如,对于一叠5盘的汉诺塔,如某一塔(一个栈)只有两个最小的盘,没有其他盘,那么该栈可表示成(0,0,0,1,2)。

STL 提供了一个常规栈类来做和其他栈相同的事情。STL 栈是一个简单的后入先出(LIFO)
机制。

下面描述了如何实现RPN计算器。同样是下面这个输入行:

​ 2 3 + 17 10 - *

如何解决它?常识告诉我们两件事情:首先,当程序读取一个数字时,必须保存下来供以后使用;其次,当程序读取一个操作符时,应执行一个操作,对两个操作数执行数据处理并保存结果。

所以,我们的策略如下。

  1. 程序读取数字时,把它压入(push)栈顶。
  2. 程序读取操作符(+,*,-或/,从栈中弹出(pop)两个值,计算结果,再将结果压回栈。由于是后入先出,所以操作符绑定的是它前面最靠近的两个表达式,这正是我们想要的。

下面来看看具体如何处理输入行 2 3 + 17 10 - *。

首先,这个算法读取数字 2 和 3,入栈。下图中,sp 是代表栈顶的栈指针(stack pointer)
STL 没有提供该指针的访问方式,但它有助于理解。

在这里插入图片描述

如下图所示,接着读取一个加号(+)。两个数字出栈,执行加法运算,结果入栈。

在这里插入图片描述

如下图所示,然后读取两个数字17和10,压入栈顶。
在这里插入图片描述

如下图所示,接着(很快就好了),算法读取下一个操作符(-)。同样两个数字出栈,执行计算,结果入栈。
在这里插入图片描述

如下图所示,最后,算法读取一个乘法操作符(*),最后一次两个数字出栈,执行乘法运算,结果入栈.

在这里插入图片描述

最终结果35,正确!

常规 STL 栈类简介

上一节演示了如何用一个简单的存储数字的栈机制来实现逆波兰记法计算器。现在介绍在程序中使用的STL栈。

为使用栈模板,需添加以下指令来开启对它的支持:

#include <stack>

然后,就可采用和 STL 列表相似的语法创建一个常规的栈机制:

stack<类型> 栈名;

记住,和列表模板一样,除非事先包含一个 using namespace std;语句,否则 std:: 前缀是必须的,即 std::stack。

下面是一些栈的例子:

# include <stack>
using namespace std;
、、、
stack<int>	stack_of_ints;
stack<Fraction> stack_of_Fraction_objects;
stack<double> xStack;

每个语句都建一个空栈。插入元素要用push成员函数。下表总结了常用的stack成员函数。

栈类函数说明
stack.push(data)将数据(具有栈的基础类型)压入栈顶
stack.top()从栈顶返回数据但不删除;删除要用pop
stack.pop()删除栈顶项(但不返回它的值)
stack.size()返回栈中当前所有项的数量
stack.empty()空栈返回true;否则返回false

用栈类将数据压入栈顶很简单:

stack<int> stack_of_ints;
stack_of_ints.push(-5);

但 STL 栈的设计是将“出栈”操作分为两步,所以为实现“返回栈顶项并删除",需同时执行 top 和pop 两个操作:

int n = stack_of_ints.top();	// 拷贝栈顶项
stack_of_ints.pop();	//删除栈顶项

例 13.2:逆波兰计算器

Visual Studio目前将 strtok 函数定义为不安全函数,要继续使用该函数而不报错,方案是在项目属性对话框中编辑预处理器定义,添加 _CRT SECURENO WARNINGS 这一行。

// rpn.cpp
# define _CRT_SECURE_NO_WARNINGS
# include <iostream>
# include <cstring>	// 使用旧式 cstring 以便使用 strtok 函数
# include <stack>

using namespace std;
# define MAX_CHARS 100

int main()
{
	char input_str[MAX_CHARS], *p;
	stack<double> num_stack;
	int c;
	double a, b, n;

	cout << "输入 RPN 字符串:";
	cin.getline(input_str, MAX_CHARS);
	p = strtok(input_str, " ");
	while (p)
	{
		c = p[0];
		if (c == '+' || c == '+' || c == '/' || c == '-')
		{
			if (num_stack.size() < 2)
			{
				cout << "错误:操作数不足或操作符太多。" << endl;
				return -1;
			}
			b = num_stack.top();
			num_stack.pop();
			a = num_stack.top();
			num_stack.pop();
			switch (c)
			{
			case '+': n = a + b; break;
			case '*': n = a * b; break;
			case '/': n = a / b; break;
			case '-': n = a - b; break;
			}
			num_stack.push(n);
		}
		else
		{
			num_stack.push(atof(p));
		}
		p = strtok(nullptr, " ");
	}
	cout << "答案是:" << num_stack.top() << endl;
	return 0;
}

和往常一样,程序以#include指令开头。注意用开启对STL栈模板的支持.

#include <stack>

这样就可创建任何基类型的栈。这里要用什么类型?
明显是 double 浮点类型,因为没理由限制用户只能输入整数。我们想执行1.4加2.345这样的计算。下个语句创建一个 double 栈。

stack<double> num_stack;

接着,程序从用户获取一行字符串输入并开始分解。如第8章所述,strtok 函数是分解字符串的“好手”,它在输入字符串中查找第一个 token(一个字或项).strtok 的第一个参数是输入字符串,第二个参数是作为 token 分隔符(定界符)使用的字符(可以是多个字符,这里是空格)。

p = strtok(input_str, "");

函数返回一个指针,指向包含第一个 token 的一个子字符串。注意,为了正确工作,每一项都必须由一个或多个空格分隔,其中包括操作符。例如,以下输入能正确工作:

2 3 + 17 10 - *

但以下输入不能正确工作:

2 3+ 17 10-*

虽然这个输入本应合理,但需要自己写更高级的词法分析器。C++14库包含对正则表达式的支持,可用来进行“分词"(tokenizing),但那属于高级主题。

strtok 在调用一次后可再次调用,并指定 nullptr 作为第一个参数,表示“从刚才使用的输入字符串中获取下个token"。换言之,nullptr 参数可用于获取下个 token,再下个,以此类推,不需要从头查找。

程序在主循环底部执行该函数调用。

P = strtok(nullptr, "");

主循环一直处理下一个 token(只要有),拿到一个 token 后,首先判断它是不是操作符(+,*,-或/),如果是,就做几件事情。第一件事情是确保栈上至少有两项。这很重要,因为对空栈执行出栈操作,STL pop 函数会进入“阴阳魔界”并造成严重问题。为防止出问题,程序用一个短的错误检查小节打印错误消息并退出。

if(num-stack.size()) < 2){
	cout << "Error:too many ops." << endl;
    return-1;
}

对操作符做的第二件事情是让两个数字出栈。分别放到变量 b 和 a 中。记住,栈后入先出,所以必须考虑顺序问题。

b = num_stack.top();
numstack.pop();
a = num_stack.top();
num_stack.pop();

使用 STL 栈类,出栈操作要分两步走,即先 top 再 pop。这两个成员函数分别负责出栈的一部分操作。

对操作符做的第三件事情是执行指定计算并使结果入栈。程序使用了第3章讲过的 switch-case 逻辑。取决于操作符是+,*,/还是-,程序跳转到对应的case语句,执行计算并从switch块退出(break)。

switch(c){
	case '+': n = a + b; break;
    case '*': n = a * b; break;
    case '/': n = a / b; break;
    case '-': n = a - b; break;
}

计算完成后,结果(n)入栈。

num_stack.push(n);

这就完成了对操作符的所有处理。如果分解出来的项不是操作符,要做的事情就简单多了。只需将其转换成浮点数,入栈即可。

num_stack.push(atof(p));

如果该项不是有效数字怎么办?例如,如果是字母呢?问题不大,atof 会返回 0,对 0 进行运算是可以的(只是不要除以它)。

练习

练习13.2.1

扩展例13.2的 RPN 计算器,添加对一元操作符#的支持,它默认计算倒数。例如,x 的计算结果是1/x,记住之前的四个操作符全部都是二元操作符,需获取两个操作数,但一元操作符的语法如下:

表达式 → 表达式 一元操作符

答案:

# define _CRT_SECURE_NO_WARNINGS
#include <iostream>
#include <cstring>  // Use old-style cstrings so that we
//  can use the strtok function.
#include <stack>

using namespace std;
#define MAX_CHARS 100

int main()
{
	char input_str[MAX_CHARS], *p;
	stack<double>  num_stack;
	int c;
	double a, b, n;

	cout << "Enter RPN string: ";
	cin.getline(input_str, MAX_CHARS);
	p = strtok(input_str, " ");
	while (p) {
		c = p[0];
		if (c == '+' || c == '*' || c == '/' || c == '-') {
			if (num_stack.size() < 2) {
				cout << "Error: too many ops." << endl;
				return -1;
			}
			b = num_stack.top(); num_stack.pop();
			a = num_stack.top(); num_stack.pop();
			switch (c) {
			case '+': n = a + b; break;
			case '*': n = a * b; break;
			case '/': n = a / b; break;
			case '-': n = a - b; break;
			}
			num_stack.push(n);
		}
		else if (c == '#') {
			if (num_stack.size() < 1) {
				cout << "Error: too many ops." << endl;
				return -1;
			}
			a = num_stack.top(); num_stack.pop();
			num_stack.push(1.0 / a);
		}
		else {
			num_stack.push(atof(p));
		}
		p = strtok(nullptr, " ");
	}
	cout << "The answer is: " << num_stack.top() << endl;
	return 0;
}

练习13.2.2

添加 ^ 操作符来执行一元取反,即反转操作数的正负号。

答案:

# define _CRT_SECURE_NO_WARNINGS
#include <iostream>
#include <cstring>  // Use old-style cstrings so that we
//  can use the strtok function.
#include <stack>

using namespace std;
#define MAX_CHARS 100

int main()
{
	char input_str[MAX_CHARS], *p;
	stack<double>  num_stack;
	int c;
	double a, b, n;

	cout << "Enter RPN string: ";
	cin.getline(input_str, MAX_CHARS);
	p = strtok(input_str, " ");
	while (p) {
		c = p[0];
		if (c == '+' || c == '*' || c == '/' || c == '-') {
			if (num_stack.size() < 2) {
				cout << "Error: too many ops." << endl;
				return -1;
			}
			b = num_stack.top(); num_stack.pop();
			a = num_stack.top(); num_stack.pop();
			switch (c) {
			case '+': n = a + b; break;
			case '*': n = a * b; break;
			case '/': n = a / b; break;
			case '-': n = a - b; break;
			}
			num_stack.push(n);
		}
		else if (c == '#') {
			if (num_stack.size() < 1) {
				cout << "Error: too many ops." << endl;
				return -1;
			}
			a = num_stack.top(); num_stack.pop();
			num_stack.push(1.0 / a);

		}
		else if (c == '^') {
			if (num_stack.size() < 1) {
				cout << "Error: too many ops." << endl;
				return -1;
			}
			a = num_stack.top(); num_stack.pop();
			num_stack.push(-1.0 * a);

		}
		else {
			num_stack.push(atof(p));
		}
		p = strtok(nullptr, " ");
	}
	cout << "The answer is: " << num_stack.top() << endl;
	return 0;
}

练习13.2.3

修改程序,反复提示用户输入下一个算式,直到直接按 ENTER 键输入一个空行来退出,也就是说,持续提示输入,作为 RPN 解释,打印答案,直到用户想要退出,而不是像以前那样运算一次就退出。顺便说一下,每次开始新的运算之i都要将栈清空.

答案:

# define _CRT_SECURE_NO_WARNINGS
#include <iostream>
#include <cstring>  // Use old-style cstrings so that we
//  can use the strtok function.
#include <stack>

using namespace std;
#define MAX_CHARS 100

int main()
{
	char input_str[MAX_CHARS], *p;
	stack<double>  num_stack;
	int c;
	double a, b, n;

	while (true) {
		cout << "Enter RPN string: ";
		cin.getline(input_str, MAX_CHARS);
		if (strlen(input_str) == 0) {  // <- This is where the
			break;                       // exit to main loop is added.
		}
		p = strtok(input_str, " ");
		while (p) {
			c = p[0];
			if (c == '+' || c == '*' || c == '/' || c == '-') {
				if (num_stack.size() < 2) {
					cout << "Error: too many ops." << endl;
					return -1;
				}
				b = num_stack.top(); num_stack.pop();
				a = num_stack.top(); num_stack.pop();
				switch (c) {
				case '+': n = a + b; break;
				case '*': n = a * b; break;
				case '/': n = a / b; break;
				case '-': n = a - b; break;
				}
				num_stack.push(n);
			}
			else if (c == '#') {
				if (num_stack.size() < 1) {
					cout << "Error: too many ops." << endl;
					return -1;
				}
				a = num_stack.top(); num_stack.pop();
				num_stack.push(1.0 / a);

			}
			else if (c == '^') {
				if (num_stack.size() < 1) {
					cout << "Error: too many ops." << endl;
					return -1;
				}
				a = num_stack.top(); num_stack.pop();
				num_stack.push(-1.0 * a);

			}
			else {
				num_stack.push(atof(p));
			}
			p = strtok(nullptr, " ");
		} // end inner while
		cout << "The answer is: " << num_stack.top() << endl;
	} // end outer while
	return 0;
}

13.3 正确解释尖括号

尖括号(<和>)在 C++中具有多重意义,所以大量运用模板时可能出现歧义。以下声明存在 C++ 语法问题:

listestack <int>> list_of_stacks;

这里本应创建一个栈列表,这完全是允许的,C++本来就允许创建包含容器类的容器类.
不管多复杂,都是有效的.

但在本例中,传统 C++遭遇了语法上的挑战,一般将连续两个右尖括号(>>)解释成右移位操作符,这造成了语法错误,顺便说一下,在 cin 等对象中,同一个操作符被重载为数据流入操作符。

所以在传统 C++ 中,需在两个右尖括号之间插入空格,这样才能正确解释。

list<stack <int> list_of_stacks;

不过C++11和后续版本就不需要添加这个空格了,现在能根据上下文正确解释连续两个右尖括号的语义。

小结

  • 启用列表模板需要使用以下include指令:
    #include <list>
    
  • 每次使用 list 这个名称都要限定为 std::list。当然,除非在程序中添加了以下 using 语句:
    using namespace std;
    
  • 用以下语法声明列表容器:
    list<类型> 列表名
    
  • 创建好列表后,用 push_back(列表末端)和 push_front(列表前端)添加相应类型的项:
    #include <list>
    using namespace std;
    … …
    list<int> Ilist;
    Ilist.push_back(11);
    Ilist.push_back(42);
    
  • 创建迭代器来访问列表成员。迭代器不是指针,但使用了几个一样的操作符。
    例如:
    list<int>::iterator iter;
    
  • 利用列表的函数 begin 和 end 遍历所有项。例如,以下代码打印列表的每一项,一项一行。
    for(iter = Ilist.begin(); iter !=  Ilist.end(); i++)
      cout << *iter << endl;
    
  • 和列装类一样,用一个 #include 开启对后入先出 (LIFO) 栈类的支持:
    #include <stack>
    using namespace std;
    … …
    stack<string> my_stack;
    
  • push函数将一项压入栈顶。
    my_stack.push("dog");	//压入栈顶
    
  • 要从栈顶弹出一项(出栈),需同时调用 top 和 pop 函数。
    string s =my_stack.top();	//返回栈顶的项
    my_stack.pop();	//删除栈顶的项
    
  • 对空栈执行出栈操作是严重错误,所以务必事先调用 size 或 empty 函数来检查。
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值