首页 > 代码库 > 重读《C程序设计语言》(2):导言

重读《C程序设计语言》(2):导言

这一章主要是概要的介绍C语言,通过实际程序引入C语言的基本元素。至于具体细节,后续章节将进一步介绍。

(1)学习一门新程序设计语言的唯一途径就是使用它编写程序。

/*
 * Copyright (C) fuchencong@163.com
 */


#include <stdio.h>


main()
{
	printf("hello, world\n");
}

(2)在UNIX中,要运行上述代码,首先要在某个文件中建立这个程序,并以" .c "作为文件的扩展名。通过如下命令对源程序进行编译,如果编译通过,当前目录中会产生a.out可执行文件。直接执行该程序即可。

技术分享

(3)cc其实是对系统c编译器的链接,如图可以看出,在我的系统中cc其实就是gcc的符号连接。

技术分享

(4)一个C语言程序,无论其大小如何,都是由函数和变量组成的。

(5)main函数是一个特殊的函数名,每个程序都从main函数的起点开始执行,这意味着每个程序都必须在某个位置包含一个main函数。

(6)函数之间进行数据交换的一种方法是调用函数向被调用函数提供一个值(参数)列表。

(7)用双引号括起来的字符序列称为字符串或字符串常量。

(8)类似于"\n"的转义字符序列为表示无法输入的字符或不可见的字符提供了一种通用的可扩展的机制。

以下为一个将华氏温度转化为摄氏温度的程序:

/*
 * Copyright (C) fuchencong@163.com
 */


#include <stdio.h>


main()
{
	int fahr, celsius;
	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;
	}
}

(9)包含在 /* 与 */中的字符序列将被编译器忽略,因此可以作为注释。

(10)各条语句均以分号结束。

(11)尽管C编译器并不关心程序的外观形式,但正确的缩进以及适当保留空格的程序设计风格对程序的易读性非常重要。

(12)建议每行只书写一条语句,并在运算符的两边各加上一个空格字符,这样可以使得运算的结合关系更清晰明了。

(13)在C语言及许多其他语言中,整数除法将执行舍位,结果中的任何小数部分都会被舍弃。(所以上述代码中采用的 5 * (fahr - 3)/ 9,而不是 5 / 9 * (fahr - 3) )

(14)printf函数并不是C语言本身的一部分,C语言本身并没有定义输入/输入功能。printf函数仅仅是标准库函数中一个有用的函数而已。ANSI C标准定义了printf函数的行为。

(15)由于输出数据不是右对齐的,所以输出结果不是很美观。可以将原程序中的printf语句做如下修改,通过在%d中指明打印宽度,打印的数字将会在打印区域内右对齐。

	printf("%3d\t%6d\n", fahr, celsius);

由于上述程序采用整型变量进行计算,造成精度不准确,以下程序采用浮点数进行计算:

/*
 * Copyright (C) fuchencong@163.com
 */


#include <stdio.h>


main()
{
	float fahr, celsius;
	int lower, upper, step;

	lower = 0;
	upper = 300;
	step = 20;

	fahr = lower;
	while(fahr <= upper) {
		celsius = 5.0 * (fahr - 32.0) / 9.0;
		printf("%3.0f\t%6.1f\n", fahr, celsius);
		fahr += step;
	}
}

(16)常数中的小数点表明该常数是个浮点数,因此两个浮点数相除,结果不会被舍位。

(17)即使浮点常量取得是整型值,在书写时最好还是为它加上一个显式的小数点,这样可以强调其浮点数性质。

(18)对于某个特定的任务,我们可以采用多种方法编写程序。

以下为该程序的更简单的实现方式:

/*
 * Copyright (C) fuchencong@163.com
 */


#include <stdio.h>


main()
{
    int fahr;

    for (fahr = 0; fahr <= 300; fahr += 20)
    {
        printf("%3d\t%6.1f\n", fahr, 5.0 / 9.0 * (fahr - 32));
    }
}

(19)C语言的一个通用规则:在允许使用某种类型变量值的地方,都可以使用该类型的更复杂的表达式。

(20)for语句比较适合初始化和增长步长都是单条语句并且逻辑相关的情形,因为它将循环控制语句集中在一起,且比while语句更紧凑。

(21)在程序中使用幻数并不是一个好习惯,它几乎无法向阅读该程序的人提供什么信息,而且使程序的修改变得更加困难。

以下为一个更好的实现方式:

/*
 * Copyright (C) fuchencong@163.com
 */


#include <stdio.h>

#define LOWER 0
#define UPPER 300
#define STEP 20


main()
{
	int fahr;
	for (fahr = LOWER; fahr <= UPPER; fahr += STEP) {
		printf("%3d\t%6.1f\n", fahr, 5.0 / 9.0 * (fahr - 32));
	}
}

