C语言中我们用到的最频繁的输入输出方式就是scanf ()与printf()。
scanf(): 从标准输入设备(键盘)读取数据,并将值存放在变量中。
printf(): 将指定的文字/字符串输出到标准输出设备(屏幕,网卡,显示器等)。注意宽度输出和精度输出控制。
C语言借助了相应的缓冲区来进行输入与输出。如下图所示
对输入输出缓冲区的理解:
(1).可以屏蔽掉低级I/O的实现,低级I/O的实现依赖操作系统本身内核的实现,所以如果能够屏蔽这部分的差异,可以很容易写出可移植的程序。
(2).可以使用这部分的内容实现“行”读取的行为,对于计算机而言是没有“行”这个概念,有了这部分,就可以定义“行”的概念,然后解析缓冲区的内容,返回一个“行”。
C++标准库提供了一组丰富的输入/输出功能,本文重点讲解C++编程中最常见的 I/O 操作
C++的 I/O 发生在流中,流是字节序列,如果字节流是从设备(如键盘,磁盘驱动器,网络连接等)流向内存,这叫做输入操作,如果字节流是从内存流向设备,这叫做输出操作。
标准输出流 :
预定义的对象 cout 是 ostream 类的一个实例,cout 对象"连接"到标准输出设备,通常是显示屏。
标准输入流 :
预定义的对象 cin 是 istream 类的一个实例,cin 对象附属到标准输入设备,通常是键盘。
我们来研究下面这一段代码
#include
#include
using namespace std;
struct Student
{
string _name;
int _age;
};
int main()
{
Student s;
cin >> s._name >> s._age;
cout << s._name << " " << s._age << endl;
scanf("%s%d", s._name.c_str(), &s._age);
printf("%s %d", s._name.c_str(), s._age);
}
这段代码看上去没有什么问题,但如果我们第二次输入的字符串过长,会导致程序崩溃,原因如下 :
string内部会有一个_Buf数组,当存储的字符串大小小于15字节时,不会去堆上开辟空间存储字符串,会将字符串存储在_Buf数组中,但无论第一次输入的字符串大小是大于15字节还是小于15字节,第二次如果输入过长,都会导致程序崩溃,原因在于第二次是用scanf进行输入,使用scanf输入,不会影响string的size和capacity,所以输入过长就会越界写入
#include
#include
using namespace std;
struct Student
{
string _name;
int _age;
};
int main()
{
Student s;
cin >> s._name >> s._age;
cout << s._name << " " << s._age << endl;
scanf("%s%d", s._name.c_str(), &s._age);
cout << s._name << " " << s._age << endl;
}
这段代码和上一段代码相比只是最后的输出使用了cout,跟上面一样,如果输入过长也会导致越界写入,除此之外,就算输入的长度在合法的范围内,打印出来的结果也不是我们想要的,原因是cout在输出时是根据string的size来输出的,而scanf输入时并没有改变string的size,所以打印结果不是我们想要的,上面的printf如果输入的长度在合法的范围内,打印结果就是我们想要的,原因是printf打印字符串时是根据’\0’的位置来判断的.
想要解决上面的两个问题,我们提前把空间开好即可
#include
#include
using namespace std;
struct Student
{
string _name;
int _age;
};
int main()
{
Student s;
cin >> s._name >> s._age;
cout << s._name << " " << s._age << endl;
s._name.resize(100); // 提前开好空间
scanf("%s%d", s._name.c_str(), &s._age);
printf("%s %d\n", s._name.c_str(), s._age);
cout << s._name << " " << s._age << endl;
}
因此,建议大家在C++中尽量去用cin和cout,用cout和cin不方便的地方,再去用scanf和printf(格式控制输出时)
C++系统实现了一个庞大的类库,其中ios为基类,其他类都是直接或间接派生自ios类
C++标准库提供了4个全局流对象cin、cout、cerr、clog,使用cout进行标准输出,即数据从内存流向控制台(显示器)。使用cin进行标准输入即数据通过键盘输入到程序中,同时C++标准库还提供了cerr用来进行标准错误的输出,以及clog进行日志的输出,从上图可以看出,cout、cerr、clog是ostream类的三个不同的对象,因此这三个对象现在基本没有区别,只是应用场景不同。
在使用时候必须要包含头文件并引入std标准命名空间。
注意:
(1). cin为缓冲流。键盘输入的数据保存在缓冲区中,当要提取时,是从缓冲区中拿。如果一次输入过多,会留在那儿慢慢用,如果输入错了,必须在回车之前修改,如果回车键按下就无法挽回了。只有把输入缓冲区中的数据取完后,才要求输入新的数据。
(2). 输入的数据类型必须与要提取的数据类型一致,否则出错。出错只是在流的状态字state中对应位置,程序继续。
(3). 空格和回车都可以作为数据之间的分格符,所以多个数据可以在一行输入,也可以分行输入。但如果是字符型和字符串,则空格(ASCII码为32)无法用cin输入,字符串中也不能有空格。回车符也无法读入。
#include
using namespace std;
int main()
{
// 输入 hello world
string a;
cin >> a;
cout << a << endl; // hello
cin >> a;
cout << a << endl; // world
getline(cin,a);
cout << a << endl;
}
(4). cin和cout可以直接输入和输出内置类型数据,原因:标准库已经将所有内置类型的输入和输出全部重载了
(5). 对于自定义类型,如果要支持cin和cout的标准输入输出,需要对<<和>>进行重载
(6). 循环输入结构
string str;
while(cin>>str)
{
cout<<str<<endl;
}
char buff[100];
while(scanf("%s",buff) != EOF)
{
printf("%s\n",buff);
}
两种方式都能达到循环输入的目的,使用ctrl + c终止输入
(1). cin>>str 等同于 istream& operator>>(cin,str),返回值为cin对象,通过operator bool()来判断是否有读取错误
(2). scanf函数当读取发生错误或读到文件末尾,会返回EOF
ofstream
输出文件流,用于创建文件并向文件写入信息
ifstream
输入文件流,用于从文件读取信息
fstream
文件流,且同时具有 ofstream 和 ifstream 两种功能,这意味着它可以创建文件,向文件写入信息,从文件读取信息
文件常见的打开方式 :
(1). in 以读的方式打开文件
(2). out 以写的方式打开文件
(3). binary 以二进制方式对文件进行操作
(4). ate 输出位置从文件的末尾开始
(5). app 以追加的方式对文件进行写入
(6). trunc 先将文件内容清空再打开文件
常用成员函数
(1). put 插入一个字符到文件
(2). write 插入一段字符到文件
(3). get 从文件提取字符
(4). read 从文件提取多个字符
(5). tellg 获取当前字符在文件当中的位置
(6). seekg 设置对文件进行操作的位置
(7). >>运算符重载 将数据形象地以“流”的形式进行输入(用于文本文件)
(8). <<运算符重载 将数据形象地以“流”的形式进行输出(用于文本文件)
以文本的形式操作文件
对文本文件进行写入操作
//以文本的形式对文件进行写入
void Write()
{
ofstream outfile("test.txt"); // 以写的方式打开文件
outfile.put('c'); // 向文件写入一个字符'c'
outfile.write("hello world",5); // 向文件写入长度为n的字符串
outfile << "hehe"; // 向文件写入字符串
outfile.close(); // 关闭文件
}
对文件文件进行读取操作
//以文本的形式对文件进行读取
void ReadTxt()
{
ifstream infile("test.txt");
// seekg(),tellg()
char c, c1, c2;
infile.seekg(2);
infile >> c;
infile.seekg(3, ios::beg);
infile >> c1;
infile.seekg(-1,ios::end);
infile >> c2;
cout << c << endl;
cout << c1 << endl;
cout << c2 << endl;
// get
char c;
while (infile.get(c))
{
cout << c;
}
cout << endl;
// read
infile.seekg(0, ios::end);
int length = infile.tellg();
infile.seekg(0, ios::beg);
char* buffer = new char[length];
infile.read(buffer, length);
if (infile)
{
cout << buffer << endl;
}
delete[]buffer;
// >>
infile.seekg(0, ios::end);
int length = infile.tellg();
infile.seekg(0, ios::beg);
char* buffer = new char[length];
infile >> buffer;
cout << buffer << endl;
}
对二进制文件进行写入操作
void write()
{
ofstream outfile("test.bin",ios::out | ios::binary);
outfile.put('l');
outfile.write("lyp hello linux",10);
}
对二进制文件进行读取操作
void read()
{
ifstream infile("test.bin", ios::in | ios::binary);
// get
char c;
while (infile.get(c))
{
cout << c;
}
cout << endl;
// read
infile.seekg(0, ios::end);
int length = infile.tellg();
infile.seekg(0, ios::beg);
char* buffer = new char[length];
infile.read(buffer, length);
if (infile)
{
cout << buffer << endl;
}
delete[]buffer;
}
在C语言中,如果想要将一个整形变量的数据转化为字符串格式,我们一般有以下两种方式
(1). itoa
int a = 20;
char buffer[10];
itoa(a,buffer,10); // 将数字转换成10进制字符
(2). sprintf
struct ServerInfo
{
char _ip[20];
int _port;
};
int main()
{
ServerInfo info = {
"2020.110.400.83",250};
char buffer[128];
// 序列化,将结构体中的内容转换成字符串
sprintf(buffer, "%s %d", info._ip, info._port); // 加空格,在buffer数组里就有空格
// 反序列化,将数组的内容读到结构体中
ServerInfo rinfo;
sscanf(buffer, "%s %d", rinfo._ip, &rinfo._port);
}
但是两个函数在转化时,都得需要先给出保存结果的空间,那空间要给多大呢,就不太好界定,而且转化格式不匹配时,可能还会得到错误的结果甚至程序崩溃。
在C++中,可以使用stringstream类对象来避开此问题。
在程序中如果想要使用stringstream类,必须要包含头文件。在该头文件下,标准库三个类:
(1). istringstream 进行流的输入
(2). ostringstream 进行流的输出
(3). stringstream 进行流的输入输出
#include
#include
using namespace std;
struct ServerInfo
{
char _ip[20];
int _port;
};
int main()
{
ServerInfo info = {
"2020.110.400.83",250};
// 将结构体的内容转换成字符串
stringstream sm;
sm << info._ip << " " << info._port;
string buffer = sm.str();
cout << buffer << endl;
// 字符串的内容输出到结构体当中
stringstream sm;
sm.str("2020.110.400.83 250");
ServerInfo rinfo;
sm >> rinfo._ip >> rinfo._port;
stringstream sm("2020.110.400.83 250");
ServerInfo rinfo;
sm >> rinfo._ip >> rinfo._port;
}
注意:
(1). stringstream实际是在其底层维护了一个string类型的对象用来保存结果。
(2). 多次数据类型转化时,一定要用clear()来清空,才能正确转化,但clear()不会将stringstream底层的string对象清空。
(3). 可以使用s. str("")方法将底层string对象设置为""空字符串。
(4). 可以使用s.str()将让stringstream返回其底层的string对象。
(5). stringstream使用string类对象代替字符数组,可以避免缓冲区溢出的危险,而且其会对参数类型进行推演,不需要格式化控制,也不会出现格式化失败的风险,因此使用更方便,更安全。