基础IO相关知识点

文章详细阐述了跨平台性的概念,解释了为何直接使用系统接口会导致语言无法跨平台,并介绍了C语言如何通过条件编译实现跨平台。同时,讨论了文件操作函数如fopen、fwrite、fprintf等的工作原理和使用注意事项,包括文件模式的选择和数据读写。此外,提到了shell中重定向符号>>用于清空文件的机制,并探讨了C库函数与底层系统调用接口如open、write的关系和用法。
摘要由CSDN通过智能技术生成

目录

1、什么是跨平台性

1.1、为什么直接使用系统接口会导致这门语言实现不了跨平台?

1.2、那有些编程语言是怎么实现跨平台的呢?

2、对文件的重新认知

3、下图中的知识点以及相关补充

3.1、FILE*fopen(const char*path,const char*mode)

3.1.1、参数path

3.1.2、参数mode

3.1.3、返回值

3.2、size_t fwrite(const void *ptr, size_t size, size_t nmemb, FILE *stream)

3.2.1、调用fwrite函数将字符串写进某个文件时,需要将字符串末尾的\0也写入文件吗?或者说调用fwrite函数,给形参size传参时,实参strlen(ptr)需要加1吗?

3.3、int fprintf (FILE* stream, const char*format,  。。。)

3.3.1、printf和fprintf的关系

3.4、void perror(const char *s)

3.4.1、char *strerror(int errno)

3.5、int fputs(const char *s, FILE *stream)

3.5.1、EOF是什么?

3.5.2、int puts(const char*s)

3.6、int fread( void *buffer, size_t size, size_t count, FILE *stream )

3.7、 int fclose(FILE * stream)

3.8、前面的一些函数用于二进制文件还是文本文件呢?

4、为什么在shell中输入指令>test.txt可以将test.txt清空呢?

5、C库函数的底层是系统调用接口,几种常见的系统调用接口有哪些呢?

5.1、int open (const char *pathname,int flags)

5.1.1、标记位flagas

5.1.2、使用open函数时存在的问题(文件不存在时不自动创建,open调用失败)

5.2、int open(const char *pathname,int flags,mode_t mode)

5.3、int close(int fd)(注意不一定是真的将文件释放)

5.3、ssize_t write(int fd, const void *buf, size_t count)

5.3.1、产生的问题(再次open时不清空文件之前的内容)

5.3.2、解决方式

5.3.3、解决完清空数据的问题后,该如何解决追加的问题呢?

5.4、ssize_t read(int fd,void *buf,size_t count)

5.4.1、返回值ssize_t

6、对同一个文件先写入数据再读取数据时,读取不到任何数据


1、什么是跨平台性

1.跨平台是指一种语言可以在不同的操作系统中使用。

2.由于硬件比如磁盘,只有操作系统可以访问它,那用户想要访问硬件的时候怎么办呢?答案是提供系统接口。但由于直接使用系统接口的成本比较高,并且直接使用系统接口会导致这门语言实现不了跨平台,所以任何一种语言都会将系统接口封装成可供用户更好地使用的函数接口,所以也导致不同的语言各自封装后形成的接口不一样,但这些不同语言的接口的源头都是系统接口。

1.1、为什么直接使用系统接口会导致这门语言实现不了跨平台?

打个比方,如果C语言写文件的fwrite函数直接用Windows的访问磁盘的系统接口实现,那么将这个用C语言写的文件拿到Linux系统中,由于Linux的访问磁盘的系统接口和Windows不一样,所以这个C语言的程序肯定是运行不了的,所以实现不了跨平台。

简单来说就是因为不同的系统,它们的系统接口不一样,如果一种语言的函数执意用Windows的系统接口封装,那么这种语言就只能在Windows平台使用。

1.2、那有些编程语言是怎么实现跨平台的呢?

就拿C语言中的fwrite函数举例,既然我用Windows的系统接口封装fwrite会导致fwrite只能在Windows机器中使用,那就干脆将不同操作系统的访问磁盘的系统接口全部封装进fwrite函数中,然后通过判断当前是什么操作系统,对此进行条件编译,通过条件编译实现动态裁剪。这样不同的操作系统就都可以使用C语言了。

2、对文件的重新认知

1.站在系统的角度,能够被input读取,或者能够output写出的设备就叫做文件。所以我们之前对文件的认识是狭义的,认为文件就是指普通的磁盘文件。从广义上来说,显示器,键盘,网卡,声卡,显卡,磁盘,几乎所有的外设,都可以称之为文件,只是这些文件不在磁盘上,由于这些外设具有读或者写的功能,所以都称为文件。

