C语言-标准IO及系统调用IO-文件属性的相关操作函数

几个基本概念

什么是IO:
    I/O输入/输出(Input/Output),分为IO设备和IO接口两个部分。
在C语言中的I/O,个人理解就是把数据传输到程序就是I,从程序中把数据传输出去就是O。
很多时候,数据来源就是文件,所有很多时候我们把对文件的操作,也叫做I/O操作了。
    关于这样的I/O操作,在Linux操作系统上分两种,其中一个是标准IO,另一个是系统调用IO。
系统调用IO与标准IO的区别:
        系统调用IO:由操作系统直接提供的操作文件的接口函数,和操作系统绑定,没有缓存,更接近硬件,可以操作普通文件与驱动文件。
        标准IO:由标准C库(第三方库)提供的操作文件的接口函数(对操作系统提供的系统IO进行二次封装),有缓存,只可以操作普通文件。

C语言中的文件
        我们对文件的概念已经非常熟悉了,比如常见的 word 文档、txt 文件、c源文件等。
文件是数据源的一种,最主要的作用是保存数据。
在C语言程序中,文件有着更广泛的定义:文件通常是在磁盘或硬盘上的已命名的储存区。
要看到是通常,那么就说明会有不寻常的。对于C程序来说,设备也可以看成是文件!
例如显示器(标准输出文件 stdout,标准错误输出stderr)和键盘(标准输入文件 stdin)。

我们不去探讨硬件设备是如何被映射成文件的,大家只需要记住,在C语言中硬件设备可以看成文件!

另外,在C程序看来,文件只有两种形式:
(1)文本文件
(2)二进制文件
        根据我们以往的经验,文本文件通常用来保存肉眼能认识的字符,比如.txt文件、.c文件、.dat文件等,用文本编辑器打开这些文件,我们能够顺利看懂文件的内容。
        二进制文件通常用来保存视频、图片、程序等不可阅读的内容,用文本编辑器打开这些文件,会看到一堆乱码,根本看不懂。
        但是从物理上讲,二进制文件和字符文件并没有什么区别,它们都是以二进制的形式保存在磁盘上的数据。

        我们之所以能看懂文本文件的内容,是因为文本文件中采用的是 ASCII、UTF-8、GBK 等字符编码,文本编辑器可以识别出这些编码格式,并将编码值转换成字符展示出来。

        而二进制文件使用的是 mp4、gif、exe 等特殊编码格式,文本编辑器并不认识这些编码格式,只能按照字符编码格式胡乱解析,所以就成了一堆乱七八糟的字符,有的甚至都没见过。

        总起来说,不同类型的文件有不同的编码格式,必须使用对应的程序(软件)才能正确解析,否则就是一堆乱码,无法使用。

文件流

        文件是数据源的一种,除了文件,还有数据库、网络、键盘等;
数据传递到内存也就是保存到C语言的变量(例如整数、字符串、数组、缓冲区等)。

        我们把数据在数据源和程序(内存)之间传递的过程叫做数据流(Data Stream)。相应的,数据从数据源到程序(内存)的过程叫做输入流(Input Stream),从程序(内存)到数据源的过程叫做输出流(Output Stream)。

        输入输出(Input output,IO)是指程序(内存)与外部设备(键盘、显示器、磁盘、其他计算机等)进行交互的操作。几乎所有的程序都有输入与输出操作,如从键盘上读取数据,从本地或网络上的文件读取数据或写入数据等。通过输入和输出操作可以从外界接收信息,或者是把信息传递给外界。
        一般情况下,我们可以把打开文件说成打开了一个流。

C语言fopen和fclose函数

        C语言中操作文件的正确流程为:打开文件 --> 读写文件 --> 关闭文件。
所谓打开文件,就是获取文件的有关信息,例如文件名、文件状态、当前读写位置等,
这些信息会被保存到一个 FILE 类型的结构体变量中。
关闭文件,就是断开与文件之间的联系,释放结构体变量,同时禁止再对该文件进行操作。

使用 <stdio.h> 头文件中的 fopen() 函数即可打开文件,它的用法为:

FILE *fopen(char *filename, char *mode);

filename为文件名(包括文件路径)
mode为打开模式,它们都是字符串。
在这里插入图片描述
fopen() 函数的返回值类型是(FILE *)
FILE 是 <stdio.h> 头文件中的一个结构体,它专门用来保存文件信息。
我们不用关心 FILE 的具体结构,只需要知道它的用法就行。

例如:fopen.c

#include <stdio.h>
int main(){
    FILE * fp=fopen("./demo.txt","w");
    return 0;
}
//编译执行可执行文件后,若当前文件夹中没有demo.txt,则创建之,
gcc -g fopen.c -o fopen.exe
./fopen

表示以“只读”方式打开当前目录下的 demo.txt 文件,并使 fp 指向该文件,
这样就可以通过 fp 来操作 demo.txt 了。fp 通常被称为文件指针。若在文件中加了内容,我再次执行可执行文件./fopen,会清空demo.txt了。
再来看一个例子:

FILE *fp = fopen("D:\\demo.txt","rb+");

表示以二进制方式打开 D 盘下的 demo.txt 文件,允许读和写。

--------文本方式和二进制方式并没有本质上的区别,只是对于换行符的处理不同。
C语言程序将\n作为换行符,类 UNIX/Linux 系统在处理文本文件时也将\n作为换行符,所以程序中的数据会原封不动地写入文本文件中,反之亦然。

但是 Windows 系统却不同,它将\r\n作为文本文件的换行符。

--------在 Windows 系统中,如果以文本方式打开文件,当读取文件时,程序会将文件中所有的\r\n转换成一个字符\n。也就是说,如果文本文件中有连续的两个字符是\r\n,则程序会丢弃前面的\r,只读入\n。

当写入文件时,程序会将\n转换成\r\n写入。也就是说,如果要写入的内容中有字符\n,则在写入该字符前,程序会自动先写入一个\r。

         总起来说,对于 Windows 平台,为了保险起见,我们最好用"t"来打开文本文件,用"b"来打开二进制文件。对于 Linux 平台,使用"t"还是"b"都无所谓,既然默认是"t",那我们什么都不写就行了。
在这里插入图片描述

判断文件是否打开成功
      打开文件出错时,fopen() 将返回一个空指针,也就是 NULL,我们可以利用这一点来判断文件是否打开成功,请看下面的代码:

