镇楼图

Pixiv:もねてぃ

这次主要解决输入、输出、文件操作等的问题.

如何从原理上解决输入/输出,如何用C代码进行文件操作,如何获取数据,处理数据,如何截取csv文件等这些基本问题都可以在这里解决。

超长文预警!!!

先来认识一个常用的宏——EOF

EOF用来表示读写失败。

==================

〇、流是什么?

要想实现输入输出必须要使用流来控制。

【流】是一种抽象的概念,不同的【流】具有统一的一些特征,通过【流】可以控制键盘、文件、屏幕。

在C语言中有三种【流】分别是

stdin 标准输入流:通过键盘来输入数据

stdout 标准输出流:用来输出数据

stderr 标准错误流:用来输出一些error错误或者warning警告

==================

一、流的特征

①读 / 写 Read / Write

这个流是只读,只写 还是 读写 的。

具有只读的流只能读取,而无法写入

具有只写的流只能写入而无法读取

具有读写的流可以在写入的同时进行读取

②文本 / 二进制 text / binary

text模式与binary模式最大的区别在于处理换行符的问题上,使用text模式会把\n或\r看成一个换行符,而binary模式只会把\n看成两个字符。

③缓冲区 buffer

缓冲区是为了解决CPU与IO设备的速度差距问题。
缓冲区具有三种模式,具有一定空间大小
在后面你将见到buffer的用处

④操作对象 orientation

刚打开时,流并不会指定对象

当进行输入输出操作时,流会指向对象。

在C语言里只有两种对象

(1)面向字节(byte-oriented)

(2)面向宽字节(wide-oriented)

大部分所接触的操作对象都是byte只有使用C语言标准库[wchar.h][]中才定义了宽字节

⑤指示器

具有三种指示器,分别是文件尾指示器(eof指示器)、位置指示器、错误状态指示器(error指示器)

从名字上你也能理解指示器是做什么的

位置指示器用来判断当前流的位置在哪

文件尾指示器用来判断位置是否在文件末尾

错误状态指示器用来判断当前流有没有错误,是什么错误

==================

二、文件打开关闭

①fopen ( const char* filename,const char* mode )

作用:

打开文件

参数:

filename

文件路径,这个文件路径可以是.../test.txt相对路径,也可以是绝对路径

mode

在C语言里提供了3种最基本的模式,然后外加+b可以构成12种模式

模式 含义 文件存在时 文件不存在时
r 只读 从头读取 打开失败
w 只写 从头写入(覆盖) 创建新文件
a 追加 末尾写入 创建新文件
r+ 扩展只读 从头读写 报错
w+ 扩展只写 从头读写(覆盖) 创建新文件
a+ 扩展追加 末尾读写 创建新文件

b表示为binary模式,可以和其他模式混用比如r+bwbab

返回值

若成功,则返回文件流的指针

若失败,则返回NULL并设置errno

注:用 b 可以将text模式更改为binary模式;

使用w模式是会将所有text清空再进行写入,使用r+是将写入的字符进行替换

②fclose ( FILE* stream )

作用:

关闭文件

参数

stream文件指针

返回

若成功,则返回0

若失败,则返回EOF并设置errno

---hello.txt-------------
Hello World
你好鸭~

---hello.c----------------
#include<stdio.h>
#include<stdlib.h>

int main(void){
    FILE* fp = fopen("hello.txt","r");
    int c;
    if(fp == NULL){
        printf("打开失败");
        exit(EXIT_FAILURE);
    }

    while((c = fgetc(fp)) != EOF){
        putchar(c);
        //这是输出函数,具体作用会在下面说明
    }
    fclose(fp);
}

③freopen ( const char* filename, const char* mode , FILE* stream )

这是非常非常非常非常有用的函数!!!

其作用就是

​ 试图关闭一个stream流的关联的文件,然后这个流以另外mode的方式去打开新的文件

一般来说这个会用在stdin、stdout、stderr中,文件一般不太会用到,转移本身就很容易操作的文件流意义不是很大。

stdin是由键盘决定,stderr、stdout会输出在屏幕上,如果想要输入输出都在文件上那就要用到这个函数

你想想在PTA里有一道数组题,你想怎么测试

