
前言
承接上一篇PartⅠ,包括内容常用语法的补充、 I/O与文件输入输出、函数与指针、数组与指针。
2. 常用语法补充
有这一标签是因为再看《C Primer Plus》过程中发现有很多小细节还是没有注意到甚至不知道的,因此作为补充。
2.1 printf()中的*与scanf()中的 *
*放在%与转换字符间,使得printf()格式化的打印信息。类似的:
printf("Weight = %*.*f\n", width, precision, weight);
其中**分别将参数格式化信息直接运用在函数中,就是表示字段宽度以及精确度的关系。
而在scanf()中,把*放在%与转换字符间,使得scanf()跳过相应的输出项。类似的:
scanf("%*d %*d %d", &n);
在控制该函数时,就会自动跳过前面输入的两个int型。
2.2 for循环中的逗号运算符
逗号运算符扩展了for循环的灵活性。以便循环头中包含更多的表的式。
for(ounces = 1, cost = FIRST_OZ; ounces <= 16; ounces++, cost += NEXT_OZ)
在初始化表达式中逗号将ounce 和cost都进行了初始化。 更新表达式中的逗号使得每次迭代ounces递增1,而cost递增20。
2.3 while中的表达式风格
在while的判断表达式,是程序员告诉编译器是否跳过循环的标志。下面有这样一段代码:
1 |
|
这段代码的示例结果如下:
CALL ME HAL.
DBMM NF IBM/
尽管这样的C语言风格已经足够简洁可读,实际上依然能对while的表达式进行合并实现更加具有C风格的代码实现。如下:
1 | while((ch = getchar()) != '\n') |
对ch = getchar()外的圆括号的作用是,在执行判断之前就将ch进行赋值,不然可能出现编译器先执行判断再执行赋值导致程序出错。
2.4 ctype.h系列的字符函数
在上述的代码中,本应是.的字符被识别成了/而输出,这是因为在ASCII中点号的ASCII码刚好比斜杠的少1。怎么达到只让程序转换字母,保留所有非字母数字呢?
在库函数ctype.h中包含这些函数的原型,接受一个字符作为参数,如果该字符属于某种特殊类别,就返回非零值(真);否则返回0(假)。如:isalpha()
函数,用以判断参数字母,则返回非零值。
1 |
|
这样对于大小写字母就会进行ASCII的加一运算,而符号就会得到保留。
同样的库中包含如下常用字符测试函数:

2.5 else与if的配对
直观上讲,对于以下代码:
1 | if(num > 6) |
往往会认为else配对于与第一个if,但实际上在没有花括号做代码块的区分是,else会与离它最近的if匹配。原因是编译器是对缩进忽略的。
2.6 逻辑运算符的优先级与求值顺序
!运算符的优先级很高,比乘法运算符还高,与递增运算符相同,只比圆括号优先级低。&&运算符的优先级比||运算符高,但二者都比关系运算符低,比赋值运算符高。
C保证了逻辑运算符的求值顺序是从左往右的。&&与||运算符都是序列点,所以程序从一个运算对象执行到下一个运算对象之前,所有的副作用都会生效。
2.7 ANSI C类型限定符
const
const
:表示变量的值不可被修改。在变量类型前加上const关键字,可以使该变量变为只读。
volatile
volatile
:表示变量的值可能会被随时修改,从而防止编译器优化掉该变量的访问操作。
restrict
restrict
:表示指针所指向的内存区域是独占的,没有其他指针指向同一内存区域。
只能用于指针。
_Atomic(C11)
_Atomic
:表示变量是原子类型,支持多线程、并发编程中的原子操作。
3. I/O与文件
3.1 单字符I/O:getchar()和putchar()与缓冲区
1 | /* echo.c -- 重复输入 */ |
对于这段代码示例:
Hello, there. I would[enter]
Hello, there. I would
like a #3 bag of potatoes.[enter]
like a
为什么会出现这种情况呢,输入的字符直接显示出来,并且#字符也不停止直接打印而是待用户输入[enter]后输出至#前。
这是由于是否用到了缓冲区的概念。
如果是无缓冲区输入,所见即所得,会出现例如HHEELLLLOO的抽象情况,这不符合程序员逻辑但是符合无缓冲区的输入,像游戏的即时反馈就需要这种无缓冲区的输入输出。