FILE *fp;
if( (fp=fopen("D:\\demo.txt","rb") == NULL ){
    printf("Fail to open file!\n");
    exit(0);  //退出程序(结束程序)
}

      以上代码是文件操作的规范写法,在打开文件时一定要判断文件是否打开成功,因为一旦打开失败,后续操作就都没法进行了,往往以“结束程序”告终。
fclose函数
      当进行打开文件操作时, 操作系统会建立文件登记表中记录该文件处于打开状态,除了申请进行打开文件操作的进程,其他程序不能对文件进行访问和写入。
      文件一旦使用完毕,应该用 fclose() 函数把文件关闭,以释放相关资源,避免数据丢失。fclose() 的用法为:

int fclose(FILE *fp);

fp 为文件指针。例如:

int fclose(fp);

文件正常关闭时,fclose() 的返回值为0,如果返回非零值则表示有错误发生。

实例演示
      最后,我们通过一段完整的代码来演示 fopen 函数的用法,这个例子会一行一行地读取文本文件的所有内容:

#include <stdio.h>
#include <stdlib.h>
int main(){
    FILE * fp =NULL;
    //判断一个,有没有成功打开文件,如果打开出错,直接退出程序
    if((fp = fopen("./demo.txt","rt")) == NULL){
        puts("fail to open file!!!");
        exit(0);
    }
    //打开文件后,就对文件进行读写操作。。。。

    //用完了文件后,一定要关闭
    if(fclose(fp) == 0){
        puts("关闭文件成功!!!");
    }else{
        puts("文件关闭失败");
        exit(0);
    }
    return 0;
}


//编译与执行
gcc -g .\fopen_close.c -o fopenclose.exe     
./fopenclose
关闭文件成功!!!

C语言fgetc和fputc函数用法

      在C语言中,读写文件比较灵活,既可以每次读写一个字符,也可以读写一个字符串,甚至是任意字节的数据(数据块)。每次可以从文件中读取一个字符,或者向文件中写入一个字符。
主要使用两个函数,分别是 fgetc() 和 fputc()。
字符读取函数 fgetc
      fgetc 是 file get char 的缩写,意思是从指定的文件中读取一个字符。fgetc() 的用法为:

int fgetc (FILE *fp);

         fp 为文件指针。
      fgetc() 读取成功时返回读取到的字符,读取到文件末尾或读取失败时返回EOF。
EOF 是 end of file 的缩写,表示文件末尾,是在 stdio.h 中定义的宏,它的值是一个负数,往往是 -1。
      fgetc() 的返回值类型之所以为 int,就是为了容纳这个负数(char不能是负数)。
注意:不能武断地认为EOF 就是 -1,也可以是其他负数,这要看编译器的实现。

fgetc() 的用法举例:

char ch;
FILE *fp = fopen("./demo.txt", "r+");
ch = fgetc(fp);

   表示从当前目录中的demo.txt文件中读取一个字符,并保存到变量 ch 中。在文件内部有一个位置指针,用来指向当前读写到的位置,也就是读写到第几个字节。
      在文件打开时,该指针总是指向文件的第一个字节。使用 fgetc() 函数后,该指针会向后移动一个字节,所以可以连续多次使用 fgetc() 读取多个字符。
注意:这个文件内部的位置指针与C语言中的指针不是一回事。
   位置指针仅仅是一个标志,表示文件读写到的位置,也就是读写到第几个字节,它不表示地址。
   文件每读写一次,位置指针就会移动一次,它不需要你在程序中定义和赋值,而是由系统自动设置,对用户是隐藏的。

【示例】在屏幕上显示 当前目录中的demo.txt 文件的内容。

#include <stdio.h>
#include <stdlib.h>
int main(){
    FILE * fp =NULL;
    char ch;
    //判断一个,有没有成功打开文件,如果打开出错,直接退出程序
    if((fp = fopen("./demo.txt","rt")) == NULL){
        puts("fail to open file!!!");
        exit(0);
    }
    //打开文件后,就对文件进行读写操作。。。。
    while((ch=fgetc(fp))!=EOF){
        printf("%c",ch);
    }

    //用完了文件后,一定要关闭
    if(fclose(fp) == 0){
        puts("\n关闭文件成功!!!");
    }else{
        puts("文件关闭失败");
        exit(0);
    }
    return 0;
}
//编译执行
>gcc -g .\fopen_close.c -o fopenclose.exe     
>./fopenclose
xiongshaowen
关闭文件成功!!!

对 EOF 的说明
      EOF 本来表示文件末尾,意味着读取结束,但是很多函数在读取出错时也返回 EOF,那么当返回 EOF 时,到底是文件读取完毕了还是读取出错了?
我们可以借助 stdio.h 中的两个函数来判断,分别是 feof() 和 ferror()。
      feof() 函数用来判断文件内部指针是否指向了文件末尾,它的原型是:

int feof ( FILE * fp );

当指向文件末尾时返回非零值,否则返回零值。

      ferror() 函数用来判断文件操作是否出错,它的原型是:

int ferror ( FILE *fp );

出错时返回非零值,否则返回零值。

      需要说明的是,文件出错是非常少见的情况,上面的示例基本能够保证将文件内的数据读取完毕。
如果追求完美,也可以加上判断并给出提示:

while((ch=fgetc(fp))!=EOF){
    printf("%c",ch);
}
if((ferror(fp))==0){
    printf("\n正常读完文件!");
}else{
    printf("\n文件读取出了错误 !");
}

字符写入函数 fputc
      fputc 是 file output char 的所以,意思是向指定的文件中写入一个字符。fputc() 的用法为:

int fputc ( int ch, FILE *fp );

      ch 为要写入的字符,fp 为文件指针。
      fputc() 写入成功时返回写入的字符,失败时返回 EOF,返回值类型为 int 也是为了容纳这个负数。
例如:

fputc('a', fp);

或者:

char ch = 'a';
fputc(ch, fp);

表示把字符 ‘a’ 写入fp所指向的文件中。

注意:写和读一样,每写入一个字符,文件内部位置指针向后移动一个字节。

【示例】从键盘输入一行字符,写入文件。

#include <stdio.h>
#include <stdlib.h>
int main(){
    FILE * fp =NULL;
    char ch;
    //判断一个,有没有成功打开文件,如果打开出错,直接退出程序
    if((fp = fopen("./demo.txt","r+")) == NULL){
        puts("fail to open file!!!");
        exit(0);
    }

    //打开文件后,就对文件进行写操作。。。。
    printf("\n请输入要写入文件的字符!以换行结束:  ");
    while((ch = getchar())!='\n'){
        fputc(ch,fp);
    }
     printf("\n写入成功!");
   
     //用完了文件后,一定要关闭
    if(fclose(fp) == 0){
        puts("\n关闭文件成功!!!");
    }else{
        puts("文件关闭失败");
        exit(0);
    }
    return 0;
}

C语言fgets和fputs函数

fgetc() 和 fputc() 函数每次只能读写一个字符,速度较慢;
      实际开发中往往是每次读写一个字符串或者一个数据块,这样能明显提高效率。

读字符串函数 fgets
      fgets() 函数用来从指定的文件中读取一个字符串,并保存到字符数组中,它的用法为:

char *fgets ( char *str, int n, FILE *fp );

str 为字符数组,n 为要读取的字符数目,fp 为文件指针。

返回值:读取成功时返回字符数组首地址,也即 str;
    读取失败时返回 NULL;
    如果开始读取时文件内部指针已经指向了文件末尾,那么将读取不到任何字符,也返回 NULL。

注意,读取到的字符串会在末尾自动添加 ‘\0’,n 个字符也包括 ‘\0’。
   也就是说,实际只读取到了 n-1 个字符,如果希望读取 100 个字符,n 的值应该为 101。
例如:

#define N 101
char str[N];
FILE *fp = fopen("D:\\demo.txt", "r");
fgets(str, N, fp);

需要重点说明的是,在读取到 n-1 个字符之前如果出现了换行,或者读到了文件末尾,则读取结束。
   这就意味着,不管 n 的值多大,fgets() 最多只能读取一行数据,不能跨行。

在C语言中,没有按行读取文件的函数,我们借助 fgets(),将 n 的值设置很大,每次就可以读取到一行行的数据。

【示例】一行一行地读取文件。

#include <stdio.h>
#include <stdlib.h>
int main(){
    FILE * fp = NULL;
    char ch;
    char str[101] = {0};  //假设读100个字符,我们要额外留下一个位置装'\0'
    //判断一个,有没有成功打开文件,如果打开出错,直接退出程序
    if((fp = fopen("./demo.txt","r+")) == NULL){
        puts("fail to open file!!!");
        exit(0);
    }
    //读当前文件demo.txt,已经有几行内容了
    while(fgets(str,101,fp) != NULL){
        printf("%s",str);
    }

     //用完了文件后,一定要关闭
    if(fclose(fp) == 0){
        puts("\n关闭文件成功!!!");
    }else{
        puts("\n文件关闭失败");
        exit(0);
    }

}
//编译执行
>gcc -g frwstring.c -o frwstring
> ./frwstring
 
熊少文
xuhuifeng
xionglingzhou
sbsbsb
aaaaaa
bbbbbb
cccccc                             

写字符串函数 fputs
      fputs() 函数用来向指定的文件写入一个字符串,它的用法为:

int fputs( char *str, FILE *fp );

      str 为要写入的字符串,fp 为文件指针。写入成功返回非负数,失败返回 EOF。例如:

char *str = "hello world";
FILE *fp = fopen("./demo.txt", "at+");
fputs(str, fp);

表示把把字符串 str 写入到 D:\demo.txt 文件中。

【示例】向上例中建立的 d:\demo.txt 文件中追加一个字符串。

#include <stdio.h>
#include <stdlib.h>
int main(){
    FILE * fp = NULL;
    char ch;
    char str[101] = {0};  //假设读100个字符,我们要额外留下一个位置装'\0'
    //判断一个,有没有成功打开文件,如果打开出错,直接退出程序
    if((fp = fopen("./demo.txt","r+")) == NULL){
        puts("fail to open file!!!");
        exit(0);
    }

    //写入字符串到当前demo.txt中
    printf("\n请输入字符串!");
    gets(str);    //gets函数,请注意不要越界101
    fputs(str,fp);

    //读当前文件demo.txt,已经有几行内容了,fp指针要赋初始值,即我们要重新打开文件,初始到最开始的位置
    printf("读取的内容如下:\n");
    //判断一个
    if((fp = fopen("./demo.txt","r")) == NULL){
        puts("fail to open file!!!");
        exit(0);
    }
    while(fgets(str,101,fp) != NULL){
        printf("\n%s",str);
    }

     //用完了文件后,一定要关闭
    if(fclose(fp) == 0){
        puts("\n关闭文件成功!!!");
    }else{
        puts("\n文件关闭失败");
        exit(0);
    }

}

注意,上面的代码,读出的是上一次写进文件后的内容不是本次的内容,这种问题以后再讲,这里只是讲一下写字符串的功能

C语言fread和fwrite的用法

      fgets() 有局限性,每次最多只能从文件中读取一行内容,因为 fgets() 遇到换行符就结束读取,上面代码我们用了while循环读了多行。
      如果希望读取多行内容,需要使用 fread() 函数;fputs一次只能写入一行,想一次写入多行数据,相应地写入函数为 fwrite()。
      对于 Windows 系统,使用 fread() 和 fwrite() 时应该以二进制的形式打开文件!因为换行时有两个字符‘‘\r\n’

      fread() 函数用来从指定文件中读取块数据块。所谓块数据,也就是若干个字节的数据,可以是一个字符,可以是一个字符串,可以是多行数据,并没有什么限制。

fread() 的原型为:

size_t fread ( void *ptr, size_t size, size_t count, FILE *fp );

fwrite() 函数用来向文件中写入块数据,它的原型为:

size_t fwrite ( void * ptr, size_t size, size_t count, FILE *fp );

对参数的说明:
(1)ptr 为内存区块的指针,它可以是数组、变量、结构体等。
fread() 中的 ptr 用来存放读取到的数据,fwrite() 中的 ptr 用来存放要写入的数据。
(2)size:表示每个数据块的字节数。
(3)count:表示要读写的数据块的块数。
(4)fp:表示文件指针。
(5)理论上,每次读写 size*count 个字节的数据。
(6)size_t 是在 stdio.h 和 stdlib.h 头文件中使用 typedef 定义的数据类型,无符号整数,常用来表示数量。

返回值:返回成功读写的块数,也即 count。如果返回值小于 count:
(1)对于 fwrite() 来说,肯定发生了写入错误,可以用 ferror() 函数检测。
(2)对于 fread() 来说,可能读到了文件末尾,可能发生了错误,可以用 ferror() 或 feof() 检测。

【示例】从键盘输入一个数组,将数组写入文件再读取出来。

#include <stdio.h>
#include <stdlib.h>
int main(){
    FILE * fp = NULL;
    char ch;
    //判断一个,有没有成功打开文件,如果打开出错,直接退出程序 ,'wb+'二进制读写文件,重写时先清清除原内容
    if((fp = fopen("./demo.txt","wb+")) == NULL){
        puts("fail to open file!!!");
        exit(0);
    }
    //练习用fread和fwrite读写文件的数据
    int a[5]={0},b[5]={0};
    int i=0;
    int size = sizeof(int);   //数据块数
    for(i=0;i<5;i++){
        scanf("%d",&a[i]);
    }
    fwrite(a,size,5,fp);
    //读取数据到b数组中
    rewind(fp);    //将文件内部的内容指针重新指向文件的头位置处
    fread(b,size,5,fp);
    printf("\n从文件中读到数组b的数据为:   \n");
    for(i=0;i<5;i++){
        printf("%d",b[i]);

    }
   //用完了文件后,一定要关闭
    if(fclose(fp) == 0){
        puts("\n关闭文件成功!!!");
    }else{
        puts("\n文件关闭失败");
        exit(0);
    }

}

数据写入完毕后,位置指针在文件的末尾,要想读取数据,必须将文件指针移动到文件开头rewind(fp);

【示例】从键盘输入两个学生数据,写入一个文件中,再读出这两个学生的数据显示在屏幕上。

#include<stdio.h>
#define N 2
struct stu{
    char name[10]; //姓名
    int num;  //学号
    int age;  //年龄
    float score;  //成绩
}boya[N], boyb[N], *pa, *pb;
int main(){
    FILE *fp;
    int i;
    pa = boya;
    pb = boyb;
    if( (fp=fopen("./demo.txt", "wb+")) == NULL ){  //w+b,表示打开文件时,清除原文件的内容,若没有文件,则创建demo.txt文件
        puts("Fail to open file!");
        exit(0);
    }
    //从键盘输入数据
    printf("Input data:\n");
    for(i=0; i<N; i++,pa++){
        scanf("%s %d %d %f",pa->name, &pa->num,&pa->age, &pa->score);
    }
    //将数组 boya 的数据写入文件
    fwrite(boya, sizeof(struct stu), N, fp);
    //将文件指针重置到文件开头
    rewind(fp);
    //从文件读取数据并保存到数据 boyb
    fread(boyb, sizeof(struct stu), N, fp);
    //输出数组 boyb 中的数据
    for(i=0; i<N; i++,pb++){
        printf("%s  %d  %d  %f\n", pb->name, pb->num, pb->age, pb->score);
    }
    fclose(fp);
    return 0;
}
//编译与执行
>gcc -g freadwrite.c -o freadwritestu.exe
>./freadwritestu
Input data:
xiong 1 50 59.6
xu 2 37 98.9
xiong  1  50  59.599998
xu  2  37  98.900002

C语言fscanf和fprintf函数

        fscanf() 和 fprintf() 函数与前面使用的 scanf() 和 printf() 功能相似,都是格式化读写函数,两者的区别在于 fscanf() 和 fprintf() 的读写对象不是键盘和显示器,而是磁盘文件。

这两个函数的原型为:

int fscanf ( FILE *fp, char * format, ... );
int fprintf ( FILE *fp, char * format, ... );

fp 为文件指针,format 为格式控制字符串,… 表示参数列表。
与 scanf() 和 printf() 相比,它们仅仅多了一个 fp 参数。

例如:r+,表示打开文件后可读写,但不会清空原文件内容,没有文件也不会创建

#include <stdio.h>
#include <stdlib.h>
int main(){
    FILE *fp;
    int i;
    if( (fp=fopen("./demo.txt", "r+")) == NULL ){  //r+,表示打开文件后可读写,但不会清空原文件内容
        puts("Fail to open file!");
        exit(0);
    }
     //读文件中的数据到数组v中,再打印v数组内容
     char v[100] = {0};
     fscanf(fp,"%s",v);
     printf("%s \n",v);
     
    //用完了文件后,一定要关闭
    if(fclose(fp) == 0){
        puts("\n关闭文件成功!!!");
    }else{
        puts("文件关闭失败");
        exit(0);
    }
    return 0;
}

写数据到文件中fprintf函数测试

#include <stdio.h>
#include <stdlib.h>
int main(){
    FILE *fp;
    int i;
    if( (fp=fopen("./demo.txt", "r+")) == NULL ){  //r+,表示打开文件后可读写
        puts("Fail to open file!");
        exit(0);
    }
    char v[100] = {0};
    
     //读文件到数组中
     //fscanf(fp,"%s",v);
     //printf("%s \n",v);

     //写数据到文件中,       这里代码功能,只是测试用,读和写我们要注释一个,不然有异常结果
     scanf("%100[^\n]",v);
     fprintf(fp,"%s",v);
   
    //用完了文件后,一定要关闭
    if(fclose(fp) == 0){
        puts("\n关闭文件成功!!!");
    }else{
        puts("文件关闭失败");
        exit(0);
    }
    return 0;
}

在这里插入图片描述

在这里插入图片描述

fprintf() 返回成功写入的字符的个数,失败则返回负数。
fscanf() 返回参数列表中被成功赋值的参数个数。

【示例】用 fscanf 和 fprintf 函数来完成对学生信息的读写。

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
//定义学生信息的结构体,两个结构体数组变量和指针
struct stu
{
    char name[10];
    int num;
    int age;
    float score;
} boya[2],boyb[2],*pa,*pb;

int main(){
    FILE *fp;
    int i;
    if( (fp=fopen("./demo.txt", "r+")) == NULL ){  //r+,表示打开文件后可读写,没有文件不创建,不清除原内容
        puts("Fail to open file!");
        exit(0);
    }
    
    //从健盘中获取数据,保存到boya,后面再写入到文件中,
    pa = boya;            //数组名代表当前数组的地址(头地址)
    pb = boyb;
    printf("请输入学生信息:  ");
    for(i=0;i<2;i++,pa++){
        scanf("%s %d %d %f",pa->name,&pa->num,&pa->age,&pa->score);  //字符数组名就是地址了,不用&取地址符
    }
    //写入文件,重新把pa指向首址,因为上面pa已指向了尾地址了
    pa = boya;
    for(i=0;i<2;i++,pa++){
        fprintf(fp,"%s %d %d %f\n",pa->name,pa->num,pa->age,pa->score);
    }

    //读文件到boyb中
    rewind(fp);            //文件内容指针重指到头位置
    for(i=0;i<2;i++,pb++){
        fscanf(fp,"%s %d %d %f",pb->name,&pb->num,&pb->age,&pb->score);
    }
    pb = boyb;
    for(i=0;i<2;i++,pb++){
        printf("%s %d %d %f\n",pb->name,pb->num,pb->age,pb->score);
    }


    //用完了文件后,一定要关闭
    if(fclose(fp) == 0){
        puts("\n关闭文件成功!!!");
    }else{
        puts("文件关闭失败");
        exit(0);
    }
    return 0;
}
//编译执行
>gcc -g fscanffprintfstu.c -o fscanfprintfstu.exe
>./fscanfprintfstu
输入学生信息:  
xiongwen 20240430 41 92.5
xuhuifeng 20240429 37 98.5
                               //显示的是boyb中的数据
xiongwen 20240430 41 92.500000
xuhuifeng 20240429 37 98.500000

关闭文件成功!!!

      打开 当前./demo.txt,发现文件的内容是可以阅读的,格式非常清晰。
      用 fprintf() 和 fscanf() 函数读写配置文件、日志文件会非常方便,不但程序能够识别,用户也可以看懂,可以手动修改,因为它是生产了文本文件,不像其它一样要用专门软件格式打开之。

   如果将 fp 设置为 stdin,那么 fscanf() 函数将会从键盘读取数据,与 scanf 的作用相同;
   设置为 stdout,那么 fprintf() 函数将会向显示器输出内容,与 printf 的作用相同。
例如:

#include<stdio.h>
int main(){
    int a, b, sum;
    fprintf(stdout, "Input two numbers: ");
    fscanf(stdin, "%d %d", &a, &b);
    sum = a + b;
    fprintf(stdout, "sum=%d\n", sum);
    return 0;
}

rewind和fseek函数-移动文件指针

      前面介绍的文件读写函数都是顺序读写,即读写文件只能从头开始,依次读写各个数据。但在实际开发中经常需要读写文件的中间部分,要解决这问题,就得先移动文件内部的位置指针,再进行读写。
   这种读写方式称为随机读写,也就是说从文件的任意位置开始读写。

移动文件内部位置指针的函数主要有两个,即 rewind() 和 fseek()。
      rewind() 用来将位置指针移动到文件开头,前面已经多次使用过,它的原型为:

void rewind ( FILE *fp );

      fseek() 用来将位置指针移动到任意位置,它的原型为:

int fseek ( FILE *fp, long offset, int origin );

参数说明:

  1. fp 为文件指针,也就是被移动的文件。

  2. offset 为偏移量,也就是要移动的字节数。
    之所以为 long 类型,是希望移动的范围更大,能处理的文件更大。
    offset 为正时,向后移动;offset 为负时,向前移动。

  3. origin 为起始位置,也就是从何处开始计算偏移量。
    C语言规定的起始位置有三种,分别为文件开头、当前位置和文件末尾,每个位置都用对应的常量来表示:
    在这里插入图片描述

例如,把位置指针移动到离文件开头100个字节处:

fseek(fp, 100, 0);

-----值得说明的是,fseek() 一般用于二进制文件(打开文件模式为b),在文本文件中由于要进行转换,计算的位置有时会出错。

文件的随机读写
      在移动位置指针之后,就可以用前面介绍的任何一种读写函数进行读写了。
由于是二进制文件,因此常用 fread() 和 fwrite() 读写。

【示例】从键盘输入三组学生信息,保存到文件中,然后读取第二个学生的信息。

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
//定义学生信息的结构体,两个结构体数组变量和指针
struct stu
{
    char name[10];
    int num;
    int age;
    float score;
} boys[3],boy,*pboys;

int main(){
    FILE *fp;
    int i;
    if( (fp=fopen("./demo.txt", "w+b")) == NULL ){  //w+b,表示打开文件后可读写,没有文件创建,清除原内容
        puts("Fail to open file!");
        exit(0);
    }
    
    //从健盘中获取数据,保存到boys,后面再写入到文件中,
    pboys = boys;            //数组名代表当前数组的地址(头地址)
    printf("请输入学生信息:  ");
    for(i=0;i<3;i++,pboys++){
        scanf("%s %d %d %f",pboys->name,&pboys->num,&pboys->age,&pboys->score);  //字符数组名就是地址了,不用&取地址符
    }
    //写入文件,写本个数据块,一个块大小为结构体所占用的数据空间
    fwrite(boys,sizeof(struct stu),3,fp);
    

    //读文件到boy中,写入三个学生信息,我们只读第二学生信息
    fseek(fp,sizeof(struct stu),SEEK_SET);  
    fread(&boy,sizeof(struct stu),1,fp);
    printf("\n第二个学生为:\n");
    printf("%s %d %d %f",boy.name,boy.num,boy.age,boy.score);

    //用完了文件后,一定要关闭
    if(fclose(fp) == 0){
        puts("\n关闭文件成功!!!");
    }else{
        puts("文件关闭失败");
        exit(0);
    }
    return 0;
}
//编译执行结果
> gcc -g freadwritestufseek.c -o fseek.exe        
> ./fseek                                         
请输入学生信息:  
xiongshaowen 1 41 96.5
xuhuifeng 2 37 98.5
huangren 3 36 90.1

第二个学生为:
xuhuifeng 2 37 98.500000
关闭文件成功!!!

C语言实现文件复制功能

      文件的复制是常用的功能,要求写一段代码,让用户输入要复制的文件以及新建的文件,然后对文件进行复制。能够复制的文件包括文本文件和二进制文件,你可以复制1G的电影,也可以复制1Byte的txt文档。

      实现文件复制的主要思路是:开辟一个缓冲区,不断从原文件中读取内容到缓冲区,每读取完一次就将缓冲区中的内容写入到新建的文件,直到把原文件的内容读取完。

这里有两个关键的问题需要解决:

  1. 开辟多大的缓冲区合适?缓冲区过小会造成读写次数的增加,过大也不能明显提高效率。目前大部分磁盘的扇区都是4K对齐的,如果读写的数据不是4K的整数倍,就会跨扇区读取,降低效率,所以我们开辟4K的缓冲区。

  2. 缓冲区中的数据是没有结束标志的,如果缓冲区填充不满,如何确定写入的字节数?最好的办法就是每次读取都能返回读取到的字节数。

fread() 的原型为:

size_t fread ( void *ptr, size_t size, size_t count, FILE *fp );

   它返回成功读写的块数,该值小于等于 count。
   如果我们让参数 size 等于1,那么返回的就是读取的字节数。

代码实现:

#include <stdio.h>
#include <stdlib.h>
//实现复制文件函数
int copyFile(char * fileRead,char * fileWrite){
    FILE * fpRead;     //指向要复制的文件
    FILE * fpWrite;    //指向复制后的文件
    //设置缓冲区
    int bufferLen = 4*1024;   //4k
    char * buffer = (char *)malloc(bufferLen);   //动态分配缓冲区
    if((fpRead = fopen(fileRead,"rb")) == NULL || (fpWrite = fopen(fileWrite,"wb")) == NULL){
        printf("打开文件失败!\n");
        exit(1);     //打开文件失败,异常退出,exit(0),没问题时退出
    }
    //不断读取源文件的数据,放到目标文件中,下面代码是很多语言通用形式
    int readCount = 0;
    while((readCount = fread(buffer,1,bufferLen,fpRead)) >0){
        fwrite(buffer,readCount,1,fpWrite);
    }

    free(buffer);     //功能寮现完成后,释放缓冲区
    fclose(fpRead);
    fclose(fpWrite);
    return 1;
}
int main(){
    char fileRead[100];    //源文件名字
    char fileWrite[100];   //目标文件名字
    //用户输入要复制的文件
    printf("请输入要复制的文件的名字: \n");
    scanf("%s",fileRead);
    printf("请输入将文件复制的文件名字: \n");
    scanf("%s",fileWrite);
    //调用函数复制文件
    if(copyFile(fileRead,fileWrite)){
        printf("复制成功! \n");
    }else{
        printf("复制失败! \n");
    }
    return 0;
}
//编译执行效果
 >gcc -g freadwritefilecopy.c -o copyfile.exe
 >./copyfile
 请输入要复制的文件的名字,不输入绝对路径,为当前目录中:
c:\\a.txt 
请输入将文件复制的文件名字:
c:\\b.txt
复制成功!

C语言获取文件大小

      实际开发中,有时候需要先获取文件大小再进行下一步操作。
C语言没有提供获取文件大小的函数,要想实现该功能,必须自己编写函数。

ftell()函数
   ftell() 函数用来获取文件内部指针(位置指针)距离文件开头的字节数,它的原型为:

long int ftell ( FILE * fp );

注意:fp 要以二进制方式打开,如果以文本方式打开,函数的返回值可能没有意义。

      先使用 fseek() 将文件内部指针定位到文件末尾,再使用 ftell() 返回内部指针距离文件开头的字节数,这个返回值就等于文件的大小。

long fsize(FILE *fp){
    fseek(fp, 0, SEEK_END);
    return ftell(fp);
}

      这段代码并不健壮,它移动了文件内部指针,可能会导致接下来的文件操作错误。
      所以,获取到文件大小后还需要恢复文件内部指针,不然其它地方会出问题,请看下面的代码:

long fsize(FILE *fp){
    long n;
    fpos_t fpos;  //当前位置
    fgetpos(fp, &fpos);  //获取当前位置
    fseek(fp, 0, SEEK_END);
    n = ftell(fp);
    fsetpos(fp,&fpos);  //恢复之前的位置
    return n;
}

      fpos_t 是在 stdio.h 中定义的结构体,用来保存文件的内部指针。
      fgetpos() 用来获取文件内部指针,fsetpos() 用来设置文件内部指针。

完整的示例:

#include<stdio.h>
#include<stdlib.h>
#include<conio.h>
long fsize(FILE *fp);
int main(){
    long size = 0;
    FILE *fp = NULL;
    char filename[30] = "c:\\2.png";
    if( (fp = fopen(filename, "rb")) == NULL ){  //以二进制方式打开文件
        printf("Failed to open %s...", filename);
        getch();
        exit(EXIT_SUCCESS);
    }
   
    printf("%ld\n", fsize(fp));
    return 0;
}

long fsize(FILE *fp){
    long n;
    fpos_t fpos;  //当前位置
    fgetpos(fp, &fpos);  //获取当前位置,并保存在fpos中
    fseek(fp, 0, SEEK_END);
    n = ftell(fp);
    fsetpos(fp,&fpos);  //恢复之前的位置
    return n;
}

在这里插入图片描述

C语言插入、删除、更改文件内容

      C语言没有提供插入、删除、修改文件内容的函数,要想实现这些功能,只能自己编写函数。
   我们平时所见的文件,例如 txt、doc、mp4 等,文件内容是按照从头到尾的顺序依次存储在磁盘上的,就像排起一条长长的队伍,称为顺序文件。
      除了顺序文件,还有索引文件、散列文件等,一般用于特殊领域,例如数据库、高效文件系统等。
      以插入数据为例,假设原来文件的大小为 1000 字节,现在要求在500字节处插入用户输入的字符串,那么可以这样来实现:

1) 创建一个临时文件,将后面500字节的内容复制到临时文件;
2) 将原来文件的内部指针调整到500字节处,写入字符串;
3) 再将临时文件中的内容写入到原来的文件。

      删除数据时,也是类似的思路。假设原来文件大小为1000字节,名称为 demo.mp4,现在要求在500字节处往后删除100字节的数据,那么可以这样来实现:

