C语言笔记(第n版):文件

        文件(file),是数据存储的一种形式,与我们在编程中的即时性数据不同,文件中的数据具有持久性。那我们可以将程序运行过程中生成的数据写入文件中,或者从文件中得到数据在编程中使用吗?当然可以,不过是将我们之前的控制台换成文件而已。

        而在这可转换的背后,是一种名为“流”(strem)之物。可以说这是一种很形象也很抽象的东西,它形象化数据成水流一样,从一端流向另一端,但是却很泛滥,因为有很多“流”。

图源: 十五:IO流_十五:io流 csdn-CSDN博客

        值得庆幸的是,在C中,我们可以用一种通用的接口去表示不同的流,而不用像Java一样那么细分。因为不管数据是字节还是字符,我们都可以统称为I/O流,在C中我们通过\textit{FIFE}类型的对象来表示,这些对象只能通过 \textit{FIFE *}类型的指针进行访问和操作。每个流都与一个外部物理设备(文件、标准输入流、打印机、串行端口等)相关联。

一、FIFE对象

        FILE是一个定义在标准库stdio.h中的一个结构体,它长得像这样

        有很多成员,但是相关文档貌似并没有特别指明这些成员的作用和含义,标准制定者的意思是希望开发者把其当作一个整体使用,所以为我们提供了一系列的函数。

        文档是这么说的

        Each FILE object denotes a C stream.

        C standard does not specify whether FILE is a complete object type. While it may be possible to copy a valid FILE, using a pointer to such a copy as an argument for an I/O function invokes unspecified behavior. In other words, FILE may be semantically non-copyable.

        每个FILE对象表示一个C流。 C标准没有指定FILE是否是完整的对象类型。虽然可以复制有效的FILE,但使用指向此类副本的指针作为I/O函数的参数会调用未指定的行为。换句话说,FILE在语义上可能是不可复制的。        

        尽管如此,在后面的文件操作中,我们仍然可以推测出这些成员的含义。

标准输入、输出和错误输出

        在C语言中,标准输入(stdin)、标准输出(stdout)和标准错误输出(stderr)是三个预定义的流对象,用于处理输入和输出。

        它们可以在<stdio.h>文件中找到

  • 标准输入(stdin)是程序从用户获取输入的流,它通常是键盘输入。可以使用scanf、getchar等函数从标准输入读取数据。
  • 标准输出(stdout)是程序输出结果的流,它通常是屏幕输出。可以使用printf、putchar等函数将数据输出到标准输出。
  • 标准错误输出(stderr)是程序输出错误信息的流,它也通常是屏幕输出。可以使用fprintf函数将错误信息输出到标准错误输出。

        这三个流对象在C语言中是预先定义的,因此程序可以直接使用它们进行输入和输出操作,而不需要先进行初始化或打开文件。

二、基本文件IO操作

(一)文件读写

        在C语言中,用于文件操作的一些常见函数包括:

C 文件API一览表

函数

描述

文件访问

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

打开一个文件,并返回一个文件指针,失败将是一个空指针。默认文件类型为文本文件,当未指明文件类型时,C编译系统按文本文件进行处理。

int fclose( FILE * fp )

文件操作结束后,应当关闭文件以释放所占内存缓冲区空间,把更新后的数据写回硬盘,否则文件可能会被误用,造成信息混乱或丢失。关闭成功返回值0,否则返回非零值;

读写字符

int fputc(int ch, FILE * fp)

把参数 c 的字符值写入到 fp 所指向的输出流中。如果写入成功,它会返回写入的字符,如果发生错误,则会返回 EOFputchar(char) == fputc(char, stdout)

int fgetc(FILE * fp)

从 fp 所指向的输入文件中读取一个字符。返回值是读取的字符,如果发生错误则返回 EOFgetchar() == fgetc(stdin)

读写字符串

char *fgets( char *buf, int n, FILE *fp )

从 fp 所指向的输入流中读取 n - 1 个字符。它会把读取的字符串复制到缓冲区 buf,并在最后追加一个 \0 字符来终止字符串。如果这个函数在读取最后一个字符之前就遇到一个换行符 '\n' 或文件的末尾 EOF,则只会返回读取到的字符,包括换行符。非正常结束时返回NULL值

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

字符串 s 写入到 fp 所指向的输出流中。如果写入成功,它会返回写入文件的字符个数,如果发生错误,则会返回NULL

文件成块读写

int fread(void * buffer, unsigned int size, unsigned int n, FILE * fp)

从文件中读取指定数量的数据块。从文件指针fp指向的文件中读取n个大小为size字节的数据项到以buffer为首地址的内存区域

int fwrite(const void * buffer, unsigned int size, unsigned int n, FILE * fp)

将指定数量的数据块写入文件。从以buffer为首地址的内存区域中读取n个大小为size字节的数据项到文件指针fp指向的文件

文件定位

int fseek(FILE *stream, long offset, int whence);

移动文件指针到指定位置。fseek 设置当前读写点到 offset 处, whence 可以是 SEEK_SET,SEEK_CUR,SEEK_END 这些值决定是从文件头、当前点和文件尾计算偏移量 offset。

long ftell(FILE * fp)

返回偏离文件头部的字节数,失败返回-1L

void rewind(FILE * fp)

设置文件位置为给定流 fp 的文件的开头

格式化读写

int fprintf(FILE * fp, char * format[,argument.......])

发送格式化输出到流  中。如果成功,则返回写入的字符总数,否则返回一个负数。

int fscanf(FILE * fp, char * format[,address.......])

从流  读取格式化输入。如果成功,该函数返回成功匹配和赋值的个数。如果到达文件末尾或发生读错误,则返回 EOF。

文件检测

int feof(FILE * stream)

检查文件是否结束,结束返回非零值,否则返回0;

int ferror(FILE *stream)

检测文件读写有无出错,未出错返回0

void clearerr(FILE *stream)

清除出错标志与文件结束标志,使它们为0值;

1、打开与关闭文件

        C的标准函数提供了打开和关闭文件的功能,其中主要使用的函数是fopenfclose

  1. fopen函数用于打开文件,它的原型如下:
FILE *fopen(const char *filename, const char *mode);

        其中,filename是一个字符串,表示要打开的文件名,mode是一个字符串,表示打开文件的模式。

        mode 说明:

模式

描述

r

打开一个已有的文本文件,允许读取文件。

w

