2018年(2)
分类: C/C++
2018-05-15 08:36:51
1983年,美国国家标准学会(ANSI)成立了一个委员会,其目标是制定“一个无歧义性的且与具体机器无关的C语言定义”。结果产生了C语言的ANSI标准。
第一章 导言
通过实际的程序引入C语言的基本元素。
一些基本概念:变量与常量、算术运算、控制流、函数、基本输入/输出
一、入门
学习一门新程序设计语言的唯一途径就是使用它编写程序。
第一个程序:打印hello,world
学习点:编写程序文本、编译、加载、运行、最后输出到某个地方。
在centos系统/目录下建立studyc目录,在此目录下保存源程序。
1、编写:vi hello.c
#include
main()
{
Printf(“hello,world\n”);
}
2、编译
在centos下,gcc hello.c
系统会生成a.out可执行文件。
3、运行
在提示符下运行a.out,结果提示-bash:a.out:command not found
我使用的是root用户编写的程序,然后工作目录转到/studyc目录下,系统默认的路径没有/studyc,所以找不到,执行./a.out,打印hello,world
一个C语言程序,无论其大小如何,都是由函数和变量组成的。函数中包含一些语句,以指定所要执行的计算操作;变量则用于存储计算过程中使用的值。
本例函数名字为main,通常情况下,函数的命名没有限制,但main是一个特殊的函数名——每个程序都从main函数的起点开始执行,这意味着每个程序都必须在某个位置包含一个main函数。
Main函数通常会调用其他函数来帮助完成某些工作,被调用的函数可以是程序设计人员自己编写的,也可以来自于函数库。本例第一行语句:
#include
用于告诉编译器在本程序中包含标准输入/输出库的信息。
函数之间进行数据交换的一种方法是调用函数向被调用函数提供一个值(称为参数)列表。函数名后面的一对圆括号将参数列表括起来。不需要任何参数,则用空参数表()表示。
函数中的语句用一对花括号{ }括起来。调用函数时,只需要使用函数名加上用圆括号括起来的参数表即可。如本例中Printf(“hello,world\n”);printf是一个用于打印输出的库函数,“hello,world\n”作为参数,打印双引号中的字符串。
用双引号括起来的字符序列称为字符串或字符串常量,字符序列\n表示换行符,在打印中遇到它时,输出打印将换行,从下一行的左端行首开始。
Printf函数永远不会自动换行,这样可以多次调用该函数以分阶段得到一个长的输出行。上例可改写成下列形式:
#include
main()
{
printf(“hello,”);
pringf(“world”);
printf(“\n”);
}
输出与前面程序输出相同。
\n只代表一个字符。类似\n的转义字符序列为表示无法输入的字符或不可见字符提供了一种通用的可扩充的机制。\t表示制表符;\b表示回退符;\”表示双引号;\\表示反斜杠符本身。
二、变量与算术表达式
华氏温度与摄氏温度对照表,公式:摄氏温度 = (5/9)(华氏温度-32):
0 -17
20 -6
40 4
60
80
100 37
120 48
140 60
160 71
180 82
200 93
220 104
240 115
260 126
280 137
300 148
/* -----Ftoc.c */
#include
/* 当fahr = 0,20,…,300时,分别打印华氏温度与摄氏温度对照表 */ ——注释
main()
{
int fahr,celius;
int lower,upper,step;
lower = 0; /* 温度表的下限 */
upper = 300; /* 温度表的上限 */
step = 20; /* 步长*/
fahr = lower;
while(fahr <= upper){
celsius = 5 * (fahr-32)/9;
printf(“%d\t%d\n”,fahr,Celsius);
fahr = fahr + step;
}
}
注释是包含在/* 与 */之间的字符序列,将被编译器忽略。
在C语言中,所有变量都必须先声明后使用。声明通常放在函数起始处,在任何可执行语句之前。声明用于说明变量的属性,它由一个类型名和一个变量表组成。
类型int表示其后所列变量为整数,float表示所列变量为浮点数(即可以带有小数部门的数)。int与float类型的取值范围取决于具体的机器。对于int类型,16位机取值范围在-32768~+32767之间,还有32位表示的int类型。Float类型通常是32位,它至少有6位有效数字。
C语言的其他一些基本数据类型:
char 字符——一个字节
short 短整型
long 长整型
double 双精度浮点型
这些数据类型对象的大小取决于具体的机器。
lower = 0;
为变量设置初值,各条语句均以分号结束。
while(fahr <= upper){
……
}
while循环语句的执行方式是:首先测试圆括号中的条件;如果条件为真(fahr <= upper),则执行循环体(括在花括号中的3条语句);然后再重新测试圆括号中的条件,如果为真,则再次执行循环体;当测试条件为假时,循环结束,并继续执行跟在while循环语句之后的下一条语句。
while语句的循环体可以是用花括号括起来的一条或多条语句,也可以是不用海阔好包括的单条语句。
书写时总是把由while控制的语句缩进一个制表位,可以突出程序的逻辑结构。建议每行只写一条语句,并在运算符两边各加上一个空格字符。
表达式写成先乘5然后再除以9而不是直接写成5/9,其原因是在C语言及许多其他语言中,整数除法操作将执行舍位,结果中的任何小数部分会被舍弃。本例如写成5/9在前,因为相除后截取所得结果为0,则计算后的所有摄氏温度都为0。
printf是一个通用输出格式化函数,该函数的第一个参数是待打印的字符串,其中的每个百分号(%)表示其他的参数(第二个、第三个、……参数)之一进行替换的位置,并指定打印格式。
printf函数的第一个参数中的各个%分别对应于第二个、第三个、……参数,他们在数目和类型上都必须匹配,否则将出现错误的结果。
printf函数并不是C语言本身的一部分,C语言本身没有定义输入/输出功能。printf仅仅是标准库函数中一个有用的函数,这些标准库函数在C怨言程序中通常都可用。但ANSI标准定义了printf函数的行为,因此,对每个符合该标准的编译器和库来说,该函数的属性都是相同的。
输出格式化问题:由于输出的数不是右对齐,在printf语句的第一个参数的%d中指明打印宽度,则打印的数字会在打印区域内右对齐。
printf(”%3d %6d\n”,fahr,celsius);
另一个问题:由于使用的是整型算术运算,因此经计算得到的摄氏温度值不太精确,如,与0℉对应的精确的摄氏温度应该为-17.8℃,而不是-17℃.为了得到更精确的结果,应该用浮点算术代替上面的整型算术运算。
#include
main()
{
float fahr,celsius;
int lower,upper,step;
lower = 0;
upper = 300;
step = 20;
fahr = lower;
while(fahr <= upper){
celsius = (5.0 / 9.0) * (fahr - 32.0);
printf("%3.0f %6.1f\n",fahr,celsius);
fahr = fahr + step;
}
}
此例与前面程序的不同在于把fahr和celsius声明为float类型,此时可以使用5.0/9.0的形式,两个浮点数相除,结果将不被舍位。
如果某个算术运算符的所有操作数均为整型,则执行整型运算。但如果某个算术运算符有一个浮点型操作数和一个整型操作数,则在开始运算之前整型操作数将会被转换为浮点型。如在表达式fahr – 32中,32在运算过程中将被自动转换为浮点数再参与运算。不过,即使浮点常量取的是整型值,在书写时最好还是为它加上一个显示的小数点,这样可以强调其浮点性质,便于阅读。
赋值语句:fahr = lower;与条件测试语句:while(fahr <= upper)也都是按照这种方式执行的,即在运算前先把int类型的操作数转换为float类型的操作数。
printf中的转换说明%3.0f表明待打印的浮点数(即fahr)至少占3个字符宽度,且不带小数点和小数部分;%6.1f表明另一个待打印的数(celsius)至少占6个字符宽,且小数点后面有1位数字。
格式说明可以省略宽度与精度,例如,%6f表示待打印的浮点数至少有6个字符宽度;%.2f指定待打印的浮点数的小数点后有两位小数,但宽度没有限制;%f则仅仅要求按照浮点数打印该数。
%d 按照十进制整型数打印
%6d 按照十进制整型数打印,至少6个字符宽度
%f 按照浮点数打印
%.2f 按照浮点数打印,小数点后有两位小数
%6.2f 按照浮点数打印,至少6个字符宽,小数点后有两位小数
%o 表示八进制数
%x 表示十六进制数
%c 表示字符;
%s 表示字符串
%%表示百分号(%)本身
三、for语句
对特定任务可以采用多种方法编程实现,对于前例:
#include
main()
{
int fahr;
for(fahr = 0; fahr <= 300; fahr = fahr + 20)
printf("%3d %6.1f\n", fahr, (5.0 / 9.0) * (fahr - 32));
}
[root@studyzdy studyc]# ./a.out
0 -17.8
20 -6.7
40 4.4
60 15.6
80 26.7
100 37.8
120 48.9
140 60.0
160 71.1
180 82.2
200 93.3
220 104.4
240 115.6
260 126.7
280 137.8
300 148.9
程序最主要的改进在于它去掉了大部分变量,只使用了一个int类型的变量fahr。在新引入的for语句中,温度的下限、上限和步长都是常量,而计算摄氏温度的表达式现在变成了printf函数的第三个参数,不再是一个单独的赋值语句。(——去掉下限、上限和步长,可以节省变量,但带来了改变的麻烦,如要修改上下限,步长时,需要每个for语句修改)
最后一点是C语言中一个通用规则的实例:在允许使用某种类型变量值的任何场合,都可以使用该类型的更复杂的表达式。
for的圆括号中共包含3个部分,第一部分是初始化部分,仅在进入循环前执行一次;第二部分是控制循环的测试或条件部分,循环控制将对该条件求值,如果结果值为真(true),则执行循环体;此后将执行第三部分以将循环变量增加一个步长,并再次对条件求值。如果计算得到的条件值为假(false),循环将终止执行。
四、符号常量
在程序中使用300、20等类似的“幻数”并不是一个好习惯,它们几乎无法向以后阅读该程序的人提供什么信息,而且使程序的修改变得更加困难。处理这种幻数的一种方法是赋予它们有意义的名字。#define指令可以把符号名(或称为符号常量)定义为一个特定的字符串:
#define 名字 替换文本
在该定义之后,程序中出现的所有在#define中定义的名字(既没有用引号引起来,也不是其他名字的一部分)都将用相应的替换文本替换。其中,名字与普通变量名的形式相同:都是以字母打头的字母和数字序列;替换文本可以是任何字符序列,而不仅限于数字。
#include
#define LOWER 0 /* xiaxian */
#define UPPER 300
#define STEP 20
main()
{
int fahr;
for(fahr = 0; fahr <= UPPER; fahr = fahr + STEP)
printf("%3d %6.1f\n",fahr,(5.0/9.0) * (fahr - 32));
}
其中,LOWER、UPPER与STEP都是符号常量,而非变量,因此不需要出现在声明中。符号常量通常用大写字母拼写,这样可以很容易与用小写字母拼写的变量名相区别。注意,#define指令行的末尾没有分号。
五、字符输入/输出
标准库提供的输入/输出模型非常简单。无论文本从何处输入,输出到何处,其输入/输出都是按照字符流的方式处理。文本流是由多行字符构成的字符序列,而每行字符则由0个或多个字符组成,行末是一个换行符。标准库负责使每个输入/输出流都能够遵守这一模型。使用标注库的C语言程序员不必关心在程序之外这些行是如何表示的。
标准库提供了一次读/写一个字符的函数,其中最简单的是getchar和putchar两个函数。每次调用时,getchar函数从文本流中读入下一个输入字符,并将其作为结果值返回。即执行语句: c = getchar(); 之后,变量c中将包含输入流中的下一个字符。这种字符通常是通过键盘输入的。每次调用putchar函数时将打印一个字符。如:
putchar(c); 将把整形变量c的内容以字符的形式打印出来,通常是显示在屏幕上。
1、文件复制
最简单的例子是把输入一次一个字符复制到输出,基本思想如下:
读一个字符
while(该字符不是文件结束指示符) /*需了解文件格式,文件结束指示符等*/
输出刚读入的字符
读下一个字符
将上述基本思想转换为C语言程序为:
#include
/* 将输入复制到输出;版本1 */
main()
{
int c;
c = getchar();
while(c != EOF){
putchar(c);
c = getchar();
}
}
字符在键盘、屏幕或其他的任何地方无论以什么形式表现,它在机器内部都是以位模式存储的。char类型专门用于存储这种类型数据,任何整型(int)也可以用于存储字符型数据。因为某些潜在的重要原因,在此使用int类型。
这里需要解决如何区分文件中有效数据与输入结束符的问题。C语言采取的解决方法是:在没有输入时,gechar函数将返回一个特殊值,这个特殊值与任何实际字符都不同。这个值称为EOF(end of file,文件结束)。我们在声明变量c的时候,必须让它大到足以存放getchar函数返回的任何值。这里之所以不把c声明成char类型,是因为它必须足够大,除了能存储任何可能的字符外还要能存储文件结束符EOF。因此将c声明成int类型。(——也就是说int类型的范围要大于char类型)。
EOF定义在头文件
在C语言中,类似于: c = getchar(); 之类的赋值操作是一个表达式,并且具有一个值,即赋值后左边变量保存的值。赋值可以作为更大的表达式的一部分出现。(——经验做法)
如果将为c赋值的操作放在while循环语句的二测试部分中,程序修改如下:
#include
/* ver:2 */
main()
{
int c;
while((c = getchar()) != EOF)
putchar(c);
}
对while语句的条件部分来说,赋值表达式两边的圆括号不能省略。不等于运算符!=的优先级比赋值运算符=的优先级要高,在不适用圆括号的情况下关系测试!=将在赋值=操作之前执行。因此语句
c = getchar() !=EOF 等价于
c = (getchar() !=EOF)
c将被值为0或1(Boolean值),不是希望的结果。
编译通过后,运行,可以随意输入字符或回车,按ctrl+c,结束。
2、字符计数
#include
/* ver:1 */
main()
{
long nc;
nc = 0;
while(getchar() != EOF)
++nc;
printf("%ld\n",nc);
}
这个程序,在gcc编译后,没有任何出错或警告信息,运行:
./a.out
输出一串字符,按ctrl+c退出,就是不打印最后的结果:
[root@study studyc]# ./a.out
fsdafsd
^C
[root@study studyc]#
gcc -v显示:gcc version 4.4.7 20120313 (Red Hat 4.4.7-16) (GCC)
修改一下:
#include
/* ver:1-2 */
main()
{
long nc;
nc = 0;
while(getchar() != EOF){
++nc;
printf(“%ld\n”,nc);
}
printf("total:%ld\n",nc);
}
可以打印循环体中的 printf(“%ld\n”,nc);无法打印最后的结果。
最终的解决:程序没有问题,问题出在最后结束输入的操作上,我使用的是ctrl+c来结束输入(实际上是想模拟文件的结束符EOF),这是不正确的,CTRL+C是用来结束程序运行的,在Linux中,在输入中模拟EOF是ctrl+d,在windows中是ctrl+z。
最后运行:
[root@studyzdy studyc]# ./a.out
abcdef
1
2
3
4
5
6
7
total:
7
因为输入的是abcdef共六个字符,再加上回车,共七个,所以结果就是打印:7
%ld告诉printf函数其对应的参数是long整型。使用double(双精度浮点型)类型可以处理更大的数字
改用for循环语句来编写:
#include
/* ver:2 */
main()
{
double nc;
for(nc = 0; getchar() != EOF; ++nc)
;
printf("Total: %.0f\n",nc);
}
对float与double类型,printf函数都使用%f进行说明。%.0f强制不打印小数点和小数部分,因此小数部分的位数为0。
该程序中,for循环语句的循环体是空的,因为所有工作都在测试(条件)部分与增加步长部分完成了。但C语言的语法规则要求for循环语句必须有一个循环体,因此用单独的分号代替。单独的分号称为空语句。
考虑如果输入中不包含字符,那么第一次调用getchar函数时,while语句或for语句中的条件测试从一开始就为假,程序的执行结果将为0,这也是正确的。while与for语句的优点之一就是在执行循环体之前就对条件进行测试。如果条件不满足,则不执行循环体,这就可能出现循环体一次都不执行的情况。
3、行计数
标准库保证输入文本流以行序列的形式出现,每一行均以换行符结束。因此,统计行数等价于统计换行符的个数。
#include
main()
{
int c,nl;
nl = 0;
while((c = getchar()) != EOF)
if( c == '\n')
++nl;
printf("Total Line:%d\n",nl);
}
双等于号==是C语言中表示“等于”关系的运算符。
单引号中的字符表示一个整型值,该值等于此字符在机器字符集中对应的数值,我们称之为字符常量。他只不过是小的整型数的另一种写法而已。例如,‘A’是一个字符常量;在ASCII字符集中其值为65(即字符A的内部表示值为65)。用‘A’要比用65好,因为‘A’的意义更清楚,且与特定的字符集无关。
字符串常量中使用的转义字符序列也是合法的字符常量,比如,‘\n’代表换行符的值,在ASCII字符集中其值为10.应当注意到,‘\n’是单个字符,在表达式中他不过是一个整型数而已;而“\n”是一个仅包含一个字符的字符串常量。即要注意单引号和双引号的区别。
练习1:编写一个统计空格、制表符与换行符个数的程序。
#include
main()
{
int c,total;
total = 0;
while((c = getchar()) != EOF){
if((c == ' ') | (c == '\t') | (c == '\n'))
++total;
}
printf("Sum:%d\n",total);
练习2:将输入复制到输出,并将其中连续的多个空格用一个空格代替。
#include
main()
{
int c,k;
k = 0;
while((c = getchar()) != EOF){
if(c != ' '){
putchar(c);
k = 0;
}
else if(c== ' ' && k == 0){
putchar(c);
++k;
}
}
}
练习3:编写一个将输入复制到输出的程序,并将其中的制表符替换为\t,把回退符替换为\b,把反斜杠替换为\\。
#include
main()
{
int c;
while((c = getchar()) != EOF){
if(c == '\t'){
putchar('\\');
putchar('t');
} else if (c == '\b') {
putchar('\\');
putchar('b');
} else if (c == '\\') {
putchar('\\');
putchar('\\');
}
else {
putchar(c);
}
}
}
4、单词计数
以下程序用于统计行数、单词数与字符数。对单词的定义比较宽泛,它是任何其中不包含空格、制表符或换行符的字符序列。
#include
#define IN 1
#define OUT 0
main()
{
int c,nl,nw,nc,state;
state = OUT;
nl = nw = nc = 0;
while((c = getchar()) != EOF){
++nc;
if(c == '\n')
++nl;
if(c == ' ' || c == '\n' || c == '\t')
state = OUT;
else if (state == OUT) {
state = IN;
++nw;
}
}
printf("%d %d %d\n",nl,nw,nc);
}
nl = nw = nc = 0;将把其中的3个变量nl、nw与nc都设置为0。要注意一个事实:在兼有值与赋值两种功能的表达式中,赋值结合次序是由右至左。上面语句等同于:
nl = (nw = (nc = 0));
运算符&&代表AND(逻辑与),||代表OR(逻辑或),&&仅比||高一个优先级。由&&或||连接的表达式由左至右求值,并保证在求值过程中只要能判断最终的结果为真或假,求值就立即终止。
六、数组
编写一个程序,以统计各个数字、空白符(包括空格符、制表符及换行符)以及所有其他字符出现的次数。
所有的输入字符可以分成12类,可以用一个数组存放各个数字出现的次数,比使用10个独立的变量更方便。
#include
main()
{
int c,i,nwhite,nother;
int ndigit[10];
nwhite = nother = 0;
for (i = 0; i < 10; ++i)
ndigit[i] = 0;
while((c = getchar()) != EOF)
if (c >= '0' && c <= '9')
++ndigit[c - '0'];
else if (c == ' ' || c == '\n' || c == '\t')
++nwhite;
else
++nother;
printf("digits = ");
for (i = 0; i < 10; ++i)
printf(" %d",ndigit[i]);
printf(",white space = %d,other = %d\n",nwhite,nother);
}
int ndigit[10];将变量ndigit声明为由10个整型数构成的数组。在C语言中,数组下标总是从0开始,因此该数组的10个元素分别为ndigit[0]、ndigit[1]、……、ndigit[9],这可以通过初始化和打印数组的两个for循环语句反映出来。
数组下标可以是任何整型数表达式,包括整型变量(如i)以及整型常量。
char类型的字符是小整型,因此char类型的变量和常量在算术表达式中等价于int类型的变量和常量。这样做既自然又方便,如:c – ‘ ‘是一个整型表达式,如果存储在c中的字符是‘0’~‘9’,其值将为0~9,因此可以从充当数组ndigit的合法下标。
程序中经常使用下列方式表示多路判定:
if(条件)
语句;
else if(条件)
语句;
….
….
else
语句。
七、函数
函数为计算的封装提供了一种简便的方法。。经常会看到在定义后仅调用了一次的短函数,这样做可以使代码更清晰易读。
第一个函数,编写一个求幂的函数power(m,n)来说明函数定义的方法。
#include
int power(int m,int n);
main()
{
int i;
for(i = 0; i < 10; ++i)
printf("%d %d %d\n", i, power(2,i),power(-3,i));
return 0;
}
int power(int base, int n)
{
int i,p;
p = 1;
for(i = 1; i <= n; ++i)
p = p * base;
return p;
}
函数定义的一般形式为:
返回值类型 函数名(0个或多个参数声明)
{
声明部分
语句序列
}
函数定义可以以任意次序出现在一个源文件或多个源文件中,但同一函数不能分割存放在多个文件中。如果源程序分散在多个文件中,那么在编译和加载时,就需要做更多的工作,但这是操作系统的原因,并不是语言的属性决定的。
main函数在下列语句中调用了两次power函数:
printf("%d %d %d\n", i, power(2,i),power(-3,i));
每次调用时,main函数向power函数传递两个参数;在调用执行完成时,power函数向main函数返回一个格式化的整数并打印。
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表示出现异常情况或出错结束条件。为间接起见,前面的main函数都是省略了return语句。在以后的main函数中包含return语句,以提醒大家注意,程序还要向其执行环境返回状态。
出现在main函数之前的声明语句
int power(int m,int n);
表明power函数有两个int类型的参数,并返回一个int类型的值。这种声明称为函数原型,它必须与power函数的定义和用法一致。如果函数的定义、用法与函数原型不一致,将出现错误。
函数原型与函数声明中参数名不要求相同。事实上,函数原型中的参数名是可选的,这样上面的函数原型也可以写成以下形式:
int power(int,int);
但是,合适的参数名能够起到很好的说明性作用,因此在函数原型中总是指明参数名。
ANSI C同较早版本C语言之间的最大区别在于函数的声明与定义方式的不同。按照C语言的最初定义,power函数应该写成下列形式:
/* power函数:求底数的n次幂;n >= 0 */
/* (早期C语言版本中的实现方法) */
power(base,n)
int base,n;
{
int i,p;
p = 1;
for (i = 1; i <=n; ++i)
p = p*base;
return p;
}
其中,参数名在圆括号内指定,参数类型在左花括号之前声明。如果没有声明某个参数的类型,则默认为int类型。函数体与ANSI C中形式相同。
在C语言的最初定义中,可以在程序的开头按照下面这种形式声明power函数:
int power();
函数声明中不允许包含参数列表,这样编译器就无法在此时检查power函数调用的合法性。事实上,power函数在默认情况下将被假定返回int类型的值,因此整个函数的声明可以全部省略。
在ANSI C中定义的函数原型语法中,编译器可以很容易检测出函数调用中参数数目和类型方面的错误。ANSI C仍然支持旧式的函数声明与定义,但强烈建议:在使用新式的编译器时,最好使用新式的函数原型声明方式。
八、参数——传值调用
在C语言中,所有函数参数都是“通过值”传递的,也就是说,传递给被调用函数的参数值存放在临时变量中,而不是存放在原来的变量中。在C语言中,被调用函数不能直接修改主调用函数中变量的值,而只能修改其私有的临时副本的值。
传值调用的利大于弊。在被调用函数中,参数可以看做是便于初始化的局部变量,因此额外使用的变量更少,程序可以更紧凑简洁。如下:
int power(int base, int n)
{
int p;
for( p = 1; n>0; --n)
p = p * base;
return p;
}
其中,参数n用作临时变量,并通过随后执行的for循环语句递减,直到其值为0,这样就不需要额外引入变量i。power函数内部对n的任何操作不会影响到调用函数中n的原始参数值。
必要时,也可以让函数能够修改主调函数中的变量。这种情况下,调用者需要向被调用函数提供待设置值的变量的地址(从技术角度看,地址就是指向变量的指针),而被调用函数则需要将对应的参数声明为指针类型,并通过它间接访问变量。
如果是数组参数,情况就有所不同了。当把数组名用作参数时,传递给函数的值是数组起始元素的位置或地址——它并不复制数组元素本身。在被调用函数中,可以通过数组下标访问或修改数组元素的值。
九、字符数组
字符数组是C语言中最常用的数组类型。下面程序说明字符数组以及操作字符数组的函数用法。该程序读入一组文本行,并把最长的文本行打印出来。基本框架:
while(还有未处理的行)
if(改行比已处理的最长行还要长)
保存该行
保存改行的长度
打印最长的行
从上面的框架看,程序很自然地分成了若干片断,分别用于读入新行、测试读入的行、保存该行,其余部分则控制这一过程。
首先,编写一个独立的函数getline,它读取输入的下一行。至少getline函数应该在读到文件末尾时返回一个信号;更为有用的设计是它能够在读入文本行时返回改行的长度,而在遇到文件结束符时返回0。由于0不是有效的行长度,因此可以作为标志文件结束的返回值。每一行至少包括一个字符,只包含换行符的行,其长度为1。
当发现某个新读入的行比以前读入的最长行还要长,就需要把该行保存起来。需要用另一个函数copy把新行复制到一个安全的位置。
最后,在主函数main中控制getline和copy这两个函数。
#include
#define MAXLINE 1000
int getline1(char line[],int maxline);
void copy(char to[],char from[]);
main()
{
int len;
int max;
char line[MAXLINE];
char longest[MAXLINE];
max = 0;
while((len = getline1(line,MAXLINE)) > 0)
if (len > max){
max = len;
copy(longest,line);
}
if (max > 0)
printf("%s",longest);
return 0;
}
int getline1(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;
}
void copy(char to[],char from[])
{
int i;
i = 0;
while ((to[i] = from[i]) != '\0')
++i;
}
出现的问题:
一开始申明的函数原型:int getline(char line[],int maxline);编译时总提示以下错误:
array2.c:4: error: conflicting types for ‘getline’
/usr/include/stdio.h:673: note: previous declaration of ‘getline’ was here
array2.c:7: error: conflicting types for ‘getline’
/usr/include/stdio.h:673: note: previous declaration of ‘getline’ was here
最后将getline函数名改为getline1编译通过,这个可能是因为getline在c的库stdio.h中已经定义了,导致冲突。
程序的开始对getline1和copy这两个函数进行了声明。
main与getline1之间通过一对参数及一个返回值进行数据交换。在getline1函数中,两个参数是通过程序行
int getline1(char s[],int lim)
声明的,它把第一个参数s声明为数组,把第二个参数lim声明为整型。声明中提供数组大小的目的是留出存储空间。在getline1函数中没有必要指明数组s的长度,这是因为该数组的大小是在main函数中设置的。getline1函数使用了一个return语句将值返回给其调用者,返回值类型为int。由于函数的默认返回值类型为int,因此这里的int可以省略。
有些函数返回有用的值,而有些函数(如copy)仅用于执行一些动作,并不需要返回值。copy函数的返回值类型为void,显式说明该函数不返回任何值。
getline1函数把字符’\0’(即空字符,其值为0)插入到它创建的数组的末尾,以标记字符串的结束。这一约定已被C语言采用:当在C语言程序中出现类似于
“hello\n”的字符串常量时,它将以字符数组的形式存储,数组的各元素分别存储字符串的各个字符,并以’\0’标志字符串的结束。
h |
e |
l |
l |
o |
\n |
\0 |
printf函数中的格式规范%s规定,对应的参数必须是以这种形式表示的字符串。copy函数的实现正是依赖于输入参数由’\0’结束这一事实,它将’\0’拷贝到输出参数中。(也就是说,空字符’\0’不是普通文本的一部分。)
十、外部变量与作用域
上例中main函数中的变量(如line、longest等)是main函数的私有变量或局部变量。由于它们是在main函数中声明的,因此其他函数不能直接访问它们。其他函数中声明的变量也同样如此。如getline1函数中声明的变量i与copy函数中声明的变量i没有关系。函数中的每个局部变量只在函数被调用时存在,在函数执行完毕退出时消失。这也是其他语言通常把这类变量称为自动变量的原因。以后使用“自动变量”代表“局部变量”。
由于自动变量只在函数调用执行期间存在,因此,在函数的两次调用之间,自动变量不保留前次调用时的赋值,且在每次进入函数时都要显式为其赋值,如果自动变量没有赋值,则其中存放的是无效值。
除自动变量外,还可以定义位于所有函数外部的变量,也就是在所有函数中都可以通过变量名访问这种类型的变量。由于外部变量可以在全局范围内访问,因此,函数间可以通过外部变量交换数据,而不必使用参数表。外部变量在程序执行期间一直存在,而不是在函数调用时产生、在函数执行完毕时消失。即使在对外部变量赋值的函数返回后,这些变量仍将保持原来的值不变。
外部变量必须定义在所有函数之外,且只能定义一次,定义后编译程序将为它分配存储单元。在每个需要访问外部变量的函数中,必须声明相应的外部变量,此时说明其类型。声明时可以用extern语句显式声明,也可以通过上下文隐式声明。
#include
#define MAXLINE 1000
int max;
char line[MAXLINE];
char longest[MAXLINE];
int getline1(void);
void copy(void);
main()
{
int len;
extern int max;
extern char longest[];
max = 0;
while((len = getline1()) > 0)
if (len > max){
max = len;
copy();
}
if (max > 0)
printf("%s",longest);
return 0;
}
int getline1(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;
}
void copy(void)
{
int i;
extern char line[],longest[];
i = 0;
while((longest[i] = line[i]) != '\0')
++i;
}
前几行定义了函数使用的几个外部变量,声明了各外部变量的类型,这样编译程序将为它们分配存储单元。函数在使用外部变量之前,必须知道外部变量的名字。要达到该目的,一种方式是在函数中使用extern类型的声明。这种类型的声明除了在前面加了一个关键字extern外,其他方面与普通变量的声明相同。
某些情况下可以省略extern声明。在源文件中,如果外部变量的定义出现在使用它的函数之前,那么在那个函数中就没有必要使用extern声明。因此,main、getline1及copy中的几个extern声明都是多余的。在通常的做法中,所有外部变量的定义都放在源文件的开始处,这样可以省略extern声明。
如果程序包含在多个源文件中,而某个变量在file1文件中定义、在file2和file3文件中使用,那么在文件file2与file3中就需要使用extern声明来建立该变量与其定义之间的联系。人们通常把变量和函数的extern声明放在一个单独的文件中(习惯上称之为头文件),并在每个源文件的开头使用#include语句把所要使用的头文件包含进来。后缀名.h约定为头文件名的扩展名。
在上面的程序中,由于getline1与copy函数都不带参数,因此从逻辑上讲,在源文件开始处它们的原型应该是getline1()与copy(),但为了与老版本的C语言程序兼容,ANSI C语言把空参数表看成老版本C语言的声明方式,并且对参数表不再进行任何检查。在ANSI C中,如果要声明空参数表,则必须使用关键字void进行显式声明。
在这里谈论外部变量时谨慎地使用了定义(define)与声明(declaration)这两个词。“定义”表示创建变量或分配存储单元,而“声明”指的是说明变量的性质,但并不分配存储单元。
使用外部变量,似乎可以简化数据的通信——参数表变短了,且在需要时总可以访问这些变量。缺点是,即使在不适用外部变量时它们也存在;使程序中的数据关系模糊不清——外部变量的值可能会被意外或不经意的修改,程序的修改变得十分困难;失去通用性。