《C程序设计语言》第一章导言
1.1 入门
与Windows平台下在Visual Studio中开发的不同。
尽管这个练习很简单,但对于初学语言的人来说,它仍然可能是一大障碍。
因为我们首先必须编写程序文本,然后成功地编译,并加载、运行。
掌握了这些操作细节以后,其他事情才会比较容易。
#include <stdio.h>
main()
{
printf("hello, world\n");
}
练习1-2 做个试验,当printf函数的参数字符串中包含\c时,观察一下会出现什么情况。
答:printf("hello, \cworld");
编译时警告:unknown escape sequence: '\c'。
执行时打印hello, cworld。
1.2 变量与算术表达式
%d 按照十进制整型数打印
%6d 按照十进制整型数打印,至少6个字符宽
%f 按照浮点数打印
%6f 按照浮点数打印,至少6个字符宽
%.2f 按照浮点数打印,小数点后有两位小数
%6.2f 按照浮点数打印,至少6个字符宽,小数点后有两位小数
练习1-3和1-4 编写一个程序打印摄氏温度转换为相应华氏温度的转换表。
答:
#include <stdio.h>
main()
{
float fahr, celsius;
float lower, upper, step;
lower = 0;
upper = 50;
step = 10;
printf("%s\t%s\n", "Celsius", "Fahr");
celsius = lower;
while (celsius <= upper) {
fahr = celsius * 9.0 / 5.0 + 32.0;
printf("%3.0f\t%6.1f\n", celsius, fahr);
celsius = celsius + step;
}
}
main()
{
float fahr, celsius;
float lower, upper, step;
lower = 0;
upper = 50;
step = 10;
printf("%s\t%s\n", "Celsius", "Fahr");
celsius = lower;
while (celsius <= upper) {
fahr = celsius * 9.0 / 5.0 + 32.0;
printf("%3.0f\t%6.1f\n", celsius, fahr);
celsius = celsius + step;
}
}
1.3 for语句
main()
{
int fahr;
for (fahr = 0; fahr <= 300; fahr = fahr + 20)
printf("%3d %6.1f\n", fahr, (5.0/9.0)*(fahr-32));
}
与while循环相比的主要改进:去掉了大部分变量而只使用一个int类型的变量。
温度的上限、下限、步长都是常量,而计算摄氏温度的表达式变成了printf函数的第三个参数。
以上几点改进中的最后一点是C语言中一个通用规则:在允许使用某种类型变量值的任何场合,
都可以使用该类型的更复杂的表达式。
练习1-5 修改温度转换程序,要求以逆序(从300度到0度的顺序)打印温度转换表。
答:
#include <stdio.h>
main()
{
int fahr;
for (fahr = 300; fahr >= 0; fahr = fahr - 20)
printf("%3d %6.1f\n", fahr, (5.0/9.0)*(fahr-32));
}
main()
{
int fahr;
for (fahr = 300; fahr >= 0; fahr = fahr - 20)
printf("%3d %6.1f\n", fahr, (5.0/9.0)*(fahr-32));
}
1.4 符号常量
程序中使用300、20等幻数不是好习惯,可以通过
#define 名字 替换文本
把符号名(或称为符号常量)定义为一个特定的字符串。
在该定义之后,程序中出现的所有在#define中定义的名字(既没有用引号引起来,
也不是其他名字的一部分)都将用相应的替换文本替换。替换文本可以是任何字符序列,
而不仅限于数字。
#define LOWER 0
#define UPPER 300
#define STEP 20
for (fahr = LOWER; fahr <= UPPER; fahr = fahr + STEP)
...
1.5 字符输入/输出
标准库提供一次读/写一个字符的函数,最简单的是getchar和putchar两个函数。
文本流是由多行字符构成的字符序列,而每行字符则由0个或多个字符组成,行末是一个换行符。
文本复制
#include <stdio.h>
main()
{
int c;
c = getchar();
while (c != EOF) {
putchar(c);
c = getchar();
}
}
在声明变量c时,必须让它大到足以存放getchar函数返回的任何值。这里之所以不把c声明成char类型,
是因为它必须足够大,除了能存储任何可能的字符外还要能存储文件结束符EOF。EOF定义在头文件
<stdio.h>中,是个整型数,其具体数值是什么并不重要,只要它与任何char类型的值都不相同即可。
对于经验比较丰富的C语言程序员,可以写的更精炼一些。在C语言中,类似于c = getchar()之类的赋值
操作是一个表达式,并且具有一个值,即赋值后左边变量保存的值。
int c;
while ((c = getchar()) != EOF)
putchar(c);
这段程序使输入集中化,更紧凑。这种方式编写的程序更易阅读。
赋值表达式两边的圆括号不能省略。不等于运算法!=的优先级比赋值运算符=的优先级要高。
相当于:c = (getchar() != EOF)。c的值将被置为0或1(取决于是否碰到文件结束标志)。
练习1-6 验证表达式getchar() != EOF的值是0还是1。
答:
#include <stdio.h>
main()
{
printf("%d\n", getchar() != EOF);
}
main()
{
printf("%d\n", getchar() != EOF);
}
结果是:1。(输入任意字符,都不等于EOF,表达式为真)
练习1-7 编写一个打印EOF值的程序。
答:
#include <stdio.h>
main()
{
printf("%d\n", EOF);
}
main()
{
printf("%d\n", EOF);
}
结果是:-1。(ASCII中没有值为-1的字符,所以变量c要用int型保存)
字符计数
练习1-8 编写一个统计空格、制表符与换行符个数的程序。
答:
#include <stdio.h>
main()
{
int space = 0, table = 0, enter = 0;
int c;
while ((c = getchar()) != EOF) {
if (c == ' ')
++space;
else if (c == '\t')
++table;
else if (c == '\n')
++enter;
}
printf("space: %d, table: %d, enter: %d\n", space, table, enter);
}
main()
{
int space = 0, table = 0, enter = 0;
int c;
while ((c = getchar()) != EOF) {
if (c == ' ')
++space;
else if (c == '\t')
++table;
else if (c == '\n')
++enter;
}
printf("space: %d, table: %d, enter: %d\n", space, table, enter);
}
练习1-9 编写一个将输入复制到输出的程序,并将其中连续的多个空格用一个空格代替。
答:
#include <stdio.h>
main()
{
int inspace = 0;
int c;
while ((c = getchar()) != EOF) {
if (c != ' ') {
if (inspace) {
putchar(' ');
inspace = 0;
}
putchar(c);
} else if (!inspace)
inspace = 1;
else
; // discard space
}
putchar('\n');
}
main()
{
int inspace = 0;
int c;
while ((c = getchar()) != EOF) {
if (c != ' ') {
if (inspace) {
putchar(' ');
inspace = 0;
}
putchar(c);
} else if (!inspace)
inspace = 1;
else
; // discard space
}
putchar('\n');
}
练习1-10 编写一个将输入复制到输出的程序,并将其中的制表符替换为\t,把回退符替换
为\b,把反斜杠替换为\\。这样可以将制表符和回退符以可见的方式显示出来。
答:
#include <stdio.h>
main()
{
int c;
while ((c = getchar()) != EOF) {
if (c == '\t') {
putchar('\\');
putchar('t');
}
else if (c == '\n') {
putchar('\\');
putchar('b');
}
else if (c == '\\') {
putchar('\\');
putchar('\\');
}
else
putchar(c);
}
putchar('\n');
}
main()
{
int c;
while ((c = getchar()) != EOF) {
if (c == '\t') {
putchar('\\');
putchar('t');
}
else if (c == '\n') {
putchar('\\');
putchar('b');
}
else if (c == '\\') {
putchar('\\');
putchar('\\');
}
else
putchar(c);
}
putchar('\n');
}
单词统计
命令wc的主要部分。
练习1-11 如何测试单词计数程序?如果程序中存在某种错误,那么什么样的输入最可能发现这类错误?
练习1-12 编写一个程序,以每行一个单词的形式打印其输入。
答:
#include <stdio.h>
main()
{
int c;
while ((c = getchar()) != EOF) {
if (c == ' ' || c == '\n' || c == '\t')
putchar('\n');
else
putchar(c);
}
putchar('\n');
}
main()
{
int c;
while ((c = getchar()) != EOF) {
if (c == ' ' || c == '\n' || c == '\t')
putchar('\n');
else
putchar(c);
}
putchar('\n');
}
1.6 数组
数组下标可以是任何整型表达式,包括整型变量和整型常量。
if (c>= '0' && c <= '9') 用于判断c中的字符是否为数字。
如果它是数字,那么该数字对应的数值是:c - '0'
练习1-14 编写一个程序,打印输入中各个字符出现频度的直方图。
答:
#include <stdio.h>
main()
{
int freq[26];
int x;
for (x = 0; x < 26; ++x)
freq[x] = 0;
int c;
while ((c = getchar()) != EOF) {
if (c >= 'a' && c <= 'z')
++freq[c - 'a'];
}
int i, j;
for (i = 0; i < 26; ++i) {
printf("%c:\t", 'a' + i);
for (j = 0; j < freq[i]; ++j)
putchar('*');
putchar('\n');
}
}
main()
{
int freq[26];
int x;
for (x = 0; x < 26; ++x)
freq[x] = 0;
int c;
while ((c = getchar()) != EOF) {
if (c >= 'a' && c <= 'z')
++freq[c - 'a'];
}
int i, j;
for (i = 0; i < 26; ++i) {
printf("%c:\t", 'a' + i);
for (j = 0; j < freq[i]; ++j)
putchar('*');
putchar('\n');
}
}
1.7 函数
函数原型与函数声明中参数名不要求相同。事实上,函数原型中的参数名是可选的。
int power(int m, int n);
int power(int base, int n)
{
int i, p;
p = 1;
for (i = 1; i <= n; ++i)
p = p * base;
return p;
}
ANSI C同较早版本C语言之间的最大区别在于函数的声明与定义的不同。
power(base, n)
int base, n;
{
...
}
参数名在圆括号内制定,参数类型在左花括号之前声明。函数体与ANSI C中形式相同。
函数不一定有返回值。不带表达式的return语句
1.8 参数--传值调用
在C语言中,所有函数参数都是通过值传递的。也就是说:
传递给被调用函数的参数值存放在临时变量中,而不是存放在原来的变量中。
被调用函数不能直接修改主调函数中变量的值,而只能修改其私有的临时副本的值。
传值调用的利大于弊。参数可看做是初始化好的局部变量,因此额外使用的变量更少。
这样程序可以更紧凑简洁。
int power(int base, int n)
{
int p;
for (p = 1; n > 0; --n)
p = p * base;
return p;
}
必要时,也可以让函数能够修改主调函数中的变量。这种情况下,调用者需要向被调用
函数提供变量的地址(就是指向变量的指针)。
1.9 字符数组
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;
}
{
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;
}
getline函数把字符'\0'插入到数组末尾,以标记字符串的结束。这一约定已被C语言采用。
printf函数中的格式规范%s规定,对应的参数必须是以这种形式表示的字符串。
copy函数的实现正是依赖于输入参数由'\0'结束这一事实。
值得一提的是,即使是这样很小的程序,在传递参数时也会遇到一些麻烦的设计问题。
getline函数无论是否到达换行符,当数组满时它将停止读字符。
main函数可以通过测试行的长度以及检查返回的最后一个字符来判断当前行是否太长。
练习1-16 修改打印最长文本行程序的main函数,使之打印任意长度的输入行长度。
练习1-17 编写一个程序,打印长度大于80个字符的所有输入行。
答:
#include <stdio.h>
#define MAXLINE 1000
int getline2(char line[], int maxline); //与stdio.h中的getline冲突
main()
{
int len;
char line[MAXLINE];
while ((len = getline2(line, MAXLINE)) > 0)
if (len > 80) {
printf("%s\n", line);
}
return 0;
}
int getline2(char line[], int maxline); //与stdio.h中的getline冲突
main()
{
int len;
char line[MAXLINE];
while ((len = getline2(line, MAXLINE)) > 0)
if (len > 80) {
printf("%s\n", line);
}
return 0;
}
练习1-18 编写一个程序,删除每个输入行末尾的空格及制表符,并删除完全是空格的行。
答:(不好确定何时字符结束开始空格,方法是从后向前找,直到最后一个字符)
int remove(char s[])
{
int i = 0;
while (s[i] != '\n')
++i;
--i; // back off from '\n'
// locate last letter
while (i >= 0 && (s[i] == ' ' || s[i] == '\t'))
--i;
if (i >= 0) {
++i;
s[i] = '\n';
++i;
s[i] = '\0'; // terminate the string
}
return i;
}
{
int i = 0;
while (s[i] != '\n')
++i;
--i; // back off from '\n'
// locate last letter
while (i >= 0 && (s[i] == ' ' || s[i] == '\t'))
--i;
if (i >= 0) {
++i;
s[i] = '\n';
++i;
s[i] = '\0'; // terminate the string
}
return i;
}
练习1-19 编写函数reverse(s),将字符串中的字符顺序颠倒过来。
答:
#include <stdio.h>
void reverse(char s[])
{
int i, j;
j = 0;
while (s[j] != '\0')
++j;
char temp;
for (i=0,j=j-1; i<j; ++i,--j) {
temp = s[i];
s[i] = s[j];
s[j] = temp;
}
}
main()
{
char s[] = "abcdefg";
reverse(s);
printf("%s\n", s);
char s2[] = "123456";
reverse(s2);
printf("%s\n", s2);
return 0;
}
void reverse(char s[])
{
int i, j;
j = 0;
while (s[j] != '\0')
++j;
char temp;
for (i=0,j=j-1; i<j; ++i,--j) {
temp = s[i];
s[i] = s[j];
s[j] = temp;
}
}
main()
{
char s[] = "abcdefg";
reverse(s);
printf("%s\n", s);
char s2[] = "123456";
reverse(s2);
printf("%s\n", s2);
return 0;
}
1.10 外部变量与作用域
函数中的每个局部变量只在函数被调用时存在,在函数执行完毕退出时消失。所以称为自动变量。
可以定义位于所有函数外部的变量,称为外部变量。
在每个需要访问外部变量的函数中,必须声明相应的外部变量,说明其类型。
声明时可以用extern语句显示声明,也可以通过上下文隐式声明。
在源文件中,如果外部变量的定义出现在使用它的函数之前,那么就没有必要使用extern声明。
通常的做法是,所有外部变量的定义都放在源代码的开始处,这样就可以省略extern声明。
如果程序包含在多个源文件中,就需要使用extern声明来建立该变量与其定义之间的联系。
通常把变量和函数的extern声明放在一个单独的文件中(习惯上称为头文件)。
练习1-20 编写程序detab,将输入中的制表符替换成适当数目的空格,使空格充满到下一个
制表符终止位的地方。