打开一个文本文件,允许写入文件。如果文件不存在,则会创建一个新文件。在这里,的程序会从文件的开头写入内容。如果文件存在,则该会被截断为零长度,重新写入。

a

打开一个文本文件,以追加模式写入文件。如果文件不存在,则会创建一个新文件。在这里,的程序会在已有的文件内容中追加内容。

r+

打开一个文本文件,允许读写文件。

w+

打开一个文本文件,允许读写文件。如果文件已存在,则文件会被截断为零长度,如果文件不存在,则会创建一个新文件。

a+

打开一个文本文件,允许读写文件。如果文件不存在,则会创建一个新文件。读取会从文件的开头开始,写入则只能是追加模式。

        如果处理的是二进制文件,则需使用下面的访问模式来取代上面的访问模式:

"rb", "wb", "ab", "rb+", "r+b", "wb+", "w+b", "ab+", "a+b"

下表列出了在使用fopen函数打开二进制文件时允许的mode参数及其说明:

mode参数说明
"rb"以只读方式打开二进制文件,文件必须存在
"wb"以写入方式创建或打开二进制文件,清空文件内容,若文件存在则截断为0字节
"ab"以追加方式打开二进制文件,若文件不存在则创建,若文件存在则在文件末尾追加
"r+b"以读写方式打开二进制文件,文件必须存在,允许读和写操作,文件内容从文件开始读取或写入内容
"w+b"以读写方式创建或打开二进制文件,清空文件内容,若文件存在则截断为0字节,允许读和写操作
"a+b"以读写方式打开二进制文件,若文件不存在则创建,若文件存在则在文件末尾追加,允许读和写操作
"x+b"以读写方式创建或打开二进制文件,若文件已存在则函数失败

        注意:以上表格中的b表示所打开的文件为二进制文件。

  fopen函数返回一个指向FILE结构的指针,该结构描述了文件的属性和状态。如果打开文件成功,则返回指向文件的指针,否则返回NULL

  1. fclose函数用于关闭文件,它的原型如下:
int fclose(FILE *stream);

        其中,stream是一个指向要关闭文件的指针。fclose函数会关闭指定的文件,并在关闭成功时返回0,否则返回非零值。

示例代码:

#include <stdio.h>

int main() {
    FILE *file = fopen("test.txt", "w");
    
    if (file != NULL) {
        // 文件打开成功
        fprintf(file, "Hello, World!");
        fclose(file);
    } else {
        // 文件打开失败
        printf("Failed to open file.\n");
    }
    
    return 0;
}

 关于打开文件的一些问题

编码问题

        以下为一个测试的程序,为了避免是不是文件编码问题,这里标注了UTF-8的字符编码,这对于英文字符不太有影响(当然,读者也可以试试不添加),然后文件都是同一文件夹下

        文件都存在,但是有中文的会出现找不到

        原因在于,在C语言中使用fopen函数打开含有中文路径的文件,尤其是在Windows平台上,因为fopen默认使用的是ANSI编码,而中文字符在ANSI编码下可能无法正确解析。

        The fopen function opens the file specified by filename. By default, a narrow filename string is interpreted using the ANSI codepage (CP_ACP). 

        fopen 函数用于打开由文件名指定的文件。默认情况下,一个窄文件名字符串是使用 ANSI 代码页(CP_ACP)来解释的。

        在这种情况下使用另一个替换函数\textbf{ \_wfopen}\textbf{ \_wfopen}\textup{\textbf{fopen}} 的宽字符版本;\textbf{ \_wfopen}的参数是宽字符字符串,除此之外,\textbf{ \_wfopen}\textup{\textbf{fopen}}的行为完全相同。仅仅使用  \textbf{ \_wfopen}不会影响文件流中使用的编码字符集。

        除了更换函数,记得在字符串之前添加\textup{L},使其转换为宽字符格式  

         也可以

         如果用户输入中文文件路径,这里笔者在本人VScode终端是完成不了中文路径的输入(倘若有读者参考了之前笔者在多文件编译的批处理命令,可能也会出现这个问题),

        注:如果读者没有类似情况可以跳过下面这块说明

         但是,应当是可以的,在纯粹的命令行,这是可行的,可能是因为命令行GBK编码

        所以可以将VScode的输入终端改为GBK(一般默认),此时如果文件编码不是GBK,则应当也改为GBK,避免终端中文乱码,修改后应该可以正常

        

        下面为,针对之前本人多文件编译批处理命令的更新,有需要的读者可以参考 

@echo off > nul
cls
echo Begin to build...
rem 删除所有obj文件
del /Q /S %cd%\obj\*.o > nul && echo All obj files have been deleted.
rem 删除所有bin文件
del /Q /S %cd%\bin\*.exe > nul && echo All bin files have been deleted.
rem 进入到obj文件夹下      
cd /d  %cd%\obj
rem 编译所有工程文件             
gcc -c %cd%\..\util\*.c || echo "Compile all files failed." && exit /b 1
rem 编译主源文件          
gcc -c %cd%\..\main.c  || echo "Compile main file failed." && exit /b 1
rem 链接所有文件
gcc -o %cd%\..\bin\main.exe *.o || echo "Link all files failed." && exit /b 1
rem 返回主目录  
cd /d %cd%/.. 
rem 编码选择 GBK 936 UTF-8 65001 
chcp 936 > nul
cls
rem 提示信息
echo Main program has been built successfully.
echo Begin running...
echo Current execute path: %cd%
rem 执行程序                   
%cd%\bin\main.exe  
echo End running.
echo Press any key to exit...
pause > nul

        作为补充,单独创建一个文件在main.c同级如entry.c,写入以下内容

#include <windows.h>

