《C程序设计语言》第四章 函数和程序结构
2011-12-15 21:09 java线程例子 阅读(303) 评论(0) 编辑 收藏 举报4.1 函数的基本知识
如果函数定义中省略了返回值类型,则默认为int类型。
练习4-1 编写函数strindex(s, t),它返回字符串t在s中最右边出现的位置。
如果s中不包含t,则返回-1。
答:
#include <stdio.h>
int strindex(char s[], char t[])
{
int i, j, k;
for (i = 0; s[i] != '\0'; i++) // to end of s
;
for (i = i - 1; i >= 0; i--) {
for (j = 0; t[j] != '\0'; j++) // to end of t
;
// compare s and t in reverse order
for (j = j - 1, k = i; j >= 0 && k >= 0; j--, k--)
if (s[k] != t[j])
break;
if (j == -1)
return k + 1;
}
return -1;
}
main()
{
char s[] = "thisiscdaiandrachel";
char t1[] = "cdai";
char t2[] = "xyz";
char t3[] = "this";
int strindex(char s[], char t[])
{
int i, j, k;
for (i = 0; s[i] != '\0'; i++) // to end of s
;
for (i = i - 1; i >= 0; i--) {
for (j = 0; t[j] != '\0'; j++) // to end of t
;
// compare s and t in reverse order
for (j = j - 1, k = i; j >= 0 && k >= 0; j--, k--)
if (s[k] != t[j])
break;
if (j == -1)
return k + 1;
}
return -1;
}
main()
{
char s[] = "thisiscdaiandrachel";
char t1[] = "cdai";
char t2[] = "xyz";
char t3[] = "this";
char t4[] = "elx";
printf("%d\n", strindex(s, t1));
printf("%d\n", strindex(s, t2));
printf("%d\n", strindex(s, t3));
printf("%d\n", strindex(s, t1));
printf("%d\n", strindex(s, t2));
printf("%d\n", strindex(s, t3));
printf("%d\n", strindex(s, t4));
}
}
4.2 返回非整型值的函数
由于atof函数的返回值类型不是int,因此该函数必须声明返回值的类型。
其次,调用函数必须知道atof函数返回的是非整型值。可以在调用函数中显示声明atof函数。
#include <stdio.h>
#define MAXLINE 100
main()
{
double sum, atof(char []);
char line[MAXLINE];
int getline(char line[], int max);
sum = 0;
while (getline(line, MAXLINE) > 0)
printf("\t%g\n", sum += atof(line));
return 0;
}
函数atof的声明与定义必须一致。如果atof函数与调用它的main函数放在同一源文件中,
并且类型不一致,编译器会检测到该错误。如果atof函数式单独编译的,这种不匹配的
错误就无法检测出来。
如果没有函数原型,则函数将在第一次出现的表达式中被隐式声明。函数的返回值将被
假定为int类型,但上下文并不对其参数作任何假设。
练习4-2 对atof函数进行扩充,使它可以处理形如123.45e-6的科学表示法。其中,浮点数
后面可能会紧跟一个e或E以及一个指数(可能有正负号)。
答:
#include <stdio.h>
#include <ctype.h>
#include <ctype.h>
/* atof: convert string s to double */
double atof(char s[])
{
double val, power, epow;
int i, sign, j, esign;
for (i = 0; isspace(s[i]); i++) /* skip white space */
;
// 1. Sign
sign = (s[i] == '-') ? -1 : 1;
if (s[i] == '+' || s[i] == '-')
i++;
// 2. Integer part
for (val = 0.0; isdigit(s[i]); i++)
val = 10.0 * val + (s[i] - '0');
// 3. Float part
if (s[i] == '.')
i++;
for (power = 1.0; isdigit(s[i]); i++, power *= 10)
val = 10.0 * val + (s[i] - '0');
// 4. E part
if (s[i] == 'e' || s[i] == 'E')
i++;
esign = (s[i] == '-') ? -1 : 1;
if (s[i] == '+' || s[i] == '-')
i++;
for (j = 0; isdigit(s[i]); i++) {
j = 10 * j + (s[i] - '0');
}
for (epow = 1.0; j > 0; j--) {
if (esign == -1)
epow /= 10;
else
epow *= 10;
}
return sign * val / power * epow;
}
main()
{
printf("%.2lf\n", atof("4.85E3"));
printf("%.2lf\n", atof("3.6"));
printf("%.2lf\n", atof("3.6e5"));
printf("%.2lf\n", atof("-3.6e5"));
printf("%.8lf\n", atof("-3.6e-5"));
printf("%.2lf\n", atof("3.6e12"));
}
4.3 外部变量
外部变量定义在函数之外,可以在许多函数中使用。
自动变量在其所在函数退出时消失,而外部变量是永久存在的。
C语言不允许在一个函数中定义其他函数,因此函数本身是外部的。
通过同一名字对外部变量的所有引用(即使这种引用来自于单独编译的不同函数)实际上都是引用同一个对象。
4.4 作用域
名字的作用域指的是程序中可以使用该名字的部分。
外部变量或函数的作用域从声明它的地方开始,到其所在文件的末尾结束。
例如:
main() { ... }
int sp = 0;
double val[MAXVAL];
void push(double f) { ... }
double pop(void) { ... }
在push与pop中不需进行任何声明就可以访问变量sp与val,但是这两个变量名不能用在main函数中,
push与pop函数也不能用在main函数中。
如果要在外部变量的定义之前使用该变量,或者外部变量的定义与变量的使用不在同一个源文件中,
则必须在相应的变量声明中强制性地使用关键字extern。将外部变量的声明与定义严格区分开来很重要。
变量声明用于说明变量的类型,而变量定义除此之外还将引起存储器的分配。
上面外部变量sp和val[]的定义int sp = 0; double val[MAXVAL];将会分配存储单元;而声明extern int sp;
extern double val[];则不会。定义中必须制定数组的长度,但extern声明则不一定要指定。
在一个源程序的所有源文件中,一个外部变量只能在某个文件中定义一次,而其他文件可以通过extern
声明来访问它。
4.5 头文件
首先分割文件:
主函数main单独放在main.c中;
push与pop函数放在stack.c;
getop放在getop.c
将getch与ungetch函数放在getch.c中。
之所以分割成多个文件,主要是考虑在实际的程序中,它们分别来自于单独编译的库。
此外,必须考虑定义和声明在这些文件之间的共享问题。尽可能把共享的部分集中在一起,放在
头文件calc.h中。
对于某些中等规模的程序,最好只用一个头文件存放程序中各部分共享的对象。
较大的程序需要使用更多的头文件,我们需要精心地组织它们。
4.6 静态变量
文件stack.c中定义的变量sp与val以及文件getch.c中定义的变量buf与bufp,它们仅供其所在的
源文件中的函数使用,其他函数不能访问。用static限定外部变量与函数,可以将其后声明的对象
的作用域限定为被编译源文件的剩余部分,达到隐藏外部对象的目的。
static char buf[BUFSIZE];
static int bufp = 0;
int getch(void) { ... }
void ungetch(int c) { ... }
其他函数就不能访问变量buf与bufp,因此这两个名字不会和同一程序中的其他文件中的相同名字
相冲突。
static也可用于声明函数,则该函数名除了对该函数声明所在的文件可见外,其他文件都无法访问。
static也可用于声明内部变量。static类型的内部变量是一种只能在某个特定函数中使用但一直占据
存储空间的变量。
练习4-11 修改getop函数,使其不必使用ungetch函数。
4.7 寄存器变量
register声明告诉编译器,将register变量放在机器的寄存器中。但编译器可以忽略此选项。
register int x;
f(register unsigned m)
4.8 程序块结构
变量i与程序块外声明的i无关。
if (n > 0) {
int i;
}
自动变量(包括形参)可以隐藏同名的外部变量与函数。
int x;
int y;
f(double x)
{
double y;
}
4.9 初始化
在不进行显式初始化的情况下,外部变量和静态变量都将被初始化为0;
而自动变量和寄存器变量的初值为无用的信息。
对于外部变量与静态变量来说,初始化表达式必须是常量表达式,且只初始化一次;
对于自动变量与寄存器变量,则在每次进入函数或程序块时都将被初始化。
对于自动变量与寄存器变量,初始化表达式可以不是常量表达式,表达式可以包含任意
在此表达式之前已经定义的值,包括函数调用。
数组的初始化可以在声明的后面紧跟一个初始化表达式列表。
int days[] = { 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31 };
当省略数组的长度时,编译器将把花括号中初始化表达式的个数作为数组的长度。
如果初始化表达式的个数比数组元素少,则没有初始化的元素将被初始化为0。
如果初始化表达式的个数比数组元素多,则是错误的。
字符数组的初始化比较特殊:可以用一个字符串来代替初始化表达式序列。
char pattern[] = "ould"; 等价于 char pattern[] = { 'o', 'u', 'l', 'd' };
4.10 递归
将一个数作为字符串打印的情况。数字是低位先于高位生成的,但必须以与此相反的次序打印。
#include <stdio.h>
/* printd: print n in decimal */
void printd(int n)
{
if (n < 0) {
puchar('-');
n = -n;
}
if (n / 10)
printd(n / 10);
putchar(n % 10 + '0');
}
递归并不节省存储器的开销,因为递归调用过程中必须在某个地方维护一个存储处理值的栈。
递归的执行速度并不快,但递归代码比较紧凑,并且比相应的非递归代码更易于编写和理解。
在描述树等递归定义的数据结构时使用递归尤为方便。
练习4-12 运用printd函数的设计思想编写一个递归版本的itoa函数,即通过递归调用把整数
转换为字符串。
答:
#include <stdio.h>
#include <math.h>
void itoa(int n, char s[])
{
static int i;
if (n / 10)
itoa(n / 10, s);
else {
i = 0;
if (n < 0)
s[i++] = '-';
}
s[i++] = abs(n) % 10 + '0';
s[i] = '\0';
}
main()
{
char s[10];
itoa(189271, s);
printf("%s\n", s);
itoa(-9012398, s);
printf("%s\n", s);
}
#include <math.h>
void itoa(int n, char s[])
{
static int i;
if (n / 10)
itoa(n / 10, s);
else {
i = 0;
if (n < 0)
s[i++] = '-';
}
s[i++] = abs(n) % 10 + '0';
s[i] = '\0';
}
main()
{
char s[10];
itoa(189271, s);
printf("%s\n", s);
itoa(-9012398, s);
printf("%s\n", s);
}
练习4-13 编写一个递归版本的reverse(s)函数,以将字符串s倒置。
答:
void reverser(char s[], int i, int len)
{
int c, j;
j = len - (i + 1);
if (i < j) {
c = s[i];
s[i] = s[j];
s[j] = c;
reverser(s, ++i, len);
}
}
本题不适合用递归方法来解决。
4.11 C预处理器
预处理器是编译过程中单独执行的第一个步骤。两个最常用指令:#include和#define指令。
以及其他的一些,如条件编译与带参数的宏。
文件包含
#include "文件名":在源文件所在位置查找文件
#include <文件名>
在大的程序中,#include指令是将声明捆绑在一起的较好的方法。保证了所有的源文件都具有
相同的定义和变量声明。
宏替换
#define 名字 替换文本
这是一种最简单的宏替换,后续所有出现名字记号的地方都将被替换为替换文本。
替换只对记号进行,对括在引号中的字符串不起作用。
替换文本可以是任意的,如:#define forever for(;;)
宏定义也可以带参数,这样可以对不同的宏调用使用不同的替换文本。
如#define max(A, B) ((A) > (B) ? (A) : (B))
则x = max(p+q, r+s)将被替换为x = ((p+q) + (r+s)) ? (p+q) : (r+s));