为什么需要用到缓冲区?
首先,把若干字符作为一个块进行传输比逐个发送字符节约时间,其次,如果用户打错字符,可以通过键盘修正。当最后按下Enter后,传输的是正确的输入。
不仅如此,缓冲也分为:完全缓冲I/O
和行缓冲I/O
。
完全缓冲是指在当缓冲区被填满时才刷新缓冲区(内容被发送到至目的地),通常出现在文件输入中。缓冲区大小取决于系统,常见的是512字节和4096字节。
行缓冲指的是出现换行符时刷新缓冲区。键盘输入通常是行缓冲输入,所以按下Enter建后才会刷新缓冲区。
计算机的不同,会导致编译器提供不同的输入选项。ANSI C决定把缓冲输入作为标准的原因:一些计算机不支持无缓冲输入,如果有些计算机允许无缓冲输入,那么C编译器很可能会提供无缓冲输入的选项。
例如在IBM PC兼容机,其原型在conio.h
的头文件中。包含用于回显无缓冲输入的getche()
以及用于无回显无缓冲输入的getch()
函数。
而,UNIX系统使用了另一种不同的方式控制缓冲。可以使用ioctl()
函数,其输入UNIX库而非C标准,指定待输入的类型,然后用getchar()
执行相应的操作。
在ANSI C中,用setbuf()
和setvbuf()
。
其中,int setvbuf(FILE * restrict fp, char * restrict buf, int mode, size_t size);
该函数创建了一个供标准I/O函数替换使用的缓冲区。用以为fd文件流提供缓冲区。
- stream:指向文件的指针,即需要设置缓冲区的文件。
- buffer:指向缓冲区的指针,如果该指针为NULL,则setvbuf()函数会自动分配缓冲区。
- mode:指定缓冲区的类型,可以取三个值中的一个:_IONBF、_IOLBF或者_IOFBF。
- _IONBF:无缓冲区,直接输出到文件。
- _IOLBF:行缓冲区,当输出换行符’\n’时才会将缓冲区中的数据输出到文件,否则数据将留在缓冲区中。
- _IOFBF:全缓冲区,当缓冲区满时才会将数据输出到文件。
- size:指定缓冲区大小,如果设置缓冲区类型为无缓冲区,则该参数被忽略。
size_t是无符号整数类型,通常表示内存大大小、数组长度等与内存相关的值。
setvbuf()函数会返回一个非零值表示设置失败,返回零表示设置成功。一般来说,只要不是在非常特殊的情况下,setvbuf()函数的返回值都应该是零。
3.2 文件结尾
对与曾经的系统会为文件内嵌Ctrl+Z来标记文件结尾。另一种方法便是存储文件大小,倘若有3000字节,当程序读到3000字节时,便达到了文件的末尾。
无论操作系统用何种方式,C语言中getchar()读取文件来检测文件结尾时将返回一个特殊的值,即EOF(end of file)。scanf()函数检测到文件结尾时也会返回EOF。
对于EOF在stdio.h中的定义是这样的:
#define EOF (-1)
因为-1在getchar()函数的返回值中是不会出现的(通常在0-127,对应标准字符集,即便系统能识别扩展字符集,该函数的返回值也可能在0-255)。
怎么使用EOF?
1 | /* echo_eof.c -- 重复输入,直到文件末尾*/ |
将getchar()的返回值同EOF比较。注意以下几点:
不用定义EOF,在stdio.h中已经定义过了。
不用担心EOF的实际值,因为在定义中是使用预处理指令定义,可直接使用,不必再编写代码假定EOF为某值。
变量ch的类型从char变为int,因为char的变量只能表示0-255的无符号整数,但EOF的值为-1。还好,getchar()函数返回的类型是int,所以可以读取到EOF字符。
使用该程序进行键盘输入,要设法输入EOF,尽管对于我们EOF是-1是字符,但是系统的要求不同EOF在文件中的形式不同,比如在UNIX以及Linux中,在一行开始处按下Ctrl+D会传输文件结尾的信号。许多微型计算机系统把一行开始处的Ctrl+D视为文件结尾信号,一些系统把任意位置的Ctrl+D解释成结尾文件。而在WIindows中这个字符是连续的Ctrl+Z。
3.3 重定向与文件
重定向是指将文件内容当作键盘输入喂给程序,或者文本内容代替键盘输入至程序。
重定向输入
echo_eof < words
‘<’符号是UNIX和DOS/Windows的重定向运算符。该运算符使得words文件与stdin流相关联,使得文件中的内容导入到echo_eof程序。程序本身不知情输入的内容是来自键盘还是文本文件。
重定向输出
假设要用echo_eof把键盘输入的内容发送到名为mywords的文件中。使用
echo_eof > mywords
‘>’符号是第二个重定向运算符。它创建一个名为mywords的新文件,然后把echo_eof的输出重定向至该文件。 倘若有该文件通常会擦除并替换内容。
组合重定向
倘若我希望制作一份mywords文件的副本,并命名为savewords。
echo_eof < mywords > savewords
如下的命令同样起作用
echo_eof > savewords < mywords
3.4 文件输入/输出
在上面提到了重定向,但是该方法对于程序需要对文件进行交互的时候有很大的限制。例如:假设需要编写一个交互程序,询问用户书名并完整的书名列表保存在文件中。如果使用重定向,应该类似于:
books > bklist
用户的输入被重定向至bklist。这样做不仅会把不符合要求的文本写入,而且用户也看不到要回答什么问题,简单来说就是整个交互都是单向的,要么输入要么输出。
在C语言中,C把文件看作一系列连续的字节,每个字节都能够被单独读取。这与UNIX环境中的文件结构相对应,由于其他环境可能无法对应这个模型,C提供了两种文件模式:文本模式与二进制模式。
文本模式与二进制模式
所有的文件内容都以二进制形式(0或1)存储。但是,如果文件最初使用二进制编码的字符(如ASCII Unicode)表示文本,该文件就是文本文件,其中包含文本内容。
如果文件中的二进制值代表机器语言代码或数值数据(使用相同的内部表示,假设,用于long或double类型的值)或图片或音乐编码,该文件就是二进制文件,其中包含二进制内容。
由于不同的操作系统可能对文本中格式不同,比如之前提到过的换行符。因此C语言为了规范文本文件的处理,提供了两种访问文件的途径:二进制模式和文本模式。
二进制模式中,程序可以访问文件的每个字节。
而在文本模式中,程序所见的内容和文件的实际内容不同。当程序以文本模式读取文件时,把本地环境表示的行末尾或文件结尾映射为C模式。
当以二进制模式读写文本文件,如果读写一个旧时MS-DOS文本文件,程序会看到文件中的/r和/n字符,不会发生映射。如果要编写旧式Mac格式、MS-DOS格式或UNIX/Linux格式的文件模式程序,应该使用二进制模式,这样程序才能确定实际的文件内容并执行相应的动作。