int main(int argc, char const *argv[])
{
    system("D:/CsProjects/C/basic/build.cmd"); // 替换为您的路径
    return 0;
}

        单独运行,可以使程序在一个独立的CMD中运行 

         当然,如果不涉及中文文件路径,可能不需要这么麻烦,但是谁也不能保证。

 路径问题

        fopen 接受在执行文件系统上的有效的路径;fopen 接受 UNC 路径和涉及映射网络驱动器的路径,只要执行代码的系统在执行时能够访问共享或映射的驱动器。在为 fopen 构造路径时,请确保驱动器、路径或网络共享在执行环境中可用。在路径中,可以使用正斜杠(/)或反斜杠(\)作为目录分隔符。  

        此外

        The format of filename is implementation-defined, and does not necessarily refer to a file (e.g. it may be the console or another device accessible though filesystem API). On platforms that support them, filename may include absolute or relative filesystem path.

        文件名的格式由具体实现来定义,不一定指的是一个文件(例如,它可能是控制台或者通过文件系统 API 可访问的其他设备)。在支持这些的平台上,文件名可能包含绝对或相对的文件系统路径。

  文件编码问题

         fopen函数可以支持以什么编码格式打开文件,但是这并不是C标准,笔者没有成功实现。以下为参考fopen, _wfopen | Microsoft Learn

       fopen支持Unicode文件流,只需在mode参数中传递 ccs=encoding指定所需编码的标志,如下。

        FILE *fp = fopen("newfile.txt", "rt+, ccs=UTF-8");

        允许的值为 UNICODEUTF-8, and UTF-16LE.

        

        当文件以Unicode模式打开时,输入函数会将从文件中读取的数据转换为以wchar_t类型存储的UTF-16数据。向以Unicode模式打开的文件写入数据的函数则期望包含以wchar_t类型存储的UTF-16数据的缓冲区。如果文件是以UTF-8编码的,则在写入时UTF-16数据会被转换为UTF-8,而在读取时,文件的UTF-8编码内容会被转换为UTF-16。在Unicode模式下尝试读取或写入奇数个字节会导致参数验证错误。若要在程序中读取或写入以UTF-8存储的数据,应使用文本或二进制文件模式而非Unicode模式,并且你需要负责任何必要的编码转换。

        如果文件已存在且是以读取或追加模式打开的,则文件中的任何字节顺序标记(BOM)将决定其编码。BOM编码优先于通过ccs标志指定的编码。ccs编码仅在不存在BOM或文件是新文件时才使用。

注意

        BOM 检测仅适用于以 Unicode 模式(即通过传递 ccs 标志)打开的文件。         

2、读写字符

fgetc 和 getc

  • 功能:从文件流中读取一个字符。
  • 区别fgetc 是一个标准C库函数,而 getc 是 fgetc 的宏实现,可能在某些编译器中通过宏展开来优化性能。两者在功能上是等价的,但 getc 可能会有额外的副作用(比如修改宏参数)。
  • 原型
    int fgetc(FILE *stream); 
    int getc(FILE *stream);

    stream\textbf{stdin}相当于\textup{\textbf{getchar}}

  • 返回值:成功时返回读取的字符(以无符号字符形式转换为int),遇到文件结束或错误时返回EOF。

fputc 和 putc

  • 功能:向文件流中写入一个字符。
  • 区别:类似于 fgetc 和 getcfputc 是标准函数,而 putc 是其宏实现。
  • 原型
    int fputc(int char, FILE *stream); 
    int putc(int char, FILE *stream);

    stream\textbf{stdout}相当于\textup{\textbf{putchar}}

  • 返回值:成功时返回写入的字符,失败时返回EOF。

ungetc

  • 功能:将一个字符退回到文件流中,使其在下一次读取时重新可用。
  • 原型
    int ungetc(int c, FILE *stream);

    ​​​​​​​返回值:成功时返回c,失败时返回EOF。

  • 注意:并非所有文件流都支持 ungetc,且退回到文件流的字符通常只能被退回一次。        
#include <stdio.h>

int main() {
    FILE *file = fopen("example.txt", "r"); // 打开文件
    if (file == NULL) {
        // 如果文件打开失败,则退出程序
        perror("Failed to open the file");
        return -1;
    }

    int ch;
    while ((ch = fgetc(file)) != EOF) { // 读取字符直到文件末尾
        printf("%c\n", ch); // 打印字符
    }

    fclose(file); // 关闭文件
    return 0;
}

        在这个示例中,首先使用fopen打开一个名为"example.txt"的文件。然后使用fgetc不断地读取文件中的字符,直到达到文件末尾(此时fgetc会返回EOF)。每个读取到的字符都会被打印出来。最后,使用fclose关闭文件。

#include <stdio.h>

int main() {
    FILE *file = fopen("output.txt", "w"); // 打开文件准备写入
    if (file == NULL) {
        // 如果文件打开失败,则退出程序
        perror("Failed to open the file for writing");
        return -1;
    }

    fputc('H', file); // 写入字符 'H'
    fputc('E', file); // 写入字符 'E'
    fputc('L', file); // 写入字符 'L'
    fputc('O', file); // 写入字符 'O'
    fputc('\n', file); // 写入换行符

    fclose(file); // 关闭文件
    return 0;
}

        在这个示例中,首先使用fopen打开一个名为"output.txt"的文件准备写入。然后使用fputc将一系列字符和一个换行符写入到文件中。最后,使用fclose关闭文件。

        值得注意的是,这不能读写中文字符,因为中文字符的一个字往往不是一个字符表示的,这时候就要用宽字符的函数,如下,它们包含在<wchat.h>中,另外需要注意的是,文件的编码和输出的终端应该匹配,避免乱码。以下使用的都是GBK,文件为文本文件,编码为ANSI

     \textbf{ wint\_t fgetwc( FILE *stream );} (since \ C95) \\ \textbf{ wint\_t getwc( FILE *stream );}

#include<stdio.h>
#include <locale.h>
#include <wchar.h>

int main(int argc, char const *argv[])
{


    setlocale(LC_ALL, "zh_CN.GBK"); // 设置语言环境为GBK
    FILE *fp = fopen("./data/test.txt", "r");
    if (fp == NULL){
        printf("Error: Unable to open file \n");
        return 1;
    }else{
        printf("File opened successfully\n");
    }

    wint_t ch;
    while ((ch = fgetwc(fp)) != WEOF) { // 读取字符直到文件末尾
        putwchar(ch);; // 打印字符
    }

    fclose(fp); // 关闭文件
    
    return 0;
}

3、读写字符串

fgets

  • 功能:从指定的文件流中读取一行文本,直到遇到换行符('\n')、文件结束符EOF或已读取了n-1个字符为止(其中n是第二个参数指定的最大字符数,包括最后的空字符'\0')。
  • 原型
    char *fgets(char *str, int n, FILE *stream);

    \textbf{stream}\textbf{stdin}时和\textbf{gets\_s}差不多

  • 参数
    • str:指向用于存储读取数据的字符数组的指针。
    • n:要读取的最大字符数(包括最后的空字符'\0')。
    • stream:指向FILE对象的指针,该对象标识了要从中读取数据的输入流。
  • 返回值:成功时,返回指向str的指针;如果发生错误或到达文件末尾而没有读取任何字符,则返回NULL