你真的能做到这种输入吗?

大多数人应该只会在PTA平台上测试,因为你真要在程序里输入可能会变成以下“惨案”

不仅输入麻烦还得要警惕保证不会输错,还要考虑缓冲区问题(之后会说明)这实在太糟糕了

但如果你在一开始尝试加入这一行代码

freopen("test.txt","r",stdin);

然后在test.txt里写下测试数据

然后运行一下

神奇的一幕出现了,原来我只要在test.txt里输入测试数据就成了!

这就是freopen的功能,原本要在键盘上接收的数据跑到了test.txt上。

同理也可以用到stderr、stdout上,让原本要输出在黑窗口上的数据输出到文件里。

==================

三、文件读取

①fgetc ( FILE * stream )

getc ( FILE * stream )

fgetc是函数,getc是宏,这两种功能一模一样只是具体细节略有不同。

getc作为宏可以避免fgetc函数调用的堆栈,速度略微快一些;但getc是宏,所以为了防止bug不能使用带有副作用的参数

副作用的参数是指除了有传递作用还能进行一次运算的参数,比如fp++这样的参数

作用:

读取当前字符并推进位置指示器

若失败则返回EOF

② fgets ( char *s , int size , FILE * stream )

作用

从stream中读取size-1个字符然后存储到s中 。若读取中遇到EOF、\n则会结束本次读取

参数

​ s 字符指针

