首页
前端面试题
前端报错总结
电子书
更多
插件下载
Search
1
JavaScript基础(二)操作符 流程控制
42 阅读
2
HTML基础
20 阅读
3
Vue基础
17 阅读
4
wctype.h
14 阅读
5
Vue2(知识点)
13 阅读
默认分类
HTML CSS
HTML基础
CSS
HTML5 CSS3
javaScript
javaScript基础
javaScript高级
Web APIs
jQuery
js小总结
WEB开发布局
Vue
PS切图
数据可视化
Git使用
Uniapp
c语言入门
标准库
嵌入式
登录
Search
liuxiaobai
累计撰写
108
篇文章
累计收到
12
条评论
首页
栏目
默认分类
HTML CSS
HTML基础
CSS
HTML5 CSS3
javaScript
javaScript基础
javaScript高级
Web APIs
jQuery
js小总结
WEB开发布局
Vue
PS切图
数据可视化
Git使用
Uniapp
c语言入门
标准库
嵌入式
页面
前端面试题
前端报错总结
电子书
插件下载
搜索到
41
篇与
的结果
2023-09-20
文件操作
文件操作本章介绍 C 语言如何操作文件。文件指针C 语言提供了一个 FILE 数据结构,记录了操作一个文件所需要的信息。该结构定义在头文件stdio.h,所有文件操作函数都要通过这个数据结构,获取文件信息。开始操作一个文件之前,就要定义一个指向该文件的 FILE 指针,相当于获取一块内存区域,用来保存文件信息。FILE* fp;上面示例定义了一个 FILE 指针fp。下面是一个读取文件的完整示例。#include <stdio.h> int main(void) { FILE* fp; char c; fp = fopen("hello.txt", "r"); if (fp == NULL) { return -1; } c = fgetc(fp); printf("%c\n", c); fclose(fp); return 0; }上面示例中,新建文件指针fp以后,依次使用了下面三个文件操作函数,分成三个步骤。其他的文件操作,大致上也是这样的步骤。第一步,使用fopen()打开指定文件,返回一个 File 指针。如果出错,返回 NULL。它相当于将指定文件的信息与新建的文件指针fp相关联,在 FILE 结构内部记录了这样一些信息:文件内部的当前读写位置、读写报错的记录、文件结尾指示器、缓冲区开始位置的指针、文件标识符、一个计数器(统计拷贝进缓冲区的字节数)等等。后继的操作就可以使用这个指针(而不是文件名)来处理指定文件。同时,它还为文件建立一个缓存区。由于存在缓存区,也可以说fopen()函数“打开一个了流”,后继的读写文件都是流模式。第二步,使用读写函数,从文件读取数据,或者向文件写入数据。上例使用了fgetc()函数,从已经打开的文件里面,读取一个字符。fgetc()一调用,文件的数据块先拷贝到缓冲区。不同的计算机有不同的缓冲区大小,一般是512字节或是它的倍数,如4096或16384。随着计算机硬盘容量越来越大,缓冲区也越来越大。fgetc()从缓冲区读取数据,同时将文件指针内部的读写位置指示器,指向所读取字符的下一个字符。所有的文件读取函数都使用相同的缓冲区,后面再调用任何一个读取函数,都将从指示器指向的位置,即上一次读取函数停止的位置开始读取。当读取函数发现已读完缓冲区里面的所有字符时,会请求把下一个缓冲区大小的数据块,从文件拷贝到缓冲区中。读取函数就以这种方式,读完文件的所有内容,直到文件结尾。不过,上例是只从缓存区读取一个字符。当函数在缓冲区里面,读完文件的最后一个字符时,就把 FILE 结构里面的文件结尾指示器设置为真。于是,下一次再调用读取函数时,会返回常量 EOF。EOF 是一个整数值,代表文件结尾,一般是-1。第三步,fclose()关闭文件,同时清空缓存区。上面是文件读取的过程,文件写入也是类似的方式,先把数据写入缓冲区,当缓冲区填满后,缓存区的数据将被转移到文件中。fopen()fopen()函数用来打开文件。所有文件操作的第一步,都是使用fopen()打开指定文件。这个函数的原型定义在头文件stdio.h。FILE* fopen(char* filename, char* mode);它接受两个参数。第一个参数是文件名(可以包含路径),第二个参数是模式字符串,指定对文件执行的操作,比如下面的例子中,r表示以读取模式打开文件。fp = fopen("in.dat", "r");成功打开文件以后,fopen()返回一个 FILE 指针,其他函数可以用这个指针操作文件。如果无法打开文件(比如文件不存在或没有权限),会返回空指针 NULL。所以,执行fopen()以后,最好判断一下,有没有打开成功。fp = fopen("hello.txt", "r"); if (fp == NULL) { printf("Can't open file!\n"); exit(EXIT_FAILURE); }上面示例中,如果fopen()返回一个空指针,程序就会报错。fopen()的模式字符串有以下几种。r:读模式,只用来读取数据。如果文件不存在,返回 NULL 指针。w:写模式,只用来写入数据。如果文件存在,文件长度会被截为0,然后再写入;如果文件不存在,则创建该文件。a:写模式,只用来在文件尾部追加数据。如果文件不存在,则创建该文件。r+:读写模式。如果文件存在,指针指向文件开始处,可以在文件头部添加数据。如果文件不存在,返回 NULL 指针。w+:读写模式。如果文件存在,文件长度会被截为0,然后再写入数据。这种模式实际上读不到数据,反而会擦掉数据。如果文件不存在,则创建该文件。a+:读写模式。如果文件存在,指针指向文件结尾,可以在现有文件末尾添加内容。如果文件不存在,则创建该文件。上一小节说过,fopen()函数会为打开的文件创建一个缓冲区。读模式下,创建的是读缓存区;写模式下,创建的是写缓存区;读写模式下,会同时创建两个缓冲区。C 语言通过缓存区,以流的形式,向文件读写数据。数据在文件里面,都是以二进制形式存储。但是,读取的时候,有不同的解读方法:以原本的二进制形式解读,叫做“二进制流”;将二进制数据转成文本,以文本形式解读,叫做“文本流”。写入操作也是如此,分成以二进制写入和以文本写入,后者会多一个文本转二进制的步骤。fopen()的模式字符串,默认是以文本流读写。如果添加b后缀(表示 binary),就会以“二进制流”进行读写。比如,rb是读取二进制数据模式,wb是写入二进制数据模式。模式字符串还有一个x后缀,表示独占模式(exclusive)。如果文件已经存在,则打开文件失败;如果文件不存在,则新建文件,打开后不再允许其他程序或线程访问当前文件。比如,wx表示以独占模式写入文件,如果文件已经存在,就会打开失败。标准流Linux 系统默认提供三个已经打开的文件,它们的文件指针如下。stdin(标准输入):默认来源为键盘,文件指针编号为0。stdout(标准输出):默认目的地为显示器,文件指针编号为1。stderr(标准错误):默认目的地为显示器,文件指针编号为2。Linux 系统的文件,不一定是数据文件,也可以是设备文件,即文件代表一个可以读或写的设备。文件指针stdin默认是把键盘看作一个文件,读取这个文件,就能获取用户的键盘输入。同理,stdout和stderr默认是把显示器看作一个文件,将程序的运行结果写入这个文件,用户就能看到运行结果了。它们的区别是,stdout写入的是程序的正常运行结果,stderr写入的是程序的报错信息。这三个输入和输出渠道,是 Linux 默认提供的,所以分别称为标准输入(stdin)、标准输出(stdout)和标准错误(stderr)。因为它们的实现是一样的,都是文件流,所以合称为“标准流”。Linux 允许改变这三个文件指针(文件流)指向的文件,这称为重定向(redirection)。如果标准输入不绑定键盘,而是绑定其他文件,可以在文件名前面加上小于号<,跟在程序名后面。这叫做“输入重定向”(input redirection)。$ demo < in.dat上面示例中,demo程序代码里面的stdin,将指向文件in.dat,即从in.dat获取数据。如果标准输出绑定其他文件,而不是显示器,可以在文件名前加上大于号>,跟在程序名后面。这叫做“输出重定向”(output redirection)。$ demo > out.dat上面示例中,demo程序代码里面的stdout,将指向文件out.dat,即向out.dat写入数据。输出重定向>会先擦去out.dat的所有原有的内容,然后再写入。如果希望写入的信息追加在out.dat的结尾,可以使用>>符号。$ demo >> out.dat上面示例中,demo程序代码里面的stdout,将向文件out.dat写入数据。与>不同的是,写入的开始位置是out.dat的文件结尾。标准错误的重定向符号是2>。其中的2代表文件指针的编号,即2>表示将2号文件指针的写入,重定向到err.txt。2号文件指针就是标准错误stderr。$ demo > out.dat 2> err.txt上面示例中,demo程序代码里面的stderr,会向文件err.txt写入报错信息。而stdout向文件out.dat写入。输入重定向和输出重定向,也可以结合在一条命令里面。$ demo < in.dat > out.dat // or $ demo > out.dat < in.dat重定向还有另一种情况,就是将一个程序的标准输出stdout,指向另一个程序的标准输入stdin,这时要使用|符号。$ random | sum上面示例中,random程序代码里面的stdout的写入,会从sum程序代码里面的stdin被读取。fclose()fclose()用来关闭已经使用fopen()打开的文件。它的原型定义在stdio.h。int fclose(FILE* stream);它接受一个文件指针fp作为参数。如果成功关闭文件,fclose()函数返回整数0;如果操作失败(比如磁盘已满,或者出现 I/O 错误),则返回一个特殊值 EOF(详见下一小节)。if (fclose(fp) != 0) printf("Something wrong.");不再使用的文件,都应该使用fclose()关闭,否则无法释放资源。一般来说,系统对同时打开的文件数量有限制,及时关闭文件可以避免超过这个限制。EOFC 语言的文件操作函数的设计是,如果遇到文件结尾,就返回一个特殊值。程序接收到这个特殊值,就知道已经到达文件结尾了。头文件stdio.h为这个特殊值定义了一个宏EOF(end of file 的缩写),它的值一般是-1。这是因为从文件读取的二进制值,不管作为无符号数字解释,还是作为 ASCII 码解释,都不可能是负值,所以可以很安全地返回-1,不会跟文件本身的数据相冲突。需要注意的是,不像字符串结尾真的存储了\0这个值,EOF并不存储在文件结尾,文件中并不存在这个值,完全是文件操作函数发现到达了文件结尾,而返回这个值。freopen()freopen()用于新打开一个文件,直接关联到某个已经打开的文件指针。这样可以复用文件指针。它的原型定义在头文件stdio.h。FILE* freopen(char* filename, char* mode, FILE stream);它跟fopen()相比,就是多出了第三个参数,表示要复用的文件指针。其他两个参数都一样,分别是文件名和打开模式。freopen("output.txt", "w", stdout); printf("hello");上面示例将文件output.txt关联到stdout,此后向stdout写入的内容,都会写入output.txt。由于printf()默认就是输出到stdout,所以运行上面的代码以后,文件output.txt会被写入hello。freopen()的返回值是它的第三个参数(文件指针)。如果打开失败(比如文件不存在),会返回空指针 NULL。freopen()会自动关闭原先已经打开的文件,如果文件指针并没有指向已经打开的文件,则freopen()等同于fopen()。下面是freopen()关联scanf()的例子。int i, i2; scanf("%d", &i); freopen("someints.txt", "r", stdin); scanf("%d", &i2);上面例子中,一共调用了两次scanf(),第一次调用是从键盘读取,然后使用freopen()将stdin指针关联到某个文件,第二次调用就会从该文件读取。某些系统允许使用freopen(),改变文件的打开模式。这时,freopen()的第一个参数应该是 NULL。freopen(NULL, "wb", stdout);上面示例将stdout的打开模式从w改成了wb。fgetc(),getc()fgetc()和getc()用于从文件读取一个字符。它们的用法跟getchar()类似,区别是getchar()只用来从stdin读取,而这两个函数是从任意指定的文件读取。它们的原型定义在头文件stdio.h。int fgetc(FILE *stream) int getc(FILE *stream);fgetc()与getc()的用法是一样的,都只有文件指针一个参数。两者的区别是,getc()一般用宏来实现,而fgetc()是函数实现,所以前者的性能可能更好一些。注意,虽然这两个函数返回的是一个字符,但是它们的返回值类型却不是char,而是int,这是因为读取失败的情况下,它们会返回 EOF,这个值一般是-1。#include <stdio.h> int main(void) { FILE *fp; fp = fopen("hello.txt", "r"); int c; while ((c = getc(fp)) != EOF) printf("%c", c); fclose(fp); }上面示例中,getc()依次读取文件的每个字符,将其放入变量c,直到读到文件结尾,返回 EOF,循环终止。变量c的类型是int,而不是char,因为有可能等于负值,所以设为int更好一些。fputc(),putc()fputc()和putc()用于向文件写入一个字符。它们的用法跟putchar()类似,区别是putchar()是向stdout写入,而这两个函数是向文件写入。它们的原型定义在头文件stdio.h。int fputc(int char, FILE *stream); int putc(int char, FILE *stream);fputc()与putc()的用法是一样,都接受两个参数,第一个参数是待写入的字符,第二个参数是文件指针。它们的区别是,putc()通常是使用宏来实现,而fputc()只作为函数来实现,所以理论上,putc()的性能会好一点。写入成功时,它们返回写入的字符;写入失败时,返回 EOF。fprintf()fprintf()用于向文件写入格式化字符串,用法与printf()类似。区别是printf()总是写入stdout,而fprintf()则是写入指定的文件,它的第一个参数必须是一个文件指针。它的原型定义在头文件stdio.h。int fprintf(FILE* stream, const char* format, ...)fprintf()可以替代printf()。printf("Hello, world!\n"); fprintf(stdout, "Hello, world!\n");上面例子中,指定fprintf()写入stdout,结果就等同于调用printf()。fprintf(fp, "Sum: %d\n", sum);上面示例是向文件指针fp写入指定格式的字符串。下面是向stderr输出错误信息的例子。fprintf(stderr, "Something number.\n");fscanf()fscanf()用于按照给定的模式,从文件中读取内容,用法跟scanf()类似。区别是scanf()总是从stdin读取数据,而fscanf()是从文件读入数据,它的原型定义在头文件stdio.h,第一个参数必须是文件指针。int fscanf(FILE* stream, const char* format, ...);下面是一个例子。fscanf(fp, "%d%d", &i, &j);上面示例中,fscanf()从文件fp里面,读取两个整数,放入变量i和j。使用fscanf()的前提是知道文件的结构,它的占位符解析规则与scanf()完全一致。由于fscanf()可以连续读取,直到读到文件尾,或者发生错误(读取失败、匹配失败),才会停止读取,所以fscanf()通常放在循环里面。while(fscanf(fp, "%s", words) == 1) puts(words);上面示例中,fscanf()依次读取文件的每个词,将它们一行打印一个,直到文件结束。fscanf()的返回值是赋值成功的变量数量,如果赋值失败会返回 EOF。fgets()fgets()用于从文件读取指定长度的字符串,它名字的第一个字符是f,就代表file。它的原型定义在头文件stdio.h。char* fgets(char* str, int STRLEN, File* fp);它的第一个参数str是一个字符串指针,用于存放读取的内容。第二个参数STRLEN指定读取的长度,第三个参数是一个 FILE 指针,指向要读取的文件。fgets()读取 STRLEN - 1 个字符之后,或者遇到换行符与文件结尾,就会停止读取,然后在已经读取的内容末尾添加一个空字符\0,使之成为一个字符串。注意,fgets()会将换行符(\n)存储进字符串。如果fgets的第三个参数是stdin,就可以读取标准输入,等同于scanf()。fgets(str, sizeof(str), stdin);读取成功时,fgets()的返回值是它的第一个参数,即指向字符串的指针,否则返回空指针 NULL。fgets()可以用来读取文件的每一行,下面是读取文件所有行的例子。#include <stdio.h> int main(void) { FILE* fp; char s[1024]; // 数组必须足够大,足以放下一行 int linecount = 0; fp = fopen("hello.txt", "r"); while (fgets(s, sizeof s, fp) != NULL) printf("%d: %s", ++linecount, s); fclose(fp); }上面示例中,每读取一行,都会输出行号和该行的内容。下面的例子是循环读取用户的输入。char words[10]; puts("Enter strings (q to quit):"); while (fgets(words, 10, stdin) != NULL) { if (words[0] == 'q' && words[1] == '\n') break; puts(words); } puts("Done.");上面的示例中,如果用户输入的字符串大于9个字符,fgets()会多次读取。直到遇到q + 回车键,才会退出循环。fputs()fputs()函数用于向文件写入字符串,和puts()函数只有一点不同,那就是它不会在字符串末尾添加换行符。这是因为fgets()保留了换行符,所以fputs()就不添加了。fputs()函数通常与fgets()配对使用。它的原型定义在stdio.h。int fputs(const char* str, FILE* stream);它接受两个参数,第一个参数是字符串指针,第二个参数是要写入的文件指针。如果第二个参数为stdout(标准输出),就是将内容输出到计算机屏幕,等同于printf()。char words[14]; puts("Enter a string, please."); fgets(words, 14, stdin); puts("This is your string:"); fputs(words, stdout);上面示例中,先用fgets()从stdin读取用户输入,然后用fputs()输出到stdout。写入成功时,fputs()返回一个非负整数,否则返回 EOF。fwrite()fwrite()用来一次性写入较大的数据块,主要用途是将数组数据一次性写入文件,适合写入二进制数据。它的原型定义在stdio.h。size_t fwrite( const void* ptr, size_t size, size_t nmemb, FILE* fp );它接受四个参数。ptr:数组指针。size:每个数组成员的大小,单位字节。nmemb:数组成员的数量。fp:要写入的文件指针。注意,fwrite()原型的第一个参数类型是void*,这是一个无类型指针,编译器会自动将参数指针转成void*类型。正是由于fwrite()不知道数组成员的类型,所以才需要知道每个成员的大小(第二个参数)和成员数量(第三个参数)。fwrite()函数的返回值是成功写入的数组成员的数量(注意不是字节数)。正常情况下,该返回值就是第三个参数nmemb,但如果出现写入错误,只写入了一部分成员,返回值会比nmemb小。要将整个数组arr写入文件,可以采用下面的写法。fwrite( arr, sizeof(arr[0]), sizeof(arr) / sizeof(arr[0]), fp );上面示例中,sizeof(a[0])是每个数组成员占用的字节,sizeof(a) / sizeof(a[0])是整个数组的成员数量。下面的例子是将一个大小为256字节的字符串写入文件。char buffer[256]; fwrite(buffer, 1, 256, fp);上面示例中,数组buffer每个成员是1个字节,一共有256个成员。由于fwrite()是连续内存复制,所以写成fwrite(buffer, 256, 1, fp)也能达到目的。fwrite()没有规定一定要写入整个数组,只写入数组的一部分也是可以的。任何类型的数据都可以看成是1字节数据组成的数组,或者是一个成员的数组,所以fwrite()实际上可以写入任何类型的数据,而不仅仅是数组。比如,fwrite()可以将一个 Struct 结构写入文件保存。fwrite(&s, sizeof(s), 1, fp);上面示例中,s是一个 Struct 结构指针,可以看成是一个成员的数组。注意,如果s的属性包含指针,存储时需要小心,因为保存指针可能没意义,还原出来的时候,并不能保证指针指向的数据还存在。fwrite()以及后面要介绍的fread(),比较适合读写二进制数据,因为它们不会对写入的数据进行解读。二进制数据可能包含空字符\0,这是 C 语言的字符串结尾标记,所以读写二进制文件,不适合使用文本读写函数(比如fprintf()等)。下面是一个写入二进制文件的例子。#include <stdio.h> int main(void) { FILE* fp; unsigned char bytes[] = {5, 37, 0, 88, 255, 12}; fp = fopen("output.bin", "wb"); fwrite(bytes, sizeof(char), sizeof(bytes), fp); fclose(fp); return 0; }上面示例中,写入二进制文件时,fopen()要使用wb模式打开,表示二进制写入。fwrite()可以把数据解释成单字节数组,因此它的第二个参数是sizeof(char),第三个参数是数组的总字节数sizeof(bytes)。上面例子写入的文件output.bin,使用十六进制编辑器打开,会是下面的内容。05 25 00 58 ff 0cfwrite()还可以连续向一个文件写入数据。struct clientData myClient = {1, 'foo bar'}; for (int i = 1; i <= 100; i++) { fwrite(&myClient, sizeof(struct clientData), 1, cfPtr); }上面示例中,fwrite()连续将100条数据写入文件。fread()fread()函数用于一次性从文件读取较大的数据块,主要用途是将文件内容读入一个数组,适合读取二进制数据。它的原型定义在头文件stdio.h。size_t fread( void* ptr, size_t size, size_t nmemb, FILE* fp );它接受四个参数,与fwrite()完全相同。ptr:数组地址。size:每个数组成员的大小,单位为字节。nmemb:数组的成员数量。fp:文件指针。要将文件内容读入数组arr,可以采用下面的写法。fread( arr, sizeof(arr[0]), sizeof(arr) / sizeof(arr[0]), fp );上面示例中,数组长度(第二个参数)和每个成员的大小(第三个参数)的乘积,就是数组占用的内存空间的大小。fread()会从文件(第四个参数)里面读取相同大小的内容,然后将ptr(第一个参数)指向这些内容的内存地址。下面的例子是将文件内容读入一个10个成员的双精度浮点数数组。double earnings[10]; fread(earnings, sizeof(double), 10, fp);上面示例中,每个数组成员的大小是sizeof(double),一个有10个成员,就会从文件fp读取sizeof(double) * 10大小的内容。fread()函数的返回值是成功读取的数组成员的数量。正常情况下,该返回值就是第三个参数nmemb,但如果出现读取错误或读到文件结尾,该返回值就会比nmemb小。所以,检查fread()的返回值是非常重要的。fread()和fwrite()可以配合使用。在程序终止之前,使用fwrite()将数据保存进文件,下次运行时再用fread()将数据还原进入内存。下面是读取上一节生成的二进制文件output.bin的例子。#include <stdio.h> int main(void) { FILE* fp; unsigned char c; fp = fopen("output.bin", "rb"); while (fread(&c, sizeof(char), 1, fp) > 0) printf("%d\n", c); return 0; }运行后,得到如下结果。5 37 0 88 255 12feof()feof()函数判断文件的内部指针是否指向文件结尾。它的原型定义在头文件stdio.h。int feof(FILE *fp);feof()接受一个文件指针作为参数。如果已经到达文件结尾,会返回一个非零值(表示 true),否则返回0(表示 false)。诸如fgetc()这样的文件读取函数,如果返回 EOF,有两种可能,一种可能是已读取到文件结尾,另一种可能是出现读取错误。feof()可以用来判断到底是那一种情况。下面是通过feof()判断是否到达文件结尾,从而循环读取整个文件的例子。int num; char name[50]; FILE* cfPtr = fopen("clients.txt", "r"); while (!feof(cfPtr)) { fscanf(cfPtr, "%d%s\n", &num, name); printf("%d %s\n", num, name); } fclose(cfPtr);上面示例通过循环判断feof()是否读到文件结尾,从而实现读出整个文件内容。feof()为真时,可以通过fseek()、rewind()、fsetpos()函数改变文件内部读写位置的指示器,从而清除这个函数的状态。fseek()每个文件指针都有一个内部指示器(内部指针),记录当前打开的文件的读写位置(file position),即下一次读写从哪里开始。文件操作函数(比如getc()、fgets()、fscanf()和fread()等)都从这个指示器指定的位置开始按顺序读写文件。如果希望改变这个指示器,将它移到文件的指定位置,可以使用fseek()函数。它的原型定义在头文件stdio.h。int fseek(FILE* stream, long int offset, int whence);fseek()接受3个参数。stream:文件指针。offset:距离基准(第三个参数)的字节数。类型为 long int,可以为正值(向文件末尾移动)、负值(向文件开始处移动)或 0(保持不动)。whence:位置基准,用来确定计算起点。它的值是以下三个宏(定义在stdio.h):SEEK_SET(文件开始处)、SEEK_CUR (内部指针的当前位置)、SEEK_END(文件末尾)请看下面的例子。// 定位到文件开始处 fseek(fp, 0L, SEEK_SET); // 定位到文件末尾 fseek(fp, 0L, SEEK_END); // 从当前位置后移2个字节 fseek(fp, 2L, SEEK_CUR); // 定位到文件第10个字节 fseek(fp, 10L, SEEK_SET); // 定位到文件倒数第10个字节 fseek(fp, -10L, SEEK_END);上面示例中,fseek()的第二个参数为 long 类型,所以移动距离必须加上后缀L,将其转为 long 类型。下面的示例逆向输出文件的所有字节。for (count = 1L; count <= size; count++) { fseek(fp, -count, SEEK_END); ch = getc(fp); }注意,fseek()最好只用来操作二进制文件,不要用来读取文本文件。因为文本文件的字符有不同的编码,某个位置的准确字节位置不容易确定。正常情况下,fseek()的返回值为0。如果发生错误(如移动的距离超出文件的范围),返回值为非零值(比如-1)。ftell()ftell()函数返回文件内部指示器的当前位置。它的原型定义在头文件stdio.h。long int ftell(FILE* stream);它接受一个文件指针作为参数。返回值是一个 long 类型的整数,表示内部指示器的当前位置,即文件开始处到当前位置的字节数,0表示文件开始处。如果发生错误,ftell()返回-1L。ftell()可以跟fseek()配合使用,先记录内部指针的位置,一系列操作过后,再用fseek()返回原来的位置。long file_pos = ftell(fp); // 一系列文件操作之后 fseek(fp, file_pos, SEEK_SET);下面的例子先将指示器定位到文件结尾,然后得到文件开始处到结尾的字节数。fseek(fp, 0L, SEEK_END); size = ftell(fp);rewind()rewind()函数可以让文件的内部指示器回到文件开始处。它的原型定义在stdio.h。void rewind(file* stream);它接受一个文件指针作为参数。rewind(fp)基本等价于fseek(fp, 0l, seek_set),唯一的区别是rewind()没有返回值,而且会清除当前文件的错误指示器。fgetpos(),fsetpos()fseek()和ftell()有一个潜在的问题,那就是它们都把文件大小限制在 long int 类型能表示的范围内。这看起来相当大,但是在32位计算机上,long int 的长度为4个字节,能够表示的范围最大为 4GB。随着存储设备的容量迅猛增长,文件也越来越大,往往会超出这个范围。鉴于此,C 语言新增了两个处理大文件的新定位函数:fgetpos()和fsetpos()。它们的原型都定义在头文件stdio.h。int fgetpos(FILE* stream, fpos_t* pos); int fsetpos(FILE* stream, const fpos_t* pos);fgetpos()函数会将文件内部指示器的当前位置,存储在指针变量pos。该函数接受两个参数,第一个是文件指针,第二个存储指示器位置的变量。fsetpos()函数会将文件内部指示器的位置,移动到指针变量pos指定的地址。注意,变量pos必须是通过调用fgetpos()方法获得的。fsetpos()的两个参数与fgetpos()必须是一样的。记录文件内部指示器位置的指针变量pos,类型为fpos_t*(file position type 的缩写,意为文件定位类型)。它不一定是整数,也可能是一个 Struct 结构。下面是用法示例。fpos_t file_pos; fgetpos(fp, &file_pos); // 一系列文件操作之后 fsetpos(fp, &file_pos);上面示例中,先用fgetpos()获取内部指针的位置,后面再用fsetpos()恢复指针的位置。执行成功时,fgetpos()和fsetpos()都会返回0,否则返回非零值。ferror(),clearerr()所有的文件操作函数如果执行失败,都会在文件指针里面记录错误状态。后面的操作只要读取错误指示器,就知道前面的操作出错了。ferror()函数用来返回错误指示器的状态。可以通过这个函数,判断前面的文件操作是否成功。它的原型定义在头文件stdio.h。int ferror(FILE *stream);它接受一个文件指针作为参数。如果前面的操作出现错误,ferror()就会返回一个非零整数(表示 true),否则返回0。clearerr()函数用来重置出错指示器。它的原型定义在头文件stdio.h。void clearerr(FILE* fp);它接受一个文件指针作为参数,没有返回值。下面是一个例子。FILE* fp = fopen("file.txt", "w"); char c = fgetc(fp); if (ferror(fp)) { printf("读取文件:file.txt 时发生错误\n"); } clearerr(fp);上面示例中,fgetc()尝试读取一个以”写模式“打开的文件,读取失败就会返回 EOF。这时调用ferror()就可以知道上一步操作出错了。处理完以后,再用clearerr()清除出错状态。文件操作函数如果正常执行,ferror()和feof()都会返回零。如果执行不正常,就要判断到底是哪里出了问题。if (fscanf(fp, "%d", &n) != 1) { if (ferror(fp)) { printf("io error\n"); } if (feof(fp)) { printf("end of file\n"); } clearerr(fp); fclose(fp); }上面示例中,当fscanf()函数报错时,通过检查ferror()和feof(),确定到底发生什么问题。这两个指示器改变状态后,会保持不变,所以要用clearerr()清除它们,clearerr()可以同时清除两个指示器。remove()remove()函数用于删除指定文件。它的原型定义在头文件stdio.h。int remove(const char* filename);它接受文件名作为参数。如果删除成功,remove()返回0,否则返回非零值。remove("foo.txt");上面示例删除了foo.txt文件。注意,删除文件必须是在文件关闭的状态下。如果是用fopen()打开的文件,必须先用fclose()关闭后再删除。rename()rename()函数用于文件改名,也用于移动文件。它的原型定义在头文件stdio.h。int rename(const char* old_filename, const char* new_filename);它接受两个参数,第一个参数是现在的文件名,第二个参数是新的文件名。如果改名成功,rename()返回0,否则返回非零值。rename("foo.txt", "bar.txt");上面示例将foo.txt改名为bar.txt。注意,改名后的文件不能与现有文件同名。另外,如果要改名的文件已经打开了,必须先关闭,然后再改名,对打开的文件进行改名会失败。下面是移动文件的例子。rename("/tmp/evidence.txt", "/home/beej/nothing.txt");
2023年09月20日
3 阅读
0 评论
0 点赞
2023-09-20
I/O 函数
I/O 函数C 语言提供了一些函数,用于与外部设备通信,称为输入输出函数,简称 I/O 函数。输入(import)指的是获取外部数据,输出(export)指的是向外部传递数据。缓存和字节流严格地说,输入输出函数并不是直接与外部设备通信,而是通过缓存(buffer)进行间接通信。这个小节介绍缓存是什么。普通文件一般都保存在磁盘上面,跟 CPU 相比,磁盘读取或写入数据是一个很慢的操作。所以,程序直接读写磁盘是不可行的,可能每执行一行命令,都必须等半天。C 语言的解决方案,就是只要打开一个文件,就在内存里面为这个文件设置一个缓存区。程序向文件写入数据时,程序先把数据放入缓存,等到缓存满了,再把里面的数据会一次性写入磁盘文件。这时,缓存区就空了,程序再把新的数据放入缓存,重复整个过程。程序从文件读取数据时,文件先把一部分数据放到缓存里面,然后程序从缓存获取数据,等到缓存空了,磁盘文件再把新的数据放入缓存,重复整个过程。内存的读写速度比磁盘快得多,缓存的设计减少了读写磁盘的次数,大大提高了程序的执行效率。另外,一次性移动大块数据,要比多次移动小块数据快得多。这种读写模式,对于程序来说,就有点像水流(stream),不是一次性读取或写入所有数据,而是一个持续不断的过程。先操作一部分数据,等到缓存吞吐完这部分数据,再操作下一部分数据。这个过程就叫做字节流操作。由于缓存读完就空了,所以字节流读取都是只能读一次,第二次就读不到了。这跟读取文件很不一样。C 语言的输入输出函数,凡是涉及读写文件,都是属于字节流操作。输入函数从文件获取数据,操作的是输入流;输出函数向文件写入数据,操作的是输出流。printf()printf()是最常用的输出函数,用于屏幕输出,原型定义在头文件stdio.h,详见《基本语法》一章。scanf()基本用法scanf()函数用于读取用户的键盘输入。程序运行到这个语句时,会停下来,等待用户从键盘输入。用户输入数据、按下回车键后,scanf()就会处理用户的输入,将其存入变量。它的原型定义在头文件stdio.h。scanf()的语法跟printf()类似。scanf("%d", &i);它的第一个参数是一个格式字符串,里面会放置占位符(与printf()的占位符基本一致),告诉编译器如何解读用户的输入,需要提取的数据是什么类型。这是因为 C 语言的数据都是有类型的,scanf()必须提前知道用户输入的数据类型,才能处理数据。它的其余参数就是存放用户输入的变量,格式字符串里面有多少个占位符,就有多少个变量。上面示例中,scanf()的第一个参数%d,表示用户输入的应该是一个整数。%d就是一个占位符,%是占位符的标志,d表示整数。第二个参数&i表示,将用户从键盘输入的整数存入变量i。注意,变量前面必须加上&运算符(指针变量除外),因为scanf()传递的不是值,而是地址,即将变量i的地址指向用户输入的值。如果这里的变量是指针变量(比如字符串变量),那就不用加&运算符。下面是一次将键盘输入读入多个变量的例子。scanf("%d%d%f%f", &i, &j, &x, &y);上面示例中,格式字符串%d%d%f%f,表示用户输入的前两个是整数,后两个是浮点数,比如1 -20 3.4 -4.0e3。这四个值依次放入i、j、x、y四个变量。scanf()处理数值占位符时,会自动过滤空白字符,包括空格、制表符、换行符等。所以,用户输入的数据之间,有一个或多个空格不影响scanf()解读数据。另外,用户使用回车键,将输入分成几行,也不影响解读。1 -20 3.4 -4.0e3上面示例中,用户分成四行输入,得到的结果与一行输入是完全一样的。每次按下回车键以后,scanf()就会开始解读,如果第一行匹配第一个占位符,那么下次按下回车键时,就会从第二个占位符开始解读。scanf()处理用户输入的原理是,用户的输入先放入缓存,等到按下回车键后,按照占位符对缓存进行解读。解读用户输入时,会从上一次解读遗留的第一个字符开始,直到读完缓存,或者遇到第一个不符合条件的字符为止。int x; float y; // 用户输入 " -13.45e12# 0" scanf("%d", &x); scanf("%f", &y);上面示例中,scanf()读取用户输入时,%d占位符会忽略起首的空格,从-处开始获取数据,读取到-13停下来,因为后面的.不属于整数的有效字符。这就是说,占位符%d会读到-13。第二次调用scanf()时,就会从上一次停止解读的地方,继续往下读取。这一次读取的首字符是.,由于对应的占位符是%f,会读取到.45e12,这是采用科学计数法的浮点数格式。后面的#不属于浮点数的有效字符,所以会停在这里。由于scanf()可以连续处理多个占位符,所以上面的例子也可以写成下面这样。scanf("%d%f", &x, &y);scanf()的返回值是一个整数,表示成功读取的变量个数。如果没有读取任何项,或者匹配失败,则返回0。如果读取到文件结尾,则返回常量 EOF。占位符scanf()常用的占位符如下,与printf()的占位符基本一致。%c:字符。%d:整数。%f:float类型浮点数。%lf:double类型浮点数。%Lf:long double类型浮点数。%s:字符串。%[]:在方括号中指定一组匹配的字符(比如%[0-9]),遇到不在集合之中的字符,匹配将会停止。上面所有占位符之中,除了%c以外,都会自动忽略起首的空白字符。%c不忽略空白字符,总是返回当前第一个字符,无论该字符是否为空格。如果要强制跳过字符前的空白字符,可以写成scanf(" %c", &ch),即%c前加上一个空格,表示跳过零个或多个空白字符。下面要特别说一下占位符%s,它其实不能简单地等同于字符串。它的规则是,从当前第一个非空白字符开始读起,直到遇到空白字符(即空格、换行符、制表符等)为止。因为%s不会包含空白字符,所以无法用来读取多个单词,除非多个%s一起使用。这也意味着,scanf()不适合读取可能包含空格的字符串,比如书名或歌曲名。另外,scanf()遇到%s占位符,会在字符串变量末尾存储一个空字符\0。scanf()将字符串读入字符数组时,不会检测字符串是否超过了数组长度。所以,储存字符串时,很可能会超过数组的边界,导致预想不到的结果。为了防止这种情况,使用%s占位符时,应该指定读入字符串的最长长度,即写成%[m]s,其中的[m]是一个整数,表示读取字符串的最大长度,后面的字符将被丢弃。char name[11]; scanf("%10s", name);上面示例中,name是一个长度为11的字符数组,scanf()的占位符%10s表示最多读取用户输入的10个字符,后面的字符将被丢弃,这样就不会有数组溢出的风险了。赋值忽略符有时,用户的输入可能不符合预定的格式。scanf("%d-%d-%d", &year, &month, &day);上面示例中,如果用户输入2020-01-01,就会正确解读出年、月、日。问题是用户可能输入其他格式,比如2020/01/01,这种情况下,scanf()解析数据就会失败。为了避免这种情况,scanf()提供了一个赋值忽略符(assignment suppression character)*。只要把*加在任何占位符的百分号后面,该占位符就不会返回值,解析后将被丢弃。scanf("%d%*c%d%*c%d", &year, &month, &day);上面示例中,%*c就是在占位符的百分号后面,加入了赋值忽略符*,表示这个占位符没有对应的变量,解读后不必返回。sscanf()sscanf()函数与scanf()很类似,不同之处是sscanf()从字符串里面,而不是从用户输入获取数据。它的原型定义在头文件stdio.h里面。int sscanf(const char* s, const char* format, ...);sscanf()的第一个参数是一个字符串指针,用来从其中获取数据。其他参数都与scanf()相同。sscanf()主要用来处理其他输入函数读入的字符串,从其中提取数据。fgets(str, sizeof(str), stdin); sscanf(str, "%d%d", &i, &j);上面示例中,fgets()先从标准输入获取了一行数据(fgets()的介绍详见下一章),存入字符数组str。然后,sscanf()再从字符串str里面提取两个整数,放入变量i和j。sscanf()的一个好处是,它的数据来源不是流数据,所以可以反复使用,不像scanf()的数据来源是流数据,只能读取一次。sscanf()的返回值是成功赋值的变量的数量,如果提取失败,返回常量 EOF。getchar(),putchar()(1)getchar()getchar()函数返回用户从键盘输入的一个字符,使用时不带有任何参数。程序运行到这个命令就会暂停,等待用户从键盘输入,等同于使用scanf()方法读取一个字符。它的原型定义在头文件stdio.h。char ch; ch = getchar(); // 等同于 scanf("%c", &ch);getchar()不会忽略起首的空白字符,总是返回当前读取的第一个字符,无论是否为空格。如果读取失败,返回常量 EOF,由于 EOF 通常是-1,所以返回值的类型要设为 int,而不是 char。由于getchar()返回读取的字符,所以可以用在循环条件之中。while (getchar() != '\n') ;上面示例中,只有读到的字符等于换行符(\n),才会退出循环,常用来跳过某行。while循环的循环体没有任何语句,表示对该行不执行任何操作。下面的例子是计算某一行的字符长度。int len = 0; while(getchar() != '\n') len++;上面示例中,getchar()每读取一个字符,长度变量len就会加1,直到读取到换行符为止,这时len就是该行的字符长度。下面的例子是跳过空格字符。while ((ch = getchar()) == ' ') ;上面示例中,结束循环后,变量ch等于第一个非空格字符。(2)putchar()putchar()函数将它的参数字符输出到屏幕,等同于使用printf()输出一个字符。它的原型定义在头文件stdio.h。putchar(ch); // 等同于 printf("%c", ch);操作成功时,putchar()返回输出的字符,否则返回常量 EOF。(3)小结由于getchar()和putchar()这两个函数的用法,要比scanf()和printf()更简单,而且通常是用宏来实现,所以要比scanf()和printf()更快。如果操作单个字符,建议优先使用这两个函数。puts()puts()函数用于将参数字符串显示在屏幕(stdout)上,并且自动在字符串末尾添加换行符。它的原型定义在头文件stdio.h。puts("Here are some messages:"); puts("Hello World");上面示例中,puts()在屏幕上输出两行内容。写入成功时,puts()返回一个非负整数,否则返回常量 EOF。gets()gets()函数以前用于从stdin读取整行输入,现在已经被废除了,仍然放在这里介绍一下。该函数读取用户的一行输入,不会跳过起始处的空白字符,直到遇到换行符为止。这个函数会丢弃换行符,将其余字符放入参数变量,并在这些字符的末尾添加一个空字符\0,使其成为一个字符串。它经常与puts()配合使用。char words[81]; puts("Enter a string, please"); gets(words);上面示例使用puts()在屏幕上输出提示,然后使用gets()获取用户的输入。由于gets()获取的字符串,可能超过字符数组变量的最大长度,有安全风险,建议不要使用,改为使用fgets()。
2023年09月20日
3 阅读
0 评论
0 点赞
2023-09-20
预处理器(Preprocessor)
预处理器(Preprocessor)简介C 语言编译器在编译程序之前,会先使用预处理器(preprocessor)处理代码。预处理器首先会清理代码,进行删除注释、多行语句合成一个逻辑行等工作。然后,执行#开头的预处理指令。本章介绍 C 语言的预处理指令。预处理指令可以出现在程序的任何地方,但是习惯上,往往放在代码的开头部分。每个预处理指令都以#开头,放在一行的行首,指令前面可以有空白字符(比如空格或制表符)。#和指令的其余部分之间也可以有空格,但是为了兼容老的编译器,一般不留空格。所有预处理指令都是一行的,除非在行尾使用反斜杠,将其折行。指令结尾处不需要分号。define#define是最常见的预处理指令,用来将指定的词替换成另一个词。它的参数分成两个部分,第一个参数就是要被替换的部分,其余参数是替换后的内容。每条替换规则,称为一个宏(macro)。#define MAX 100上面示例中,#define指定将源码里面的MAX,全部替换成100。MAX就称为一个宏。宏的名称不允许有空格,而且必须遵守 C 语言的变量命名规则,只能使用字母、数字与下划线(_),且首字符不能是数字。宏是原样替换,指定什么内容,就一模一样替换成什么内容。#define HELLO "Hello, world" // 相当于 printf("%s", "Hello, world"); printf("%s", HELLO);上面示例中,宏HELLO会被原样替换成"Hello, world"。#define指令可以出现在源码文件的任何地方,从指令出现的地方到文件末尾都有效。习惯上,会将#define放在源码文件的头部。它的主要好处是,会使得程序的可读性更好,也更容易修改。#define指令从#开始,一直到换行符为止。如果整条指令过长,可以在折行处使用反斜杠,延续到下一行。#define OW "C programming language is invented \ in 1970s."上面示例中,第一行结尾的反斜杠将#define指令拆成两行。#define允许多重替换,即一个宏可以包含另一个宏。#define TWO 2 #define FOUR TWO*TWO上面示例中,FOUR会被替换成2*2。注意,如果宏出现在字符串里面(即出现在双引号中),或者是其他标识符的一部分,就会失效,并不会发生替换。#define TWO 2 // 输出 TWO printf("TWO\n"); // 输出 22 const TWOs = 22; printf("%d\n", TWOs);上面示例中,双引号里面的TWO,以及标识符TWOs,都不会被替换。同名的宏可以重复定义,只要定义是相同的,就没有问题。如果定义不同,就会报错。// 正确 #define FOO hello #define FOO hello // 报错 #define BAR hello #define BAR world上面示例中,宏FOO没有变化,所以可以重复定义,宏BAR发生了变化,就报错了。带参数的宏基本用法宏的强大之处在于,它的名称后面可以使用括号,指定接受一个或多个参数。#define SQUARE(X) X*X上面示例中,宏SQUARE可以接受一个参数X,替换成X*X。注意,宏的名称与左边圆括号之间,不能有空格。这个宏的用法如下。// 替换成 z = 2*2; z = SQUARE(2);这种写法很像函数,但又不是函数,而是完全原样的替换,会跟函数有不一样的行为。#define SQUARE(X) X*X // 输出19 printf("%d\n", SQUARE(3 + 4));上面示例中,SQUARE(3 + 4)如果是函数,输出的应该是49(7*7);宏是原样替换,所以替换成3 + 4*3 + 4,最后输出19。可以看到,原样替换可能导致意料之外的行为。解决办法就是在定义宏的时候,尽量多使用圆括号,这样可以避免很多意外。#define SQUARE(X) ((X) * (X))上面示例中,SQUARE(X)替换后的形式,有两层圆括号,就可以避免很多错误的发生。宏的参数也可以是空的。#define getchar() getc(stdin)上面示例中,宏getchar()的参数就是空的。这种情况其实可以省略圆括号,但是加上了,会让它看上去更像函数。一般来说,带参数的宏都是一行的。下面是两个例子。#define MAX(x, y) ((x)>(y)?(x):(y)) #define IS_EVEN(n) ((n)%2==0)如果宏的长度过长,可以使用反斜杠(\)折行,将宏写成多行。#define PRINT_NUMS_TO_PRODUCT(a, b) { \ int product = (a) * (b); \ for (int i = 0; i < product; i++) { \ printf("%d\n", i); \ } \ }上面示例中,替换文本放在大括号里面,这是为了创造一个块作用域,避免宏内部的变量污染外部。带参数的宏也可以嵌套,一个宏里面包含另一个宏。#define QUADP(a, b, c) ((-(b) + sqrt((b) * (b) - 4 * (a) * (c))) / (2 * (a))) #define QUADM(a, b, c) ((-(b) - sqrt((b) * (b) - 4 * (a) * (c))) / (2 * (a))) #define QUAD(a, b, c) QUADP(a, b, c), QUADM(a, b, c)上面示例是一元二次方程组求解的宏,由于存在正负两个解,所以宏QUAD先替换成另外两个宏QUADP和QUADM,后者再各自替换成一个解。那么,什么时候使用带参数的宏,什么时候使用函数呢?一般来说,应该首先使用函数,它的功能更强、更容易理解。宏有时候会产生意想不到的替换结果,而且往往只能写成一行,除非对换行符进行转义,但是可读性就变得很差。宏的优点是相对简单,本质上是字符串替换,不涉及数据类型,不像函数必须定义数据类型。而且,宏将每一处都替换成实际的代码,省掉了函数调用的开销,所以性能会好一些。另外,以前的代码大量使用宏,尤其是简单的数学运算,为了读懂前人的代码,需要对它有所了解。#运算符,##运算符由于宏不涉及数据类型,所以替换以后可能为各种类型的值。如果希望替换后的值为字符串,可以在替换文本的参数前面加上#。#define STR(x) #x // 等同于 printf("%s\n", "3.14159"); printf("%s\n", STR(3.14159));上面示例中,STR(3.14159)会被替换成3.14159。如果x前面没有#,这会被解释成一个浮点数,有了#以后,就会被转换成字符串。下面是另一个例子。#define XNAME(n) "x"#n // 输出 x4 printf("%s\n", XNAME(4));上面示例中,#n指定参数输出为字符串,再跟前面的字符串结合,最终输出为"x4"。如果不加#,这里实现起来就很麻烦了。如果替换后的文本里面,参数需要跟其他标识符连在一起,组成一个新的标识符,可以使用##运算符。它起到粘合作用,将参数“嵌入”一个标识符之中。#define MK_ID(n) i##n上面示例中,n是宏MK_ID的参数,这个参数需要跟标识符i粘合在一起,这时i和n之间就要使用##运算符。下面是这个宏的用法示例。int MK_ID(1), MK_ID(2), MK_ID(3); // 替换成 int i1, i2, i3;上面示例中,替换后的文本i1、i2、i3是三个标识符,参数n是标识符的一部分。从这个例子可以看到,##运算符的一个主要用途是批量生成变量名和标识符。不定参数的宏宏的参数还可以是不定数量的(即不确定有多少个参数),...表示剩余的参数。#define X(a, b, ...) (10*(a) + 20*(b)), __VA_ARGS__上面示例中,X(a, b, ...)表示X()至少有两个参数,多余的参数使用...表示。在替换文本中,__VA_ARGS__代表多余的参数(每个参数之间使用逗号分隔)。下面是用法示例。X(5, 4, 3.14, "Hi!", 12) // 替换成 (10*(5) + 20*(4)), 3.14, "Hi!", 12注意,...只能替代宏的尾部参数,不能写成下面这样。// 报错 #define WRONG(X, ..., Y) #X #__VA_ARGS__ #Y上面示例中,...替代中间部分的参数,这是不允许的,会报错。__VA_ARGS__前面加上一个#号,可以让输出变成一个字符串。#define X(...) #__VA_ARGS__ printf("%s\n", X(1,2,3)); // Prints "1, 2, 3"undef#undef指令用来取消已经使用#define定义的宏。#define LIMIT 400 #undef LIMIT上面示例的undef指令取消已经定义的宏LIMIT,后面就可以重新用 LIMIT 定义一个宏。有时候想重新定义一个宏,但不确定是否以前定义过,就可以先用#undef取消,然后再定义。因为同名的宏如果两次定义不一样,会报错,而#undef的参数如果是不存在的宏,并不会报错。GCC 的-U选项可以在命令行取消宏的定义,相当于#undef。$ gcc -ULIMIT foo.c上面示例中的-U参数,取消了宏LIMIT,相当于源文件里面的#undef LIMIT。include#include指令用于编译时将其他源码文件,加载进入当前文件。它有两种形式。// 形式一 #include <foo.h> // 加载系统提供的文件 // 形式二 #include "foo.h" // 加载用户提供的文件形式一,文件名写在尖括号里面,表示该文件是系统提供的,通常是标准库的库文件,不需要写路径。因为编译器会到系统指定的安装目录里面,去寻找这些文件。形式二,文件名写在双引号里面,表示该文件由用户提供,具体的路径取决于编译器的设置,可能是当前目录,也可能是项目的工作目录。如果所要包含的文件在其他位置,就需要指定路径,下面是一个例子。#include "/usr/local/lib/foo.h"GCC 编译器的-I参数,也可以用来指定include命令中用户文件的加载路径。$ gcc -Iinclude/ -o code code.c上面命令中,-Iinclude/指定从当前目录的include子目录里面,加载用户自己的文件。#include最常见的用途,就是用来加载包含函数原型的头文件(后缀名为.h),参见《多文件编译》一章。多个#include指令的顺序无关紧要,多次包含同一个头文件也是合法的。if...#endif#if...#endif指令用于预处理器的条件判断,满足条件时,内部的行会被编译,否则就被编译器忽略。#if 0 const double pi = 3.1415; // 不会执行 #endif上面示例中,#if后面的0,表示判断条件不成立。所以,内部的变量定义语句会被编译器忽略。#if 0这种写法常用来当作注释使用,不需要的代码就放在#if 0里面。#if后面的判断条件,通常是一个表达式。如果表达式的值不等于0,就表示判断条件为真,编译内部的语句;如果表达式的值等于0,表示判断条件为伪,则忽略内部的语句。#if...#endif之间还可以加入#else指令,用于指定判断条件不成立时,需要编译的语句。#define FOO 1 #if FOO printf("defined\n"); #else printf("not defined\n"); #endif上面示例中,宏FOO如果定义过,会被替换成1,从而输出defined,否则输出not defined。如果有多个判断条件,还可以加入#elif命令。#if HAPPY_FACTOR == 0 printf("I'm not happy!\n"); #elif HAPPY_FACTOR == 1 printf("I'm just regular\n"); #else printf("I'm extra happy!\n"); #endif上面示例中,通过#elif指定了第二重判断。注意,#elif的位置必须在#else之前。如果多个判断条件皆不满足,则执行#else的部分。没有定义过的宏,等同于0。因此如果UNDEFINED是一个没有定义过的宏,那么#if UNDEFINED为伪,而#if !UNDEFINED为真。#if的常见应用就是打开(或关闭)调试模式。#define DEBUG 1 #if DEBUG printf("value of i : %d\n", i); printf("value of j : %d\n", j); #endif上面示例中,通过将DEBUG设为1,就打开了调试模式,可以输出调试信息。GCC 的-D参数可以在编译时指定宏的值,因此可以很方便地打开调试开关。$ gcc -DDEBUG=1 foo.c上面示例中,-D参数指定宏DEBUG为1,相当于在代码中指定#define DEBUG 1。ifdef...#endif#ifdef...#endif指令用于判断某个宏是否定义过。有时源码文件可能会重复加载某个库,为了避免这种情况,可以在库文件里使用#define定义一个空的宏。通过这个宏,判断库文件是否被加载了。#define EXTRA_HAPPY上面示例中,EXTRA_HAPPY就是一个空的宏。然后,源码文件使用#ifdef...#endif检查这个宏是否定义过。#ifdef EXTRA_HAPPY printf("I'm extra happy!\n"); #endif上面示例中,#ifdef检查宏EXTRA_HAPPY是否定义过。如果已经存在,表示加载过库文件,就会打印一行提示。#ifdef可以与#else指令配合使用。#ifdef EXTRA_HAPPY printf("I'm extra happy!\n"); #else printf("I'm just regular\n"); #endif上面示例中,如果宏EXTRA_HAPPY没有定义过,就会执行#else的部分。#ifdef...#else...#endif可以用来实现条件加载。#ifdef MAVIS #include "foo.h" #define STABLES 1 #else #include "bar.h" #define STABLES 2 #endif上面示例中,通过判断宏MAVIS是否定义过,实现加载不同的头文件。defined 运算符上一节的#ifdef指令,等同于#if defined。#ifdef FOO // 等同于 #if defined FOO上面示例中,defined是一个预处理运算符,如果它的参数是一个定义过的宏,就会返回1,否则返回0。使用这种语法,可以完成多重判断。#if defined FOO x = 2; #elif defined BAR x = 3; #endif这个运算符的一个应用,就是对于不同架构的系统,加载不同的头文件。#if defined IBMPC #include "ibmpc.h" #elif defined MAC #include "mac.h" #else #include "general.h" #endif上面示例中,不同架构的系统需要定义对应的宏。代码根据不同的宏,加载对应的头文件。ifndef...#endif#ifndef...#endif指令跟#ifdef...#endif正好相反。它用来判断,如果某个宏没有被定义过,则执行指定的操作。#ifdef EXTRA_HAPPY printf("I'm extra happy!\n"); #endif #ifndef EXTRA_HAPPY printf("I'm just regular\n"); #endif上面示例中,针对宏EXTRA_HAPPY是否被定义过,#ifdef和#ifndef分别指定了两种情况各自需要编译的代码。#ifndef常用于防止重复加载。举例来说,为了防止头文件myheader.h被重复加载,可以把它放在#ifndef...#endif里面加载。#ifndef MYHEADER_H #define MYHEADER_H #include "myheader.h" #endif上面示例中,宏MYHEADER_H对应文件名myheader.h的大写。只要#ifndef发现这个宏没有被定义过,就说明该头文件没有加载过,从而加载内部的代码,并会定义宏MYHEADER_H,防止被再次加载。#ifndef等同于#if !defined。#ifndef FOO // 等同于 #if !defined FOO预定义宏C 语言提供一些预定义的宏,可以直接使用。__DATE__:编译日期,格式为“Mmm dd yyyy”的字符串(比如 Nov 23 2021)。__TIME__:编译时间,格式为“hh:mm:ss”。__FILE__:当前文件名。__LINE__:当前行号。__func__:当前正在执行的函数名。该预定义宏必须在函数作用域使用。__STDC__:如果被设为1,表示当前编译器遵循 C 标准。__STDC_HOSTED__:如果被设为1,表示当前编译器可以提供完整的标准库;否则被设为0(嵌入式系统的标准库常常是不完整的)。__STDC_VERSION__:编译所使用的 C 语言版本,是一个格式为yyyymmL的长整数,C99 版本为“199901L”,C11 版本为“201112L”,C17 版本为“201710L”。下面示例打印这些预定义宏的值。#include <stdio.h> int main(void) { printf("This function: %s\n", __func__); printf("This file: %s\n", __FILE__); printf("This line: %d\n", __LINE__); printf("Compiled on: %s %s\n", __DATE__, __TIME__); printf("C Version: %ld\n", __STDC_VERSION__); } /* 输出如下 This function: main This file: test.c This line: 7 Compiled on: Mar 29 2021 19:19:37 C Version: 201710 */line#line指令用于覆盖预定义宏__LINE__,将其改为自定义的行号。后面的行将从__LINE__的新值开始计数。// 将下一行的行号重置为 300 #line 300上面示例中,紧跟在#line 300后面一行的行号,将被改成300,其后的行会在300的基础上递增编号。#line还可以改掉预定义宏__FILE__,将其改为自定义的文件名。#line 300 "newfilename"上面示例中,下一行的行号重置为300,文件名重置为newfilename。error#error指令用于让预处理器抛出一个错误,终止编译。#if __STDC_VERSION__ != 201112L #error Not C11 #endif上面示例指定,如果编译器不使用 C11 标准,就中止编译。GCC 编译器会像下面这样报错。$ gcc -std=c99 newish.c newish.c:14:2: error: #error Not C11上面示例中,GCC 使用 C99 标准编译,就报错了。#if INT_MAX < 100000 #error int type is too small #endif上面示例中,编译器一旦发现INT类型的最大值小于100,000,就会停止编译。#error指令也可以用在#if...#elif...#else的部分。#if defined WIN32 // ... #elif defined MAC_OS // ... #elif defined LINUX // ... #else #error NOT support the operating system #endifpragma#pragma指令用来修改编译器属性。// 使用 C99 标准 #pragma c9x on上面示例让编译器以 C99 标准进行编译。
2023年09月20日
3 阅读
0 评论
0 点赞
1
...
8
9
10
...
14