fputs

  • 功能:将一个字符串写入到指定的文件流中,但不包括空字符('\0')。
  • 原型
    int fputs(const char *str, FILE *stream);

    \textbf{stream}\textbf{stdout}时和\textbf{puts}差不多,\textbf{puts}也不会终止符写入标准输出,但是会添加换行符。

  • 参数
    • str:指向要写入文件的空终止字符串的指针。
    • stream:指向FILE对象的指针,该对象标识了要写入数据的输出流。
  • 返回值:成功时,返回非负值;失败时,返回EOF
#include <stdio.h>  
#include <stdlib.h>  
  
int main() {  
    FILE *file;  
    char filename[] = "example.txt";  
    char buffer[100];  
  
    // 打开文件以写入。如果文件不存在,将创建它。  
    file = fopen(filename, "w");  
    if (file == NULL) {  
        perror("Error opening file");  
        return 1;  
    }  
  
    // 使用fputs写入字符串到文件  
    fputs("Hello, World!\n", file);  
    fputs("This is a test.\n", file);  
  
    // 关闭文件  
    fclose(file);  
  
    // 重新打开文件以读取  
    file = fopen(filename, "r");  
    if (file == NULL) {  
        perror("Error opening file");  
        return 1;  
    }  
  
    // 使用fgets从文件中读取字符串  
    while (fgets(buffer, sizeof(buffer), file) != NULL) {  
        printf("%s", buffer);  
    }  
  
    // 关闭文件  
    fclose(file);  
  
    return 0;  
}

        在这个示例中,我们首先使用fopen函数以写入模式("w")打开一个名为example.txt的文件。如果文件不存在,fopen会创建它。然后,我们使用fputs函数将两个字符串写入文件。

        在写入字符串后,我们关闭文件,然后重新以读取模式("r")打开它。接下来,我们使用fgets函数从文件中读取字符串,直到文件的末尾。每次调用fgets时,它都会读取最多sizeof(buffer)-1个字符(为末尾的null字符留出空间)并存储在buffer中。当fgets到达文件末尾或发生错误时,它会返回NULL。

        最后,我们打印出从文件中读取的每个字符串,并关闭文件。

        正常情况下,使用此对函数读写中文没有问题,注意编码统一。

4、成块读写

        首先,我们使用 fwrite 将一些数据写入一个文件。

#include <stdio.h>  
  
int main() {  
    FILE *file = fopen("example.bin", "wb"); // 打开文件以写入二进制数据  
    if (file == NULL) {  
        perror("Error opening file");  
        return 1;  
    }  
  
    int data[] = {1, 2, 3, 4, 5}; // 要写入的数据  
    size_t items = sizeof(data) / sizeof(data[0]); // 数据项的数量  
    size_t size = sizeof(data[0]); // 每个数据项的大小  
  
    // 使用fwrite写入数据  
    size_t written = fwrite(data, size, items, file);  
    if (written != items) {  
        perror("Error writing to file");  
        fclose(file);  
        return 1;  
    }  
  
    fclose(file); // 关闭文件  
    return 0;  
}

        接下来,我们使用 fread 从刚才写入的文件中读取数据。

#include <stdio.h>  
  
int main() {  
    FILE *file = fopen("example.bin", "rb"); // 打开文件以读取二进制数据  
    if (file == NULL) {  
        perror("Error opening file");  
        return 1;  
    }  
  
    int data[5]; // 用于存储读取的数据的数组  
    size_t items = sizeof(data) / sizeof(data[0]); // 数据项的数量  
    size_t size = sizeof(data[0]); // 每个数据项的大小  
  
    // 使用fread读取数据  
    size_t read = fread(data, size, items, file);  
    if (read != items) {  
        perror("Error reading from file");  
        fclose(file);  
        return 1;  
    }  
  
    // 打印读取的数据  
    for (size_t i = 0; i < items; ++i) {  
        printf("%d ", data[i]);  
    }  
    printf("\n");  
  
    fclose(file); // 关闭文件  
    return 0;  
}

5、文件定位

        在C语言中,文件定位函数(File Positioning Functions)用于操作文件指针的位置,即控制读写操作在文件中的位置。

C语言提供了以下几个文件定位函数:

  1. long ftell(FILE *stream):获取文件指针的当前位置。该函数返回一个long类型的值,表示文件指针在文件中的偏移量(以字节为单位)。如果函数执行成功,返回的值大于等于0;如果函数执行失败,返回-1。

  2. int fseek(FILE *stream, long offset, int origin):将文件指针定位到指定位置。offset表示相对于origin的偏移量,该偏移量可以是正数或负数。origin表示相对于哪个位置进行偏移,可以取以下三个值:

    • SEEK_SET:从文件开头开始偏移。
    • SEEK_CUR:从当前位置开始偏移。
    • SEEK_END:从文件末尾开始偏移。 若函数执行成功(返回值为0),则文件指针将被移动到指定位置;若执行失败(返回值为非0),文件指针的位置不变。
  3. void rewind(FILE *stream):将文件指针重新定位到文件开头。该函数相当于fseek(stream, 0L, SEEK_SET)。它无返回值。

        这些文件定位函数可以用于对文件进行随机访问,即可以直接定位到文件中任意位置进行读写操作。文件定位函数在处理大型文件、数据库文件等场景中非常有用。

#include <stdio.h>  
  
int main() {  
    FILE *file;  
    long position;  
    char buffer[100];  
  
    // 打开一个文件,假设它存在且可读  
    file = fopen("example.txt", "r");  
    if (file == NULL) {  
        perror("Error opening file");  
        return 1;  
    }  
  
    // 使用ftell获取并打印当前文件位置  
    printf("Initial file position: %ld\n", ftell(file));  
  
    // 读取一些数据,然后再次使用ftell  
    fgets(buffer, sizeof(buffer), file);  
    printf("After reading: %s\n", buffer);  
    printf("File position after reading: %ld\n", ftell(file));  
  
    // 使用fseek移动到文件的某个位置  
    if (fseek(file, 10, SEEK_SET) != 0) {  
        perror("Error seeking in file");  
        fclose(file);  
        return 1;  
    }  
    printf("File position after fseek: %ld\n", ftell(file));  
  
    // 使用rewind将文件位置重置为文件开头  
    rewind(file);  
    printf("File position after rewind: %ld\n", ftell(file));  
  
    // 关闭文件  
    fclose(file);  
  
    return 0;  
}

        在Windows系统中这可以间接查看一个文件的大小,如

        只需将文件指针移到最后

        

