类型转换运算符(循环输入原理)
循环输入
有些oj题会要求我们实现多组输入,也就是像下面这样要在外面套一层循环。
循环输入时,在vs编译器下输入 ctrl + z 然后回车结束输入。
while (cin >> a)
{
}
while (cin >> a >> b >> c)
{
}
while (cin >> str)
{
}
在我们输入 ctrl + z 之后循环是怎么停止的呢?
我们知道,string 类 重载了 >>
运算符:
istream& operator>> (istream& is, string& str);
实际上,当它返回 istream&
类型后会发生隐式类型转换,将 istream
转换为 bool
类型。这种将类类型转换为内置类型的操作可以通过定义类型转换运算符来实现。
**类型转换运算符(conversion operator)**是类的一种特殊成员函数,它负责将一个类类型转换成其他类型。类型转换函数的一般形式如下所示:
operator type() const;
其中 type 是任意可以作为函数返回类型的类型(除了 void
之外)
类型转换运算符既没有显式的返回类型,也没有形参,而且必须定义成类的成员函数,类型转换运算符通常不应该改变待转换对象的内容,所以一般要用 const
修饰。
例子:
给日期类定义类型转换运算符,如果是2022年的对象就转换成false,否则转换成true
class Date
{
friend ostream& operator << (ostream& out, const Date& d);
friend istream& operator >> (istream& in, Date& d);
public:
Date(int year = 1, int month = 1, int day = 1)
:_year(year)
, _month(month)
, _day(day)
{}
operator bool() const
{
// 如果是2022年就返回false,否则返回true
if (_year == 2022)
return false;
else
return true;
}
private:
int _year;
int _month;
int _day;
};
istream& operator >> (istream& in, Date& d)
{
in >> d._year >> d._month >> d._day;
return in;
}
ostream& operator << (ostream& out, const Date& d)
{
out << d._year << " " << d._month << " " << d._day;
return out;
}
int main()
{
Date d(2022, 10, 12);
if (d) cout << "不是2022";
else cout << "是2022";
return 0;
}
// 结果:是2022
Date 也可以转换为 int,怎么转换可以自己设计:
operator int()
{
return _year + _month + _day;
}
Date d(2022, 10, 12);
int sum = d;
cout << sum;
//结果:2044
所以 istream
类内部一定是实现了 operator bool
,能够保证在接收到 ctrl+z 时返回 false
:ios::operator bool - C++ Reference (cplusplus.com)
文件输入输出
简介
头文件 fstream 定义了三个类型来支持文件IO:
- ifstream 从一个给定文件读取数据(只读)
- ofstream 向一个给定文件写入数据(只写)
- fstream 可以读写给定文件(读写)
这些类型提供的操作和我们之前已经使用过的对象 cin
和 cout
的操作一样。比如,我们可以用IO运算符(<<
和 >>
)来读写文件,可以用 getline
从一个 ifstream
读取数据。
当我们想要读写一个文件时,可以定义一个文件流对象,并将对象与文件关联起来,每个文件流类都定义了一个名为 open 的成员函数,它完成一些系统相关的操作,来定位给定的文件,并视情况打开为读或写模式。
- 创建文件流对象时,如果提供了文件名,则 open 会被自动调用。
- 当一个文件流对象被销毁时,会自动调用 close 关闭文件。
ifstream in(ifile); // 构造一个 ifstream 并打开给定文件
ofstream out; // 输出文件流为关联到任何文件
文件名 ifile
既可以是 string
对象,也可以是 C 风格字符数组。
例子:
读取 test.txt 文件内容并打印
int main()
{
ifstream ifs("test.txt");
while (ifs)
{
char ch = ifs.get();
cout << ch;
}
return 0;
}
文件模式
fstream fstrm(s, mode);
创建一个文件流对象 fstrm
,其中s是文件名,mode为文件模式。
每个流都有一个关联的文件模式,用来指出如何使用文件,如果要指定多个文件模式,就将不同的代表文件模式的变量按位或,然后再传入。
文件模式及含义:
in
以读方式打开out
以写方式打开app
每次写操作前均定位到文件末尾ate
打开文件后立即定位到文件末尾trunc
截断文件binary
以二进制方式进行IO
这些变量可以在对应的文件流类型中找到。
注意:
ifstream
会隐式指定in
模式,我们不能给它设定out
模式,ofstream
会隐式指定out
模式,不能给它设定in
模式- 以
out
模式打开文件会丢弃已有数据,这一点和C语言指定"w"
是一样的,如果要保留已有数据,那么可以显式指定app
或in
模式。
具体用例参考下方的二进制读写
二进制读写与文本文件读写
我们知道,函数形参如果是基类,那么调用函数可以传入其对应的派生类对象。
由于 fstream
是 iostream
的派生类,所以在任何接受 iostream
类型引用(或指针)参数的函数,可以用一个对应的 fstream
(或 sstream
)类型来调用,如果有一个函数接受 ostream&
参数,可以传递 ofstream
对象,对 istream&
和 ifstream
也类似。
<<
和 >>
运算符重载就是这样的函数,所以文件流对象可以像 cin
、cout
一样使用
二进制读写,与文本文件读写:
struct ServerInfo
{
char _address[32];
int _port;
Date _date;
};
struct ConfigManager
{
public:
ConfigManager(const char* filename)
:_filename(filename)
{}
void WriteBin(const ServerInfo& info)
{
ofstream ofs(_filename, ios_base::out | ios_base::binary);
ofs.write((const char*)&info, sizeof(info));
}
void ReadBin(ServerInfo& info)
{
ifstream ifs(_filename, ios_base::in | ios_base::binary);
ifs.read((char*)&info, sizeof(info));
}
void WriteText(const ServerInfo& info)
{
ofstream ofs(_filename);
ofs << info._address << " " << info._port << " " << info._date;
}
void ReadText(ServerInfo& info)
{
ifstream ifs(_filename);
ifs >> info._address >> info._port >> info._date;
}
private:
string _filename; // 配置文件
};
string 流
sstream
头文件定义了三个类型来支持内存 IO,这些类型可以向 string 写入数据,读取数据,就像 string 是一个 IO 流一样。
istringstream
从 string 读取数据ostringstream
向 string 写入数据stringstream
既可以从 string 读取数据,也可以向 string 写入数据
与 fstream 类似,它们也继承自 iostream 头文件中定义的类型。
除了继承来的操作,stringstream 还有一些特有的操作:
sstream strm; // 创建一个未绑定的string流对象,sstream是上述三个类型之一
sstream strm(s); // s是string对象,strm保存s的一个拷贝
strm.str() // 返回strm所保存的string的拷贝
strm.str(s) // 将string s拷贝到strm中,返回void
istringstream 使用
使用 istringstream 可以方便地处理整行文本内的单个单词。
假设我们有这样一段信息:
列出一些人的名字和他们的电话号码,一个人可能有多个电话号码——家庭电话,移动电话等
morgan 2015552368 8625550123
drew 9735550130
lee 6095550132 2015550175 8005550000
对这些信息进行处理并保存:
// 一个人的信息包括名字和电话
struct PersonInfo
{
string name;
vector<string> phones;
};
int main()
{
string line, word; // 分别保存输入的一行和单词
vector<PersonInfo> people; // 保存所有信息
while (getline(cin, line)) // 从标准输入流中按行读取
{
PersonInfo info;
istringstream record(line); // 将string流record绑定到一行的信息
record >> info.name; // 像cin一样使用
while (record >> word)
{
info.phones.push_back(word);
}
people.push_back(info);
}
return 0;
}
ostringstream 使用
对上一个例子,我们想逐个验证电话号码,并改变格式。如果所有号码都是有效的,则将它们改变格式后的信息输出到一个新的文件。如果出现无效号码,则打印一条包含人名和无效号码为的错误信息。
假定我们已经实现验证号码valid和改变格式format的功能
for (const auto& entry : people) // 对每个人
{
ostringstream formatted, badNums; // 分别存放有效号码和无效号码
for (const auto& nums : entry.phones) // 对每个号码
{
if (!valid(nums)) // 如果是无效号码
{
badNums << " " << nums; // 将号码存入badNums
}
else // 如果是有效号码
{
formatted << " " << format(nums);// 格式化后存入formatted
}
}
if (badNums.str().empty()) // 如果没有错误的号码
os << entry.name << " " << formatted.str() << endl; // 打印名字和格式化的号码
else
{
cerr << "input error: " << entry.name << " invalid number(s) " << badNums.str() << endl; // 打印错误信息
}
}
总结:文件流对象和string流对象的使用和cin、cout的使用是差不多的,它们最大的区别就是去向不同,cin、cout是从键盘读,输出到屏幕,文件流对象是从文件读写,string流对象是从string读写。