1) 创建一个临时文件,先将前500字节的数据复制到临时文件,再将600字节之后的所有内容复制到临时文件;
2) 删除原来的文件,并创建一个新文件,命名为 demo.mp4;
3) 将临时文件中的所有数据复制到 demo.mp4。

      修改数据时,如果新数据和旧数据长度相同,那么设置好内部指针,直接写入即可;
      如果新数据比旧数据长,相当于增加新内容,思路和插入数据类似;
      如果新数据比旧数据短,相当于减少内容,思路和删除数据类似。

   实际开发中,我们往往会保持新旧数据长度一致,以减少编程的工作量,所以我们不再讨论新旧数据长度不同的情况。

总起来说,本节重点讨论数据的插入和删除。
文件复制函数
      在数据的插入删除过程中,需要多次复制文件内容,我们有必要将该功能实现为一个函数,如下所示:

/**
 * fsource :要复制的源文件
 * offsetSource:开发复制的位置
 * len:复制数据的长度
 * fTarget:数据复制到的目标文件
 * offsetTarget:放数据到目标文件的这个位置
 **/
long fcopy(FILE *fsource, long offsetSource, long len, FILE *fTarget,
           long offsetTarget) {
    int bufferLen = 4 * 1024;
    char *buffer = (char *)malloc(bufferLen);
    int readCount = 0;  //每次读取的数据长度
    long nBtytes = 0;   //记录已经读了的数据长度
    int n = 0;  //需要调用多少次fread,源文件要被读取多少次,能读完数据

    //重置源文件和目标文件的位置指针
    fseek(fsource, offsetSource, SEEK_SET);
    fseek(fTarget, offsetTarget, SEEK_SET);

    //开始复制内容
    if (len < 0) {  //从开始位置开始,后面的所有内容全部复制
        while ((readCount = fread(buffer, 1, bufferLen, fsource)) > 0) {
            nBtytes += readCount;
            fwrite(buffer, readCount, 1, fTarget);
        }
    } else {  //从开始位置复制len长度的数据
        n = (int)ceil(
            (double)((double)len / bufferLen));  // ceil函数向上取整,1.1===2 要include <math.h>
        for (int i = 1; i <= n; i++) {
            if ((len - nBtytes) < bufferLen) {
                bufferLen = len - nBtytes;
            }
            readCount = fread(buffer, 1, bufferLen, fsource);
            fwrite(buffer, readCount, 1, fTarget);
            nBtytes += readCount;
        }
    }
    fflush(fTarget);  //刷新系统缓冲区,保证数据都进入到目标文件的磁盘上去
    free(buffer);
    return nBtytes;
}

