《C程序设计语言》第四章 函数和程序结构


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";
     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, 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>

/* 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);
}

练习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));





posted on 2011-12-15 21:09  毛小娃  阅读(211)  评论(0编辑  收藏  举报

导航