​ size 要读取的字符数(包括'\0'

​ stream FILE指针

返回

若调用成功,则返回s所指向的内存地址

若读取中遇到EOF,则设置eof指示器,返回NULL

若还未读取就遇到EOF,s不会发生变化,返回NULL

若读取时错误,则设置error指示器,返回NULL

==================

四、文件写入

① fputc ( int ch , FILE * stream )

putc ( int ch , FILE * stream )

作用:

将字符ch写入到stream中,并推进位置指示器

参数

​ ch 要写入的字符

​ stream FILE指针

返回

若成功写入,返回写入的字符ch

若写入失败,返回EOF

fputc为函数

putc为宏

---hello.txt-------------
Hello world!
    
---test.txt-------------

    
---hello.c----------------
#include<stdio.h>
#include<stdlib.h>

int main(void){
    FILE * fp1,* fp2;
    char c;
    if((fp1 = fopen("hello.txt","r")) == NULL){
        printf("打开失败");
        exit(EXIT_FAILURE);
    }

    if((fp2 = fopen("test.txt","w")) == NULL){
        printf("打开失败");
        exit(EXIT_FAILURE);
    }

    while((c = fgetc(fp1)) != EOF){
		fputc(c,fp2);
		printf("%c",c);
    }

    fclose(fp1);
    fclose(fp2);
}

运行前

只有hello.c、hello.txt两个文件

运行后

多出新的test.txt文件,而且内容和hello.txt一致

② fputs ( const char*s , FILE * stream )

作用

将字符串(不包括'\0')写入到stream中

参数

​ s 字符指针

​ stream FILE指针

返回

若成功,返回非0值

若失败,返回EOF

---test.txt-------------

    
---hello.c----------------
#include<stdio.h>
#include<stdlib.h>

int main(void){
    FILE * fp;

    if((fp = fopen("test.txt","a")) == NULL){
        printf("打开失败");
        exit(EXIT_FAILURE);
    }
	fgets("Hello World\n",fp);
    fgets("你好鸭~",fp);
    
    fclose(fp);
}

==================

五、stdin、stdout流的读取/写入

①int getchar()

作用

​ 从stdin流中读取一个字符

返回

​ 成功时返回读取的字符

​ 失败时为EOF

②int putchar(int ch)

作用

​ 将字符ch写至stdout流中

返回

​ 成功时返回读取的字符

​ 失败时为EOF

③int puts(const char *str)

作用

​ 将字符串str写至stdout流中

返回

​ 成功时返回非负值

​ 失败时为EOF

int ch = getchar();
putchar(ch);
char *s1 = "123456789\n";
char s2[] = "123456789";
puts(s1);
puts(s2);

==================

六、格式标签的构成

我们清楚输入输出要么是字符串、要么是宽字符串,那么有什么方法可以输入输出一些字符串以外的一些数据呢?

格式化正是为了解决输入输出其他数据类型而有的特殊语法

这是将其他数据类型转换为字符型的重要方法

同时这也是用来输入变量的重要方法

比如printf("%d",n)那么就可以将整数型的n转换为字符型从而进行输入输出

组成部分

%[flags] [width] [.precision] [length] specifier

①说明符specifier

%加上一些字母来表示转换说明符

符号 含义
c 单个字符
s 多个字符
d/i 整数
u 无符号位整数
o 无符号位八进制整数
x/X 无符号位十六进制整数(x表示所涉及的字母小写,X表示所涉及的字母大写)
p 指针
f 浮点数
e/E 科学计数法(e表示所涉及的字母小写,E表示所涉及的字母大写)
a/A 十六进制浮点数(a表示所涉及的字母小写,A表示所涉及的字母大写)
g/G 分为%f和%e/E,按照结果最短的来(g表示所涉及的字母小写,G表示所涉及的字母大写)
注意(1)浮点数的inf和nan

②标志flags

符号 含义
- 默认为右对齐,而加上负号可以将输入输出的数据变为左对齐
+ 强制让正数显示加号
< space > 可插入一个空格
# 与 o、x 或、X 说明符一起使用时,非零值前面会分别显示 0、0x 或 0X
与 e/E 、 f、a/A 一起使用时,会强制输出包含一个小数点的数。默认情况下,若后边没有数字,则不会显示显示小数点
与 g 或 G 一起使用时,结果与使用 e 或 E 时相同,但是尾部的零不会被移除
0 若结果位数少于width则填充0

③宽度width

符号 含义
< numbers > 用数字表示字符的长度。若不足会默认用空格填充。当让可以用flags中的0来填充0
* 会增加一个参数,用变量来表示你需要的 < numbers > 。若不存在参数表示跳过这个格式化的数据

④精度.precision

符号 含义
< numbers > 对于整数,和width的效果一致
对于浮点数,代表小数位数
对于字符串s,代表输入的最大位数
对于字符c,没有任何效果
* 会增加一个参数,用变量来表示你需要的 < numbers >

⑤长度length

由图可以得出l是对应long修饰符,h是对应short修饰符,其余一般也不会用到。

==================

七、格式化读写

①int scanf( const char* format, ... )

作用

从stdin流中读取字符串,读到参数中

参数

​ format 格式字符

返回

​ 返回参数个数

②int printf( const char* format, ... )

作用

输出字符串至stdout流上

参数

​ format 格式字符

返回

​ 返回打印字符数

int a,b;
scanf("%d\n%d",&a,&b);
int c = printf("%d %d",a,b);
printf("%d",c);

③int fscanf( FILE * stream,const char* format, ... )

作用

从FILE文件流中读取字符串,读到参数中

参数

​ FILE* 文件指针

​ format 格式字符

返回

​ 返回参数个数

④int fprintf( const char* format, ... )

作用

输出字符串至FILE文件流上

参数

​ FILE* 文件指针

​ format 格式字符

返回

​ 返回打印字符数

#include<stdio.h> 
#include<stdlib.h>

int main(void){
    FILE *fp;
    if((fp = fopen("date.txt","w")) == NULL){
        printf("打开失败");
        exit(EXIT_FAILURE);
    }
    
    int buf = fscanf(fp,"hp:%d,atk:%d,def:%d",100,30,72);
    printf("打印字符数为%d\n\n\n",buf);
    fclose(fp);
    
    int hp,atk,def;
    if((fp = fopen("date.txt","r")) == NULL){
        printf("打开失败");
        exit(EXIT_FAILURE);
    }
    fscanf(fp,"hp:%d,atk:%d,def:%d",&hp,&atk,&def);
    fclose(fp);
    
    printf("hp是%d,atk是%d,def是%d",hp,atk,def);
    
}

==================

八、位置指示器

现在我们要更进一步来更高效的完成读写操作,貌似到现在位置我们基本上输入输出都从头到尾一个个输入下来的,并没有跳到着输入一个跳到那输入一个。但这种情况也是经常遇到的,现在来操纵一个叫【位置指示器】的东西,它移动到哪,就决定了你在哪读写,在没有这一块的内容的情况下,位置指示器是从头到尾逐步增加的。当你学完这一块内容后也许你能逆着输入。

①long ftell ( FILE* stream )

作用

​ 返回当前在stream文件中的位置指示器的位置。

​ 若操作失败,则会出现-1L

----test.txt----------
Hello World

----hello.c-----------
#include<stdio.h> 

int main(void){
	FILE* fp;
	fp = fopen("test.txt","r");
	printf("当前位置在%ld\n",ftell(fp));
	fgetc(fp);
	fgetc(fp);
	printf("当前位置在%ld\n",ftell(fp));
	for(int i = 0;i < 10;i++) {
		fgetc(fp);
	}
	printf("当前位置在%ld\n",ftell(fp));
	fgetc(fp);
	printf("当前位置在%ld\n",ftell(fp));
}
//结果分别显示0,2,11,11
//由此可以推断在最末尾时无法再推进位置指示器

当ftell在最后时我们便能知道这个所指向的位置就代表了这个文件多大。

②rewind ( FILE*stream )

作用

​ 将位置指示器移动到最开始的位置

③fseek ( FILE*stream , long offset , int origin )

作用

​ 将位置指示器移动到任意指定的位置

​ 计算从origin初始量开始,offset偏移量

\[pos \ = \ origin + offset\\ 通过对初始量的加减来指定偏移位置 \]

参数

​ FILE* 文件指针

​ offset 偏移量

​ origin 初始量

origin参数值 含义
SEEK_SET 文件头
SEEK_CUR 当前位置
SEEK_END 文件尾
返回

​ 成功时为0

​ 失败时为非零

/*在搭配二进制模式、结构体时可以用C构建出一个数据库,然后可以通过fseek函数来查找到指定的数据
这里就不演示了,有兴趣的可以和我探讨探讨

另外这里可以引申出因为不同系统对于字符存放的某些差别导致的可移植性问题。

==================

九、错误处理

在经典C的标准流当中,我们打开一个文件需要使用文件指针,同样,stdin、stdout、stderr同样也是一个指针,我们也可以使用fputs、fprintf、fputc等具有写入功能的函数来输出到屏幕上

fputs("Hello World",stdout);
//要输出到stdout流 / 屏幕上不一定要使用printf,只要具有写入功能的函数都可以指定到stdout流上

当然这里我们侧重讲如何错误处理

①feof ( FILE*stream )

作用

​ 若位置指示器已经达到文件尾,则返回非0

​ 若位置指示器未达到文件尾,则返回0

参数

​ FILE* 文件指针

②errno

在C语言里存在一个errno的错误码,当程序异常时会设置这个宏。

每一个错误码对应着一个错误信息,在C语言中具有一百多个错误码

比如当errno = 27时,对应的错误是FILE TOO LARGE

不过你想使用errno必须要引入头文件errno.h

③ferror ( FILE*stream )

作用

​ 检查流当中的errno是否存在错误

​ 若存在错误,则返回非零

​ 若没错误,则返回0

#include<stdio.h>
#include<errno.h>

int main(void){
	FILE*fp; 
	fp = fopen("18dsafasfaenfadiuaf.txt","r");
    //这是不存在乱打一通的文件名
	if(ferror){
		printf("出错了!\n错误代号为%d",errno);
	}
}
/*但你可能对这个结果并不满意
还要查找errno多麻烦啊!
接下来的perror你将看到更加好用的操作

④perror ( const char*s )

作用

​ 光是知道一个errno的错误码是不够的,你得要一个更加具体地显示错误的方法

​ perror的作用就是能将当前errno所对应的内容打印出来

​ 而参数只是一个提示语

#include<stdio.h>
#include<errno.h>

int main(void){
	FILE*fp; 
	fp = fopen("18dsafasfaenfadiuaf.txt","r");
    //这是不存在乱打一通的文件名
	if(ferror){
		perror("出错了!\n错误信息为");
	}
}
//使用这个函数也意味着不需要直接使用errno.h头文件

⑤clearerr(FILE*stream)

作用

​ 清除errno

//使用这个函数也意味着不需要直接使用errno.h头文件
//也可以使用error = 0来代替这个功能 

==================

十、IO缓冲区

众所周知,CPU的速度和IO设备的速度是天差地别,为了弥补这种速度的差距,引入了缓冲区这一概念。

就在C语言的视角来看,以前你可能是这么认为的

【程序】←→【IO流】

但现在你应该这么看

【程序】←→【缓冲区】←→【IO流】

现在我们研究的对象正是这中间级【缓冲区】

缓冲区本质上就是一个具有一定空间大小的字符数组

比如

#include<stdio.h>

int main(void){
	int a,b,ch;
	scanf("%d,%d",&a,&b);
	ch = getchar();
	switch (ch){
		case '+':
			printf("%d",a+b);
			break;
		case '-':
			printf("%d",a-b);
			break;
		case '*':
			printf("%d",a*b);
			break;
	}
}


这是一个非常简单的计算器程序,可以做加、减、乘的运算。但我们来执行一下程序。

当我输入1,2后程序立刻结束了!

我好像还没用后面的getchar吧

现在我会说这是缓冲区没处理好,而在刚刚接触缓冲区这个概念时我可是完全没看出这哪错了

那么这个过程发生了什么?

首先我们要认识到【缓冲区】到底是什么意思

缓冲区作为程序与流的中间级,起到一个过渡作用。

也就是当程序运行到scanf后我需要再按Enter键才能再运行下去吧。

没错!正是这个Enter酿成了这个程序的惨剧。stdin输入流在输入Enter后会进入到缓冲区,程序继续运行到getchar时便会获取这个Enter的数据。

也就是我们定义的ch接收到的竟然是'\n'那也难怪程序会出错了。

那么这个程序如何改进呢?

很简单

#include<stdio.h>

int main(void){
	int a,b,ch;
	scanf("%d,%d",&a,&b);
    getchar();
    //加上getchar去吸收在缓冲区不必要的字符
	ch = getchar();
	switch (ch){
		case '+':
			printf("%d",a+b);
			break;
		case '-':
			printf("%d",a-b);
			break;
		case '*':
			printf("%d",a*b);
			break;
	}
}

这是缓冲区酿成的第一个问题,然后再来看一个问题

#include<stdio.h>

int main(void){
	FILE*fp;
    fp = fopen("test.txt","w");
    //这里就忽略失败的情况了
    fprintf(fp,"%d %d %d %d",1,2,3,4);
    getchar();
    fclose(fp);
}

一个误区:认为读写函数在调用完成后就立刻读写

这是很自然也是很容易误导人的观点,你可以这么对他说“函数一调用完就结束了鸭~”

好像很有道理,但再一次强调缓冲区的功能,读写操作必须要先经过缓冲区才能到达对面,如果缓冲区都无法通过,那么也是不可能到达对面的。

我们运行不输入任何数据卡在getchar那里看看test.txt是否真的被写入了

很明显没被写入,直到我们输入数据后才被写入

那么我们怎样才能确定缓冲区何时向流输出呢?

这里我们先认识三个函数

在C语言中提供了三种缓存模式,缓存模式能很明确地告诉你缓冲区是怎样向流输出的

——按块缓存(全缓存)

​ 数据一直存在缓冲区内,当缓冲区满溢的时候向流写入数据

——按行缓存(缓冲\n之前的数据)

​ 每读入一行数据,缓冲区向流写入一行数据

——不缓存(不经过缓冲区直接读写)

在默认情况下是全缓存模式

但我们可以通过函数setvbuf来设置缓冲模式

①int setvbuf ( FILE*stream , char * buffer , int mode , size_t size )

作用

​ ■更改stream文件流的mode缓冲模式

​ ■若buffer为NULL,则系统自设的buffer大小重设为size

​ ■若buffer不为NULL,则缓冲区设置为由程序猿设定的空间大小为size的buffer。且成功调用setvbuf后buffer的内容将变得不确定。而且在buffer的生存期结束前要关闭流

参数

​ FILE* 文件指针

​ buffer 指向缓冲区的指针

​ mode 表示缓冲模式

​ size 缓冲区的大小

mode参数 含义
_IOFBF 全缓冲file
_IOLBF 行缓冲line
_IONBF 不缓冲none
返回

​ 成功时为0

​ 失败时为非零

#include<stdio.h>

int main(void){
    setvbuf(stdout,NULL,_IONBF,1024);
    //这里就忽略失败的情况了
	printf("这是无缓冲可以立即输出这行");
	getchar();
}
#include<stdio.h>

int main(void){
    setvbuf(stdout,NULL,_IOFBF,1024);
    //这里就忽略失败的情况了
	printf("这是全缓冲,必须先输入一个字符\n");
	getchar();
    /*while(getchar() != '\n');*/
    printf("%c",getchar());
    /*
    这行代码表明输入的数据
    可能不会是刚刚好是一个字符
    所以会多出来一些数据在stdin的缓冲区等待被输入
    这时候你可以使用以下代码来吸收不必要的数据
    while(getchar() != '\n');
    */
}