-------------该函数可以将原文件任意位置的任意长度的内容复制到目标文件的任意位置,非常灵活。
那么可以像这面这样调用:

fcopy(fSource, 0, -1, fTarget, 0);

文件内容插入函数
先看代码:

/**
 * filename:要插入内容的文件名字,包含路径
 * offset:要插入内容的位置
 * buffer:要插入的内容
 * len:要插入的内容的长度
 * */
int finsert(char *filename, long offset, void *buffer, int len) {
    //打开文件
    FILE *fp = NULL;
    if ((fp = fopen(filename, "r+b")) == NULL) {
        puts("fail to open file!");
        return -1;  //操作失败
    }

    //获取目标文件的长度
    long fileSize = fsize(fp);
    if (offset > fileSize || offset < 0 || len < 0) {
        return -1;
    }

    if (offset == fileSize) {         //插入的数据是直接在末尾
        fseek(fp, offset, SEEK_SET);  //位置指针移动到了文件的末尾
        if (!fwrite(buffer, len, 1, fp)) {
            return -1;
        }
    }

    if (offset < fileSize) {
        FILE *fpTemp =
            tmpfile();  //自动创建临时文件,可读写,fclose后,临时文件会自动删除
        fcopy(fp, 0, offset, fpTemp, 0);  //灵活的文件复制函数就被使用了
        fwrite(buffer, len, 1, fpTemp);  //把插入的内容写到临时文件的尾巴上
        fcopy(fp, offset, -1, fpTemp,
              offset + len);  //把源文件的后半部分复制到临时文件的尾巴上
        freopen(filename, "wb+", fp);
        fcopy(fpTemp, 0, -1, fp, 0);
        fclose(fpTemp);
    }
    fclose(fp);
    return 0;  //操作成功
}

