1. Hello, world
#include <stdio.h>
main()
{
printf("Hello, world\n");
}
a. #include < stdio.h>用于告诉编译器在本程序中包含标准输入/输出库的信息。
b. printf是一个用于打印输出的库函数。顺便指出,printf函数并不是C语言本身的一部分,C语言本身并没有定义输入输出功能。printf仅仅是标准库函数中的一个有用的函数而已,这些标准库函数在C语言程序中通常可以使用。但是,ANSI标准定义了printf函数的行为,因此对每个符合该标准的编译器和库来说,该函数的属性都是相同的。
c. 类似于\n的转义字符序列为表示无法输入的字符或不可见的字符提供了一种通用的可扩充的机制。
2.字符输入/输出
标准库提供的输入/输出模型非常简单。无论文本从何处输入,输出到何处,其输入/输出都是按照字符流的方式处理。文本流是由多行字符构成的字符序列,而每行字符则由0个或多个字符组成,行末是一个换行符。标准库负责使每个输入/输出流都遵守这一模型。许多程序不过是这一模型的扩充版本而已。
标准库提供了一次读/写一个字符的函数,其中最简单的是getchar和putchar这两个函数。每次调用时,getchar函数从文本流中读入下一个输入字符,并将其作为结果值返回。调用putchar函数时将打印一个字符,例如putchar(c)将整型变量c的内容以字符的形式打印出来,显示在屏幕上。使用这两个函数的最简单的例子是:将输入逐字符地复制到输出中。
2.1. 将输入复制到输出
#include <stdio.h>
/**<将输入复制到输出;版本1 */
main()
{
int c;
c = getchar();
while (c != EOF)
{
putchar(c);
c = getchar();
}
}
a. 关于int c。字符在键盘、屏幕或其他的任何地方无论以什么形式表现,它在机器内部都是以位模式存储的。char类型专门用于存储这种字符型数据,当然任何整型也可以用于存储字符型数据。
b. 关于EOF。EOF表示文件结束,EOF定义在头文件<stdio.h>中,是一个整型数,其具体数值不重要,只要它与任何char类型的值都不相同即可。这里使用符号常量,可以确保程序不需要依赖于其对应的任何特定的数值。在黑框中,键入CTRL+Z即表示EOF。用下面的程序得知,EOF在本机上显示为-1。
#include <stdio.h>
main()
{
printf("%d",EOF);
}
考虑一个更精炼的版本:
#include <stdio.h>
/**<将输入复制到输出;版本2 */
main()
{
int c;
while ((c = getchar()) != EOF)
putchar(c);
}
a. 关于(c = getchar()) != EOF。这段程序表示:在while内部,首先读一个字符,并将其赋值给c,然后检测该字符是否为文件结束标志。
2.2 字符计数
#include <stdio.h>
/**< 统计输入的字符数;版本1 */
main()
{
long nc;
nc = 0;
while (getchar() != EOF)
++nc;
printf("%1d\n", nc);
}
a. 程序使用long类型的变量存放计数值。长整型至少占用32位存储单元。在某些机器上long和int类型的长度相同,但在一些机器上,int型的可能只有16位存储单元的长度。使用double型可以处理更大的数字。
b. ++nc可以用nc = nc + 1进行代替。但++nc效率更高。
考虑一个更精炼的版本:
#include <stdio.h>
/**< 统计输入的字符数;版本2 */
main()
{
double nc;
for (nc = 0; getchar() != EOF; ++nc)
;
printf("%.0f\n", nc);
}
a. 所有工作都在条件语句部分与增加布长部分完成了,但C语言的语法要求for的循环语句必须有一个循环体,因此用单独的分号代替。单独的分号称为空语句。
2.3. 行计数
#include <stdio.h>
/**< 行计数 */
main()
{
int c, nl;
nl = 0;
while ((c = getchar()) != EOF)
if (c == '\n')
++nl;
printf("%d\n", nl);
}
a. 行计数即统计有多少个换行符。
b. 单引号中的字符表示一个整型值,该值等于此字符在机器字符集中对应的数值,即字符常量。比如,'A'是一个字符常量,在ASCII字符集中其值为65。用'A'比用65要好,因为'A'的意义更清楚,且与特定的字符集无关。'\n'在表达式中不过是一个整型数,而"\n"是一个仅包含一个字符的字符串常量。
#include <stdio.h>
#define BLANK ' '
/** 练习 1-9
*
* 编写一个将输入复制到输出的程序,
* 将其中连续的多个空格用一个空格代替。
*
*
*/
main()
{
int c, last;
while ((c = getchar()) != EOF)
{
if (c != BLANK || last != BLANK)
putchar(c);
last = c;
}
}
#include <stdio.h>
#define BACKSLASH '\\'
#define BACKSPACE '\b'
#define TAB '\t'
/** 练习 1-10
*
* 编写一个将输入复制到输出的程序,
* 将制表符替换为\t,
* 将回退符替换为\b,
* 将反斜杠替换为\\,
* 将制表符和回退符以可见的方式显示出来。
*/
main()
{
int c;
while ((c = getchar()) != EOF)
{
if (c == BACKSLASH)
printf("\\\\");
else if (c == BACKSPACE)
printf("\b");
else if (c == TAB)
printf("\t");
else
putchar(c);
}
}
2.4. 单词计数
#include <stdio.h>
#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", nl, nw, nc);
}
a. 关于IN和OUT。使用IN和OUT,使得程序更具有可读性。
b. 关于nl = nw = nc = 0。在兼有值与赋值两种功能的表达式中,赋值结合次序是从右到左。该语句等同于nl = ( nw = ( nc = 0 ) )。
#include <stdio.h>
/** 练习 1-12
*
* 编写一个程序,
* 以每行一个单词的形式打印其输入。
*
*
*/
main()
{
int c, INSPACE; /**< INSPACE记录之前是否位于单词中。0为在,1为不在。 */
INSPACE = 0;
while ((c = getchar()) != EOF)
{
if (c == '\t' || c == '\n' || c == ' ')
{
if (INSPACE == 0)/**< 之前在单词中,而现在不在 */
putchar('\n');
INSPACE = 1;
}
else
{
INSPACE = 0;
putchar(c);
}
}
}
3. 函数
#include <stdio.h>
/**< 计算x的y次方 */
int power(int x, int y);
main()
{
int i;
for (i = 0; i < 10; ++i)
printf("%d\t%d\t%d\n", i, power(2, i), power(-3, i));
return 0;
}
int power(int x, int y)
{
int i, p;
p = 1;
for (i = 1; i <= y; ++i)
p = p * x;
return p;
}
a. 区分函数声明(declaration)与函数定义(define)。定义表示创建变量或分配储存单元,而声明指的是说明变量的性质,但并不分配存储单元。在上面的例子中,出现在main函数之前的语句是声明语句,声明参数的类型、名字以及该函数返回结果的类型。这种声明被称为函数原型。出现在main函数后面的语句是函数定义语句。
b. 关于main函数末的return 0语句。main本身也是函数,因此可以向其调用者返回一个值,该调用者实际上就是程序的执行环境。一般而言,返回值为0表示正常终止,返回值非0表示出现异常情况或出错结束条件。为简洁起见,前面的main函数都省略了return语句,但在之后的main函数中都包含return语句,提醒大家注意,程序还要向其执行环境返回状态。
4. 字符数组
#include <stdio.h>
#define MAXLINE 1000 /**< 允许的输入行的最大长度 */
int getline(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 = getline(line, MAXLINE)) > 0)
{
if (len > max)
{
max = len;
copy(longest, line);
}
}
if (max > 0) /**< 存在这样的最长行 */
printf("%s", longest);
return 0;
}
/**< 将一行读入s中并返回其长度 */
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;
}
/**< 将from复制到to */
void copy(char to[], char from[])
{
int i;
i = 0;
while ((to[i] = from[i]) != '\0')
++i;
}
a. 关于'\0'。getline函数将空字符插入到它创建的数组的末尾,以标记字符串的结束。该约定已经被C语言采用。例如,挡在C语言程序中出现"hello\n"的字符串常量时,它将以字符数组的形式存储,数组的各元素分别存储字符串的各个字符,并以'\0'标记结束。
h | e | l | l | o | \n | \0 |
#include <stdio.h>
#define MAXLINE 1000
#define CRITICALVALUE 80
int getline(char line[], int maxline);
<pre name="code" class="cpp">/** 练习 1-17
*
* 打印长度大于80个字符的所有输入行
*
*/
main()
{
int c;
int len = 0;
char line[MAXLINE];
while ((len = getline(line,MAXLINE)) > 0)
if (len > CRITICALVALUE)
printf("%s", line);
return 0;
}
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;
}
#include <stdio.h>
#define MAXLINE 1000
int getline(char line[], int maxline);
void reverse(char line[], int length);
/** 练习 1-19
*
* 编写函数reverse(s),将字符串中的字符顺序颠倒过来。
* 每次颠倒一个输入行中的字符顺序。
*/
main()
{
int len;
char line[MAXLINE];
while ((len = getline(line, MAXLINE)) > 0)
{
reverse(line, len);
printf("%s", line);
}
return 0;
}
int getline(char s[], int lim)
{
int c, len;
for (len = 0; len < lim - 1 && (c = getchar()) != EOF && c != '\n'; ++len)
s[len] = c;
if (c == '\n')
{
s[len] = c;
++len;
}
s[len] = '\0';
return len;
}
void reverse(char s[], int len)
{
int i;
for (i = 0; i < len / 2; ++i)
{
int tmp = s[i];
s[i] = s[len - 2 - i];
s[len - 2 - i] = tmp;
}
}
5. 外部变量与作用域
外部变量必须定义在所有函数之外,且只能定义一次,定以后编译程序将为它分配存储单元。在每个需要访问外部变量的函数中,必须声明对应的外部变量,此时说明其类型。声明时可以用extern语句显式声明,也可以通过上下文隐式声明。
#include <stdio.h>
#define MAXLINE 1000 /**< 允许的输入行的最大长度 */
int max;
char line[MAXLINE];
char longest[MAXLINE];
int getline();
void copy();
/**< 读入一组文本行,并把其中最长的文本行打印出来:特殊版本 */
main()
{
int len; /**< 当前行长度 */
extern int max; /**< 目前发现的最大行长度 */
extern char longest[MAXLINE]; /**< 用于保存最长的行 */
max = 0;
while ((len = getline()) > 0)
{
if (len > max)
{
max = len;
copy();
}
}
if (max > 0) /**< 存在这样的最长行 */
printf("%s", longest);
return 0;
}
/**< 读入并返回其长度 */
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;
}
/**< 复制 */
void copy(char to[], char from[])
{
int i;
extern char line[], longest[];
i = 0;
while ((longest[i] = line[i]) != '\0')
++i;
}
a. 函数在使用外部变量之前,必须要知道外部变量的名字。要达到该目的,一种方式是在函数中使用extern类型的声明。某些情况下可以省略extern声明。在源文件中,如果外部变量的定义出现在使用它的函数之前,那么在那个函数中就没有必要使用extern声明。因此,main、getline、copy中的几个extern声明都是多余的。在通常的做法中,所有外部变量的定义都放在源文件的开始处,这样就可以省略extern声明。
如果程序包含在多个源文件中,而某个变量在文件1中定义,在文件2和文件3中使用,那么在文件2和文件3中就需要使用extern声明来建立该变量与其定义之间的联系。人们通常把变量和函数的extern声明放在一个单独的头文件中,并在每个源文件的开头使用#include语句把所要用的头文件包含进来。
过分依赖外部变量会导致一定的风险,因为它会使程序中的数据关系模糊不清——外部变量的值可能会被意外地或不经意地修改,而程序的修改变得十分困难。上面所示的代码V2比V1要差,原因在于:V2使用了外部变量;V2中的函数把所操纵的变量名直接写入了函数,从而使这两个有用的函数失去了通用性。