代码改变世界

Functions and Program Structure_2

2017-02-09 18:07  星星之火✨🔥  阅读(301)  评论(0)    收藏  举报

注:本篇所有内容都是对TCPL读书笔记&心得第40条的扩充,搞懂了这部分对体会函数与程序结构的好处和意义应该是大有脾益的。

1、在有了基本框架后,对计算器程序进行扩充就比较简单了。在该程序中加入取模(%)运算符,并注意考虑负数的情况。

  这里,我们只需要修改主程序和函数getop 即可满足要求,修改后的主程序如下:

#include <stdio.h>
#include <stdlib.h> // for atof()
#include <math.h> // for fmod()

#define MAXOP 100 // max size of operator
#define NUMBER '0' // signal that a number was found

int getop(char []);
void push(double);
double pop(void);

// reverse Polish calculator
int main(void)
{
	int type;
	double op2;
	char s[MAXOP];
	
	while((type = getop(s)) != EOF)
	{
		switch(type)
		{
			case NUMBER:
				push(atof(s));
				break;
			case '+':
				push(pop() + pop());
				break;
			case '-':
				op2 = pop();
				push(pop() - op2);
				break;
			case '*':
				push(pop() * pop());
				break;
			case '/':
				op2 = pop();
				if(op2 != 0.0)
					push(pop() / op2);
				else 
					printf("error: zero divisor\n");
				break;
			case '%':
				op2 = pop();
				if(op2 != 0.0)
					push(fmod(pop(), op2));
				else
					printf("error: zero divisor");
					break;
			case '\n':
				printf("\t%.8g\n", pop());
				break;
			default:
				printf("error: unknown command %s", s);
				break;
		}
	}	
	return 0;
}

  修改后的getop 函数如下:

#include <stdio.h>
#include <string.h>
#include <ctype.h>

#define NUMBER '0' // signal that a number was found

int getch(void);
void ungetch(int);

// getop: get next operator or numeric operand
int getop(char s[])
{
	int i, c;
	
	while((s[0] = c = getch()) == ' ' || c == '\t')
		;
	s[1] = '\0';
	i = 0;
	if(!isdigit(c) && c != '.' && c != '-')
		return c; // not a number
	if(c == '-')
	{
		if(isdigit(c = getch()) || c == '.')
			s[++i] = c; // negative number
		else
		{
			if(c != EOF)
				ungetch(c);
			return '-'; // minus sign
		}
	}
	if(isdigit(c)) // collect integer part
		while(isdigit(s[++i] = c = getch()))
			;
	if(c == '.') // collect fraction part
		while(isdigit(s[++i] = c = getch()))
			;
	s[i] = '\0';
	if(c != EOF)
		ungetch(c);
	return NUMBER;
}

  附注:getop函数需要检查紧跟在符号- 后面的那个字符,以判断该符号到底代表一个减号,还是代表一个负号。比如说:- 1(- 和1 之间有一个空格)是一个减号后面跟一个数字,而-1.23(- 和1 之间没有空格)是一个负数。
  可以用下面两组数据简单测试一下扩展后的计算器能否正常工作:1 -1 +(结果为0) -10 3 %(结果为-1)。

2、在栈操作中添加几个命令,分别用于在不弹出元素的情况下打印栈顶元素;复制栈顶元素;交换栈顶两个元素的值。另外增加一个命令用于清空栈。

  这里只需要对主函数和与栈相关的函数修改即可达到目的,对主函数的修改如下:

#include <stdio.h>
#include <stdlib.h> // for atof()

#define MAXOP 100 // max size of operand or operator
#define NUMBER '0' // signal that a number was found

int getop(char []);
void push(double);
double pop(void);
void clear(void);