I/O的级别与标准文件
处理文件访问时有两个级别:
底层I/O,使用操作系统提供的基本I/O服务。
标准高级I/O使用C库的标准包和stdio.h头文件定义。
因为无法保证所有的操作系统都使用相同的底层I/O模型,C标准只支持标准I/O包。有些C实现会提供底层库,但C标准建立了可移植的I/O模型。
C程序会自动打开3个文件,分别是标准输入、标准输出和标准错误输出。标准输入是指系统的普通输入设备,通常为键盘;标准输出和标准错误输出是系统的普通输出设备,通常为显示屏。
通常标准输入为程序提供输入,它是getchar()和scanf()使用的文件。
程序通常输出到标准输出,它是putchar()、puts()和printf()使用的文件。
标准错误输出提供了一个逻辑上不同的地方来发送消息。例如:如果使用重定向把输出发送给文件而不是屏幕,那么发送至标准错误输出的内容仍然会被发送发到屏幕,不用仅限于打开文件才能看到错误消息。
stdio.h
头文件将3个文件指针和3个标准文件相关联,C程序会自动打开这3个文件。

这些文件都指向FILE的指针,所以他们可用作标准I/O的参数。如fclose(fp)中的fp。
fopen()函数
该函数用以打开一个文件流,声明在<stdio.h>
中。
参数1: 待打开文件的名称,更准确的说是一个包含该文件名的字符串地址
参数2:为一个字符串,指定待打开文件的模式,如下:

需要注意的是,C11新增带x字母的写模式,该模式允许在打开文件失败时,也不会对原内容进行修改;其次,如果环境允许,x模式的独占特性使得其他程序或线程无法访问正在打开的文件。
程序完成打开文件后,fopen()讲返回文件指针,其他I/O函数可以使用该指针指定该文件。文件指针的类型是指向FILE的指针,FILE是一个定义在stdio.h中的派生类型。文件指针fp本身并不指向实际的文件,而是一个包含文件信息的数据对象,其中包含错做文件的I/OI函数所用的缓冲区信息。
getc()和putc()函数
这两个函数类似于getchar()和putchar()函数,但有所不同的是,对于前者需要告诉它们使用哪个文件。如下:
ch = getchar();
表示从标准输入获取一个字符。
ch = getc(fp);
表示从fp指定的文件中获取一个字符。
同样的,
putc(ch, fpout);
表示把一个字符ch放入FILE指针fpout指定的文件中。
当fpout的参数改成stdout
其作用就与putchar(ch)
一样,均表示讲字符ch标准输出。
文件结尾与fclose()函数
如果getc()函数读取一个字符时发现是文件结尾,它将返回一个特殊值EOF。所以只有C程序读到超过文件末尾时才会大仙文件结尾。
为避免读到空文件,应当使用入口条件循环进行文件输入。鉴于getc()设计,如下:
1 | //范例1 |
同样可以简化为:
1 | int ch; |
fclose(fp)
函数关闭fp指定的文件,必要时刷新缓冲区。对于较正式的程序,应该检查是否成功关闭文件。
if (fclose(fp) != 0)
倘若硬盘已满、移动硬盘被移除或I/O出现错误,都会导致fclose()函数失败。
文件I/O:函数介绍
fprintf()和fscanf()函数
其工作方式类似于printf()和scanf(),区别在于该函数需要用第1个参数指定待处理的文件。
rewind()
函数,让程序回到文件的开始处,方便while循环打印整个文件的内容。同样以文件指针作为参数。
rewind(fp);
fgets()和fputs()函数
一样的,对比gets()和puts()函数,第1个参数表示输入位置的地址(char *类型);第2个参数是一个整数,表示待输入字符串的大小;最后一个只参数是文件指针,指定待读取的文件;
fgets(buf, STLEN, fp);
buf表示char类型数组的名称,STLEN表示字符串大小,fp指向FILE的指针。
fgets()
函数读取输入直到第一个换行符的后面,或者读到文件结尾,或者读取STLEN-1个字符。
之后,fgets()在末尾添加一个空字符使之成为一个字符串。字符串的大小是其字符数加上一个空字符。
如果函数在读到字符上限之前已经读完一行,它会把表示行结尾的换行符放在空字符前面。
同样的fgets()在遇到EOF时返回NULL值,因此可以通过这样一机制检查是否到达文件结尾;
fputs()
函数接受两个参数:第1个是字符串地址;第2个是文件指针。该函数根据传入地址找到字符串写入指定的文件中。
和puts()函数不同的是,fputs()
在打印字符串时不会再末尾添加换行符。
随机访问:fseek()和ftell()
在fseek()
函数中,文件被看作数组,在fopen()打开的文件中直接移动到任意字节处。
其中fseek()
有3个参数,返回int类型的值;ftell()
函数返回一个long类型的值,表示文件中的当前位置。
fseek(fp, 10L, SEEK_SET);
表示定位至文件中的第10个字节。
参数1:FILE指针,指向待查找的文件,并且保证fopen()已经打开该文件。
参数2:偏移量,该参数表示从起始点开始要移动的距离,可以是正或负。
参数3: 模式,该参数确定起始点。
模式 | 偏移量的起始点 |
---|---|
SEEK_SET | 文件开始处 |
SEEK_CUR | 当前位置 |
SEEK_END | 文件末尾 |
1 | fseek(fp, 0L, SEEK_END); |
表示把当前位置设置为距文件末尾0字节偏移量。而下一句,
把从文件开始处到文件末尾的字节数赋给last。
对于文本模式,ANSI C规定,ftell()返回的值可以作为fseek()的第2个参数。
可移植性
理论上,fseek()
和ftell()
应该符合UNIX模型,但是不同系统存在着明显的差异,有时无法做到和UNIX模型一致,因此,ANSI对这些函数降低了要求
在二进制模式中,实现不必支持SEEK_END模式。移植性更高的方法是逐字节读取整个文件直至文件末尾。
文本模式中,只有以下调用能保证其相应的行为。
函数调用 | 效果 |
---|---|
fseek(file, 0L, SEEK_SET) | 定位至文件开始处 |
fseek(file, 0L, SEEK_CUR) | 保持当前位置不变 |
fseek(file, 0L, SEEK_END) | 定位至文件结尾 |
fseek(file, ftell-pos, SEEK_SET) | 到据文件开始处ftell-pos的位置,ftell-pos是ftell()返回值 |
fgetpos()和fsetpos()函数
上述的两个函数有一个潜在的问题,就是随着存储设备的容量增大,使用long类型已经不能满足检索的需求,因此:
int fgetpos(FILE * restrict stream, fpos_t * restrict post);
表示fpos_t类型的值放在pos指向的位置上,该值描述了文件中的一个位置。如果成功返回0;如果失败返回非0;
int fsetpos(FILE * stream, const fpos_t *pos);
表示从pos指向位置上的fpos_t类型值来设置文件指针指向该值指定的位置。如果成功返回0;反之,返回非0。fpos_t类型的值应通过fgetpos()获得。
文件I/O的机理
介绍了这么多函数,那么文件I/O究竟是做了什么事情,它的原理又是什么。
首先,fopen()
打开了一个文件,不仅如此,同时创建了一个缓冲区以及一个包含文件和缓冲区数据的结构。返回的指针,以便其他函数方便找到该结构。假设该指针为fp;
其次,打开的流分为两种,1. 文本模式打开,获得文本流; 2. 二进制模式打开,获得二进制流。
使用标准I/O调用定义在<stdio.h>中的输入函数,如fscanf(),getc()或fgets()
。将文件中的数据块拷贝到缓冲区,缓冲区大小因C实现而已,一般是512字节或它的倍数。
有了缓冲区,有了流,再需要一个文件位置指示器。我们就实现了I/O对文件的控制。
其他标准I/O函数
见《C Primer Plus》第427页,不再过多赘述函数上的使用。
4. 函数与指针介绍
4.1 递归
尾递归
递归调用置于函数的末尾,在return语句之前。相当于循环
1 | void to_binary(unsigned long n) |
这个递归函数计算了一个整数的2进制表达方式。
4.2 指针
间接运算符*:假设ptr指向bah
ptr = &bah;
使用间接运算符找出储存在bah中的值。
val = *ptr; //找出ptr指向的值
语句ptr = &bah;
和val = *ptr;
放在一起相当于下面的语句;
val = bah;
由此可见,使用地址和间接运算符可以间接完成这一条语句的功能,这也是“间接运算符”的名称由来。
间接运算符*
也是一元运算符,用于获取指针所指向变量的值。例如,如果p
是一个指向整数的指针变量,那么*p
就是获取p
所指向的整数变量的值。可以将*
运算符应用于指针变量来访问所指向的变量,例如int x = *p
就是将p
所指向的整数变量的值赋值给变量x
。
简单来说,&
是用于获取某个变量的地址,*
是用于获取指针所指向变量的值。
4.3 函数与指针
要声明一个指向特定类型函数的指针,可以先声明一个该类型的函数,然后把函数名替换成(*pf)形式的表达式。然后pf就成了指向该类型函数的指针。
qsort()
有一个参数接受指向函数的指针,而后可以使用该指针提供的方法进行排序,无论是数组中的元素还是整数、字符串还是结构。
1 | void ToUpper(char *); //把字符串中的字符转换成大写字符的函数 |
注意,指针pf可以指向任意其他带char *类型参数、返回类型为void的函数。
5. 数组与指针
使用const
声明数组,将数组设置为只读。程序只能从数组中检索值,不能将新值写入数组。如下:
const int days[MONTHS] = {31,28,31,30,31,30,31,31,30,31,30,31};
5.1 指定初始化器(C99)
对于传统的C初始化语法,必须初始化最后一个元素之前的所有元素,才能初始化它:
int arr[6] = {0,0,0,0,0,212};
而C99规定使用带方括号的下标进行指明待初始化的元素:
int arr[6] = {[5] = 212};
同时初始化器还能初始化[]下标更后面的值:
[4] = 31,30,31;
这样会初始化下标为5和6的元素分别为30,31。
5.2 使用指针处理数组
假设flizny是一个数组,则:
flizny = &flizny[0]; //数组名是该数组的首元素地址
现在flizny
以及&flizny[0]
都表示数组首元素的内存地址。两者都是常量,在程序的运行过程中,不会改变。
指针的值是它所指向对象的值。
指针前面使用*运算符可以得到指针所指向的对象的值。
指针加1,指针的值递增他所指类型的大小(以字节为单位)。
同样,对于高维数组,例如:
zippo[6][6]
可以使用双解引用的形式:
*( *(zippo+2) + 1 )
表示zippo[2][1];

