从入门到精通:基础IO

打印 上一主题 下一主题

主题 1075|帖子 1075|积分 3229

马上注册,结交更多好友,享用更多功能,让你轻松玩转社区。

您需要 登录 才可以下载或查看,没有账号?立即注册

x
弁言

在编程的天下里,文件输入输出(IO)是与操作系统交互的重要方式。无论你是开辟应用程序、处理数据,还是管理系统资源,掌握文件IO操作都是必不可少的。本篇博客将带你深入了解C语言中的基础IO操作,从入门到精通,全面覆盖文件操作的方方面面。本文不仅介绍基础的文件读写操作,还会扩展到系统调用接口、文件形貌符、重定向、软硬链接、动态库和静态库等内容。
1. 复习C文件IO相关操作

1.1 认识文件相关系统调用接口

在C语言中,文件操作是最根本的功能之一。常用的文件操作接口包括fopen、fclose、fread和fwrite等。你大概会想:“这不就是打开、关闭、读和写文件吗?有什么难的?”但是,深入了解这些函数如何工作、它们的参数和返回值以及如那里理错误,会让你成为一个更高效、更可靠的程序员。
fopen函数用于打开文件,它接受两个参数:文件名和模式。模式可以是“r”表示只读,“w”表示写入(如果文件不存在会创建它,如果文件存在会截断它),“a”表示追加写入等等。以下是一个简朴的示例代码,展示如何使用这些接口进行文件读写操作。
  1. #include <stdio.h>
  2. #include <string.h>
  3. int main() {
  4.     FILE *fp = fopen("myfile", "w");
  5.     if (!fp) {
  6.         printf("fopen error!\n");
  7.     }
  8.     const char *msg = "hello world!\n";
  9.     int count = 5;
  10.     while (count--) {
  11.         fwrite(msg, strlen(msg), 1, fp);
  12.     }
  13.     fclose(fp);
  14.     return 0;
  15. }
复制代码
在这个示例中,fopen函数打开一个文件,fwrite函数将数据写入文件,最后用fclose函数关闭文件。简朴,对吧?但这只是冰山一角。
接下来,我们看看如何读取文件。下面的代码展示了如何使用fread函数从文件中读取数据:
  1. #include <stdio.h>
  2. #include <string.h>
  3. int main() {
  4.     FILE *fp = fopen("myfile", "r");
  5.     if (!fp) {
  6.         printf("fopen error!\n");
  7.     }
  8.     char buf[1024];
  9.     const char *msg = "hello world!\n";
  10.     while (1) {
  11.         ssize_t s = fread(buf, 1, strlen(msg), fp);
  12.         if (s > 0) {
  13.             buf[s] = 0;
  14.             printf("%s", buf);
  15.         }
  16.         if (feof(fp)) {
  17.             break;
  18.         }
  19.     }
  20.     fclose(fp);
  21.     return 0;
  22. }
复制代码
在这个示例中,我们打开一个文件进行读取,使用fread函数读取数据,并使用feof函数检查是否到达文件末端。如果你曾经读过一本好书,你就会明确到达文件末端的感觉:既满意又有点空虚。
fopen函数的错误处理非常重要。比如,如果你试图打开一个不存在的文件,你须要确保你的程序可以或许优雅地处理这种情况,而不是直接瓦解。在上面的示例中,我们检查fopen的返回值,如果它返回NULL,我们就打印一条错误信息并退出程序。
掌握这些基础的文件操作函数是迈向高级编程的第一步。接下来,我们将探究文件形貌符和重定向,了解如何更深入地控制文件I/O。
1.2 文件形貌符与重定向

文件形貌符是一个神奇的小整数,用于标识进程打开的文件。系统调用如open、read、write和close等使用文件形貌符来操作文件。想象一下,每个文件形貌符就像是你桌上的一个文件夹标签,标志了你打开的每个文件。
以下代码展示了如何使用系统调用进行文件操作。
  1. #include <stdio.h>
  2. #include <sys/types.h>
  3. #include <sys/stat.h>
  4. #include <fcntl.h>
  5. #include <unistd.h>
  6. #include <string.h>
  7. int main() {
  8.     umask(0);
  9.     int fd = open("myfile", O_WRONLY | O_CREAT, 0644);
  10.     if (fd < 0) {
  11.         perror("open");
  12.         return 1;
  13.     }
  14.     int count = 5;
  15.     const char *msg = "hello world!\n";
  16.     int len = strlen(msg);
  17.     while (count--) {
  18.         write(fd, msg, len);
  19.     }
  20.     close(fd);
  21.     return 0;
  22. }
