一、C 语言的输入与输出
在 C 语言中,我们用到的最频繁的输入输出方式就是scanf()
与printf()
。
scanf()
:从标准输入设备(键盘)读取数据,并将值存放在变量中。printf()
:将指定数据输出到标准输出设备(显示器)。注意宽度输出和精度输出控制。
C 语言借助了相应的缓冲区来进行输入与输出。如下图所示:
对输入输出缓冲区的理解:
① 可以屏蔽掉低级 I/O 的实现,低级 I/O 的实现依赖于操作系统本身内核的实现,所以如果能够屏蔽这部分的差异,就可以很容易写出可移植的程序。
② 可以使用这部分的内容实现 “行” 读取的行为,对于计算机而言是没有 “行” 这个概念的,有了这部分,就可以定义 “行” 的概念,然后解析缓冲区的内容,返回一个 “行” 。
二、流是什么
“流” 即是流动的意思,是物质从一处向另一处流动的过程,是对一种有序连续且具有方向性的数据(其单位可以是 bit, byte, packet)的抽象描述。
C++ 流是指信息从外部输入设备(如键盘)向计算机内部(如内存)输入和从内存向外部输出设备(显示器)输出的过程。这种输入输出的过程被形象地比喻为 “流” 。它的特性是:有序连续、具有方向性。
为了实现这种流动,C++ 定义了 I/O 标准类库,这些每个类都称为流/流类,用以完成某方面的功能。
三、C++ IO 流
C++ 系统实现了一个庞大的类库,其中 ios 为基类,其他类都是直接或间接派生自 ios 类。
1. C++ 标准 IO 流
C++ 标准库提供了 4 个全局流对象 cin 、cout 、cerr 、clog ,使用 cout 进行标准输出,即数据从内存流向控制台(显示器)。使用 cin 进行标准输入即数据通过键盘输入到程序中,同时 C++ 标准库还提供了 cerr 用来进行标准错误的输出,以及 clog 进行日志的输出,从上图可以看出,cout 、cerr 、clog 是 ostream 类的三个不同的对象,因此这三个对象现在基本没有区别,只是应用场景不同。
注:
① cin 和 cout 可以直接输入和输出内置类型数据,原因:标准库已经将所有内置类型的输入和输出全部重载了。
#include <iostream>
using namespace std;
int main()
{
int i = 1;
double d = 2.22;
//输入
cin >> i; // -> cin.operator>>(i); int类型
cin >> d; // -> cin.operator>>(d); double类型
cin >> i >> d; // 其实是两个operator>>函数的调用
//输出
cout << i; // -> cout.operator<<(i); int类型
cout << d; // -> cout.operator<<(d); double类型
cout << i << d; // 其实是两个operator<<函数的调用
return 0;
}
② 对于自定义类型,如果要支持 cin 和 cout 的标准输入输出,需要对 >> 和 << 进行重载。其实 C++ 的 IO 流,使用面向对象 + 运算符重载的方式,能更好地兼容自定义类型,从而对内置类型和自定义类型,都使用一样的方式,去流提取和流插入数据。
比如标准库里的 string 类型:
③ 在线OJ中的输入和输出:
#include <iostream>
#include <string>
using namespace std;
int main()
{
// C++:多组输入
string str;
while (cin >> str) // -> operator>>(cin, str)
{
cout << str << endl;
}
// C:多组输入
char a[128];
while (scanf("%s", a) != EOF)
{
cout << a << endl;
}
return 0;
}
上面的字符串输入,都是遇到空格或换行符结束本轮输入。
为什么在 while 那里,返回的 istream 类型对象能做逻辑判断呢?
原因是进行了类型运算符重载 operator bool() 。当返回的 istream 类型对象做条件逻辑判断时,会调用类型运算符重载函数 operator bool() ,如果接收流失败,或者有结束标志,则返回 false 。
// bool类型运算符重载的例子
class B
{
public:
operator bool() // 进行了bool类型运算符重载
{
return _a != 0;
}
int _a;
};
int main()
{
B b;
while (b) // -> b.operator bool()
{
cin >> b._a;
}
return 0;
}
2. C++ 文件 IO 流
C 语言文件 IO:
// 使用文件IO流用文本及二进制方式演示读写配置文件
struct ServerInfo
{
char _ip[32]; // ip
int _port; // 端口
};
//fopen/fclose
//二进制读写 fwrite/fread
//文本读写 fprintf/fscanf
void TestC_W_Bin()
{
FILE* fout = fopen("test.bin", "wb");
assert(fout);
ServerInfo info = { "127.0.0.1", 80 };
fwrite(&info, sizeof(info), 1, fout);
fclose(fout);
}
void TestC_R_Bin()
{
FILE* fin = fopen("test.bin", "rb");
assert(fin);
ServerInfo info;
fread(&info, sizeof(info), 1, fin);
fclose(fin);
printf("%s:%d\n", info._ip, info._port);
}
void TestC_W_Text()
{
FILE* fout = fopen("test.txt", "w");
assert(fout);
ServerInfo info = { "127.0.0.1", 80 };
fprintf(fout, "%s %d", info._ip, info._port);
// 注意fscanf和scanf类似,在读多个数据的时候,都以' '或'\n'区分
fclose(fout);
}
void TestC_R_Text()
{
FILE* fin = fopen("test.txt", "r");
assert(fin);
ServerInfo info;
fscanf(fin, "%s%d", info._ip, &info._port);
fclose(fin);
printf("%s:%d\n", info._ip, info._port);
}
int main()
{
二进制读写
//TestC_W_Bin();
//TestC_R_Bin();
文本读写
//TestC_W_Text();
//TestC_R_Text();
return 0;
}
C++ 文件 IO 流:
C++ 根据文件内容的数据格式将文件分为二进制文件和文本文件。
使用文件流对象操作文件的一般步骤:
① 定义一个文件流对象。
② 使用文件流对象的成员函数打开一个磁盘文件,使得文件流对象和磁盘文件之间建立联系。
③ 使用流提取和流插入运算符或使用成员函数对文件进行读写操作。
④ 关闭文件。
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)
{}
private:
int _year;
int _month;
int _day;
};
ostream& operator<<(ostream& out, const Date& d)
{
out << d._year << " " << d._month << " " << d._day;
return out;
}
istream& operator>>(istream& in, Date& d)
{
in >> d._year >> d._month >> d._day;
return in;
}
struct ServerInfo
{
char _ip[32]; // ip
int _port; // 端口
Date _d; // 日期
};
class ConfigManager
{
public:
ConfigManager(const char* filename)
:_filename(filename)
{}
void WriteBin(const ServerInfo& info)
{
ofstream ofs(_filename.c_str(), ios_base::out | ios_base::binary);
ofs.write((const char*)&info, sizeof(ServerInfo));
}
void ReadBin(ServerInfo& info)
{
ifstream ifs(_filename.c_str(), ios_base::in | ios_base::binary);
ifs.read((char*)&info, sizeof(ServerInfo));
}
void WriteText(const ServerInfo& info)
{
ofstream ofs(_filename.c_str());
ofs << info._ip << " " << info._port << " " << info._d;
// 注意ifs,在读多个数据的时候,都以' '或'\n'区分
}
void ReadText(ServerInfo& info)
{
ifstream ifs(_filename.c_str());
ifs >> info._ip >> info._port >> info._d;
}
private:
string _filename;
};
int main()
{
//ServerInfo winfo = { "127.0.0.1", 80, { 2022,10,10 } };
//ServerInfo rinfo;
二进制读写
//ConfigManager cm("config.bin");
//cm.WriteBin(winfo);
//cm.ReadBin(rinfo);
//cout << rinfo._ip << ":" << rinfo._port << " " << rinfo._d << endl;
文本读写
//ConfigManager cm("config.txt");
//cm.WriteText(winfo);
//cm.ReadText(rinfo);
//cout << rinfo._ip << ":" << rinfo._port << " " << rinfo._d << endl;
return 0;
}
3. C++ 字符串流
C++ 字符串流的其中一个用途是序列化和反序列化结构数据:
struct PersonInfo
{
string _name; // 名字
int _age; // 年龄
Date _d; // 日期
};
int main()
{
// 序列化:结构信息 -> 字符串
PersonInfo winfo = { "张三", 18, { 2022,10,10 } };
ostringstream oss;
oss << winfo._name << " " << winfo._age << " " << winfo._d;
string str = oss.str();
cout << str << endl;
// 反序列化:字符串 -> 结构信息
PersonInfo rinfo;
istringstream iss(str);
iss >> rinfo._name >> rinfo._age >> rinfo._d;
cout << rinfo._name << endl;
cout << rinfo._age << endl;
cout << rinfo._d << endl;
return 0;
}
注:
① stringstream 实际是在其底层维护了一个 string 类型的对象用来保存结果。
② stringstream 的成员函数str()
的作用是,返回 stringstream 底层的 string 对象。