在这个的基础上,C语言的灵活性更是达到了令人咂舌的地步,以下的语法也是同样有效的:
int (*pz)[2]; //pz指向一个内含两个int类型值的数组
代码将pz声明为指向一个数组的指针,该数组内含有两个int类型的值。因为[]的优先级高于*,这是为什么需要使用到圆括号。
int * pax[2]; //pax是一个内含两个指针元素的数组,每个元素都指向int的指针
C const和C++ const
C和C++中的const用法很相似,但区别之一在于,C++允许声明数组大小时使用const整数,而C不允许;区别之二是,C++的指针赋值检查更严格
const int y;
const int *p2 = &y;
int *p1;
p1 = p2; //在C++中这是不被允许的,但是C中仅仅给出了警告
C++不允许把const指针赋给非const指针。
5.3 复合字面量(C99)
复合字面量类似数组初始化列表,前面使用括号括起来的类型名。例如:
int diva[2] = {10,20};
下面的符合字面量创建一个和diva数组相同的匿名数组,也有两个int类型的值:
(int [2]){10, 20}; //复合字面量
初始化有数组名的数组时,可以省略数组大小,复合字面量也可以胜率大小。
(int []){50.20.90} //内含3个元素的符合字面量
复合字面量的类型名也代表首元素的地址,所以可以把它付给指向int的指针。然后便可使用该指针
int * pt1;
pt1 = (int [2]) {10, 20};
本例中*pt1是10,pt1[1]是20。