复制代码
在这个示例中,我们使用open函数打开一个文件,它返回一个文件形貌符(一个小整数),我们可以使用这个文件形貌符来读写文件。这里,我们使用write函数将数据写入文件,最后用close函数关闭文件。
你大概会问:“为什么不直接使用fopen和fwrite?”答案是系统调用提供了更底层的控制,答应你进行更细粒度的操作。例如,当你须要高性能或精细控制文件I/O时,使用系统调用是更好的选择。
文件形貌符不仅仅用于文件。标准输入(stdin)、标准输出(stdout)和标准错误(stderr)也使用文件形貌符,分别是0、1和2。你可以重定向这些文件形貌符,将输出重定向到文件或将输入从文件读取。例如:
  1. #include <stdio.h>
  2. #include <sys/types.h>
  3. #include <sys/stat.h>
  4. #include <fcntl.h>
  5. #include <unistd.h>
  6. #include <string.h>
  7. int main() {
  8.     char buf[1024];
  9.     ssize_t s = read(0, buf, sizeof(buf));
  10.     if (s > 0) {
  11.         buf[s] = 0;
  12.         write(1, buf, strlen(buf));
  13.         write(2, buf, strlen(buf));
  14.     }
  15.     return 0;
  16. }
复制代码
在这个示例中,我们从标准输入读取数据,并将其写入标准输出和标准错误。文件形貌符的使用非常机动,答应你在程序中轻松地进行输入输出重定向。
重定向是一种改变文件形貌符指向的方法,可以将标准输入输出重定向到文件或其他设备。例如:
  1. #include <stdio.h>
  2. #include <sys/types.h>
  3. #include <sys/stat.h>
  4. #include <fcntl.h>
  5. #include <unistd.h>
  6. #include <stdlib.h>
  7. int main() {
  8.     close(1);
  9.     int fd = open("myfile", O_WRONLY | O_CREAT, 00644);
  10.     if (fd < 0) {
  11.         perror("open");
  12.         return 1;
  13.     }
  14.     printf("fd: %d\n", fd);
  15.     fflush(stdout);
  16.     close(fd);
  17.     exit(0);
  18. }
复制代码
在这个示例中,我们关闭标准输出,然后打开一个文件,并将标准输出重定向到该文件。如许,所有写入标准输出的数据都会被写入文件中。这种本领在日记记录、调试和许多其他应用中非常有用。
通过了解和使用文件形貌符和重定向,你可以更机动地控制文件I/O操作,进步程序的可维护性和性能。接下来,我们将深入探究文件系统中的inode概念。
1.3 文件形貌符的分配规则

每个进程都有三个默认的文件形貌符:标准输入(0),标准输出(1),和标准错误(2)。当进程打开新的文件时,系统会分配一个未被使用的最小整数作为文件形貌符。这种机制确保了每个文件形貌符都是唯一的,并且容易管理。
文件形貌符的分配规则简朴而有效。操作系统维护一个文件形貌符表,每个表项对应一个打开的文件。当你打开一个文件时,操作系统会在表中查找第一个未使用的表项,并将其分配给新打开的文件。例如,如果你的程序已经打开了三个文件,那么下一个文件形貌符大概是3。
这种分配方式使得文件形貌符管理变得非常简朴。你可以轻松地打开和关闭文件,而不必担心文件形貌符冲突。例如,以下代码展示了如何使用文件形貌符进行文件操作:
  1. #include <stdio.h>
  2. #include <sys/types.h>
  3. #include <sys/stat.h>
  4. #include <fcntl.h>
  5. #include <unistd.h>
  6. #include <string.h>
  7. int main() {
  8.     char buf[1024];
  9.     ssize_t s = read(0, buf, sizeof(buf));
  10.     if (s > 0) {
  11.         buf[s] = 0;
  12.         write(1, buf, strlen(buf));
  13.         write(2, buf, strlen(buf));
  14.     }
  15.     return 0;
  16. }
复制代码
在这个示例中,我们从标准输入读取数据,并将其写入标准输出和标准错误。这种操作非常简朴,但却展示了文件形貌符的强大之处。
重定向是文件形貌符的一个重要应用。通过重定向,你可以改变文件形貌符的指向,从而将输入输出重定向到不同的文件或设备。例如,以下代码展示了如何将
标准输出重定向到一个文件:
  1. #include <stdio.h>
  2. #include <sys/types.h>
  3. #include <sys/stat.h>
  4. #include <fcntl.h>
  5. #include <unistd.h>
  6. #include <stdlib.h>
  7. int main() {
  8.     close(1);
  9.     int fd = open("myfile", O_WRONLY | O_CREAT, 00644);
  10.     if (fd < 0) {
  11.         perror("open");
  12.         return 1;
  13.     }
  14.     printf("fd: %d\n", fd);
  15.     fflush(stdout);
  16.     close(fd);
  17.     exit(0);
  18. }
复制代码
在这个示例中,我们起首关闭标准输出,然后打开一个文件,并将文件形貌符1(标准输出)重定向到该文件。如许,所有写入标准输出的数据都会被写入文件中。这种本领在许多应用中非常有用,例如日记记录和调试。
文件形貌符的管理和重定向是C语言文件I/O操作的重要构成部门。通过理解这些概念,你可以编写出更机动和高效的代码。接下来,我们将探究重定向的本质以及更多的高级本领。
1.4 重定向

