目录
一、C语言的输入和输出
二、缓冲区 (以输入缓冲区为例)
三、流 (以C语言为例)
四、C++ IO流
1.C++标准IO流
类型转换:
标准IO流中的一些细节:
2.C++文件IO流
基本概念
常用接口
二进制格式的读写
文本格式的读写
五、 stringstream的简单介绍
一、C语言的输入和输出
C语言中我们用到的最频繁的输入输出方式就是scanf ()与printf()。
- scanf(): 从标准输入设备(键盘)读取数据,并将值存放在变量中。
- printf(): 将指定的文字/字符串输出到标准输出设备(屏幕)。(注意宽度输出和精度输出控制)
二、缓冲区 (以输入缓冲区为例)
1.输入分类:
输入分为无缓冲输入和缓冲输入,无缓冲输入意味着正在等待的程序可以立即使用输入的字符;缓冲输入,就是将用户输入的字符先放在一个区域(缓冲区),等用户按下某个代表结束的键后,程序才可以使用键入的数据,在这个过程中存储收集用户输入字符的区域就叫做缓冲区。
2.缓冲区的意义:
- 节约时间,将若干字符当成一个块进行传输比单个字节传输快;
- 易于修改,无缓冲输入中,输入的字符立即被使用,无法修改,而有缓冲输入中,在按下结束键(一般都是Enter)前都可以修改要输入的数据。
3.缓冲分类:
- 完全缓冲输入(完全缓冲I/O):缓冲区被填满时才刷新缓冲区(内容内发送到目的地)。一般文件输入都是完全缓冲输入。
- 行缓冲输入(行缓冲I/O):在出现换行符时刷新缓冲区。键盘输入通常是行缓冲。
4.缓冲区大小
取决于系统。一般是512B和4096B。
三、流 (以C语言为例)
不同的系统处理文件的规则往往不同,主要体现在存储、文件末尾标记和衡量文件的大小三个方面。(详情见 C Primer Plus p188)
由于这些差异,编程语言为了增强系统间的兼容性,不会直接处理文件,而是使用标准的I/O包处理流。流是一个实际输入或输出映射的理想化数据流。C/C++语言将不同属性和不同种类的输入,用属性更统一的流来表示,文件的打开和关闭等过程就是把流与文件相关联,这涉及创建用于处理文件的标准模型和一套标准的I/O函数。举个例子,用if(ch\=='\\n')检查换行符,不同的系统会使用不同的方式标记文件末尾(回车符或者换行符),I/O函数会在这两种表示方法之间转换。流是一个将实际输入或输出的内容映射成理想化的数据流。比如C语言中,stdin流表示键盘输入,stdout流表示屏幕输出。
四、C++ IO流
cplusplus.com/reference/iolibrary/
1.C++标准IO流
C++标准库提供了4个全局流对象cin、cout、cerr、clog,使用cout进行标准输出,即数据从内存流向控制台(显示器)。使用cin进行标准输入即数据通过键盘输入到程序中,同时C++标准库还提供了cerr用来进行标准错误的输出,以及clog进行日志的输出,从上图可以看出,cout、cerr、clog是ostream类的三个不同的对象。
int main() { ostream out(nullptr); cerr << "xxxx" << endl; clog << "yyyy" << endl; //ostream& put (char c); cout.put(48);//整形截断,打印0 //https://cplusplus.com/reference/ostream/ostream/put/ // 作条件判断的类型(表真假):bool 整形 指针 string s1; // istream& operator>> (istream& is, string& str); //while (cin>>s1) while (operator>>(cin, s1))//括号内的表达式返回值类型是istream&,可以隐式转换成bool { cout << s1 << endl; } //结束:暴力ctrl+c,非暴力ctrl+/n //https://cplusplus.com/reference/ios/ios/operator_bool/ return 0; }
关于istream&转换成bool,大家可能疑惑?这是怎么转换的呢?
下面我们对类型之间的转换,做一个小归纳。
类型转换:
1.内置类型转换成内置类型,一般进行隐式转换或强制类型转换即可。
int* p = nullptr; int i = (int)p;
2.内置类型转换成自定义类型。需要通过构造函数,将内置类型的变量构造成自定义类的的对象。
class C { public: C(int x) {} }; int main() { C c1 = 2; string s = "yls"; }
3.自定义类型转换成自定义类型,通过构造函数。
class C { public: C(int x) {} }; class D { public: D(const C& c) {} }; int main() { list<int> lt; list<int>::const_iterator it = lt.begin();//普通迭代器通过构造函数转换成const迭代器 }
4.自定义类型转内置类型,使用opera built-in_type()实现类型转换。
cplusplus.com/reference/ios/ios/operator_bool/
在上面的while循环中,本质上就是调用istream中operator bool()。我们举一个简单的例子,
class E { public: operator int() { // ... return 0; } }; int main() { E e; int x = e; cout << x << endl; }
输出:
标准IO流中的一些细节:
1. cin为缓冲流。键盘输入的数据保存在缓冲区中,当要提取时,是从缓冲区中拿。如果一次输入过多,数据会存在缓冲区待下一次提取,如果输入错了,必须在回车之前修改,如果回车键按下就无法挽回了。只有把输入缓冲区中的数据取完后,才要求输入新的数据。
2. 输入的数据类型必须与要提取的数据类型一致,否则出错。出错只是在流的状态字state中对应位置位(置1),程序继续。
3. 空格和回车都可以作为数据之间的分格符,所以多个数据可以在一行输入,也可以分行输入。但如果是字符型和字符串,则空格(ASCII码为32)无法用cin输入,字符串中也不能有空格。回车符也无法读入。
4. cin和cout可以直接输入和输出内置类型数据,原因:标准库已经将所有内置类型的输入和输出全部重载了。
5. 对于自定义类型,如果要支持cin和cout的标准输入输出,需要对<<和>>进行重载。
6. 在线OJ中的输入和输出:
- 对于IO类型的算法,一般都需要循环输入;
- 输出:严格按照题目的要求进行,多一个少一个空格都不行;
- 连续输入时,vs系列编译器下在输入ctrl+Z时结束。
// 单个元素循环输入 while(cin>>a) { // ... } // 多个元素循环输入 while(c>>a>>b>>c) { // ... } // 整行接收 while(cin>>str) { // ... }
7. istream类型对象转换为逻辑条件判断值
istream& operator>> (int& val); explicit operator bool() const;
cplusplus.com/reference/istream/istream/operator>>/
cplusplus.com/reference/ios/ios/operator_bool/
实际上我们看到使用while(cin>>i)去流中提取对象数据时,调用的是operator>>,返回值是istream类型的对象,那么这里可以做逻辑条件值,源自于istream的对象又调用了operator
bool,operator bool调用时如果接收流失败,或者有结束标志,则返回false。
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(2022, 4, 10); cout << d; while (d) { cin >> d; cout << d; } return 0; }
8.默认情况下,C++和C流是同步的
cplusplus.com/reference/ios/ios_base/sync_with_stdio/
std::cin
与stdio
同步,就是std::cin
与stdin
共用缓冲区。通过设置in.sync_with_stdio(false)
之后,std::cin
就有了自己的缓冲区。不过解绑之后, C++和 C 的输入输出使用不同的缓冲区,会导致输入和输出顺序无法保障。因此要么用std::cin/std::cout
,要么用printf/scanf
,尽量不要混用。(注:该机制可能依赖编译器和操作系统的实现。)
2.C++文件IO流
基本概念
1.文件是什么?
文件通常是在磁盘或固态硬盘上的一段已命名的存储区。C语言把文件看成一系列连续的字节,每个字节都能被单独的读取。这与UNIX环境中(C的 发源地)的文件结构相对应。由于其他环境中可能无法完全对应这个模型,C提供两种读取文件的模式:文本模式和二进制模式。2.文本模式和二进制模式
- 所有文件的内容都以二进制的形式存储。
- 文本文件是指用二进制编码的字符表示文本的文件,内容为文本内容,容易被人理解;
- 二进制文件是指用二进制值代表机器语言、数值数据、图片和音乐编码的文件,内容为二进制内容,容易被计算机理解。
C语言提供两种访问文件的途径:二进制模式和文本模式。
在二进制模式中,程序可以访问文件的每个字节;在文本模式中,程序所见的内容和文件的实际内容不同。
程序以文本模式读取文件时,会把本地环境表示的行末尾和文件结束映射为C模式,以二进制模式读取文件,一般不发生映射。
常用接口
同样的,C++根据文件内容的存储格式分为二进制文件和文本文件,并通常用文件流对象读写文件。采用文件流对象操作文件的一般步骤:
- 定义一个文件流对象
ifstream ifile(输入,程序从文件流中读取数据)
ofstream ofile(输出,程序中的数据读出到文件流)
fstream iofile(既输入又输出)
- 使用文件流对象的成员函数打开一个磁盘文件,使得文件流对象和磁盘文件之间建立联系
- 使用提取和插入运算符对文件进行读写操作,或使用成员函数进行读写
- 关闭文件
cplusplus.com/reference/fstream/
我们写一个类,记录服务器的相关信息,
struct ServerInfo
{
char _address[32];
double _x;
Date _date;
};
将服务器类的文件读写封装在另一个类中。(二进制模式的读写类名就是BinIO,文本模式的读写类名是TextIO。)
二进制格式的读写
class BinIO
{
BinIO(const char* filename = "info.bin")
:_filename(filename)
{}
//写入文件中
void write(const ServerInfo& winfo)
{
//定义一个输出文件流对象
ofstream ofs(_filename, ofstream::out | ofstream::binary);
//调用该对象的写
ofs.write((char*)&winfo, sizeof(winfo));//ostream& write (const char* s, streamsize n);char*可以隐式类型转换成const char*
}
//读出文件
void read(ServerInfo& rinfo)
{
ifstream ifs(_filename, ofstream::in | ofstream::binary);
ifs.read((const char*)&rinfo, sizeof(rinfo));
}
private:
string _filename;
};
我们执行下面的主函数,
// 写
int main()
{
ServerInfo winfo = { "192.0.0.1",12.13,{ 2022, 4, 10 } };
BinIO bin;
bin.write(winfo);
return 0;
}
源文件所在文件夹就多了文件“info.bin”。
我们再执行“二进制的写”
//二进制读
int main()
{
ServerInfo info;
BinIO bin;
bin.read(info);
cout << info._address << endl;
cout << info._x << endl;
cout << info._date << endl;
return 0;
}
程序打印:
上面的服务器信息的地址是用char数组存储,我们换成string,如下
struct ServerInfo
{
//char _address[32];
string _address;
double _x;
Date _date;
};
重新执行“二进制的写”,
没有问题。
我们再执行一下“二进制的读”,
调用析构函数时抛异常了。这是为什么呢?
ServerInfo中成员变量_address的_str(string的私有成员)变成了野指针,在运行“写进程”后,该指针指向的空间已经释放,所以在“读进程”中该指针是野指针。我们模拟string的类,都有一个成员变量初始化需要动态开辟内存。注意:当前文件夹已有"yls.txt"文件。
根据“说法”模拟的代码,
struct new_S
{
new_S(const char* s = "")
{
_s = new char[strlen(s) + 1];
strcpy(_s, s);
}
new_S(const new_S& s)
{
_s = new char[strlen(s._s) + 1];
strcpy(_s, s._s);
}
~new_S()
{
delete[] _s;
cout << "~new_S()" << endl;
}
char* _s;
};
int main()
{
new_S s;
ifstream ifs("yls.txt", ofstream::in | ofstream::binary);
ifs.read((char*)&s, sizeof(s));
}
根据上面调试的结果,不难发现在read之后,s的成员变量_s变成了野指针,丧失了访问空间的权限。
这段程序调试中显示无法访问内存,但对原“读进程”的调试并没有显示内存访问错误,
这也符合野指针的特性——可能使程序出现不可预知的错误。
这是用两个进程分别运行“读写”。我们把读写放在一个进程中看看,
int main()
{
//写
ServerInfo winfo = { "192.0.0.1",12.13,{ 2022, 4, 10 } };
BinIO bin;
bin.write(winfo);
//读
ServerInfo info;
bin.read(info);
cout << info._address << endl;
cout << info._x << endl;
cout << info._date << endl;
return 0;
}
调试一下,调用write前后,winfo无变化。
调用read前,
调用read后,
我们根据调试图片,不难发现_Ptr都指向同一块空间,这样会对同一块空间析构两次,而且全是野指针。
文本格式的读写
文本格式的写:
//文本格式读写
class TextIO
{
public:
TextIO(const char* filename = "info.text")
:_filename(filename)
{}
//写入文件中
void write(const ServerInfo& winfo)
{
ofstream ofs(_filename);
ofs << winfo._address << endl;
ofs << winfo._x << endl;
ofs << winfo._date << endl;
}
//读出文件
void read(ServerInfo& rinfo)
{
ifstream ifs(_filename);
}
private:
string _filename;
};
// 写
int main()
{
ServerInfo winfo = { "https://legacy.cplusplus.com/reference/fstream/ifstream/ifstream/", 12.13, { 2022, 4, 10 } };
TextIO text;
text.write(winfo);
//浮点型数据转换成字符串
//c环境
//double d = 1.111;
//char buf[128];
//sprintf(buf, "%lf", d);
//cout << d << "\n" << buf << endl;
//cpp环境
执行上面的程序,
文本格式的读:
class TextIO
{
public:
TextIO(const char* filename = "info.text")
:_filename(filename)
{}
//写入文件中
void write(const ServerInfo& winfo)
{
ofstream ofs(_filename);
ofs << winfo._address << endl;
ofs << winfo._x << endl;
ofs << winfo._date << endl;
}
//读出文件
void read(ServerInfo& rinfo)
{
//这里要不要处理换行?
//不需要,因为当流提取多个值时,默认空格和换行就是多个值之间的分割符
//如果值里面包括空格呢?可以用getline,https://cplusplus.com/reference/istream/istream/getline/
ifstream ifs(_filename);
ifs >> rinfo._address ;
ifs >> rinfo._x;
ifs >> rinfo._date;
}
private:
string _filename;
};
//读
int main()
{
ServerInfo info;
TextIO text;
text.read(info);
//打印
cout << info._address << endl;
cout << info._x << endl;
cout << info._date << endl;
return 0;
}
Output:
五、 stringstream的简单介绍
1.引入stringstream
int main()
{
char sql1[128];
char name[10];
scanf("%s", name);
//我们假设spl1是一个数据库 ,我们在这个数据库查某人的分数,我们需要输入sql语句(虽然是在内存中查询)
//比如下面这个语句,我们只要修改name,就可以在数据库中查询任意name对应的分数
//这是C语言的写法
sprintf(sql1, "select * from t_scroe where name = '%s'", name);
printf("%s\n", sql1);
//cpp写法
string sql2;
sql2 += "select * from t_scroe where name = '";
sql2 += name;
sql2 += "'";
cout << sql2 << endl;
//如果是我们要查询某天的数据呢
Date d(2024, 1, 19);
string sql3;
sql3 += "select * from t_data where date = '";
sql3 += d;//这样转换就会出错
sql3 += "'";
cout << sql3 << endl;
return 0;
}
Output:
那么我们怎么把Date类型的对象转换成字符串类型呢?我们就可以使用stringstream。
cplusplus.com/reference/sstream/stringstream/str/
使用stringstream后的代码,
但一般我们不像上面那样写,而是下面这样写,
把各种信息转换成字符串,我们把这个过程叫做序列化。
把字符串中的信息提取出来转换成各种类型的数据叫做反序列化。
但stringstream对一些复杂的类型不能很好的支持,在项目中更多的使用json。
我们再举一个例子,
struct ChatInfo
{
string _name; // 名字
int _id; // id
Date _date; // 时间
string _msg; // 聊天信息
};
int main()
{
//假设是从界面获取的信息winfo
ChatInfo winfo = { "张三", 135246, { 2022, 4, 10 }, "晚上一起看电影吧" };
//转换成字符串并打印
stringstream oss;
oss << winfo._name << endl;
oss << winfo._id << endl;
oss << winfo._date << endl;
oss << winfo._msg << endl;
cout << oss.str() << endl;
// 通过网络传输到另一个人的设备上,
ChatInfo rinfo;
string str = oss.str();//获取输出流上的字符串
stringstream iss(str);//将字符串转换成流插入
//插入程序的某个对象中
iss >> rinfo._name;
iss >> rinfo._id;
iss >> rinfo._date;
iss >> rinfo._msg;
//再输出对象
cout << "-------------------------------------------------------" << endl;
cout << "姓名:" << rinfo._name << "(" << rinfo._id << ") ";
cout << rinfo._date << endl;
cout << rinfo._name << ":>" << rinfo._msg << endl;
cout << "-------------------------------------------------------" << endl;
return 0;
}
Output:
参考资料:
C++ 中 sync_with_stdio 的作用 - 阅微堂 (zhiqiang.org)