除了上面,比较常见的文件定位函数,C还提供了两个具有差不多功能的函数,如下:

  1. fgetpos函数:

    • 函数原型:int fgetpos(FILE *stream, fpos_t *pos)
    • 功能:获取文件位置指针的当前位置,并将其保存在给定的fpos_t类型的变量中。
    • 参数:
      • stream: 文件指针,指向要获取位置的文件流。
      • pos: fpos_t类型的变量指针,用于保存文件位置指针的当前位置。
    • 返回值:如果成功,返回0;如果失败,返回非零值。
  2. fsetpos函数:

    • 函数原型:int fsetpos(FILE *stream, const fpos_t *pos)
    • 功能:设置文件位置指针的位置,使其指向给定的fpos_t类型变量中保存的位置。
    • 参数:
      • stream: 文件指针,指向要设置位置的文件流。
      • pos: fpos_t类型的变量指针,指定文件位置指针要设置的位置。
    • 返回值:如果成功,返回0;如果失败,返回非零值。

        

6、格式化读写

        以下是一个简单的示例,演示如何使用 fprintf 将一些文本写入文件:

#include <stdio.h>  
  
int main() {  
    FILE *file;  
    file = fopen("example.txt", "w"); // 打开文件以写入数据  
    if (file == NULL) {  
        printf("无法打开文件\n");  
        return 1;  
    }  
  
    fprintf(file, "Hello, World!\n"); // 写入字符串  
    fprintf(file, "整数:%d\n", 123); // 写入整数  
    fprintf(file, "浮点数:%f\n", 3.14); // 写入浮点数  
  
    fclose(file); // 关闭文件  
    return 0;  
}

        以下是一个简单的示例,演示如何使用 fscanf 从文件中读取数据:

#include <stdio.h>  
  
int main() {  
    FILE *file;  
    char str[100];  
    int num;  
    float floatNum;  
  
    file = fopen("example.txt", "r"); // 打开文件以读取数据  
    if (file == NULL) {  
        printf("无法打开文件\n");  
        return 1;  
    }  
  
    fscanf(file, "%s", str); // 读取字符串  
    printf("读取的字符串:%s\n", str);  
  
    fscanf(file, "整数:%d", &num); // 读取整数  
    printf("读取的整数:%d\n", num);  
  
    fscanf(file, "浮点数:%f", &floatNum); // 读取浮点数  
    printf("读取的浮点数:%f\n", floatNum);  
  
    fclose(file); // 关闭文件  
    return 0;  
}

        这个示例将打开之前使用 fprintf 写入的 example.txt 文件,并使用 fscanf 从中读取数据。注意,我们使用 fopen 函数以读取模式("r")打开文件,并在完成读取后使用 fclose 关闭文件。在调用 fscanf 时,我们需要传递变量的地址(使用 & 运算符)来存储从文件中读取的数据。

        

7、错误处理

        在C语言中,处理文件输入/输出(I/O)时,经常会遇到需要检测和处理错误或文件结束(EOF)的情况。clearerrfeofferror 和 perror 是标准I/O库中用于这些目的的重要函数。

1. clearerr 函数

功能:清除与文件流相关的错误指示符和文件结束指示符。

原型void clearerr(FILE *stream);

  • 参数stream 是一个指向 FILE 对象的指针,该对象标识了一个打开的文件流。
  • 作用:当文件流发生错误(如读写错误)或达到文件末尾(EOF)时,相应的错误指示符和文件结束指示符会被设置。clearerr 函数会清除这些指示符,使得文件流再次处于“干净”状态,可以继续进行读写操作。
  • 返回值:无返回值。

2. feof 函数

功能:检查文件流是否已到达文件末尾(EOF)。

原型int feof(FILE *stream);

  • 参数stream 是一个指向 FILE 对象的指针,该对象标识了一个打开的文件流。
  • 返回值:如果文件流已经到达EOF,则返回非零值(通常为1);否则返回0。
  • 注意feof 只有在尝试读取操作之后才能准确地指示是否已到达文件末尾。仅因为 feof 返回0,并不意味着接下来读取操作会成功。

3. ferror 函数

功能:检查文件流是否发生了错误。

原型int ferror(FILE *stream);

  • 参数stream 是一个指向 FILE 对象的指针,该对象标识了一个打开的文件流。
  • 返回值:如果文件流在最近的I/O操作中发生了错误,则返回非零值(通常为1);否则返回0。
  • 用途:在尝试读写文件后,可以使用 ferror 来检查操作是否成功。如果返回非零值,则说明发生了错误,可能需要清除错误状态(使用 clearerr),或者根据具体情况采取其他措施。

4. perror 函数

功能:打印最后发生的系统错误的描述到标准错误(stderr)。

原型void perror(const char *s);

  • 参数s 是一个指向以空字符终止的字符串的指针,该字符串将被输出到标准错误流之前,通常用于描述发生错误的上下文。
  • 作用perror 函数首先输出传递给它的字符串(如果非NULL),后跟一个冒号、一个空格和最后发生的系统错误的文本描述。这个描述通常来自于全局变量 errno,该变量在出错时被设置。
  • 用途:当程序遇到系统级错误(如文件打开失败)时,perror 可以用来向用户报告错误的具体原因。
#include <stdio.h>

int main() {
    FILE *file;
    int ch;
    file = fopen("example.txt", "r");  
    if (file == NULL) {  
        perror("无法打开文件");  
        return 1;  
    }  

    // 循环读取文件内容,直到文件结束或发生错误  
    while ((ch = fgetc(file)) != EOF) {  
        putchar(ch);  
        if (feof(file)) {  
            printf("\n文件结束\n");  
            break;  
        }  
    }  

    fclose(file);  
    return 0;
}

        在这个例子中,我们使用 fgetc 函数从文件中逐个字符地读取内容,并在每次读取后检查 feof。当 feof 返回非零值时,我们知道已经到达了文件的末尾,于是打印出“文件结束”并退出循环。

#include <stdio.h>