重定向是一种强大的技术,可以改变文件形貌符的指向。它答应你将标准输入、标准输出和标准错误重定向到文件、设备或其他进程。重定向的应用范围非常广泛,从简朴的日记记录到复杂的进程间通讯,都是重定向的典范应用。
重定向的本质是改变文件形貌符的指向。文件形貌符是操作系统用来跟踪打开文件的小整数。当你打开一个文件时,操作系统会返回一个文件形貌符,表示该文件在系统中的唯一标识。通过重定向,你可以改变文件形貌符的指向,使其指向不同的文件或设备。
以下代码展示了如何使用重定向将标准输出重定向到一个文件:
  1. #include <stdio.h>
  2. #include <sys/types.h>
  3. #include <sys/stat.h>
  4. #include <fcntl.h>
  5. #include <unistd.h>
  6. #include <stdlib.h>
  7. int main() {
  8.     close(1);
  9.     int fd = open("myfile", O_WRONLY | O_CREAT, 00644);
  10.     if (fd < 0) {
  11.         perror("open");
  12.         return 1;
  13.     }
  14.     printf("fd: %d\n", fd);
  15.     fflush(stdout);
  16.     close(fd);
  17.     exit(0);
  18. }
复制代码
在这个示例中,我们起首关闭标准输出(文件形貌符1),然后打开一个文件,并将文件形貌符1重定向到该文件。如许,所有写入标准输出的数据都会被写入文件中。这种本领在日记记录、调试和进程间通讯中非常有用。
重定向不仅可以用于标准输出,还可以用于标准输入和标准错误。例如,以下代码展示了如何将标准输入重定向到一个文件:
  1. #include <stdio.h>
  2. #include <sys/types.h>
  3. #include <sys/stat.h>
  4. #include <fcntl.h>
  5. #include <unistd.h>
  6. #include <stdlib.h>
  7. int main() {
  8.     close(0);
  9.     int fd = open("myfile", O_RDONLY);
  10.     if (fd < 0) {
  11.         perror("open");
  12.         return 1;
  13.     }
  14.     char buf[1024];
  15.     ssize_t s = read(0, buf, sizeof(buf));
  16.     if (s > 0) {
  17.         buf[s] = 0;
  18.         printf("%s", buf);
  19.     }
  20.     close(fd);
  21.     exit(0);
  22. }
复制代码
在这个示例中,我们起首关闭标准输入(文件形貌符0),然后打开一个文件,并将文件形貌符0重定向到该文件。如许,所有从标准输入读取的数据都会来自文件。这种本领在数据处理和批处理脚本中非常有用。
除了简朴的重定向,C语言还提供了一些高级技术,如dup和dup2系统调用。dup系统调用用于复制文件形貌符,而dup2系统调用用于将一个文件形貌符复制到另一个文件形貌符。例如:
  1. #include <stdio.h>
  2. #include <sys/types.h>
  3. #include <sys/stat.h>
  4. #include <fcntl.h>
  5. #include <unistd.h>
  6. #include <stdlib.h>
  7. int main() {
  8.     int fd = open("myfile", O_WRONLY | O_CREAT, 00644);
  9.     if (fd < 0) {
  10.         perror("open");
  11.         return 1;
  12.     }
  13.     dup2(fd, 1);
  14.     printf("This will be written to the file\n");
  15.     close(fd);
  16.     exit(0);
  17. }
复制代码
在这个示例中,我们使用dup2系统调用将文件形貌符fd复制到文件形貌符1(标准输出)。如许,所有写入标准输出的数据都会被写入文件中。
通过了解和使用重定向技术,你可以更机动地控制文件I/O操作,进步程序的可维护性和性能。接下来,我们将深入探究文件系统中的inode概念。
2. 理解文件系统中inode的概念

在文件系统中,inode(索引节点)是存储文件元数据的结构。每个文件都有一个唯一的inode,包含文件的所有信息,如文件大小、所有者、权限和时间戳等。inode不存储文件名,而是通过目录结构将文件名映射到inode。
理解inode的概念有助于我们更深入地理解文件系统的工作原理。每个文件都有一个唯一的inode,通过这个inode可以快速访问文件的元数据。以下是一个示例,展示如何使用stat命令检察文件的inode信息:
  1. [root@localhost linux]# stat test.c
  2.   File: "test.c"
  3.   Size: 654         Blocks: 8          IO Block: 4096   普通文件
  4. Device: 802h/2050d  Inode: 263715      Links: 1
  5. Access: (0644/-rw-r--r--)  Uid: (    0/    root)   Gid: (    0/    root)
  6. Access: 2017-09-13 14:56:57.059012947 +0800
  7. Modify: 2017-09-13 14:56:40.067012944 +0800
  8. Change: 2017-09-13 14:56:40.069012948 +0800
