首页
前端面试题
前端报错总结
电子书
更多
插件下载
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语言入门
标准库
嵌入式
页面
前端面试题
前端报错总结
电子书
插件下载
搜索到
104
篇与
的结果
2023-09-20
变量说明符
变量说明符C 语言允许声明变量的时候,加上一些特定的说明符(specifier),为编译器提供变量行为的额外信息。它的主要作用是帮助编译器优化代码,有时会对程序行为产生影响。constconst说明符表示变量是只读的,不得被修改。const double PI = 3.14159; PI = 3; // 报错上面示例里面的const,表示变量PI的值不应改变。如果改变的话,编译器会报错。对于数组,const表示数组成员不能修改。const int arr[] = {1, 2, 3, 4}; arr[0] = 5; // 报错上面示例中,const使得数组arr的成员无法修改。对于指针变量,const有两种写法,含义是不一样的。如果const在*前面,表示指针指向的值不可修改。// const 表示指向的值 *x 不能修改 int const * x // 或者 const int * x下面示例中,对x指向的值进行修改导致报错。int p = 1 const int* x = &p; (*x)++; // 报错如果const在*后面,表示指针包含的地址不可修改。// const 表示地址 x 不能修改 int* const x下面示例中,对x进行修改导致报错。int p = 1 int* const x = &p; x++; // 报错这两者可以结合起来。const char* const x;上面示例中,指针变量x指向一个字符串。两个const意味着,x包含的内存地址以及x指向的字符串,都不能修改。const的一个用途,就是防止函数体内修改函数参数。如果某个参数在函数体内不会被修改,可以在函数声明时,对该参数添加const说明符。这样的话,使用这个函数的人看到原型里面的const,就知道调用函数前后,参数数组保持不变。void find(const int* arr, int n);上面示例中,函数find的参数数组arr有const说明符,就说明该数组在函数内部将保持不变。有一种情况需要注意,如果一个指针变量指向const变量,那么该指针变量也不应该被修改。const int i = 1; int* j = &i; *j = 2; // 报错上面示例中,j是一个指针变量,指向变量i,即j和i指向同一个地址。j本身没有const说明符,但是i有。这种情况下,j指向的值也不能被修改。staticstatic说明符对于全局变量和局部变量有不同的含义。(1)用于局部变量(位于块作用域内部)。static用于函数内部声明的局部变量时,表示该变量的值会在函数每次执行后得到保留,下次执行时不会进行初始化,就类似于一个只用于函数内部的全局变量。由于不必每次执行函数时,都对该变量进行初始化,这样可以提高函数的执行速度,详见《函数》一章。(2)用于全局变量(位于块作用域外部)。static用于函数外部声明的全局变量时,表示该变量只用于当前文件,其他源码文件不可以引用该变量,即该变量不会被链接(link)。static修饰的变量,初始化时,值不能等于变量,必须是常量。int n = 10; static m = n; // 报错上面示例中,变量m有static修饰,它的值如果等于变量n,就会报错,必须等于常量。只在当前文件里面使用的函数,也可以声明为static,表明该函数只在当前文件使用,其他文件可以定义同名函数。static int g(int i);autoauto说明符表示该变量的存储,由编译器自主分配内存空间,且只存在于定义时所在的作用域,退出作用域时会自动释放。由于只要不是extern的变量(外部变量),都是由编译器自主分配内存空间的,这属于默认行为,所以该说明符没有实际作用,一般都省略不写。auto int a; // 等同于 int a;externextern说明符表示,该变量在其他文件里面声明,没有必要在当前文件里面为它分配空间。通常用来表示,该变量是多个文件共享的。extern int a;上面代码中,a是extern变量,表示该变量在其他文件里面定义和初始化,当前文件不必为它分配存储空间。但是,变量声明时,同时进行初始化,extern就会无效。// extern 无效 extern int i = 0; // 等同于 int i = 0;上面代码中,extern对变量初始化的声明是无效的。这是为了防止多个extern对同一个变量进行多次初始化。函数内部使用extern声明变量,就相当于该变量是静态存储,每次执行时都要从外部获取它的值。函数本身默认是extern,即该函数可以被外部文件共享,通常省略extern不写。如果只希望函数在当前文件可用,那就需要在函数前面加上static。extern int f(int i); // 等同于 int f(int i);registerregister说明符向编译器表示,该变量是经常使用的,应该提供最快的读取速度,所以应该放进寄存器。但是,编译器可以忽略这个说明符,不一定按照这个指示行事。register int a;上面示例中,register提示编译器,变量a会经常用到,要为它提供最快的读取速度。register只对声明在代码块内部的变量有效。设为register的变量,不能获取它的地址。register int a; int *p = &a; // 编译器报错上面示例中,&a会报错,因为变量a可能放在寄存器里面,无法获取内存地址。如果数组设为register,也不能获取整个数组或任一个数组成员的地址。register int a[] = {11, 22, 33, 44, 55}; int p = a; // 报错 int a = *(a + 2); // 报错历史上,CPU 内部的缓存,称为寄存器(register)。与内存相比,寄存器的访问速度快得多,所以使用它们可以提高速度。但是它们不在内存之中,所以没有内存地址,这就是为什么不能获取指向它们的指针地址。现代编译器已经有巨大的进步,会尽可能优化代码,按照自己的规则决定怎么利用好寄存器,取得最佳的执行速度,所以可能会忽视代码里面的register说明符,不保证一定会把这些变量放到寄存器。volatilevolatile说明符表示所声明的变量,可能会预想不到地发生变化(即其他程序可能会更改它的值),不受当前程序控制,因此编译器不要对这类变量进行优化,每次使用时都应该查询一下它的值。硬件设备的编程中,这个说明符很常用。volatile int foo; volatile int* bar;volatile的目的是阻止编译器对变量行为进行优化,请看下面的例子。int foo = x; // 其他语句,假设没有改变 x 的值 int bar = x;上面代码中,由于变量foo和bar都等于x,而且x的值也没有发生变化,所以编译器可能会把x放入缓存,直接从缓存读取值(而不是从 x 的原始内存位置读取),然后对foo和bar进行赋值。如果x被设定为volatile,编译器就不会把它放入缓存,每次都从原始位置去取x的值,因为在两次读取之间,其他程序可能会改变x。restrictrestrict说明符允许编译器优化某些代码。它只能用于指针,表明该指针是访问数据的唯一方式。int* restrict pt = (int*) malloc(10 * sizeof(int));上面示例中,restrict表示变量pt是访问 malloc 所分配内存的唯一方式。下面例子的变量foo,就不能使用restrict修饰符。int foo[10]; int* bar = foo;上面示例中,变量foo指向的内存,可以用foo访问,也可以用bar访问,因此就不能将foo设为 restrict。如果编译器知道某块内存只能用一个方式访问,可能可以更好地优化代码,因为不用担心其他地方会修改值。restrict用于函数参数时,表示参数的内存地址之间没有重叠。void swap(int* restrict a, int* restrict b) { int t; t = *a; *a = *b; *b = t; }上面示例中,函数参数声明里的restrict表示,参数a和参数b的内存地址没有重叠。
2023年09月20日
4 阅读
0 评论
0 点赞
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 点赞
1
...
12
13
14
...
35