int main(void)
{
	int type;
	double op1, op2;
	char s[MAXOP];
	
	while((type = getop(s)) != EOF)
	{
		switch(type)
		{
			case NUMBER:
				push(atof(s));
				break;
			case '+':
				push(pop() + pop());
				break;
			case '*':
				push(pop() * pop());
				break;
			case '-':
				op2 = pop();
				push(pop() - op2);
				break;
			case '/':
				op2 = pop();
				if(op2 != 0.0)
					push(pop() / op2);
				else
					printf("error: zero divisor");
				break;
			case '?': // print top element of the stack
				op2 = pop();
				printf("\t%.8g\n", op2);
				push(op2);
				break;
			case 'c': // clear the stack
				clear();
				break;
			case 'd': // duplicate top elem. of the stack
				op2 = pop();
				push(op2);
				push(op2);
				break;
			case 's': // swap the top two elements
				op1 = pop();
				op2 = pop();
				push(op1);
				push(op2);
				break;
			case '\n': 
				printf("\t%.8g\n", pop());
				break;
			default:
				printf("error: unknown command %s\n", s);
				break;	
		}
	}
	return 0;
}

  对与栈相关的函数修改如下:

#include <stdio.h>
#include "calc.h"
#define MAXVAL 100 // maximum depth of val stack

int sp = 0; // next free stack position
double val[MAXVAL]; // value stack

// push: push f onto value stack
void push(double f)
{
	if(sp < MAXVAL)
		val[sp++] = f;
	else
		printf("error: stack full, can't push %g\n", f);
}

// pop: pop and return top value from stack
double pop(void)
{
	if(sp > 0)
		return val[--sp];
	else
	{
		printf("error: stack empty\n");
		return 0.0;
	}
}

// clear: clear the stack
void clear(void)
{
	sp = 0;
}

  现有的换行操作符将弹出栈顶元素并打印。我们新增的"?" 将弹出栈顶元素并打印,再把它压入堆栈。"?" 操作符不会像换行操作符那样把栈顶元素永久地弹出堆栈,而采用"出栈、打印、入栈"这一方案的原因是为了避免主程序直接对堆栈和堆栈指针变量sp 进行操作。

  复制栈顶元素的过程是:先弹出栈顶元素,再把它压入两次。

  交换栈顶两个元素的操作过程是:先依次弹出两个栈顶元素,在按相反的顺序把他们压入堆栈。

  清除堆栈内容的工作很容易完成:把sp 设置为零即可。我们增加了一个完成这项工作的函数,并把它与push 和pop 放置在一起。这样做的目的是只允许这三个函数维护堆栈、访问堆栈及堆栈指针变量。

3、给计算器增加访问sin、exp 与pow 等库函数的操作。这里需要修改的部分有main 函数、getop 函数,此外我们增加了一个新的函数mathfnc。

  main 函数如下:

#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <math.h>

#define MAXOP 100 // max size of operand or operator
#define NUMBER '0' // signal that a number was found
#define NAME 'n' // signal that a name was found

int getop(char []);
void push(double);
double pop(void);
void mathfnc(char []);

// reverse Polish calculator
int main(void)
{
	int type;
	double op2;
	char s[MAXOP];
	
	while((type = getop(s)) != EOF)
	{
		switch(type)
		{
			case NUMBER:
				push(atof(s));
				break;
			case NAME:
				mathfnc(s);
				break;
			case '+':
				push(pop() + pop());
				break;
			case '*':
				push(pop() * pop());
				break;
			case '-':
				op2 = pop();
				push(pop() - op2);
				break;
			case '/':
				op2 = pop();
				if(op2 != 0.0)
					push(pop() / op2);
				else
					printf("error: zero divisor\n");
				break;
			case '\n':
				printf("\t%8g\n", pop());
				break;
			default:
				printf("error: unknown command %s\n", s);
				break;	
		}
	}
	return 0;
}

  getop 函数如下:

#include <stdio.h>
#include <string.h>
#include <ctype.h>

#define NUMBER '0' // signal that a number was found
#define NAME 'n' // signal that a name was found

int getch(void);
void ungetch(int);