代码说明:

  1. fsize() 是前面自定义的函数,用来获取文件大小(以字节计)。
  2. 第17行判断数据的插入位置,如果是在文件末尾,就非常简单了,直接用 fwrite() 写入即可。
  3. 如果从文件开头或中间插入,就得创建临时文件。

tmpfile() 函数用来创建一个临时的二进制文件,可以读取和写入数据,相当于 fopen() 函数以"wb+"方式打开文件。该临时文件不会和当前已存在的任何文件重名,并且会在调用 fclose() 后或程序结束后自动删除。

文件内容删除函数
看下面的代码:

int fdelete(char *filename, long offset, int len) {
    //打开文件
    FILE *fp = NULL;
    if ((fp = fopen(filename, "r+b")) == NULL) {
        puts("fail to open file!");
        return -1;  //操作失败
    }

    //获取目标文件的长度
    long fileSize = fsize(fp);
    if (offset > fileSize || offset < 0 || len < 0) {
        return -1;
    }

    FILE *fpTemp =
        tmpfile();  //自动创建临时文件,可读写,fclose后,临时文件会自动删除
    fcopy(fp, 0, offset, fpTemp, 0);  //灵活的文件复制函数就被使用了
    fcopy(fp, offset + len, -1, fpTemp,
          offset);  //把源文件的后半部分复制到临时文件的尾巴上
    freopen(filename, "wb+", fp);
    fcopy(fpTemp, 0, -1, fp, 0);
    fclose(fpTemp);

    fclose(fp);
    return 0;  //操作成功
 }

调用,我们设置了一个字符指针,字符有123456789,分别插入,再删除,最终等于对demo.txt文件什么也没做

#include <stdio.h>
#include <stdlib.h>
#include <math.h>
int main(){
    char *str ="123456789";
    //finsert("./demo.txt",3,str,9);
    fdelete("./demo.txt",3,9);
    return 0;
}
//编译执行,先插入,再删除,分别注释不同的代码进行测试
>gcc -g main.c -o main.exe
>./main
//demo.txt文件的变化
aaaaaaaaaa
aaa123456789aaaaaaa
aaaaaaaaaa

系统调用IO

什么是文件描述符
       文件描述符:File descriptor,简称fd,当应用程序请求内核打开/新建一个文件时(操作系统的核心叫内核,是一个独立的软件),内核会返回一个文件描述符用于对应这个打开/新建的文件,其fd本质上就是一个非负整数。
       实际上,fd是一个索引值,指向内核为每一个进程所维护的该进程打开文件的记录表。

       操作系统会为每一个进程维护了一个文件描述符表,该表的索引值都从从0开始的,所以在不同的进程中可以看到相同的文件描述符,这种情况下相同的文件描述符可能指向同一个文件,也可能指向不同的文件,具体情况需要具体分析,下面用一张简图就可以很容易的明白了:
在这里插入图片描述
当一个应用程序刚刚启动的时候,0是标准输入,1是标准输出,2是标准错误。
如果此时去打开一个新的文件,它的文件描述符会是3。
IEEE的可移植操作系统(POSIX)标准要求每次打开文件时必须使用当前进程中最小可用的文件描述符号
什么是inode:
       文件存储在硬盘上,硬盘的最小存储单位叫做“扇区”(Sector)。每个扇区储存512字节(相当于0.5KB)。

       操作系统读取硬盘的时候,不会一个个扇区的读取,这样效率太低,而是一次性连续读取多个扇区,即一次性读取一个“块”(block)。这种由多个扇区组成的“块”,是文件存取的最小单位。“块”的大小,最常见的是4KB,即连续八个sector组成一个block。

       文件数据都储存在“块”中,那么很显然,我们还必须找到一个地方储存文件的“元信息”,比如文件的创建者、文件的创建日期、文件的大小等等。这种储存文件元信息的区域就叫做inode,中文译名为"索引节点"。
       每一个文件都有对应的inode,里面包含了与该文件有关的一些信息。

       Unix/Linux系统内部不使用文件名,而使用inode号码来识别文件。对于系统来说,文件名只是inode号码便于识别的别称。

open()函数---对应标准IO的fopen

需要引入头文件:

#include <fcntl.h>
#include <sys/types.h>
#incldue <sys/stat.h>

功能:打开和创建文件(建立一个文件描述符,其他的函数可以通过文
件描述符对指定文件进行读取与写入的操作。)

原型

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

参数说明:

  1. pathname
    要打开或创建的目标文件
  2. flags,文件的状态标志
    打开文件时,可以传入多个参数选项,用下面的一个或者多个常量进行“或”运算,构成falgs常用参数:
    O_RDONLY: 只读打开
    O_WRONLY: 只写打开
    O_RDWR: 读,写打开
    上面三个常量,必须指定一个且只能指定一个。
    O_APPEND: 追加写,如果文件已经有内容,
    这次打开文件所写的数据附加到文件的末尾而不覆盖原来的内容。
    O_NONBLOCK、SYNC、O_ASYNC:这些和网络通信有关,学到后面在了解。
    O_CREAT: 若文件不存在,则创建它,需要使用mode选项,来指明新文件的访问权限。
    使用O_CREAT的时候需要配套第三个参数是设定该文件的权限:
    我们将权限用数字表示,其中 r 表示4,w表示2,x表示1,分别是2的0次方,1次方,2次方。
    那么我们可以这样理解:
    具有 rwx 权限的数字就是 7,具有 rw- 权限的数字是 6,具有 r-- 权限的数字是 4。
    我们常用的权限的数字模式又这几种:
    644:rw-r–r–
    755:rwxr-xr-x
    777:rwxrwxrwx
    返回值
    成功:新打开的文件描述符
    失败:-1

