中铁建设集团华北分公司网站,竞价托管 微竞价,做网站有的浏览器,网站空间是不是服务器其实输入与输出对于不管什么系统的设计都是异常重要的#xff0c;比如设计 C 接口函数#xff0c;首先要设计好输入参数、输出参数和返回值#xff0c;接下来才能开始设计具体的实现过程。C 语言标准库提供的接口功能很有限#xff0c;不像 Python 库。不过想把它用好也不容…其实输入与输出对于不管什么系统的设计都是异常重要的比如设计 C 接口函数首先要设计好输入参数、输出参数和返回值接下来才能开始设计具体的实现过程。C 语言标准库提供的接口功能很有限不像 Python 库。不过想把它用好也不容易本文总结 C 标准库基础 IO 的常见操作和一些特别需要注意的问题如果你觉着自己还不是大神那么请相信我读完全文后你肯定会有不少收获。一、操作句柄打开文件其实就是在操作系统中分配一些资源用于保存该文件的状态信息及文件的标识以后用户程序可以用这个标识做各种读写操作关闭文件则释放占用的资源。打开文件的函数#include FILE *fopen(const char *path, const char *mode);FILE 是 C 标准库定义的结构体类型其包含文件在内核中的标识(文件描述符)、IO 缓冲区和当前读写位置信息调用者不需知道 FILE 的具体成员由库函数内部维护调用者不应该直接访问这些成员。像 FILE* 这样的文件指针称为句柄(Handle)。打开文件操作是对文件资源进行操作的所以有可能打开文件失败所以在打开函数时一定要判断返回值如果失败则返回错误信息以方便快速定位错误。打开文件应该与关闭文件成对存在虽然程序在退出时会释放相应的资源但是对于一个长时间运行服务程序来说经常打开而不关闭文件是会造成进程资源耗尽的因为进程的文件描述符个数是有限的及时关闭文件是个好习惯。关闭文件的函数#include int fclose(FILE *fp);fopen 函数参数 mode 总结r只读文件必须存在。w只写如果不存在则创建存在则覆盖。a追加如果不存在则创建。r允许读和写文件必须存在。w允许读和写文件不存在则创建存在则覆盖。a允许读和追加文件不存在则创建。二、关于stdin/stdout/stderr在用户程序启动时main 函数还没开始执行之前会自动打开三个 FILE* 指针分别是stdin、stdout、stderr这三个文件指针是 libc 中定义的全局变量在 stdio.h 中声明printf 向 stdout 写而 scanf 从 stdin 读用户程序也可以直接使用这三个文件指针。stdin 只用于读操作称为标准输入stdout 只用于写操作称为标准输出stderr 也用于写操作称为标准错误输出通常程序的运行结果打印到标准输出而错误提示打印到标准错误输出一般标准输出和标准错误都是屏幕。通常可以标准输出重定向到一个常规文件而标准错误输出仍然对应终端设备这样就可以将运行结果与错误信息分开。三、以字节为单位的IO函数fgetc 函数从指定的文件中读一个字节getchar从标准输入读一个字节调用 getchar() 相当于 fgetc(stdin)#include int fgetc(FILE *stream);int getchar(void);fputc 函数向指定的文件写入一个字节putchar 向标准输出写一个字节调用 putchar() 相当于调用 fputc(c, stdout)。#include int fputc(int c, FILE *stream);int putchar(int c);参数和返回值类型为什么使用 int 类型可以看到这几个函数的参数和返回值类型都是 int而非 unsigned char 型。因为错误或读到文件末尾时将返回 EOF即 -1如果返回值是 unsigned char(0xff)与实际读到字节 0xff 无法区分如果使用 int 就可以避免这个问题。四、操作读写位置函数当我们在操作文件时有一个叫「文件指针」的家伙来记录当前操作的文件位置比如刚打开文件调用了 1 次 fgetc 后此时文件指针指向了第 1 个字节后边注意是以字节为单位记录的。改变文件指针位置的函数#include int fseek(FILE *stream, long offset, int whence);whence从何处开始移动取值SEEK_SET | SEEK_CUR | SEEK_ENDoffset移动偏移量取值可取正 | 负void rewind(FILE *stream);举几个简单例子fseek(fp, 5, SEEK_SET); // 从文件头向后移动5个字节fseek(fp, 6, SEEK_CUR); // 从当前位置向后移动6个字节fseek(fp, -3, SEEK_END); // 从文件尾向前移动3个字节offset 可正可负负值表示向文件开头的方向移动正值表示向文件尾方向移动如果向前移动的字节数超过文件开头则出错返回如果向后移动的字节数超过了文件末尾再次写入会增加文件尺寸文件空洞字节都是 0$ echo 5678 file.txtfp fopen(file.txt, r);fseek(fp, 10, SEEK_SET);fputc(K, fp)fclose(fp)// 通过结果可以看出字母K是从第10个位置开始写的liwei:/tmp$ od -tx1 -tc -Ax file.txt0000000 35 36 37 38 0a 00 00 00 00 00 4b5 6 7 8 \n \0 \0 \0 \0 \0 Krewind(fp) 等价于 fseek(fp, 0, SEEK_SET)ftell(fp) 函数比较简单直接返回当前文件指针在文件中的位置// 实现计算文件字节数的功能fseek(fp, 0, SEEK_END);ftell(fp);五、以字符串为单位的IO函数fgets 从指定的文件中读一行字符到调用者提供的缓冲区读入内容不超过 size 。char *fgets(char *s, int size, FILE *stream);char *gets(char *s);首先要说明 gets() 函数强烈不推荐使用类似 strcpy 函数用户不可以指定缓冲区大小很容易造成缓冲区溢出错误。不过 strcpy 程序员还是可以避免而 gets 的输入用户可以提供任意长的字符串唯一避免方法就是不使用 gets而使用 fgets(buf, size, stdin)fgets 函数从 stream 所指文件读取以 \n 结尾的一行包括 \n 在内存到缓冲区中并在该行结尾添加一个 \0 组成完整的字符串。如果文件一行太长fgets 从文件中读了 size-1 个字符还没有读到 \n就把已经读到的 size-1 个字符和一个 \0 字符存入缓冲区文件行剩余的内容可以在下次调用 fgets 时继续读。若一次 fgets 调用在读入若干字符后到达文件末尾则将已读到的字符加上 \0 存入缓冲区并返回如果再次调用则返回 NULL可以据此判断是否读到文件末尾。fputs 向指定文件写入一个字符串缓冲区保存的是以 \0 结尾的字符串与 fgets 不同的是fputs 不关心字符串中的 \n 字符。int fputs(const char *s, FILE *stream);int puts(const char *s);六、以记录为单位的IO函数size_t fread(void *ptr, size_t size, size_t nmemb, FILE *stream);size_t fwrite(const void *ptr, size_t size, size_t nmemb, FILE *stream);fread 和 fwrite 用于读写记录这里的记录是指一串固定长度的字节比如一个 int、一个结构体货或一个定长数组。参数 size 指出一条记录的长度nmemb 指出要读或写多少条记录这些记录在 ptr 所指内存空间连续存放共占 size * nmemb 个字节。fread 和 fwrite 返回的记录数有可能小于 nmemb 指定的记录数。例如当读写位置距文件末尾只有一条记录长度调用 fread 指定 nmemb 为 2则返回值为 1。如果写文件时出错则 fwrite 的返回值小于 nmemb 指定的值。struct t{int a;short b;};struct t val {1, 2};FILE *fp fopen(file.txt, w);fwrite(val, sizeof(val), 1, fp);fclose(fp);liwei:/tmp$ od -tx1 -tc -Ax file.txt0000000 01 00 00 00 02 00 00 00001 \0 \0 \0 002 \0 \0 \0从结果可以看出写入的是 8 个字节有兴趣的同学可以就此分析下系统的「大小端」和结构体的「对齐补齐」问题。七、格式化IO函数(1). printf / scanfint printf(const char *format, ...);int scanf(const char *format, ...);这两个函数是我们学习 C 语言最早接触可能也是接触比较多的了没什么特别要说的。printf 就是格式化打印到标准输出。下面总结下 printf 常用的方式。printf(%d\n, 5); // 打印整数 5printf(-%10s-\n, hello) // 设置显示宽度并左对齐- hello-printf(-%-10s-\n, hello) // 设置显示宽度并右对齐- hello-printf(%#x\n, 0xff); // 0xff 不加#则显示ffprintf(%p\n, main); // 打印 main 函数首地址printf(%%\n); // 打印一个 %scanf 就是从标准输入中读取格式化数据简单举个例子int year, month, day;scanf(%d/%d/%d, year, month, day);printf(year %d, month %d, day %d\n, year, month, day);(2). sprintf / sscanf / snprintfsprintf 并不打印到文件而是打印到用户提供的缓冲区中并在末尾加 \0由于格式化后的字符串长度很难预计所以很可能造成缓冲区溢出强烈推荐 snprintf 更好一些参数 size 指定了缓冲区长度如果格式化后的字符串超过缓冲区长度snprintf 就把字符串截断到 size - 1 字节再加上一个 \0保证字符串以 \0 结尾。如果发生截断返回值是截断之前的长度通过对比返回值与缓冲区实际长度对比就知道是否发生截断。int sscanf(const char *str, const char *format, ...);int sprintf(char *str, const char *format, ...);int snprintf(char *str, size_t size, const char *format, ...);sscanf 是从输入字符串中按照指定的格式去读取相应的数据函数功能非常的强大支持类似正则表达式匹配的功能。具体的使用格式请自行查询官方手册这里总结出最常用、最重要的几种使用场景和方式。最基本的用法char buf[1024] 0;sscanf(123456, %s, buf);printf(%s\n, buf);// 结果为123456取指定长度的字符串sscanf(123456, %4s, buf);printf(%s\n, buf);// 结果为1234取第1个字符串sscanf(hello world, %s, buf);printf(%s\n, buf);// 结果为hello 因为默认是以空格来分割字符串的%s读取第一个字符串hello读取到指定字符为止的字符串sscanf(123456#abcdef, %[^#], buf);// 结果为123456// %[^#]表示读取到#符号停止不包括#读取仅包含指定字符集的字符串sscanf(123456abcdefBCDEF, %[1-9a-z], buf);// 结果为123456abcdef// 表达式是要匹配数字和小写字母匹配到大写字母就停止匹配了。读取指定字符集为止的字符串sscanf(123456abcdefBCDEF, %[^A-Z], buf);// 结果为123456abcdef读取两个符号之间的内容(和.之间的内容)sscanf(liwei0526viplinuxblogs.cn, %*[^]%[^.], buf);// 结果为linuxblogs// 先读取符号前边内容并丢弃然后读接着读取.符号之前的内容linuxblogs不包含字符.给一个字符串sscanf(hello, world, %*s%s, buf);// 结果为world// 先忽略一个字符串hello,遇到空格直接跳过匹配%s保存 world 到 buf// %*s 表示第 1 个匹配到的被过滤掉即跳过hello,如果没有空格则结果为 NULL稍微复杂点的sscanf(ABCabcAB, %*[A-Z]%*[a-z]%[^a-z], buf);// 结果为AB 自己尝试分析哈包含特殊字符处理sscanf(201*1b_-cdZA, %[0-9|_|--|a-z|A-Z||*], buf);// 结果为201*1b_-cdZA如果能将上述几个例子搞明白相信基本上已经掌握了 sscanf 的用法实践才是检验真理的唯一标准只有多使用多思考才能真正理解它的用法。(3). fprintf / fscanffprintf 打印到指定的文件 stream 中fscanf 从文件中格式化读取数据类似 scanf 函数。相关函数的声明如下int fprintf(FILE *stream, const char *format, ...);int fscanf(FILE *stream, const char *format, ...);还是通过简单实例来说明基本用法。FILE *fp fopen(file.txt, w);fprintf(fp, %d-%s-%f\n, 32, hello, 0.12);fclose(fp);liwei:/tmp$ cat file.txt32-hello-0.120000而 fscanf 函数的使用基本上与 sscanf 函数使用方式相同。八、IO缓冲区还有个关于 IO 非常重要的概念就是 IO 缓冲区。C 标准库为每个打开的文件分配一个 I/O 缓冲区用户调用读写函数大多数都在 I/O 缓冲区中读写只有少数请求传递给内核。以 fgetcfputc 为例当第一次调用 fgetc 读一个字节时fgetc 函数可能通过系统调用进入内核读 1k 字节到缓冲区然后返回缓冲区中第一个字节给用户以后用户再调用 fgetc就直接从缓冲区读取。另一方面fputc 通常只是写到缓冲区中如果缓冲区满了fputc 就通过系统调用把缓冲区数据传递给内核内核将数据写回磁盘。如果希望把缓冲区数据立即写入磁盘可以调用 fflush 函数。C 标准库 IO 缓冲区有三种类型全缓冲、行缓冲和无缓冲区不同类型的缓冲区具有不同的特性。全缓冲如果缓冲区写满了就写回内核。常规文件通常是全缓冲的。行缓冲如果程序写的数据中有换行符就把这一行写回内核或者缓冲区满就写回内核。标准输入和标准输出对应终端设备时通常是行缓冲的。无缓冲用户程序每次调用库函数做写操作都要通过系统调用写回内核。标准错误输出通常是无缓冲的用户程序的错误信息可以尽快输出到设备。printf(hello world);while(1);// 运行程序会发现屏幕并没有打印hello world// 因为缓冲区没满且没有\n符号除了写满缓冲区、写入换行符之外行缓冲还有一种情况会自动做 flush 操作如果用户程序调用库函数从无缓冲的文件中读取或从行缓冲的文件中读取且这次读操作会引发系统调用从内核读取数据那么会读之前自动 flush 所有行缓冲程序退出时通常也会自动 flush 缓冲区如果不想完全依赖自动的 flush 操作可以调用 fflush 函数手动操作。若调用 fflush(NULL) 可以对所有打开文件的 IO 缓冲区做 flush 操作。缓冲区大小也可以自定义设置一般情况无需设置默认即可。