// getop: get next operator, numeric operand, or math fnc
int getop(char s[])
{
	int c, i;
	
	while((s[0] = c = getch()) == ' ' || c == '\t')
		;
	s[1] = '\0';
	i = 0;
	if(islower(c)) // command or NAME
	{
		while(islower(s[++i] = c = getch()))
			;
		s[i] = '\0';
		if(c != EOF)
			ungetch(c); // went one char too far
		if(strlen(s) > 1)
			return NAME; // >1 char; it is NAME
		else
			return c; // it may be a command
	}	
	if(!isdigit(c) && c != '.')
		return c; // not a number
	if(isdigit(c)) // collect integer part
		while(isdigit(s[++i] = c = getch()))
			;
	if(c == '.') // collect fraction part
		while(isdigit(s[++i] = c = getch()))
			;
	s[i] = '\0';
	if(c != EOF)
		ungetch(c);
	return NUMBER;
}

  新增函数mathfnc

// mathfnc: check string s for supported math functions 
void mathfnc(char s[])
{
	double op2;
	
	if(strcmp(s, "sin") == 0)
		push(sin(pop()));
	else if(strcmp(s, "cos") == 0)
		push(cos(pop()));
	else if(strcmp(s, "exp") == 0)
		push(exp(pop()));
	else if(strcmp(s, "pow") == 0)
	{
		op2 = pop();
		push(pow(pop(), op2));
	}
	else 
		printf("error: %s not supported\n", s);
}

  改进后的getop 函数能够识别出一个由小写字母开头的字符串并把它返回为类型NAME。而主程序将把NAME 识别为一个合法的类型并调用函数mathfnc。

  函数mathfnc 是新增加的,它将通过一系列if 语句来寻找与字符串s 匹配的函数名。

  我们的针对性测试数据:

  表达式 3.1415926 2 / sin 的结果为1。 3.1415926 2 / sin 0 cos + 的结果为2。5 2 pow 4 2 pow + 的结果为41。

  getop 函数并不知道具体将会调用哪个数学函数,它只负责识别并返回它找到的字符串。这种做法的好处是可以很容易地在mathfnc 函数中添加其他的数学函数。

 4、编写一个函数ungets(s),将整个字符串s 压回到输入中。ungets 函数需要使用buf 和bufp 吗?它能否仅使用ungetch 函数?

#include <string.h>

// ungets: push string back onto the input
void ungets(char s[])
{
	int len = strlen(s);
	void ungetch(int);
	
	while(len > 0)
		ungetch(s[--len]);
}

  函数ungets 将调用函数ungetch len 次,每次都会把字符串s 中的一个字符重新压回输入。ungets 函数将按逆序把字符串重新压回输入。

  ungets 函数不需要直接对buf 和bufp 进行操作,buf、bufp 和出错检查将由函数ungetch 处理。

5、假定最多只压回一个字符,请相应地修改getch 和ungetch 这两个函数。

#include <stdio.h>

char buf = 0;

// getch: get a (possibly pushed back) character
int getch(void)
{
	int c;
	
	if(buf != 0)
		c = buf;
	else
		c = getchar();
	buf = 0;
	return c;
}

// ungetch: push character back onto the input
void ungetch(int c)
{
	if(buf != 0)
		printf("ungetch: too many characters\n");
	else
		buf = c;
}

  根据题目要求,输入缓冲区buf 将不再是一个数组,因为任意时刻该缓冲区最多保存一个字符;buf 的初值为0,而每次getch 函数在读取一个字符之后,会再次把buf 设置为0。ungetch 函数把一个字符重新压回输入缓冲区之前要首先检查该缓冲区是否为空。如果缓冲区不为空,ungetch 函数将产生一条出错信息。

6、以上介绍的getch 和ungetch 函数不能正确地处理压回的EOF。考虑压回EOF 时应该如何处理,请实现你的设计方案。

#include <stdio.h>

#define BUFSIZE 100

int buf[BUFSIZE]; // buffer for ungetch
int bufp = 0; // next free position in buf

// getch:get a (possibly pushed back) character
int getch(void)
{
	return (bufp > 0) ? buf[--bufp]:getchar();
}