(22)define指令可以把符号名定义为特定的字符串。定义之后,程序中所有出现在define中定义的名字(既没有用引号引起来,也不是其它名字的一部分)都将用相应的替换文本替换。

(23)通常符号常量都用大写字母拼写,这样可以很容易与用小写字母拼写的变量名相区别。

(24)define指令的末尾没有分号。

以下为一个文件复制程序:

/*
 * Copyright (C) fuchencong@163.com
 */


#include <stdio.h>


main()
{
	int c;
	while ( (c = getchar()) != EOF) {
		putchar(c);
	}
}

(25)标准库提供的输入/输出模型非常简单。无论文本从何处输入,输出到何处,其输入/输出都是按照字符流的方式处理。

(26)文本流是由多行字符构成的字符序列,而每行字符是由0或多个字符组成,行末是一个换行符。标准库负责使输入/输出流都能够遵循这一模型。

(27)getchar函数从文本流中读入下一个输入字符,并将其作为结果值返回。putchar函数将整形变量以字符的形式输出到标准输出。

(28)字符在键盘,屏幕或其它任何地方无论以什么形式表现,它在机器内部都是以位模式存储的。

(29)char类型专门用于存储字符型数据,当然任何整型也可以用于存储字符型数据。上述代码之所以采用int,是因为getchar函数有可能读到文件结束符(EOF),即代表输入结束。而EOF的具体数值与平台相关,但是可以保证与任何char类型的值都不相同。所以有可能EOF是char类型无法表示的,所以必须使用int来存储getchar的返回值,这样才能正确处理返回EOF的情况。

(30)赋值操作是一个表达式,并且具有一个值,即赋值后左边变量保存的值。

(31)赋值表达式两边的圆括号不能省略,因为不等于运算符(!=)的优先级要高于赋值运算符(=)。

以下为一个字符统计程序,采用double型以处理更多的输入:

/*
 * Copyright (C) fuchencong@163.com
 */

#include <stdio.h>


main()
{
	double nc;

	for(nc = 0.0; getchar() != EOF; nc++) {
		; // NULL	
	}
	printf("%.0f\n", nc);
}

(32)打印浮点数时,通过%.0f来强制不打印小数点和小数部分。

(33)单独的分号称为空语句。

以下为一个行统计程序:

/*
 * Copyright (C) fuchencong@163.com
 */


#include <stdio.h>


main()
{
	int c, nl;

	nl = 0;
	while ( (c = getchar()) != EOF) {
		if (c == '\n') {
			++nl;
		}
	}
	printf("%d\n", nl);
}

(34)单引号内的字符表示一个整型值,该值等于此字符所在机器字符集中对应的数值,我们称之为字符常量。

(35)转义字符也是合法的字符常量。

以下为一个单词计数程序,它也是UNIX系统中wc程序的骨干:

/*
 * Copyright (C) fuchencong@163.com
 */


#include <stdio.h>

#define STATE_OUT 0
#define STATE_IN 1

main()
{
	int nc, nl, nw, c, state;

	nc = nl = nw = 0;
	state = STATE_OUT;
	while ( (c = getchar()) != EOF) {
		++nc;
		if (c == '\n') {
			++nl;
		}
		if (c == ' ' || c == '\t' || c == '\n') {
			state = STATE_OUT;
		}
		else if (state == STATE_OUT) {
			state = STATE_IN;
			++nw;
		}
	}
	printf("%d %d %d\n", nc, nl, nw);
}

(36)如果程序中的幻数都以符号常量的形式出现,对程序进行大量修改就会容易的多。

(37)赋值的结合次序是从由右至左。

以下为一个统计各个数字,空白符以及其他所有字符的程序:

/*
 * Copyright (C) fuchencong@163.com
 */


#include <stdio.h>


main()
{
	int i, c, nwhite, nother;
	int ndigit[10];

	nwhite = nother = 0;
	for (i = 0; i < 10; i++) {
		ndigit[i] = 0;
	}
	
	while ( (c = getchar()) != EOF) {
		if (c == ' ' || c == '\t' || c == '\n') {
			++nwhite;
		}
		else if (c >= '0' && c <= '9') {
			++ndigit[c - '0'];
		}
		else {
			++nother;
		}
	}

	printf("digits =");
	for (i = 0; i < 10; i++) {
		printf(" %d", ndigit[i]);	
	}
	printf(", nwhite = %d, nother = %d\n", nwhite, nother);	

}

(38)在C语言中,数组下标总是从0开始。数组下标可以是任何整型表达式。

(39)上述程序正常能够正常运行,依赖于这么一个假设:‘0‘,‘1‘,‘2‘......‘9‘具有连续递增的值。幸运的是,在所有字符集中,这个假设都成立。

(40)char类型的字符是小整型,因此char类型的变量和常量在算术表达式中等价于int类型的变量和常量。

以下为一个计算幂运算的程序(该程序不够健壮):

