C++ primer|第 8 章 IO库

       

目录

8.1 IO类

8.1.1 IO 对象无拷贝或赋值

8.1.2 条件状态

8.1.2 节练习

 8.1.3 管理输出缓冲

 8.2 文件输入输出

8.2.1 使用文件流对象

8.2.1 节练习

8.2.2 文件模式

8.3 string 流

8.3.1 使用 istringstream

8.3.1 节练习

 8.3.2 使用 ostringstream

8.3.2 节练习


        C++ 语言不直接处理输入输出,而是通过一组定义在标准库中的类型来处理 IO。这些类型支持从设备读取数据、向设备写入数据的 IO操作,设备可以是文件、控制台窗口等。还有一些类型允许内存IO,即,从 string 读取数据,向 string 写入数据。

        我们已经使用过的 IO 库设施有:

  • istream(输入流)类型,提供输入操作。
  • ostream(输出流)类型,提供输出操作。
  • cin,一个 istream 对象,从标准输入读取数据。
  • cout,一个 ostream对象,从标准输出写入数据。
  • cerr,一个 ostream 对象,通常用于输出程序错误消息,写入到标准错误。
  • >> 运算符,用来从一个 istream 对象读取输入数据。
  • << 运算符,用来向一个 ostream 对象写入数据。
  • getline 函数,从一个给定的 istream 读取一行数据,存入一个给定的 string 对象中。

8.1 IO类

        为了支持不同种类的 IO 处理操作,标准库还定义了其他一些 IO 类型。

IO 库类型和头文件
头文件        类型
iostreamistream,wistream 从流读取数据
ostream,wostream 向流写入数据
iostream,wiostream 读写流
fstreamifstream,wifstream 从文件读取数据
ofstream,wofstream 向文件写入数据
fstream,wfstream 读写文件
sstreamistringstream,wistringstream 从 string 读取数据
ostringstream,wostringstream 向 string 写入数据
stringstream ,wstringstream 读写 string

IO 类型间的关系

        概念上,设备类型和字符大小都不会影响我们要执行的 IO 操作。例如,我们可以用 >> 读取数据,而不用管是从 一个控制台窗口,一个磁盘文件,还是一个 string 读取。类似地,也不用管读取的字符能存入一个 char 对象内,还是需要一个 wchar_t 对象来存储。

        标准库使我们能忽略这些不同类型的流之间的差异,这是通过 继承机制(inheritance)实现的。

        简单地说,继承机制使我们可以声明一个特定的类继承自另一个类。我们通常可以将一个派生类(继承类)对象当作基类(所继承的类)对象来使用。

8.1.1 IO 对象无拷贝或赋值

        我们不能拷贝或对 IO 对象赋值:

ofstream out1, out2;
out1 = out2;    //错误:不能对流对象赋值
ofstream print(ofstream);    //错误:不能初始化 ofstream 参数
out2 = print(out2);    //错误:不能拷贝流对象

        由于不能拷贝 IO 对象,因此我们也不能将形参或返回类型设置为流类型。进行 IO 操作的函数通常以引用方式传递和返回流。读写一个 IO 对象会改变其状态,因此传递和返回的引用不能是 const 的。

8.1.2 条件状态

        下表列出了 IO 类所定义的一些函数和标志,可以帮助我们访问和操纵流的条件状态(condition state)。

IO 库条件状态
strm::iostatestrm 是一种 IO 类型。iostate 是一种机器相关的类型,提供了表达条件状态的完整功能。
strm::badbitstrm::badbit 用来指出流已崩溃
strm::failbitstrm::failbit 用来指出一个 IO 操作失败了
strm::eofbitstrm::eofbit 用来指出流到达了文件结束
strm::goodbitstrm::goodbit 用来指出流未处于错误状态。此值保证为 0
s.eof()若 s 的  eofbit 置位,则返回 true
s.fail()若流 s  的 failbit 或 badbit 置位,则返回 true
s.bad()若流 s 的 badbit 置位,则返回 true
s.good()若流 s 处于有效状态,则返回 true
s.clear()若流 s 中所有条件状态位复位,将流的状态设置为有效。返回 void
s.clear(flags)根据给定的 flags 标志位,将流 s 中对应条件状态位复位。flags 的类型为 strm::iostate。返回 void
s.setstate(flags)根据给定的 flags 标志位,将流 s 中对应条件状态位复位。flags 的类型为 strm::iostate。返回 void
s.rdstate()返回流 s 的当前状态,返回值类型为 strm::iostate

        一个流一旦发生错误,其上后续的 IO 操作都会失败。只有当一个流处于无错状态时,才可以从它读取数据。因此,代码通常应该在使用一个流之前检查它是否处于良好状态。确定一个流对象的状态的最简单的方法是将它当作一个条件使用:

while(cin >> word)
    //读取成功......

        while 循环检查 >> 表达式返回的流的状态。如果操作成功,流保持有效状态,则条件为真。

查询流的状态

        

管理条件状态

        流对象的 rdstate 成员返回一个 iostate 值,对应流的当前状态。setstate 操作将给定条件位置位,表示发生了对应错误。clear 成员是一个重载的成员:它有一个不接受参数的版本,而另一个版本接受一个 iostate 类型的参数。

        clear 不接受参数的版本清除(复位)所有错误标志位。执行 clear() 后,调用 good 会返回 true。我们可以这样使用这些成员:

//记住 cin 的当前状态
auto old_state = cin.rdstate();    //记住 cin 的当前状态
cin.clear();                       //使 cin 有效
process_input(cin);                //使用 cin
cin.setstate(old_state);           //将 cin 置为原有状态

        带参数的 clear 版本接受一个 iostate 值,标志流的新状态。为了复位单一的条件状态位,我们首先用 rdsate 读出当前条件状态,然后用位操作将所需位复位来生成新的状态。例如,下面的代码将 failbit 和 badbit 复位,但保持 eofbit 不变。

//复位 failbit 和 badbit ,保持其他标志位不变
cin.clear(cin.rdstate() & ~cin.failbit & ~cin.badbit);

8.1.2 节练习

练习 8.1:编写函数,接受一个 istream& 参数,返回值类型也是 istream&。此函数须从给定流中读取数据,直至遇到文件结束标识时停止。它将读取的数据打印在标准输出上。完成这些操作后,在返回流之前,对流进行复位,使其处于有效状态。

//练习8.1
istream& f(istream& in)
{
	int v;
	//直至遇到文件结束符才停止读取
	while (in >> v, !in.eof())
	{
		if (in.fail())
		{
			cerr << "数据错误,请重试:" << endl;
			in.clear();
			in.ignore(100, '\n');
			continue;
		}
		cout << v << endl;
	}
	in.clear();
	return in;
}

练习 8.3:什么情况下,下面的 while 循环会终止?

while(cin >> i)    /* ... */

        遇到了文件结束符,或者遇到了 IO 流错误,或者读入了无用的数据。

 8.1.3 管理输出缓冲

        每个输出都管理一个缓冲区,用来保存程序读写的数据。例如,如果执行下面的代码:

    os << "please enter a value: ";

文本可能立即打印出来,但也可能被操作系统保存在缓冲区,随后再打印。有了缓冲机制,操作系统就可以将程序的多个输出操作组合成单一的系统级写操作。由于设备的写操作可能很耗时,允许操作系统组合为单一的设备写操作可以带来很大的性能提升

        导致缓冲刷新(即,数据真正写到输出设备或文件)的原因有很多:

  • 程序正常结束,作为 main 函数的 return 操作的一部分,缓冲刷新被执行。
  • 我们可以使用操纵符如 endl 来显式刷新缓冲区。
  • 在每个输出操作之后,我们可以用操作符 unitbuf 设置流的内部状态,来清空缓冲区。默认情况下,对 cerr 是何止 unitbuf 的,因此写到 cerr 的内容都是立即刷新的。
  • 一个输出流可能被关联到另一个流。在这种情况下,当读写被关联到流时,关联到的流的缓冲区会被刷新。例如,默认情况下, cin 和 cerr 都关联到 cout。因此,读 cin 或写 cerr 逗号导致 cout 的缓冲区被刷新。