int main() {
    FILE *file;
    int ch;
    file = fopen("nonexistent.txt", "r");  
    if (file == NULL) {  
        perror("无法打开文件");  
        return 1;  
    }  

    // 尝试读取文件内容  
    ch = fgetc(file);  
    if (ferror(file)) {  
        printf("读取文件时发生错误\n");  
        clearerr(file); // 清除错误标志  
    } else {  
        printf("读取的字符是:%c\n", ch);  
    }  

    fclose(file);  
    return 0;
}

        在这个例子中,我们尝试打开一个不存在的文件。由于文件不存在,fgetc 调用将失败,并且 ferror 将返回非零值。我们检测到错误后,使用 clearerr 函数清除错误标志,以便进行后续的文件操作。

(二) IO缓冲

        在C语言中,缓冲区是内存中的一块区域,用于临时存储数据,以减少对底层硬件(如磁盘或网络)的直接访问次数,从而提高程序的效率。

图源: Linux文件系统(IO缓冲区+磁盘+软硬链接)_linux文件系统 缓存 map-CSDN博客

        

        对于文件I/O操作,C标准库提供了三种类型的缓冲区:无缓冲(unbuffered)、行缓冲(line buffered)和全缓冲(fully buffered)。

  • 无缓冲(Unbuffered):无缓冲意味着每次对文件或流的读写操作都会直接作用于底层硬件,不经过缓冲区。这通常用于需要即时反馈的情况,比如错误信息的输出(在某些系统上,stderr是默认无缓冲的)。
  • 行缓冲(Line Buffered):行缓冲意味着输入/输出数据在遇到换行符(如'\n')或缓冲区满时会进行实际的I/O操作。标准输出(stdout)在大多数系统上默认是行缓冲的(当输出到终端时),这有助于在程序运行时即时看到输出结果。
  • 全缓冲(Fully Buffered):全缓冲意味着只有在缓冲区满或显式刷新缓冲区时(如使用fflush函数),才会进行实际的I/O操作。对于磁盘文件,标准库通常会使用全缓冲,以提高磁盘I/O的效率。

1、设置缓冲区

        这些缓冲机制主要通过stdio.h库中的函数如setvbufsetbuf等来设置,但默认情况下,标准输入(stdin)、标准输出(stdout)和标准错误(stderr)的缓冲类型可能依赖于具体的系统和库实现。

        这两个函数要用到三个宏用于设置缓冲的模式

setbuf函数
void setbuf(FILE *stream, char *buf);
  • stream: 指向要设置缓冲的文件流的指针。
  • buf: 如果不为NULL,则指向一个缓冲区,其大小至少为BUFSIZ(在stdio.h中定义)字节。如果为NULL,则文件流被设置为无缓冲模式。

注意:

  • 一旦文件流被使用(即进行了读/写操作),就不能再调用setbuf来改变其缓冲模式。
  • 并非所有系统都支持无缓冲的文件流,特别是在标准输入和标准输出上。

        如果 BUFSIZ 不是合适的缓冲区大小,可以使用 setvbuf 来更改它。setvbuf 还应当用于检测错误,因为 setbuf 不会表明成功或失败。此函数只能在流与打开的文件相关联之后,但在任何其他操作(除了 setbuf/setvbuf 的失败调用)之前使用。一个常见的错误是将 stdin 或 stdout 的缓冲区设置为一个生存期在程序终止前结束的数组。

setvbuf函数
int setvbuf(FILE *stream, char *buf, int mode, size_t size);
  • stream: 指向要设置缓冲的文件流的指针。
  • buf: 如果不为NULL,则指向一个用户提供的缓冲区。如果为NULL,则库会尝试分配一个大小为\textbf{BUFSIZE}的缓冲区。
  • mode: 指定缓冲模式,可以是_IOFBF(全缓冲)、_IOLBF(行缓冲)或_IONBF(无缓冲)。
  • size: 缓冲区的大小,如果buf非NULL,则忽略此值,否则此值决定了分配的缓冲区的大小。

返回值:

  • 成功时返回0;失败时返回非0值,并设置errno以指示错误。
#include <stdio.h>

int main() {
    FILE *fp = fopen("test.txt", "w");
    if (fp == NULL) {
        printf("Failed to open file\n");
        return 1;
    }

    char buffer[BUFSIZ];

    if (setvbuf(fp, buffer, _IOFBF, BUFSIZ) != 0) {
        printf("Failed to set buffer\n");
        return 1;
    }

    fputs("Hello, World!", fp);

    fclose(fp);

    return 0;
}
 

fflush函数
int fflush(FILE *stream);
  • stream: 指向要清空输出缓冲区的文件流的指针。如果为NULL,则清空所有输出流的缓冲区。

返回值:

  • 成功时返回0;失败时返回EOF,并设置errno以指示错误。
#include <stdio.h>  
  
int main() {  
    char buffer[1024];  
    FILE *fp = fopen("example.txt", "w");  
    if (fp == NULL) {  
        perror("Failed to open file");  
        return 1;  
    }  
    setvbuf(fp, buffer, _IOFBF, sizeof(buffer)); // 将fp设置为全缓冲,使用自定义缓冲区  
    fputs("This will be written when the buffer is full or flushed.\n", fp);  
    fflush(fp); // 显式刷新缓冲区  
    fclose(fp);  
    return 0;  
}

二、IO重定向

        前面我们知道如何文件与程序之间数据传递,也知道程序和控制台之间如何数据传递,现在我们试试能不能从控制台到文件直接的IO流传输,因为文件和控制台都可以通过\textup{\textbf{FILE}}指针表示。

 (一)freopen

        freopen函数是一个用于文件重定向的函数。它可以重新定向一个标准的输入输出流到一个指定的文件。

函数原型:

FILE *freopen(const char *filename, const char *mode, FILE *stream);

参数说明:

  • filename:要打开的文件名,可以是相对路径或绝对路径。
  • mode:文件打开模式,包括"r"(只读)、"w"(只写)、"a"(追加)、"rb"(二进制读)、"wb"(二进制写)等。
  • stream:要重定向的流,可以是stdin(标准输入流)、stdout(标准输出流)、stderr(标准错误流)等。

返回值:

  • 成功:返回一个指向FILE结构的指针,指向已打开的文件。
  • 失败:返回NULL,并设置errno错误码。

函数作用:

  • 通过调用freopen函数,可以将输入输出流重定向到指定文件,从而实现文件输入输出操作。
  • 通过重定向标准输入输出流,可以实现文件的读取和写入。