2.文件还可以被划分为内存文件和磁盘文件。

3、下图中的知识点以及相关补充

代码如下

c700240bb8104a87845d79a263b43dae.png

运行结果如下

e5797d09d04e4a17a12ce95c17a62c13.png

3.1、FILE*fopen(const char*path,const char*mode)

说一下,fopen只能打开普通文件,不能打开目录文件。

3.1.1、参数path

即想要打开的文件的路径。

3.1.2、参数mode

mode表示以什么模式打开对应的文件。

1,r表示只读,如果文件不存在则fopen失败。文件打开后从文件的起始开始读。从起始开始读表示返回的指针指向文件的开始。

2,r+表示读写,对比w+,如果文件不存在则fopen失败。文件打开后从文件的起始开始读写。

3,w表示只写,每次以w方式打开文件时,都会将文件的内容清空后再打开。如果文件不存在,则会在当前路径,即cwd中创建这个文件,然后再打开。文件打开后从文件的起始开始读。

4,w+表示读写,每次以w+的方式打开文件时,都会将文件的内容清空后再打开。如果文件不存在,则会在当前路径,即cwd中创建这个文件,然后再打开。文件打开后从文件的起始开始读写。

5,a(append)表示写,可以在文件中追加数据,可以在文件末尾写入一些数据来更新文件。文件打开后从文件的末尾开始写。

6,a+表示读写,在文件中追加数据并更新它,这意味着它可以在最后写入,同时也可以读取文件。在只写日志的实际情况下,a和a+都是合适的,但如果还需要读取文件中的某些内容(以追加模式使用已打开的文件),则需要使用“ a+ ”。文件打开后从文件的末尾开始读写。

3.1.3、返回值

文件顺利打开后,指向该流的指针就会被返回。如果文件打开失败则返回NULL,并把错误码存在error中。

3.2、size_t fwrite(const void *ptr, size_t size, size_t nmemb, FILE *stream)

(说一下,下一段可能说得不太清楚,这里换一种说法,fwrite就是从ptr指向的地址开始算,一共往stream中写入size乘nmemb个字节,比如要写入100字节的数据时,无所谓size和nmemb分别是多少,但相乘后一定得是100。如果size乘nmemb个字节大于了ptr指向的数据所占的字节,则会向stream中写入一些随机数据,比如说ptr指向的数据只有8字节,但size乘nmemb等于100,则把ptr指向的8字节写入stream后,还会把ptr后面的92字节的随机值写进stream)

常见误解,比如ptr指向字符串为linux so easy!,大小为15(加了\0),那么想写两个linux so easy!该怎么办呢?是不是把size设置成14(去掉\0),然后nmemb设置为2即可呢?答案:错!fwrite向文件中写的总字节数为size乘nmemb,如果size乘nmemb大于了ptr指向空间,那么只会输出ptr指向空间后面的不属于ptr的不合法数据,一般表现为写入了很多乱码。所以想要写进文件多少数据,前提是你提供的缓冲区有这么多的数据,即想要写入size乘nmemb大小的内容,前提是ptr指向内容的大小有size乘nmemb,目前因为ptr指向的字符串只有1个linux so easy!,所以调用一次fwrite是不可能写两个linux so easy!的,所以如果想要写两个linux so easy!,就得调用两次fwrite函数。

1.const void *ptr : 指针指向要写出数据的内存首地址 。

2.size_t size : 从代码的可读性角度上说,规定size是要fwrite写入的数据的基本单元的字节大小 , 比如如果是要写入一个int数组,那么size就应该是sizeof(int);但从实际上,如果我想写入100字节,那么无所谓size和nmemb分别是多少,只要他俩相乘等于100即可成功写入。

3.size_t nmemb : 从代码的可读性角度上说,规定nmemb是要fwrite写入数据的 基本单元 的个数 ,比如要写入一个int a【10】时,那么nmemb就应该是10;但从实际上,如果我想写入100字节,那么无所谓size和nmemb分别是多少,只要他俩相乘等于100即可成功写入。但有一个点要注意,就是如果想写入总字节数为20,这时设置size为1,nmemb为20时,那么写入成功后,fwrite的返回值就会是20;而如果size为20,nmemb为1,那么写入成功后fwrite的返回值就是1。

4.FILE *stream : 打开的文件指针 。

5.返回值说明 : size_t 返回值返回的是实际写出到文件的 基本单元个数 。

