apt
- apt-get update
- git (config)
- vim
- tree
- mlocate (updatedb)
1 | sudo apt-get update |
bash
- bashmy
- bashrc
1 | git clone https://github.com/forestdbin/code-snippet.git |
vim
- update-alternatives
- vimrc
1 | sudo update-alternatives --config editor |
sudo
1 | # visudo |
1 | sudo apt-get update |
1 | git clone https://github.com/forestdbin/code-snippet.git |
1 | sudo update-alternatives --config editor |
1 | # visudo |
道法术器 is a concept rooted in Chinese philosophy, particularly in Daoism and strategic thinking. It represents four key elements:
The fundamental principle or way, often referring to the natural order or ultimate truth.
The rules, methods, or laws that guide actions and governance.
The techniques or strategies used to achieve goals.
The tools or tangible resources that support execution.
This framework is often used in leadership, governance, and personal development, emphasizing the importance of aligning one’s actions with universal principles while utilizing effective methods, strategies, and tools.
Would you like to explore how this applies to a specific area, like bussiness or personal growth? :)
1 | docker pull <your-registry-mirror>[:<port>]/library/busybox |
1 | $ docker pull registry |
1 | # /etc/docker/registry/config.yml |
1 | # /etc/docker/daemon.json |
UNIX操作系统通过一系列的系统调用提供服务,这些系统调用实际上是操作系统内的函数,它们可以被用户程序调用。…因为,我们经常需要借助于系统调用以获得最高的效率,或者访问标准库中没有的某些功能。但是,即使读者是在其他操作系统上使用C语言,本章的例子也将会帮助你对C语言程序设计有更深入 的了解。不同系统中的代码具有相似性,只是一些细节上有区别而已。因为ANSI C标准库函数是以UNIX系统为基础建立起来的,所以,学习本章中的程序还将有助于更好地理解标准库。
…输入/输出、文件系统和存储分配。其中,前两部分的内容要求读者对UNIX系统的外部特性有一定的了解。
(标准)输入/输出接口对任何操作系统都是一样的。在任何特定的系统中,标准库函数的实现必须通过宿主系统提供的功能来实现。
在UNIX操作系统中,所有的外围设备(包括键盘和显示器)都被看作是文件系统中的文件,因此,所有的输入/输出都要通过读文件或写文件完成。也就是说,通过一个单一的接口就可以处理外围设备和程序之间的所有通信。
通常情况下,在读或写文件之前,必须先将这个意图通知系统,该过程称为打开文件。如果是写一个文件,则可能需要先创建该文件,也可能需要丢弃该文件中原先已存在的内容。系统检查你的权力(该文件是否存在?是否有访问它的权限?),如果一切正常,操作系统将向程序返回一个小的非负整数,该整数称为文件描述符。任何时候对文件的输入/输出都是通过文件描述符标识文件,而不是通过文件名标识文件。(文件描述符类似于标准库中的文件指针或MS-DOS中的文件句柄。)系统负责维护已打开文件的所有信息,用户程序只能通过文件描述符引用文件。
因为大多数的输入/输出是通过键盘和显示器来实现的,为了方便起见,UNIX对此做了特别的安排。当命令解释程序(即“shell”)运行一个程序的时候,它将打开3个文件,对应的文件描述符分别为0、1、2,依次标识标准输入、标准输出和标准错误。如果程序从文件0中读,对1和2进行写,就可以进行输入/输出而不必关心打开文件的问题。
程序的使用者可通过<
和>
重定向程序的I/O:
1 | prog <输入文件名 >输出文件名 |
这种情况下,shell把文件描述符0和1的默认赋值改变为指定的文件。通常,文件描述符2仍与显示器关联,这样,出错信息会输出到显示器上。与管道相关的输入/输出也有类似的特性。在任何情况下,文件赋值的改变都不是由程序完成的,而是由shell完成的。只要程序使用文件0作为输入,文件1和2作为输出,它就不会知道程序的输入从哪里来,并输出到哪里去。
read
和write
输入与输出是通过read
和write
系统调用实现的。在C语言程序中,可以通过函数read
和write
访问这两个系统调用。这两个函数中,第一个参数是文件描述符,第二个参数是程序中存放读或写的数据的字符数组,第三个参数是要传输的字节数。
1 | int n_read = read(int fd, char *buf, int n); |
每个调用返回实际传输的字节数,在读文件时,函数的返回值可能会小于请求的字节数。如果返回值为0,则表示已到达文件的结尾;如果返回值为-1,则表示发生了某种错误。在写文件时,返回值是实际写入的字节数。如果返回值与请求写入的字节数不相等,则说明发生了错误。
在一次调用中,读出或写入的数据的字节数可以为任意大小。最常用的值为1,即每次读出或写入1个字符(无缓冲),或是类似于1024或4096这样的与外围设备的物理块大小相应的值。用更大的值调用该函数可以获得更高的效率,因为系统调用的次数减少了。
…程序可以将任意输入复制到任意输出,因为输入/输出可以重定向到任何文件或设备。
open
、creat
、close
和unlink
除了默认的标准输入、标准输出和标准错误文件外,其他文件都必须在读或写之前显式地打开。系统调用open
和creat
用于实现该功能。
open
与fopen
很相似,不同的是,前者返回一个文件描述符,它仅仅只是一个int
类型的数值,而后者返回一个文件指针。如果发生错误,open
将返回-1。
1 | #include <fcntl.h> |
与fopen
一样,参数name
是一个包含文件名的字符串。第二个参数flags
是一个int
类型的值,它说明以何种方式打开文件,主要的几个值如下所示:
1 | O_RDONLY 以只读方式打开文件 |
在System V UNIX系统中,这些常量在头文件<fcntl.h>
中定义,而在Berkeley(BSD)版本中则在<sys/file.h>
中定义。
如果用open
打开一个不存在的文件,则将导致错误。可以使用creat
系统调用创建新文件或覆盖已有的旧文件,如下所示:
1 | int creat(char *name, int perms); |
如果creat
成功地创建了文件,它将返回一个文件描述符,否则返回-1。如果此文件已存在,creat
将把该文件的长度截断为0,从而丢弃原先已有的内容。使用creat
创建一个已存在的文件不会导致错误。
如果要创建的文件不存在,则creat
用参数perms
指定的权限创建文件。
一个程序同时打开的文件数是有限制的(通常为20)。相应地,如果一个程序需要同时处理许多文件,那么它必须重用文件描述符。函数close(int fd)
用来断开文件描述符和已打开文件之间的连接,并释放此文件描述符,以供其他文件使用。close
函数与标准库中的fclose
函数相对应,但它不需要清洗(flush)缓冲区。如果程序通过exit
函数退出或从主程序中返回,所有打开的文件将被关闭。
函数unlink(char *name)
将文件name
从文件系统中删除,它对应于标准库函数remove
。
lseek
输入/输出通常是顺序进行的:每次调用read
和write
进行读写的位置紧跟在前一次操作的位置之后。但是,有时候需要以任意顺序访问文件,系统调用lseek
可以在文件中任意移动位置而不实际读写任何数据:
1 | long lseek(int fd, long offset, int origin); |
将文件描述符为fd
的文件的当前位置设置为offset
,其中,offset
是相对于origin
指定的位置而言的。随后进行的读写操作将从此位置开始。origin
的值可以为0、1或2,分别用于指定offset
从文件开始、从当前位置或从文件结束处开始算起。例如,为了向一个文件的尾部添加内容(在UNIX shell程序中使用重定向符>>
或在系统调用fopen
中使用参数“a”),则在写操作之前必须使用下列系统调用找到文件的末尾:
1 | lseek(fd, 0L, 2); |
若要返回文件的开始处(即反绕),则可以使用下列调用:
1 | lseek(fd, 0L, 0); |
请注意,参数0L也可以写为(long) 0,或仅仅写为0,但是系统调用lseek的声明必须保持一致。
使用lseek
系统调用时,可以将文件视为一个大数组,其代价是访问速度会慢一些。
lseek
系统调用返回一个long
类型的值,此值表示文件的新位置,若发生错误,则返回-1。标准库函数fseek
与系统调用lseek
类似,所不同的是,前者的第一个参数是FILE *
类型,且在发生错误时返回一个非0值。
fopen
和getc
函数的实现我们常常还需要对文件系统执行另一种操作,以获得文件的有关信息,而不是读取文件的具体内容。目录列表程序便是其中的一个例子,比如UNIX命令ls
,它打印一个目录中的文件名以及其他一些可选信息,如文件长度,访问权限等等。MS-DOS操作系统中的dir
命令也有类似的功能。
由于UNIX中的目录就是一种文件,因此,ls
只需要读此文件就可获得所有的文件名。但是,如果需要获取文件的其他信息,比如长度等,就需要使用系统调用。在其他一些系统中,甚至获取文件名也需要使用系统调用,例如在MS-DOS系统中即如此。无论实现方式是否同具体的系统有关,我们需要提供一种与系统无关的访问文件信息的途径。
在UNIX系统中,目录就是文件,它包含了一个文件名列表和一些指示文件位置的信息。“位置”是一个指向其他表(即i结点表)的索引。文件的i结点是存放除文件名以外的所有文件信息的地方。目录项通常仅包含两个条目:文件名和i结点编号。
系统调用stat
以文件名作为参数,返回文件的i结点中的所有信息;若出错,则返回-1。如下所示:
1 | char *name; |
它用文件name的i结点信息填充结构stbuf。头文件<sys/stat.h>
中包含了描述stat
的返回值的结构。
…等类型在头文件<sys/types.h>
中定义。
…它的确说明了一些重要的思想。首先,许多程序并不是“系统程序”,它们仅仅使用由操作系统维护的信息。对于这样的程序,很重要的一点是,信息的表示仅出现在标准头文件中,使用它们的程序只需要在文件中包含这些头文件即可,而不需要包含相应的声明。其次,有可能为与系统相关的对象创建一个与系统无关的接口。标准库中的函数就是很好的例子。
malloc
在必要时调用操作系统以获取更多的存储空间。
malloc
并不是从一个在编译时就确定的固定大小的数组中分配存储空间,而是在需要时向操作系统申请空间。因为程序中的某些地方可能不通过malloc
调用申请空间(也就是说,通过其他方式申请空间),所以,malloc
管理的空间不一定是连续的。这样,空闲存储空间以空闲块链表的方式组织,每个块包含一个长度、一个指向下一块的指针以及一个指向自身存储空间的指针。这些块按照存储地址的升序组织,最后一块(最高地址)指向第一块。
当有申请请求时,malloc
将扫描空闲块链表,直到找到一个足够大的块为止。该算法称为“首次适应”(first fit);与之相对的算法是“最佳适应”(best fit),它寻找满足条件的最小块。如果该块恰好与请求的大小相符合,则将它从链表中移走并返回给用户。如果该块太大,则将它分成两部分:大小合适的块返回给用户,剩下的部分留在空闲块链表中。如果找不到一个足够大的块,则向操作系统申请一个大块并加入到空闲块链表中。
释放过程也是首先搜索空闲块链表,以找到可以插入被释放块的合适位置。如果与被释放块相邻的任一边是一个空闲块,则将这两个块合成一个更大的块,这样存储空间不会有太多的碎片。因为空闲块链表是以地址的递增顺序链接在一起的,所以很容易判断相邻的块是否空闲。
…即确保由malloc
函数返回的存储空间满足将要保存的对象的对齐要求。虽然机器类型各异,但是,每个特定的机器都有一个最受限的类型:如果最受限的类型可以存储在某个特定的地址中,则其他所有的类型也可以存放在此地址中。在某些机器中,最受限的类型是double
类型;而在另外一些机器中,最受限的类型是int
或long
类型。
空闲块包含一个指向链表中下一个块的指针、一个块大小的记录和一个指向空闲空间本身的指针。位于块开始处的控制信息称为“头部”。为了简化块的对齐,所有块的大小都必须是头部大小的整数倍,且头部已正确地对齐。
在malloc
函数中,请求的长度(以字符为单位)将被舍入,以保证它是头部大小的整数倍。实际分配的块将多包含一个单元,用于头部本身。实际分配的块的大小将被记录在头部的size字段中。malloc
函数返回的指针将指向空闲空间,而不是块的头部。用户可对获得的存储空间进行任何操作,但是,如果在分配的存储空间之外写入数据,则可能会破坏块链表。
UNIX系统调用sbrk(n)
返回一个指针,该指针指向n个字节的存储空间。如果没有空闲空间,尽管返回NULL
可能更好一些,但sbrk
调用返回-1。必须将-1强制转换为char *
类型,以便与返回值进行比较。而且,强制类型转换使得该函数不会受不同机器中指针表示的不同的影响。但是,这里任然假定,由sbrk
调用返回的指向不同块的多个指针之间可以进行有意义的比较。ANSI标准并没有保证这一点,它只允许指向同一个数组的指针间的比较。因此,只有在一般指针间的比较操作有意义的机器上,该版本的malloc
函数才能够移植。
输入/输出功能并不是C语言本身的组成部分。…标准库…输入/输出函数、字符串处理函数、存储管理函数与数学函数。
ANSI标准精确地定义了这些库函数,所以,在任何可以使用C语言的系统中都有这些函数的兼容形式。如果程序的系统交互部分仅仅使用了标准库提供的功能,则可以不经修改地从一个系统移植到另一个系统中。
标准库实现了简单的文本输入/输出模式。文本流由一系列行组成,每一行的结尾是一个换行符。如果系统没有遵循这种模式,则标准库将通过一些措施使得该系统适应这种模式。例如,标准库可以在输入端将回车符和换页符都转化为换行符,而在输出端进行反向转换。
最简单的输入机制是使用getchar
函数从标准输入中(一般为键盘)一次读取一个字符:
1 | int getchar(void) |
getchar
函数在每次被调用时返回下一个输入字符。若遇到文件结尾,则返回EOF
。符号常量EOF
在头文件<stdio.h>
中定义,其值一般为-1,但程序中应该使用EOF
来测试文件是否结束,这样才能保证程序同EOF
的特定值无关。
在许多环境中,可以使用符号<
来实现输入重定向,它将把键盘输入替换为文件输入:如果程序prog中使用了函数getchar
,则命令行prog <infile
将使得程序prog从输入文件infile(而不是从键盘)中读取字符。实际上,程序prog本身并不在意输入方式的改变,并且,字符串“<infile”也并不包含在argv
的命令行参数中。如果输入通过管道机制来自于另一个程序,那么这种输入切换也是不可见的。比如,在某些系统中,下列命令行:other prog | prog
将运行两个程序otherprog和prog,并将程序otherprog的标准输出通过管道重定向到程序prog的标准输入上。
函数
1 | int putchar(int) |
用于输出数据。putchar(c)
将字符c送至标准输出上,在默认情况下,标准输出为屏幕显示。如果没有发生错误,则函数putchar
将返回输出的字符;如果发生了错误,则返回EOF
。同样,通常情况下,也可以使用“>outfile”的格式将输出重定向到某个文件中。例如,如果程序prog调用了函数putchar
,那么命令行prog >outfile
将把程序prog的输出从标准输出设备重定向到文件中。如果系统支持管道,那么命令行prog | anotherprog
将把程序prog的输出从标准输出通过管道重定向到程序anotherprog的标准输入中。
函数printf
也向标准输出设备上输出数据。我们在程序中可以交叉调用函数putchar
和printf
,输出将按照函数调用的先后顺序依次产生。
使用输入/输出库函数的每个源程序文件必须在引用这些函数之前包含下列语句:#include <stdio.h>
。当文件名用一对尖括号<
和>
括起来时,预处理将在由具体实现定义的有关位置中查找指定的文件(例如,在UNIX系统中,文件一般放在目录/usr/include
中)。
许多程序只从一个输入流中读取数据,并且只向一个输出流中输出数据。对于这样的程序,只需要使用函数getchar
、putchar
和printf
实现输入/输出即可,并且对程序来说已经足够了。特别是,如果通过重定向将一个程序的输出连接到另一个程序的输入,仅仅使用这些函数就足够了。
printf
函数输出函数printf
将内部数值转换为字符的形式。
1 | int printf(char *format, arg1, arg2, ...) |
函数printf
在输出格式format
的控制下,将其参数进行转换与格式化,并在标准输出设备上打印出来。它的返回值为打印的字符数。
格式字符串包含两种类型的对象:普通字符和转换说明。在输出时,普通字符将原样不动地复制到输出流中,而转换说明并不直接输出到输出流中,而是用于控制printf
参数的转换和打印。每个转换说明都由一个百分号字符(即%
)开始,并以一个转换字符结束。在字符%
和转换字符中间可能依次包含下列组成部分:
h
或l
,字母h
表示将整数作为short
类型打印,字母l
表示将整数作为long
类型打印。转换字符:
int
类型;十进制数int
类型;无符号八进制数(没有前导0
)int
类型;无符号十六进制数(没有前导0x
或0X
),10~15分别用abcdef
或ABCDEF
表示int
类型;无符号十进制数int
类型;单个字符char *
类型;顺序打印字符串中的字符,直到遇到'\0'
或已打印了由精度指定的字符数为止double
类型;十进制小数 [-]_m.dddddd_,其中 d 的个数由精度指定(默认值为6)double
类型;[-]_m.dddddd e/E+/-xx_,其中 d 的个数由精度指定(默认值为6)double
类型;如果指数小于-4或大于等于精度,则用%e/E
格式输出,否则用%f
格式输出。尾部的0和小数点不打印void *
类型;指针(取决于具体实现)%
如果%
后面的字符不是一个转换说明,则该行为是未定义的。
在转换说明中,宽度或精度可以用星号*
表示,这时,宽度或精度的值通过转换下一参数(必须为int
类型)来计算。
注意:函数printf
使用第一个参数判断后面参数的个数及类型。如果参数的个数不够或者类型错误,则将得到错误的结果。
函数sprintf
执行的转换和函数printf
相同,但它将输出保存到一个字符串中:
1 | int sprintf(char *string, char *format, arg1, arg2, ...) |
sprintf
函数和printf
函数一样,按照format
格式格式化参数序列arg1
、arg2
、…,但它将输出结果存放到string
中,而不是输出到标准输出中。当然,string
必须足够大以存放输出结果。
函数printf
的正确声明形式为:
1 | int printf(char *fmt, ...) |
其中,省略号表示参数表中参数的数量和类型是可变的。省略号只能在出现在参数表的尾部。
标准头文件<stdarg.h>
中包含一组宏定义,它们对如何遍历参数表进行了定义。该头文件的实现因不同的机器而不同,但提供的接口是一致的。
va_list
类型用于声明一个变量,该变量将依次引用各参数。…将该变量称为ap
,意思是“参数指针”。宏va_start
将ap
初始化为指向第一个无名参数的指针。在使用ap
之前,该宏必须被调用一次。参数表必须至少包含一个有名参数,va_start
将最后一个有名参数作为起点。
每次调用va_arg
,该函数都将返回一个参数,并将ap
指向下一个参数。va_arg
使用一个类型名来决定返回的对象类型、指针移动的步长。最后,必须在函数返回之前调用va_end
,以完成一些必要的清理工作。
scanf
函数输入函数scanf
对应于输出函数printf
,它在与后者相反的方向上提供同样的转换功能。具有变长参数表的函数scanf
的声明形式如下:
1 | int scanf(char *format, ...) |
scanf
函数从标准输入中读取字符序列,按照format
中的格式说明对字符序列进行解释,并把结果保存到其余的参数中。其他所有参数都必须是指针,用于指定经格式转换后的相应输入保存的位置。
当scanf
函数扫描完其格式串,或者碰到某些输入无法与格式控制说明匹配的情况时,该函数将终止,同时,成功匹配并赋值的输入项的个数将作为函数值返回,所以,该函数的返回值可以用来确定已匹配的输入项的个数。如果到达文件的结尾,该函数将返回EOF
。注意,返回EOF
与0
是不同的,0
表示下一个输入字符与格式串中的第一个格式说明不匹配。下一次调用scanf
函数将从上一次转换的最后一个字符的下一个字符开始继续搜索。
另外还有一个输入函数sscanf
,它用于从一个字符串(而不是标准输入)中读取字符序列:
1 | int sscanf(char *string, char *format, arg1, arg2, ...) |
它按照格式参数format
中规定的格式扫描字符串string
,并把结果分别保存到arg
、arg2
、…这些参数中。这些参数必须是指针。
格式串通常都包含转换说明,用于控制输入的转换。格式串可能包含下列部分:
%
),用于匹配输入流中下一个非空白符字符。%
、一个可选的赋值禁止字符*
、一个可选的数值(指定最大字段宽度)、一个可选的h
、l
或L
字符(指定目标对象的宽度)以及一个转换字符组成。转换说明控制下一个输入字段的转换。一般来说,转换结果存放在相应的参数指向的变量中。但是,如果转换说明中有赋值禁止字符*
,则跳过该输入字段,不进行赋值。输入字段定义为一个不包括空白符的字符串,其边界定义为到下一个空白符或达到指定的字符宽度。这表明scanf
函数将越过行边界读取输入,因为换行符也是空白符。(空白符包括空格符、横向制表符、换行符、回车符、纵向制表符以及换页符)。
转换字符指定对输入字段的解释。对应的参数必须是指针,这也是C语言通过值调用语义所要求的。
转换字符:
int *
类型int *
类型,可以是八进制(以0
开头)或十六进制(以0x
或0X
开头)0
开头,也可以不以0
开头);int *
类型unsigned int *
类型0x
或0X
开头,也可以不以0x
或0X
开头);int *
类型char *
类型,将接下来的多个输入字符(默认为1个字符)存放到指定位置。该转换规范通常不跳过空白符。如果需要读入下一个非空白符,可以使用%1s
char *
类型,指向一个足以存放该字符串(还包括尾部的字符\0
)的字符数组。字符串的末尾将被添加一个结束符\0
float *
类型%
;不进行任何赋值操作转换说明d
、i
、o
、u
及x
的前面可以加上字符h
或l
。前缀h
表明参数表的相应参数是一个指向short
类型而非int
类型的指针,前缀l
表明参数表的相应参数是一个指向long
类型的指针。类似地,转换说明e
、f
和g
的前面也可以加上前缀l
,它表明参数表的相应参数是一个指向double
类型而非float
类型的指针。
字符字面值也可以出现在scanf
的格式串中,它们必须与输入中相同的字符匹配。
scanf
函数忽略格式串中的空格和制表符。此外,在读取输入值时,它将跳过空白符(空格、制表符、换行符等等)。如果要读取格式不固定的输入,最后每次读入一行,然后再用sscanf
将合适的格式分离出来读入。
scanf
函数可以和其他输入函数混合使用。无论调用哪个输入函数,下一个输入函数的调用将从scanf
没有读取的第一个字符处开始读取数据。
注意,scanf
和sscanf
函数的所有参数都必须是指针。
标准输入和标准输出是操作系统自动提供给程序访问的。
在读写一个文件之前,必须通过库函数fopen
打开该文件。fopen
用外部名与操作系统进行某些必要的连接和通信(我们不必关心这些细节),并返回一个随后可以用于文件读写操作的指针。
该指针称为文件指针,它指向一个包含文件信息的结构,这些信息包括:缓冲区的位置、缓冲区中当前 字符的位置、文件的读或写状态、是否出错或是否已经到达文件结尾等等。用户不必关心这些细节,因为<stdio.h>
中已经定义了一个包含这些信息的结构FILE
。在程序中只需要按照下列方式声明一个文件指针即可:
1 | FILE *fp; |
在本例中,fp是一个指向结构FILE
的指针,并且,fopen
函数返回一个指向结构FILE
的指针。注意,FILE
像int
一样是一个类型名,而不是结构标记。它是通过typedef
定义的。
在程序中,可以像这样调用fopen
函数:
1 | fp = fopen(name, mode); |
fopen
的第一个参数是一个字符串,它包含文件名。第二个参数是访问模式,也是一个字符串,用于指定文件的使用方式。允许的模式包括:读(“r
”)、写(“w
”)及追加(“a
”)。某些系统还区分文本文件和二进制文件,对后者的访问需要在模式字符串中增加字符“b
”。
如果打开一个不存在的文件用于写或追加,该文件将被创建(如果可能的话)。当以写方式打开一个已存在的文件时,该文件原来的内容将被覆盖。但是,如果以追加方式打开一个文件,则该文件原来的内容将保留不变。读一个不存在的文件会导致错误,其他一些操作也可能导致错误,比如试图读取一个无读取权限的文件。如果发生错误,fopen
将返回NULL
。
文件被打开后,就需要考虑采用哪些方法对文件进行读写。有多种方法可供考虑,其中,getc
和putc
函数最为简单。getc
从文件中返回下一个字符,它需要知道文件指针,以确定对哪个文件执行操作:
1 | int getc(FILE *fp) |
getc
函数返回fp指向的输入流中的下一个字符。如果到达文件尾或出现错误,该函数将返回EOF
。
putc
是一个输出函数,如下所示:
1 | int putc(int c, FILE *fp) |
该函数将字符c写入到fp指向的文件中,并返回写入的字符。如果发生错误,则返回EOF
。类似于getchar
和putchar
,getc
和putc
是宏而不是函数。
启动一个C语言程序时,操作系统环境负责打开3个文件,并将这3个文件的指针提供给该程序。这3个文件分别是标准输入、标准输出和标准错误,相应的文件指针分别为stdin
、stdout
和stderr
,它们在<stdio.h>
中声明。在大多数环境中,stdin
指向键盘,而stdout
和stderr
指向显示器。stdin
和stdout
可以被重定向到文件或管道。
getchar
和putchar
函数可以通过getc
、putc
、stdin
及stdout
定义如下:
1 | #define getchar() getc(stdin) |
对于文件的格式化输入或输出,可以使用函数fscanf
和fprintf
。它们与scanf
和printf
函数的区别仅仅在于它们的第一个参数是一个指向所要读写的文件的指针,第二个参数是格式串。如下所示:
1 | int fscanf(FILE *fp, char *format, ...) |
文件指针stdin
与stdout
都是FILE*
类型的对象。但它们是常量,而非变量,因此不能对它们赋值。
函数int fclose(FILE *fp)
执行和fopen
相反的操作,它断开由fopen
函数建立的文件指针和外部名之间的连接,并释放文件指针以供其他文件使用。因为大多数操作系统都限制了一个程序可以同时打开的文件数,所以,当文件指针不再需要时就应该释放,这是一个好的编程习惯。对输出文件执行fclose
还有另外一个原因:它将把缓冲区中由putc
函数正在收集的输出写到文件中。当程序正常终止时,程序会自动为每个打开的文件调用fclose
函数。(如果不需要使用stdin
与stdout
,可以把它们关闭掉。也可以通过库函数freopen
重新指定它们。)
stderr
和exit
…另一个输出流以与stdin
和stdout
相同的方式分派给程序,即stderr
。即使对标准输出进行了重定向,写到stderr
中的输出通常也会显示在屏幕上。
…首先,将fprintf
函数产生的诊断信息输出到stderr
上,因此诊断信息将会显示在屏幕上,而不是仅仅输出到管道或输出文件中。诊断信息中包含argv[0]
中的程序名,因此,当该程序和其他程序一起运行时,可以识别错误的来源。
其次,程序使用了标准库函数exit
,当该函数被调用时,它将终止调用函数的执行。任何调用该程序的进程都可以获取exit
的参数值,因此,可通过另一个将该程序作为子进程的程序来测试该程序的执行是否成功。按照惯例,返回值0表示一切正常,而非0返回值通常表示出现了异常情况。exit
为每个已打开的输出文件调用fclose
函数,以将缓冲区中的所有输出写到相应的文件中。
在主程序main
中,语句return expr
等价于exit(expr)
。但是,使用函数exit
有一个优点,它可以从其他函数中调用,并且可以查找这些调用。
如果流fp
中出现错误,则函数ferror
返回一个非0值。
1 | int ferror(FILE *fp) |
尽管输出错误很少出现,但还是存在的(例如,当磁盘满时),因此,成熟的产品程序应该检查这种类型的错误。
函数feof(FILE*)
与ferror
类似。如果指定的文件到达文件结尾,它将返回一个非0值。
1 | int feof(FILE *fp) |
…但对于任何重要的程序来说,都应该让程序返回有意义且有用的值。
标准库提供了一个输入函数fgets
,它和getline
类似。
1 | char *fgets(char *line, int maxline, FILE *fp) |
fgets
函数从fp指向的文件中读取下一个输入行(包括换行符),并将它存放在字符数组line
中,它最多可读取maxline-1
个字符。读取的行将以\0
结尾保存到数组中。通常情况下,fgets
返回line
,但如果遇到了文件结尾或发生了错误,则返回NULL
。
输出函数fputs
将一个字符串(不需要包含换行符)写入到一个文件中:
1 | int fputs(char *line, FILE *fp) |
如果发生错误,该函数将返回EOF
,否则返回一个非负值。
库函数gets
和puts
的功能与fgets
和fputs
函数类似,但它们是对stdin
和stdout
进行操作。有一点我们需要注意,gets
函数在读取字符串时将删除结尾的换行符('\n'
),而puts
函数在写入字符串时将在结尾添加一个换行符。
ANSI标准规定,ferror
在发生错误时返回非0值,而fputs
在发生错误时返回EOF
,其他情况返回一个非负值。
…它们都在头文件<string.h>
中定义。
头文件<ctype.h>
中定义了一些用于字符测试和转换的函数。
ungetc
函数函数system(char*s)
执行包含在字符串s中的命令,然后继续执行当前程序。s的内容在很大程度上与所用的操作系统有关。system
函数返回一个整型的状态值,其值来自于执行的命令,并同具体系统有关。在UNIX系统中,返回的状态是exit
的返回值。
函数malloc
和calloc
用于动态地分配存储块。函数malloc
的声明如下:
1 | void *malloc(size_t n) |
当分配成功时,它返回一个指针,该指针指向n字节长度的未初始化的存储空间,否则返回NULL
。函数calloc
的声明为
1 | void *calloc(size_t n, size_t size) |
当分配成功时,它返回一个指针,该指针指向的空闲空间足以容纳由n个指定长度的对象组成的数组,否则返回NULL
。该存储空间被初始化为0。
根据请求的对象类型,malloc
或calloc
函数返回的指针满足正确的对齐要求。
free(p)
函数释放p指向的内存空间,其中,p是此前通过调用malloc
或calloc
函数得到的指针。存储空间的释放顺序没有什么限制,但是,如果释放的不是通过调用malloc
或calloc
函数得到的指针所指向的存储空间,将是一个很严重的错误。
使用已经释放的存储空间同样是错误的。
头文件<math.h>
中声明了20多个数学函数。
函数rand()
生成介于0
和RAND_MAX
之间的伪随机整数序列。其中RAND_MAX
是在头文件<stdlib.h>
中定义的符合常量。下面是一种生成大于等于0但小于1的随机浮点数的方法:
1 | #define frand() ((double) rand() / (RAND_MAX + 1.0)) |
(如果所用的函数库中已经提供了一个生成浮点随机数的函数,那么它可能比上面这个函数具有更好的统计学特性。)
函数srand(unsigned)
设置rand
函数的种子数。
结构是一个或多个变量的集合,这些变量可能为不同的类型,为了处理的方便而将这些变量组织在一个名字之下。由于结构将一组相关的变量看作一个单元而不是各自独立的实体,因此结构有助于组织复杂的数据,特别是在大型的程序中。
ANSI标准在结构方面最主要的变化是定义了结构的赋值操作——结构可以拷贝、赋值、传递给函数,函数也可以返回结构类型的返回值。多年以前,这一操作就已经被大多数的编译器所支持,但是,直到这一标准才对其属性进行了精确定义。在ANSI标准中,自动结构和数组现在也可以进行初始化。
关键字struct
引入结构声明。结构声明由包含在花括号内的一系列声明组成。关键字struct
后面的名字是可选的,称为结构标记。结构标记用于为结构命名。在定义之后,结构标记就代表花括号内的声明,可以用它作为该声明的简写形式。
结构中定义的变量称为成员。结构成员、结构标记和普通变量(即非成员)可以采用相同的名字,它们之间不会冲突,因为通过上下文分析总可以对它们进行区分。另外,不同结构中的成员可以使用相同的名字,但是,从编程风格方面来说,通常只有密切相关的对象才会使用相同的名字。
struct
声明定义了一种数据结构。在标志结构成员表结束的右花括号之后可以跟一个变量表,这与其他基本类型的变量声明是相同的。
如果结构声明的后面不带变量表,则不需要为它分配存储空间,它仅仅描述了一个结构的模板或轮廓。但是,如果结构声明中带有标记,那么在以后定义结构实例时可以使用该标记定义。结构的初始化可以在定义的后面使用初值表进行。初值表中同每个成员对应的初值必须是常量表达式。自动结构也可以通过赋值初始化,还可以通过调用返回相应类型结构的函数进行初始化。
在表达式中,可以通过下列形式引用某个特定结构中的成员:结构名.成员。其中的结构成员运算符.
将结构名与成员名连接起来。
结构可以嵌套。
结构的合法操作只有几种:作为一个整体复制和赋值,通过&
运算符取地址,访问其成员。其中,复制和赋值包括向函数传递参数以及从函数返回值。结构之间不可以进行比较。可以用一个常量成员值列表初始化结构,自动结构也可以通过赋值进行初始化。
至少可以通过3种可能的方法传递结构:一是分别传递各个结构成员,而是传递整个结构,三是传递指向结构的指针。这3种方法各有利弊。
注意,参数名和结构成员同名不会引起冲突。事实上,使用重名可以强调两者之间的关系。
…结构类型的参数和其他类型的参数一样,都是通过值传递的。
如果传递给函数的结构很大,使用指针方式的效率通常比复制整个结构的效率要高。…结构成员运算符.
的优先级比*
的优先级高。
结构指针的使用频度非常高,为了使用方便,C语言提供了另一种简写方式。假定p是一个指向结构的指针,可以用 p->结构成员 这种形式引用相应的结构成员。
运算符.
和->
都是从左至右结合的。
C语言提供了一个编译时(compile-time)一元运算符sizeof
,它可用来计算任一对象的长度。表达式 sizeof 对象 以及 sizeof(类型名) 将返回一个整型值,它等于指定对象或类型占用的存储空间字节数。(严格地说,sizeof
的返回值是无符号整型值,其类型为size_t
,该类型在头文件<stddef.h>
中定义。)其中,对象可以是变量、数组或结构;类型可以是基本类型,也可以是派生类型。
条件编译语句#if
中不能使用sizeof
,因为预处理器不对类型名进行分析。但预处理器并不计算#define
语句中的表达式,因此,在#define
中使用sizeof
是合法的。
但是,千万不要认为结构的长度等于各成员长度的和。因为不同的对象有不同的对齐要求,所以,结构中可能会出现未命名的“空穴”(hole)。使用sizeof
运算符可以返回正确的对象长度。
…具体采用哪种写法属于个人的习惯问题,可以选择自己喜欢的方式并始终保持自己的风格。
一个包含其自身实例的结构是非法的。
…自引用结构的一种变体:两个结构相互引用。
对齐要求一般比较容易满足,只需要确保分配程序始终返回满足所有对齐限制要求的指针就可以了,其代价是牺牲一些存储空间。
C语言提供了一个称为typedef
的功能,它用来建立新的数据类型名。
注意,typedef
中声明的类型在变量名的位置出现,而不是紧接在关键字typedef
之后。typedef
在语法上类似于存储类extern
、static
等。
从任何意义上讲,typedef
声明并没有创建一个新类型,它只是为某个已存在的类型增加了一个新的名称而已。typedef
声明也没有增加任何新的语义:通过这种方式声明的变量与通过普通方式声明的变量具有相同的属性。实际上,typedef
类似于#define
语句,但由于typedef
是由编译器解释的,因此它的文本替换功能要超过预处理器的能力。
除了表达方式更简洁之外,使用typedef
还有另外两个重要原因。首先,它可以使程序参数化,以提高程序的可移植性。如果typedef
声明的数据类型同机器有关,那么,当程序移植到其他机器上时,只需改变typedef
类型定义就可以了。一个经常用到的情况是,对于各种不同大小的整数值来说,都使用通过typedef
定义的类型名,然后,分别为各个不同的宿主机选择一组合适的short
、int
和long
类型大小即可。标准库中有一些例子,例如size_t
和ptrdiff_t
等。
typedef
的第二个作用是为程序提供更好的说明性。
联合是可以(在不同时刻)保存不同类型和长度的对象的变量,编译器负责跟踪对象的长度和对齐要求。联合提供了一种方式,以在单块存储区中管理不同类型的数据,而不需要在程序中嵌入任何同机器有关的信息。
联合的目的——一个变量可以合法地保存多种数据类型中任何一种类型的对象。其语法基于结构。
…必须足够大,以保存最大的一种,具体长度同具体的实现有关。这些类型中的任何一种类型的对象都可赋值给u,且可使用在随后的表达式中,但必须保证是一致的:读取的类型必须是最近一次存入的类型。程序员负责跟踪当前保存在联合中的类型。如果保存的类型与读取的类型不一致,其结果取决于具体的实现。
可以通过下列语法访问联合中的成员:联合名.成员
或联合指针->成员
,它与访问结构的方式相同。
联合可以使用在结构和数组中,反之亦可。访问结构中的联合(或反之)的某一成员的表示法与嵌套结构相同。
实际上,联合就是一个结构,它的所有成员相对于基地址的偏移量都为0,此结构空间要大到足够容纳最“宽”的成员,并且,其对齐方式要适合于联合中所有类型的成员。对联合允许的操作与对结构允许的操作相同:作为一个整体单元进行赋值、复制、取地址及访问其中的一个成员。
联合只能用其第一个成员类型的值进行初始化。
在存储空间很宝贵的情况下,有可能需要将多个对象保存在一个机器字中。一种常用的方法是,使用类似于编译器符号表的单个二进制位标志集合。外部强加的数据格式(如硬件设备接口)也经常需要从字的部分位中读取数据。
最简洁的方法就是使用一个char
或int
对象中的位标志集合。通常采用的方法是,定义一个与相关位的位置对应的“屏蔽码”集合。
C语言提供了一种可替代的方法,即直接定义和访问同一个字中的位字段的能力,而不需要通过按位逻辑运算符。位字段(bit-bield),或简称字段,是“字”中相邻位的集合。“字”(word)是单个的存储单元,它同具体的实现有关。
单个字段的引用方式与其他结构成员相同。字段的作用与小整数相似。同其他整数一样,字段可出现在算术表达式中。
字段的所有属性几乎都同具体的实现有关。字段是否能覆盖字边界由具体的实现定义。字段可以不命名,无名字段(只有一个冒号和宽度)起填充作用。特殊宽度0可以用来强制在下一个字边界上对齐。
某些机器上字段的分配是从字的左端至右端进行的,而某些机器上则相反。这意味着,尽管字段对维护内部定义的数据结构很有用,但在选择外部定义数据的情况下,必须仔细考虑哪端优先的问题。依赖于这些因素的程序是不可移植的。字段也可以仅仅声明为int
,为了方便移植,需要显示声明该int
类型是signed
还是unsigned
类型。字段不是数组,并且没有地址,因此对它们不能使用&
运算符。
指针是一种保存变量地址的变量。在C语言中,指针的使用非常广泛,原因之一是,指针常常是表达某个计算的惟一途径,另一个原因是,同其他方法比较起来,使用指针通常可以生产更高效、更紧凑的代码。指针与数组之间的关系十分密切。
指针和goto
语句一样,会导致程序难以理解。如果使用者粗心,指针很容易就指向了错误的地方。但是,如果谨慎地使用指针,便可以利用它写出简单、清晰的程序。
ANSI C的一个最重要的变化是,它明确地制定了操纵指针的规则。事实上,这些规则已经被很多优秀的程序设计人员和编译器所采纳。此外,ANSI C使用类型void *
(指向void
的指针)代替char *
作为通用指针的类型。
通常的机器都有一系列连续编号或编址的存储单元,这些存储单元可以单个进行操纵,也可以以连续成组的方式操纵。通常情况下,机器的一个字节可以存放一个char
类型的数据,两个相邻的字节存储单元可存储一个short
(短整型)类型的数据,而4个相邻的字节存储单元可存储一个long
(长整型)类型的数据。指针是能够存放一个地址的一组存储单元(通常是两个或4个字节)。
一元运算符&
可用于取一个对象的地址,…称p为“指向”c的指针。地址运算符&
只能应用于内存中的对象,即变量与数组元素。它不能作用于表达式、常量或register
类型的变量。
一元运算符*
是间接寻址或间接引用运算符。当它作用于指针时,将访问指针所指向的对象。
…指针的声明,是为了便于记忆。这种声明变量的语法与声明该变量所在表达式的语法类似。同样的原因,对函数的声明也可以采用这种方式。
…指针只能指向某种特定类型的对象,也就是说,每个指针都必须指向某种特定的数据类型。(一个例外情况是指向void
类型的指针可以存放指向任何类型的指针,但它不能间接引用其自身。)。
如果指针ip指向整形变量x,那么在x可以出现的任何上下文中都可以使用*ip。
一元运算符*
和&
的优先级比算术运算符的优先级高。…从右至左的结合顺序。
由于指针也是变量,所以在程序中可以直接使用,而不必通过间接引用的方法使用。
由于C语言是以传值的方式将参数传递给被调用函数,因此,被调用函数不能直接修改主调函数中变量的值。
…可以使主调函数将指向变量的指针传递给被调用函数。
指针参数使得被调用函数能够访问和修改主调函数中对象的值。
在C语言中,指针和数组之间的关系十分密切。通过数组下标所能完成的任何操作都可以通过指针来实现。一般来说,用指针编写的程序比用数组下标编写的程序执行速度快,但另一方面,用指针实现的程序理解起来稍微困难一些。
声明
int a [10];
定义了一个长度为10的数组a。换句话说,它定义了一个由10个对象组成的集合,这10个对象存储在相邻的内存区域中,名字分别为a[0]、a[1]、…、a[9]。a[i]
表示该数组的第i个元素。…指针pa指向数组a的第0个元素,pa的值为数组元素a[0]的地址。
如果pa指向数组中的某个特定元素,那么,根据指针运算的定义,pa+1将指向下一个元素,pa+i将指向pa所指向数组元素之后的第i个元素,而pa-i将指向pa所指向数组元素之前的第i个元素。因此,如果指针pa指向a[0],那么pa+i是数组元素a[i]的地址,*(pa+i)引用的是数组元素a[i]的内容。
无论数组a中元素的类型或数组长度是什么,上面的结论都成立。“指针加1”就意味着,pa+1指向pa所指向的对象的下一个对象。相应地,pa+i指向pa所指向的对象之后的第i个对象。
下标和指针运算之间具有密切的对应关系。根据定义,数组类型的变量或表达式的值是该数组第0个元素的地址。执行赋值语句
pa = &a[0];
后,pa和a具有相同的值。因为数组名所代表的就是该数组最开始的一个元素的地址,所以,赋值语句也可以写成下列形式:
pa = a;
…一个通过数组和下标实现的表达式可等价地通过指针和偏移量实现。
数组名和指针之间有一个不同之处。指针是一个变量…但数组名不是变量。
当把数组名传递给一个函数时,实际上传递的是该数组第一个元素的地址。在被调用函数中,该参数是一个局部变量,因此,数组名参数必须是一个指针,也就是一个存储地址值的变量。
在函数定义中,形式参数char s[];
和char *s;
是等价的。我们通常更习惯于使用后一种形式,因为它比前者更直观地表明了该参数是一个指针。如果将数组名传递给函数,函数可以根据情况判定是按照数组处理还是按照指针处理,随后根据相应的方式操作该参数。为了直观且恰当地描述函数,在函数中甚至可以同时使用数组和指针这两种表示方法。
也可以将指向子数组起始位置的指针传递给函数,这样,就将数组的一部分传递给了函数。…对于函数来说,它并不关心所引用的是否只是一个更大数组的部分元素。
如果确信相应的元素存在,也可以通过下标访问数组第一个元素之前的元素。当然,引用数组边界之外的对象是非法的。
如果p是一个指向数组中某个元素的指针,那么p++将对p进行自增运算并指向下一个元素,而p+=i将对p进行加i的增量运算,使其指向指针p当前所指向的元素之后的第i个元素。这类运算是指针或地址算术运算中最简单的形式。
C语言中的地址算术运算方法是一致且有规律的,将指针、数组和地址的算术运算集成在一起是该语言的一大优点。
一般情况下,同其他类型的变量一样,指针也可以初始化。通常,对指针有意义的初始化值只能是0或者是表示地址的表达式,对后者来说,表达式所代表的地址必须是在此前已定义的具有适当类型的数据的地址。
…C语言保证,0永远不是有效的数据地址,因此,返回值0可用来表示发生了异常事件。
指针与整数之间不能相互转换,但0是惟一的例外:常量0可以赋值给指针,指针也可以和常量0进行比较。程序中经常用符号常量NULL
代替常量0,这样便于更清晰地说明常量0是指针的一个特殊值。
指针算术运算有以下几个重要特点。首先,在某些情况下对指针可以进行比较运算。例如,如果指针p和q指向同一个数组的成员,那么它们之间就可以进行类似于==
、!=
、<
、>=
的关系比较运算。如果p指向的数组元素的位置在q指向的数组元素位置之前,那么关系表达式p < q的值为真(true)。任何指针与0进行相等或不等的比较运算都有意义。但是,指向不同数组的元素的指针之间的算术或比较运算没有定义。(这里有一个特例:指针的算术运算中可使用数组最后一个元素的下一个元素的地址。)
指针可以和整数进行相加或相减运算。结构p + n表示指针p当前所指向的对象之后第n个对象的地址。…n将根据p指向的对象的长度按比例缩放,而p指向的对象的长度则取决于p的声明。
指针的减法运算也是有意义的:如果p和q指向相同数组中的元素,且p < q,那么q-p+1就是位于p和q指向的元素之间的元素的数目。
指针的算术运算具有一致性:所有的指针运算都会自动考虑它所指向的对象的长度。
有效的指针运算包括相同类型指针之间的赋值运算;指针同整数之间的加法或减法运算;指向相同数组中元素的两个指针间的减法或比较运算;将指针赋值为0或指针与0之间的比较运算。其他所有形式的指针运算都是非法的。
字符串常量是一个字符数组,在字符串的内部表示中,字符数组以空字符\0
结尾,所以,程序可以通过检查空字符找到字符数组的结尾。字符串常量占据的存储单元数也因此比双引号内的字符数大1。
字符串常量最常见的用法也许是作为函数参数,…实际上是通过字符指针访问该字符串的。字符串常量可通过一个指向其第一个元素的指针访问。
…该过程并没有进行字符串的复制,而只是涉及到指针的操作。C语言没有提供将整个字符串作为一个整体进行处理的运算符。
由于指针本身也是变量,所以它们也可以像其他变量一样存储在数组中。
…消除了因移动文本行本身所带来的复杂的存储管理和巨大的开销这两个孪生问题。
…通常情况下,最好将程序划分成若干个与问题的自然划分相一致的函数,并通过主函数控制其他函数的执行。
C语言提供了类似于矩阵的多维数组,但实际上它们并不像指针数组使用得那样广泛。
在C语言中,二维数组实际上是一种特殊的一维数组,它的每个元素也是一个一维数组。除了表示方式的区别外,C语言中二维数组的使用方式和其他语言一样。数组元素按行存储,因此,当按存储顺序访问数组时,最右边的数组下标(即列)变化得最快。
数组可以用花括号括起来的初值表进行初始化,二维数组的每一行由相应的子列表进行初始化。
如果将二维数组作为参数传递给函数,那么在函数的参数声明中必须指明数组的列数。数组的行数没有太大关系,函数调用时传递的是一个指针,它指向由行向量构成的一维数组,其中每个行向量是具有13个整型元素的一维数组。在该例子中,传递给函数的是一个指向很多对象的指针,其中每个对象是由13个整型元素构成的一维数组。
…一般来说,除数组的第一维(下标)可以不指定大小外,其余各维都必须明确指定大小。
…由于上述声明中没有指明数组的长度,因此,编译器编译时将对初值个数进行统计,并将这一准确数字填入数组的长度。
…指针数组的一个重要优点在于,数组的每一行长度可以不同。
…指针数组最频繁的用处是存放具有不同长度的字符串。
在支持C语言的环境中,可以在程序开始执行时将命令行参数传递给程序。调用主函数main
时,它带有两个参数。第一个参数(习惯上称为argc
,用于参数计数)的值表示运行程序时命令行中参数的数目;第二个参数(称为argv
,用于参数向量)是一个指向字符串数组的指针,其中每个字符串对应一个参数。我们通常用多级指针处理这些字符串。
按照C语言的约定,argv[0]
的值是启动该程序的程序名,因此argc
的值至少为1.如果argc
的值为1,则说明程序名后面没有命令行参数。…第一个可选参数为argv[1]
,而最后一个可选参数为argv[argc-1]
。另外,ANSI标准要求argv[argc]
的值必须为一空指针。
UNIX系统中的C语言程序有一个公共的约定:以负号开头的参数表示一个可选标志或参数。
可选参数应该允许以任意次序出现,同时,程序的其余部分应该与命令行中参数的数目无关。此外,如果可选参数能够组合使用,将会给使用者带来更大的方便。
在C语言中,函数本身不是变量,但可以定义指向函数的指针。这种类型的指针可以被赋值、存放在数组中、传递给函数以及作为函数的返回值等等。
…因为它们是函数,所以前面不需要加上取地址运算符&
。
C语言常常因为声明的语法问题而受到人们的批评,特别是涉及到函数指针的语法。C语言的语法力图使声明和使用相一致。对于简单的情况,C语言的做法是很有效的,但是,如果情况比较复杂,则容易让人混淆,原因在于,C语言的声明不能从左至右阅读,而且使用了太多的圆括号。…*
是一个前缀运算符,其优先级低于()
,所以,声明中必须使用圆括号以保证正确的结合顺序。
尽管实际中很少用到过于复杂的声明,但是,懂得如何理解甚至如何使用这些复杂的声明是很重要的。…一种比较好的方法是,使用typedef
通过简单的步骤合成。
函数可以把大的计算任务分解成若干个较小的任务,程序设计人员可以基于函数进一步构造程序,而不需要重新编写一些代码。一个设计得当的函数可以把程序中不需要了解的具体操作细节隐藏起来,从而使整个程序结构更加清晰,并降低修改程序的难度。
C语言在设计中考虑了函数的高效性与易用性这两个因素。C语言程序一般都由许多小的函数组成,而不是由少量较大的函数组成。一个程序可以保存在一个或者多个源文件中。各个文件可以单独编译,并可以与库中已编译过的函数一起加载。
ANSI标准对C语言所做的最明显的修改是函数声明与函数定义这两方面。目前C语言已经允许在声明函数时声明参数的类型。为了使函数的声明与定义相适应,ANSI标准对函数定义的语法也做了修改。基于该原因,编译器就有可能检测出比以前的C语言版本更多的错误。并且,如果参数声明得当,程序可以自动地进行适当的强制类型转换。
ANSI标准进一步明确了名字的作用域规则,特别要求每个外部对象只能有一个定义。初始化的适用范围也更加广泛了,自动数组与结构都可以进行初始化。
C语言预处理器的功能也得到了增强。新的预处理器包含一组更完整的条件编译指令(一种通过宏参数创建带引号的字符串的方法),对宏扩展过程的控制更严格。
尽管我们可以把所有的代码都放在主程序main
中,但更好的做法是,利用其结构把每一部分设计成一个独立的函数。分别处理3个小的部分比处理一个大的整体更容易,因为这样可以把不相关的细节隐藏在函数中,从而减少了不必要的相互影响的机会,并且,这些函数也可以在其他程序中使用。
函数的定义形式如下:
1 | 返回值类型 函数名(参数声明表) |
函数定义中的各构成部分都可以省略。最简单的函数如下所示:
1 | dummy() {} |
该函数不执行任何操作也不返回任何值。这种不执行任何操作的函数有时很有用,它可以在程序开发期间用以保留位置(留待以后填充代码)。如果函数定义中省略了返回值类型,则默认为int
类型。
程序可以看成是变量定义和函数定义的集合。函数之间的通信可以通过参数、函数返回值以及外部变量进行。函数在源文件中出现的次序可以是任意的。只要保证每一个函数不被分离到多个文件中,源程序就可以分成多个文件。
被调用函数通过return
语句向调用者返回值,return
语句的后面可以跟任何表达式:
1 | return expression; |
在必要时,表达式将被转换为函数的返回值类型。表达式两边通常加一对圆括号,此处的括号是可选的。
调用函数可以忽略返回值。并且,return
语句的后面也不一定需要表达式。当return
语句的后面没有表达式时,函数将不向调用者返回值。当被调用函数执行到最后的右花括号而结束执行时,控制同样也会返回给调用者(不返回值)。如果某个函数从一个地方返回时有返回值,而从另一个地方返回时没有返回值,该函数并不非法,但可能是一种出问题的征兆。在任何情况下,如果函数没有成功地返回一个值,则它的“值”肯定是无用的。
…主程序main
返回了一个状态。该返回值可以在该程序的环境中使用。
在不同的系统中,保存在多个源文件中的C语言程序的编译与加载机制是不同的。
首先,由于函数的返回值类型不是int
,因此函数必须声明返回值的类型。返回值的类型名应放在函数名字之前。
其次,调用函数必须知道函数返回的是非整型值,这一点也是很重要的。为了达到该目的,一种方法是在调用函数中显式声明函数。
函数的声明与定义必须一致。如果函数与调用它的主函数main
放在同一源文件中,并且类型不一致,编译器就会检测到该错误。但是,如果函数是单独编译的(这种可能性更大),这种不匹配的错误就无法检测出来,函数将返回double
类型的值,而main
函数却将返回值按照int
类型处理,最后的结果值毫无意义。
根据前面有关函数的声明如何与定义保持一致的讨论,发生不匹配现象似乎很令人吃惊。其中的一个原因是,如果没有函数原型,则函数将在第一次出现的表达式中被隐式声明。如果先前没有声明过的一个名字出现在某个表达式中,并且其后紧跟一个左圆括号,那么上下文就会认为该名字是一个函数名字,该函数的返回值将被假定为int
类型,但上下文并不对其参数作任何假设。并且,如果函数声明中不包含参数,那么编译程序也不会对函数的参数作任何假设,并会关闭所有的参数检查。对空参数表的这种特殊处理是为了使新的编译器能编译比较老的C语言程序。不过,在新编写的程序中这么做是不提倡的。如果函数带有参数,则要声明它们;如果没有参数,则使用void
进行声明。
在下列形式的return
语句中:
1 | return (expression); |
其中,表达式的值在返回之前将被转换为函数的类型。这种操作可能会丢失信息,某些编译器可能会对此给出警告信息。在该函数中,由于采用了类型转换的方法显式表明了所要执行的转换操作,因此可以防止有关的警告信息。
C语言程序可以看成由一系列的外部对象构成,这些外部对象可能是变量或函数。形容词 external 与 internal 是相对的,internal 用于描述定义在函数内部的函数参数及变量。外部变量定义在函数之外,因此可以在许多函数中使用。由于C语言不允许在一个函数中定义其他函数,因此函数本身是“外部的”。默认情况下,外部变量与函数具有下列性质:通过同一个名字对外部变量的所有引用(即使这种引用来自于单独编译的不同函数)实际上都是引用同一个对象(标准中把这一性质称为外部连接)。
因为外部变量可以在全局范围内访问,这就为函数之间的数据交换提供了一种可以代替函数参数与返回值的方式。任何函数都可以通过名字访问一个外部变量,当然这个名字需要通过某种方式进行声明。
如果函数之间需要共享大量的变量,使用外部变量要比使用一个很长的参数表更方便、有效。但是,这样做必须非常谨慎,因为这种方式可能对程序结构产生不良的影响,而且可能会导致程序中各个函数之间具有太多的数据联系。
外部变量的用途还表现在它们与内部变量相比具有更大的作用域和更长的生存期。自动变量只能在函数内部使用,从其所在的函数被调用时变量开始存在,在函数退出时变量也将消失。而外部变量是永久存在的,它们的值在一次函数调用到下一次函数调用之间保持不变。因此,如果两个函数必须共享某些数据,而这两个函数互不调用对方,这种情况下最方便的方式便是把这些共享数据定义为外部变量,而不是作为函数参数传递。
构成C语言程序的函数与外部变量可以分开进行编译。一个程序可以存放在几个文件中,原先已编译过的函数可以从库中进行加载。
名字的作用域指的是程序中可以使用该名字的部分。对于在函数开头声明的自动变量来说,其作用域是声明该变量名的函数。不同函数中声明的具有相同名字的各个局部变量之间没有任何关系。函数的参数也是这样的,实际上可以将它看作是局部变量。
外部变量或函数的作用域从声明它的地方开始,到其所在的(待编译的)文件的末尾结束。
另一方面,如果要在外部变量的定义之前使用该变量,或者外部变量的定义与变量的使用不在同一个源文件中,则必须在相应的变量声明中强制性地使用关键字extern
。
将外部变量的声明与定义严格区分开来很重要。变量声明用于说明变量的属性(主要是变量的类型),而变量定义除此以外还将引起存储器的分配。
在一个源程序的所有源文件中,一个外部变量只能在某个文件中定义一次,而其他文件可以通过extern
声明来访问它(定义外部变量的源文件中也可以包含对该外部变量的extern
声明)。外部变量的定义中必须指定数组的长度,但extern
声明则不一定要指定数组的长度。
外部变量的初始化只能出现在其定义中。
…把程序分割到若干个源文件中的情况,如果该程序的各组成部分很长,那么这么做还是有必要的。…主要是考虑在实际的程序中,它们分别来自于单独编译的库。
此外,还必须考虑定义和声明在这些文件之间的共享问题。我们尽可能把共享的部分集中在一起,这样就只需要一个副本,改进程序时也容易保证程序的正确性。我们把这些公共部分放在头文件中,在需要使用该头文件时通过#include
指令将它包含进来。
我们对下面两个因素进行了折衷:一方面是我们期望每个文件只能访问它完成任务所需的信息;另一方面是现实中维护较多的头文件比较困难。我们可以得出这样一个结论:对于某些中等规模的程序,最好只用一个头文件存放程序中各部分共享的对象。较大的程序需要使用更多的头文件,我们需要精心地组织它们。
某些变量,它们仅供其所在的源文件中的函数使用,其他函数不能访问。用static
声明限定外部变量与函数,可以将其后声明的对象的作用域限定为被编译源文件的剩余部分。通过static
限定外部对象,可以达到隐藏外部对象的目的。
要将对象指定为静态存储,可以在正常的对象声明之前加上关键字static
作为前缀。…因此名字不会和同一程序中的其他文件中的相同的名字相冲突。
外部的static
声明通常多用于变量,当然,它也可用于声明函数。通常情况下,函数名字是全局可访问的,对整个程序的各个部分而言都可见。但是,如果把函数声明为static
类型,则该函数名除了对该函数声明所在的文件可见外,其他文件都无法访问。
static
也可用于声明内部变量。static
类型的内部变量同自动变量一样,是某个特定函数的局部变量,只能在该函数中使用,但它与自动变量不同的是,不管其所在函数是否被调用,它一直存在,而不像自动变量那样,随着所在函数的被调用和退出而存在和消失。换句话说,static
类型的内部变量是一种只能在某个特定函数中使用但一直占据存储空间的变量。
register
声明告诉编译器,它所声明的变量在程序中使用频率较高。其思想是,将register
变量放在机器的寄存器中,这样可以使程序更小、执行速度更快。但编译器可以忽略此选项。
register
声明只适用于自动变量以及函数的形式参数。
实际使用时,底层硬件环境的实际情况对寄存器变量的使用会有一些限制。每个函数中只有很少的变量可以保存在寄存器中,且只允许某些类型的变量。但是,过量的寄存器声明并没有什么害处,这是因为编译器可以忽略过量的或不支持的寄存器变量声明。另外,无论寄存器变量实际上是不是存放在寄存器中,它的地址都是不能访问的。在不同的机器中,对寄存器变量的数目和类型的具体限制也是不同的。
C语言并不是程序块结构的语言,它不允许在函数中定义函数。但是,在函数中可以以程序块结构的形式定义变量。变量的声明(包括初始化)除了可以紧跟在函数开始的花括号之后,还可以紧跟在任何其他标识复合语句开始的左花括号之后。以这种方式声明的变量可以隐藏程序块外与之同名的变量,它们之间没有任何关系,并在与左花括号匹配的右花括号出现之前一直存在。
每次进入程序块时,在程序块内声明以及初始化的自动变量都将被初始化。静态变量只在第一次进入程序块时被初始化一次。
自动变量(包括形式参数)也可以隐藏同名的外部变量与函数。
在一个好的程序设计风格中,应该避免出现变量名隐藏外部作用域中相同名字的情况,否则,很可能引起混乱和错误。
在不进行显式初始化的情况下,外部变量和静态变量都将被初始化为0,而自动变量和寄存器变量的初值则没有意义(即初值为无用的信息)。
定义标量变量时,可以在变量名后紧跟一个等号和一个表达式来初始化变量。对于外部变量与静态变量来说,初始化表达式必须是常量表达式,且只初始化一次(从概念上讲是在程序开始执行前进行初始化)。对于自动变量与寄存器变量,则在每次进入函数或程序块时都将被初始化。
对于自动变量与寄存器变量来说,初始化表达式可以不是常量表达式:表达式中可以包含任意在此表达式之前已经定义的值,包括函数调用。实际上,自动变量的初始化等效于简写的赋值语句。究竟采用哪一种形式,还得看个人的习惯。考虑到变量声明中的初始化表达式容易被人忽略,且距使用的位置较远,我们一般使用显式的赋值语句。
数组的初始化可以在声明的后面紧跟一个初始化表达式列表,初始化表达式列表用花括号括起来,各初始化表达式之间通过逗号分隔。当省略数组的长度时,编译器将把花括号中初始化表达式的个数作为数组的长度。
如果初始化表达式的个数比数组元素数少,则对外部变量、静态变量和自动变量来说,没有初始化表达式的元素将被初始化为0。如果初始化表达式的个数比数组元素数多,则是错误的。不能一次将一个初始化表达式指定给多个数组,也不能跳过前面的数组元素而直接初始化后面的数组元素。
字符数组的初始化比较特殊:可以用一个字符串来代替用花括号括起来并用逗号分隔的初始化表达式序列。
C语言中的函数可以递归调用,即函数可以直接或间接调用自身。
函数递归调用自身时,每次调用都会得到一个与以前的自动变量集合不同的新的自动变量集合。
递归并不节省存储器的开销,因为递归调用过程中必须在某个地方维护一个存储处理值的栈。递归的执行速度并不快,但递归代码比较紧凑,并且比相应的非递归代码更易于编写与理解。在描述树等递归定义的数据结构时使用递归尤其方便。
C语言通过预处理器提供了一些语言功能。从概念上讲,预处理器是编译过程中单独执行的第一个步骤。两个最常用的预处理器指令是:#include
指令(用于在编译期间把指定文件的内容包含进当前文件中)和#define
指令(用任意字符序列替代一个标记)。…预处理器的其他一些特性,如条件编译与带参数的宏。
文件包含指令(即#include
指令)使得处理大量的#define
指令以及声明更加方便。在源文件中,任何形如:
#include "文件名"
或
#include <文件名>
的行都将被替换为由文件名指定的文件的内容。如果文件名用引号引起来,则在源文件所在的位置查找该文件;如果在该位置没有找到文件,或者如果文件名是用尖括号<
与>
括起来的,则将根据相应的规则查找该文件,这个规则同具体的实现有关。被包含的文件本身也可包含#include
指令。
源文件的开始处通常都会有多个#include
指令,它们用以包含常见的#define
语句和extern
声明,或从头文件中访问库函数的函数原型声明。
在大的程序中,#include
指令是将所有声明捆绑在一起的较好的方法。它保证所有的源文件都具有相同的定义与变量声明,这样可以避免出现一些不必要的错误。很自然,如果某个包含文件的内容发生了变化,那么所有依赖于该包含文件的源文件都必须重新编译。
宏定义的形式如下:
#define 名字 替换文本
这是一种最简单的宏替换——后续所有出现名字记号的地方都将被替换为替换文本。#define
指令中的名字与变量名的命名方式相同,替换文本可以是任意字符串。通常情况下,#define
指令占一行,替换文本是#define
指令行尾部的所有剩余部分内容,但也可以把一个较长的宏定义分成若干行,这时需要在待续的行末尾加上一个反斜杠符\
。#define
指令定义的名字的作用域从其定义点开始,到被翻译的源文件的末尾处结束。宏定义中也可以使用前面出现的宏定义。替换只对记号进行,对括在引号中的字符串不起作用。
替换文本可以是任意的。
宏定义也可以带参数,这样可以对不同的宏调用使用不同的替换文本。使用宏看起来很像是函数调用,但宏调用直接将替换文本插入到代码中。形式参数的每次出现都将被替换成对应的实际参数。
如果对各种类型的参数的处理是一直的,则可以将同一个宏定义应用于任何数据类型,而无需针对不同的数据类型需要定义不同的函数。
…存在一些缺陷。其中,作为参数的表达式要重复计算两次,如果表达式存在副作用(比如含有自增运算符或输入/输出),则会出现不正确的情况。同时还必须注意,要适当使用圆括号以保证计算次序的正确性。
但是,宏还是很有价值的。…这样可以避免调用函数所需的运行时开销。
可以通过#undef
指令取消名字的宏定义。
形式参数不能用带引号的字符串替换。但是,如果在替换文本中,参数名以#
作为前缀则结果将被扩展为由实际参数替换成该参数的带引号的字符串。在实际参数中,每个双引号"
将被替换为\"
,反斜杠\
将被替换为\\
,因此替换后的字符串是合法的字符串常量。
预处理器运算符##
为宏扩展提供了一种连接实际参数的手段。如果替换文本中的参数与##
相邻,则该参数将被实际参数替换,##
与前后的空白符将被删除,并对替换后的结果重新扫描。
还可以使用条件语句对预处理本身进行控制,这种条件语句的值是在预处理执行的过程中进行计算。这种方式为在编译过程中根据计算所得的条件值选择性地包含不同代码提供了一种手段。
#if
语句对其中的常量整形表达式(其中不能包含sizeof
、类型转换运算符或enum
常量)进行求值,若该表达式的值不等于0,则包含其后的各行,直到遇到#endif
、#elif
或#else
语句为止。在#if
语句中可以使用表达式defined(名字)
,该表达式的值遵循下列规则:当名字已经定义时,其值为1;否则,其值为0。
C语言专门定义了两预处理语句#ifdef
与#ifndef
,它们用来测试某个名字是否已经定义。
程序语言中的控制流语句用于控制各计算操作执行的次序。
表达式之后加上一个分号(;
),它们就变成了语句。
在C语言中,分号是语句结束符。
用一对花括号“{
”与“}
”把一组声明和语句括在一起就构成了一个复合语句(也叫作程序块),复合语句在语法上等价于单条语句。函数体中被花括号括起来的语句便是明显一例。if
、else
、while
与for
之后被花括号括住的多条语句也是类似的例子。右花括号用于结束程序块,其后不需要分号。
if-else
语句if-else
语句用于条件判定。
1 | if (expression) |
其中else
部分是可选的。该语句执行时,先计算表达式的值,如果其值为真(即表达式的值为非0),则执行语句1;如果其值为假(即表达式的值为0),并且该语句包含else
部分,则执行语句2。
由于if
语句只是简单测试表达式的数值,因此可以对某些代码的编写进行简化。最明显的例子是用如下写法if (expression)
来代替if (expression != 0)
。某些情况下这种形式是自然清晰的,但也有某些情况下可能会含义不清。
因为if-else
语句的else
部分是可选的,所以在嵌套的if
语句中省略它的else
部分将导致歧义。解决的方法是将每个else
与最近的前一个没有else
配对的if
进行匹配。…如果这不符合我们的意图,则必须使用花括号强制实现正确的匹配关系。
…建议在有if
语句嵌套的情况下使用花括号。
…从语法上讲,跟在if
后面的应该是一条语句。
else-if
语句1 | if (expression) |
这种if
语句序列是编写多路判定最常用的方法。其中的各表达式将被依次求值,一旦某个表达式结果为真,则执行与之相关的语句,并终止整个语句序列的执行。同样,其中各语句既可以是单条语句,也可以是用花括号括住的复合语句。
最后一个else
部分用于处理“上述条件均不成立”的情况或默认情况,也就是当上面各条件都不满足时的情形。有时候并不需要针对默认情况执行显式的操作,这种情况下,可以把该结构末尾的else
部分省略掉;该部分也可以用来检查错误,以捕获“不可能”的条件。
switch
语句switch
语句是一种多路判定语句,它测试表达式是否与一些常量整数值中的某一个值匹配,并执行相应的分支动作。
1 | switch (expression) { |
每一个分支都由一个或多个整数值常量或常量表达式标记。如果某个分支与表达式的值匹配,则从该分支开始执行。各分支表达式必须互不相同。如果没有哪一分支能匹配表达式,则执行标记为default
的分支。default
分支是可选的。如果没有default
分支也没有其他分支与表达式的值匹配,则该switch
语句不执行任何动作。各分支及default
分支的排列次序是任意的。
break
语句将导致程序的执行立即从switch
语句中退出。在switch
语句中,case
的作用只是一个标号,因此,某个分支中的代码执行完后,程序将进入下一分支继续执行,除非在程序中显式地跳转。跳出switch
语句最常用的方法是使用break
语句与return
语句。
依次执行各分支的做法有优点也有缺点。好的一面是它可以把若干个分支组合在一起完成一个任务。但是,正常情况下为了防止直接进入下一个分支执行,每个分支后必须以一个break
语句结束。从一个分支直接进入下一个分支执行的做法并不健全,这样做在程序修改时很容易出错。除了一个计算需要多个标号的情况下,应尽量减少从一个分支直接进入下一个分支执行这种用法,在不得不使用的情况下应该加上适当的程序注释。
作为一种良好的程序设计风格,在switch
语句最后一个分支(即default
分支)的后面也加上一个break
语句。这样做在逻辑上没有必要,但当我们需要向该switch
语句后添加其他分支时,这种防范措施会降低犯错误的可能性。
while
循环与for
循环在while
循环语句
1 | while (expression) |
中,首先求表达式的值。如果其值为真非0,则执行语句,并再次求该表达式的值。这一循环过程一直进行下去,直到该表达式的值为假(0)为止,随后继续执行语句后面的部分。
for
循环语句:
1 | for (expression1; expression2; expression3) |
它等价于下列while
语句:
1 | expression1; |
但当while
或for
循环语句中包含continue
语句时,上述二者之间就不一定等价了。
从语法角度看,for
循环语句的3个组成部分都是表达式。最常见的情况是,表达式1与表达式3是赋值表达式或函数调用,表达式2是关系表达式。这3个组成部分中的任何部分都可以省略,但分号必须保留。如果在for
语句中省略表达式1与表达式3,它就退化成了while
循环语句。如果省略测试条件,即表达式2,则认为其值永远是真值,因此,下列for
循环语句:
1 | for (;;) { |
是一个“无限”循环语句,这种语句需要借助其他手段(如break
语句或return
语句)才能终止循环。
在设计程序时到底选用while
循环语句还是for
循环语句,主要取决于程序设计人员的个人偏好。…因为其中没有初始化或重新初始化的操作,所以使用while
循环语句更自然一些。
如果语句中需要执行简单的初始化和变量递增,使用for
语句更合适一些,它将循环控制语句集中放在循环的开头,结构更紧凑、更清晰。通过下列语句可以很明显地看出这一点:
1 | for (i = 0; i < n; i++) |
这是C语言处理数组前n个元素的一种习惯性用法。…在C语言中,for
循环语句的循环变量和上限在循环体内可以修改,并且当循环因某种原因终止后循环变量i的值任然保留。因为for
语句的各组成部分可以是任何表达式,所以for
语句并不限于通过算术级数进行循环控制。尽管如此,牵强地把一些无关的计算放到for
语句的初始化和变量递增部分是一种不好的程序设计风格,该部分放置循环控制运算更合适。
把循环控制部分集中在一起,对于多重嵌套循环,优势更为明显。…注意,即使最外层for
循环的控制变量不是算术级数,for
语句的书写形式任然没有变,这就说明for
语句具有很强的通用性。
逗号运算符“,
”也是C语言优先级最低的运算符,在for
语句中经常会用到它。被逗号分隔的一对表达式将按照从左到右的顺序进行求值,各表达式右边的操作数的类型和值即为其结果的类型和值。这样,在for
循环语句中,可以将多个表达式放在各个语句成分中,比如同时处理两个循环控制变量。
某些情况下的逗号并不是逗号运算符,比如分隔函数参数的逗号,分隔声明中变量的逗号等,这些逗号并不保证各表达式按从左至右的顺序求值。
应该慎用逗号运算符。逗号运算符最适用于关系紧密的结构中,…对于需要在单个表达式中进行多步计算的宏来说也很适合。…这样,元素的交换过程便可以看成是一个单步操作。
do-while
循环while
与for
这两种循环在循环体执行前对终止条件进行测试。与此相反,C语言中的第三种循环——do-while
循环则在循环体执行后测试终止条件,这样循环体至少被执行一次。
do-while
循环的语法形式如下:
1 | do |
在这一结构中,先执行循环体中的语句部分,然后再求表达式的值。如果表达式的值为真,则再次执行语句,依此类推。当表达式的值变为假,则循环终止。
经验表明,do-while
循环比while
循环和for
循环用得少得多。尽管如此,do-while
循环语句有时还是很有用的。
…其中的do-while
语句体中只有一条语句,尽管没有必要,但我们任然用花括号将该语句括起来了,这样做可以避免草率的读者将while
部分误认为是另一个while
循环的开始。
break
语句与continue
语句不通过循环头部或尾部的条件测试而跳出循环,有时是很方便的。break
语句可用于从for
、while
与do-while
等循环中提前退出,就如同从swtich
语句中提前退出一样。break
语句能使程序从switch
语句或最内层循环中立即跳出。
continue
语句与break
语句是相关联的,但它没有break
语句常用。continue
语句用于使for
、while
或do-while
语句开始下一次循环的执行。在while
与do-while
语句中,continue
语句的执行意味着立即执行测试部分;在for
循环中,则意味着使控制转移到递增循环变量部分。continue
语句只用于循环语句,不用于switch
语句。某个循环包含的switch
语句中的continue
语句,将导致进入下一次循环。
当循环的后面部分比较复杂时,常常会用到continue
语句。这种情况下,如果不适用continue
语句,则可能需要把测试颠倒过来或者缩进另一层循环,这样做会使程序的嵌套更深。
goto
语句与标号C语言提供了可随意滥用的goto
语句以及标记跳转位置的标号。从理论上讲,goto
语句是没有必要的,实践中不使用goto
语句也可以很容易地写出代码。
但是,在某些场合下goto
语句还是用得着的。最常见的用法是终止程序在某些深度嵌套的结构中的处理过程,例如一次跳出两层或多层循环。这种情况下使用break
语句是不能达到目的的,它只能从最内层循环退出到上一级的循环。下面是使用goto
语句的一个例子:
1 | for (...) |
在该例子中,如果错误处理代码很重要,并且错误可能出现在多个地方,使用goto
语句将会比较方便。
标号的命名同变量命名的形式相同,标号的后面要紧跟一个冒号。标号可以位于对应的goto
语句所在函数的任何语句的前面。标号的作用域是整个函数。
所有使用了goto
语句的程序代码都能改写成不带goto
语句的程序,但可能会增加一些额外的重复测试或变量。
大多数情况下,使用goto
语句的程序段比不适用goto
语句的程序段要难以理解和维护,少数情况除外。尽管该问题并不太严重,但我们还是建议尽可能少地使用goto
语句。
变量和常量是程序处理的两种基本数据对象。声明语句说明变量的名字和类型,也可以指定变量的初值。运算符指定将要进行的操作。表达式则把变量和常量组合起来生成新的值。对象的类型决定该对象可取值的集合以及可以对该对象执行的操作。
ANSI标准对语言的基本类型与表达式做了许多小的修改与增补。所有整型都包括signed
(带符号)和unsigned
(无符号)两种形式,且可以表示无符号常量与十六进制字符常量。浮点运算可以单精度进行,还可以使用更高精度的long double
类型运算。字符串常量可以在编译时连接。ANSI C还支持枚举类型,该语言特性经过了长期的发展才形成。对象可以声明为const
(常量)类型,表明其值不能修改。该标准还对算术类型之间的自动强制转换规则进行了扩充,以适合于更多的数据类型。
对变量的命名与符号常量的命名存在一些限制条件。
名字是由字母和数字组成的序列,但其第一个字符必须为字母。
下划线'_'
被看做是字母,通常用于命名较长的变量名,以提高其可读性。
由于库例程的名字通常以下划线开头,因此变量名不要以下划线开头。
大写字母与小写字母是有区别的。
在传统的C语言用法中,变量名使用小写字母,符号常量名全部使用大写字母。
对于内部名(函数内部变量的名字)而言,至少前31个字符是有效的。
函数名与外部变量名包含的字符数目可能小于31,这是因为汇编程序和加载程序可能会使用这些外部名,而语言本身是无法控制加载和汇编程序的。(别起那么长的名字)
对于外部名,ANSI标准仅保证前6个字符的唯一性,并且不区分大小写。
类似于if
、else
、int
、float
等关键字是保留给语言本身使用的,不能把它们用作变量名。
所有关键字中的字符都必须小写。
选择的变量名要能够尽量从字面上表达变量的用途,这样做不容易引起混淆。
局部变量一般使用较短的变量名(尤其是循环控制变量),外部变量使用较长的名字。
基本数据类型:
char
字符型,占用一个字节,可以存放本地字符集中的一个字符int
整型,通常反映了所用机器中整数的最自然长度float
单精度浮点型double
双精度浮点型…限定符。short
与long
用于限定整型(int
可以省略,习惯这么做)。
int
通常代表特定机器中整数的自然长度。short
类型通常为16位,long
类型通常为32位,int
类型可以为16位活32位。
各编译器可以根据硬件特性自主选择合适的类型长度,但要遵循下列限制:short
与int
类型至少为16位,而long
类型至少为32位,并且short
类型不得长于int
类型,而int
类型不得长于long
类型。
类型限定符signed
与unsigned
可用于限定char
类型或任何整型。
unsigned
类型的数总是正值或0,并遵守算术模2^n
定律。
signed
类型变量的取值范围和对二的补码的机器有关。
不带限定符的char
类型对象是否带符号则取决于具体机器,但可打印字符总是正值。
long double
类型表示高精度的浮点数。
浮点数的长度也取决于具体的实现,float
、double
与long double
类型可以表示相同的长度,也可以表示两种或三种不同的长度。
有关这些类型长度定义的符号常量以及其他与机器和编译器有关的属性可以在标准头文件<limits.h>
与<float.h>
中找到。
类似于1234的整数常量属于int
类型。
long
类型的常量以字面l
或L
结尾。
如果一个整数太大以至于无法用int
类型表示时,也将被当作long
类型处理。
无符号常量以字面u
或U
结尾。
后缀ul
或UL
表明是unsigned long
类型。
浮点数常量中包含一个小数点(如123.4)或一个指数(如1e-2),也可以两者都有。
没有后缀的浮点数常量为double
类型。
后缀f
或F
表示float
类型,而后缀l
或L
则表示long double
类型。
整型数除了用十进制表示外,还可以用八进制或十六进制表示。
带前缀0
的整型常量表示它为八进制形式;前缀为0x
或0X
,则表示它为十六进制形式。
八进制与十六进制的常量也可以使用后缀L
表示long
类型,使用后缀U
表示unsigned
类型。
一个字符常量是一个整数,书写时将一个字符括在单引号中。
字符在机器字符集中的数值就是字符常量的值。
如果使用字符代替这个与具体字符集有关的值,那么,程序就无需关心改字符对应的具体值,增加了程序的易读性。
字符常量一般用来与其他字符进行比较,但也可以像其他整数一样参与数值运算。
某些字符可以通过转义字符序列表示为字符和字符串常量。
转义字符序列看起来像两个字符,但只表示一个字符。
可以用'\ooo'
表示任意的字节大小的位模式。其中,ooo
代表1~3个八进制数字。
这种位模式还可以用'\xhh'
表示,其中,hh
是一个或多个十六进制数字。
\a
响铃符\b
回退符\f
换页符\n
换行符\r
回车符\t
横向制表符\v
纵向制表符\\
反斜杠\?
问号\'
单引号\"
双引号\ooo
八进制数\xhh
十六进制数字符常量'\0'
表示值为0的字符,也就是空字符(null
)。通常用'\0'
的形式代替0,以强调某些表达式的字符属性,但其数字值为0。
常量表达式是仅仅只包含常量的表达式。这种表达式在编译时求值,而不在运行时求值。它可以出现在常量可以出现的任何位置。
字符串常量也叫字符串字面值,是用双引号括起来的0个或多个字符组成的字符序列。
双引号不是字符串的一部分,它只用于限定字符串。
字符常量中使用的转义字符序列同样也可以用在字符串中。
在字符串中使用\"
表示双引号字符。
编译时可以将多个字符串常量连接起来。字符串常量的连接为将较长的字符串分散在若干个源文件行中提供了支持。
从技术角度看,字符串常量就是字符数组。字符串的内部表示使用一个空字符\0
作为串的结尾,因此,存储字符串的物理存储单元数比括在双引号中的字符数多一个。这种表示方法也说明,C语言对字符串的长度没有限制,但程序必须扫描完整个字符串后才能确定字符串的长度。标准库函数strlen(s)
可以返回字符串参数s的长度,但长度不包括末尾的\0
。
标准头文件<string.h>
中声明了strlen
和其他字符串函数。
枚举常量是另外一种类型的常量。枚举是一个整型常量值的列表。
在没有显式说明的情况下,enum
类型中第一个枚举名的值为0,第二个为1,依次类推。如果只指定了部分枚举名的值,那么未指定值的枚举名的值将依着最后一个指定值向后递增。
不同枚举中的名字必须互不相同。同一枚举中不同的名字可以具有相同的值。
枚举为建立常量值与名字之间的关联提供了一种便利的方式。相对于#define
语句来说,它的优势在于常量值可以自动生成。尽管可以声明enum
类型的变量,但编译器不需要检查这种类型的变量中存储的值是否为该枚举的有效值。不过,枚举变量提供了这种检查的机会,因此枚举比#define
更具优势。此外,调试程序可以以符号形式打印出枚举变量的值。
所有变量都必须先声明后使用,尽管某些变量可以通过上下文隐式地声明。一个声明指定一种变量类型,后面所带的变量表可以包含一个或多个该类型的变量。
一个声明语句中的多个变量可以拆开在多个声明语句中声明。按照这种形式书写代码需要占用较多的空间,但便于向各声明语句中添加注释,也便于以后修改。
还可以在声明的同时对变量进行初始化。在声明中,如果变量名的后面紧跟一个等号以及一个表达式,该表达式就充当对变量进行初始化的初始化表达式。
如果变量不是自动变量,则只能进行一次初始化操作,从概念上讲,应该是在程序开始执行之前进行,并且初始化表达式必须为常量表达式。
每次进入函数或程序块时,显式初始化的自动变量都将被初始化一次,其初始化表达式可以是任何表达式。
默认情况下,外部变量与静态变量将被初始化为0.
未经显式初始化的自动变量的值为未定义值(即无效值)。
任何变量的声明都可以使用const
限定符限定。该限定符指定变量的值不能被修改。对数组而言,const
限定符指定数组所有元素的值都不能被修改。
const
限定符也可配合数组参数使用,它表明函数不能修改数组元素的值。
如果试图修改const
限定符限定的值,其结果取决于具体的实现。
二元算术运算符包括:+
、-
、*
、/
、%
(取模运算符)。
整数除法会截断结果中的小数部分。
取模运算符%
不能应用于float
或double
类型。
在有负操作数的情况下,整数除法截取的方向以及取模运算结果的符号取决于具体机器的实现,这和处理上溢或下溢的情况是一样的。
二元运算符+
和-
具有相同的优先级,它们的优先级比运算符*
、/
和%
的优先级低,而运算符*
、/
和%
的优先级又比一元运算符+
和-
的优先级低。
算术运算符采用从左到右的结合规律。
关系运算符包括下列几个运算符:>
>=
<
<=
。它们具有相同的优先级。优先级仅次于它们的是相等性运算符:==
!=
。
关系运算符的优先级比算术运算符低。
逻辑运算符&&
与||
有一些较为特殊的属性。由&&
与||
连接的表达式按从左到右的顺序进行求值,并且,在知道结果值为真或假后立即停止计算。绝大多数C语言程序运用了这些属性。
运算符&&
的优先级比||
的优先级高,但两者都比关系运算符和相等性运算符的优先级低。
运算符!=
的优先级高于赋值运算符的优先级(可以使用圆括号达到预期的目的)。
根据定义,在关系表达式或逻辑表达式中,如果关系为真,则表达式的结果值为数值1;如果为假,则结果值为数值0.
逻辑非运算符!
的作用是将非0操作数转换为0,将操作数0转换为1。
…读起来更直观一些…可能会难于理解。
当一个运算符的几个操作数类型不同时,就需要通过一些规则把它们转换为某种共同的类型。
一般来说,自动转换是指把“比较窄的”操作数转换为“比较宽的”操作数,并且不丢失信息的转换。
不允许使用无意义的表达式,例如,不允许把float
类型的表达式作为下标。
针对可能导致信息丢失的表达式,编译器可能会给出警告信息,比如把较长的整型值赋给较短的整型变量,把浮点型值赋值给整型变量,等等,但这些表达式并不非法。
由于char
类型就是较小的整型,因此在算术表达式中可以自由使用char
类型的变量,这就为实现某些字符转换提供了很大的灵活性。
在ASCII字符集中,大写字母与对应的小写字母作为数字值来说具有固定的间隔,并且每个字母表都是连续的——也就是说,在A~Z之间只有字母。但是,后面一点对EBCDIC字符集是不成立的。
标准头文件<ctype.h>
定义了一组与字符集无关的测试和转换函数。
将字符类型转换为整型时,我们需要注意一点。C语言没有指定char
类型的变量是无符号变量(signed
)还是带符号变量(unsigned
)。当把一个char
类型的值转换为int
类型的值时,其结果有没有可能为负整数?对于不同的机器,其结果也不同,这反映了不同机器结构之间的区别。在某些机器中,如果char
类型值的最左一位为1,则转换为负整数(进行符号扩展)。而在另一些机器中,把char
类型值转换为int
类型时,在char
类型值的左边添加0,这样导致的转换结果值总是正值。
C语言的定义保证了机器的标准打印字符集中的字符不会是负值,因此,在表达式中这些字符总是正值。但是,存储在字符变量中的位模式在某些机器中可能是负的,而在另一些机器上可能是正的。为了保证程序的可移植性,如果要在char
类型的变量中存储非字符数据,最好指定signed
或unsigned
限定符。
当关系表达式以及由&&
、||
连接的逻辑表达式的判断结果为真时,表达式的值为1;当判定结果为假时,表达式的值为0.
某些函数在结果为真时可能返回任意的非0值。在if
、while
、for
等语句的测试部分中,“真”就意味着“非0”,这二者之间没有区别。
C语言中,很多情况下会进行隐式的算是类型转换。一般来说,如果二元运算符的两个操作数具有不同的类型,那么在进行运算之前先要把“较低”的类型提升为“较高”的类型。运算的结果为较高的类型。
表达式中float
类型的操作数不会自动转换为double
类型,这一点与最初的定义有所不同。一般来说,数学函数使用双精度类型的变量。使用float
类型主要是为了在使用较大的数组时节省存储空间,有时也为了节省机器执行时间(双精度算术运算特别费时)。
赋值时也要进行类型转换。赋值运算符右边的值需要转换为左边变量的类型,左边变量的类型即赋值表达式结果的类型。
无论是否进行符号扩展,字符型变量都将被转换为整型变量。
当把较长的整数转换为较短的整数或char
类型时,超出的高位部分将被丢弃。
当把float
类型转换为int
类型时,小数部分将被截取掉;当把double
类型转换为float
类型时,是进行四舍五入还是截取取决于具体的实现。
由于函数调用的参数是表达式,所以在把参数传递给函数时也可能进行类型转换。在没有函数原型的情况下,char
与short
类型都将被转换为int
类型,float
类型将被转换为double
类型。因此,即使调用函数的参数为char
或float
类型,我们也把函数参数声明为int
或double
类型。
在任何表达式中都可以使用一个称为强制类型转换的一元运算符进行显式类型转换。
C语言提供了两个用于变量递增与递减的特殊运算符。自增运算符++
使其操作数递增1,自减运算符--
使其操作数递减1.
++
与--
这两个运算符特殊的地方主要表现在:他们既可以用作前缀运算符(用在变量前面),也可以用作后缀运算符(用在变量后面)。在这两种情况下,其效果都是将变量的值加1。但是,它们之间有点不同。表达式++n
先将n的值递增1,然后再使用变量n的值,而表达式n++
则是先使用变量n的值,然后再将n的值递增1。也就是说,对于使用变量n的值的上下文来说,++n
和n++
的效果是不同的。
自增与自减运算符只能作用于变量。
在不需要使用任何具体值且仅需要递增变量的情况下,前缀方式后后缀方式的效果相同。但在某些情况下需要酌情考虑。
C语言提供了6个位操作运算符。这些运算符只能作用于整型操作数,即只能作用于带符号或无符号的char
、short
、int
与long
类型:
&
按位与(AND)|
按位或(OR)^
按位异或(XOR)<<
左移>>
右移~
按位求反(一元操作符)按位与运算符&
经常用于屏蔽某些二进制位。
按位或运算符|
常用于将某些二进制位置为1.
按异或运算符^
当两个操作数的对应位不相同时将该位设置为1,否则,将该位设置为0。
我们必须将位运算符&
、|
同逻辑运算符&&
、||
区分开来,后者用于从左至右求表达式的真值。
移位运算符<<
与>>
分别用于将运算的左操作数左移与右移,移动的位数则由右操作数指定(右操作数的值必须是非负值)。
…左移,右边空出的位用0填补。
在对unsigned
类型的无符号值进行右移位时,左边空出的部分将用0填补;当对signed
类型的带符号值进行右移时,某些机器将对左边空出的部分用符号位填补(即“算术移位”),而另一些机器则对左边空出的部分用0填补(即“逻辑移位”)。
一元运算符~
用于求整数的二进制反码,即分别将操作数各二进制位上的1变为0,0变为1。
…x & ~077
与机器字长无关,它比形式为x & 0177700
的表达式要好,因为后者假定x是16位的数值。这种可移植的形式并没有增加额外开销,因为~077
是常量表达式,可以在编译时求值。
在赋值表达式中,如果表达式左边的变量重复出现在表达式的右边,则可以将这种表达式缩写。其中的运算符+=
称为赋值运算符。
大多数二元运算符(即有左、右两个操作数的运算符)都有一个对应的赋值运算符*op=,其中,op=*可以是下面这些运算符之一:
+
-
*
/
%
<<
>>
&
^
|
如果expr1和expr2是表达式,那么expr1 op= expr2
等价于expr1 = (expr1) op (expr2)
。它们的区别在于,前一种形式expr1只计算一次。注意,在第二种形式中,expr2两边的圆括号是必不可少的。
除了简洁外,赋值运算符还有一个优点:表示方式与人们的思维习惯比较接近。另外,对于复杂的表达式,赋值运算符使程序代码更易于理解。并且,赋值运算符还有助于编译器产生高效代码。
赋值语句具有值,且可以用在表达式中。其他赋值运算符也可以用在表达式中,尽管这种用法比较少见。在所有的这类表达式中,赋值表达式的类型是它的左操作数的类型,其值是赋值操作完成后的值。
条件表达式(使用三元运算符?:
)提供了另外一种方法…。在表达式expr1 ? expr2 : expr3
中,首先计算expr1,如果其值不等于0(为真),则计算expr2的值,并以该值作为条件表达式的值,否则计算expr3的值,并以该值作为条件表达式的值。expr2与expr3中只能有一个表达式被计算。
应该注意,条件表达式实际上就是一种表达式,它可以用在其他表达式可以使用的任何地方。如果expr2与expr3的类型不同,结果的类型将由本章前面讨论的转换规则决定。
条件表达式中第一个表达式两边的圆括号并不是必须的,这是因为条件运算符?:
的优先级非常低,仅高于赋值运算符。但我们还是建议使用圆括号,因为这可以使表达式的条件部分更易阅读。
采用条件表达式可以编写出很简洁的代码。编写这样的代码可能需要一些技巧,但比用等价的if-else
结构编写的代码要紧凑一些。
C语言没有指定同一运算符中多个操作数的计算顺序。为了保证特定的计算顺序,可以把中间结果保存在临时变量中。
C语言也没有指定函数各参数的求值顺序。
函数调用、嵌套赋值语句、自增与自减运算符都有可能产生“副作用”——在对表达式求值的同时,修改了某些变量的值。在有副作用影响的表达式中,其执行结果同表达式中的变量被修改的顺序之间存在着微妙的依赖关系。对这种情况编译器的解释可能不同,并因此产生不同的结果。C语言标准对大多数这类问题有意未作具体规定。表达式何时会产生这种副作用(对变量赋值),将由编译器决定,因为最佳的求值顺序同机器结构有很大关系。(ANSI C标准明确规定了所有对参数的副作用都必须在函数调用之前生效。)
在任何一种编程语言中,如果代码的执行结果与求值顺序相关,则都是不好的程序设计风格。很自然,有必要了解哪些问题需要避免,但是,如果不知道这些问题在各种机器上是如何解决的,就最好不要尝试运用某种特殊的实现方式。