刷新输出缓冲区

        我们已经使用过操纵符 endl,它完成换行并刷新缓冲区的工作。IO 库中还有两个类似的操纵符:flush 和 ends。flush  刷新缓冲区,但不输出任何额外的字符;ends 向缓冲区插入一个空字符,然后刷新缓冲区。

unitbuf 操纵符

        如果想在每次输出操作后都刷新缓冲区,我们可以使用 unitbuf 操纵符。它告诉流在接下来的每次写操作之后都进行一次 flush 操作。而 nounitbuf 操纵符则重置流,使其恢复使用正常的系统管理的缓冲区刷新机制。       

cout << unitbuf;    //所有输出操作后都会立即刷新缓冲区
//任何输出都立即刷新,无缓冲
cout << nounitbuf;    //回到正常的缓冲方式

        警告:如果程序崩溃,输出缓冲区不会被刷新


        如果程序异常终止,输出缓冲区是不会被刷新的。当一个程序崩溃后,它所输出的数据很可能停留在输出缓冲区中等待打印。

        当调试一个已经崩溃的程序时,需要确认那些你认为已经输出的数据确实已经刷新了。否则,可能将大量时间浪费在追踪代码为什么没有执行上,而实际上代码已经执行了,只是程序崩溃后缓冲区没有被刷新,输出数据被挂起没有打印而已。

 关联输入和输出流

        当一个输入流被关联到一个输出流时,任何试图从输入流读取数据的操作都会先刷新关联的输出流。标准库将 cout  和 cin 关联在一起,因此下面语句:

cin >> ival;

导致 cout 的缓冲区被刷新。

        交互式系统通常应该关联输入流和输出流。这意味着所有输出,包括用户提示信息,都会在读操作之前被打印出来。

 8.2 文件输入输出

        头文件 fstream 定义了三个类型来支持文件 IO:ifstream 从一个给定文件读取数据,ofstream 向一个给定文件写入数据,以及 fstream 可以读写给定文件。

fstream 特有的操作
fstream fstrm;创建一个未绑定的文件流。fstream是头文件 fstream 中定义的一个类型
fstream fstrm(s);创建一个 fstream,并打开名为 s 的文件。s 可以是 string 类型,或者是一个指向 C 风格字符串的指针。这些构造函数都是 explicit 的。默认的文件模式 mode 依赖于 fstream 的类型。
fstream fstrm(s,mode);与前一个构造函数类似,但按指定 mode 打开文件
fstrm.open(s)打开名为 s 的文件,并将文件与 fstrm 绑定。 s 可以是一个 string 或一个指向 C 风格字符串的指针。默认的文件 mode 依赖于 fstream 的类型。返回 void
fstrm.close()关闭与 fstrm 绑定的文件。返回 void
fstrm.is_open()返回一个 bool 值,指出与 fstrm 关联的文件是否成功打开并且尚未关闭

8.2.1 使用文件流对象

        当我们想要读写一个文件时,可以定义一个文件流对象,并将对象与文件关联起来。每个文件流都定义了一个名为 open 的成员函数,它完成一些系统相关的操作,来定位给定的文件,并视情况打开读或写模式。

        创建文件流对象时,我们可以提供文件名(可选的)。如果提供了一个文件名,则 open 会自动被调用:

ifstream in(ifile);    //构造一个 ifstream 并打开给定文件
ofstream out;          //输出文件流未关联到任何文件  

用 fstream 代替 iostream&

        在要求使用基类型对象的地方,我们可以用继承类型的对象来替代。这意味着,接受一个 iostream 类型引用(或指针)参数的函数,可以用一个对应的 fstream(或 sstream)类型来调用。也就是说,如果有一个函数接受一个 ostream& 参数,我们在调用这个函数时,可以传递给它一个 ofstream 对象,对 istream& 和 ifstream 也是类似的。

成员函数 open 和 close

        如果我们定义了一个空文件流对象,可以随后调用 open 来将它与文件关联起来:

ifstream in(ifile);    //构筑一个 ifstream 并打开给定文件
ofstream out;    //输出文件流未与任何文件相关联
out.open(ifile + ".copy");    //打开指定文件

        如果调用失败,failbit 会被置位。因为调用 open  可能失败,进行 open 是否成功的检测通常是一个好习惯。