复制代码
在这个示例中,我们使用stat命令检察文件test.c的inode信息。输出表现了文件的大小、块数、inode号码、链接数、权限、所有者、组和时间戳等信息。通过这些信息,我们可以了解文件的具体属性。
inode不仅存储文件的元数据,还包含指向文件数据块的指针。文件的数据存储在磁盘上的数据块中,而inode包含指向这些数据块的指针。当你访问文件时,文件系统通过inode找到文件的数据块,从而读取或写入数据。
inode在文件系统中的位置和作用类似于书的目录。目录记录了每个章节的页码,而inode记录了文件的数据块位置。通过inode,文件系统可以快速找到文件的数据,进步文件访问的效率。
inode还包含文件的权限信息。每个文件都有一个权限位字段,定义了文件所有者、组和其他用户的访问权限。这些权限通过三个八进制数字表示,每个数字表示读、写和执行权限。例如,权限0644表示文件所有者有读写权限,组和其他用户只有读取权限。
理解inode的概念有助于我们更好地管理和优化文件系统。例如,通过调整inode的数量和大小,可以进步文件系统的性能和效率。在实际应用中,了解inode的工作原理可以帮助我们办理文件系统的性能问题,进步系统的可靠性。
接下来,我们将探究软硬链接的概念,了解如何使用链接来管理文件。
3. 认识软硬链接

在文件系统中,链接是指多个文件名指向同一个文件数据的方式。链接分为硬链接和软链接(符号链接)。理解链接的概念有助于我们更机动地管理文件,进步文件系统的效率和可靠性。
3.1 硬链接

硬链接是指不同的文件名指向同一个inode。硬链接的关键特点是它们共享相同的文件数据和元数据。当你创建一个硬链接时,实际上是创建了一个新的文件名,但指向的是同一个inode。因此,硬链接具有以下特点:

  • 多个硬链接共享相同的文件数据。
  • 删除一个硬链接不会影响其他硬链接。
  • 只有当所有硬链接都被删除时,文件的数据才会被删除。
以下示例展示了如何创建硬链接:
  1. [root@localhost linux]# ln abc def
  2. [root@localhost linux]# ls -i abc def
  3. 263466 abc
  4. 263466 def
复制代码
在这个示例中,我们使用ln命令创建了一个名为def的硬链接,指向文件abc。通过ls -i命令可以看到,abc和def共享相同的inode号码(263466),表明它们指向同一个文件数据。
硬链接在文件系统管理中非常有用。例如,你可以使用硬链接来创建文件的备份,而不须要额外的
存储空间。只需创建一个硬链接,就可以在不同位置访问相同的数据。
3.2 软链接

软链接(符号链接)是另一种链接方式,它与硬链接不同之处在于软链接是一个特殊的文件,包含另一个文件的路径名。软链接具有以下特点:

  • 软链接是一个独立的文件,包含指向目的文件的路径。
  • 软链接可以跨文件系统创建。
  • 删除软链接不会影响目的文件,但删除目的文件会使软链接无效。
以下示例展示了如何创建软链接:
  1. [root@localhost linux]# ln -s abc def
  2. [root@localhost linux]# ls -i abc def
  3. 263466 abc
  4. 261678 def -> abc
复制代码
在这个示例中,我们使用ln -s命令创建了一个名为def的软链接,指向文件abc。通过ls -i命令可以看到,def是一个软链接,指向abc。
软链接在许多应用场景中非常有用。例如,你可以使用软链接来创建文件的快捷方式,简化文件的访问路径。软链接还可以用于配置文件的管理,将多个配置文件指向同一个目的文件,方便同一管理和更新。
链接的使用不仅进步了文件系统的机动性,还提供了一种高效的文件管理方式。通过理解和使用硬链接和软链接,你可以更机动地管理文件,进步系统的效率和可靠性。
接下来,我们将探究动态库和静态库的概念,了解如何使用库来进步程序的可重用性和效率。
4. 动态库和静态库

在软件开辟中,库(Library)是指一组预编译的函数和代码,用于执行特定的使命。库的使用可以大大进步程序的可重用性和开辟效率。根据链接方式的不同,库可以分为静态库和动态库。理解动态库和静态库的概念,有助于我们更好地管理和优化程序。
4.1 静态库

静态库是在编译时将库的代码链接到可执行文件中,程序运行时不再须要静态库。静态库的优点是链接后的可执行文件独立性强,不依赖外部库,运行时效率高。缺点是天生的可执行文件较大,且更新库时须要重新编译程序。
以下示例展示了如何创建和使用静态库:
  1. # 创建静态库
  2. [root@localhost linux]# gcc -c add.c -o add.o
  3. [root@localhost linux]# gcc -c sub.c -o sub.o
  4. [root@localhost linux]# ar -rc libmymath.a add.o sub.o
  5. # 使用静态库
  6. [root@localhost linux]# gcc main.c -L. -lmymath
  7. [root@localhost linux]# ./a.out
