目录
1.C语言的输入输出
C语言中我们用到最频繁的输入输出方式就是scanf()和printf().
- scanf():从标准输入设备(键盘)读取数据,并将值存放在变量中。
- printf():将指定的文字/字符串输入到标准输出设备(屏幕)。注意宽度输出和精度输出控制
C语言借助了相应的缓冲区来进行输入与输出。
对输入输出缓冲区的理解:
1.可以屏蔽掉低等级I/O的实现,低级I/O的实现依赖操作系统本身内核实现,所以如果能够屏蔽这部分的差异,可以很容易写出可移植程序。
2.可以使这部分的内容实现“行”读取的行为,对于计算机而言是没有“行”这个概念,有了这部分,就可以定义“行”的概念,然后解析缓冲区内容,返回一个“行”。
2.流是什么
“流”即流动的意思,是物质从一处流向另一处流动的过程,是对一种有程序连续且具有方向性的数据(其单位可以是bit/byte/packet)的抽象描述。C++流是指信息从外部输入设备(如键盘)向计算机内部(如内存)输入和从内存向外部输出设备(显示器)输出的过程。这种输入输出的过程被形象的比喻为“流”。它的特性是:有序连续、具有方向性。
为了实现这种流动,C++定义了I/O标准类库,这些每个类都称为流/流类,用以完成某方面的功能
【扩展】:
在一些I/O类型题当中,会让你输入多组测试用例,会写出这样的形式
//C++
string str;
while (cin >> str)
{
//...
cout << str << endl;
}
//C
char a[128];
while (scanf("%s", a) != EOF)//等于EOF就结束,类似于结束信号
//while (!scanf("%s", a))
{
cout << a << endl;
}
那么如何停下来呢?
C表示方法
在C文档中有提到,当一个输入的数据成功,那么EOF就是返回值。这个EOF就是一个退出码,类似于推出信号。这种情况下,输入ctrl+z就可以退出。
C++
cin>>str相当于operator>>(cin,str),这个cin是istream类型,传给operator>>函数后返回this,也就是返回cin,cin是作为一个返回值返回到while的判断循环中,也就是说operator>>返回不能等于0。平时这个判断条件要么是整型,要么是指针,遇到nullptr就停下来。但是我们并没有遇到对象做判断条件,这个就源自于一个类型运算符重载,类型运算符重载,重载了operator bool,当istream作为条件判断值,会把它转成一个operator bool类型
所以输入0就停下来
C++IO流
C++系统实现了一个庞大的类库,其中ios为基类,其他类都是直接或间接派生自ios类
3.C++标准IO流
C++标准库提供了4个全局流对象cin、cout、cerr、clog,使用cout进行标准输出,即数据从内存流向控制台(显示器)。使用cin进行标准输入即数据通过键盘输入到程序中,同时C++标准库还提供了cerr用来进行标准错误的输出,以及clog进行日志的输出,从上图可以看出,cout、cerr、clog是ostream类的三个不同的对象,因此这三个对象现在基本没有区别,只是应用场景不同。
在使用时候必须要包含文件并引入std标准命名空间
【注意】:
1. cin为缓冲流。键盘输入的数据保存在缓冲区中,当要提取时,是从缓冲区中拿。如果一次输入过多,会留在那儿慢慢用,如果输入错了,必须在回车之前修改,如果回车键按下就无法挽回了。只有把输入缓冲区中的数据取完后,才要求输入新的数据。
2. 输入的数据类型必须与要提取的数据类型一致,否则出错。出错只是在流的状态字state中对应位置位(置1),程序继续。
3. 空格和回车都可以作为数据之间的分格符,所以多个数据可以在一行输入,也可以分行输入。但如果是字符型和字符串,则空格(ASCII码为32)无法用cin输入,字符串中也不能有空格。回车符也无法读入。4.对于自定义类型,如果要支持cin和cout的标准输入输出,需要对<<和>>进行重载
C++文件IO流
C语言中,读写文件分为二进制读写和文本读写,同样C++也分为这两种。
C语言中,打开/关闭文件用到了fopen/fclose。
二进制读写:写入/读取fwrite/fread。写出去什么样子,就是什么样子。写入二进制的0/1,文件中就被写入了二进制字符。但是文件不可见 。
文本读写:写入/读取-- fprintf/fscanf(),像printf/scanf一样规定格式即可。为什么写入是printf,读取是fscanf呢?因为只有向控制台中输入的才是写入,所以未见读写中,虽然fscanf叫做读取,但是它实际上是向控制台写入。
因为对于文件而言,它只认识字符串,并不认识整数,浮点数这些。所以文件读写,如果不是字符串,就把它们转成字符串写入文件,从文件中读,就把字符串再转回其他类型文本读写--好处是写在文件里可以看见
定义一个结构体,表示写入文件内容的结构类型
struct ServerInfo
{
char _ip[32];//ip
int _port; //端口
};
C语言读写方法
void TestC_W_Bin()//二进制写
{
ServerInfo info = { "127.0.0.1", 80 };//结构体写入内容
FILE* fout = fopen("test.bin", "wb");//打开文件
assert(fout);
fwrite(&info, sizeof(info), 1, fout);//写入结构体内容,取结构体地址,大小为info大小一共一个
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_Test()//文本写入
{
FILE* fout = fopen("test.txt", "w");//打开文件
assert(fout);
ServerInfo info = { "127.0.0.1", 80 };//定义结构体内容
fprintf(fout, "%s %d", info._ip, info._port);//将fout文件写入字符串和十进制数
fclose(fout);//关闭文件
printf("%s:%d\n", info._ip, info._port);
}
void TestC_R_Test()//文本读
{
FILE* fin = fopen("test.txt", "r");//打开文件
assert(fin);
ServerInfo info;
fscanf(fin, "%s%d", info._ip, &info._port);//ip不用取地址,数组就是首元素的地址
fclose(fin);
printf("%s:%d\n", info._ip, info._port);
}
C++读写方法
C++与C不同之处就在于C++的类,我们可以写一个文件读写的类,向这个类初始化一个文件,它的成员变量就是文件,然后对这个成员变量进行读写操作
C++类和构造函数
这里文件成员变量是string类型,文件的读写方法函数在上面的图中可以看到,他都是在fstream这个库函数中,所以要包两个头文件
#include <string>
#include <fstream>
class ConfigManger
{
public:
ConfigManger(const char* filename)
:_filename(filename)
{}
private:
string _filename;
};
二进制写入
这里文件的读写方法,是用或操作符来确定的。这些读写方法都是在ios_base库中的,上图可以看见。一个方法表示一个二进制位,或运算符将两个方法或在一起,他们表示的二进制位就被1占满。比如说我们将将各个读写操作的方法打印出来,看看他们分别代表几(这里只用三个举例子)。但因看出,out写入文件操作符是2,用二进制代表10;读取操作符代表1,二进制binary函数方法代表1000,他们用或操作符运算,就知道运用了那些过程。
void WriteBin(ServerInfo& info)//向文件中写入结构体内容
{
ofstream ofs(_filename.c_str(), ios_base::out | ios_base::binary);
//可以不close,析构函数已经关闭文件
ofs.write((const char*)&info, sizeof(ServerInfo));//因为info引用类型,引用了ServerInfo类型,所以要强转
}
write函数的第一个参数类型是const char*,所以info取地址后要强转一下。
测试一下
int main()
{
ServerInfo info = { "127.0.0.1", 80 };
ConfigManger cm("config.bin");
cm.WriteBin(info);
return 0;
}
二进制文件写入操作成功。
二进制读
void ReadBin(ServerInfo& info)
{
ifstream ifs(_filename.c_str(), ios_base::in | ios_base::binary);
ifs.read((char*)&info, sizeof(ServerInfo));
}
文本写入
void WriteText(ServerInfo& info)
{
ofstream ofs(_filename.c_str());//ofstream默认给到out写入操作
ofs << info._ip << " " << info._port << endl;
}
文本读取
void ReadText(ServerInfo& info)
{
ifstream ifs(_filename.c_str());//向某个文件的读写方式,不用写读写方式,因为ifstream的默认给的缺省值是ios_base::in
ifs>>"ip:">>info._ip>>info._port
}
还有一种情况,当我们写了一个自定义类,在ServerInfo结构体中还有一个自定义类的成员变量
class Date
{
public:
Date(int year = 1900, int month = 1, int day = 1)
:_year(year)
, _month(month)
, _day(day)
{}
private:
int _year;
int _month;
int _day;
};
struct ServerInfo
{
char _ip[32];//ip
int _port; //端口
Date _d;
};
如果要对自定义类对象读写,自定义类的内部成员变量是不能被访问的,下面这样写可能会报错。
void WriteText(ServerInfo& info)
{
ofstream ofs(_filename.c_str());//ofstream默认给到out写入操作
ofs << info._ip << " " << info._port << endl << " " << info._d._year << info._d._month << info._d._day << endl;
}
所以我们可以重载一下文件读和写操作符,这个操作符需要在类外面定义,要不然它的ofstream类型参数和其他类型参数顺序颠倒。这样在类外面定义的话,需要应用到类里面的成员变量,所以要将重载操作符变成友元函数。
class Date
{
friend ofstream& operator<<(ofstream& ofs, Date& d);
public:
Date(int year = 2022, int month = 1, int day = 1)
:_year(year)
, _month(month)
, _day(day)
{}
private:
int _year;
int _month;
int _day;
};
struct ServerInfo
{
char _ip[32];//ip
int _port; //端口
Date _d;
};
ofstream& operator<<(ofstream& ofs, Date& d)//写
{
ofs << d._year << " " << d._month << " " << d._day;
return ofs;
}
class ConfigManger
{
public:
ConfigManger(const char* filename)
:_filename(filename)
{}
void WriteText(ServerInfo& info)
{
ofstream ofs(_filename.c_str());//ofstream默认给到out写入操作
ofs << info._ip << " " << info._port << endl;
ofs << info._d;
}
};
int main()
{
Date d1(2023, 4, 2);
ServerInfo winfo = { "127.0.0.2", 80, d1 };//要写入的信息结构体
ConfigManger cm("config.txt");//初始化文件
cm.WriteText(winfo);
}
sstream
想要把结构体的信息都转换为字符串,C语言中有sscanf和sprintf,那么C++是采用什么方法呢?
C++的sstream中的ostringstream提供了str()函数来类来实现结构体内容转字符串。同样如果想把字符串再转回结构体的类型,那么直接用istringstream中的流提取操作符
PersonInfo info = { "张三", 18 };
ostringstream oss;
oss << info._name << info._age;
string str = oss.str();
istringstream iss(str);
string name;
int age;
iss >> name >> age;
看到结果的age并没有取到值,这还是因为写入的时候没有给空格,无法区分
这样就成功显示啦。
这个知识点在网络传输部分非常有用,比如果我给你聊天法消息,那么这个数据怎么传输过去呢?一般这个聊天消息至少包含两个信息,如名字+时间+内容+其它。一般我们是在程序里面定义一个结构体来包含这样的信息,如果要发过去信息,如名字和年龄发过去,我们发过去的都是数据流,网络和文件是一样的,都要发过去字节流,都要把信息转成字符串。如果用二进制将这个结构体信息发过去,这样抓包就看不见实际内容。所以要转成字节流,如果是字符串就拼接到一起,整型就自己转成字符串,传过去之后再自己解析,很麻烦。但是有了ostringstream和istringstream后,无论结构体有多少个值,我都像流一样弄到对象里面去,另外一边接收到字符串后,接受方要知道二者之间的约定,字符串是如何传输定义的,包括它包含了什么信息。再把字符串放给istringstream去解析,完成信息的网络传输。
其实这一系列的过程有一个专业的名称,序列化和反序列化。
#include <sstream>
struct PersonInfo
{
string _name;
int _age;
Date _d;
};
int main()
{
PersonInfo info = { "张三", 18 };
ostringstream oss;
oss << info._name <<" "<< info._age;
string str = oss.str();
istringstream iss(str);
string name;
int age;
iss >> name >> age;
return 0;
}