使用示例:

#include <stdio.h>

int main() {
    FILE *fp;
    char ch;

    // 打开文件并将标准输入流重定向到该文件
    fp = freopen("input.txt", "r", stdin);
    if (fp == NULL) {
        printf("Failed to open file.\n");
        return 1;
    }

    // 从文件中读取字符,并输出到标准输出流
    while ((ch = getchar()) != EOF) {
        putchar(ch);
    }

    // 关闭文件
    fclose(fp);

    return 0;
}

        由于标准错误是不会通过标准输出显式,所以可以通过重定向将其重定向进一个文件中

三、非文本文件的操作

        以上都是对普通的字符文件的操作,换句话说,以上的操作对于能使用普通的记事本打开的文件格式基本都OK,接下来对其它的文件进行尝试,即不能简单使用记事本打开的文件。这些文件通常由一些专门的处理软件打开,在其二进制形式中包含大量的一些元数据,处理这些文件往往需要知道其文件的格式。

(一)文字文件

        在一个docx文档里面写一点文字,你就会发现,它的大小就有右图这么多,并且如果简单的从文件的二进制形式看,也找不到这些文字

        但如果将其文件后缀改了,使其变成一个压缩文件,就会发现别有洞天 

         

        而我们的内容,可以在一个xml里面找到

           那像如果要操作这样的文件,可以想到的一点就是,通过\textbf{rename}函数使其变成一个压缩文件,通过比如\textup{system}函数调用解压缩程序解压,之后的事情就好办多了。

(二)图片

        如果是简单的复制之类的,像上面那样的文件操作完全就可以,但是图片的信息能不能通过C获取,或者C能不能通过修改二进制数据改变图片的呈现效果。

        这里以一些简单的图片,比如位图BMP(Bitmap)为例,来操作图片。它由微软公司为其Windows操作系统开发。BMP图像文件几乎不进行压缩,因此它们通常比其他格式的图像文件要大。也因此,我们更容易得到图片的相关信息。

        BMP文件的格式,可以参考:BMP格式详解_bmp头部信息-CSDN博客。也可以查考更官方的文档BMP file format - Wikipedia

        BMP文件的格式说简单也需要一些功夫,特别是数据部分。元数据比较好获取,不过要注意数据通常是小端存储。

        这里使用VScode的插件Hex Editor打开的一张位图如下,其大小存储在第2到第4字节,所以要像下面一样反过来看。

        我们可以设计一个结构体表示其头部的数据,比如:

typedef struct{

    unsigned char type[2] ;                 // 文件类型,2字节
    unsigned char size[4];                  // 文件大小,4个字节
    unsigned char reserved1[2];             // 2字节,保留,必须为 0
    unsigned char reserved2[2];             // 2字节,保留,必须为 0
    unsigned char offset[4];                // 从文件头到实际图像数据的偏移量,4个字节
    unsigned char informationSize[4];       // 信息头的大小,4个字节
    unsigned char width[4];                 // 图像的宽度,4个字节
    unsigned char height[4];                // 图像的高度,4个字节
    unsigned char planes[2];                 // 颜色平面数,2个字节
    unsigned char bitPerPixel[2];           // 每个像素的位数,2个字节
    unsigned char compression[4];            // 压缩类型,4个字节
    unsigned char imageSize[4];              // 图像数据大小,4个字节
    unsigned char xPixelsPerMeter[4];       // 水平分辨率,4个字节
    unsigned char yPixelsPerMeter[4];       // 垂直分辨率,4个字节
    unsigned char colorsUsed[4];            // 使用的颜色数,4个字节
    unsigned char importantColors[4];        // 重要的颜色数,4个字节
  

}__attribute__((packed)) BMP, * BMP_PTR;

         然后通过指针转换可以更方便得到对应的数据区,得到的数据还需要经过转换为实际的值

char buffer[sizeof(BMP)] = {0};

BMP_PTR bmp = (BMP_PTR)buffer;

fread(buffer, sizeof(BMP), 1, fp);

printf("BMP file type: %c%c\n", bmp->type[0], bmp->type[1]);

printf("BMP file size: %f Mb\n", endianSwap(&bmp->size, sizeof(bmp->size)) / 1024.0 / 1024 );

free(data);
fclose(fp);

         一个小端转换的参考代码:

unsigned long endianSwap(void* ptr, int size){

    // 将指针转换为 unsigned char* 类型,方便单字节操作
    unsigned char *buffer = (unsigned char*)ptr;  
    unsigned long result = 0;
    short isBegin = 0;
    for(int i = 0; i < size; i++){
        if(!isBegin && buffer[size - i - 1]){
            isBegin = 1;
        }
        if(isBegin){
            result = result << 8 | buffer[size - i - 1];
        }
    }
    return result;
}

四、控制字符序列

        在前面,简要介绍过通过输出特殊的转义字符可以控制输出产生不同的效果。这些特殊的字符序列通常被称为转义序列(Escape Sequences)或控制字符(Control Characters)。

         以下参考【Console Virtual Terminal Sequences - Windows Console | Microsoft Learn