复制代码
在这个示例中,我们起首编译了add.c和sub.c源文件,天生目的文件add.o和sub.o。然后使用ar命令将目的文件打包成静态库libmymath.a。最后,在编译main.c时,使用-L选项指定库路径,使用-l选项链接静态库libmymath.a。
静态库的使用非常简朴,只需在编译时链接库文件即可。静态库实用于不频繁更新且对运行时性能要求较高的应用场景。
4.2 动态库

动态库是在程序运行时加载,多个程序可以共享同一个动态库,从而节流内存和磁盘空间。动态库的优点是更新方便,只需更换动态库文件即可,不须要重新编译程序。缺点是运行时须要加载库,大概会影响启动速率。
以下示例展示了如何创建和使用动态库:
  1. # 创建动态库
  2. [root@localhost linux]# gcc -fPIC -c sub.c add.c
  3. [root@localhost linux]# gcc -shared -o libmymath.so *.o
  4. # 使用动态库
  5. [root@localhost linux]# export LD_LIBRARY_PATH=.
  6. [root@localhost linux]# gcc main.c -L. -lmymath
  7. [root@localhost linux]# ./a.out
复制代码
在这个示例中,我们起首编译sub.c和add.c源文件,天生位置无关代码(PIC)的目的文件。然后使用-shared选项将目的文件打包成动态库libmymath.so。在编译main.c时,使用-L选项指定库路径,使用-l选项链接动态库libmymath.so。最后,通过设置LD_LIBRARY_PATH环境变量,指定动态库的搜索路径,并运行程序。
动态库的使用非常机动,可以在运行时加载和更新库文件。动态库实用于须要频繁更新且资源共享的应用场景。
4.3 动态库的使用

在使用动态库时,须要指定动态库的路径和名称。以下是一些常见的动态库管理本领:

  • 环境变量:使用LD_LIBRARY_PATH环境变量指定动态库的搜索路径。例如:
    1. export LD_LIBRARY_PATH=/path/to/library
    复制代码
  • 配置文件:在/etc/ld.so.conf.d/目录下创建配置文件,添加动态库路径。例如:
    1. echo "/path/to/library" > /etc/ld.so.conf.d/mylib.conf
    2. ldconfig
    复制代码
  • 编译选项:在编译时使用-L选项指定库路径,使用-l选项链接动态库。例如:
    1. gcc main.c -L/path/to/library -lmylib
    复制代码
通过这些本领,可以方便地管理和使用动态库,进步程序的可维护性和可扩展性。
动态库和静态库是软件开辟中常用的工具,通过合理使用库,可以进步程序的可重用性、效率和机动性。接下来,我们将探究系统文件I/O操作,了解如何使用系统调用进行文件操作。
5. 系统文件I/O操作

除了标准库函数,C语言还提供了系统调用接口来进行文件I/O操作。系统调用接口包括open、close、read、write、lseek等函数。系统调用提供了更底层的控制,答应我们进行更细粒度的文件操作。
5.1 open函数

open函数用于打开文件,返回文件形貌符。文件形貌符是一个小整数,用于标识进程打开的文件。以下是open函数的原型:
  1. #include <sys/types.h>
  2. #include <sys/stat.h>
  3. #include <fcntl.h>
  4. int open(const char *pathname, int flags);
  5. int open(const char *pathname, int flags, mode_t mode);
复制代码
pathname参数指定要打开的文件路径,flags参数指定打开文件的模式,如只读、只写或读写,mode参数指定文件的权限(当创建文件时)。以下示例展示了如何使用open函数打开一个文件:
  1. #include <sys/types.h>
  2. #include <sys/stat.h>
  3. #include <fcntl.h>
  4. #include <unistd.h>
  5. #include <stdio.h>
  6. int main() {
  7.     int fd = open("myfile", O_RDONLY);
  8.     if (fd < 0) {
  9.         perror("open");
  10.         return 1;
  11.     }
  12.     printf("File opened successfully with file descriptor %d\n", fd);
  13.     close(fd);
  14.     return 0;
  15. }
复制代码
在这个示例中,我们使用open函数以只读模式打开文件myfile,并检查打开是否成功。如果成功,open函数返回文件形貌符,否则返回-1并设置errno以指示错误。我们使用perror函数打印错误信息,并在成功打开文件后使用close函数关闭文件。
5.2 read和write函数

read函数用于从文件中读取数据,write函数用于向文件中写入数据。以下是read和write函数的原型:
  1. ssize_t read(int fd, void *buf, size_t count);
  2. ssize_t write(int fd, const void *buf, size_t count);