②setbuf ( FILE*stream , char * buffer )

作用

​ 设置由程序猿定义的缓冲区

​ 若buffer为NULL,则会关闭缓冲也就是将缓冲模式设置为_IONBF

​ 若buffer不为NULL,则会设置缓冲区,模式默认为_IOFBF

参数

​ FILE* 文件指针

​ buffer 指向缓冲区的指针

/*setbuf和setvbuf相比
少了设置缓冲模式、设置缓冲区大小的参数
这里不再演示了

③fflush ( FILE*stream )

file flush

作用

​ 立即清空某一个流的缓冲区,将这个缓冲区的内容输出到指定流中

返回

​ 若成功,则会返回零

​ 若失败,则会返回EOF

#include<stdio.h>

int main(void){
    setvbuf(stdout,NULL,_IOFBF,1024);
    //这里就忽略失败的情况了
	printf("虽然是全缓冲,但有了fflush函数可以立即输出\n");
    fflush(stdout);
	getchar();
}

因为你学会了缓冲区,现在你可以认识这两个读写函数

④sprintf ( char* str , char const format , ... )

作用

​ 将format写入到字符串str中,注意这个str也可以是缓冲区

参数

​ *str 被写入的字符指针

​ format 格式字符串

返回

​ 输出的字符数

⑤sscanf ( char* str , char const format , ... )

作用

​ 从字符串str中尝试读取format字符串,注意这个str也可以是缓冲区

没错不仅能从IO、文件中读取,还可以从字符串读取

妹想到吧!

参数

​ *str 被写入的字符指针

​ format 格式字符串

返回

​ 参数的数量

==================

十一、文件操作

这一块内容也特别常见,可以以程序的方式执行一些特别常见的操作

①remove ( const char*filename )

作用

​ 删除指定文件filename

​ 若成功,则返回0

​ 若失败,则返回非零

②reaname ( const char* old_filename , const char* new_filename )

作用

​ 将旧文件的名字改成新文件的名字

​ 若成功,则返回0

​ 若失败,则返回非零

③tmpfile ( void )

作用

​ 直接生成一个临时文件

​ 若成功生成,则返回这个临时文件流的指针

​ 若错误,则返回NULL

#include<stdio.h>
#include<stdlib.h>

int main(void){
	FILE*fp; 
    if((fp = tmpfile()) == NULL){
		printf("出错啦!!!");
        exit(EXIT_FAILURE);
    }
	fputs("test",fp);
	char ch[10];
	fseek(fp,0,SEEK_SET);
	fscanf(fp,"%s",ch);
	printf("%s",ch);
    /*getchar();*/
}
//临时文件也将是一个重要的点

==================

LAST、索引 csv 文件

csv文件是一种表格文件,可以通过excel导出得到。

内容已经移植到另一篇随笔里了
点我

==================

THE END

C语言的blog到这里就结束了,我会额外写一篇如何调用、开发SLL、DLL,有兴趣的可以看看。接下来我会开始写数据结构/算法、8086汇编语言、C++。

前端开发还要再等半年,博主正在补数学基础

参考资料

https://fishc.com.cn

http://www.cplusplus.com/reference

https://www.icourse163.org/course/ZJU-200001

posted on 2021-04-13 13:46  摸鱼鱼的尛善  阅读(625)  评论(0编辑  收藏  举报