3.2.1、调用fwrite函数将字符串写进某个文件时,需要将字符串末尾的\0也写入文件吗?或者说调用fwrite函数,给形参size传参时,实参strlen(ptr)需要加1吗?

代码如下

2aee52bc86794a68a1e710b0915598cd.png

情景如上图,答案是不需要+1,因为\0作为字符串结束标志只是C语言的规定,文件不需要遵守,如果给形参size传入strlen(ptr)+1,即把\0也写进文件,那用文本编辑器打开这个被写的文件时,里面会有乱码,因为\0的二进制会被文本编辑器翻译成乱码,如下图中的^@。

程序运行后,log.txt就被创建出并被写入了字符串,文件log.txt的内容如下

7c48c45ea56047a795ff6f1f2e7062a4.png

3.3、int fprintf (FILE* stream, const char*format,  。。。)

1.stream代表向哪个流,即哪个文件输出。

2.format代表要写入流的文本的C字符串,它可以选择性地包含嵌入的格式说明符,这些格式说明符被随后的附加参数中指定的值替换,并按要求格式化。比如有char c【】=“asdfghjkl”;现在使用fprintf(stdout,“%s”,c)即可向文件stdout,即显示器打印字符数组c。

3. 。。。表示可变参数列表,就像printf函数的参数是可变的一样。

4.返回值:成功后,返回写入的字符总数。如果发生写入错误,则会设置错误指示符(ferror)并返回负数。如果在编写宽字符时发生多字节字符编码错误,则将errno设置为EILSEQ,并返回负数。

3.3.1、printf和fprintf的关系

printf函数是fprintf函数的子集,printf只能向stdout文件输出,而fprintf可以指定文件。

3.4、void perror(const char *s)

1.perror()用来将上一个函数发生错误的原因输出到标准错误(stderr)。参数s所指的字符串会先打印出,后面再加上依照全局变量errno的值解析出的表示错误原因的字符串。errno可以认为是退出码或者函数返回值。

2.如下图是perror的模拟实现。

代码如下

9597f703966247889760bc995b1a19e1.png

运行结果如下

3.4.1、char *strerror(int errno)

参数errno就是一个整形的全局变量,在头文件<errno.h>中。

用于将错误代码errno转换为字符串错误信息。比如cout<<strerror(2)就可以观察到退出码2代表的错误信息。

3.5、int fputs(const char *s, FILE *stream)

1.fputs 函数用于将一个字符串写入到指定的文件中,表示字符串结尾的\0不会被一并写入。

2.如果函数调用成功,返回0。

3.如果函数调用失败,返回 EOF。

3.5.1、EOF是什么?

EOF是文件结束标志,是一个宏,值为-1。注意不要和\0搞混了,\0表示字符串的结束标志,而EOF是文件结束标志。

3.5.2、int puts(const char*s)

和fputs函数的区别是puts函数会接收\n,比如等待用户输入时,用户会输入“xxx”,并按回车键\n结束输入,这个回车键\n也被puts函数读取到了,所以最后printf输出的时候会自动换行。

3.6、int fread( void *buffer, size_t size, size_t count, FILE *stream )

常见误解:和fwrite的一样想要从文件读取size *count数据,前提是文件内有这么多的数据,如果size*count大于文件内容的大小,那么只会把文件读完,即读到EOF就结束。

1.void *buffer 参数 : 将文件中的二进制数据读取到该缓冲区中。

2.size_t size 参数 : 从代码的可读性角度上说,规定size是读取的 基本单元 字节大小 , 单位是字节 , 一般是 buffer 缓冲的单位大小,比如 buffer缓冲区是char数组 , 则该参数的值是 sizeof(char) 。如果buffer 缓冲区是 int 数组,则该参数的值是sizeof(int) ;但实际上,如果我要读取100字节,那么无所谓size和count分别是多少,只需要他俩相乘等于100即可成功读取。

3.size_t count 参数 : 从代码的可读性角度上说,规定count是读取的 基本单元 个数 ;但实际上,如果我要读取100字节,那么无所谓size和count分别是多少,只需要他俩相乘等于100即可成功读取。但有一个点要注意,就是如果想读取总字节数为20,这时设置size为1,nmemb为20时,那么读取成功后,fread的返回值就会是20;而如果size为20,nmemb为1,那么读取成功后fread的返回值就是1。

4.FILE *stream 参数 : 文件指针 。

5.size_t 返回值 : 实际从文件中读取的 基本单元 个数 ; 读取的字节数是 基本单元数 * 基本单元字节大小 。

