目录
一、C语言的标准输入输出
对于新手而言,C语言所用的最频繁的输入输出方式就是scanf ()和printf()。
- scanf(): 从输入设备(键盘)获取数据,并将数据存在一些变量中。
- printf(): 将指定的文字/字符串输出至输出设备(屏幕。printf()需留意宽度输出和精度输出的控制)。
将数据从输入设备获取并保存、将数据输出至输出设备,这个过程中借助了输入缓冲区和输出缓冲区。
【Tips】输入/输出缓冲区的功能
- 低级I/O的实现依赖操作系统本身内核的实现,所以如果能够屏蔽这部分的差异,可以很容易写出可移植的程序。通过输入/输出缓冲区,就可以屏蔽掉低级I/O的实现;
- 计算机是没有“行”这个概念的,有了输入/输出缓冲区,就可以定义“行”的概念,只要解析缓冲区的内容,返回一个“行”,就实现了“行”读取的行为。
二、C++的IO流
“流”是流动的意思,指物质从一处向另一处流动的过程,是对一种有序连续且具有方向性的数据(其单位可以是bit、byte、packet)的抽象描述。
C++流是指,信息从输入设备(键盘)向计算机内部(内存)输入,和从计算机内部向输出设备(显示器)输出的过程。这种过程的特点是,连续有序,具有方向性。
C++定义了I/O标准类库来实现这种流动的过程,它是一个庞大的类库,库中每个类都称为流类(或简称流),用以完成某些特定的功能。流和流之间有继承关系,例如ios为基类,istream、ostream、fstream、stringstream等都是直接或间接派生自ios类。
其中,iostream就是标准IO流,ifstream、ofstream、fstream就是文件IO流。
1.标准IO流
iostream就是标准IO流,它继承了istream流和ostream流,同时也继承了istream的cin,ostream的cout、cerr、clog。
cin、cout、cerr、clog是4个全局流对象,用于C++标准输入输出。
- cin:进行标准输入(意思是数据通过键盘输入)到程序中。
- cout:进行标准输出(意思是数据从内存流向控制台/显示器)。
- cerr:进行标准错误的输出。
- clog:进行日志的输出。
cin(流插入)和cout(流提取)可以看作是C++对C语言中scanf()和printf()的改进。scanf()和printf()只能处理内置类型,而cin和cout可以自动识别类型(是以运算符重载的方式实现的),并且面向对象,可以更好支持自定义类型,更形象。
【ps】cin和cout的使用须知
- 空格和回车都可以作为数据之间的分格符,所以多个数据可以在一行输入,也可以分行输 入。但cin无法识别字符型数据之间的空格(因为空格的ASCII码为32),它会自动跳过空白(空格,换行符,制表符),直到遇到非空白字符。同样的,cin能读取的字符串中也不能有空格和回车符;
- cin可以根据类型选择读取,如果输入数据的类型不匹配存储数据的变量的类型,那cin就不会读入数据,而返回一个0放入变量中;
- 使用cin输入时会返回一个istream对象,这个对象最终会隐式类型转换为bool值;
- 从键盘输入时,cin会先将从键盘获取的数据保存在缓冲区中,当cout提取时才从缓冲区中取出。如果输入的数据过多,一次提取不完,就会将数据留在缓冲区中慢慢使用,只有当缓冲区中的数据取完,cin才会获取新的数据;如果输入时输错了,就必须在按下回车键之前修改;
- cin输入的数据类型与cout要提取的数据类型应保持一致,否则会出错(但这个出错只是在流的状态字state中,程序是可以继续的);
- 对于内置类型的数据,cin和cout是可以直接输入和输出的,因为标准库已经为>>和<<实现了相应的重载;
- 对于自定义类型的数据,如果要支持cin和cout的标准输入输出,需要手动对>>和<<进行重载;
- 在线OJ中的输入和输出: 1)对于IO类型的算法,一般都需要循环输入;2)输出:严格按照题目的要求进行,多一个少一个空格都不行;3)连续输入时,vs系列编译器下在输入ctrl+Z时结束。
// 单个元素循环输入
while(cin>>a)
{
// ...
}
// 多个元素循环输入
while(c>>a>>b>>c)
{
// ...
}
// 整行接收
while(cin>>str)
{
// ...
}
//while (cin >> str)可以实现持续的输入,直到ctrl z+换行 结束
int main()
{
string str;
while (cin >> str) // operator>>(cin, str).operator bool()
//cin >> str返回了一个istream对象,最终隐式类型转换为bool值
//这个过程通过从父类继承的operator bool()来实现
//输入结束或输入发生错误返回false,否则返回true
//ctrl z修改了流中的标准,ctrl c发信号结束进程
{
cout << str << endl;
}
return 0;
}
int main()
{
int a, b;
while (cin>>a>>b) // cin.operator>>(a).operator>>(b).operator bool()
{
cout << a << endl;
cout << b << endl;
}
}
//[补]类型转换
class A
{
public:
A(int a)
:_a(a)
{}
//operator()无法重载,原因是被仿函数占用
operator int () //这里int就是重载函数的返回值
//explicit operator int() //加上explicit,放宽了限制,使非int的内置类型也支持来自自定义类型的转换
{
return _a;
}
operator bool()//还可以重载为其他类型
{
return _a;
}
int _a;
};
int main()
{
// 自定义类型<-内置类型
A aa1 = 100; //单参数的构造支持隐式类型转换
// 内置类型<-自定义类型 - 需要重载的支持
int i = aa1; //operator int ()
cout << i << endl;//100
bool ret = aa1;
cout << ret << endl;//1
//double d = aa1;
//int* ptr = aa1;
//加上explicit后都可以实现
//没加就需要显示调用
double d = (int)aa1;
return 0;
}
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()
{ // 假设输入_year为0,则结束
if (_year == 0)
{
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;
}
// C++ IO流,使用面向对象+运算符重载的方式
// 能更好的兼容自定义类型,流插入和流提取
int main()
{
// 自动识别类型的本质--函数重载
// 内置类型可以直接使用--因为库里面ostream类型已经实现了
int i = 1;
double j = 2.2;
cout << i << endl;
cout << j << endl;
// 自定义类型则需要我们自己重载<< 和 >>
Date d(2023, 1, 2);
cout << d << endl;
while (cin >> d)
{
cout << d << endl;;
}
return 0;
}
2.文件IO流
C++根据文件内容的数据格式,将文件分为二进制文件和文本文件。
【Tips】采用文件流对象操作文件的一般步骤:
- 定义一个文件流对象(ifstream ifile - 只输入用;ofstream ofile - 只输出用;fstream iofile - 既输入又输出用);
- 使用文件流对象的成员函数打开一个磁盘文件,使文件流对象和磁盘文件之间建立联系;
- 使用提取和插入运算符对文件进行读写操作,或使用成员函数进行读写;
- 关闭文件;
//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)
{}
operator string()
{
string str;
str += to_string(_year);
str += ' ';
str += to_string(_month);
str += ' ';
str += to_string(_day);
return str;
}
operator bool()
{
// 假设输入_year为0,则结束
if (_year == 0)
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(2023, 10, 14);
//FILE* fin = fopen("file.txt", "w");
//以二进制方式写入文件
/*fwrite(&d, sizeof(Date), 1, fin);
fclose(fin);*/
//但文件当中只有字节流,不转换成字符串是看不懂的
//将日期转换成字符串
//string str = d;
//fputs(str.c_str(), fin);
return 0;
}
//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)
{}
/*operator string()
{
string str;
str += to_string(_year);
str += ' ';
str += to_string(_month);
str += ' ';
str += to_string(_day);
return str;
}*/
operator bool()
{
// 假设输入_year为0,则结束
if (_year == 0)
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;
}
#include<fstream>
int main()
{
Date d(2023, 10, 14);
//ofstream ofs("file.txt", ios_base::out | ios_base::binary);
ofstream ofs("file.txt");
// 二进制的方式
//ofs.write((const char*)&d, sizeof(d));
// 文本的方式
ofs << d;
}
// C++文件流的优势就是可以对内置类型和自定义类型,都适用
// 一样的方式,去流插入和流提取数据
// 当然以下自定义类型Date需要重载>> 和 <<
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()
{
if (_year == 0)
{
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;
}
struct ServerInfo
{
//string _address;
char _address[32];
// 二进制读写不能用string vector这样的对象存数据
// 否则写出去就是一个指针,进程结束就是野指针,另一个进程再读进来进坑了
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; // 配置文件
};
int main()
{
ServerInfo winfo = { "777.6.5.4", 80, { 2023, 5, 4 } };
// 二进制读写
ConfigManager cf_bin("test.bin");
cf_bin.WriteBin(winfo);
ServerInfo rbinfo;
cf_bin.ReadBin(rbinfo);
cout << rbinfo._address << " " << rbinfo._port << " " << rbinfo._date << endl;
//777.6.5.4 80 2023 5 4
// 文本读写
ConfigManager cf_text("test.text");
cf_text.WriteText(winfo);
ServerInfo rtinfo;
cf_text.ReadText(rtinfo);
cout << rtinfo._address << " " << rtinfo._port << " " << rtinfo._date << endl;
//777.6.5.4 80 2023 5 4
return 0;
}
补、stringstream
在C语言中,想要将一个整形变量的数据转化为字符串格式,一般通过以下两个函数:
- _itoa()
- sprintf()
但是两个函数在转化时,都必须先为保存结果预留空间,但预留空间的具体大小并不好界定, 另外,转化格式不匹配时,可能会得到错误的结果,甚至引发程序崩溃。
int main()
{
int n = 123456789;
char s1[32];
itoa(n, s1, 10);
char s2[32];
sprintf(s2, "%d", n);
char s3[32];
sprintf(s3, "%f", n);
return 0;
}
在C++中,可以使用stringstream流来避开此问题,而要使用stringstream,必须要包含头文件 <sstream>(在头文件<sstream>下含有三个流:istringstream - 输入、ostringstream - 输出、stringstream - 输入输出)。
stringstream主要可以用来:
1、将数值类型数据格式化为字符串
#include<sstream>
int main()
{
int a = 7654321;
string sa;
// 将一个整形变量转化为字符串,存储到string类对象中
stringstream s;
s << a;
s >> sa;
cout << sa << endl;
s.str("");
// s.str("");
// 将stringstream底层管理string对象设置成"",
// 否则多次转换时,会将结果全部累积在底层string对象中
s.clear(); // 清空s, 不清空会转化失败
// clear()
// 注意多次转换时,必须使用clear将上次转换状态清空掉
// stringstreams在转换结尾时(即最后一个转换后),会将其内部状态设置为badbit
// 因此下一次转换是必须调用clear()将状态重置为goodbit才可以转换
// 但是clear()不会将stringstreams底层字符串清空掉
double d = 12.34;
s << d;
s >> sa;
string sValue;
sValue = s.str(); // str()方法:返回stringsteam中管理的string类型
cout << sValue << endl;
return 0;
}
2、字符串拼接
#include<sstream>
int main()
{
stringstream sstream;
// 将多个字符串放入 sstream 中
sstream << "first" << " " << "string,";
sstream << " second string";
cout << "strResult is: " << sstream.str() << endl;
// 清空 sstream
sstream.str("");
sstream << "third string";
cout << "After clear, strResult is: " << sstream.str() << endl;
return 0;
}
3、 序列化和反序列化结构数据
#include<sstream>
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()
{
if (_year == 0)
{
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()
{
// istringstream和ostringstream可以简单序列化和反序列化
//序列化是指,把日常表示的各种类型的值转换成字符串,或文本能表示的字节流的值
Date d1(2023, 10, 16);
ostringstream oss;
oss << d1;
string str1 = oss.str();
cout << str1 << endl;
//反序列化是指,把序列化的值给解析出来
string str2("2023 10 16");
istringstream iss(str2);
Date d2;
iss >> d2;
cout << d2 << endl;
//数据在内存中的表示是很复杂的,例如:
//基础类型:
// 整形 补码存储
// 浮点型 整形和浮点数部分分开存储
// 布尔 其实就是整型,用非0表示真,0表示假
// enum 实际上是整形常量
// 联合
// 字符串
//复杂类型:由基础类型组合而来
// 结构
// 类
//实际中,计算机对这些数据不仅要进行存储、表达,还要在其基础上进行计算
//数据在内存中的表示如此复杂,是为了方便存储和计算
return 0;
}
#include<sstream>
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()
{
if (_year == 0)
{
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;
}
struct ChatInfo
{
string _name; // 名字
int _id; // id
Date _date; // 时间
string _msg; // 聊天信息
};
int main()
{
// 结构信息序列化为字符串
ChatInfo winfo = { "张三", 135246, { 2023, 10, 16 }, "晚上一起看电影吧" };
ostringstream oss;
oss << winfo._name << " " << winfo._id << " " << winfo._date << " " << winfo._msg;
string str = oss.str();
cout << str << endl << endl;
// 我们通过网络这个字符串发送给对象,实际开发中,信息相对更复杂,
// 一般会选用Json、xml等方式进行更好的支持
// 字符串解析成结构信息
ChatInfo rInfo;
istringstream iss(str);
iss >> rInfo._name >> rInfo._id >> rInfo._date >> rInfo._msg;
cout << "-------------------------------------------------------" << endl;
cout << "姓名:" << rInfo._name << "(" << rInfo._id << ") ";
cout << "时间" << rInfo._date << endl;
cout << "信息:>" << rInfo._msg << endl;
cout << "-------------------------------------------------------" << endl;
return 0;
}
【小结】stringstream流对象的使用须知
- stringstream实际是在其底层维护了一个string类型的对象用来保存结果。
- 多次数据类型转化时,一定要用clear()来清空,才能正确转化,但clear()不会将stringstream底层的string对象清空。
- 可以使用s. str("")方法将底层string对象设置为""空字符串。
- 可以使用s.str()将让stringstream返回其底层的string对象。
- stringstream使用string类对象代替字符数组,可以避免缓冲区溢出的危险,而且其会对参数类型进行推演,不需要格式化控制,也不会出现格式化失败的风险,因此使用更方便,更安全。