IO使用函数
标准IO | 文件IO(低级IO) | |
---|---|---|
打开 | fopen, freopen, fdopen | open |
关闭 | fclose | close |
读 | getc, fgetc, getchar, fgets, gets, fread printf fprintf | read |
写 | putc, fputc, putchar, fputs, puts, fwrite scanf fscanf | write |
操作文件指针 | fseek | lseek |
其它 | fflush rewind ftell |
文件描述符
当打开文件时,文件相关信息(如创建时间、所属者、操作权限等)会被加载到内存,操作系统用struct file
结构体维护这些信息,多个打开的文件对应的struct file
通过双向链表管理。
每个进程的进程控制块task_struct
中,有一个指针files
指向files_struct
表。该表包含一个指针数组,数组中的每个指针指向内存中的一个struct file
,实现进程与文件的连接。文件描述符是该数组的下标,通过它可定位内存中的文件。
数组前三个下标通常固定对应标准输入、标准输出和标准错误(perror
)。
这三个文件描述符(0号-标准输入,1号-标准输出,2号-标准错误)在UNIX/Linux系统中被预设为进程的输入和输出通道,是为了方便程序与用户或者其他程序进行通信。
-
标准输入(stdin):默认情况下,这是键盘输入的数据。如果程序需要从用户处接收输入,它就会从这个描述符读取数据。
-
标准输出(stdout):默认情况下,这是显示器的输出。如果程序需要向用户显示信息,它就会写入这个描述符。
-
标准错误(stderr):默认情况下,这同样是显示器的输出。不过,它专门用于输出错误信息。这样做的好处是,即使标准输出被重定向到其他位置(如文件),错误信息仍然可以显示给用户看。
这种设计可以大大增加程序的灵活性。例如,你可以通过重定向机制,将一个程序的标准输出作为另一程序的标准输入,这就形成了所谓的"管道"(pipe),能将多个程序连接起来,协同完成一项工作。这也是UNIX/Linux广受欢迎的一个重要原因。
总结:文件修饰符本质是一个数字,用于表示要操作文件在系统里抽象的文件指针数组里的下标,通过下标(文件修饰符)就可以通过函数来间接操作文件。
1:open函数
调用低级io相关函数一般我们会引入以下头文件:
#include <fcntl.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
-
<fcntl.h>
: 定义了open
函数和相关的常量,如O_RDONLY
,O_WRONLY
,O_RDWR
,O_CREAT
,O_EXCL
等。 -
<unistd.h>
: 定义了read
,write
,lseek
, 和close
函数,以及很多UNIX标准的系统调用。 -
<sys/types.h>
: 通常包含了一些用于系统调用的数据类型定义,比如off_t
,size_t
等。 -
<sys/stat.h>
: 如果你在调用open
函数时需要设置文件权限,这个头文件中定义了文件权限相关的宏,比如S_IRUSR
,S_IWUSR
等
调用open()函数可打开或创建一个文件,其函数原型如下:
int open(const char *pathname, int flags)
int open(const char *pathname, int flags, mode_t mode)
-
返回值:返回一个新的文件描述符,指向打开的文件,如果调用失败则返回-1。如果返回-1,那么c程序会同时将错误信息状态码设为文件打开失败的对应信息,可以通过
perror
来显示 -
参数
-
pathname:要打开或创建的文件的路径.相对于当前目录或使用绝对路径
-
flags:指定程序对文件的操作权限和其他的设置
-
O_RDONLY 以只读方式打开文件:
-
O_WRONLY 以只写方式打开文件
-
O_RDWR 以可读写方式打开文件。上述三种旗标是互斥的,也就是不可同时使用,但可与下列的旗标**利用OR(|)**运算符组合。
-
O_CREAT 若欲打开的文件不存在则自动建立该文件。
-
O_EXCL 如果O_CREAT 也被设置,此指令会去检查文件是否存在。文件若不存在则建立该文件,否则将导致打开文件错误。
此外,若O_CREAT与O_EXCL同时设置,并且欲打开的文件为符号连接,则会打开文件失败。
-
O_APPEND 当读写文件时会从文件尾开始移动,也就是所写入的数据会以附加的方式加入到文件后面。追加模式(写入时指针位于文件末尾)。
-
O_SYNC 以同步的方式打开文件。
-
-
-
mode:用于设置当文件创建时的权限,只有使用了O_CREAT,即创建文件才会生效。比如0664指所属用户以及用户组都是读写权限,其他用户则只有读权限(r:4 w:2 X:1)(x执行)
-
第一个6表示文件创建者的权限
-
第二个6表示文件所属组的权限
-
第三个4表示其他用户的权限
-
-
示例代码:
int main()
{
char *filename = "new_file.txt";
int fd = open(filename, O_RDWR | O_CREAT , 0666);
if (fd == -1)
{
perror("无法创建文件");
return 1;
}
return 0;
}
2:write函数
调用函数write()可向已打开的文件中写入数据,其函数模型如下:
#include <unistd.h>
ssize_t write(int fd, const void *buf, size_t count);
参数说明
-
fd(文件描述符)
:这是一个整数,作用是标识要写入数据的目标文件或设备。
-
像
STDOUT_FILENO
(标准输出,一般对应的值是 1),数据会默认输出到控制台。 -
STDERR_FILENO
(标准错误,通常值为 2),用于输出错误信息。 -
还可以是通过
open()
函数打开文件时返回的文件描述符。
-
-
buf:它是一个指向内存缓冲区的指针,里面存放着要写入磁盘的数据,一般是数组的地址。
-
count:表示要写入数据的字节大小,通常就是数组的大小。
返回值情况
-
成功时 :函数会返回实际写入的字节数,这个返回值通常和参数
count
的值相同。 -
失败时 :函数会返回 - 1,并且会设置
errno
变量来指示具体的错误类型。 -
部分写入情况 :要是返回值大于 0 但小于
count
,这表明只写入了部分数据。此时,你需要再次调用write()
函数,从缓冲区的剩余部分继续写入。
示例代码
1. 向标准输出(控制台)写入数据
#include <unistd.h>
int main()
{
char message[] = "Hello, World!\n";
write(STDOUT_FILENO, message, sizeof(message) - 1);
// 也可以直接使用1代替STDOUT_FILENO
return 0;
}
2. 向文件写入数据
int main()
{
char *filename = "new_file.txt";
int fd = open(filename, O_RDWR | O_CREAT , 0666);
if (fd == -1)
{
perror("无法创建文件");
return 1;
}
char buffer[] = "这是一个测试文件\n";
ssize_t bytes_written = write(fd, buffer, sizeof(buffer) - 1);//sizeof(buffer) - 1 等价于 strlen(buffer),减去‘\0',不需要写入文件,只写入有效字符。
if (bytes_written == -1)
{
perror("写入失败");
}
else
{
printf("成功写入 %zd 字节\n", bytes_written);
}
close(fd);
return 0;
}
3. 处理部分写入的情况
int main()
{
int fd = open("data.txt", O_RDWR | O_CREAT, 0666);
char buffer[] = "这是一个较长的数据,可能需要多次写入才能完成\n";
size_t total_bytes = sizeof(buffer) - 1;//sizeof(buffer)计算数组总大小(包含字符串结尾的\0),减 1 后得到有效数据长度(不包含\0)。
size_t bytes_written = 0;//记录已成功写入的字节数,初始化为 0。
while (bytes_written < total_bytes)//只要未写入的字节数大于 0,就继续循环。
{
ssize_t result = write(fd, buffer + bytes_written, total_bytes - bytes_written);
//buffer + bytes_written:指针偏移到未写入部分的起始位置。
//total_bytes - bytes_written:计算剩余未写入的字节数。
//result:本次实际写入的字节数(可能小于请求写入的字节数)。
if (result == -1)
{
perror("写入失败");
break;
}
bytes_written += result;
}
close(fd);
return 0;
}
关键注意事项
-
文件打开模式的影响 :如果文件是以
O_APPEND
模式打开的,那么每次调用write()
时,数据都会被追加到文件的末尾,而文件偏移量会自动更新。 -
错误处理的重要性 :在调用
write()
函数后,一定要检查返回值,以此来确保数据成功写入。常见的错误原因有磁盘已满、达到文件大小限制或者权限不足等。 -
写入二进制数据 :
write()
函数可以用于写入任意类型的数据,包括二进制数据。不过在处理二进制数据时,要特别注意字节顺序和数据对齐的问题。 -
缓冲区和系统调用 :由于
write()
是一个系统调用,频繁调用会带来一定的开销。所以,在实际应用中,通常会先将数据缓存到内存中,然后再批量写入。
3:read函数
调用函数read()可从打开的文件中读取数据:
#include<unistd.h>
ssize_t read(int fd, void *buf, size_t count);
-
参数
-
fd 文件描述符,这个文件描述符可以通过
open()
获得,也可以是标准输入STDIN_FILENO
(值为 0)。 -
buf 读取到的数据要存放到的地方(缓冲区地址),一般是一个数组的地址
-
count 要读取的最大字节数
-
-
返回值
-
成功:
-
返回实际读取的字节数(范围是 0 到
count
) -
若返回值为 0,说明已经到达文件末尾(EOF),没有更多数据可读取。
-
-
失败:
- 返回-1
-
-
注意
-
读普通文件时,在读到要求字节数之前已经达到了文件结尾。eg:若在达到文件尾端之前有30个字节,而要求读50个字节,则read返回30,下次在调用read时将返回0;
-
当从终端设备读时, 通常一次最多读一行。
-
当从网络读时,网络中的缓冲机制可能造成返回值小于所要求读的字节数。
-
-
示例代码:
1. 从标准输入读取数据
int main() { char buffer[100] = { 0 }; ssize_t bytes_read = read(STDIN_FILENO, buffer, sizeof(buffer)); if (bytes_read == -1) { perror("读取失败"); return 1; } printf("成功读取 %zd 字节\n", bytes_read); return 0; }
2. 从文件读取数据(循环读取直到文件末尾)
int main()
{
int fd = open("data.txt", O_RDONLY);
if (fd == -1)
{
perror("文件打开失败");
return 1;
}
char buffer[50];
ssize_t bytes_read;
while ((bytes_read = read(fd, buffer, sizeof(buffer))) > 0)
{
// 处理读取到的数据,例如打印
write(STDOUT_FILENO, buffer, bytes_read);
}
if (bytes_read == -1)
{
perror("读取失败");
}
close(fd);
return 0;
}
总结:
-
单次调用
read()
返回 0 → 未读取到任何数据。 -
循环中
read()
返回 0 → 文件已全部读完。
4:close函数
当打开一个文件之后,就会占据系统资源,如果我们使用完之后不及时清理,会造成资源的浪费,所以一般使用之后就会关闭文件,释放占用的资源
#include <unistd.h>
int close(int fd);
注意:关闭已打开文件时会释放该进程加在该文件中上的所有记录锁。当一个进程终止时,内核自动关闭其所有已打开的文件。很多程序都利用这一功能而不是在程序中用函数close()关闭已打开的文件
5:lseek函数
每个文件都有一个与之相关联的文件偏移量,用以度量文件起始位置到当前位置的字节数。通常情况下,读写操作都是从当前文件偏移量处开始的,读写完成后,文件偏移量会自动增加所读写的字节数。打开一个文件后,该文件偏移量默认设置为0。通过设置偏移量就可以实现文件指定位置进行插入内容。
调用函数lseek()可显式的为打开的文件设置文件偏移量,用于改变文件的读写位置。其函数原型如下:
off_t lseek(int fd, off_t offset, int whence);
-
参数
-
fd: 文件描述符,是一个整数,表示要操作的文件。
-
offset: 表示相对于
whence
所指定位置的字节数。这个值可以是正数(向文件末尾方向移动)、负数(向文件开头方向移动)或者 0(保持当前位置不变)。 -
whence: 参考点,表示偏移量的起始位置。
- whence的值可以是以下三个常量之一: SEEK_SET:从文件开始处计算偏移量(文件起始位置加offset个字节)。
SEEK_CUR:从当前位置计算偏移量(文件当前位置加offset个字节)。 SEEK_END:从文件结束处计算偏移量(文件末尾位置加offset个字节)。
-
-
返回值
-
成功: 返回新的文件偏移量,这个值是从文件起始位置开始计算的字节数。
-
失败:返回-1
-
函数lseek()仅将当前文件的文件偏移量记录在内核中,并不会引起任何I/O的操作,该文件偏移量用于下一次读写操作 。
示例代码:
1. 将文件偏移量设置到文件开头
int main()
{
int fd = open("data.txt", O_RDWR);
if (fd == -1)
{
perror("文件打开失败");
return 1;
}
// 将文件偏移量设置到文件开头
off_t new_offset = lseek(fd, 0, SEEK_SET);
if (new_offset == -1)
{
perror("lseek失败");
close(fd);
return 1;
}
// 现在可以从文件开头开始读取或写入
close(fd);
return 0;
}
2. 计算文件大小
int main()
{
int fd = open("data.txt", O_RDONLY);
if (fd == -1)
{
perror("文件打开失败");
return 1;
}
// 将文件偏移量设置到文件末尾,并获取新的偏移量(即文件大小)
off_t file_size = lseek(fd, 0, SEEK_END);
if (file_size == -1)
{
perror("lseek失败");
close(fd);
return 1;
}
printf("文件大小为 %lld 字节\n", (long long)file_size);
close(fd);
return 0;
}
3. 定位到文件中间位置进行读取
int main()
{
int fd = open("data.txt", O_RDONLY);
if (fd == -1)
{
perror("文件打开失败");
return 1;
}
// 定位到文件中间位置
off_t file_size = lseek(fd, 0, SEEK_END);
lseek(fd, file_size / 2, SEEK_SET);
// 从文件中间位置开始读取数据
char buffer[100];
ssize_t bytes_read = read(fd, buffer, sizeof(buffer));
if (bytes_read > 0)
{
// 处理读取到的数据
}
close(fd);
return 0;
}
写一个程序来实现文件复制功能:
#include <stdio.h>
#include <stdbool.h>
#include <string.h>
#include <stdlib.h>
#include <fcntl.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
int main(int argc, char *argv[])
{
char *fileName = "test.txt";
char *otherName = "copy.txt";
// 1: 打开文件:------------------------------
int fd = open(fileName, O_RDWR );
int fd2 = open(otherName, O_RDWR | O_CREAT, 0666 );
if(fd == -1 || fd2 == -1)
{
perror("文件创建失败!");
exit(EXIT_FAILURE);
}
// 2: 对文件进行读或写操作:--------------------
// 2-1: 读进数据
char buf[64] = {0};
int length = sizeof(buf) / sizeof(buf[0]);
ssize_t size;
while((size = read( fd, buf, length )) != 0)
{
// 2-2: 写出数据
write( fd2, buf , size );
}
// 3: 关闭文件 ------------------------------
close(fd);
close(fd2);
puts("---end---");
return 0;
}