C++初阶 -- C++的IO流

目录

1.C语言的输入输出

2.流是什么

C++IO流

3.C++标准IO流

 C++文件IO流

C语言读写方法

 C++读写方法

 sstream



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;
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值