复制代码
fd参数是文件形貌符,buf参数是数据缓冲区,count参数是要读取或写入的字节数。以下示例展示了如何使用read和write函数进行文件读写操作:
  1. #include <sys/types.h>
  2. #include <sys/stat.h>
  3. #include <fcntl.h>
  4. #include <unistd.h>
  5. #include <stdio.h>
  6. #include <string.h>
  7. int main() {
  8.     int fd = open("myfile", O_WRONLY | O_CREAT, 0644);
  9.     if (fd < 0) {
  10.         perror("open");
  11.         return 1;
  12.     }
  13.     const char *msg = "Hello, world!\n";
  14.     ssize_t bytes_written = write(fd, msg, strlen(msg));
  15.     if (bytes_written < 0)
  16. {
  17.         perror("write");
  18.         close(fd);
  19.         return 1;
  20.     }
  21.     close(fd);
  22.     fd = open("myfile", O_RDONLY);
  23.     if (fd < 0) {
  24.         perror("open");
  25.         return 1;
  26.     }
  27.     char buf[1024];
  28.     ssize_t bytes_read = read(fd, buf, sizeof(buf) - 1);
  29.     if (bytes_read < 0) {
  30.         perror("read");
  31.         close(fd);
  32.         return 1;
  33.     }
  34.     buf[bytes_read] = '\0';
  35.     printf("Read from file: %s", buf);
  36.     close(fd);
  37.     return 0;
  38. }
复制代码
在这个示例中,我们起首使用open函数以只写模式打开文件myfile,并使用write函数将字符串写入文件。然后,我们关闭文件并再次打开它,以只读模式使用read函数读取数据,并将读取的数据打印到标准输出。
5.3 lseek函数

lseek函数用于移动文件指针。文件指针是文件中当前读写位置的标志。以下是lseek函数的原型:
  1. #include <unistd.h>
  2. off_t lseek(int fd, off_t offset, int whence);
复制代码
fd参数是文件形貌符,offset参数是相对于whence的偏移量,whence参数指定偏移的基准点,可以是SEEK_SET(文件开始处)、SEEK_CUR(当前位置)或SEEK_END(文件末端)。
以下示例展示了如何使用lseek函数移动文件指针:
  1. #include <sys/types.h>
  2. #include <sys/stat.h>
  3. #include <fcntl.h>
  4. #include <unistd.h>
  5. #include <stdio.h>
  6. int main() {
  7.     int fd = open("myfile", O_RDONLY);
  8.     if (fd < 0) {
  9.         perror("open");
  10.         return 1;
  11.     }
  12.     off_t offset = lseek(fd, 5, SEEK_SET);
  13.     if (offset == (off_t)-1) {
  14.         perror("lseek");
  15.         close(fd);
  16.         return 1;
  17.     }
  18.     char buf[1024];
  19.     ssize_t bytes_read = read(fd, buf, sizeof(buf) - 1);
  20.     if (bytes_read < 0) {
  21.         perror("read");
  22.         close(fd);
  23.         return 1;
  24.     }
  25.     buf[bytes_read] = '\0';
  26.     printf("Read from file: %s", buf);
  27.     close(fd);
  28.     return 0;
  29. }
复制代码
在这个示例中,我们使用lseek函数将文件指针移动到文件的第6个字节(偏移量5),然后使用read函数从该位置读取数据,并将读取的数据打印到标准输出。
通过理解和使用系统文件I/O操作,你可以更机动地控制文件操作,进步程序的性能和可靠性。接下来,我们将探究库函数与系统调用的关系,了解它们的区别和联系。
6. 库函数与系统调用的关系

在C语言中,库函数和系统调用是文件I/O操作的两种重要方式。库函数是对系统调用的封装,提供了更高级别的接口,便于开辟人员使用。例如,fopen、fclose、fread、fwrite等库函数都是对open、close、read、write等系统调用的封装。
6.1 FILE结构体

在C语言中,FILE结构体封装了文件形貌符,提供了更高级别的文件操作接口。FILE结构体包含文件形貌符、缓冲区和其他文件信息。以下是一个简朴的示例,展示如何使用FILE结构体进行文件操作:
  1. #include <stdio.h>
  2. #include <string.h>
  3. int main() {
  4.     const char *msg0 = "hello printf\n";
  5.     const char *msg1 = "hello fwrite\n";
  6.     const char *msg2 = "hello write\n";
  7.     printf("%s", msg0);
  8.     fwrite(msg1, strlen(msg0), 1, stdout);
  9.     write(1, msg2, strlen(msg2));
  10.     return 0;
  11. }
复制代码
在这个示例中,我们使用printf和fwrite库函数将数据写入标准输出。printf和fwrite库函数内部使用FILE结构体进行文件操作,封装了底层的系统调用。write系统调用直接使用文件形貌符进行文件操作,不经过FILE结构体。
库函数提供了更高级别的接口,简化了文件操作。例如,fopen函数打开文件并返回一个FILE指针,而open系统调用返回一个文件形貌符。fopen函数内部调用open系统调用,并初始化FILE结构体。以下是fopen函数的实现示例:
  1. #include <stdio.h>
  2. #include <fcntl.h>
  3. #include <unistd.h>
  4. FILE *fopen(const char *pathname, const char *mode) {
  5.     int flags;
  6.     switch (mode[0]) {
  7.         case 'r':
  8.             flags = O_RDONLY;
  9.             break;
  10.         case 'w':
  11.             flags = O_WRONLY | O_CREAT | O_TRUNC;
  12.             break;
  13.         case 'a':
  14.             flags = O_WRONLY | O_CREAT | O_APPEND;
  15.             break;
  16.         default:
  17.             return NULL;
  18.     }
  19.     int fd = open(pathname, flags, 0644);
  20.     if (fd < 0) {
  21.         return NULL;
  22.     }
  23.     FILE *fp = fdopen(fd, mode);
  24.     return fp;
  25. }