3.7、 int fclose(FILE * stream)

1.fclose用来关闭fopen打开的文件,使用fopen打开的文件,一定要记得使用fclose关闭,否则会出现很多意想不到的情况,例如对文件的更改没有被记录到磁盘上,其他进程无法存取该文件等。

2.注意fclose不一定是真的将文件释放,详情见close函数。

3.8、前面的一些函数用于二进制文件还是文本文件呢?

1.fread / fwrite 函数既可以操作二进制文件又可以操作文本文件。

2.getc / putc 函数,fscanf / fprintf 函数,fgets / fgets 函数只能用于文本文件。

4、为什么在shell中输入指令>test.txt可以将test.txt清空呢?

>表示输出重定向,在shell中输入指令echo ”123“>test.txt可以将123写进文件test.txt,>test.txt可以将文件清空。为什么呢?

可以将>理解成一个用c语言写的可执行文件,可执行文件>中有fopen(“/xx/xxx/test.txt”,“w”)的逻辑,每次以w的模式打开文件都会先将文件清空,然后调用fwrite或者其他接口将指定的字符串写入文件,又因为>test.txt这条指令中的字符串为空,所以先清空了文件,但之后不会向文件写入任何字符串,最后fclose关闭该文件,也就完成了清空一个文件的功能。再次强调fopen后必须fclose,不然会导致文件更新失败,所以也能看出fclose就相当于保存。

5、C库函数的底层是系统调用接口,几种常见的系统调用接口有哪些呢?

5.1、int open (const char *pathname,int flags)

1.头文件有三个,<sys/types.h>,<sys/stat.h>,<fcntl.h>。

2.参数pathname表示路径加文件名。

3.返回值:函数调用失败返回-1,并设置好errno的值。调用成功时返回文件描述符fd,fd为一个整形的值。文件描述符的详情见另一篇文章《文件描述符fd》。

5.1.1、标记位flagas

如fopen函数需要参数mode确定以何种方式(比如r表示只读)打开一个文件,系统调用接口同样需要确定一种打开文件的模式,但和fopen的形参mode只需要一个实参(比如r)不同,open的形参flags需要若干个(即不一定是1个)选项(如下图1红框中的几个宏定义),但flags只有一个值,该如何表示若干个选项呢?

既然open的形参flags需要表示多个选项(即多个宏),那么可以通过位操作将各种选项组合在一起作为实参传给形参flags,每一个选项(即宏)只表示参数flags其中的一个比特位,就如同下图2中的宏ONE只表示整形flags中最低位的比特位,如果参数flags同时需要ONE和TWO两个选项,通过位操作将ONE | TWO传给flags即可,因为按位或的特性就是遇到1则为1,又因为一个宏表示一个比特位,这样flags的值表示的二进制中最低位和次低位都是1,通过一些操作,比如用flags的值和ONE相与(&)后的结果不为0,即可验证选项ONE的存在,函数也就可以进行ONE选项存在时的相关操作。这样可以类比成open函数的某两个选项存在并且执行对应语句。

e66e8095eddc4f4da1ce65e8bf14919e.png

 代码如下

d5b060f037c341ca992285aff45077dd.png

运行结果如下71fc2cfb9a2b4f488eb2d456b538481b.png

5.1.2、使用open函数时存在的问题(文件不存在时不自动创建,open调用失败)

fopen函数以w只写模式打开文件时,文件不存在则会自动创建文件,然后再打开文件。而open函数不一样,如果只传O_WRONLY给flags(即表示只写),如下图,则目标文件不存在时不会自动创建,open函数调用会失败并返回-1。

185e721d41494216aca2fcdad9810996.png

问题:那如何变得和fopen函数一样,以只写(w)模式打开文件时,若文件不存在则自动创建呢?

答案:给参数flags多添加一个选项(即下图红框的宏)即可,如:open(“log.txt”,O_WRONLY | O_CREAT)。

f5b7477a1da94ee6b1fe1890db0322ac.png

新问题:增加O_CREAT这个宏后确实将不存在的文件创建出并打开了,但会发现文件的权限和touch出的文件不一样,如下图,如何解决呢?

300cae4358a34f1faf3efb26ca8a6f3f.png

答案:解决方案是使用另一个open函数,详见标题为5.2的内容。

5.2、int open(const char *pathname,int flags,mode_t mode)

此函数和函数int open(const char *pathname,int flags)不一样,增加了一个参数mode,表示权限。如下图中传给mode的参数为0666,0用于表示数字666为8进制数。