例子:

#include <stdio.h>
#include <fcntl.h>
#include <sys/types.h>
#include <sys/stat.h>

int main() {
    int filefd = 0;
    if ((filefd = open("./test.txt", O_CREAT | O_RDWR,0666)) == -1) {
        printf("++++++++++++++=open file fail\n");
        return -1;
    }
    return 0;
}

//在使用open()建立新文件时,该参数mode并非真正建立文件的权限,
//而是(mode&~umask)的权限值。
//例如,在建立文件时指定文件权限为0666,通常umask值默认为022,
//则该文件的真正权限则为0666&~022=0644,也就是rw-r-r--。

close()函数---对应标准IO的fclose

      close用于关闭一个已打开文件。
进程终止时,内核会自动关闭它所有的打开文件,应用程序经常利用这一点而不显式关闭文件。
需要引入头文件:

#include <unistd.h>

原型

int close(int fd);

返回值:
成功返回0,失败返回-1

例子:

#include<stdio.h>
#include<unistd.h>
#include<fcntl.h>

int main(){
        int fd=open("./test.txt",O_RDWR|O_CREAT,0666);
        if(fd<0){
                perror("open error");
                return -1;
        }
        printf("%d\n",fd);
        close(fd);
        return 0;
}

read()函数

   read用于从打开文件中读数据。
需要引入头文件:

#include <unistd.h>

原型

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

参数说明:
   fd:open返回的文件描述符-文件操作句柄,,通过fd指定要往哪个文件读数据
   buf:从文件中读取数据放到哪块缓冲区的首地址
   count:想要读取的数据长度,注意这个count不能大于缓冲区的大小
返回值:
   成功返回读到的字节数;若读到文件尾则返回0;失败返回-1

#include <stdio.h>
#include <stdlib.h>
#include <fcntl.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <unistd.h>

int main() {
    int filefd = 0;  //相当于标准i/o中的 FILE *fp,用于后面的文件操作
    if ((filefd = open("./test.txt", O_CREAT | O_RDWR,0666)) == -1) {
        printf("++++++++++++++=open file fail\n");
        return -1;
    }
     char buf[1024] = {0};     //设一个缓冲区,最好赋一个初值,以免出现不必要的异常
     //文件数据
     int ret = read(filefd,buf,1024);       //成功返回读到的字节数;若读到文件尾则返回0;失败返回-1
     if(ret <0){
        printf("读取异常!\n");
        exit(1);
     }
     printf("读取文件的数据大小和内容为: %d-\n%s\n",ret,buf);
     printf("文件描述号: %d\n",filefd);     //显示文件描述符
     close(filefd);                       //关闭文件
    return 0;
}

//编译与执行
[root@localhost os_io]# gcc -g read.c -o read
[root@localhost os_io]# ./read
读取文件的数据大小和内容为: 60-
xiongshaowen熊少文
xuhuifeng
xionglinzhou
xionglinxiao
文件描述号: 3

write()函数

      向文件写入数据。

#include <unistd.h>

//成功返回写入的字节数,失败返回-1

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

参数
   fd:open返回的文件描述符-文件操作句柄,,通过fd指定要往哪个文件写入数据
   buf:要写入文件的数据的空间首地址
   count:要写入的数据大小
   返回值:返回实际写入文件的数据字节长度,错误返回-1

#include <stdio.h>
#include <stdlib.h>
#include <fcntl.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <unistd.h>

int main() {
    int fd = 0,fdb=0;  //相当于标准i/o中的 FILE *fp,用于后面的文件操作
    if ((fd = open("./test.txt", O_CREAT | O_RDWR,0666)) == -1) {
        printf("++++++++++++++=open file fail\n");
        return -1;
    }
     char buf[1024] = {0};     //设一个缓冲区,最好赋一个初值,以免出现不必要的异常
     //读文件数据
     int ret = read(fd,buf,1024);       //成功返回读到的字节数;若读到文件尾则返回0;失败返回-1
     if(ret <0){
        printf("读取异常!\n");
        exit(1);
     }
      printf("读取文件的数据大小和内容为: %d-\n%s\n",ret,buf);

     //把buf中读到的数据保存到另一个文件(testb.txt)中。
     if((fdb=open("./testb.txt",O_RDWR | O_CREAT,0666)) == -1){
        printf("file testb.txt open fail!\n");
        return -1;
     }
     int retb = write(fdb,buf,strlen(buf));
     if(retb <0){
        printf("写数据异常!\n");
        exit(1);
     }

     printf("两文件描述号: %d %d\n",fd,fdb);     //显示文件描述符
     close(fd);                       //关闭文件
     close(fdb);
    return 0;
}

lseek()函数---标准IO中fseek函数
   lseek用于设置打开文件的偏移量。

#include <sys/types.h>
#include <unistd.h>

成功返回新的文件偏移量,失败返回-1

off_t lseek(int fd, off_t offset, int whence);

对offset的解释取决于whence的值:
   若whence == SEEK_SET,则将文件偏移量设为距文件开头offset个字节,此时offset必须为非负
   若whence == SEEK_CUR,则将文件偏移量设为当前值 + offset,此时offset可正可负
   若whence == SEEK_END,则将文件偏移量设为文件长度 + offset,此时offset可正可负
注意:
      lseek仅将新的文件偏移量记录在内核中,它并不引起任何IO操作,因此它不是系统调用IO,但该偏移量会用于下一次read/write操作。

dup()和dup2()函数

      两个函数都可以来复制一个现有的文件描述符,使多个文件描述符指向同一个文件,也可以把标准输入和输出换成某个文件的输入输出:

#include <unistd.h>
 int  dup(int fd);
 int dup2(int fd, int fd2);

   关于dup函数,当我们调用它的时候,dup会返回一个新的描述符,这个描述一定是当前可用文件描述符中的最小值。

      我们知道,一般的0,1,2描述符分别被标准输入、输出、错误占用,所以在程序中如果close(1)关掉标准输出,调用dup函数,此时返回的描述符就是1。

   对于dup2,可以用fd2指定新描述符的值,如果fd2本身已经打开了,则会先将其关闭。
返回fd2。如果fd刚好和fd2相等,就直接返回fd2,不关闭。

总之:这两个函数返回的描述符与fd描述符所指向的文件共享同一文件表项。

dup和dup2的例子:

#include <stdio.h>
#include <stdlib.h>
#include <fcntl.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <unistd.h>
int main() {
    int fd = 0;  //相当于标准i/o中的 FILE *fp,用于后面的文件操作
    if ((fd = open("./test.txt", O_CREAT | O_RDWR,0666)) == -1) {
        printf("++++++++++++++=open file fail\n");
        return -1;
    }

    if(write(fd,"hello 111111\n",13)!=13){  //写入之前如果文件存在,手动清除原内容,
        printf("写错误!");
        exit(1);
    }
    //int dup_fd = dup(fd); //或   
    int dup_fd = dup(fd,4);               //复制文件描述符,重新指定描述符,
    printf("fd:%d dup_fd:%d\n",fd,dup_fd);   //fd:3 dup_fd:4,此时,4和3是描述指向同一个文件
    if(write(4,"hello 222222\n",13)!=13){    //再往4描述符指向的文件中写入数据,是向test.txt写入的
        printf("写错误!");
        exit(1);
    }
   close(dup_fd); 
   close(fd);
    return 0;
}

在这里插入图片描述
      我们知道,一般0,1,2描述符分别被标准输入、输出、错误占用,我在如下代码中,我让fd指向1标准输出,即让test.txt代替了屏幕文件了。

#include <stdio.h>
#include <stdlib.h>
#include <fcntl.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <unistd.h>
int main() {
    int fd = 0;  //相当于标准i/o中的 FILE *fp,用于后面的文件操作
    if ((fd = open("./test.txt", O_CREAT | O_RDWR,0666)) == -1) {
        printf("++++++++++++++=open file fail\n");
        return -1;
    }

    if(write(fd,"hello 111111\n",13)!=13){  //写入之前如果文件存在,手动清除原内容,
        printf("写错误!");
        exit(1);
    }
   
    int dup_fd = dup2(fd,1);               //复制文件描述符,重新指定描述符,
    printf("fd:%d dup_fd:%d\n",fd,dup_fd);   //fd:3 dup_fd:1,此时,1和3是描述指向同一个文件,此时为1,即打印到文件test.txt中了,而不是屏幕上
    fflush(stdout);                   //刷新标准输出
    close(dup_fd);
    close(fd);
    return 0;
}

在这里插入图片描述

fcntl()和ioctl()函数

fcntl原型

#include <unistd.h>
#include <fcntl.h>
int fcntl(int fd, int cmd, ... /* arg */ );

功能
      fcntl函数其实是File Control的缩写,通过fcntl可以设置、或者修改已打开的文件性质。