/*
 * Copyright (C) fuchencong@163.com
 */


#include <stdio.h>

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 = 0; i < n; i++) {
		p = base * p;
	}
	return p;
}

(41)函数为计算的封装提供了一种简便的方法,此后使用函数时不需要考虑它是如何实现的。

(42)函数定义可以以任意次序出现在一个源文件或多个源文件中,但同一函数不能分割存放在多个文件中。

(43)函数参数使用的名字只在函数内部有效。

(44)main本身也是函数,因此也可以向其调用者返回一个值,该调用者实际上就是程序的执行环境。一般来说,返回值为0表示正常终止,返回值为非0表示出现异常情况或出错结束条件。

(45)函数原型必须与函数定义和用法一致。如果函数的定义、用法和函数原型不一致,将会出现错误。

(46)函数原型中的参数名是可选的,但是合适的参数名能够起到很好的说明性作用,因此我们在函数原型中总是指明参数名。

以下使用较早版本的C语言实现该程序:

/*
 * Copyright (C) fuchencong@163.com
 */


#include <stdio.h>

int power();

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(base, n)
int base, n;
{
	int i, p;
	
	p = 1;
	for(i = 0; i < n; i++) {
		p = base * p;
	}
	return p;
}

(47)ANSI C同较早版本的C语言之间的最大区别在于函数的声明与定义的方式不同。

(48)在C语言最初的定义中,函数声明中不允许包含参数列表,这样编译器就无法在此时检查出函数调用的合法性。而在ANSI C中定义的函数原型语法中,编译器可以很容易地检测出函数调用中的参数数目和类型方面的错误。

(49)ANSI C仍然支持旧式的函数声明与定义,但在使用新式的编译器时,最好使用新式的函数原型声明方式。

(50)在C语言中,所有函数参数都是“通过值”传递的。在C语言中,被调用函数不能直接修改主调函数中变量的值,而只能修改自己私有的临时副本的值。

(51)传值调用利大于弊,在被调用函数中,参数可以看做是便于初始化的局部变量,因此额外使用的变量更少,这样使程序更简洁。

(52)如果是数组参数,情况就有所不同了。把数组名作为参数时,传递给函数的值是数组首元素的地址--它并不复制数组元素本身。

(53)函数的默认返回值类型为int。

以下程序读入一组文本行,并将最长的文本行打印出来(该程序不够健壮):

/*
 * Copyright (C) fuchencong@163.com
 */


#include <stdio.h>

#define MAXLINE 1000


int get_line(char line[], int maxline);
void copy(char to[], char from[]);


main()
{
	int len, max;
	char line[MAXLINE];
	char longest[MAXLINE];

	len = max = 0;
	while ( (len = get_line(line, MAXLINE)) != 0) {
		if (len > max) {
			copy(longest, line);
			max = len;
		}
	}
	if (max > 0) {
		printf("%s", longest);	
	}
	return 0;
}


int
get_line(char line[], int maxline)
{
	int c, i;
	for (i = 0; i < maxline-1 && (c = getchar()) != EOF && c != '\n'; i++) {
		line[i] = c;
	}

	if (c == '\n') {
		line[i++] = c;
	}

	line[i] = '\0';
	return i;
}


void
copy(char to[], char from[])
{
	int i;

	i = 0;
	while ( (to[i] = from[i]) != '\0') {
		i++;
	}
}

(54)C语言中的字符串常量,以字符数组的形式存储,数组中的各元素分别存储字符串的各个字符,并以‘\0‘标志字符串的结束。printf函数中的格式规范%s规定,对应的参数必须是以这种形式表示的字符串。

(55)自动变量只在函数调用期间存在。如果自动变量没有赋值,则其中存放的是无效值。

(56)在所有函数中都可以通过变量名访问的变量称为外部变量,外部变量可以在全局范围内访问,因此函数间可以通过外部变量交换数据,而不必使用参数表。

(57)外部变量在程序执行期间一直存在。

(58)外部变量必须定义在所有函数之外,且只能定义一次,定义后编译程序将为它分配存储单元。

(59)在每个需要访问外部变量的函数中,必须声明相应的外部变量,声明时可以使用extern语句显示声明,也可以通过上下文隐式声明。

(60)在源文件中,如果外部变量的定义出现在使用它的函数之前,那么在这个函数中就没有必要使用extern语句声明该外部变量。

(61)ANSI C语言把空参数列表看成老版本C语言的声明方式,并且对参数表不再进行任何检查。在ANSI C中,如果要声明空参数表,则必须使用关键字void进行显示声明。

(62)谨慎地使用定义与声明。定义表示创建变量或分配存储单元,而声明指的是说明变量的性质,并不分配存储单元。

(63)过分依赖外部变量会导致一定的风险,因为它会使程序中的数据关系模糊不清,而且会使程序的修改变得十分困难


重读《C程序设计语言》(2):导言