if(out)    //检查 open 是否成功
           //open 成功,我们可以使用文件了 

        一旦一个文件流已经打开,它就保持与对应文件的关联。实际上,对一个已经打开的文件流调用 open 会失败,并会导致 failbit 被置位。

        为了将文件流关联到另外一个文件,必须首先关闭已经关联的文件。一旦文件成功关闭,就可以打开新的文件。

        如果 open 成功,则 open 会设置流的状态,使得 good() 为 true。

自动构造和析构

        

8.2.1 节练习

//练习 8.4:编写函数,以读模式打开一个文件,将其内容读入到一个 string 的 vector 中,
//将每一行作为一个独立的元素存于 vector 中。
int f()
{
	ifstream in("data");	//打开文件
	if (!in)
	{
		cerr << "无法打开输入文件" << endl;
		return -1;
	}

	string line;
	vector<string> words;
	while (getline(in, line))//从文件中读取一行
	{
		words.push_back(line);//添加到 vector 中
	}

	in.close();				//输入完毕,关闭文件

	vector<string>::const_iterator it = words.begin();//迭代器
	while (it != words.end())
	{
		cout << *it << endl;
		++it;
	}
	return 0;
}

练习 8.5:重写上面的程序,将每个单词作为一个独立的元素进行存储。

//将 while(getline(in, line))改为
while(in >> line)

8.2.2 文件模式

        每个流都有一个关联的文件模式(file mode),用来指出如何使用文件。

文件模式
in以读方式打开
out以写方式打开
app每次写操作前均定位到文件末尾
ate打开文件后立即定位到文件末尾
trunc截断文件
binary以二进制方式进行 IO

        无论用哪种方式打开文件,我们都可以指定文件模式,调用 open 文件时可以,用一个文件名初始化流来隐式打开文件时也可以。指定文件模式有如下限制:

  • 只可以对 ofstream 或 fstream 对象设定 out 模式。
  • 只可以对 ifstream 或 fstream 对象设定 in 模式。
  • 只有当 out 也被设定时才可设定 trunc 模式。
  • 只要 trunc 没被设定,就可以设定 app 模式。在 app 模式下,即使没有显式指定 out 模式,文件也总是以输出方式被打开。
  • 默认情况下,即使我们没有指定 trunc ,以  out 模式打开的文件也会被截断。为了保留以 out 模式打开的文件的内容,我们必须同时指定 app 模式,这样只会将数据追加到文件末尾;或者同时指定 in 模式,即打开文件同时进行读写操作。
  • ate 和 binary 模式可用于任何类型的文件流对象,且可以与其他任何文件模式组合使用。

        当我们未指定文件模式时,就使用默认模式。与 ifstream 关联的文件默认以 in 模式打开;与 ofstream 关联的文件默认以 out 模式打开;与 fstream 关联的文件默认以 in 和 out 模式打开。

以 out 模式打开文件会丢弃已有数据

        默认情况下,当我们打开一个 ofstream 时,文件的内容会被丢弃。阻止一个 ofstream 清空给定内容的方法是同时指定 app 模式或 in 模式。

每次调用 open 时都会确定文件模式

        对于一个给定流,每当打开文件时,都可以改变其文件模式。

ofstream out;    //未指定文件打开模式
out.open("scratchpad");    //模式隐含设置为输出和截断
out.close();    //关闭 out,以便我们将其用于其他文件
out.open("precious", ofstream::app);    //模式为输出和追加
out.close();

8.3 string 流

        sstream 头文件定义了三个类型来支持内存 IO,这些类型可以向 string 写入数据,从 string 读取数据,就像 string 是一个 IO 流一样。

        istringstream 从 string 读取数据,ostringstream 向 string 写入数据,而头文件 stringstream 既可从 string 读数据也可向 string 写数据。

stringstream 特有的操作
sstream strm;strm 是一个未绑定的 stringstream 对象。sstream 是头文件 sstream 中定义的一个类型
sstream strm(s);strm 是一个 sstream 对象,保存 string s 的一个拷贝。此构造函数是 explicit 的
strm.str()返回 strm 所保存的 string 的拷贝
strm.str(s)将 string s 拷贝到 strm 中。返回 void

