前言
在我的
评论中,我多次提及Andrew Tanenbaum的书《操作系统设计和实现》(
第一版)以及C在其中的表示方式。 这些评论一直很有趣。 我认为是时候将该简介翻译成C了。 它仍然是相关的。 尽管当然有些人尚未听说过编程语言
PL / 1 ,甚至尚未听说过操作系统
Minix 。
从历史的角度以及对于理解C语言自诞生以来到整个IT行业已经走了多远的角度,此描述也很有趣。
我想立即预订我的第二种语言是法语:

但这被46年的
编程经验所抵消。
所以,让我们开始吧,轮到安德鲁·塔南鲍姆(Andrew Tanenbaum)了。
C语言简介(pp.350-362)
C编程语言是AT&T贝尔实验室的Dennis Ritchie创建的,它是用于开发UNIX操作系统的高级编程语言。 当前,该语言被广泛用于各个领域。 C在系统程序员中特别流行,因为它使您可以简单,简洁地编写程序。
描述C语言的主要书籍是Brian Kernigan和Dennis Ritchie的书The C Programming Language(1978)。 关于C语言的书籍由Bolon(1986),Gehani(1984),Hancock和Krieger(1986),Harbison和Steele(1984)等人撰写。
在本附录中,我们将尝试对C进行相当完整的介绍,以便熟悉Pascal,PL / 1或Modula 2等高级语言的人能够理解本书中给出的大多数MINIX代码。 此处不讨论MINIX中未使用的C功能。 省略了许多细微之处。 重点是阅读C程序,而不是编写代码。
A.1。 C语言基础
C程序由一组过程组成(通常称为函数,即使它们不返回值)。 这些过程包含声明,运算符和其他元素,它们一起告诉计算机要做什么。 图A-1显示了一个小过程,其中声明了三个整数变量并为其分配了值。 该过程的名称为main。 该过程没有形式参数,如过程名称后面方括号之间没有任何标识符所示。 该过程的主体括在大括号({})中。 此示例说明C具有变量,并且必须在使用前声明这些变量。 C也有运算符,在此示例中,它们是赋值运算符。 所有语句必须以分号结尾(不同于Pascal,Pascal在语句之间使用冒号,而不是在后面)。
注释以字符“ / *”开头,以字符“ * /”结尾,并且可以跨越多行。
main () { int i, j, k; i = 10; j = i + 015; k = j * j + 0xFF; } . Al. .
该过程包含三个常量。 第一次分配中的常数为10
它是一个普通的十进制常数。 015常数是八进制常数
(等于十进制的13)。 八进制常数始终从零开始。 常数0xFF是十六进制常数(等于255十进制)。 十六进制常数始终以0x开头。 这三种类型都在C中使用。
A.2。 基本数据类型
C有两种主要的数据类型(变量):整数和字符,分别声明为int和char。 没有单独的布尔变量。 int变量用作布尔变量。 如果此变量包含0,则表示false / false,其他任何值表示true / true。 C也有浮点类型,但是MINIX不使用它们。
您可以将短,长或无符号的“形容词”应用于定义值范围(取决于编译器)的int类型。 大多数8088处理器将16位整数用于int和short int,将32位整数用于long int。 8088处理器上的无符号整数(unsigned int)的范围是0到65535,而不是-32768到+32767,与普通整数(int)一样。 一个字符占8位。
寄存器说明符还可以用于int和char,并且向编译器提示已声明的变量应放在寄存器中,以使程序更快地工作。
一些广告如图所示。 A-2。
int i; short int z1, z2; / * */ char c; unsigned short int k; long flag_poll; register int r; . -2. .
类型之间的转换是允许的。 例如,运算符
flag_pole = i;
即使我的类型为int并且flag_pole很长也可以。 在很多情况下
强制在数据类型之间进行转换是必要的或有用的。 对于强制转换,只需将目标类型放在表达式前面的方括号中即可进行转换。 例如:
( (long) i);
指令将整数i转换为long,然后再将其作为参数传递给过程p,该过程需要long参数。
在类型之间进行转换时,请注意符号。
将字符转换为整数时,某些编译器将字符视为带符号,即从-128到+127,而其他编译器将它们视为
无符号,即从0到255。在MINIX中,诸如
i = c & 0377;
从(字符)转换为整数,然后执行逻辑与
(&)八进制常数0377。结果是高8位
设置为零,实际上将c视为8位无符号数,范围为0到255。
A.3。 复合类型和指针
在本节中,我们将研究构建更复杂的数据类型的四种方法:数组,结构,联合和指针。 数组是相同类型的元素的集合/集合。 C中的所有数组均以元素0开头。
公告公告
int a [10];
声明一个具有10个整数的数组a,该数组将存储在从[0]到[9]的数组元素中。 其次,数组可以是3维或3维以上,但在MINIX中不使用。
结构是变量的集合,通常是各种类型。 C中的结构类似于Pascal中的记录。 操作员
struct {int i; char c;} s;
将s声明为包含两个成员(整数i和字符c)的结构。
要将结构s的成员i分配给6,请编写以下表达式:
si = 6;
点运算符表示元素i属于结构s。
工会也是一组成员,类似于结构,但在任何时候只有一个成员可以加入工会。 公告公告
union {int i; char c;} u;
表示您可以使用整数或字符,但不能同时使用。 编译器应分配足够的合并空间,以便可以容纳最大的合并元素(从占用内存的角度来看)。 联合仅在MINIX中的两个位置使用(将消息定义为几种不同结构的联合,并将磁盘块定义为数据块,索引节点块,目录块等的联合)。
指针用于将机器地址存储在C中。 它们被非常非常频繁地使用。 星号(*)用于指示广告中的指针。 公告公告
int i, *pi, a [10], *b[10], **ppi;
声明一个整数i,一个指向整数pi的指针,一个包含10个元素的数组a,一个包含10个指向整数的指针的数组b,以及一个指向整数的指针ppi的指针。
结合数组,指针和其他类型的复杂声明的确切语法规则有些复杂。 幸运的是,MINIX仅使用简单的声明。
图A-3显示了结构表结构数组z的声明,每个结构表都具有
三个成员,整数i,指向字符c和字符c的指针cp。
struct table { int i; / * */ char *cp, c; } z [20]; . - 3. .
结构数组在MINIX中很常见。 此外,可以将名称表声明为可在后续声明中使用的结构表结构。 举个例子
register struct table *p;
声明p指向结构表结构的指针并建议保存它
在寄存器中。 在程序执行期间,p可以指示例如z [4]或
到z中的任何其他元素,其中所有20个元素都是struct table类型的结构。
要使p指向z [4],只需编写
p = &z[4];
“&”号作为一元(一元)运算符的意思是“采用其后的地址”。 将成员i的值复制到整数变量n
p指向的结构可以如下进行:
n = p->i;
请注意,箭头用于通过指针访问结构的成员。 如果使用变量z,则必须使用点运算符:
n = z [4] .i;
区别在于z [4]是结构,并且点运算符选择元素
直接来自复合类型(结构,数组)。 使用指针,我们不会直接选择参与者。 指针指示您首先选择一个结构,然后才选择该结构的成员。
有时为复合类型命名很方便。 例如:
typedef unsigned short int unshort;
将unshort定义为unsigned short(无符号的短整数)。 现在unshort可以在程序中用作主要类型。 举个例子
unshort ul, *u2, u3[5];
声明一个短无符号整数,一个指向短无符号整数的指针,以及
短无符号整数的数组。
A.4。 经营者
C中的过程包含声明和声明。 我们已经看到了声明,因此现在我们将考虑运算符。 条件和循环运算符的目的与其他语言基本相同。 图A-4显示了其中的几个示例。 唯一需要注意的是花括号用于分组运算符,而while语句具有两种形式,第二种类似于Pascal的repeat语句。
C也有一个for语句,但是它看起来不像任何其他语言的for语句。 for语句具有以下形式:
for (<>; <>; <>) ;
通过while语句可以表达相同的意思:
<> while(<>) { <>; <> }
例如,考虑以下语句:
for (i=0; i <n; i = i+l) a[i]=0;
此运算符将数组a的前n个元素设置为零。 运算符的执行从将i设置为零开始(在循环外部完成)。 然后,在执行i的赋值和增加的同时,重复操作符直到i <n。 当然,代替在零数组的当前元素上赋值的运算符,可能会在大括号中包含一个复合运算符(块)。
if (x < 0) k = 3; if (x > y) { i = 2; k = j + l, } if (x + 2 <y) { j = 2; k = j - 1; } else { m = 0; } while (n > 0) { k = k + k; n = n - l; } do { / * while */ k = k + k; n = n - 1; } while (n > 0); . A-4. if while C.
C还具有类似于Pascal中的case运算符的运算符。 这是一个switch语句。 图A-5中显示了一个示例。 根据switch中指定的表达式的值,选择一个或另一个case语句。
如果表达式与任何case语句都不匹配,则选择默认语句。
如果该表达式不与任何case语句关联,并且缺少默认语句,则从switch语句之后的下一个语句继续执行。
应当注意,要退出case块,请使用break语句。 如果没有break语句,将执行下一个case块。
switch (k) { case 10: i = 6; break; case 20: i = 2; k = 4; break; / * default* / default: j = 5; } . A-5. switch
break语句还在for和while循环内起作用。 应当记住,如果break语句位于一系列嵌套循环内,则输出仅向上一级。
一个相关的语句是continue语句,它不会退出循环,
但是导致当前迭代的完成和下一个迭代的开始
立即。 从本质上讲,这是循环顶部的返回。
C具有可以带或不带参数调用的过程。
根据Kernigan和Ritchie(p。121)的说法,不允许转移数组,
结构或过程作为参数,尽管将指针传递给所有这些
允许的。 是否有一本书(它将在我的记忆中弹出:-“如果火星上有生命,如果火星上没有生命”),许多C编译器都允许使用结构作为参数。
如果数组的名称不带索引,则表示数组的指针,从而简化了数组指针的传输。 因此,如果a是任何类型的数组的名称,则可以通过编写将其传递给g
g();
此规则仅适用于数组;此规则不适用于结构。
过程可以通过执行return语句来返回值。 该语句可能包含一个表达式,其结果将作为过程的值返回,但是调用者可以放心地忽略该返回值。 如果过程返回一个值,则将类型值写入过程名称之前,如图2所示。 A-6。 像参数一样,过程不能返回数组,结构或过程,但是可以返回指向它们的指针。 设计该规则是为了实现更有效的实现-所有参数和结果始终对应一个机器字(地址存储在其中)。 允许将结构用作参数的编译器通常还允许将其用作返回值。
int sum (i, j) int i, j ; { return (i + j); } . -6. , .
C没有内置的I / O。 输入/输出通过调用库函数来实现,最常见的函数如下所示:
printf («x=% dy = %oz = %x \n», x, y, z);
第一个参数是引号之间的字符串(实际上,这是一个字符数组)。
任何不是百分比的字符都将按原样打印。
出现百分比时,以下参数将以该百分比后面的字母所定义的形式打印:
d-打印为十进制整数
o-打印为八进制整数
u-打印为无符号十进制整数
x-打印为十六进制整数
s-打印为字符串
c-打印为一个字符
字母D,0和X也允许用于长数字的十进制,八进制和十六进制打印。
A.5。 表达方式
通过组合操作数和运算符来创建表达式。
算术运算符(例如+和-)和关系运算符(例如<
并且>类似于其他语言中的对应词。 %运算符
用模。 值得注意的是,等于运算符为==,而不等于运算符为! =。 要检查a和b是否相等,可以这样编写:
if (a == b) <>;
C还允许您将赋值运算符与其他运算符结合使用,因此
a += 4;
相当于录音
= + 4;
其他运算符也可以以此方式组合。
C具有用于操作单词位的运算符。 允许移位和按位逻辑运算。 左右移位运算符是<<
和>>。 按位逻辑运算符&,| 和^分别为逻辑AND(AND),包括OR(OR)和异或(XOP)。 如果i的值为035(八进制),则表达式i和06的值为04(八进制)。 另一个例子,如果i = 7,则
j = (i << 3) | 014;
得到j的074
另一重要的运算符组是一元运算符,每个运算符仅接受一个操作数。 作为一元运算符,“&”号获取变量的地址。
如果p是一个指向整数的指针,而i是一个整数,则该运算符
p = &i;
计算地址i并将其存储在变量p中。
与获取地址相反的是,运算符采用指针作为输入并计算该地址处的值。 如果我们只是将地址i分配给指针p,则* p的含义与i相同。
换句话说,作为一元运算符,星号后跟指针(或
给出指针的表达式)并返回其指向的元素的值。 如果我的值为6,则运算符
j = *;
将为j分配数字6。
操作员! (感叹号是取反运算符)如果其操作数非零,则返回0;如果其运算符为0,则返回1。
它主要用于if语句,例如
if (!x) k=0;
检查x的值。 如果x为零(假),则为k分配值0。实际上,算子! 取消其后的条件,就像Pascal中的not运算符一样。
〜运算符是按位补码运算符。 其操作数中的每个0
变为1,每1变为0。
sizeof运算符以字节为单位报告其操作数的大小。 关于
在具有2个字节整数的计算机上,由20个整数a组成的数组,例如sizeof a将具有40的值。
最后一组运算符是增减运算符。
操作员
++;
表示p的增加。 p将增加多少取决于其类型。
整数或字符加1,但指针增加
以这种方式指向的对象的大小,如果a是结构的数组,而p是指向这些结构之一的指针,则我们写
p = &a[3];
使p指向数组中的结构之一,然后增加p
无论结构有多大,都将指向[4]。 操作员
p--;
类似于p ++运算符,除了它减小而不是增大操作数的值。
在声明中
n = k++;
如果两个变量都是整数,则将k的原始值分配给n,
只有这样,k才会增加。 在声明中
n = ++ k;
k首先增加,然后将其新值存储在n中。
因此,可以在其操作数之前或之后写入++(或-)运算符,从而产生各种值。
最后的陈述是这个吗? (问号),它选择了两种选择之一
用冒号隔开。 例如操作员
i = (x < y ? 6 : k + 1);
将x与y比较。 如果x小于y,那么我得到的值是6; 否则,变量i的值为k +1。方括号是可选的。
A.6。 程序结构
C程序由一个或多个包含过程和声明的文件组成。
这些文件可以单独编译为目标文件,然后将它们彼此链接(使用链接器)以形成可执行程序。
与Pascal不同,过程声明不能嵌套,因此所有声明都写在程序文件的“顶层”。
允许在过程外部声明变量,例如,在第一次声明过程之前在文件的开头声明变量。 这些变量是全局变量,并且可以在整个程序的任何过程中使用,除非static关键字在声明之前。 在这种情况下,这些变量不能在另一个文件中使用。 相同的规则适用于过程。 在过程内部声明的变量是该过程的局部变量。
该过程可以访问在另一个文件中声明的整数变量v(前提是该变量不是静态的),并在外部声明它:
extern int v;
每个全局变量必须在没有extern属性的情况下准确地声明一次,以便为其分配内存。
声明时可以初始化变量:
int size = 100;
数组和结构也可以初始化。 未显式初始化的全局变量的默认值为零。
A.7。 C预处理器
在将源文件传输到C编译器之前,将对其进行自动处理
称为预处理程序的程序。 它是预处理器的输出,而不是
原始程序被馈送到编译器的输入。 预处理器执行
在将文件传递给编译器之前,对文件进行三个基本转换:
1.包含文件。
2.定义和替换宏。
3.有条件的编译。
所有预处理指令均在第一列中以数字符号(#)开头。
当一个视图指令
#include "prog.h"
由预处理器满足,它在一行中逐行包含prog.h文件
要传递给编译器的程序。 当#include指令写为
#include <prog.h>
然后在/ usr / include目录而不是工作目录中搜索包含的文件。 在C中,通常的做法是将多个文件使用的声明分组在头文件中(通常带有后缀.h),并在必要时包括它们。
预处理器还允许宏定义。 举个例子
#define BLOCK_SIZE 1024
定义BLOCK_SIZE宏并将其分配为1024。从现在开始,文件中每次出现的10个字符的字符串“ BLOCK_SIZE”都将由4个字符的字符串“ 1024”代替,然后编译器才能使用程序查看文件。按照惯例,宏名称以大写形式编写。宏可以具有参数,但实际上很少。预处理器的第三个功能是条件编译。MINIX在几个地方专门为8088处理器编写代码,并且在为另一个处理器编译时不应包含此代码。这些部分如下所示: #ifdef i8088 < 8088> #endif
如果定义了i8088字符,则预处理器输出中将包含两个预处理器指令#ifdef i8088和#endif之间的语句;否则将被跳过。使用命令调用编译器 cc -c -Di8088 prog.c
或在程序中包含一条语句 #define i8088
我们定义了符号i8088,因此包含了8088的所有相关代码。随着MINIX的发展,它可能会获得适用于68000和其他处理器的特殊代码。作为预处理器工作方式的一个示例,请考虑程序图。A-7(a)。它包含一个prog.h文件,其内容如下: int x; #define MAXAELEMENTS 100
想象一下,编译器是由命令调用的 cc -E -Di8088 prog.c
文件通过预处理器后,输出将如图10所示。A-7(b)。作为C编译器的输入而给出的是此输出而不是源文件。
请注意,预处理器完成了其工作,并删除了以#号开头的所有行。如果编译器会像这样被调用 cc -c -Dm68000 prog.c
然后将包含另一张印刷品。如果这样调用: cc -c prog.c
那么将不包含任何打印内容。(读者可能会想一想如果同时使用-D fl ags标志调用编译器会发生什么情况。)A.8。成语
在本节中,我们将研究几种典型的C结构,但在其他编程语言中并不常见。首先,考虑循环: while (n--) *p++ = *q++;
变量p和q通常是字符指针,而n是计数器。循环将n字符串从q指向的地方复制到p指向的字符串。在循环的每次迭代中,计数器都会减少直到达到0,并且每个指针都会增加,因此它们依次指向具有更高编号的存储单元。另一个常见的设计: for (i = 0; i < N; i++) a[i] = 0;
将a的前N个元素设置为0。编写此循环的另一种方法如下: for (p = &a[0]; p < &a[N]; p++) *p = 0;
在此语句中,整数指针p初始化为指向数组的零元素。循环继续直到p到达数组第N个元素的地址。指针构造比数组构造更有效,因此通常使用它。分配运算符可能会出现在意外的地方。举个例子
if (a = f (x)) < >;
首先调用函数f,然后分配调用函数a的结果,最后检查它是true(非零)还是false(零)。如果a不等于零,则满足条件。操作员
if (a = b) < >;
同样,首先,检查变量a的变量b的值,然后检查a值是否为非零。而且这个运算符与 if (a == b) < >;
比较两个变量,如果相等则执行运算符。后记
仅此而已。您不会相信我对编写本文有多喜欢。我记得从同一C语言中有用的东西有多少。希望您也喜欢进入C语言的美好世界。