ac7eba5783e449739febc84fad6144e1.png

问题:运行后确实权限变得正常了许多,如下图,但权限依然和我们设置的0666不同,为什么呢?

9c7ce4148db546a9b8e924e830a88abf.png

答案:因为有权限掩码的存在,此时权限掩码为八进制的0002,由于权限掩码中除了属于other组的八进制位为2,其他的位都为0,所以只需要考虑other组的最终权限即可。用户设置的初始权限中(即0666)属于other组的初始权限为八进制的6,即二进制110,而权限掩码(0002)中属于other组的权限为八进制的2,二进制为010。由于权限掩码中出现的权限不能出现在最终权限里,所以经过权限掩码过滤后,other组的最终权限为二进制的100,即八进制的4,即上图属于other组中的r - - 。

问题:如何屏蔽umask呢?

1.在shell界面上使用umask命令,将当前进程的权限掩码修改成0,即清空umask即可。

2.在编写程序时使用umask函数,如下两图。

42c62794a45149cfa99eb7f763f9e873.png

2ed9f21383f247f8ad663f861aec2bc7.png

假如上图程序运行后产生的进程叫pro,则上图中设置的权限掩码只属于pro进程,不会将bash进程(shell程序启动后产生的进程)的权限掩码假设为0002修改成0。

5.3、int close(int fd)(注意不一定是真的将文件释放)

1.函数声明在头文件<unistd.h>中。

2.正如使用fopen函数后必须使用fclose函数收尾,使用open函数后也必须使用close函数进行收尾工作,如下图。

3.使用close不一定是真的将文件释放掉,因为一个进程close一个文件时,只能说明你这个进程不需要这个文件了,但其他进程可能还需要用,所以close本质上只是将fd的引用计数减一,当fd的引用计数为0时,才会真的释放文件。

da9012c184dd4b3199d5e4495ea3ccf0.png

5.3、ssize_t write(int fd, const void *buf, size_t count)

d444d74768eb49e48019b7081e53e6c3.png

使用方式如上图红框,open打开成功后,则write可以向fd对应的log.txt文件中写入对应数据。

5.3.1、产生的问题(再次open时不清空文件之前的内容)

但就如open函数在只写w模式下,不加O_CREAT选项打开不存在的文件时,文件不会被自动创建一样,当open函数在只写w模式下追加数据时,open函数也不会像fopen函数那样先将文件的内容清空,然后再追加数据,而是覆盖掉原来数据的一部分。

代码如下

c73b287251fb47fe894edac9f99af930.png

执行结果如下

3ff29415a7de4e98b9baddb98aa94087.png

如上图,之前文件中的hello write并没有被清除,而是被追加的字符串aa覆盖掉了一部分。

5.3.2、解决方式

b66d043a756d482da283fc3584255e88.png

如上图,添加O_TRUNC选项即可。

从这也能看出fopen的w选项的底层就是上图的open函数加3个选项。

5.3.3、解决完清空数据的问题后,该如何解决追加的问题呢?

6c95d41c2cd84dc2b778cef5b42e7924.png

如上图,将选项从O_TRUNC改为O_APPEND即可。

fopen的a选项的底层实现就是上图中open函数加三个选项。

5.4、ssize_t read(int fd,void *buf,size_t count)

1.从文件描述符fd对应的文件里读取count字节的数据到buf指针指向的空间中。如果buf指向的空间是用于存储字符串,由于使用printf等函数打印字符串时,如果不遇到\0则一直打印,所以要在使用read函数之前将buf指向的空间全初始化成\0,不然之后打印buf时可能会有乱码,如下图。

df8552b1ef5f4e819dfaa91617ccbb42.png

2.当fd是0时,即从键盘文件read时,用户输入的回车,即\n也是会被读取的,所以注意将\n改成\0。

5.4.1、返回值ssize_t

注意ssize_t本质就是long int,是一个有符号整数,不要和size_t混淆了。ssize_t是一个整形,表示实际读到的字节个数。什么意思呢?比如输入的count为60,但等待用户输入时(就像scanf会等待用户输入)只输入6个字节,那么返回值就是6,而不是60。

6、对同一个文件先写入数据再读取数据时,读取不到任何数据

这是因为文件中有一个文件指针,写入完数据后,文件指针是指向刚写入的数据的后面的,此时读取当然读不到任何内容,如果想要读取刚刚写入的数据,就需要把文件指针往前面移动。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值