(一)光标定位

        通过将光标定位,在某些时候可以很好控制输入输出效果。

        CSI(Control Sequence Introducer)序列\x1b[(其中\x1b是ESC字符的十六进制表示)用于引入控制序列,在某些教程中也会使用\033[,这两者其实是一样的。

        可以将\x1b[先定义为宏

光标定位
序列说明
\textbf{ESC[nA}将光标移动到当前列的前n行
\textbf{ESC[nB}将光标移动到当前列的后n行
\textbf{ESC[nC}将光标移动到当前位置的前n列
\textbf{ESC[nD}将光标移动到当前位置的后n列
\textbf{ESC[nE}将光标移动到后n行,列变为第1列
\textbf{ESC[nF}将光标移动到前n行,列变为第1列
\textbf{ESC[nd}将光标移动到当前列的第n行
\textbf{ESC[nG}将光标移动到当前行的第n
\textbf{ESC[r;cH} 将光标移动到当前视界的第r行第c列
\textbf{ESC[r;cf}将光标移动到当前视界的第r行第c列

        <n> 表示移动的距离,是一个可选参数。如果 <n> 被省略或者等于 0 ,则将其视为 1 。<n> 不能大于 32767 (最大短整型值),<n> 不能为负数。 

        示例 

    printf(ESC"[10;10H(1)这里是第10行第10列");
    printf(ESC"[5d(2)这里是第5行第10列");
    printf(ESC"[20g(3)这里是第5行第20列");
    printf(ESC"[10;10f(4)这是也是第10行第10列");
    printf(ESC"[1A(5)上移动一行");
    printf(ESC"[1B(6)下移动一行");
    printf(ESC"[1C(7)前移动");
    printf(ESC"[1D(8)后移动");
    printf(ESC"[3E(9)下移动3行");
    printf(ESC"[3F(10)上移动3行");

        效果 

        

(二)光标状态

        以下控制序列,可以设置光标的状态

序列说明
ESC [ ? 12 h启动文本光标闪烁效果
ESC [ ? 12 l禁用文本光标闪烁效果
ESC [ ? 25 h显示文本光标
ESC [ ? 25 l隐藏文本光标
ESC[0 SP q默认光标形状
ESC[1 SP q闪烁的块光标形状
ESC[2 SP q块光标形状
ESC[3 SP q闪烁的下划线光标形状
ESC[4 SP q下划线光标形状
ESC[5 SP q闪烁的条光标形状
ESC[6 SP q条光标形状

        其中SP是一个小空白字符(0x20),q是字符(0x71)。可以自行设计宏定义,方便操作

 

滚动控制台窗口的控制序列\x1b[%dT\x1b[%dS分别用于向下和向上滚动指定的行数。然而,这些序列的广泛支持可能因终端而异。

(三)文本修改

        以下序列可以进行一些文本操作

序列说明
ESC[n@在光标当前位置插入n个空格,将所有现有文本向右移动。退出右侧屏幕的文本将被删除。
ESC[nP删除当前光标位置的n个字符,从屏幕右边缘移位空格字符。
ESC[nX从当前光标位置擦除n个字符,方法是用空格字符覆盖它们。
ESC[nL插入n行到光标位置的缓冲区中。光标所在的行及其下方的行将向下移动。
ESC [nM从光标当前行开始删除n行

以上的n将是0如果忽略不写,以下的序列的n有且仅有三种有效值

  • 0 从当前光标位置(包括该位置)擦除到行/显示的末尾。
  • 1 从行/显示的开头擦除到并包括当前光标位置。
  • 2 擦除整行/显示。
ESC[nJ将当前视口/屏幕中由 <n> 指定的所有文本用空格字符替换。
ESC[nK将由 <n> 所指定的、光标所在行上的所有文本替换为空格字符。

          示例

 

printf("This is first line\n"); //换行,刷新缓冲区
printf("Next Line");  // 使光标有所移动
printf(ESC"[0K");  // 清除当前行后面的内容

         如果,不换行,效果就不同了

printf("This is first line"); //不换行
printf(ESC"[5C");  // 使光标有所移动
printf(ESC"[0K");  // 清除当前行后面的内容

(四)文本颜色 

        SGR(Select Graphic Rendition)序列可以设置输出的文本颜色。

序列说明
ESC [ <n> mn是由分号分割的若干数字,可以接受0~16个参数,但是将会从右向左应用,并选择第一个有效的前景色与背景色

​​​​​​​

前景色 (Foreground Colors)

描述行为描述
0默认将所有属性恢复到修改前的状态
1粗体/高亮对前景色应用亮度/强度标志
22无粗体/高亮从前景色移除亮度/强度标志
30前景色黑色对前景色应用非粗体/高亮的黑色
31前景色红色对前景色应用非粗体/高亮的红色
32前景色绿色对前景色应用非粗体/高亮的绿色
33前景色黄色对前景色应用非粗体/高亮的黄色
34前景色蓝色对前景色应用非粗体/高亮的蓝色
35前景色品红色对前景色应用非粗体/高亮的品红色
36前景色青色对前景色应用非粗体/高亮的青色
37前景色白色对前景色应用非粗体/高亮的白色
39前景色默认仅应用默认设置的前景部分(见0)
90粗体/高亮黑色对前景色应用粗体/高亮的黑色
91粗体/高亮红色对前景色应用粗体/高亮的红色
92粗体/高亮绿色对前景色应用粗体/高亮的绿色
93粗体/高亮黄色对前景色应用粗体/高亮的黄色
94粗体/高亮蓝色对前景色应用粗体/高亮的蓝色
95粗体/高亮品红色对前景色应用粗体/高亮的品红色
96粗体/高亮青色对前景色应用粗体/高亮的青色
97粗体/高亮白色对前景色应用粗体/高亮的白色

背景色 (Background Colors)

描述行为描述
40背景色黑色对背景色应用非粗体/高亮的黑色
41背景色红色对背景色应用非粗体/高亮的红色
42背景色绿色对背景色应用非粗体/高亮的绿色
43背景色黄色对背景色应用非粗体/高亮的黄色
44背景色蓝色对背景色应用非粗体/高亮的蓝色
45背景色品红色对背景色应用非粗体/高亮的品红色
46背景色青色对背景色应用非粗体/高亮的青色
47背景色白色对背景色应用非粗体/高亮的白色
49背景色默认仅应用默认设置的背景部分(见0)
100粗体/高亮黑色背景对背景色应用粗体/高亮的黑色
101粗体/高亮红色背景对背景色应用粗体/高亮的红色
102粗体/高亮绿色背景对背景色应用粗体/高亮的绿色
103粗体/高亮黄色背景对背景色应用粗体/高亮的黄色
104粗体/高亮蓝色背景对背景色应用粗体/高亮的蓝色
105粗体/高亮品红色背景对背景色应用粗体/高亮的品红色
106粗体/高亮青色背景对背景色应用粗体/高亮的青色
107粗体/高亮白色背景对背景色应用粗体/高亮的白色

        

 示例

printf(ESC"[34;46m前景色蓝色,背景色青色\r\n");
printf(ESC"[0m恢复\n");

(五)设置窗口标题

        OSC(Operating System Command)序列,以\x1b]开头,可以设置设置终端或控制台的窗口标题。这个序列不是所有终端都支持,但在许多Unix-like系统和较新版本的Windows 10控制台中是有效的。        

序列说明
\textbf{ ESC ] 0 ; <string> <ST> }设置窗口标题为string
\textbf{ ESC ] 1 ; <string> <ST> }

         ST为字符结束符,可以是ESC \ (0x1B 0x5C)或BEL (0x07) ,这里是0x07。

  • 16
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值