23、C++文件操作全解析

C++文件操作全解析

在实际编程中,对文件进行访问操作是很常见的需求。C++在处理文件访问时,与处理标准输入输出的方式类似。下面将详细介绍C++中文件操作的相关知识。

1. 流的概念

流指的是字节从源流入程序,或者从程序流向目标的过程。如果是流入程序(如从键盘输入),则称为输入流;如果是从程序流出(如输出到屏幕),则称为输出流。在之前的程序中,输入流通常与键盘关联,输出流与屏幕关联。而大型程序可能需要额外的流,比如与磁盘文件或其他设备(如传感器、打印机、相机等)关联的流。

常见的 cout cin 对象就分别代表了输出流和输入流。例如, cout ostream 类的对象,它有存储输出格式信息的成员,还有用于存储待显示信息的内存缓冲区。当执行 cout << "This is a message"; 时,信息会先存储在这个缓冲区中,然后再输出到屏幕。同样, cin 代表输入流,信息从输入设备(如键盘)通过 cin 对象流入程序。

许多程序员在从流中读取数据时,喜欢使用 while(cin >> a) while(cin.getline(s, sizeof(s)) 这样的表达式。这两个循环的工作方式类似, operator>> 函数从流中读取一个值,存储到变量中,并返回流的引用。如果读取错误或没有更多数据,条件就会变为 false ,循环终止。

2. 流的状态

在对流执行操作(如写入数据)后,需要检查流的状态,以确定操作是否成功。可以通过检查特定的标志(flag)或调用相应的函数来判断是否发生错误。这些标志在 ios_base 类中声明,具体如下:
| 标志 | 描述 | 函数 |
| ---- | ---- | ---- |
| badbit | 若发生不可恢复的错误,其值变为1 | bad(),若badbit值为1则返回true |
| failbit | 若输入/输出操作失败,其值变为1 | fail(),若badbit或failbit值为1则返回true |
| eofbit | 若到达文件末尾,其值变为1 | eof(),若eofbit值为1则返回true |
| goodbit | 若没有错误发生,即前三个值都为0,其值变为0 | good(),若没有错误发生则返回true |

还有 clear() 函数,默认参数值为0,例如 clear(); 可以清除 badbit failbit eofbit 的值。如果这些标志位被设置,并且想再次使用流,就需要清除它们以重置流的状态。通常可以使用 operator! 函数来检查流的状态,如 if(!cin) ,若发生错误,条件为 true

3. 文本文件和二进制文件
  • 文本文件 :由一行或多行按照标准格式(如ASCII码)的可读字符组成。使用任何可用的编辑器都可以轻松处理文本文件。每行以操作系统用于表示行尾的特殊字符结尾,在Windows系统中,是 '\r' (回车)和 '\n' (换行)字符对(即CR/LF,ASCII码分别为13和10);而在Unix系统中,仅 '\n' 字符表示行尾。
  • 二进制文件 :其字节不一定代表可读字符,例如可执行的C++程序、图像或声音文件通常以二进制形式存储。打开二进制文件时,可能会看到一些难以理解的字符。二进制文件不分行,也不会进行字符转换,在Windows中,写入二进制文件时,换行字符不会扩展为 \r\n

另外,操作系统可能会在文本文件末尾添加一个特殊字符来标记文件结束,如在Windows中, Ctrl+Z 字符标记文本文件的结束;而二进制文件中没有字符具有特殊意义。

存储数据时,二进制文件相比文本文件可以节省空间。例如,使用ASCII字符集将数字47654写入文本文件,由于该数字由五个字符表示,文件大小为5字节;而以二进制形式存储时,文件大小仅为2字节。不过,二进制文件也有缺点,当从一个系统传输到另一个系统时,数据的表示方式可能不同,因为不同系统存储数据的方式可能不同,而且数据类型的大小也可能因系统而异。

4. 文件输入输出

C++提供了多种有用的类来管理文件,具体如下:
- ofstream 类:从 ostream 派生而来,用于向文件中写入数据。
- ifstream 类:从 istream 派生而来,用于读取文件。
- fstream 类:从 iostream 派生而来,而 iostream 又从 ostream istream 派生,用于同时读写文件。

这些类继承了 ios_base 的常量和函数,用于检查流的状态。使用时需要包含 <fstream> 头文件。

下面是一个简单的向文件写入数据的示例:

#include <iostream>
#include <fstream>
#include <cstdlib>

int main()
{
    int i;
    std::ofstream fout;
    fout.open("test.txt");
    if(fout.is_open() == false)
    {
        std::cout << "Error: File can not be opened\n";
        exit(EXIT_FAILURE);
    }
    for(i = 0; i < 3; i++)
        fout << "Hello_" << i+1 << '\n';
    fout.close();
    return 0;
}

上述代码的步骤为:首先创建一个 ofstream 对象,然后将其与文件关联,最后像使用 cout 一样使用该对象向文件中写入数据。如果文件不存在,程序会创建它;如果文件已存在,其内容会被删除。

如果要读取文件内容,需要创建一个 ifstream 对象,将其与文件关联,然后像使用 cin 一样使用该对象读取内容。示例如下:

#include <iostream>
#include <fstream>
#include <string>
#include <cstdlib>
using namespace std;

int main()
{
    double temp;
    string fname;

    cout << "Enter file name: ";
    cin >> fname;
    ifstream fin(fname.c_str());
    if(fin.is_open() == false)
    {
        cout << "Error: File can not be opened\n";
        exit(EXIT_FAILURE);
    }
    while(1)
    {
        fin >> temp;
        if(!fin)
            break;
        if(temp >= -5 && temp <= 5)
            cout << temp << '\n';
    }
    fin.close();
    return 0;
}

如果要同时读写文件,则需要使用 fstream 对象。示例如下:

#include <iostream>
#include <fstream>
#include <cstdlib>
#include <string>
using namespace std;

struct Student
{
    string name;
    int code;
    float grd;
};

int main()
{
    Student s1 = {"J.Lee", 100, 5.8}, s2;

    fstream fstr("test.txt", ios_base::in | ios_base::out | ios_base::trunc);
    if(fstr.is_open() == false)
    {
        cout << "Error: File can not be opened\n";
        exit(EXIT_FAILURE);
    }
    fstr << s1.name << ' ' << s1.code << ' ' << s1.grd << '\n';
    fstr.seekg(0);
    fstr >> s2.name >> s2.code >> s2.grd;
    cout << s2.name << ' ' << s2.code << ' ' << s2.grd << '\n';
    fstr.close();
    return 0;
}
5. 流缓冲区

ofstream 类使用缓冲区来存储数据,为每个创建的 ofstream 对象,程序会分配相应的内存。这个内存作为程序和文件之间的中间缓冲区,数据不会直接写入文件,而是先存储在输出缓冲区中。当缓冲区满时,程序会刷新缓冲区,将数据写入文件。这种缓冲方式可以提高性能,因为一次向磁盘进行大的数据传输比多次小的数据传输更高效。同样,使用 ifstream 对象从磁盘读取文件时也会应用缓冲机制,程序会读取大块数据,存储在对象的输入缓冲区中,然后处理缓冲区中的数据。

可以在需要时刷新输出缓冲区,例如使用 flush endl ends unitbuf 操纵符。另外,输出流可以与另一个流关联,在第二个流上执行读写操作可能会导致输出流的缓冲区刷新。例如, cin cerr 流默认与 cout 关联,从 cin 读取或向 cerr 写入会刷新 cout 的缓冲区。

如果程序异常结束(如崩溃),缓冲区不会被刷新,那么缓冲区中的数据可能不会被处理。因此,为了更好地控制程序,与程序操作相关的诊断消息应该直接输出(如使用 endl ),这样即使程序崩溃,程序员也能看到这些消息。

6. 关闭文件

在使用缓冲方式向文件写入数据时,数据不会直接写入磁盘上的文件,而是先写入为其预留的缓冲区。当缓冲区满或文件关闭时,缓冲区会被刷新,数据会存储到文件中。这样可以避免频繁访问磁盘,提高程序性能。 close() 函数用于关闭打开的文件,如果文件是为写入而打开的, close() 会将缓冲区中可能剩余的数据存储到文件中。

虽然程序正常终止时,打开的文件会自动关闭,但最好在不再使用文件时手动关闭它。例如,以下代码可能会导致数据丢失:

int *p;
ofstream fout("test.txt");
// Write data in the file.
...
// We finished with the data writing, but we did not close the file.
*p = 20; // Wrong action.

由于 p 没有指向有效的地址,程序可能会崩溃,此时缓冲区中可能剩余的数据会丢失。如果在写入完成后调用了 close() ,缓冲区会被刷新,数据会存储到文件中。所以,要记得在不再需要文件时关闭它,以确保数据不会丢失。

7. 文件打开模式

在将文件与流对象关联时,可以使用第二个参数来指定对文件执行的操作。 ios_base 类定义了表示打开模式的常量,使用 | 位运算符可以启用多种模式。具体的打开模式如下:
| 打开模式 | 操作 |
| ---- | ---- |
| ios_base::in | 以只读模式打开文件 |
| ios_base::out 或 ios_base::out | ios_base::trunc | 以写入模式打开文件,如果文件存在,其内容会被截断;如果文件不存在,则会创建它 |
| ios_base::app | 以追加模式打开文件,如果文件存在,现有数据会被保留,新数据会添加到文件末尾;如果文件不存在,则会创建它 |
| ios_base::in | ios_base::out | 以读写模式打开文件 |
| ios_base::in | ios_base::out | ios_base::trunc | 以读写模式打开文件,如果文件存在,其内容会被截断;如果文件不存在,则会创建它 |
| ios_base::in | ios_base::out | ios_base::app | 以读写和追加模式打开文件,如果文件存在,现有数据会被保留,新数据会添加到文件末尾;如果文件不存在,则会创建它 |

还有 ios_base::ate 选项,它会将文件指针置于文件末尾。 ios_base::app ios_base::ate 的区别在于,前者只允许在文件末尾添加数据,而后者只是将文件指针置于文件末尾。

打开文本文件时,选择上述模式之一;打开二进制文件时,需要添加 ios_base::binary 常量。例如,以二进制模式打开文件进行读取,可使用 ios_base::in | ios_base::binary ;以二进制模式打开文件进行读写,可使用 ios_base::in | ios_base::out | ios_base::trunc | ios_base::binary

打开模式是可选参数,如果未指定,会应用默认模式。 ifstream open() 函数和构造函数的默认模式是 ios_base::in ofstream open() 函数和构造函数的默认模式是 ios_base::out fstream open() 函数和构造函数的默认模式是 ios_base::in | ios_base::out 。以下是一些示例:

ifstream fin("test.txt"); // 以默认模式ios_base::in打开文件进行读取
ofstream fout("test.txt", ios_base::app); // 以写入和追加模式打开文件,保留现有数据
fstream f("test.txt", ios_base::in | ios_base::out | ios_base::trunc); // 以读写模式打开文件,删除现有数据
8. 打开多个文件

应用程序可能需要打开多个文件。如果需要同时打开多个文件,则必须为每个文件声明单独的对象。可以同时打开的文件数量取决于操作系统。

在打开多个文件的应用程序中,建议在不再需要某个文件时关闭它,因为操作系统对同时打开的文件数量有限制。关闭文件后,可以将相应的对象用于另一个文件,而无需声明新的对象,这样可以避免分配新的内存。例如:

ifstream fin;
fin.open(fname1);
/* Use fin to read the file. */
fin.close();
fin.open(fname2);
/* Use the same object to read a new file. */
fin.close();
...
9. 文件处理

当文件被打开并与相应的对象关联时,与文件相关的信息会存储在对象的成员中。例如, ofstream 对象包含一个指针成员,指示文件中写入数据的位置; ifstream 对象包含一个指针成员,指示文件中读取数据的位置。

当文件以读取或写入模式打开时,指针指向文件的开头;如果以追加模式打开,指针指向文件末尾。执行读写操作时,指针的值会自动更新。例如,以读取模式打开文件并读取50个字符后,指针会指向距离文件开头50字节的位置;以写入模式打开文件时,指针会前进与写入字节数相等的位置,下一次写入操作将在新位置进行。

处理文件可以使用串行访问和随机访问两种方式。串行访问时,每个读写操作按顺序执行,例如要读取第50个字符,必须先读取前49个字符;而随机访问可以直接移动到文件内的任何位置,使用 seekg() seekp() 函数可以实现文件的随机访问。

以下是 seekg() 函数的两种原型:

istream& seekg(streamoff offset, ios_base::seekdir origin);
istream& seekg(streampos offset);

第一个版本将输入指针移动到距离 origin 指定位置 offset 字节的新位置, streamoff 是某种整数类型的同义词,如果 offset 为负,指针会向后移动。 origin 的值应该是 ios_base 类的以下整数常量之一:
- ios_base::beg :指针从文件开头移动 offset 字节。
- ios_base::cur :指针从当前位置移动 offset 字节。
- ios_base::end :指针从文件末尾移动 offset 字节。

第二个版本中, offset 的值表示从文件开头以八位字节为单位的新位置。以下是一些示例:

seekg(0, ios_base::end); // 移动到文件末尾
seekg(20, ios_base::beg); // 从文件开头移动20字节
seekg(-5, ios_base::cur); // 从当前位置向后移动5字节

如果要查找指针的当前位置,可以使用 tellg() 函数,它返回一个 streampos 值,表示输入指针相对于文件开头的当前位置(以字节为单位),如果发生错误则返回 -1。可以结合使用 tellg() seekg() 返回到文件的先前位置:

// Store the location to which we want to return.
streampos old_pos = fin.tellg();
// ... move to another location to read.
fin.seekg(old_pos); // Return back to the first location.

同样,对于与 ofstream 对象关联的文件的随机访问,可以使用 seekp() tellp() 函数。需要注意的是,在文本文件中使用 seekg() seekp() 时,要注意换行字符的处理。例如,在Windows系统中,如果字符串包含换行字符,使用 write() 函数时可能会出现意外结果,因此使用 seekp() seekg() 时,二进制文件比文本文件更安全。

fstream 对象的情况下,继承的输入和输出指针会同步移动,因此 tellg() tellp() 返回相同的值。如果文件是为读写而打开的,要在读写之间切换,应该先调用文件定位函数(如 seekp() )或先刷新缓冲区(如使用 flush )。可以使用 fail() 函数检查 seekg() seekp() 是否执行成功。

10. 文件结束标志

操作系统可能会在文本文件末尾添加一个特殊字符来标记文件结束,而二进制文件没有这样的标记。在Dos/Windows应用程序中,通常使用ASCII值为26的 Ctrl+Z 字符来标记文本文件的结束。例如,以下程序在这样的系统中运行时,只会显示字符 a b

#include <iostream>
#include <fstream>
#include <cstdlib>
using namespace std;

int main()
{
    int ch;

    fstream fstr("test.txt", ios_base::in | ios_base::out | ios_base::trunc);
    if(fstr.is_open() == false)
    {
        cout << "Error: is_open() failed\n";
        exit(EXIT_FAILURE);
    }
    fstr << 'a' << 'b' << (char)26 << 'c' << 'd';
    fstr.seekg(0);
    while(1)
    {
        ch = fstr.get();
        if(ch == EOF)
            break;
        cout << (char)ch;
    }
    fstr.close();
    return 0;
}

如果以二进制模式创建文件,程序会显示所有字符,因为二进制文件中没有字符具有特殊意义。

11. 重定向

包含 <iostream> 文件后,可以使用 cin cout cerr clog 对象。 cin 与标准输入设备关联, cout 与标准输出设备关联, cerr clog 对应标准错误流,与标准输出设备(如屏幕)关联,都可用于显示消息。需要注意的是, cerr 是无缓冲的,输出会立即显示。

许多操作系统允许对 cin cout 进行重定向。例如,如果可执行文件名为 test ,在命令行中输入 test >output.txt cout 中写入的数据将不会显示在屏幕上,而是会写入 output.txt 文件中。同样,可以使用 test <input.txt cin 的输入重定向到 input.txt 文件,还可以同时进行两种重定向,如 test <input.txt >output.txt

为了与其他程序保持一致,通常继续使用 cout 输出各种消息,但在输出错误消息时,最好使用 cerr clog 而不是 cout 。因为如果 cout 被重定向,在打开文件之前无法看到消息,而写入 cerr clog 的错误消息会实时显示在屏幕上。

12. 读写文本文件

处理文本文件时,可以使用与 cout cin 对象相同的函数,如 operator<<() operator>>() 函数。以下是一个将对象写入文件的示例:

#include <iostream>
#include <fstream>
#include <cstdlib>
#include <string>
#include <vector>
using namespace std;

class Student
{
public: // For simplicity, I declare all as public.
    int code;
    string name;
    float grd;
    Student(int c, const string& n, float g) : code(c), name(n), grd(g) {}
};

int main()
{
    int i;
    Student s1(1, "First", 2.5), s2(2, "Sec", 3.5), s3(3, "Third", 4.5);
    vector<Student> v;

    v.push_back(s1);
    v.push_back(s2);
    v.push_back(s3);

    std::ofstream fout("test.txt");
    if(fout.is_open() == false)
    {
        std::cout << "Error: File can not be opened\n";
        exit(EXIT_FAILURE);
    }
    for(i = 0; i < v.size(); i++)
        fout << v[i].code << '\t' << v[i].name << '\t' << v[i].grd << '\n';
    fout.close();
    return 0;
}

如果要指定数据在文件中的存储格式,可以使用操纵符等工具。例如,要将成绩以两位小数的形式存储,可以这样写:

fout << fixed << setprecision(2) << v[i].name << '\t' << v[i].code << '\t' << v[i].grd << '\n';

在读取文件时,最常用的函数是 operator>>() getline() get() 。使用 operator>>() 函数从文件中读取数据并赋值给适当的变量时,程序员需要知道文件中存储数据的类型和顺序。例如,要读取上述程序创建的文件,需要知道每行依次包含一个整数、一个字符串和一个浮点数。以下是读取文件的示例:

#include <iostream>
#include <fstream>
#include <cstdlib>
#include <string>
using namespace std;

int main()
{
    int code;
    string name;
    float grd;

    ifstream fin("test.txt");
    if(fin.is_open() == false)
    {
        cout << "Error: is_open() failed\n";
        exit(EXIT_FAILURE);
    }
    while(1)
    {
        fin >> code >> name >> grd;
        if(!fin)
            break;
        cout << code << ' ' << name << ' ' << grd << '\n';
    }
    fin.close();
    return 0;
}

在循环中读取每行数据并存储到相应的变量中。如果尝试在文件末尾之后读取或发生其他错误,流的状态会变为 fail ,循环会结束。可以使用 eof() 函数确定是到达文件末尾还是其他原因导致读取失败。

也可以使用 getline() 函数逐行显示文件内容,例如:

string line;
while(1)
{
    getline(fin, line);
    if(!fin)
        break;
    cout << line << '\n';
}

许多程序员将读取语句放在 while 条件中,即 while(getline(fin, line)) cout << line << '\n'; ,但使用无限循环可以更清晰地看到读取语句和终止循环的条件。还可以使用 get() 函数逐个字符读取文件。如果到达文件末尾并想再次访问文件,需要调用 clear() 清除 eofbit

13. 读写二进制文件

读写二进制文件分别使用 write() read() 函数,虽然它们主要用于二进制文件,但也可以谨慎地用于文本文件。

write() 函数的原型为:

ostream& write(const char *buf, streamsize count);

buf 参数指向存储要写入文件数据的内存, streamsize 是某种整数类型的同义词, count 参数指定要写入文件的字节数。以下是一些使用 write() 函数的示例:

int arr[1000];
ofstream fout("test.dat", ios_base::out | ios_base::trunc | ios_base::binary);
fout.write((char*)arr, sizeof(arr));

double d = 1.2345;
fout.write((char*)&d, sizeof(double));

Student s;
fout.write((char*)&s, sizeof(s));

需要注意的是,只有对象的数据会被写入文件,而不是函数。应该将地址强制转换为 char* ,并使用 sizeof 运算符指定大小,以确保程序的平台独立性。可以通过检查流的状态来验证是否发生错误,例如 if(!fout) cout << "write error\n";

read() 函数的原型为:

istream& read(char *buf, streamsize count);

buf 参数指向存储读取数据的内存, count 参数指定从文件中读取的字节数。由于不同系统可能存在差异(如数据类型大小不同),不能保证使用 write() 函数写入的文件在另一个系统中能被 read() 函数正确读取。以下是使用 read() 函数的示例:

double d;
ifstream fin("test.dat", ios_base::in | ios_base::binary);
fin.read((char*)&d, sizeof(d));

int arr[1000];
fin.read((char*)arr, sizeof(arr));

使用 write() read() 函数时,二进制文件比文本文件更安全,因为文本文件中换行字符的潜在转换可能会产生意外结果。另外,在处理使用虚拟函数的类时要小心,因为虚拟函数会有一个隐藏指针,该指针会被存储在文件中,再次读取文件时可能会导致指针值无效,从而使程序崩溃。

相关练习

1. 练习C.24.1

将示例C.24.5中 Student 类的成员改为私有,并完成以下程序,使其显示两个学生的数据:

#include <iostream>
#include <fstream>
#include <cstdlib>
#include <string>
using namespace std;

class Student
{
private:
    int code;
    string name;
    float grd;
public:
    Student(int c, const string& n, float g) : code(c), name(n), grd(g) {}
    friend istream& operator>>(istream& in, Student& s);
    friend ostream& operator<<(ostream& out, const Student& s);
    void show() const {cout << "N:" << name << " C:" << code << " G:" << grd << '\n';}
};

istream& operator>>(istream& in, Student& s)
{
    in >> s.name >> s.code >> s.grd;
    return in;
}

ostream& operator<<(ostream& out, const Student& s)
{
    out << s.name << '\t' << s.code << '\t' << s.grd << '\n';
    return out;
}

int main()
{
    Student s1(1, "First", 2.5), s2(5, "Second", 7.5);

    fstream fstr("test.txt", ios_base::in | ios_base::out | ios_base::trunc);
    if(fstr.is_open() == false)
    {
        cout << "Error: File can not be opened\n";
        exit(EXIT_FAILURE);
    }
    fstr << s1 << s2;
    fstr.seekg(0);
    fstr >> s1 >> s2;
    s1.show();
    s2.show();
    fstr.close();
    return 0;
}
2. 练习C.24.2

编写一个程序,连续读取产品代码和价格,并将它们存储在 test.txt 文件中。如果用户输入价格为 -1,则停止插入产品。然后,程序应读取一个产品代码,在文件中查找并显示其价格(如果找到):

#include <iostream>
#include <fstream>
#include <cstdlib>
#include <string>
using namespace std;

int main()
{
    bool found;
    string code, tmp;
    double prc;

    fstream fstr("test.txt", ios_base::in | ios_base::out | ios_base::trunc);
    if(fstr.is_open() == false)
    {
        cout << "Error: is_open() failed\n";
        exit(EXIT_FAILURE);
    }
    while(1)
    {
        cout << "Enter price: ";
        cin >> prc;
        if(prc == -1)
            break;
        cin.get();
        cout << "Enter product code: ";
        getline(cin, code);
        fstr << code << ' ' << prc << '\n';
    }
    cin.get();
    cout << "Enter product code to search for: ";
    getline(cin, tmp);

    found = 0;
    fstr.seekg(0);
    while(1)
    {
        fstr >> code >> prc;
        if(!fstr)
            break;
        if(code == tmp)
        {
            found = 1;
            break;
        }
    }
    if(found == 0)
        cout << tmp << " code is not listed\n";
    else
        cout << "The price for product " << code << " is " << prc << '\n';
    fstr.close();
    return 0;
}
3. 练习C.24.3

假设 students.txt 文件的每行包含学生的姓名和两门课程的成绩,编写一个程序,读取每行数据,将平均成绩大于或等于5的学生的姓名和成绩存储在 suc.txt 文件中,将平均成绩小于5的学生的姓名和成绩存储在 fail.txt 文件中,并显示每个文件中写入的学生数量:

#include <iostream>
#include <fstream>
#include <cstdlib>
#include <string>
using namespace std;

int main()
{
    string fnm, lnm;
    int suc_stud, fail_stud;
    double grd1, grd2;

    ifstream fin("students.txt");
    if(fin.is_open() == false)
    {
        cout << "Error: students.txt can't be loaded\n";
        exit(EXIT_FAILURE);
    }
    ofstream fout_suc("suc.txt");
    if(fout_suc.is_open() == false)
    {
        fin.close();
        cout << "Error: suc.txt can't be created\n";
        exit(EXIT_FAILURE);
    }
    ofstream fout_fail("fail.txt");
    if(fout_fail.is_open() == false)
    {
        fin.close();
        fout_suc.close();
        cout << "Error: fail.txt can't be created\n";
        exit(EXIT_FAILURE);
    }
    suc_stud = fail_stud = 0;
    while(1)
    {
        fin >> fnm >> lnm >> grd1 >> grd2;
        if(!fin)
            break;
        if((grd1+grd2)/2 >= 5)
        {
            fout_suc << fnm << ' ' << lnm << ' ' << grd1 << ' ' << grd2 << '\n';
            suc_stud++;
        }
        else
        {
            fout_fail << fnm << ' ' << lnm << ' ' << grd1 << ' ' << grd2 << '\n';
            fail_stud++;
        }
    }
    cout << "Fail: " << fail_stud << " Success: " << suc_stud << '\n';
    fin.close();
    fout_suc.close();
    fout_fail.close();
    return 0;
}
4. 练习C.24.4

编写一个程序,读取两个文件的名称,逐行比较它们,并显示它们的第一行相同内容。如果两个文件没有相同的行,程序应显示相关消息:

#include <iostream>
#include <fstream>
#include <cstdlib>
#include <string>
using namespace std;

int main()
{
    bool found;
    string fname, str1, str2;

    cout << "Enter first file: ";
    cin >> fname;
    ifstream fin_f(fname.c_str());
    if(fin_f.is_open() == false)
    {
        cout << "Error: first is_open() failed\n";
        exit(EXIT_FAILURE);
    }
    cout << "Enter second file: ";
    cin >> fname;
    ifstream fin_s(fname.c_str());
    if(fin_s.is_open() == false)
    {
        fin_f.close();
        cout << "Error: second is_open() failed\n";
        exit(EXIT_FAILURE);
    }
    found = 0;
    while(1)
    {
        getline(fin_f, str1);
        getline(fin_s, str2);
        if(!fin_f || !fin_s)
            break;
        if(str1 == str2)
        {
            cout << "The same line is: " << str1 << '\n';
            found = 1;
            break;
        }
    }
    if(found == 0)
        cout << "There is no common line\n";
    fin_f.close();
    fin_s.close();
    return 0;
}
5. 练习C.24.5

编写一个程序,读取一个文件的名称,并显示其第二行。要求使用 get() 函数而不是 getline() 函数:

#include <iostream>
#include <fstream>
#include <cstdlib>
#include <string>
using namespace std;

int main()
{
    int ch, lines;
    string fname;

    cout << "Enter file name: ";
    cin >> fname;
    ifstream fin(fname.c_str());
    if(fin.is_open() == false)
    {
        cout << "Error: is_open() failed\n";
        exit(EXIT_FAILURE);
    }
    cout << "Line contents: ";
    lines = 1;
    while(1)
    {
        ch = fin.get();
        if((!fin) || (lines > 2))
            break;
        if(lines == 2)
            cout << (char)ch;
        if(ch == '\n')
            lines++;
    }
    fin.close();
    return 0;
}
6. 练习C.24.6

编写一个程序,读取一个密钥字符和一个文本文件的名称,通过将文件中的每个字符与密钥进行异或运算来加密文件内容,并将加密后的字符存储在用户选择的第二个文件中:

#include <iostream>
#include <fstream>
#include <cstdlib>
#include <string>
using namespace std;

int main()
{
    int ch, key_ch;
    string fname;

    cout << "Enter input file: ";
    cin >> fname;
    ifstream fin(fname.c_str());
    if(fin.is_open() == false)
    {
        cout << "Error: Input file can't be loaded\n";
        exit(EXIT_FAILURE);
    }
    cout << "Enter output file: ";
    cin >> fname;
    ofstream fout(fname.c_str());
    if(fout.is_open() == false)
    {
        fin.close();
        cout << "Error: Output file can't be created\n";
        exit(EXIT_FAILURE);
    }
    cin.get();
    cout << "Enter key char: ";
    key_ch = cin.get();
    while(1)
    {
        ch = fin.get();
        if(!fin)
            break;
        fout << (char)(ch ^ key_ch);
    }
    fin.close();
    fout.close();
    return 0;
}

如果重新运行程序,将加密后的文件作为输入并使用相同的密钥,输出文件将与原始文件相同,因为根据布尔代数 (a ^ b) ^ b = a

7. 练习C.24.7

定义 Country 类,其私有成员包括名称、首都和人口。假设 country.txt 文件的每行包含相应国家的数据,格式为 name capital population 。编写一个程序,读取文件并使用该类将国家数据存储在 Country 元素的向量中。然后,程序应读取一个数字,并显示人口高于该数字的国家的数据:

#include <iostream>
#include <fstream>
#include <cstdlib>
#include <string>
#include <vector>
using namespace std;

class Country
{
private:
    string name;
    string capital;
    int pop;
public:
    Country(const string& n = "", const string& c = "", int p = 0) : name(n), capital(c), pop(p) {}
    void set(const string& n, const string& c, int p);
    int get_pop() const {return pop;}
    void show() const;
};

void Country::set(const string& n, const string& c, int p)
{
    name = n;
    capital = c;
    pop = p;
}

void Country::show() const
{
    cout << name << '\t' << capital << '\t' << pop << '\n';
}

int main()
{
    int i, pop;
    string name, capital;
    Country tmp;
    vector<Country> cntr;

    ifstream fin("country.txt");
    if(fin.is_open() == false)
    {
        cout << "Error: is_open() failed\n";
        exit(EXIT_FAILURE);
    }
    while(1)
    {
        fin >> name >> capital >> pop;
        if(!fin)
            break;
        tmp.set(name, capital, pop);
        cntr.push_back(tmp);
    }

    cout << "Enter population: ";
    cin >> pop;
    for(i = 0; i < cntr.size(); i++)
        if(cntr[i].get_pop() >= pop)
            cntr[i].show();

    fin.close();
    return 0;
}
8. 练习C.24.8

编写一个程序,声明一个包含5个整数(值为10、20、30、40和50)的数组,并将它们写入二进制文件。然后,程序应读取一个整数,将存储的第三个整数替换为输入的数字。最后,程序应读取并显示文件的内容:

#include <iostream>
#include <fstream>
#include <cstdlib>
using namespace std;

int main()
{
    int i, arr[5] = {10, 20, 30, 40, 50};

    fstream fstr("test.bin", ios_base::in | ios_base::out | ios_base::trunc | ios_base::binary);
    if(fstr.is_open() == false)
    {
        cout << "Error: is_open() failed\n";
        exit(EXIT_FAILURE);
    }
    fstr.write((char*)arr, sizeof(arr));

    cout << "Enter new value: ";
    cin >> i;
    fstr.seekp(2*sizeof(int));
    fstr.write((char*)&i, sizeof(int));

    fstr.seekg(0);
    fstr.read((char*)arr, sizeof(arr));
    if(!fstr)
    {
        fstr.close();
        cout << "Error: read() failed\n";
        exit(EXIT_FAILURE);
    }
    cout << "\n***** File contents *****\n";
    for(i = 0; i < 5; i++)
        cout << arr[i] << '\n';

    fstr.close();
    return 0;
}
9. 练习C.24.9

编写一个程序,读取10本书的标题(使用一个100字符的数组),并将它们写入用户选择的二进制文件中。先写入标题的大小,再写入标题。然后,程序应读取一个标题,并显示一条消息,指示该标题是否包含在文件中:

#include <iostream>
#include <fstream>
#include <cstdlib>
#include <string>
#include <cstring>
using namespace std;

int main()
{
    bool found;
    int i, len;
    char title[100], str[100];
    string fname;

    cout << "Enter file name: ";
    cin >> fname;

    fstream fstr(fname.c_str(), ios_base::in | ios_base::out | ios_base::trunc | ios_base::binary);
    if(fstr.is_open() == false)
    {
        cout << "Error: is_open() failed\n";
        exit(EXIT_FAILURE);
    }
    cin.get();
    for(i = 0; i < 10; i++)
    {
        cout << "Enter title: ";
        cin.getline(title, sizeof(title));
        len = strlen(title);
        fstr.write((char*)&len, sizeof(int));
        fstr.write(title, len);
    }
    cout << "Enter title to search: ";
    cin.getline(str, sizeof(str));

    found = 0;
    fstr.seekg(0);
    while(1)
    {
        fstr.read((char*)&len, sizeof(int));
        if(!fstr)
            break;
        fstr.read(title, len);
        if(!fstr)
            break;
        title[len] = '\0';
        if(strcmp(title, str) == 0)
        {
            found = 1;
            break;
        }
    }
    if(found == 0)
        cout << "\nTitle not found\n";
    else
        cout << "\nTitle found\n";
    fstr.close();
    return 0;
}
10. 练习C.24.10

假设 test.bin 二进制文件包含一个学生的成绩,文件开头声明了成绩的数量。编写一个程序,从二进制文件中读取成绩(使用 float 类型),并将它们存储在动态分配的内存中。然后,程序应读取一个数字,并显示大于该数字的成绩:

#include <iostream>
#include <fstream>
#include <cstdlib>
using namespace std;

int main()
{
    int i, grd_num;
    float *grd_arr, grd;

    ifstream fin("test.bin", ios_base::in | ios_base::binary);
    if(fin.is_open() == false)
    {
        cout << "Error: is_open() failed\n";
        exit(EXIT_FAILURE);
    }
    fin.read((char*)&grd_num, sizeof(int));
    if(!fin)
    {
        fin.close();
        cout << "Error: read() failed\n";
        exit(EXIT_FAILURE);
    }
    grd_arr = new float[grd_num];
    fin.read((char*)grd_arr, grd_num * sizeof(float));
    if(fin)
    {
        cout << "Enter grade: ";
        cin >> grd;
        for(i = 0; i < grd_num; i++)
            if(grd_arr[i] > grd)
                cout << grd_arr[i] << '\n';
    }
    else
        cout << "Error: read() failed to read grades\n";

    delete[] grd_arr;
    fin.close();
    return 0;
}
11. 练习C.24.11

一种常见的杀毒软件识别病毒的方法是基于签名的检测。签名是一系列字节(如 F3 BA 20 63 7A 1B ),用于识别特定的病毒。编写一个程序,读取一个病毒签名(如五个整数),并检查它是否包含在二进制文件 test.dat 中:

#include <iostream>
#include <fstream>
#include <cstdlib>
#include <cstring>
using namespace std;

int main()
{
    const int SIZE = 5;
    bool found;
    int i, len, back, buf[SIZE], sign[SIZE];

    ifstream fin("test.dat", ios_base::in | ios_base::binary);
    if(fin.is_open() == false)
    {
        cout << "Error: is_open() failed\n";
        exit(EXIT_FAILURE);
    }
    cout << "Enter virus signature (" << SIZE << " integers)\n";
    for(i = 0; i < SIZE; i++)
    {
        cout << "Enter number: ";
        cin >> sign[i];
    }
    len = sizeof(sign);
    found = 0;
    back = len - sizeof(int);
    while(1)
    {
        fin.read((char*)buf, SIZE * sizeof(int));
        if(!fin)
            break;
        if(memcmp(buf, sign, len) == 0)
        {
            found = 1;
            break;
        }
        else
            fin.seekg(-back, ios_base::cur);
    }
    if(found == 1)
        cout << "SOS: Virus found\n";
    else
        cout << "That virus signature isn't found\n";
    fin.close();
    return 0;
}
12. 练习C.24.12

定义 Employee 结构体,其成员包括姓名(使用一个100字符的数组)、税号和工资。编写一个程序,使用该类型读取100名员工的数据,并将它们存储在一个这样的结构体数组中。如果用户输入姓名为 fin ,则停止数据插入,并将结构体写入 test.bin 二进制文件:

#include <iostream>
#include <fstream>
#include <cstdlib>
#include <cstring>
using namespace std;

struct Employee
{
    char name[100];
    int tax_num;
    int salary;
};

int main()
{
    const int SIZE = 100;
    int i, num_empl;
    Employee empl[SIZE];

    ofstream fout("test.bin", ios_base::out | ios_base::trunc | ios_base::binary);
    if(fout.is_open() == false)
    {
        cout << "Error: is_open() failed\n";
        exit(EXIT_FAILURE);
    }
    num_empl = 0;
    for(i = 0; i < SIZE; i++)
    {
        cout << "\nEnter full name: ";
        cin.getline(empl[i].name, sizeof(empl[i].name));
        if(strcmp(empl[i].name, "fin") == 0)
            break;

        cout << "Enter tax number: ";
        cin >> empl[i].tax_num;

        cout << "Enter salary: ";
        cin >> empl[i].salary;

        num_empl++;
        cin.get();
    }
    fout.write((char*)empl, num_empl * sizeof(Employee));
    fout.close();
    return 0;
}
13. 练习C.24.13

假设 test.bin 二进制文件包含上一个练习中定义的 Employee 类型的结构体。编写一个程序,读取这些结构体,并将工资超过用户输入金额的员工数据复制到 data.bin 二进制文件中。程序还应显示存储在 data.bin 文件中的员工的平均工资:

#include <iostream>
#include <fstream>
#include <cstdlib>
using namespace std;

struct Employee
{
    char name[100];
    int tax_num;
    int salary;
};

int main()
{
    int count, amount, sum_sal;
    Employee tmp_emp;

    ifstream fin("test.bin", ios_base::in | ios_base::binary);
    if(fin.is_open() == false)
    {
        cout << "Error: Input file can't be loaded\n";
        exit(EXIT_FAILURE);
    }
    ofstream fout("data.bin", ios_base::out | ios_base::trunc | ios_base::binary);
    if(fout.is_open() == false)
    {
        fin.close();
        cout << "Error: Output file can't be created\n";
        exit(EXIT_FAILURE);
    }
    cout << "Enter amount: ";
    cin >> amount;
    count = sum_sal = 0;
    while(1)
    {
        fin.read((char*)&tmp_emp, sizeof(Employee));
        if(!fin)
            break;
        if(tmp_emp.salary > amount)
        {
            fout.write((char*)&tmp_emp, sizeof(Employee));
            sum_sal += tmp_emp.salary;
            count++;
        }
    }
    if(count)
        cout << "Avg:" << (float)sum_sal/count << '\n';
    else
        cout << "No employee gets more than " << amount << '\n';
    fin.close();
    fout.close();
    return 0;
}

未解决的练习

以下是一些未解决的练习,供读者进一步练习和探索:
1. 假设 grades.txt 文本文件包含一些学生的成绩,编写一个程序,读取该文件并显示最好和最差的成绩,以及不及格(成绩 < 5)和及格(成绩 ≥ 5)学生的平均成绩。假设成绩在[0, 10]范围内。
2. 编写一个程序,检查两个用户选择的文本文件是否具有相同的内容。
3. 编写一个程序,在用户选择的文本文件中查找连续的重复单词(如 “In this this chapter we present…”),并将它们写入另一个文本文件。
4. 编写一个程序,从命令行读取两个文本文件的名称,将第二个文件的内容追加到第一个文件中,然后显示第一个文件的内容。
5. 编写一个程序,将用户选择的文本文件中的大写字母转换为小写字母,反之亦然。提示:在连续的读写操作之间调用文件定位函数(如 seekp() )。
6. 假设 students.txt 文本文件的每行包含一个学生的姓名和三门课程的成绩,编写一个程序,读取该文件并显示平均成绩最好的学生的姓名。如果有多个学生具有相同的最好平均成绩,程序应显示所有这些学生的姓名。
7. 编写一个程序,读取一个文本文件的名称,并将其内容以相反的顺序(即从最后一个字符到第一个字符)复制到用户选择的第二个文本文件中。提示:将第一个文件以二进制模式打开。
8. 编写一个程序,从命令行读取一个文本文件的名称,并显示其最长的行。假设没有多行具有相同的最长长度。程序还应显示文件中每个数字[0, 9]的出现次数。此外,程序应读取一个整数(如 x ),并显示文件的最后 x 行。程序应检查输入的数字不大于总行数。例如,如果用户输入3,程序应显示文件的最后三行。限制条件是程序应仅读取每行一次。
9. 定义 Book 结构体,其成员包括标题、作者和价格。假设 book.dat 二进制文件包含100个这样的结构体。编写一个程序,读取一个书籍条目的序号(如25)和新数据,并用新数据替换现有数据。然后,程序应从文件中读取该条目的数据并显示它们,以验证是否正确写入。
10. 考虑上一个练习中的 book.dat 文件,编写一个程序,读取现有条目,并将它们以相反的顺序写入 book_rvs.dat 二进制文件中,即最后一个条目应首先写入,倒数第二个条目应第二个写入,依此类推。
11. 假设 grades.dat 二进制文件包含一些学生的成绩,文件开头存储了学生的数量。编写一个程序,读取该文件,并将成绩按升序排序后写入 grd_sort.dat 二进制文件中。
12. 凯撒算法是朱利叶斯·凯撒使用的最古老的加密方法之一。根据该算法,每个字符被位于其后三个位置的字符替换。例如,在英文字母表中应用凯撒算法时,消息 “Watch out for Ovelix !!!” 被加密为 “Zdwfk rxw iru Ryhola !!!”。注意,字符 x 被加密为 a ,因为替换从字母表的开头继续。同样, y 被替换为 b z 被替换为 c 。接收者通过将每个字符替换为位于其前三个位置的字符来解密消息。编写一个程序,提供一个菜单来执行以下操作:
- 文件加密:程序应读取一个文件的名称和用于加密其内容的密钥。例如,在凯撒算法的情况下,密钥是3。程序应仅加密小写和大写字符。
- 文件

C++文件操作全解析

14. 综合应用示例

下面通过一个综合示例,展示如何将前面所学的文件操作知识应用到实际场景中。假设我们要开发一个简单的学生成绩管理系统,该系统可以实现学生信息的录入、成绩的存储、查询和统计等功能。

#include <iostream>
#include <fstream>
#include <cstdlib>
#include <string>
#include <vector>

using namespace std;

// 定义学生结构体
struct Student {
    string name;
    int id;
    float score;
};

// 录入学生信息
void inputStudents(vector<Student>& students) {
    int n;
    cout << "请输入学生数量: ";
    cin >> n;
    for (int i = 0; i < n; ++i) {
        Student s;
        cout << "请输入第 " << i + 1 << " 个学生的姓名: ";
        cin >> s.name;
        cout << "请输入第 " << i + 1 << " 个学生的学号: ";
        cin >> s.id;
        cout << "请输入第 " << i + 1 << " 个学生的成绩: ";
        cin >> s.score;
        students.push_back(s);
    }
}

// 将学生信息写入文件
void writeStudentsToFile(const vector<Student>& students, const string& filename) {
    ofstream fout(filename, ios_base::binary);
    if (!fout) {
        cout << "无法打开文件!" << endl;
        return;
    }
    for (const auto& s : students) {
        fout.write(reinterpret_cast<const char*>(&s), sizeof(Student));
    }
    fout.close();
}

// 从文件中读取学生信息
vector<Student> readStudentsFromFile(const string& filename) {
    vector<Student> students;
    ifstream fin(filename, ios_base::binary);
    if (!fin) {
        cout << "无法打开文件!" << endl;
        return students;
    }
    Student s;
    while (fin.read(reinterpret_cast<char*>(&s), sizeof(Student))) {
        students.push_back(s);
    }
    fin.close();
    return students;
}

// 查询学生信息
void queryStudent(const vector<Student>& students) {
    int id;
    cout << "请输入要查询的学生学号: ";
    cin >> id;
    for (const auto& s : students) {
        if (s.id == id) {
            cout << "姓名: " << s.name << ", 学号: " << s.id << ", 成绩: " << s.score << endl;
            return;
        }
    }
    cout << "未找到该学生!" << endl;
}

// 统计平均成绩
float calculateAverageScore(const vector<Student>& students) {
    if (students.empty()) return 0;
    float sum = 0;
    for (const auto& s : students) {
        sum += s.score;
    }
    return sum / students.size();
}

int main() {
    vector<Student> students;
    string filename = "students.dat";

    // 录入学生信息
    inputStudents(students);

    // 将学生信息写入文件
    writeStudentsToFile(students, filename);

    // 从文件中读取学生信息
    vector<Student> readStudents = readStudentsFromFile(filename);

    // 查询学生信息
    queryStudent(readStudents);

    // 统计平均成绩
    float averageScore = calculateAverageScore(readStudents);
    cout << "学生的平均成绩为: " << averageScore << endl;

    return 0;
}

这个示例程序包含了学生信息的录入、写入文件、从文件读取、查询和统计平均成绩等功能。通过这个示例,我们可以看到如何将不同的文件操作函数组合起来,实现一个完整的应用程序。

15. 性能优化建议

在进行文件操作时,性能是一个需要考虑的重要因素。以下是一些性能优化的建议:
- 批量读写 :尽量使用 write() read() 函数进行批量数据的读写,避免频繁的单个字符或小数据块的读写操作,这样可以减少磁盘I/O次数,提高性能。例如:

int arr[1000];
ofstream fout("test.dat", ios_base::out | ios_base::trunc | ios_base::binary);
fout.write((char*)arr, sizeof(arr));
  • 合理使用缓冲区 :利用流缓冲区的特性,减少磁盘访问。例如,在写入大量数据时,让缓冲区自动填满后再进行刷新,而不是频繁手动刷新。
  • 及时关闭文件 :在不再使用文件时,及时调用 close() 函数关闭文件,避免占用系统资源。
  • 选择合适的文件打开模式 :根据实际需求选择合适的文件打开模式,避免不必要的文件截断或覆盖操作。
16. 错误处理与异常处理

在文件操作过程中,可能会出现各种错误,如文件打开失败、读写错误等。为了保证程序的健壮性,需要进行错误处理和异常处理。

#include <iostream>
#include <fstream>
#include <cstdlib>

using namespace std;

int main() {
    ifstream fin("nonexistent.txt");
    if (!fin) {
        cerr << "文件打开失败!" << endl;
        return EXIT_FAILURE;
    }
    // 进行文件读取操作
    // ...
    fin.close();
    return 0;
}

在上述代码中,通过检查 ifstream 对象的状态,判断文件是否成功打开。如果打开失败,使用 cerr 输出错误信息,并返回 EXIT_FAILURE 表示程序异常终止。

另外,还可以使用异常处理机制来处理文件操作中的错误。例如:

#include <iostream>
#include <fstream>
#include <stdexcept>

using namespace std;

void readFile(const string& filename) {
    ifstream fin(filename);
    if (!fin) {
        throw runtime_error("文件打开失败!");
    }
    // 进行文件读取操作
    // ...
    fin.close();
}

int main() {
    try {
        readFile("nonexistent.txt");
    } catch (const exception& e) {
        cerr << "发生异常: " << e.what() << endl;
        return EXIT_FAILURE;
    }
    return 0;
}

在这个示例中,定义了一个 readFile 函数,在函数内部如果文件打开失败,抛出一个 runtime_error 异常。在 main 函数中,使用 try-catch 块捕获异常,并输出错误信息。

17. 总结

通过本文的介绍,我们详细学习了C++中文件操作的相关知识,包括流的概念、流的状态、文本文件和二进制文件的区别、文件的输入输出、流缓冲区、文件打开模式、文件处理方式、文件结束标志、重定向以及读写文本文件和二进制文件的方法等。同时,通过大量的示例代码和练习,我们可以更好地掌握这些知识,并将其应用到实际的编程项目中。

在实际应用中,需要根据具体的需求选择合适的文件操作方式和函数,同时要注意错误处理和性能优化。希望本文能帮助你更好地理解和掌握C++文件操作,提高编程能力。

总结与展望

C++的文件操作功能强大且灵活,能够满足各种不同的应用场景。通过合理运用文件操作知识,可以实现数据的持久化存储、数据的交换和共享等功能。

未来,随着计算机技术的不断发展,文件操作的需求也会越来越多样化和复杂化。例如,在大数据、人工智能等领域,需要处理海量的数据,对文件操作的性能和效率提出了更高的要求。因此,我们需要不断学习和探索新的文件操作技术和方法,以适应不断变化的需求。

同时,我们也可以结合其他编程语言和工具,进一步拓展文件操作的功能和应用范围。例如,使用Python进行数据处理和分析,然后将结果存储到文件中,再使用C++进行高性能的文件读写操作。

总之,掌握C++文件操作是每个C++程序员必备的技能之一,希望大家在今后的学习和工作中,能够灵活运用这些知识,开发出更加优秀的程序。

流程图示例

graph TD;
    A[开始] --> B[录入学生信息];
    B --> C[将学生信息写入文件];
    C --> D[从文件中读取学生信息];
    D --> E[查询学生信息];
    D --> F[统计平均成绩];
    E --> G[结束];
    F --> G;

表格总结

操作类型 相关函数 用途
文件打开 open() 打开文件并关联流对象
文件关闭 close() 关闭打开的文件,刷新缓冲区
写入数据 write() 向文件写入二进制数据
读取数据 read() 从文件读取二进制数据
定位指针 seekg() seekp() 移动文件指针到指定位置
获取指针位置 tellg() tellp() 获取文件指针的当前位置
检查流状态 fail() eof() bad() good() 检查流的状态,判断操作是否成功
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符  | 博主筛选后可见
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值