静下心来,从头学习一遍C语言;
导言
导言的目的是希望读者能尽快地编写出有用的程序;
导言的内容是使用变量与常量、算术运算、控制流、函数、基本输入/输出;
一个经验丰富的程序员应该可以从导言里的内容中推知他们自己进行程序设计所需要的一些基本元素;
1.1 入门
学习一门新程序设计语言的唯一途径就是使用它编写程序;
#include <stdio.h>
void main()
{
printf("Hello,world");
}
程序的过程就是编写程序文本,然后成功地进行编译,加载、运行,最后输出到某个地方。
上面的内容输出的是使用printf函数;
使用VS输出:
使用linux:
wang@myubuntu:~/code$ vim hello.c
wang@myubuntu:~/code$ more hello.c
#include <stdio.h>
void main()
{
printf("hello,world");
}
wang@myubuntu:~/code$ cc hello.c
wang@myubuntu:~/code$ ls
a.out hello.c
wang@myubuntu:~/code$ ./a.out
hello,worldwang@myubuntu:~/code$
一个C语言程序,无论其大小如何,都是由变量和函数组成的。函数中包含了一些语句,以指定所要执行的计算操作。变量则用于存储计算过程中使用的值。函数的命名没有限制,main是一个特殊的函数名——每个程序都是从main函数的起点开始执行,这意味着每个程序都必须有一个main函数。
main函数通常会调用其他函数来帮助完成某些工作,被调用的函数可以是程序设计人员自己编写的,也可以是来自函数库。
#include用来告诉编译器在本程序中会使用到的函数或者变量所在文件位置;
函数之间进行数据交换的一种方式是调用函数向被调用函数提供一个值(成为参数)列表;函数名后面的一对圆括号将参数列表括起来。
使用函数时,只需要使用函数名加上圆括号括起来的参数表即可。
用双括号扣起来的字符序列成为字符串或字符串常量。
在C语言中,字符序列\n表示换行符,在打印中遇到它时,输出打印将换行,从下一行的左端行首开始。
程序中每一条语句的结尾都应该是一个分号;
;
当printf函数的参数字符串中包含\c时,观察一下会出现什么情况;
#include <stdio.h>
void main()
{
printf("Hello,world \c");
}
1.2 变量与算术表达式
打印华氏温度与摄氏温度对照表:
#include <stdio.h>
void main()
{
int lower, upper, step;
int fahr, celsius;
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 lower, upper, step;
int fahr, celsius;
类型int表示其后所列变量为整数,与之相对应的,float表示所列变量为浮点数(通常带有小数部分的数);int与float的取值范围取决于具体的机器。
对于int类型,通常32位,其取值范围在-2147483648~+2147483647之间;
对于float类型,通常32位,其取值范围在3.4E-38 ~3.4E+38或者-(3.4E-38 ~3.4E+38)float的指数位有8位,尾数位有23位,符号位1位。
除了int和float,C语言还有char字符、short短整型、long长整型以及double双精度浮点类型;这些数值类型是C语言的基本数据类型;
while语句:
循环语句是程序中重要的一环,在C语言中,有while、do-while、for组成循环功能;这里使用的while语句,while的执行方式为:首先测试圆括号中的条件;如果条件为真,则执行循环体;然后再重新判断圆括号内部的条件,如果为真,则再次执行循环体;当圆括号内的条件测试结果为假时,循环结束,并继续执行跟在while循环语句之后的下一条语句。
while语句的循环体可以使用花括号括起来的一条或多条语句,也可以是不用花括号包括的单条语句;
C编译器并不关心程序的外观形式,但正确的缩进以及保留适当空格的程序设计风格对程序的易读性非常重要。
程序中的计算公式,是先乘法后除法;
celsius = 5 * (fahr - 32) / 9;
上式和下式不同,下式计算出的数值会与上式计算数值不同,下式的结果都是0;
celsius = 5 / 9 * (fahr - 32);
其原因是在C语言及许多其他语言中,整数除法操作为执行舍位,结果中的任何小数部分都会被舍弃。下式中5/9结果为小数,后被截断后只有0留下,所以结果都是0了;
printf函数
printf函数并不是C语言本身的一部分,C语言本身并没有定义输入/输出功能。
printf仅仅是标准库函数中一个有用的函数而已,这些标准库函数在C语言程序中通常都可以使用。
printf("%3.0f %6.1f\n", fahr, (5.0 / 9.0) * (fahr - 32));
格式说明可以省略宽度与精度;
%3.0f表明待打印的浮点数至少占3个字符宽,且不带小数点和小数部分;
%6.1f表明另一个待打印的数至少占6个字符宽,且小数点后面有1位数字。
%.2f指定待打印的浮点数的小数点后有两位小数,但宽度没有限制;
%f则仅仅要求按照浮点数打印该数;
%d表示十进制数;
%o表示八进制数;
%x表示十六进制数;
%c表示字符;
%s表示字符串;
%%表示百分号(%)本身;
1.3 for
ANSI标准定义了printf函数的行为,所以,对于每个符合该标准的编译器和库来说,该函数的属性都是相同的。
C语言中一个通用规则:在允许使用某种类型变量的任何场合,都可以使用该类型的更复杂的表达式;
1.4 符号常量
处理幻数/魔数的一种方法是赋予它们有意义的名字;
#define指令可以把符号名(或成为符号常量)定义一个特定的字符串:
#define 名字 替换文本
在该定义之后,程序中出现的所有在#define中定义的名字(既没有用引号引起来,也不是其他名字的一部分)都将用相应的替换文本替换。
符号常量同变量名规则类似,以字母打头的字母和数字序列;
符号常量通常用大写字母拼写,这样可以容易与用小写字母拼写的变量名区别;
符号常量不是变量,不需要出现在声明中。
替换文本可以是任何字符序列,而不仅限于数字;
注意:#define指令行的末尾没有分号;
#include <stdio.h>
#define LOWER 0 // 表的下限
#define UPPER 300 // 表的上限
#define STEP 20 // 步长
// 打印华氏温度-摄氏温度对照表
void main()
{
int fahr;
for (fahr = LOWER; fahr <= UPPER; fahr++) {
printf("%3d %6.1f\n", fahr, (5.0 / 9.0) * (fahr - 32));
}
}
1.5 字符输入和输出
标准库提供的输入/输出模型非常简单。输入和输出都是按照字符流的方式处理。
文本流是由多行字符构成的字符序列,而每行字符则由0个或多个字符组成,行末是一个换行符。
标准库负责使每个输入/输出流都能够遵守这一模型。
标准库提供的一次读/写一个字符的函数,其中最简单的是getchar和putchar两个函数;
getchar函数从文本流中读入下一个输入字符,并将其作为结果值返回;getchar通常是通过键盘输入的;
putchar函数将变量以字符的形式打印一个字符;
1.5.1 文件复制
使用getchar与putchar函数把输入一次一个字符复制到输出;
思路为:
读一个字符;
while(该字符不是文件结束指示符)
输出刚读入的字符
读下一个字符
代码实现:
字符在键盘、屏幕或其他的任何地方无论以什么形式表现,它在机器内部都是以位模式存储的。
char类型专门用于存储这种字符型数据,当然任何整型(int)也可以用于存储字符型数据;
#include <stdio.h>
void main()
{
int c;
c = getchar();
while (c != EOF)
{
putchar(c);
c = getchar();
}
}
如何区分文件中有效数据与输入结束符问题:
C语言采取的解决办法是:在没有输入时,getchar函数将返回一个特殊值,这个特殊值与任何实际字符都不同。这个值成为EOF(end of file,文件结束)注意:在声明变量c的时候,必须让它大到足以存放getchar函数返回的任何值。这里之所以不把c声明成char类型,是因为它必须足够大,除了能够存储任何可能的字符外还要能存储文件结束符EOF,所以声明为了int类型。
EOF定义在头文件<stdio.h>中,是一个整型数,它与任何char类型的值都不相同。
上述程序,可以简化为:
#include <stdio.h>
void main()
{
int c;
while ((c = getchar()) != EOF)
putchar(c);
}
这样getchar函数在程序中只出现一次,这样就缩短了程序,使得整个程序看起来更紧凑;
while语句的条件部分,赋值表达式两侧的圆括号不能省略。
**!=
(不等于运算符)的优先级比=
(赋值运算符)的优先级高;**这样,在不使用圆括号的情况下关系测试!=
将在赋值=
操作之前执行。
c = getchar() != EOF
// 等价于
c = (getchar() != EOF)
// 语句执行后,c要么是0,要么是1;
1.5.2 字符计数
功能:对字符进行计数,用于统计输入单个字符的个数;
#include <stdio.h>
void main()
{
long nc;
nc = 0;
while (getchar() != EOF)
++nc;
printf("%ld\n", nc);
}
注意:使用long类型,是防止++
造成nc的溢出;
#include <stdio.h>
void main()
{
double nc;
for (nc = 0; getchar() != EOF; ++nc)
;
printf("%.0f\n", nc);
}
在该程序段中,for循环语句的循环体是空的,这是因为所有工作在测试(条件)部分与增加步长部分完成了。但C语言的语法规则要求for循环语句必须由一个循环体,因此用单独的分号代替。单独的分号称为空语句。
while语句与for语句的优点之一就是在执行循环体之前就对条件进行测试。如果条件不满足,则不执行循环体,这就可能出现循环体一次都不执行的情况。
1.5.3 行计数
标准库保证输入文本流以行序列的形式出现,每一行均以换行符结束;
功能:统计行数等价于统计换行符的个数;
#include <stdio.h>
void main()
{
int c, n1;
n1 = 0;
while ((c = getchar()) != EOF){
if (c == '\n')
++n1;
}
printf("%d\n", n1);
}
单引号中的字符表示一个整型值,该值等于此字符在机器字符集中对应的数值,我们称之为字符常量。
用’A’要比65好,因为’A’的意义更清楚,且与特定的字符集无关;
字符串常量中使用的转义字符序列也是合法的字符常量。
1.5.4 单词统计
功能:UNIX系统中wc程序的骨干部分:
#include <stdio.h>
#define IN 1 // 在单词内部
#define OUT 0 // 在单词外部
void main()
{
int c, n1, nw, nc, state;
state = OUT;
n1 = nw = nc = 0;
while ((c = getchar()) != EOF) {
++nc;
if (c == '\n')
++n1;
if (c == ' ' || c == '\n' || c == '\t')
state = OUT;
else if (state == OUT) {
state = IN;
++nw;
}
}
printf("%d %d %d\n", n1, nw, nc);
}
虽然使用符号变量来对应幻数,会提高工作量,但是,增加的一点工作量与提高程序可读性带来的好处相比是值得的;如果程序中的幻数都是以符号常量的形式出现,对程序进行大量修改就会相对容易得多;
在兼有值与赋值两种功能的表达式中,赋值结合次序是由右至左。
由&&
或||
连接的表达式由左至右求值,并保证求值过程中只要能够判断最终的结果为真或假,求值就立即终止;
数组
功能:统计各个数字、空白符(包括空格符、制表符以及换行符)以及所有其他字符出现的次数。
思路:所有的输入字符可以分成12类,因此可以用一个数组存放各个数字出现的次数;使用数组比使用10个独立的变量更方便;
#include <stdio.h>
void 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);
}
数组下标可以是任何整型表达式,包括整型变量(如i)以及整型常量。
注意:char类型的字符是小整型,因此char类型的变量和常量在算术表达式中等价于int类型的变量和常量。
1.7 函数
函数为计算的封装体用了一种简便的方法,此后使用函数时不需要考虑它是如何实现的。
使用设计正确的函数,程序员无需考虑功能是如何实现的,而只需要它具有哪些功能就够了。
例子:
#include <stdio.h>
int power(int m, int n);
void main()
{
int i;
for (i = 0; i < 10; ++i) {
printf("%d %d %d\n", i, power(2, i), power(-3, i));
}
}
int power(int base, int n)
{
int i, p;
p = 1;
for (i = 1; i <= n; ++i) {
p = p * base;
}
return p;
}
函数定义的一般形式:
返回值类型 函数名(0个或个参数声明)
{
声明部分
语句序列
}
函数定义可以以任意次序出现在一个源文件或多个源文件,但同一函数不能分割存放在多个文件中。
函数声明声明了参数的类型、名字以及该函数返回结果的类型。
函数参数使用的名字只在函数内部有效,对其他任何函数都是不可见的:其他函数可以使用与之相同的参数名字而不会引起冲突;
通常把函数定义中圆括号内列表中出现的变量成为形式参数(形参);把函数调用中与形式参数对应的值成为实际参数(实参);
函数不一定都有返回值。
不带表达式的return语句将把控制权返回给调用者,但不返回有用的值。
在main函数中,一般来说,返回值为0表示正常终止,返回值为非0表示出现异常情况或出错结束条件。
不带函数体的函数声明,是函数原型;函数原型与函数声明中参数不要求相同。事实上,函数原型中的参数名是可选的,上面的函数原型也可以写成一下形式:
int power(int,int);
合适的参数名能够起到很好的说明作用,因此我们在函数原型中总是指明参数名;
1.8 参数——传值调用
在C语言中,所有函数参数都是“通过值”传递的。意味着,传递给被调用函数的参数值存放在临时变量中,而不是存放在原理的变量中。
在C语言中,被调用函数不能直接修改主调函数中变量的值,而只能修改其私有的临时变量的副本的值;
传值调用的利大于弊。在被调用函数中,参数可以看做是便于初始化的局部变量,因此额外使用的变量更少,这样程序可以更紧凑简洁。
如果需要函数修改主调函数中的变量时,调用者需要向被调用函数提供设置值的变量的地址,而被调用函数则需要将对应的参数声明为指针类型,并通过它间接访问变量;
注意:如果传入参数是数组时,传递给函数的值是数组起始元素的位置或地址——它并不复制数组元素本身。
1.9 字符数组
字符数组是C语言中最常用的数组类型。
功能:程序读入一组文本,并把最长的文本行打印出来;
while(还有未处理的行)
if(该行比已处理的最长行还要长)
保存该行
保存该行的长度
打印最长的行
上面的程序已经是一个小程序的雏形了,从读入数据,判断,保存的流程分别对应了不同的片段;
#include <stdio.h>
#define MAXLINE 1000 // 允许的输入行的最大长度
int getline(char line[], int maxline);
void copy(char to[], char from[]);
int 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;
}
// getline函数:将一行读入到s中并返回其长度;
int getline(char s[], int lim)
{
int c, i;
for (i = 0; i < lim && (c = getchar()) != EOF && c != '\n'; ++i)
s[i] = c;
if (c == '\n') {
s[i] = c;
++i;
}
s[i] = '\0';
return i;
}
// copy函数:将from复制到to;这里假定to足够大;
void copy(char to[], char from[])
{
int i;
i = 0;
while ((to[i] = from[i]) != '\0')
++i;
}
程序中的细节:
1.使用getline方法读入一行字符串;
2.使用copy用于字符串之间的拷贝,我们还没有学着使用memcpy,strcpy;
3.字符串的结尾是以\0
,用以标记字符串的结束;
4.字符串常量,是以字符数组的形式存储,数组的各元素分别存储在字符串的各个字符,并以\0
标志字符串的结束;
1.10 外部变量与作用域
main函数中的变量是main函数的私有变量或局部变量,其他函数不能直接访问它们,其他函数声明的变量也同样如此;
函数中的每个局部比那里只在函数调用时存在,在函数执行完毕后退出时消失,有些语言也将这种变量成为是自动变量;
注意:如果自动变量没有赋值,则其中存放的是无效值;
除了自动变量外,还可以定义位于所有函数外部的变量,在所有函数中都可以通过变量名访问这种类型的变量;
由于外部变量可以在全局范围内访问,因此,函数间可以通过外部变量交换数据,而不必使用参数表。再者,由于外部变量会在程序执行期间一直存在,而不是在函数调用时产生、在函数执行完毕时消失。即使在对外部变量赋值的函数返回后,这些变量仍将保持原来的值不变;
外部变量必须定义在所有函数之外,且只能定义一次,定义后编译程序将为它分配存储单元。在每个需要访问外部变量的函数中,必须声明相应的外部变量,此时说明其类型。声明时可以用extern语句显示声明,也可以通过上下文隐式声明。
#include <stdio.h>
#define MAXLINE 1000 // 允许的输入行的最大长度
int max; // 当前已发现的最长行的长度
char line[MAXLINE]; // 当前的输入行
char longest[MAXLINE]; // 用于保存最长的行
int getline(void);
void copy(void);
int main()
{
int len; // 当前行长度
extern int max;
extern char longest[];
max = 0;
while ((len = getline()) > 0)
if (len > max) {
max = len;
copy();
}
if (max > 0) // 如果存在最大行;
printf("%s", longest);
return 0;
}
// getline函数:将一行读入到s中并返回其长度;
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函数:将from复制到to;这里假定to足够大;
void copy(void)
{
int i;
extern char line[], longest[];
i = 0;
while ((longest[i] = line[i]) != '\0')
++i;
}
注意:在ANSI C中,如果要声明空参数表,则必须使用关键字void进行显示声明;
函数在使用外部变量之前,必须要知道外部变量的名字,使用extern
类型的声明;
某些情况下可以省略extern声明,比如在源文件中,如果外部变量的定义在使用它的函数之前,那么在那个函数之前就没有必要使用extern声明。通常的做法是:所有外部变量的定义都放在源文件的开始出,这样就可以省略extern声明;
如果要在多个源文件中使用外部变量,通常是吧变量和函数的extern声明放在一个单独的文件中(头文件),并在每个源文件的开始位置使用#include
语句把所要用到的头文件包含进来。
但是,过分依赖外部变量会导致一定风险,因为它会使程序中的数据关系变得模糊不清——外部变量的值可能会被意外地或不经意地修改,而程序的修改又变得十分困难。
将函数使用的外部参数都以外部变量的方式在函数内部使用,就使得函数失去了通用性。
至此,导言部分就结束了,使用变量,数组,循环,输入和输出就可以写出一些简单功能。