8.3.1 使用 istringstream

        当我们的某些工作是对整行文本进行处理,而其他一些工作是处理行内的单个单词时,通常可以使用 istringstream。

        举个例子,假定有一个文件,列出了一些人和他们的电话号码。某些人只有一个号码,而另一些人则有多个——家庭电话、工作电话、移动电话等。我们的输入文件看起来可能是这样的:

        morgan 20128444 5556356

        drew 67666634

        lee 1253625 28689265 9008873800

        文件中每条记录都以一个人名开始,后面跟随一个或多个电话号码。我们首先定义一个简单的类来描述输入数据:

struct PersonIno
{
	string name;
	vector<string> phones;
};

        我们的程序会读取数据文件,并创建一个 PersonInfo 的 vector。vector 中每个元素对应文件中的一条记录。我们在一个循环中处理输入数据,每个循环步读取一条记录,提取出一个人名和若干电话号码:

void f1()
{
	string line, word;			//分别保存来自输入的一行和单词
	vector<PersonInfo> people;	//保存来自输入的所有记录
	//逐行从输入读取数据,直至 cin 遇到文件尾(或其他错误)
	while (getline(cin, line))
	{
		PersonInfo info;			//创建一个保存此记录数据的对象
		istringstream record(line);	//将记录绑定到刚读入的行
		record >> info.name;	//读取名字
		while (record >> word)		//读取电话号码
			info.phones.push_back(word);//保持它们
		people.push_back(info);		//将此记录追加到 people 末尾
	}

}

8.3.1 节练习

练习 8.9:使用你为 8.1.2 节第一个练习所编写的函数打印一个 istringstream 对象的内容。

#include<iostream>
#include<sstream>
#include<string>
#include<stdexcept>

using namespace std;

istream & f(istream &in)
{
	string v;
	while (in >> v, !in.eof())//直到遇到文件结束符才停止读取
	{
		if (in.bad())
		{
			throw runtime_error("IO流错误");
		}
		if (in.fail())
		{
			cerr << "数据错误,请重试:" << endl;
			in.clear();
			in.ignore(100, '\n');
			continue;
		}
		cout << v << endl;
	}
	in.clear();
	return in;
}

int main()
{
	ostringstream msg;
	msg << "C++ primer 第五版" << endl;
	istringstream in(msg.str());
	f(in);
	return 0;
}

练习 8.10:

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

using namespace std;

//练习8.10:编写程序,将来自一个文件的行保存在一个 vector<string>中。
//然后使用一个 istringstream 从 vector读取元素,每次读取一个单词

int main()
{
	ifstream in("data");
	if (!in)
	{
		cerr << "无法打开输入文件" << endl;
		return -1;
	}

	string line;
	vector<string> words;
	while (getline(in, line))	//从文件中读取一行
	{
		words.push_back(line);	//	添加到 vector 中
	}
	in.close();					//输入完毕,关闭文件

	vector<string>::const_iterator it = words.begin();
	while (it != words.end())
	{
		istringstream line_str(*it);
		string word;
		while (line_str >> word)	//通过 istringstream 从 vector 中读取数据
			cout << word << " ";
		cout << endl;
		++it;
	}
	return 0;
}

练习 8.11:

#include<iostream>
#include<string>
#include<vector>
#include<sstream>

using namespace std;

//练习8.11:本节的程序在外层 while 循环中定义了 istringstream 对象。如果 recoed 对象
//定义在循环之外,你需要对程序做怎样的修改?重写程序,将record 的定义移到 while 循环
//之外,验证你设想的修改方法是否正确
struct PersonInfo
{
	string name;
	vector<string> phones;
};
int main()
{
	string line, word;			//分别保存来自输入的一行和单词
	vector<PersonInfo> people;	//保存来自输入的所有记录
	istringstream record;

	//逐行从输入读取数据,直至 cin 遇到文件尾(或其他错误)
	while (getline(cin, line))
	{
		PersonInfo info;			//创建一个保存此记录数据的对象
		record.clear();				//重复使用字符串流时,每次都要调用 clear
		record.str(line);			//将记录绑定到刚读入的行
		record >> info.name;		//读取名字
		while (record >> word)		//读取电话号码
			info.phones.push_back(word);//保持它们
		people.push_back(info);			//将此记录追加到 people 末尾
	}
	return 0;
}