复制代码
在这个示例中,我们定义了一个fopen函数,该函数根据传入的模式设置open系统调用的标志,并使用open系统调用打开文件。然后,我们使用fdopen函数将文件形貌符转换为FILE指针。
6.2 缓冲区的管理

库函数提供了缓冲区管理,进步了文件操作的性能。缓冲区用于暂时存储数据,淘汰系统调用的次数。根据缓冲区的革新时机,缓冲区分为全缓冲、行缓冲和无缓冲。


  • 全缓冲:数据在缓冲区填满后革新。例如,文件操作通常使用全缓冲。
  • 行缓冲:每次输入输出操作后革新。例如,标准输出通常使用行缓冲。
  • 无缓冲:每次输入输出操作立即革新。例如,标准错误通常使用无缓冲。
以下示例展示了如何使用fflush函数手动革新缓冲区:
  1. #include <stdio.h>
  2. #include <string.h>
  3. int main() {
  4.     const char *msg = "hello buffer\n";
  5.     fwrite(msg, strlen(msg), 1, stdout);
  6.     fflush(stdout);
  7.     return 0;
  8. }
复制代码
在这个示例中,我们使用fwrite函数将数据写入标准输出,并使用fflush函数手动革新缓冲区。fflush函数确保缓冲区中的数据立即写入文件或设备,进步了数据的一致性和可靠性。
通过理解库函数与系统调用的关系,你可以更机动地选择文件操作的方式,根据具体需求优化程序的性能和可靠性。接下来,我们将探究文件缓冲区的管理,了解如何进步文件I/O操作的性能。
7. 文件缓冲区

文件缓冲区用于进步文件I/O操作的性能。标准库函数如printf和fwrite使用缓冲区来淘汰系统调用的次数。缓冲区的管理和使用是进步文件操作性能的关键。
7.1 缓冲区的种类

根据缓冲区的革新时机,缓冲区分为全缓冲、行缓冲和无缓冲。


  • 全缓冲:数据在缓冲区填满后革新。例如,文件操作通常使用全缓冲。全缓冲的优点是淘汰系统调用的次数,进步了I/O操作的效率。缺点是如果程序瓦解,缓冲区中的数据大概丢失。
  • 行缓冲:每次输入输出操作后革新。例如,标准输出通常使用行缓冲。行缓冲的优点是确保每行数据立即输出,进步了数据的实时性。缺点是每行数据都进行革新,大概会增长系统调用的次数。
  • 无缓冲:每次输入输出操作立即革新。例如,标准错误通常使用无缓冲。无缓冲的优点是确保数据立即输出,进步了数据的一致性和可靠性。缺点是每次操作都进行革新,大概会低落I/O操作的效率。
以下示例展示了如何设置缓冲区:
  1. #include <stdio.h>
  2. #include <stdlib.h>
  3. int main() {
  4.     FILE *fp = fopen("myfile", "w");
  5.     if (!fp) {
  6.         perror("fopen");
  7.         return 1;
  8.     }
  9.     char buf[1024];
  10.     setvbuf(fp, buf, _IOFBF, sizeof(buf));  // 设置全缓冲
  11.     // setvbuf(fp, buf, _IOLBF, sizeof(buf));  // 设置行缓冲
  12.     // setvbuf(fp, NULL, _IONBF, 0);  // 设置无缓冲
  13.     const char *msg = "hello buffer\n";
  14.     fwrite(msg, strlen(msg), 1, fp);
  15.     fflush(fp);
  16.     fclose(fp);
  17.     return 0;
  18. }
复制代码
在这个示例中,我们使用setvbuf函数设置缓冲区范例。_IOFBF表示全缓冲,_IOLBF表示行缓冲,_IONBF表示无缓冲。通过设置不同的缓冲区范例,我们可以优化文件I/O操作的性能和举动。
7.2 缓冲区的革新

缓冲区的革新可以通过fflush函数手动进行,也可以在缓冲区满、文件关闭或程序退出时主动进行。以下示例展示了如何使用fflush函数手动革新缓冲区:
  1. #include <stdio.h>
  2. #include <string.h>
  3. int main() {
  4.     const char *msg = "hello buffer\n";
  5.     fwrite(msg, strlen(msg), 1, stdout);
  6.     fflush(stdout);
  7.     return 0;
  8. }