// ungetch:push character back onto the input
void ungetch(int c)
{
	if(bufp >= BUFSIZE)
		printf("ungetch: too many characters.\n");
	else
		bufp[bufp++] = c;
}

原来的缓冲区buf 被声明为一个字符数组,而C语言不要求char 变量是signed 或unsigned 类型的。这就涉及到了系统底层的一些知识:
在某些机器上,把char 类型的值转换为int 型时,如果char 的最高位是1,那么转换成整形时高位补1,产生负值。而在另一些机器上不管char 的最高位是0还是1,都将补零,这样结果在整形表示上会是一个正数。因此EOF(-1)在字符型表示上是0XFF,而将其转换为整数时对不同的机器而言,有两种可能的结果:0XFFFF(-1) 或0X00FF(255)。为了防止这种荒缪的情况出现,所以我们将缓冲区buf声明成一个整数数组。

7、另一种方法是通过getline 函数读入整个输入行,这种情况下可以不使用getch 与ungetch 函数。请运用这种方法修改计算器程序。

#include <stdio.h>
#include <ctype.h>

#define MAXLINE 100
#define NUMBER '0' // signal that a number was found

int getline(char line[], int limit);

int li = 0; // input line index
char line[MAXLINE]; // one input line

// getop: get next operator or numeric operand
int getop(char s[])
{
	int c, i;
	
	if(line[li] == '\0')
	{
		if(getline(line, MAXLINE) == 0)
			return EOF;
		else
			li = 0;
	}
	
	while((s[0] = c = line[li++]) == ' ' || c == '\t' )
		;
	s[1] = '\0';
	if(!isdigit(c) && c != '.')
		return c; // not a number
	i = 0;
	if(isdigit(c)) // collect integer part
		while(isdigit(s[++i] = c = line[li++]))
			;
	if(c == '.') // collect fraction part
		while(isdigit(s[++i] = c = line[li++]))
			;
	s[i] = '\0';
	li--;
	return NUMBER;
}

该程序大体上的意思就是从保存到line 数组里的输入行里读取数据,然后转成字符串放到形参——数组s 中。每调用一次getop 函数,便取得一个操作符或者操作数,当getop 函数到达输入行的末尾或者尚未读入第一个输入行时,getop 将会调用getline 读入一个输入行。在getop 函数末尾,不再需要调用ungetch 把一个字符重新压回输入缓冲区,只需对变量li 减一便可达到后退一个字符的目的。

需要注意的一点:在C 语言中,任何函数都能使用和改变另外一个函数的外部变量。这就意味着li 和line 也有被getop 以外的其他函数改变的可能。如果想阻止这类情况发生,就应该把外部变量声明为static 变量

8、修改getop 函数,使其不必使用ungetch 函数。提示:可以使用一个static 类型的内部变量来解决该问题

#include <stdio.h>
#include <ctype.h>

#define NUMBER '0' // signal that a number was found

int getch(void);

// getop: get next operator or numeric operand
int getop(char s[])
{
	int c, i;
	static int lastc = 0;
	
	if(lastc == 0)
		c = getch();
	else
	{
		c = lastc;
		lastc = 0;
	}
	
	while((s[0] = c) == ' ' || c == '\t' )
		c = getch();
	s[1] = '\0';
	if(!isdigit(c) && c != '.')
		return c; // not a number
	i = 0;
	if(isdigit(c)) // collect integer part
		while(isdigit(s[++i] = c = getch()))
			;
	if(c == '.') // collect fraction part
		while(isdigit(s[++i] = c = getch()))
			;
	s[i] = '\0';
	if(c != EOF)
		lastc = c;
	return NUMBER;
}

当getop 被调用时,它首先检查变量lastc 中是否保存有前一个字符。如果没有,则调用getch 读取一个新字符;如果有,getop 就把该字符复制到变量c 并对lastc 清零。此外,因为getop 只有在检查完当前字符c 之后才需要读取一个新字符,所以我们对第一个while 语句也做了一些改动。