参数
   fd:指向打开文件
   cmd:控制命令,通过指定不同的宏来修改fd所指向文件的性质。
F_DUPFD
      用来查找大于或等于参数 arg 的最小且仍未使用的文件描述符,并且复制参数 fd 的文件描述符。
      作用就和dup和dup2一样,虽然能实现一样的工程,但项目中推荐用dup和dup2哈!
      返回值:返回复制后的新文件描述

F_GETFL、F_SETFL
      取得/设置文件状态标志,通过第三个参数设置
可以更改的几个标志是:O_APPEND、O_NONBLOCK、SYNC、O_ASYNC
(O_RDONLY、O_WRONLY和O_RDWR不适用)

什么时候需要fcntl来补设?
      当文件描述符不是你自己open得到,而是调用别人给的函数,别人的函数去open某个文件,
      然后再将文件描述符返回给你用,在这种情况下,我们是没办法去修改被人的函数,
      在他调用的open函数里补加文件状态标志。用的时候,先获取,然后在设置!
F_GETFD、F_SETFD
F_GETOWN、F_SETOWN
F_GETLK 或 F_SETLK 或 F_SETLKW
      这些和信号、网络通信、文件锁有关系,学习了相关知识后在深入。
例:fcntl 模拟dup和dup2,但不建议用fcntl

#include <fcntl.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <unistd.h>
int main() {
    int fd = 0;  //相当于标准i/o中的 FILE *fp,用于后面的文件操作
    if ((fd = open("./test.txt", O_CREAT | O_RDWR,0666)) == -1) {
        printf("++++++++++++++=open file fail\n");
        return -1;
    }

    if(write(fd,"hello 111111\n",13)!=13){  //写入之前如果文件存在,手动清除原内容,
        printf("写错误!");
        exit(1);
    }
   
    //int dup_fd = dup2(fd,4);               //复制文件描述符,重新指定描述符,
    int dup_fd = fcntl(fd,F_DUPFD,4);        //fcntl具有dup2功能,
    printf("fd:%d dup_fd:%d\n",fd,dup_fd);   //fd:3 dup_fd:4,此时,4和3是描述指向同一个文件
    fflush(stdout);
    close(dup_fd);
    close(fd);
    return 0;
}
//编译执行
[root@localhost os_io]# gcc -g fcntl.c -o fcntl
[root@localhost os_io]# ./fcntl
fd:3 dup_fd:4

例:fcntl补设O_APPEND文件状态标志

#include <fcntl.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <unistd.h>
int main() {
    int fd = 0;  //相当于标准i/o中的 FILE *fp,用于后面的文件操作
    if ((fd = open("./test.txt", O_CREAT | O_RDWR,0666)) == -1) {
        printf("++++++++++++++=open file fail\n");
        return -1;
    }

    //用fcntl修改文件属性,原来我们是在打开文件中设置,这里我们用fcntl设置插入内容在文件尾属性
    int flag = 0;
    flag = fcntl(fd,F_GETFL,0);       //获取原有的标志
    flag = flag | O_APPEND;           //叠加,在原有的flag基础上,增加O_APPEND属性性质
    fcntl(fd,F_SETFL,flag);

    if(write(fd,"hello 111111\n",13)!=13){  //写入之前如果文件存在,手动清除原内容,
        printf("写错误!");
        exit(1);
    }
   
    
    printf("fd:%d\n",fd);   
    
   
    close(fd);
    return 0;
}
//编译执行
[root@localhost os_io]# gcc -g fcntl2.c -o fcntl
[root@localhost os_io]# ./fcntl
//会发现test.txt的文件尾部增加了内容,而不是原来的覆盖插入内容

ioctl()函数---io设备控制函数

 #include <unistd.h>
 #include <sys/ioctl.h>
 int ioctl(int fd, int cmd,)

函数功能:

  1. 控制I/O设备 ,提供了一种获得设备信息和向设备发送控制参数的手段。
  2. 用于向设备发控制和配置命令 ,这些数据是不能用read / write 读写的,要用ioctl()。

参数说明:

  1. @fd :就是用户程序打开设备时使用open函数返回的文件标示符,设备映射的文件。
  2. @cmd :就是用户程序对设备的控制命令,
  3. @… : 控制指令的参数,一般最多一个,有或没有是和cmd的意义相关的。

返回值:成功为0,出错为-1

例子:从键盘 IO 设备中,读取键盘的数据,下面代码要在linux中编译执行,输入设备,可以从/dev/input中查看(cd /dev/input —>ll)

#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <string.h>
#include <errno.h>
#include <stdlib.h>
#include <stdio.h>
#include <fcntl.h>
#include <sys/ioctl.h>
#include <linux/input.h>

int main(int argc, const char *argv[])
{
    int fd = -1;
    char name[256] = "Unknown";
    struct input_event event;   //事件源
    int ret =0;
    //打开一个硬件设备的文件,
    if((fd = open("/dev/input/event0",O_RDONLY)) <0){
        printf("open device file failed!");
        exit(1);
    }
    //发送指令,EVIOCGNAME 宏,获取IO设备的名字,存放在name中,event0是电源按钮设备文件,event1是键盘设备
    if(ioctl(fd,EVIOCGNAME(sizeof(name)),name)<0){
        printf("error!");
        exit(1);
    }

    printf("name : %s\n",name);
    close(fd);
    return 0;

}
//编译执行
honghao@ubuntu:/mnt/hgfs/workspaceC/os_io$ gcc -g ioctl.c -o ioctl
zhonghao@ubuntu:/mnt/hgfs/workspaceC/os_io$ sudo ./ioctl
name : Power Button

文件属性的相关操作函数

1、C语言stat()函数:获取文件属性

头文件:

#include <sys/stat.h>  
#include <unistd.h>

定义函数:

int stat(const char * file_name, struct stat *buf);

      函数说明:stat()用来将参数file_name 所指的文件属性, 复制到参数buf所指的结构中。

下面是struct stat 内各参数的说明:

struct stat
{
    dev_t st_dev; //包含文件的设备编号
    ino_t st_ino; //inode 文件的i-node
    mode_t st_mode; //protection 文件的类型和存取的权限 
    nlink_t st_nlink; //number of hard links 连到该文件的硬连接数目, 刚建立的文件值为1.
    uid_t st_uid; //user ID of owner 文件所有者的用户识别码
    gid_t st_gid; //group ID of owner 文件所有者的组识别码
    dev_t st_rdev; //device type 若此文件为装置设备文件, 则为其设备编号
   off_t st_size; //total size, in bytes 文件大小, 以字节计算
   unsigned long st_blksize; //blocksize for filesystem I/O 文件系统的I/O 缓冲区大小.
   unsigned long st_blocks; //number of blocks allocated 文件占用的扇区个数, 每一扇区大小为512 个字节.
   time_t st_atime; //time of lastaccess 文件最近一次被存取或被执行的时间,
   time_t st_mtime; //time of last modification 文件最后一次文件内容被修改的时间
   time_t st_ctime; //time of last change i-node 最近一次文件属性被更改的时间
};

      先前所描述的st_mode 则定义了下列数种情况:

S_IFMT 0170000 文件类型的位掩码 
//mode_t val;  val=(mystat.st_mode & ~S_IFMT);  (val & S_IRUSR) ? printf("r") : printf("-");
S_IFSOCK 0140000 scoket
S_IFLNK 0120000 符号连接
S_IFREG 0100000 一般文件
S_IFBLK 0060000 区块装置
S_IFDIR 0040000 目录
S_IFCHR 0020000 字符装置
S_ISVTX 01000 文件的粘连位,目录设置粘连才有意义:表示目录里的文件只能是文件所有者和root用户才能删除

S_IRUSR (S_IREAD) 00400 文件所有者具可读取权限
S_IWUSR (S_IWRITE)00200 文件所有者具可写入权限
S_IXUSR (S_IEXEC) 00100 文件所有者具可执行权限
S_IRGRP 00040 用户组具可读取权限
S_IWGRP 00020 用户组具可写入权限
S_IXGRP 00010 用户组具可执行权限
S_IROTH 00004 其他用户具可读取权限
S_IWOTH 00002 其他用户具可写入权限
S_IXOTH 00001 其他用户具可执行权限
     //上述的文件类型在 POSIX 中定义了检查这些类型的宏定义
S_ISLNK (st_mode) 判断是否为符号连接
S_ISREG (st_mode) 是否为一般文件
S_ISDIR (st_mode) 是否为目录
S_ISCHR (st_mode) 是否为字符装置文件
S_ISBLK (st_mode) 是否为先进先出
S_ISSOCK (st_mode) 是否为socket 

      返回值:执行成功则返回0,失败返回-1,错误代码存于errno。

范例

#include <stdio.h>
#include <sys/stat.h>
#include <unistd.h>

int main() {
    struct stat buf;
    stat("./file.txt", &buf);  //结构体变量与基本类型变量一样用取地址符取地址
    printf("st_ino: %ld\r\n", buf.st_ino);   //文件i-node节
    printf("st_size: %ld\rn\n",buf.st_size);  //文件的大小
    return 0;
}
//编译执行
>gcc -g filepro.c -o filep.exe
>./filep
0
12

在这里插入图片描述

2、C语言fstat()函数:获取文件属性

   头文件:

#include <sys/stat.h>  
#include <unistd.h>

   定义函数:

int fstat(int fildes, struct stat *buf);

      函数说明:fstat()用来将参数fildes 所指的文件状态, 复制到参数buf 所指的结构中(struct stat). Fstat()与stat()作用完全相同, 不同处在于传入的参数为已打开的文件描述符.

      返回值:执行成功则返回0, 失败返回-1, 错误代码存于errno.

范例

#include <fcntl.h>
#include <stdio.h>
#include <sys/stat.h>
#include <unistd.h>

int main() {
    struct stat buf;
    int fd;
    fd = open("./file.txt", O_RDONLY);
    fstat(fd, &buf);
    printf("file size +%ld\n", buf.st_size);
    return 0;
}

3、C语言link()函数:建立文件连接,相当于ln命令

   头文件:

#include <unistd.h>

   定义函数:

int link (const char * oldpath, const char * newpath);

      函数说明:link()以参数newpath 指定的名称来建立一个新的连接(硬连接)到参数oldpath 所指定的已存在文件.
      如果参数newpath 指定的名称为一已存在的文件则不会建立连接,类似于 windows 的快捷方式.linux中有硬软两种连接,硬连接不加-s参数。软连接加 -s参数,软硬连接不同在于:连接两个文件属性可能不同,但功能相同(读写修改),软连接更像windows快捷方式。

[root@localhost mnt]# pwd
/mnt
[root@localhost mnt]# touch xiong.txt
[root@localhost mnt]# ln xiong.txt /usr/xiong.txt   或软连 ln -s ....txt /usr/....txt
[root@localhost mnt]# cd /usr
[root@localhost usr]# ll
总用量 272
。。。。。。。
-rw-r--r--.   2 root root     0 53 09:18 xiong.txt
[root@localhost usr]# vim xiong.txt
    fsdfjsdlfksdlfksdlfsdkfs
ESC:wq!保存退出
[root@localhost usr]#cd /mnt
[root@localhost usr]#cat xiong.txt
    fsdfjsdlfksdlfksdlfsdkfs

注意:本c语言实验中,由于是windows与liunx共享文件夹(hgfs/workspaceC),所以在此文件夹下,无法建ln硬软连接
      返回值:成功则返回0, 失败返回-1, 错误原因存于errno.
      附加说明:link()所建立的硬连接无法跨越不同文件系统, 如果需要请改用symlink().

范例:

#include <unistd.h>
main()
{
    link("./file", "file1");
    return 0;
}

C语言symlink()函数:建立文件符号连接,相当于命令ln -s
   头文件:

#include <unistd.h>

   定义函数:

int symlink(const char * oldpath, const char * newpath);

      函数说明:symlink()以参数newpath 指定的名称来建立一个新的连接(符号连接)到参数oldpath 所指定的已存在文件. 参数oldpath 指定的文件不一定要存在, 如果参数newpath 指定的名称为一已存在的文件则不会建立连接.

   返回值:成功则返回0, 失败返回-1, 错误原因存于errno.

范例

#include <unistd.h>
#include <stdio.h>
#include <sys/stat.h>
//这个程序我是在window下写的,再复制放到linux中执行的,不是共享文件夹中(执行但没有产生效果)
main()
{
    //link("./file","./filelink");
    symlink("./file","./slink");
    return 0;
}

C语言lstat()函数:获取文件属性

头文件:

#include <sys/stat.h>   
#include <unistd.h>

定义函数:

int lstat (const char * file_name, struct stat * buf);

      函数说明:lstat()与stat()作用完全相同, 其差别在于, 当文件为符号连接时, lstat()会返回该link本身文件的属性。
   返回值:执行成功则返回0, 失败返回-1, 错误代码存于errno.

C语言readlink()函数:取得符号连接所指的文件

头文件:

#include <unistd.h>

定义函数:

int readlink(const char * path, char * buf, size_t bufsiz);

      函数说明:readlink()会将参数path 的符号连接内容存到参数buf 所指的内存空间, 返回的内容不是以NULL作字符串结尾, 但会将字符串的字符返回. 若参数bufsiz 小于符号连接的内容长度, 过长的内容会被截断.

      返回值:执行成功则传符号连接所指的文件路径字符串, 失败则返回-1, 错误代码存于errno.

#include <unistd.h>
#include <stdio.h>
#include <sys/stat.h>
//这个程序我是在window下写的,再复制放到linux中执行的,不是共享文件夹中(执行但没有产生效果)
main()
{
    //link("./file","./filelink");
    //symlink("./file","./slink");
    char buf[10]={0};
    readlink("./slink",,buf,10);   
    printf("readlink: %s\n",buf);
    return 0;
}
//编译执行
[root@localhost c]# gcc main.c -o main
[root@localhost c]# ./main

readlink: ./file 

C语言access()函数:判断是否具有存取文件的权限

头文件:

#include <unistd.h>

定义函数:

int access(const char * pathname, int mode);

      函数说明:access()会检查是否可以读/写某一已存在的文件。

参数mode 有几种情况组合:

1、R_OK, W_OK, X_OK和F_OK: R_OK, W_OK 与X_OK 用来检查文件是否具有读娶写入和执行的权限。
2、F_OK: 则是用来判断该文件是否存在。

      返回值:若所有欲查核的权限都通过了检查则返回0 值,表示成功,只要有一权限被禁止则返回-1。

范例:

#include <stdio.h>
#include <unistd.h>
int main(){
    int rs = access("./file",F_OK |R_OK |W_OK |X_OK);
    printf ("rs:  %d \n",rs);
    return 0;         //0表示,有打开,读取,写入,执行权限,-1表示没有权限

}
//编译执行
[root@localhost file]# gcc -g fileaccess.c -o access
[root@localhost file]# ./access
rs:  0 
/* 判断是否允许读取/etc/passwd */
#include <unistd.h>
int main()
{
    if(access("/etc/passwd", R_OK) == 0)
    printf("/etc/passwd can be read\n");
}

C语言chmod()函数:修改文件权限

头文件:

#include <sys/types.h>   
#include <sys/stat.h>

定义函数:

int chmod(const char * path, mode_t mode);

函数说明:chmod()会依参数mode 权限来更改参数path 指定文件的权限。

参数 mode 有下列数种组合:

S_ISUID 04000 文件的 (set user-id on execution)位
S_ISGID 02000 文件的 (set group-id on execution)位
S_ISVTX 01000 文件的sticky位

S_IRUSR (S_IREAD) 00400 文件所有者具可读取权限
S_IWUSR (S_IWRITE)00200 文件所有者具可写入权限
S_IXUSR (S_IEXEC) 00100 文件所有者具可执行权限
S_IRGRP 00040 用户组具可读取权限
S_IWGRP 00020 用户组具可写入权限
S_IXGRP 00010 用户组具可执行权限
S_IROTH 00004 其他用户具可读取权限
S_IWOTH 00002 其他用户具可写入权限
S_IXOTH 00001 其他用户具可执行权限

注:只有该文件的所有者或有效用户识别码为0,才可以修改该文件权限。

      基于系统安全,如果欲将数据写入一执行文件,而该执行文件具有S_ISUID 或S_ISGID 权限,则这两个位会被清除。如果一目录具有S_ISVTX 位权限,表示在此目录下只有该文件的所有者或root 可以删除该文件。

      返回值:权限改变成功返回0, 失败返回-1, 错误原因存于errno.

范例

/* 将当前目录下的file文件权限设成S_IRUSR|S_IWUSR|S_IRGRP*/
#include <sys/types.h>
#include <sys/stat.h>
#include <stdio.h>
#include <stdlib.h>
int main(){
    int rs=chmod("./file",S_IRUSR |S_IWUSR );
    printf("rs:  %d\n",rs);
    return 0;
}

在这里插入图片描述

C语言chown()函数:改变文件所有者和所属组

头文件:

#include <sys/types.h>   
#include <unistd.h>

定义函数:

int chown(const char * path, uid_t owner, gid_t group);

      函数说明:
         chown()会将参数path 指定文件的所有者变更为参数owner 代表的用户,而将该文件的组变更为参数group 组。如果参数owner 或group 为-1,对应的所有者或组不会有所改变。一般情况下root才有权限改名文件的所有者和所属组。
下图是linux shell中chown操作示例
在这里插入图片描述

      返回值:成功则返回0, 失败返回-1, 错误原因存于errno.

范例

/* 将./file 的所有者和组都设为g用户 */
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <unistd.h>
int main(){
    int rs = chown("./file",1002,1002);   //cat /etc/passwd中可看到g用户是1002号
    printf("rs: %d\n",rs);
    return 0;
}

C语言unlink()函数:删除文件

头文件:

#include <unistd.h>

定义函数:

int unlink(const char * pathname);

      函数说明:unlink()会删除参数pathname 指定的文件. 如果该文件名为最后连接点, 但有其他进程打开了此文件, 则在所有关于此文件的文件描述符皆关闭后才会删除. 如果参数pathname 为一符号连接, 则此连接会被删除。

      返回值:成功则返回0, 失败返回-1, 错误原因存于errno

#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
int main(){
    int rs = unlink("./slink");
    printf("rs:%d   \n",rs);
    return 0;
}
//编译执行

lrwxrwxrwx 1 root root     6 53 19:07 slink -> ./file
//这一行没有了
  • 18
    点赞
  • 23
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

qq_33406021

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值