复制代码
在这个示例中,我们使用fwrite函数将数据写入标准输出,并使用fflush函数手动革新缓冲区。fflush函数确保缓冲区中的数据立即写入文件或设备,进步了数据的一致性和可靠性。
缓冲区的革新时机非常重要。在程序运行过程中,如果缓冲区未及时革新,缓冲区中的数据大概丢失。通过手动革新缓冲区,我们可以确保数据的及时输出,进步程序的稳定性和可靠性。
缓冲区管理是进步文件I/O操作性能的重要手段。通过合理设置缓冲区范例和革新时机,可以优化文件操作的效率和举动。接下来,我们将探究文件操作的错误处理,了解如那里理文件操作中的常见错误。
8. 文件操作的错误处理

在进行文件操作时,错误处理是一个重要的方面。无论是文件不存在、权限不足还是磁盘空间不足,错误都是不可避免的。为了编写健壮的程序,我们须要处理各种大概的错误情况,并提供得当的错误信息。
8.1 错误处理的根本原则

错误处理的根本原则是:检测错误、陈诉错误和恢复错误。检测错误是指在每次文件操作后检查返回值,确定操作是否成功。陈诉错误是指在检测到错误后,向用户或日记系统提供有意义的错误信息。恢复错误是指采取得当的措施,使程序可以或许继续运行或安全退出。
以下示例展示了如何进行根本的错误处理:
  1. #include <stdio.h>
  2. #include <errno.h>
  3. int main() {
  4.     FILE *fp = fopen("nonexistentfile", "r");
  5.     if (!fp) {
  6.         perror("fopen");
  7.         return 1;
  8.     }
  9.     return 0;
  10. }
复制代码
在这个示例中,我们实验打开一个不存在的文件nonexistentfile。fopen函数返回NULL表示操作失败,我们使用perror函数打印错误信息,并返回非零值以指示错误。
8.2 错误处理的高级本领

在实际应用中,错误处理大概须要更复杂的逻辑和更具体的错误信息。以下是一些常见的高级错误处理本领:

  • 检查返回值:在每次文件操作后检查返回值,确保操作成功。例如:
    1. FILE *fp = fopen("myfile", "r");
    2. if (!fp) {
    3.     perror("fopen");
    4.     return 1;
    5. }
    6. char buf[1024];
    7. if (fread(buf, sizeof(char), sizeof(buf), fp) < sizeof(buf)) {
    8.     if (feof(fp)) {
    9.         printf("End of file reached\n");
    10.     } else if (ferror(fp)) {
    11.         perror("fread");
    12.         fclose(fp);
    13.         return 1;
    14.     }
    15. }
    16. fclose(fp);
    复制代码
  • 使用errno:errno是一个全局变量,存储了最近一次系统调用的错误码。通过检查errno,可以获取具体的错误信息。例如:
    1. int fd = open("myfile", O_RDONLY);
    2. if (fd < 0) {
    3.     printf("Error opening file: %s\n", strerror(errno));
    4.     return 1;
    5. }
    复制代码
  • 定义错误码:在大型程序中,可以定义自己的错误码,以便同一管理和处理错误。例如:
    1. #define ERR_FILE_NOT_FOUND 1
    2. #define ERR_PERMISSION_DENIED 2
    3. int open_file(const char *filename) {
    4.     int fd = open(filename, O_RDONLY);
    5.     if (fd < 0) {
    6.         switch (errno) {
    7.             case ENOENT:
    8.                 return ERR_FILE_NOT_FOUND;
    9.             case EACCES:
    10.                 return ERR_PERMISSION_DENIED;
    11.             default:
    12.                 return -1;
    13.         }
    14.     }
    15.     return fd;
    16. }
    复制代码
  • 日记记录:使用日记系统记录错误信息,以便后续分析和调试。例如:
    1. void log_error(const char *message) {
    2.     FILE *log = fopen("error.log", "a");
    3.     if (log) {
    4.         fprintf(log, "%s\n", message);
    5.         fclose(log);
    6.     }
    7. }
    8. FILE *fp = fopen("myfile", "r");
    9. if (!fp) {
    10.     log_error("Error opening file");
    11.     return 1;
    12. }
    复制代码
通过合理的错误处理,可以进步程序的稳定性和可维护性。错误处理不仅包括检测和陈诉错误,还包括采取得当的措施恢复错误,使程序可以或许继续运行或安全退出。
结论

本文具体介绍了C语言中基础IO操作的各个方面,从文件读写到系统调用,从文件形貌符到重定向,再到文件系统和动态静态库的使用。通过这些内容的学习和实践,你将可以或许更加深入地理解和掌握文件IO操作,为开辟高效、可靠的程序打下坚固的基础。
嗯,就是如许啦,文章到这里就结束啦,至心感谢你花时间来读。
以为有点劳绩的话,不妨给我点个赞吧!
如果发现文章有啥漏洞或错误的地方,欢迎私信我或者在批评里提醒一声~

免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!更多信息从访问主页:qidao123.com:ToB企服之家,中国第一个企服评测及商务社交产业平台。
回复

使用道具 举报

0 个回复

倒序浏览

快速回复

您需要登录后才可以回帖 登录 or 立即注册

本版积分规则

东湖之滨

论坛元老
这个人很懒什么都没写!
快速回复 返回顶部 返回列表