目录
一,C语言的输入与输出
C语言中我们最常用的输入输出方式为scanf()和printf()
①scanf()表示从标准输入设备(键盘等)读取数据,并将值存放在变量中。
②printf()表示将指定的内容格式化输出到标准输入设备(屏幕等)。
C语言借助了相应的缓冲区来进行输入和输出,如下图:
对缓冲区的理解:
①可以屏蔽调低级IO的实现,因为低级IO的实现依赖操作系统本身内核的实现,所以如果能屏蔽这部分的差异,可以很容易写出可移植性高的程序
②可以使用这部分的内容实现“行”读取的行为,对于计算机本身而言是没有“行”这个概念的,具体细节我们到Linux系统编程的文件系统章节再细讲
二,流是什么
“流”即流动的意思,是物质从一处向另一处流动的过程,是对一种有序连续且具有方向性的数据的抽象描述
C++流指的是信息从外部设备向计算机内部输入和从内存向外部输出设备输出的过程。
流的特性是:有序连续,具有方向性
所以为了实现这种流动,C++定义了IO标准类库,这些每个类都称为流或者流类。
三,C++标准IO流
C++系统实现了一个庞大的类库,其中ios为基类,其他类全是直接或间接的是继承ios的子类
3.1 四个全局流对象
C++标准库提供了4个全局流对象cin,cout,cerr,clog。使用cout进行标准输出,即数据从内存流向控制台(屏幕)。使用cin进行标准输入即数据从外部设备(键盘)输入到程序中,同时C++标准库还提供了cerr用来进行标准错误输出,以及clog日志输出。但是从上图可以看出,cout,cerr,clog是ostream类的三个不同的对象,所以这三个基本无区别,至少应用场景不同
注意:
①cin为缓冲流。键盘输入的数据都把偶从你在缓冲区中,当要提取时,是从缓冲区中拿。如果一次输入过多,数据会存在缓冲区中按需被提取。如果输入错了,必须在回车之前修改,因为流是单向的。而且只有把输入缓冲区的数据读取完后,才会要求输入新的数据
int main()
{
int a, b;
cin >> a;
cout << a << endl;
cin >> b;
cout << b << endl;
return 0;
}
②输入的数据类型必须与要提取的数据类型一致,否则报错
③空格和回车都可以作为数据之间的分隔符,所以多个数据可以在一行输入,也可以分行输入,但是如果是字符串和字符型,则空格(ASCLL为32)无法用cin输入,字符串内也不能用空格。
int main()
{
string s1;
cin >> s1; //如果输入hello world,字符串里面包含空格,则下面打印的时候只打印hello
cout << s1 << endl;
return 0;
}
所以对于有空格的字符串,我们用getline函数读取,这个函数遇到'\n'才会停下来
int main()
{
string s2;
getline(cin, s2);
cout << s2 << endl;
return 0;
}
④C++标准库中的cin和cout可以直接输入和输出所有内置类型数据,因为标准库已经重载了所有的内置类型的输入和输出,对于自定义类型则需要自己重载输入输出
void main()
{
//int i = 1;
//double j = 2.2;
//cout << i << j << endl;//其实这里的自动识别类型就是operator<<和重载
//2022 11 28
//输入多个值,默认都是用空格或者换行分割
int year, month, day;
cin >> year >> month >> day;
scanf_s("%d%d%d", &year, &month, &day);
//scanf("%d %d %d", &year, &month, &day); //不需要去加空格
//20240410 强制要求这样输入
scanf_s("%4d%2d%2d", &year, &month, &day);
string str;
cin >> str;//利用分割函数可以把字符串分割出来
year = stoi(str.substr(0,4));
month = stoi(str.substr(4,2));
day = stoi(str.substr(6,2));
}
3.2 OJ题中的输入和输出
对于IO类型的算法,一般需要循环输入,而且输出也是按照题目的要求严格进行。
// OJ题要输入多组测试用例
void main()
{
int year, month, day;
string str;
//ctrl+c 9号信号强杀
//ctrl+z+换行 流对象提取到结束标志
//istream& operator>> (istream& is, string& str);
while (cin >> str)
{
//为什么istream的cin对象可以转bool
//因为istream有explicit operator bool() const;
//支持自定义类型转内置类型
year = stoi(str.substr(0, 4));
month = stoi(str.substr(4, 2));
day= stoi(str.substr(6, 2));
cout << year << "年" << month << "月" << day << "日" << endl;
}
}
3.3 自定义类型重载输入和输出
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,ofstream和stringstream也可以用,因为用的是继承体系
ostream& operator << (ostream& out, const Date& d)
{
out << d._year << " " << d._month << " " << d._day;
return out;
}
void main()
{
//自动识别类型的本质--函数重载
//内置类型可以直接使用--因为库里面ostream类型已经实现了
int i = 1;
double j = 2.2;
cout << i << endl;
cout << j << endl;
// 自定义类型则需要我们自己重载<< 和 >>
Date d(2022, 4, 10);
cout << d;
while (d)
{
cin >> d;
cout << d;
}
}
四,C++文件IO流
4.1 C++文件操作步骤
4.1.1 操作文件的类
类 | 操作 |
---|---|
fstream | 读和写 |
ifstream | 只读 |
ofstream | 只写 |
4.1.2 文件打开方式
方式 | 功能 |
---|---|
in | 以读的方式打开文件 |
out | 以写的方式打开文件 |
binary | 以二进制的方式对文件进行操作 |
ate | 从文件的末尾开始输出 |
app | 对文件进行追加写入 |
trunc | 将文件清空再打开 |
4.1.3 文件操作常用函数
函数 | 功能 |
---|---|
put | 插入一个字符到文件 |
write | 插入字符串到文件 |
get | 从文件提取字符 |
read | 从文件提取多个字符 |
tellg | 获取当前字符在文件中的位置 |
seekg | 设置对文件进行操作的位置 |
<<运算符重载 | 将数据“流”输入 |
>>运算符重载 | 将数据“流”输出 |
4.1.4 演示
//fstream文件流
void main1()
{
std::ofstream ofs("test.txt", ofstream::out | ofstream::app);
ofs << "hello world";
ofs << "hello world"; //把字符串写到文件里去
}
void main2()
{
ifstream ifs("test.txt");
//fscanf("%d%s%f",);C语言将文档内容读到变量里的方法
//读
int i;
string s;
double d;
Date de;//C++的优势,可以读自定义类型,前提是自定义对象重载了>>
ifs >> i >> s >> d >> de;
}
4.2 以二进制和文本方式读写演示代码
struct ConfigManager
{
public:
ConfigManager(const char* filename = "server.config")
:_filename(filename)
{}
void WriteBin(const ServerInfo& info) //二进制写
{
ofstream ofs(_filename, ios_base::out | ios_base::binary);
ofs.write((char*)&info, sizeof(info));
}
void ReadBin(ServerInfo& info) //二进制读
{
ifstream ifs(_filename, ios_base::in | ios_base::binary);
ifs.read((char*)&info, sizeof(info));
}
//C语言的方式 -- 太复杂,不好搞
/*void WriteText(const ServerInfo& info)
{
ofstream ofs(_filename, ios_base::out);
ofs.write(info._address.c_str(), info._address.size());
ofs.put('\n');
const string str = to_string(info._port);
ofs.write(str.c_str(), str.size());
}
void ReadText(ServerInfo& info)
{
ifstream ifs(_filename, ios_base::in | ios_base::binary);
char buff[128];
ifs.getline(buff, 128);
info._address = buff;
ifs.getline(buff, 128);
info._port = stoi(buff);
}*/
//我们直接用C++重载的流操作符
void WriteText(const ServerInfo& info) //文本写
{
ofstream ofs(_filename, ios_base::out);
ofs << info._address << endl;
ofs << info._port << endl;
ofs << info._date << endl;
}
void ReadText(ServerInfo& info) //文本读
{
ifstream ifs(_filename, ios_base::in | ios_base::binary);
ifs >> info._address >> info._port >> info._date;
}
private:
string _filename; // 配置文件
};
void main1()
{
//二进制写进去
//ServerInfo winfo = { "127.0.0.1",888 };
ServerInfo winfo = { "https://legacy.cplusplus.com/reference/istream/istream/read/",888 };
ConfigManager cm;
cm.WriteBin(winfo);
//上面的执行一次就写一次
//二进制读
ServerInfo rinfo;
cm.ReadBin(rinfo);
cout << rinfo._address << endl;//读不了地址,因为写入时创建了指针,写完后析构了,指针变成野指针,所以二进制写入不敢用string,最好用数组
cout << rinfo._port << endl; //ServerInfo为char数组时不崩,是string时就崩
}
void main2()
{
// 文本写出去
//ServerInfo winfo = { "127.0.0.1", 888};
ServerInfo winfo = { "https://legacy.cplusplus.com/reference/istream/istream/read/", 888, { 2022, 11, 30 } };
ConfigManager cm;
cm.WriteText(winfo);
// 文本的读
ServerInfo rinfo;
cm.ReadText(rinfo);
cout << rinfo._address << endl;
cout << rinfo._port << endl;
cout << rinfo._date << endl;
}
4.3 二进制和文本读写优缺点
二进制读写:再内存如何存储,就如何写道磁盘文件
优点:快
缺点:写入内容不可见,需要二进制读的时候才能看见
文本读写:对象数据序列化字符串写出来,读回来也是字符串,反序列化转成对象数据
优点:可以看见写出去的是什么
缺点: 存在转换过程,要慢一些
五,stringstream介绍
5.1 C语言整形转字符串
在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;
}
5.2 stringstream
标准库三个类:istringstream,ostringstream和stringstream,分别用来进行流的输入,输出和输入输出操作。
stringstream主要可以用来做下面几种事
5.2.1 将数值类型数据格式化为字符串
#include<sstream>
int main()
{
int a = 1234;
string sa;
stringstream s;
s << a; //输入流输入int a
s >> sa; //将a输出到string sa中
cout << sa << endl;
s.str(""); //将stringstream底层管理的string对象设置为空字符串
s.clear(); //清楚上次转换状态
double b = 3.14;
s << b;
sa = s.str();
cout << sa << endl;
}
5.2.2 字符串拼接
#include <sstream>
int main()
{
string rets;
stringstream s;
s << "hello" << "2024"; //将多个字符串放入stringstream中
s >> rets; //方式一获取
cout << rets << endl;
s.str(""); //将stringstream底层管理的string对象设置为空字符串
s.clear(); //将上次转换状态清空掉
s << "You" << " " << "looks" << " " << "good"; //将多个字符串放入stringstream中
rets = s.str();
cout << rets << endl;
return 0;
}
5.2.3 序列化和反序列化
struct ChatInfo
{
string _name; // 名字
int _id; // id
Date _date; // 时间
string _msg; // 聊天信息
};
//stringstream字符串流运用场景
void main()
{
// 序列化 -- 不管你是整形还是对象,都给你转成字符串
ChatInfo winfo = { "张三", 135246, { 2022, 4, 10 }, "晚上一起看电影吧" };
//ostringstream oss;
stringstream oss;
oss << winfo._name << endl;
oss << winfo._id << endl;
oss << winfo._date << endl;
oss << winfo._msg << endl;
string str = oss.str();
cout << str << endl;
// 网络传输str,另一端接收到了字符串串信息数据
// 反序列化 -- 解析上面的转换后的数据
ChatInfo rInfo;
//istringstream iss(str);
stringstream iss(str);
iss >> rInfo._name;
iss >> rInfo._id;
iss >> rInfo._date;
iss >> rInfo._msg;
cout << "----------------------------------" << endl;
cout << rInfo._date << endl;
cout << rInfo._name << "[" << rInfo._id << "]:>" << rInfo._msg << endl;
}
5.3 注意事项
①stringstream实际是在底层维护了一个string的对象来保存结果
②多次数据类型转化时,需要clear()来清空上次转换,下次转换才能正常转换,但是clear()不会将string对象清空
③可以使用s.str("");将底层string设置为空字符串
④可以使用s.str() 让stringstream返回底层的string对象
⑤stringstream使用string类代替字符数组,可以避免缓冲区溢出的情况,而且会参数类型进行推演,不需要格式化控制,也不会出现格式化失败的风险,因此使用更安全,也更方便