C 语言 第四章
第四章 函数与程序结构
一个设计得当的函数可以把程序中不需要了解的具体才做细节隐藏起来,使整个程序结构更加清晰,降低修改程序的难度。
4.1 函数的基本知识
函数定义如下:
返回值类型 函数名(参数声明)
{
声明和语句
}
练习4-1.
1 int strrindex(char s[], char t[]) 2 { 3 int i, j, k, start, end; 4 start = end = -1; 5 for (i = strlen(s)-1; i >= 0; i--){ 6 for (j = i,k = strlen(t)-1;s[j] == t[k]; j--, k--) //多了j这个零时变量,用来做累加,保留i作为第一次找到的位置。 7 //printf("%s\n","标记"); 8 if (k == 0) //如果k等于0 代表 字符串t完全被找到 9 { 10 start = j; 11 end = i; 12 printf("start=%d\nend=%d",start,end); 13 } 14 } 15 }
4.2 返回非整型值的函数
如果使用一个没有声明过的名字在表达式中,并且其后紧跟一个左圆括号,那么上下文就会认为该名字是一个函数的名字,该函数的返回值将假定为int类型,上下文不会对其参数做仍和假设。
如果函数带有参数,则要声明它们,如果没有采纳数,则使用void进行声明。
练习4-2.
1 int main() 2 { 3 double n,a; 4 char s[50] = "15.11E-5"; 5 char t[10] = "bdfg"; 6 int strrindex(char s[],char t[]); 7 double atofa(char s[]); 8 n = atofa(s); 9 printf("%f",n); 10 } 11 double atofa(char s[]) 12 { 13 double val, power,sciebtific; 14 int i, sign,exp; 15 sciebtific = 1; 16 for (i = 0; isspace(s[i]); i++); 17 sign = (s[i] == '-') ? -1 : 1; 18 if (s[i] == '+' || s[i] == '-') 19 i++; 20 for (val = 0.0; isdigit(s[i]); i++) 21 val = 10.0 * val + (s[i] - '0'); 22 if (s[i] == '.') 23 i++; 24 for (power = 1.0; isdigit(s[i]); i++) 25 { 26 val = 10.0 * val + (s[i] - '0'); 27 power *= 10; 28 } 29 val = sign * val /power; 30 if (s[i] == 'e' || s[i] == 'E') 31 { 32 i++; 33 sign = (s[i] == '-') ? -1 : 1; 34 if (s[i] == '+' || s[i] == '-') 35 i++; 36 for (exp = 0; isdigit(s[i]); i++) //得到后面的数字 37 exp = 10 * exp + (s[i] - '0'); //转换为数字 38 if (sign == 1) 39 { 40 while (exp-- > 0) //每次-1 例如 12.2E+5 等于 12.2 *10*10*10*10*10 41 val *= 10; 42 } else{ 43 while (exp-- >0) // 12.2E-5 等于 12.2 除以 5次 10 44 val /= 10; 45 } 46 } 47 return val; 48 }
4.3 外部变量
C语言程序可以看成由一系列的外部对象构成,这些外部对象可能是变量或函数。C语言不允许在一个函数中定义其他函数,因此函数本身是“外部的”。
外部变量与函数具有下列性质:通过同一个名字对外部变量的所有引用(即使引用来自单独编译的不同函数)实际上都是引用同一个对象(标准中把这一性质成为外部链接)。
自动变量只能在函数内部使用,从其所在的函数被调用时变量开始存在,在函数退出时变量也将消失。外部变量永久存在。
练习4-3到4-10全部 215行之后是4-10
1 #include <stdio.h> 2 #include <string.h> 3 #include <stdlib.h> 4 #include <ctype.h> 5 #include <math.h> 6 #define MAXOP 100 7 #define NUMBER '0' 8 #define MAXVAL 100 9 #define BUFSIZE 100 10 #define NAME 'n' 11 int sp = 0; 12 int bufp = 0; 13 int getop(char []); 14 void push(double); 15 double pop(void); 16 char buf[BUFSIZE]; //字符集 17 double val[MAXVAL]; //数组 18 int getch(void); 19 void ungetch(int); 20 void clear(void); 21 void matchfnc(char []); 22 int main() 23 { 24 int type, i, var = 0; 25 double op1,op2,v; 26 char s[MAXOP]; 27 double variable[26]; //定义一个代表26个字母的数组,测试只能是数字在前,字母在后,例如 4 A = 2 A + 不可以是 A 4 28 for (i = 0; i < 26; i++) //给26个赋值 29 variable[i] = 0.0; 30 while ((type = getop(s)) != EOF){ 31 switch (type) { 32 case NUMBER: //如果是‘0’; 33 push(atof(s)); //把字符串s[]里面的字符转换成数字并且,并且存入到val[]数组中。 34 break; 35 case NAME: 36 matchfnc(s); 37 break; 38 case '+': //如果是+ 39 push(pop() + pop()); //把从val[]里面拿出的数字相加算出结果。然后重新存入val[]中。 40 break; 41 case '*': //同上+法 42 push(pop() * pop()); 43 break; 44 case '-': 45 op2 = pop(); //外部存储最后一个数 46 push(pop() - op2); //然后两个数想减 47 break; 48 case '/': 49 op2 = pop(); //同减去 50 if (op2 != 0.0) 51 push(pop() / op2); 52 else 53 printf("error: zero divisor\n"); 54 break; 55 case '?': 56 op2 = pop(); //复制弹出 57 printf("\t%.8g\n", op2); //打印 58 push(op2); //重新推入 59 break; 60 case 'c': 61 clear(); 62 break; 63 case 'd': 64 op2 = pop(); //弹出顶元素 65 push(op2); //压入两次 66 push(op2); 67 break; 68 case 's': 69 op1 = pop(); //弹出元素 70 op2 = pop(); 71 push(op1); //推入元素 72 push(op2); 73 break; 74 case '%': 75 op2 = pop(); 76 if (op2 != 0.0) 77 push(fmod(pop(),op2)); 78 else 79 printf("error: zero divison\n"); 80 break; 81 case '=': 82 pop(); //取出一个数 83 if (var >= 'A' && var <= 'Z') //如果上一个字符是A-Z中的 84 variable[var - 'A'] = pop(); //给variable赋值,这里取值和赋值是一样的方式,数组里面对应的是0-26,0代表A,依次类推,取值时让那个字母-A就得到相应的数字来。 85 else 86 printf("error: no variable name\n"); 87 break; 88 case '\n': 89 v = pop(); //把数值存入pop 90 printf("\t%.8g\n", pop()); 91 break; 92 default: 93 if (type >= 'A' && type <= 'Z') //如果是A-Z 94 push(variable[type - 'A']); //推入variable[]中存入的那个对应字母的数字 95 else if (type == 'v') //如果等于V 96 push(v); //推入V 97 else 98 printf("error: unknown command %s\n", s); 99 break; 100 } 101 var = type; 102 } 103 return 0; 104 } 105 void ungets(char s[]) 106 { 107 int len = strlen(s); 108 while (len > 0) 109 ungetch(s[--len]); 110 } 111 void matchfnc(char s[]) 112 { 113 double op2; 114 if (strcmp(s,"sin") == 0) 115 push(sin(pop())); 116 else if (strcmp(s,"cos") == 0) 117 push(cos(pop())); 118 else if (strcmp(s,"exp") == 0) 119 push(exp(pop())); 120 else if (strcmp(s,"pow") == 0) 121 { 122 op2 = pop(); 123 push(pow(pop(),op2)); 124 } 125 else 126 printf("error: %s not supported\n", s); 127 } 128 void clear(void) 129 { 130 sp = 0; 131 } 132 void push(double f) 133 { 134 if (sp < MAXVAL) //如果这个数组没有满 135 val[sp++] = f; //存入 136 else 137 printf("error: stack full, can't push %g\n", f); 138 } 139 double pop(void) 140 { 141 if (sp > 0) //如果里面有数字 142 return val[--sp]; //那就拿出最后一个数字 143 else { 144 printf("error: stack empty\n"); 145 return 0.0; 146 } 147 } 148 int getop(char s[]) 149 { 150 int i, c; 151 while ((s[0] = c = getch()) == ' ' || c == '\t') 152 ; 153 s[1] = '\0'; 154 i = 0; 155 if (islower(c)) //查看是否是小写字母 156 { 157 while (islower(s[++i] = c = getch())); //获得字母 158 s[i] = '\0'; 159 if (c != EOF) //如果不是结束符 160 ungetch(c); //存入字符集 161 if (strlen(s) > 1) //如果s字符集大于1 162 return NAME; //返回预订条件 163 else 164 return c; //否则返回单个字符 165 } 166 if (!isdigit(c) && c != '.' && c != '-') //如果不是数字 不是点 167 return c; //返回c 168 if (c == '-'){ //如果是-号 169 if (isdigit(c = getchar()) || c == '.') //检查-号后面那个字符是不是数字 170 s[++i] = c; //是数字就存进去 171 else { //不是数字 172 if (c != EOF) //不是结束符 173 ungetch(c); //存入buf字符集中 174 return '-'; //返回-号 175 } 176 } 177 if (isdigit(c)) //如果是数字 178 {while (isdigit(s[++i] = c = getch()));} //如果getch()函数返回的是数字,就继续循环,并且把这些获得的数字存储到字符串S[]中 179 if (c == '.') //如果是. 180 {while (isdigit(s[++i] = c = getch()));} //继续获取和上面一样 181 s[i] = '\0'; //用‘\0’做结束符 182 if (c != EOF) //这一步比较重要,c现在的位置是指向获得以上数字之后输入的那个字符,如果不是结束符 183 ungetch(c); //这个函数用来储存这个数 184 return NUMBER; //返回数字,这个数字在上面做了定义 默认是‘0’; 185 } 186 int getch(void) 187 { 188 return (bufp > 0) ? buf[--bufp] : getchar(); //如果bufp大于0,说明buf字符集里面有字符,不然返回输入读取 189 } 190 void ungetch(int c) 191 { 192 if (bufp >= BUFSIZE) //首先确定这个储存是不是满了 193 printf("ungetch: too many characters\n"); 194 else 195 buf[bufp++] = c; //然后储存进去 196 } 197 //char buf = 0; 198 //int getch(void) 199 //{ 200 // int c; 201 // if (buf != 0) 202 // c = buf; 203 // else 204 // c = getchar(); 205 // buf = 0; 206 // return c; 207 //} 208 //void ungectch(int c) 209 //{ 210 // if (buf != 0) 211 // printf("ungetch: too many characters\n"); 212 // else 213 // buf = c; 214 //} 215 #define MAXLINE 100 216 #define NUMBER '0' 217 218 int getline(char line,int limit); 219 int li = 0; 220 char line [MAXLINE]; 221 int getop1(char s[]) 222 { 223 int c, i; 224 if (line[li] == '\0') 225 if (getline(line, MAXLINE) == 0) 226 return EOF; 227 else 228 li = 0; 229 while ((s[0] = c = line[li++]) == ' ' || c == '\t'); 230 s[1] = '\0'; 231 if (!isdigit(c) && c != '.') 232 return c; 233 i = 0; 234 if (isdigit(c)) 235 while (isdigit(s[++i] = c = line[li++])) 236 ; 237 if (c == '.') 238 while (isdigit(s[++i] = c = line[li++])); 239 240 s[i] = '\0'; 241 li--; 242 return NUMBER; 243 }
4.4 作用域规则
构成C语言程序的函数与外部变量可以分开进行编译。一个程序可以存放在几个文件中,原先已编译过的函数可以从库中进行加载。
名字的作用域指的是程序中可以使用该名字的部分。对于在函数开头声明的自动变量来说,其作用域是声明该变量名的函数。不同函数中声明的具有相同名字的各个局部变量之间没有任何关系。函数的参数也是这样,可以看作是局部变量。
外部变量或函数的作用域从声明它的地方开始,到其所在的文件的末尾结束。
如果要在外部变量的定义之前使用该变量,或外部变量的定义与变量的使用不再同一个源文件中,则必须在相应的变量声明中强制性的使用关键字extern。
外部变量的声明和定义需要严格区分,变量声明用于说明变量的属性(变量类型),而变量定义除此以外换会引起存储器的分配。
int sp;
double val[MAXVAL]
上面两条语句定义外部变量sp与val,并分配存储单元,这两条语句还可以作为该源文件中其余部分的声明。
extern int sp;
extern double val[];
为原文件的其余部分声明了一个int类型的外部变量sp以及一个double数组类型的外部变量val,这两个声明没有分配存储单元。
在一个源程序的所有源文件中,一个外部变量只能在某个文件中定义一次,其他文件可以通过extern声明来访问它(定义外部变量的源文件中也可以包含对该外部变量的extern声明)。外部变量的定义中必须指定数组的长度,但extern声明则不一定要指定数组的长度。
外部变量的初始化只能出现在其定义中。
4.5 头文件
可以创建一个hearder.h的头文件然后把共享变量放进去,在各个文件使用#include "hearder.h"的方式共享变量。
4.6 静态变量
用static声明限定外部变量与函数,可以将其后声明的对象的作用域限定为被编译源文件的声誉部分。通过static限定外部对象,可以达到隐藏外部对象的目的.
外部的static声明通常多用于变量,当然,它也可用于声明函数。通常情况下,函数名字是全局可访问的,对整个程序的各个部分而言都可见。但是,如果把函数声明为static类型,则该函数名除了对该函数声明所在的文件可见,其他文件都无法访问。
static也可用于声明内部变量。static类型的内部变量同自动变量一样,是某个特定函数的局部变量,只能在该函数中使用,和自动变量不同的是,不管其所在函数是否被调用,它一直存在,自动变量会所在函数的被调用和退出而存在和消失。换句话说,static类型的内部是一种只能在某个特定函数中使用但一直占据存储空间的变量。
4-11
1 int getop(char s[]) 2 { 3 int i, c; 4 static int lastc = 0; //静态变量用来存储C 5 if (lastc == 0) 6 c = getchar(); 7 else 8 { 9 c = lastc; 10 lastc = 0; 11 } 12 while ((s[0] = c = getch()) == ' ' || c == '\t') 13 ; 14 s[1] = '\0'; 15 i = 0; 16 if (islower(c)) //查看是否是小写字母 17 { 18 while (islower(s[++i] = c = getch())); //获得字母 19 s[i] = '\0'; 20 if (c != EOF) //如果不是结束符 21 lastc = c; //存入静态变量 22 if (strlen(s) > 1) //如果s字符集大于1 23 return NAME; //返回预订条件 24 else 25 return c; //否则返回单个字符 26 } 27 if (!isdigit(c) && c != '.' && c != '-') //如果不是数字 不是点 28 return c; //返回c 29 if (c == '-'){ //如果是-号 30 if (isdigit(c = getchar()) || c == '.') //检查-号后面那个字符是不是数字 31 s[++i] = c; //是数字就存进去 32 else { //不是数字 33 if (c != EOF) //不是结束符 34 lastc = c; //存入静态变量 35 return '-'; //返回-号 36 } 37 } 38 if (isdigit(c)) //如果是数字 39 {while (isdigit(s[++i] = c = getch()));} //如果getch()函数返回的是数字,就继续循环,并且把这些获得的数字存储到字符串S[]中 40 if (c == '.') //如果是. 41 {while (isdigit(s[++i] = c = getch()));} //继续获取和上面一样 42 s[i] = '\0'; //用‘\0’做结束符 43 if (c != EOF) //这一步比较重要,c现在的位置是指向获得以上数字之后输入的那个字符,如果不是结束符 44 lastc = c; //这个函数用来储存这个数 45 return NUMBER; //返回数字,这个数字在上面做了定义 默认是‘0’; 46 }
4.7 寄存器变量
register声明告诉编译器,它说声明的变量在程序中使用频率较高。思想是将register变量存放在机器的寄存器中,这样可以是程序更小、执行速度更快。但编译器可以忽略此项选项。
register int x;
register声明只适用于自动变量以及函数的形式参数。
f (register unsigned m, register long n)
{
register int i;
}
实际使用时,地层硬件环境的实际情况对寄存器变量的使用会有一些限制。每个函数中只有很少的变量可以保存在寄存器中,且只允许某些类型的变量。过量的寄存器声明并没有害处,编译器可以忽略过量的或不支持的寄存器变量声明。无论寄存器变量实际上是不是存放在寄存器中,它的地址都是不能访问的。在不同的机器中,对寄存器变量的数目和类型的具体限制也不同。
4.8 程序块结构
变量的声明(包括初始化)除了可以紧跟在函数开始的花括号之后,还可以紧跟在任何其他标识复合语句开始的左花括号之后。以这种方式声明的变量可以隐藏程序块外与之同名的变量,他们之间没有任何关系,并在与左花括号匹配的右花括号出现之前一直存在。
在一个好的程序设计风格中,应该避免出现变量名隐藏外部作用域中相同名字的情况。
4.9 初始化
在不进行显示初始化的情况下,外部变量和静态变量都被初始化为0,而自动变量和寄存器变量的初值则没有定义(初始值为无用信息)。
对于外部变量与静态变量来说,初始化表达式必须是常量表达式,且只初始化一次(从概念上讲是在程序开始执行前进行初始化)。对于自动变量与寄存器变量,则在每次进入函数或程序块时都将被初始化。
对于自动变量与寄存器变量来说,初始化表达式可以不是常量表达式,表达式中可以包含任意在此表达式之前已经定义的值,包括函数调用。
数组的初始化可以在声明的后面紧跟一个初始化表达式表,初始化表达式列表用花括号括起来,用逗号隔开。
当省略数组长度时,编译器将把花括号中初始化表达式的个数作为数组的长度。
如果初始化表达式的个数比数组元素少,对外部变量、静态变量和自动变量来说,没有初始化表达式的元素将被初始化为0,如初始化表达式的个数比数组元素多,则是错误的,不能一次将一个初始化表达式指定给多个数组元素,不能跳过前面的数组元素直接初始化后面的数组元素。
字符数组的初始化可以用一个字符串来代替用花括号括起来并用逗号分割的初始化表达式序列。
4.10 递归
C语言中的函数可以递归调用,即函数可以直接或间接调用自身。递归并不节省存储器开销,因为递归调用过程中必须在某个地方维护一个存储处理值的栈。
练习4-12
1 void itoa1(int n, char s[]) 2 { 3 static int i; 4 if (n / 10) 5 itoa1(n / 10, s); 6 else{ 7 i = 0; 8 if (n < 0) 9 s[i++] = '-'; 10 } 11 printf("this is n=%d\n",n); 12 s[i++] = abs(n) % 10 + '0'; 13 s[i] = '\0'; 14 printf("this is s=%s\n",s); 15 }
练习4-13
1 void reverser(char s[],int i,int len) 2 { 3 int c, j; 4 j = len - (i + 1); //strlen函数获得是字符串的长度,但是多一位数。所以减去1,len值是使用strlen获得的 5 if (i < j){ 6 c = s[i]; 7 s[i] = s[j]; 8 s[j] = c; 9 reverser(s,++i,len); 10 } 11 }
4.11 C预处理器
C语言通过预处理器提供了一些语言功能。概念上,预处理器是编译过程中单独执行的第一个步骤。两个最常见用的预处理器指令:#include指令(编译期间把指定文件的内容包含进当前文件中)和#define指令(任意字符序列代替一个标记)。
4.11.1 文件包含
#include <文件名> 的行都将被替换为文件名指定的文件内容。如果文件名用引号引起来,则在源文件所在位置查找该文件,如果在该位置没有找到文件,或者如果文件名使用尖括号括起来的,则将根据相应的规则查找该文件,规则同具体实现有关。被包含的文件本身也可包含#include指令。
源文件的开始通常都会有多个#include指令,用以包含常见的#define语句和extern声明,或从头文件中访问库函数的函数原型声明。
如果某个包含文件的内容发生变化,那么所有依赖该包含文件的源文件都必须重新编译。
4.11.2 宏替换
定义如下:
#define 名字 替换文本
所有出现(名字)的地方都将被替换为(替换文本)。通常情况下,#define指令占一行,替换文本是#define指令行尾部的所有剩余部分内容,也可以把一个较长的宏定义分成若干行,待续的行末尾需加上一个反斜杆符\。#define指令定义的名字作用域从 其定义点开始,到被编译的源文件的末尾处结束。宏定义中也可以使用前面出现的宏定义。替换只对记号进行,对括在引号中的字符串不起作用。
#define forever for(;;) 该语句为for(;;)起名为forever
宏定义也可以带参数,这样可以对不同的宏调用使用不同的替换文本。
#define max(A, B) ((A) >(B) ? (A) : (B)) 使用宏max看起来很像函数调用,但宏调用直接将替换文本插入到代码中。如果各种类型的参数的处理是一致的,可以将同一个宏定义应用于仍和数据类型,无需针对不同的数据类型需要定义不同的max函数。
<stdio.h>头文件中有一个很实用的例子:getchar和putchar函数在实际中常常被定义为宏,这样可以避免处理字符时调用函数所需的运行时开销。<ctype.h>头文件中定义的函数也常常是通过宏实现的。
可以通过#undef指令取消名字的宏定义,这样可以保证后续的调用是函数调用,不是宏调用
#undef getchar
int getchar(){}
形式参数不能用带引号的字符串替换。如果在替换文本中,参数名以#开头作为前缀,则结果将被扩展为由实际参数替换该参数的带引号的字符串。
#define dprint(expr) printf(#expr " = %g\n", expr)
dprint(x/y)
printf("x/y" " = %g\n", x\y); 等价于 printf("x/y = %g\n", x/y);在实际参数中,每个双引号将被替换为\",反斜杠将被替换为\\,因此替换后的字符串是合法的字符串常量。
预处理器运算符##为宏扩展提供了一种连接实际参数的手段。如果替换文本中的参数与##相邻,则该参数将被实际参数替换,##与前后的空白符都将被删除,并对替换后的结果重新扫描。
#define paste(front, back) front ## back
paste(name, 1)结果将建立记号name1。
练习4-14
1 #define swap(t,x,y) {t _z; \ 2 _z = y; \ 3 y = x; \ 4 x = _z; } //这里的_z不会被用作变量名为假设前提。
4.11.3 条件包含
可以使用条件语句对于处理本身进行控制,这种条件语句的值是在预处理执行的过程中进行计算。在编译过程中根据计算所得的条件值选择性的包含不同代码提供一种手段。
在#if语句中可以使用表达式defined(名字),该表达式的值遵循下列规则:当名字已经定义时,其值为1;否则,其值为0.
例如,为了保证hdr.h文件的内容只被包含一次
#if !defined(HDR)
#define HDR
/* hdr.h文件的内容 */
#endif
第一次包含头文件hdr.h时,将定义名字HDR;此后再次包含该头文件时,该名字已经定义,将直接跳转到#endif处。类似的方式也可以用来避免多次重复包含同一文件。如果多个头文件能够一致的使用这种方式,那么,每个头文件都可以将它所依赖的任何头文件包含进来,用户不必考虑和处理头文件之间的各种依赖关系。
C语言定义了两个预处理语句#ifdef和#ifndef,用来测试某个名字是否已经定义。
#ifndef HDR
#define HDR
/* hdr.h文件的内容 */
#endif

浙公网安备 33010602011771号