C:PartⅡ
Zane Lv4

前言

承接上一篇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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#include <stdio.h>
#define SPACE ' '
int main()
{
char ch;

ch = getchar();
while(ch != '\n')
{
if(ch == SPACE)
putchar(ch);
else
putchar(ch + 1);
ch = getchar();
}
putchar(ch);
}

这段代码的示例结果如下:

CALL ME HAL.

DBMM NF IBM/

尽管这样的C语言风格已经足够简洁可读,实际上依然能对while的表达式进行合并实现更加具有C风格的代码实现。如下:

1
2
3
4
5
while((ch = getchar()) != '\n')
等同于
while(
(ch = getchar())
!= '\n')

对ch = getchar()外的圆括号的作用是,在执行判断之前就将ch进行赋值,不然可能出现编译器先执行判断再执行赋值导致程序出错。

2.4 ctype.h系列的字符函数

在上述的代码中,本应是.的字符被识别成了/而输出,这是因为在ASCII中点号的ASCII码刚好比斜杠的少1。怎么达到只让程序转换字母,保留所有非字母数字呢?

在库函数ctype.h中包含这些函数的原型,接受一个字符作为参数,如果该字符属于某种特殊类别,就返回非零值(真);否则返回0(假)。如:isalpha()函数,用以判断参数字母,则返回非零值。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#include <stdio.h>
#include <ctypes.h>
int main(void)
{
char ch;

while((ch = getchar()) != '\n')
{
if(isalpha(ch))
putchar(ch + 1);
else
putchar(ch);
}
putchar(ch);

return 0;
}

这样对于大小写字母就会进行ASCII的加一运算,而符号就会得到保留。

同样的库中包含如下常用字符测试函数:

2.5 else与if的配对

直观上讲,对于以下代码:

1
2
3
4
5
if(num > 6)
if(num < 12)
printf();
else
printf();

往往会认为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
2
3
4
5
6
7
8
9
10
/* echo.c -- 重复输入 */
#include <stdio.h>
int main()
{
char ch;

while((ch = getchar()) != '#')
putchar(ch);
return 0;
}

对于这段代码示例:

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
2
3
4
5
6
7
8
9
10
/* echo_eof.c -- 重复输入,直到文件末尾*/
#include <stdio.h>
int main()
{
int ch;

while((ch = getchar()) != EOF)
putchar(ch);
return 0;
}

将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
2
3
4
5
6
7
8
9
10
//范例1
int ch;
FILE * fp;
fp = fopen("wacky.txt", "r");
ch = getc(fp) //获取初始输入
while(ch != EOF)
{
putchar(ch);
ch = getc(fp);
}

同样可以简化为:

1
2
3
4
5
6
7
int ch;
FILE * fp;
fp = fopen("wacky.txt", "r");
while((ch = getc(fp)) != EOF)
{
putchar(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
2
fseek(fp, 0L, SEEK_END);
last = ftell(fp);

表示把当前位置设置为距文件末尾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
2
3
4
5
6
7
8
9
void to_binary(unsigned long n)
{
int r;
r = n % 2;
if (n >= 2)
to_binary(n / 2);
putchar(r == 0 ? '0' : '1');
return;
}

这个递归函数计算了一个整数的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
2
3
4
5
6
7
8
9
10
11
12
void ToUpper(char *); //把字符串中的字符转换成大写字符的函数
void (*pf)(char *); //pf是一个指向函数的指针
/* ----------*/

void ToUpper(char *);
void ToLower(char *);
int round(double);
void (*pf)(char *);
pf = ToUpper; //有效,ToUpper是该类型函数的地址
pf = Tolower; //有效,ToLower是该类型函数的地址
pf = round; //无效,round与指针类型
pf = Tolower();

注意,指针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。

由 Hexo 驱动 & 主题 Keep