练习 8.12:我们为什么没有在 PersonInfo 中使用类内初始化?

        由于每个人的电话号码数量不固定,因此更好的方式不是通过类内初始化指定人名和所有电话号码,而是在缺省初始化之后,在程序中设置人名并逐个添加电话号码。

 8.3.2 使用 ostringstream

        当我们逐步构造输出,希望最后打印时,ostringstream 是很有用的。例如,对于上一节的例子,我们可能想逐个验证电话号码并改变其格式。

        由于我们不希望输出有无效电话号码的人,因此对每个人,直到验证完所有电话号码后才可以进行输出操作。但是,我们可以先将输出内容“写入”到一个内存 ostringstream 中。

        

ostringstream os;	
for (const auto &entry : people)			//对 people 中每一项
	{
		ostringstream formatted, badNums;		//每个循环步创建的对象
		for (const auto &nums : entry.phones)	//对每个数
		{
			if (!valid(nums))
			{
				badNums << " " << nums;			//将数的字符串形式存入 badNums
			}
			else
			{
				//将格式化的字符串写入 formatted
				formatted << " " << format(nums);
			}
		}
		if (badNums.str().empty())				//没有错误的数
		{
			os << entry.name << " "					//打印名字
				<< formatted.str() << endl;			//和格式化的数
		}
		else
		{
			//否则,打印名字和错误的数
			cerr << "input error: " << entry.name
				<< " invalid number(s)" << badNums.str() << endl;
		}
		

8.3.2 节练习

练习 8.13:重写本节的电话号码程序,从一个命名文件而非 cin 读取数据。

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

using namespace std;

struct PersonInfo
{
	string name;
	vector<string> phones;
};
string format(const string &s)
{
	return s;
}
bool valid(const string &s)
{
	return true;
}
int main(int argc,char *argv[])
{
	string line, word;			//分别保存来自输入的一行和单词
	vector<PersonInfo> people;	//保存来自输入的所有记录
	istringstream record;

	if (argc != 2)
	{
		cerr << "请给出文件名" << endl;
		return -1;
	}
	ifstream in(argv[1]);
	if (!in)
	{
		cerr << "无法打开输入文件" << endl;
		return -1;
	}
	while (getline(in, line))
	{
		PersonInfo info;			//创建一个保存此记录数据的对象
		record.clear();				//重复使用字符串流时,每次都要调用 clear
		record.str(line);			//将记录绑定到刚读入的行
		record >> info.name;		//读取名字
		while (record >> word)		//读取电话号码
			info.phones.push_back(word);//保持它们
		people.push_back(info);			//将此记录追加到 people 末尾
	}
	ostringstream os;
	for (const auto &entry : people)			//对 people 中每一项
	{
		ostringstream formatted, badNums;		//每个循环步创建的对象
		for (const auto &nums : entry.phones)	//对每个数
		{
			if (!valid(nums))
			{
				badNums << " " << nums;			//将数的字符串形式存入 badNums
			}
			else
			{
				//将格式化的字符串“写入“ formatted
				formatted << " " << format(nums);
			}
		}
		if (badNums.str().empty())				//没有错误的数
		{
			os << entry.name << " "					//打印名字
				<< formatted.str() << endl;			//和格式化的数
		}
		else
		{
			//否则,打印名字和错误的数
			cerr << "input error: " << entry.name
				<< " invalid number(s)" << badNums.str() << endl;
		}
		
	}

	return 0;
}

练习 8.14:我们为什么将 entry 和 nums 定义为 const auto&?

        这两条语句分别使用范围 for 语句枚举 peoplel 中所有项(人)和每项的 people 中的所有项(电话号码)。使用 const 表明在循环中不会改变这些项的值; auto 是请求编译器依据 vector 元素类型来推断出 entry 和 nums 的类型,既简化代码又避免出错;使用引用的原因是,people 和 phones 的元素分别是结构对象和字符串对象,使用引用可避免对象拷贝。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值