02 C语言程序设计之导言
文章目录
- 1、入门
- 1-1、引例
- 1-2、练习题
- 1-2-1、Job1
- 1-2-2、Job2
- 2、变量与算术表达式
- 2-1、引例
- 2-2、练习题
- 2-2-1、Job1
- 2-2-2、Job2
- 3、for语句
- 3-1、引例
- 3-2、练习题
- 4、符号常量
- 5、字符输入/输出
- 5-1、文件复制
- 5-1-1、引例
- 5-1-2、练习题
- 5-1-2-1、Job1
- 5-1-2-2、Job2
- 5-2、字符计数
- 5-2-1、引例
- 5-3、行计数
- 5-3-1、引例
- 5-3-2、练习题
- 5-3-2-1、Job1
- 5-3-2-2、Job2
- 5-3-2-3、Job3
- 5-4、单词计数
- 5-4-1、引例
- 5-4-2、练习题
- 5-4-2-1、Job1
- 5-4-2-2、Job2
- 6、数组
- 6-1、引例
- 6-2、练习题
- 6-2-1、Job1
- 6-2-2、Job2
- 7、函数
- 7-1、引例
- 7-2、练习题
- 8、参数——传值调用
- 9、字符数组
- 9-1、引例
- 9-2、练习题
- 9-2-1、Job1
- 9-2-2、Job2
- 9-2-2、Job3
- 9-2-2、Job4
- 10、外部变量与作用域
- 10-1、引例
- 10-2、练习题
- 10-2-1、Job1
- 10-2-2、Job2
- 10-2-3、Job3
- 10-2-4、Job4
- 10-2-5、Job5
- 参考资料
1、入门
学习一门新程序设计语言的唯一途径就是使用它编写程序。而对于所有语言的初学者来说,编写的第一个程序几乎都是相同的,即:
Hell,World!
1-1、引例
在C语言中,我可以使用以下程序打印出上述字符:
#include <stdio.h> //包含标准库的信息main() //定义名为main的函数,他不接受参数值
{ //main函数的语句都被括在花括号内printf("Hello,World!\n"); //main函数调用库函数printf以显示字符序列;\n代表换行符
}
以本程序为例进行说明。一个C语言程序,无论其大小如何,都是由函数和变量组成的。函数中包含一些语句,以指定所要执行的计算操作;变量则用于存储计算过程中所使用的值。在该例中,函数的名字是main。通常情况下,函数的命名没有限制,但main是一个特殊的函数名——每个程序都会从main函数的起点开始执行,这意味着每个程序都必须在某个位置包含一个main函数。
函数之间进行数据交换的一种方法是调用函数向被调用函数提供一个值(称作参数)列表。函数名后面的一对圆括号需要将参数列表括起来。在本例中,main函数不需要任何参数,因为此用空参数表()表示。而对于printf函数来说,Hello,World!\n作为参数调用printf函数。printf是一个用于打印输出的库函数,在此处,它打印双引号中间的字符串。其中,用双引号阔气阿里的字符序列被称为字符串或字符串常量。
1-2、练习题
1-2-1、Job1
在自己打印的系统中运行**Hello,World!**程序。再有意去掉程序中的部分内容,看看会得到什么出错信息。
- 将程序中printf()后面的分号省略,对于本例,编译器将会识别到少于一个分号并给出相应位置的出错信息,具体如下所示:
- 在上述例子中,将 \n 后面的双引号 " 被错写成单引号 ‘ 。于是,这个单引号及其后面的右括号和分号都将被看作整个输出字符串的一部分。编译器会将这种情况视作一个错误,会报告缺失一个双引号;在右花括号前面缺失一个右圆括号。
1-2-2、Job2
做个实验,当printf函数的参数字符串中包含 \c (其中\c是上面的转义字符序列中未曾列出来的某个字符)时,则观察一下会出现什么情况。
上面这段代码的执行器结果与具体的编译器有关。其中 是ASCII值等于7的字符所产生的一声蜂鸣声。在\的后面,可以用最多三个八进制数字来表示一个字符,而\7在ASCII字符集中代表一声蜂鸣声。
2、变量与算术表达式
2-1、引例
根据公式℃ = (5/9)(℉ - 32)打印一下华氏温度和摄氏温度的对温表。该程序仍然包含一个main的函数定义,不过会引入一些新的概念,包括注释、声明、变量、算术表达式、循环及格式化输出等内容。该程序如下所示:
#include <stdio.h>
/*当fahr = 0, 20, ..., 300时,分别打印华氏温度与摄氏温度对应表
*/int main()
{int fahr, celsius;int lower, upper, step;int index;lower = 0; /* 温度表的下限 */upper = 0; /* 温度表的上限 */step = 20; /* 步长 */index = 0; /* 序号 */celsius = 0;fahr = celsius;while (fahr <= upper){/* code */celsius = 5 * (fahr - 32) / 9;index++;printf("%d: %d\t%d\n", index, fahr, celsius);fahr += step;}return 0;
}
- 包含在 /*与 */ 之间的字符序列将会被编译器忽略,这部分被称作注释。
- 在C语言中,所有变量都必须先声明再使用。声明通常是放在函数起始处,在任何可执行语句之前。
- 声明用于说明变量的属性,它是由一个类型名和一个变量表组成的。
建议每行只是书写一条语句,并在运算符两边各加上一个空格字符,这样在阅读代码及运算的结合关系中更加清晰明了。
在该程序中,绝大部分工作都是在循环体中完成的。循环体的赋值语句
celsius = 5 * (fahr - 32) / 9;
用于计算与指定华氏温度与之相应的摄氏温度值,并将结果传递给变量celsius。在该语句中,之所以把表达式写成先乘以5然后再除以9而不是直接写成5/9,其原因是在于C语言及许多其它语言中,整数除法操作将执行舍位,结果中的任何小数部分都会被舍弃。由于5和9都是整数,5/9后经截断所得结果就是0。因此求得的所有摄氏温度都降为0。
根据以上分析来看,目前程序存在两个问题:1、由于输出的数不是右对齐的,所以输出的结果不是很美观。解决办法:在printf语句的第二个参数的%d中指明打印宽度,则打印出的数字会在打印区域内右对齐。2、由于使用了整数类型进行算术运算,导致计算后的摄氏温度值不太准确。解决办法:使用浮点算术运算代替上面的整型算术运算。则修改前后的程序如下所示:
如果某个算术运算符的所有操作数均为整数类型,则执行整数型运算。但是,如果某个算术运算符有一个浮点型操作数和一个整型操作数,则在开始运算之前整型操作数将会被转换成浮点型。不过。即使浮点常量取的是整型值,在书写时也最好为它加上一个显式的小数点,这样就可以强调其浮点性质,便于阅读。
字符 | 含义 |
---|---|
%d | 按照十进制整型数打印 |
%6d | 按照十进制整型数打印,至少6个字符宽度 |
%f | 按照浮点数打印 |
%6f | 按照浮点数打印,至少6个字符宽度 |
%.2f | 按照浮点数打印,小数点后有两位小数 |
%6.2f | 按照浮点数打印,至少6个字符宽度,小数点后有两位小数 |
字符 | 含义 |
---|---|
%o | printf函数按照八进制数打印 |
%x | printf函数按照十六进制数打印 |
%c | printf函数按照字符打印 |
%s | printf函数按照字符串打印 |
%% | printf函数按照百分号本身打印 |
2-2、练习题
2-2-1、Job1
修改温度转换程序,使之能在转换表的顶部打印出一个标题。
#include <stdio.h>
/*当fahr = 0, 20, ..., 300时,分别打印华氏温度与摄氏温度对应表
*/int main()
{float fahr, celsius;int lower, upper, step;lower = 0; /* 温度表的下限 */upper = 300; /* 温度表的上限 */step = 20; /* 步长 */printf("-Fahr Celsius-\n");fahr = lower;while (fahr <= upper){celsius = 5.0 * (fahr - 32.0) / 9.0;printf("%3.0f\t%6.1f\n", fahr, celsius);fahr += step;}return 0;
}
2-2-2、Job2
编写一个程序打印摄氏温度转化为华氏温度的转换表。
#include <stdio.h>
/*当celsius = 0, 20, ..., 300时,分别打印摄氏温度与华氏温度对应表
*/int main()
{float fahr, celsius;int lower, upper, step;lower = 0; /* 温度表的下限 */upper = 300; /* 温度表的上限 */step = 20; /* 步长 */printf("-Celsius Fahr-\n");celsius = lower;while (celsius <= upper){fahr = (celsius * 9.0) / 5.0 + 32.0;printf("%3.0f\t%6.1f\n", celsius, fahr);celsius += step;}return 0;
}
3、for语句
3-1、引例
#include <stdio.h>
/*fahr = 0, 20, ..., 300时,分别打印华氏温度与摄氏温度对应表
*/int main()
{int fahr;printf("-Fahr Celsius-\n");for (fahr = 0; fahr <= 300; fahr += 20){printf("%3d\t%6.1f\n", fahr, (5.0 * (fahr - 32.0) / 9.0));}return 0;
}
该程序的执行结果与上一节While语句的执行结果是一致的,但是程序本身却有所不一样。主要改进在于它去掉了大部分变量,而是只用了一个int类型的fahr。for语句是一种循环语句,它是对while语句的推广。圆括号内共包含3个部分,各部分之间用分号隔开。
1、第一部分
fahr = 0
是初始化部分,仅在进入循环前执行一次。
2、第二部分
fahr <= 300
是控制循环的测试或条件部分。循环控制将对该条件求值,如果结果为真(True),则执行循环体(本例中的循环体仅包含一个printf函数调用语句)。
3、第三部分
fahr += 20
是将循环变量fahr增加一个步长,并再次对条件求值。如果计算得到的条件值为假(False),循环将终止执行。与while语句一样,for循环语句的循环体可以只有一条语句,也可以是用花括号括起来的一组语句。
通用规则:在允许使用某种类型变量值的任何场合,都可以使用该类型的更复杂的表达式。
3-2、练习题
修改温度转换程序,要求以逆序(即按照从300度到0度的顺序)打印温度转换表。
#include <stdio.h>
/*fahr = 0, 20, ..., 300时,分别打印华氏温度与摄氏温度对应表
*/int main()
{int fahr;printf("-Fahr Celsius-\n");for (fahr = 300; fahr >= 0; fahr -= 20){printf("%3d\t%6.1f\n", fahr, (5.0 * (fahr - 32.0) / 9.0));}return 0;
}
4、符号常量
在结束讨论温度转换程序前,我们再来看一下符号常量。在程序中使用到的300、20等类似的“幻数”并不是一个好习惯,他们几乎无法想以后阅读该程序的人提供什么信息,而且使程序的修改变得更加困难。故,处理这种“幻数”的一种方法是赋予它们有意义的名字。#define指令可以把符号名(或符号常量)定义为一个特定的字符串:
#define 名字 替换文本
在该定义之后,程序中出现的所有在#define中定义的名字都将用相应的替换文本替换。其中,名字和普通变量名的形式相同:它们都是以字母开头的字母和数字序列;替换文本可以是任何字符序列,而不仅限于数字。注意:符号常量名通常用大写字母拼写,指令行的末尾没有分号。
#include <stdio.h>
/*fahr = 0, 20, ..., 300时,分别打印华氏温度与摄氏温度对应表
*/
#define LOWER 0
#define UPPER 300
#define STEP 20int main()
{int fahr;printf("-Fahr Celsius-\n");for (fahr = LOWER; fahr <= UPPER; fahr += STEP){printf("%3d\t%6.1f\n", fahr, (5.0 * (fahr - 32.0) / 9.0));}return 0;
}
5、字符输入/输出
5-1、文件复制
标准库提供的输入/输出模型非常简单。无论文本从何处输入,输出到何处,其输入/输出都是按照字符流的方式在处理。文本流是由多行字符构成的字符序列,而每行字符则由0个或多个字符组成,行末是一个换行符。
标准库提供了一次读/写一个字符的函数,其中最简单的是getchar和putchar函数。每次调用时,getchar函数从文本流中读入下一个输入字符,并将其作为结果值返回。也就是说,在执行语句
c = getchar()
之后,变量c中将包含输入流中的下一个字符。每次调用putchar函数时将打印一个字符。例如执行语句
putchar(c)
将把整个变量c的内容以字符的形式打印出来,通常显示在屏幕上。
5-1-1、引例
借助于putchar和getchar函数,可以在不了解其他输入/输出知识的情况下编写出数量惊人的有用代码。最简单的例子就是把输入一次一个字符地复制到输出,其基本思想如下:
读一个字符
while(该字符不是文件结束指示符)输出刚输入的字符读下一个字符
#include <stdio.h>
/* 将输入复制到输出,初版 */
int main()
{int c;c = getchar();while (c != EOF){putchar(c);c = getchar();}return 0;
}
字符在键盘、屏幕或其他的任何地方无论以什么形式表现,它在机器内部都是以位模式存储的。char类型专门用于存储这种字符型数据,当然任何整型(int)也可以用于存储字符型数据。
这里需要解决如何区分文件中有效数据和输入结束符的问题。C语言采取的措施是:在没有输入时,getchar函数将返回一个特殊值,这个值与任何实际字符都不同。这个值被称为EOF(end of file,文件结束)。我们在声明变量的时候,必须让它大到足以存放getchar函数返回的任何值。这里之所以不把变量c声明为char类型,是因为它必须足够大,除了能存储任何可能的字符外还要能存储文件结束符EOF。因此,把它声明为int类型。
EOF定义在头文件<stdio.h>中,是一个整型数。其具体数值是什么不重要,只要它与任何char类型的值都不相同即可。这里使用符号常量,可以确保程序不需要依赖于其对应的任何特定的数值。
对于上述代码,可以再精炼一些,具体如下所示:
#include <stdio.h>
/* 将输入复制到输出,升级版 */
int main()
{int c;while ((c = getchar()) != EOF){putchar(c);}return 0;
}
5-1-2、练习题
5-1-2-1、Job1
验证表达式 getchar() != EOF 的值是0还是1。
#include <stdio.h>
/* 将输入复制到输出,升级版 */
int main()
{int c;while (c = getchar() != EOF){printf("%d\n", c);putchar(c);printf("%d - at EOF\n", c);}return 0;
}
5-1-2-2、Job2
编写一个打印EOF值的程序。
#include <stdio.h>
/* 将输入复制到输出,升级版 */
int main()
{printf("EOF is %d\n", EOF);return 0;
}
5-2、字符计数
5-2-1、引例
使用程序对字符进行计数,它与上面的复制程序类似。
#include <stdio.h>
/* 统计输入的字符数,初版 */
int main()
{long Cnt = 0;while (getchar() != EOF){++Cnt;}printf("Cnt is %ld\n", Cnt);return 0;
}
该字符计数程序使用long类型的变量存放计数值,而没有使用int类型的变量。long类型数(长整型)至少需要用到32 bit存储单元,在某些机器上int和long类型的长度是一样的,但在一些机器上,int类型的值只可能有16 bit存储单元的长度(最大值:0xFFFF = 32767)。这样,相当小的输入都可能使int类型的计数变量溢出。格式说明%ld告诉printf函数其对应的参数是long整型。
使用double(双精度浮点数)类型可以处理更大的数字。这里采用for循环语句来展示另外一种编写方法:
#include <stdio.h>
/* 统计输入的字符数,for循环版 */
int main()
{double Cnt = 0;for (Cnt = 0; getchar() != EOF; ++Cnt){/* code */;}printf("Cnt is %.0f\n", Cnt);return 0;
}
在该程序段中,for循环语句的循环体是空的,这是因为所有工作在测试(条件)部分与增加步长部分完成了。但是C语言的语法规则要求for循环语句必须有一个循环体,因此用单独的分号代替表示。单独的分号成为空语句。
在结束讨论字符计数程序之前,我们要考虑以下情况:如果输入中不包含字符,那么,在第一次调用getchar函数的时候,while语句或for语句中的条件测试从一开始就是假的,程序的执行结果就是0,这也是正确的结果。该点很重要,while语句和for语句的优点之一就是在执行循环体之前就对条件进行测试。如果条件不满足,则不执行循环体,这就可能出现循环体一次都不执行的情况。
5-3、行计数
5-3-1、引例
接下来的这个程序主要用于统计输入的行数。我们在上面提到过,标准库保证输入文本流以行序列的形式出现,每一行均以换行符结束。故,统计行数等价于统计换行符的个数。
#include <stdio.h>
/* 统计输入的行数,初版 */int main()
{int c;int Cnt = 0;while ((c = getchar()) != EOF){if ('\n' == c){++Cnt; /* code */}}printf("Cnt is %d\n", Cnt);return 0;
}
在该程序中,单引号中的字符表示一个整型值,该值等于此字符在机器字符集中对应的数值,我们称之为字符常量。但是,他只不过是小的整型数的另一种写法而已。例如,‘A’是一个字符常量,在ASCII字符集中其值是65(即字符A的内部表示值为65)。当然,用’A’要比用65好,因为’A’的意义更清楚,且与特定的字符集无关。不过我们应当注意,’\n’是单个字符,在表达式中他只不过是一个整型数而已;而"\n"是一个仅包含一个字符的字符串常量。
5-3-2、练习题
5-3-2-1、Job1
编写一个统计空格、制表符与换行符个数的程序。
#include <stdio.h>int main()
{int c;int Cnt1 = 0, Cnt2 = 0, Cnt3 = 0;while ((c = getchar()) != EOF){if ('\n' == c){++Cnt3; /* code */}else if (' ' == c){++Cnt1;}else if ('\t' == c){++Cnt2;}}printf("Space Number is %d\n", Cnt1);printf("Tab Number is %d\n", Cnt2);printf("Enter Number is %d\n", Cnt3);return 0;
}
5-3-2-2、Job2
编写一个将输入复制到输出的程序,并将其中连续的多个空格用一个空格代替。
5-3-2-3、Job3
编写一个将输入复制到输出的程序,并将其中的制表符替换为\t,将回退符替换为\b,将反斜杠替换为\。这样可以将制表符和回退符以可见的方式显示出来。
5-4、单词计数
5-4-1、引例
本节将介绍第四个实用程序用于统计行数、单词数与字符数。这里对单词的定义比较宽泛,它是任何其中不包含空格、制表符或换行符的字符序列。下面的程序:
#include <stdio.h>
#define IN 1 /* 在单词内 */
#define OUT 0 /* 在单词外 *//* 统计输入的行数,单词数与字符数 */
int main()
{int c, nl, nw, nc, state;state = OUT;nl = nw = nc = 0;while ((c = getchar()) != EOF){++nc;if ('\n' == c){++nl;}if ((' ' == c) || ('\n' == c) || ('\t' == c)){state = OUT;}else if (OUT == state){state = IN;++nw;}}printf("nl is %d\n", nl);printf("nw is %d\n", nw);printf("nc is %d\n", nc);return 0;
}
程序执行时,每当遇到单词的第一个字符时,他就作为一个新单词加以统计。state变量记录程序当前是否正处在一个单词之中,它的初值是“不在单词之中”,即初始值被赋值为OUT。但要注意这样一个事实:在兼有值与赋值两种功能的表达式中,赋值结合次序是由右向左。所以上面的判定语句等同于:
nl = (nw = (nc = 0));
相应地,运算符&&代表AND(逻辑与),它仅比||高一个优先级。由&&与||连接的表达式是由左至右求值。并保证在求值过程中只要能够判断最终的结果为真或假,求值过程就会立即终止。
5-4-2、练习题
5-4-2-1、Job1
你准备如何测试单词计数程序?如果程序中存在某种错误,那么什么样的输入最可能发现这类错误呢?
5-4-2-2、Job2
编写一个程序,以每行一个单词的形式打印其输入。
6、数组
6-1、引例
在这部分内容中,我们来编写一个程序,以统计各个数字、空白符(包括空格符、制表符及换行符)以及所有其它字符出现的次数。这个程序的实用意义并不大,但我们可以通过该程序讨论C语言多方面的问题。所有的输入字符可以分成12 类,因此可以用一个数组存放各个数字出现的次数,这样比使用10个独立的变量更方便。下面是该程序的一种版本:
#include <stdio.h>
#define CHAR_ARRAY_NUM 10/* count digits, white space, others. */
int main()
{int c, i, nwhite, nother;int ndigits[CHAR_ARRAY_NUM];nwhite = nother = 0u;for (i = 0; i < CHAR_ARRAY_NUM; i++){/* code */ndigits[i] = 0u;}while ((c = getchar()) != EOF){/* code */if (c >= '0' && c <= '9'){++ndigits[c - '0'];}else if ((c == ' ') || (c == '\n') || (c == '\t')){/* code */++nwhite;}else{++nother;}}printf("digit = ");for (i = 0; i < CHAR_ARRAY_NUM; i++){/* code */printf(" %d", ndigits[i]);}printf(", white space = %d, other = %d.\n", nwhite, nother);return 0;
}
如上所示,将变量ndigits声明为由10个整型数构成的数组。在C语言中,数组下标总是从0开始,因此该数组的10 个元素分别为ndigits[0]、ndiglts[1]、…、ndigits[9],这可以通过初始化和打印数组的两个for循环语句反映出来。数组下标可以是任何整型表达式,包括整型变量(如i)以及整型常量。
由定义可知,char类型的字符是小整型,因此char类型的变量和常量在算术表达式中等价于int类型的变量和常量。这样做既自然又方便,例如,c - ‘0’是一个整型表达式,如果存储在c 中的字符是’0’~’9’,其值将为0~9,因此可以充当数组ndigit 的合法下标。
6-2、练习题
6-2-1、Job1
编写一个程序,打印输入中单词长度的直方图。水平方向的直方图比较容易绘制,垂直方向的直方图则要困难些。
6-2-2、Job2
编写一个程序,打印输入中各个字符出现频度的直方图。
7、函数
C 语言中的函数等价于 Fortran 语言中的子程序或函数,也等价于 Pascal 语言中的过程或函数。函数为计算的封装提供了一种简便的方法,此后使用函数时不需要考虑它是如何实现的。使用设计正确的函数,程序员无需考虑功能是如何实现的,而只需知道它具有哪些功能就够了。在 C 语言中可以简单、方便、高效地使用函数。我们经常会看到在定义后仅调用了一次的短函数,这样做可以使代码段更清晰易读。到目前为止,我们所使用的函数(如 printf、getchar 和 putchar 等)都是函数库中提供的函数。
7-1、引例
C语言没有像 Fortran 语言一样提供类似于**的求幂运算符,我们现在通过编写一个求幂的函数power(m, n)来说明函数定义的方法。power(m, n)函数用于计算整数m的n次幂,其中n是正整数。对函数调用power(2,5)来说,其结果值为 32。该函数并非一个实用的求幂函数,它只能处理较小的整数的正整数次幂,但这对于说明问题已足够了。(标准库中提供了一个计算xy的函数 pow(x, y)。)
#include "stdio.h"int Power(int m, int n);/* Test power function. */
int main(void)
{int i;for (i = 0; i < 10; ++i){/* code */printf("%d %d %d\n", i, Power(2, i), Power(-3, i));}return 0;
}/* power function: raise base to n-th power; n >= 0 */
int Power(int base, int n)
{int i;int p = 1;for (i = 0; i <= n; ++i){p *= base;/* code */}return p;
}
函数定义可以以任意次序出现在一个源文件或多个源文件中,但同一函数不能分割存放在多个文件中。如果源程序分散在多个文件中,那么,在编译和加载时,就需要做更多的工作,但这是操作系统的原因,并不是语言的属性决定的。我们暂且假定将 main 和 power 这两个函数放在同一文件中,这样前面所学的有关运行 C 语言程序的知识仍然有效。
power 函数的第一行语句
int Power(int base, int n)
声明参数的类型、名字以及该函数返回结果的类型。power 函数的参数使用的名字只在 power函数内部有效,对其它任何函数都是不可见的:其它函数可以使用与之相同的参数名字而不会引起冲突。变量 i 与 p 也是这样:power 函数中的 i 与 main 函数中的 i 无关。通常把函数定义中圆括号内列表中出现的变量称为形式参数,而把函数调用中与形式参数对应的值称为实际参数。
power 函数计算所得的结果通过 return 语句返回给 main 函数。关键字 return 的后面可以跟任何表达式,形式为:
return 表达式;
函数不一定都有返回值。不带表达式的 return 语句将把控制权返回给调用者,但不返回有用的值。这等同于在到达函数的右终结花括号时,函数就“到达了尽头”。主调函数也可以忽略函数返回的值。main函数的末尾有一个 return 语句。由于main本身也是函数,因此也可以向其调用者返回一个值,该调用者实际上就是程序的执行环境。一般来说,返回值为 0 表示正常终止,返回值为非 0 表示出现异常情况或出错结束条件。
7-2、练习题
重新编写 1.2 节中的温度转换程序,使用函数实现温度转换计算。
#include <stdio.h>
/*fahr = 0, 20, ..., 300时,分别打印华氏温度与摄氏温度对应表
*/
#define LOWER 0
#define UPPER 300
#define STEP 20float Fahr2CelsiusFunc(void);int main()
{int fahr;printf("-Fahr Celsius-\n");Fahr2CelsiusFunc();return 0;
}float Fahr2CelsiusFunc(void)
{int fahr;for (fahr = LOWER; fahr <= UPPER; fahr += STEP){printf("%3d\t%6.1f\n", fahr, (5.0 * (fahr - 32.0) / 9.0));}
}
8、参数——传值调用
在 C 语言中,所有函数参数都是“通过值”传递的。也就是说,传递给被调用函数的参数值存放在临时变量中,而不是存放在原来的变量中。这与其它某些语言是不同的,比如,Fortran 等语言是“通过引用调用”,Pascal 则采用 var 参数的方式,在这些语言中,被调用的函数必须访问原始参数,而不是访问参数的本地副本。最主要的区别在于,在 C 语言中,被调用函数不能直接修改主调函数中变量的值,而只能修改其私有的临时副本的值。
对于Figure22. power function中的程序而言,参数 n 用作临时变量,并通过随后执行的 for 循环语句递减,直到其值为 0,这样就不需要额外引入变量 i;power 函数内部对 n 的任何操作不会影响到调用函数中 n 的原始参数值。
必要时,也可以让函数能够修改主调函数中的变量。这种情况下,调用者需要向被调用函数提供待设置值的变量的地址(从技术角度看,地址就是指向变量的指针),而被调用函数则需要将对应的参数声明为指针类型,并通过它间接访问变量。如果是数组参数,情况就有所不同了。当把数组名用作参数时,传递给函数的值是数组起始元素的位置或地址——它并不复制数组元素本身。在被调用函数中,可以通过数组下标访问或修改数组元索的值。
9、字符数组
9-1、引例
字符数组是 C 语言中最常用的数组类型。下面我们通过编写一个程序,来说明字符数组以及操作字符数组的函数的用法。该程序读入一组文本行,并把最长的文本行打印出来。该算法的基本框架非常简单:
while (还有未处理的行)
if (该行比已处理的最长行还要长)保存该行为最长行保存该行的长度
打印最长的行
从上面的框架中很容易看出,程序很自然地分成了若干片断,分别用于读入新行、测试读入的行、保存该行,其余部分则控制这一过程。因为这种划分方式比较合理,所以可以按照这种方式编写程序。
首先,我们编写一个独立的函数 getline,它读取输入的下一行。我们尽量保持该函数在其它场台也有用。至少getline 函数应该在读到文件末尾时返回一个信号;更为有用的设计是它能够在读入文本行时返回该行的长度,而在遇到文件结束符时返回 0。由于 0 不是有效的行长度,因此可以作为标志文件结束的返回值。每一行至少包括一个字符,只包含换行符的行,其长度为 1。
当发现某个新读入的行比以前读入的最长行还要长时,就需要把该行保存起来。也就是说,我们需要用另一个函数 copy 把新行复制到一个安全的位置。
最后,我们需要在主函数 main 中控制 getline 和 copy 这两个函数。以下便是我们编写的程序:
#include <stdio.h>
#define MAXLINE 1000 /* maximum input line length */int Getline(char s[], int lim);
void Copy(char to[], char from[]);/* print the longest input line */
int main(void)
{int len; /* current line length */int max; /* maximum length seen so far */char line[MAXLINE]; /* current input line */char longestline[MAXLINE]; /* longest line saved here */max = 0;while ((len = Getline(line, MAXLINE) > 0U)){if (len > max){max = len;Copy(longestline, line);}}if (max > 0U) /* there was a line */{printf("%s", longestline);}return 0;
}/* getline: read a line into s, return length */
int Getline(char s[], int lim)
{int c, i;for (i = 0; i < lim - 1 && (c = getchar()) != EOF && c != '\n'; ++i){s[i] = c;}if (c == '\n'){s[i] = c;++i;}s[i] = '\0';return i;
}/* copy: copy 'from' into 'to'; assume to is big enough */
void Copy(char to[], char from[])
{int i = 0;while ((to[i] = from[i]) != '\0'){++i;}
}
9-2、练习题
9-2-1、Job1
修改打印最长文本行的程序的主程序 main,使之可以打印任意长度的输入行的长度,并尽可能多地打印文本。
9-2-2、Job2
编写一个程序,打印长度大于 80 个字符的所有输入行。
9-2-2、Job3
编写一个程序,删除每个输入行末尾的空格及制表符,并删除完全是空格的行。
9-2-2、Job4
编写函数 reverse(s),将字符串 s 中的字符顺序颠倒过来。使用该函数编写一个程序,每次颠倒一个输入行中的字符顺序。
10、外部变量与作用域
main 函数中的变量(如 line、longest 等)是 main 函数的私自变量或局部变量。由于它们是在 main 函数中声明的,因此其它函数不能直接访问它们。其它函数中声明的变量也同样如此。例如,getline 函数中声明的变量 i 与 copy 函数中声明的变量 i 没有关系。函数中的每个局部变量只在函数被调用时存在,在函数执行完毕退出时消失。这也是其它语言通常把这类变量称为自动变量的原因。以后我们使用“自动变量”代表“局部变量”
。
由于自动变量只在函数调用执行期间存在,因此,在函数的两次调用之间,自动变量不保留前次调用时的赋值,且在每次进入函数时都要显式为其赋值。如果自动变量没有赋值,则其中存放的是无效值。除自动变量外,还可以定义位于所有函数外部的变量,也就是说,在所有函数中都可以通过变量名访问这种类型的变量。
由于外部变量可以在全局范围内访问,因此,函数间可以通过外部变量交换数据,而不必使用参数表。再者,外部变量在程序执行期间一直存在,而不是在函数调用时产生、在函数执行完毕时消失。即使在对外部变量赋值的函数返回后,这些变量仍将保持原来的值不变。外部变量必须定义在所有函数之外,且只能定义一次,定义后编译程序将为它分配存储单元。在每个需要访问外部变量的函数中,必须声明相应的外部变量,此时说明其类型。声明时可以用 extern 语句显式声明,也可以通过上下文隐式声明。
10-1、引例
#include <stdio.h>
#define MAXLINE 1000 /* maximum input line length */int max; /* maximum length seen so far */
char line[MAXLINE]; /* current input line */
char longestline[MAXLINE]; /* longest line saved here */
int Getline(void);
void Copy(void);/* print the longest input line */
int main(void)
{int len; /* current line length */extern int max;extern char longestline[];max = 0;while ((len = Getline() > 0U)){if (len > max){max = len;Copy();}}if (max > 0U) /* there was a line */{printf("%s", longestline);}return 0;
}/* getline: read a line into s, return length */
int Getline(void)
{int c, i;extern char line[];for (i = 0; i < MAXLINE - 1 && (c = getchar()) != EOF && c != '\n'; ++i){line[i] = c;}if (c == '\n'){line[i] = c;++i;}line[i] = '\0';return i;
}/* copy: copy 'from' into 'to'; assume to is big enough */
void Copy(void)
{int i = 0;extern char line[], longestline[];while ((longestline[i] = line[i]) != '\0'){++i;}
}
在该例子中,前几行定义了 main、getline 与 copy 函数使用的几个外部变量,声明了各外部变量的类型,这样编译程序将为它们分配存储单元。从语法角度看,外部变量的定义与局部变量的定义是相同的,但由于它们位于各函数的外部,因此这些变量是外部变量。函数在使用外部变量之前,必须要知道外部变量的名字。要达到该目的,一种方式是在函数中使用 extern 类型的声明。这种类型的声明除了在前面加了一个关键字 extern 外,其它方面与普通变量的声明相同。
某些情况下可以省略 extern 声明。在源文件中,如果外部变量的定义出现在使用它的函数之前,那么在那个函数中就没有必要使用 extern 声明。因此,main、getline 及 copy中的几个 extern 声明都是多余的。在通常的做法中,所有外部变量的定义都放在源文件的开始处,这样就可以省略 extern 声明。
如果程序包含在多个源文件中,而某个变量在 file1 文件中定义、在 file2 和 file3文件中使用,那么在文件 file2 与 file3 中就需要使用 extern 声明来建立该变量与其定义之间的联系。人们通常把变量和函数的 extern 声明放在一个单独的文件中(习惯上称之为头文件),并在每个源文件的开头使用#include 语句把所要用的头文件包含进来。后缀名.h 约定为头文件名的扩展名。例如,标准库中的函数就是在类似于<stdio.h>的头文件中声明的。
这节中我们在谈论外部变量时谨慎地使用了定义(define)与声明(declaration)这两个词。“定义”表示创建变量或分配存储单元,而“声明”指的是说明变量的性质,但并不分配存储单元。
顺便提一下,现在越来越多的人把用到的所有东西都作为外部变量使用,因为似乎这样可以简化数据的通信——参数表变短了,且在需要时总可以访问这些变量。但是,即使在不使用外部变量的时候,它们也是存在的。过分依赖外部变量会导致一定的风险,因为它会使程序中的数据关系模糊不清——外部变量的值可能会被意外地或不经意地修改,而程序的修改又变得十分困难。我们前面编写的打印最长文本行的程序的第 2 个版本就不如第 1 个版本好,原因有两方面,其一便是使用了外部变量;另一方面,第 2 个版本中的函数将它们所操纵的变量名直接写入了函数,从而使这两个有用的函数失去了通用性。
10-2、练习题
10-2-1、Job1
编写程序 detab,将输入中的制表符替换成适当数目的空格,使空格充满到下一个制表符终止位的地方。假设制表符终止位的位置是固定的,比如每隔 n 列就会出现一个制表符终止位。n 应该作为变量还是符号常量呢?
10-2-2、Job2
编写程序 entab,将空格串替换为最少数量的制表符和空格,但要保持单词之间的间隔不变。假设制表符终止位的位置与练习 1-20 的 detab 程序的情况相同。当使用一个制表符或者一个空格都可以到达下一个制表符终止位时,选用哪一种替换字符比较好?
10-2-3、Job3
编写一个程序,把较长的输入行“折”成短一些的两行或多行,折行的位置在输入行的第 n 列之前的最后一个非空格之后。要保证程序能够智能地处理输入行很长以及在指定的列前没有空格或制表符时的情况。
10-2-4、Job4
编写一个删除 C 语言程序中所有的注释语句。要正确处理带引号的字符串与字符常量。在 C 语言中,注释不允许嵌套。
10-2-5、Job5
编写一个程序,查找 C 语言程序中的基本语法错误,如圆括号、方括号、花括号不配对等。要正确处理引号(包括单引号和双引号)、转义字符序列与注释。(如果读者想把该程序编写成完全通用的程序,难度会比较大。)
参考资料
- 02 嵌入式技术之VS Code编译环境搭建
- C程序设计语言 (Brian W. Kernighan, Dennis M. Ritchie) (Z-Library)