【C++】类型转换和IO流

C++完结

文章目录


前言

首先我们看看C语言中的类型转换:

C 语言中,如果 赋值运算符左右两侧类型不同,或者形参与实参类型不匹配,或者返回值类型与
接收返回值类型不一致时,就需要发生类型转化 C 语言中总共有两种形式的类型转换: 隐式类型
转换和显式类型转换
1. 隐式类型转化:编译器在编译阶段自动进行,能转就转,不能转就编译失败
2. 显式类型转化:需要用户自己处理
int main()
{
	int i = 1;
	// 隐式类型转换
	double d = i;
	printf("%d, %.2f\n", i, d);
	int* p = &i;
	// 显示的强制类型转换
	int address = (int)p;
	printf("%x, %d\n", p, address);
}

对于上面这种C语言的类型转换其实是有一些缺陷的,转换的可视性比较差,所有的转换形式都是以一种相同形式书写,难以跟踪错误的转换。

下面我们看看C++中对于类型转换的修改


一、C++的四种类型转换

为什么C++需要重新改进类型转换呢?

C 风格的转换格式很简单,但是有不少缺点的:
1. 隐式类型转化有些情况下可能会出问题:比如数据精度丢失
2. 显式类型转换将所有情况混合在一起,代码不够清晰
因此 C++ 提出了自己的类型转化风格,注意 因为 C++ 要兼容 C 语言,所以 C++ 中还可以使用 C 语言的
转化风格
标准 C++ 为了加强类型转换的可视性,引入了四种命名的强制类型转换操作符:
static_cast reinterpret_cast const_cast dynamic_cast

第一种:static_cast

static_cast 用于非多态类型的转换(静态转换),编译器隐式执行的任何类型转换都可用
static_cast ,但它不能用于两个不相关的类型进行转换

下面我们用代码演示一下:

int main()
{
	int i = 1;
	double d = static_cast<double>(i);
	float c = static_cast<float>(d);
	return 0;
}

我们只需要记住:static_cast适用于可以隐式转换的类型。

第二种:reinterpret_cast

reinterpret_cast 操作符通常为操作数的位模式提供较低层次的重新解释,用于将一种类型转换
为另一种不同的类型
下面我们用代码演示一下:
int main()
{
	int i = 1;
	int* p = &i;
	int t = reinterpret_cast<int>(p);
	cout << t << endl;

	int c = 10;
	int* d = reinterpret_cast<int*>(c);
	cout << d << endl;
	return 0;
}

 对于reinterpret_cast的使用我们只需要记住适用于不同类型之间的转换即可。

第三种:const_cast

const_cast 最常用的用途就是删除变量的 const 属性,方便赋值.
int main()
{
	const int a = 2;
	int* p = const_cast<int*>(&a);
	cout << *p << endl;
	*p = 10;
	cout << *p << endl;
	return 0;
}

对于const类型的常变量一般是不能直接修改的,但是可以像我们上面那样间接的修改,主要还是因为常变量是放在栈中的不是放在常量区的,注意:常量区是一定不可以修改的。

下面我们看一道常考的题:

int main()
{
	const int a = 2;
	int* p = const_cast<int*>(&a);
	*p = 3;
	cout << a << endl;
	cout << *p << endl;
	return 0;
}

 上面这段代码的运行结果是什么?很多人都会以为是3和3,因为p指向a的空间,修改*p那么a中的内容也会被修改,思路没错,但是没有考虑到编译器的优化,这道题的正确答案是2 3。

由于const变量在编译器看来是不会被修改的,所以本来应该先从内存读数据到寄存器结果被编译器优化为直接从寄存器拿数据。对于这种情况我们只需要让编译器不优化,这样的话编译器就会从内存中读取a的内容打印了:

 可以看到这次的结果就正确了。所以对于const_cast转化是将原本const属性去掉单独分出来,这个时候我们就应该小心了,const变量尽量不要去改变。

第四种:dynamic_cast

dynamic_cast 用于将一个父类对象的指针 / 引用转换为子类对象的指针或引用 ( 动态转换 )
向上转型:子类对象指针 / 引用 -> 父类指针 / 引用 ( 不需要转换,赋值兼容规则 )
向下转型:父类对象指针 / 引用 -> 子类指针 / 引用 ( dynamic_cast 转型是安全的 )
注意:
1. dynamic_cast 只能用于父类含有虚函数的类
2. dynamic_cast 会先检查是否能转换成功,能成功则转换,不能则返回 0

 我们可以看到父类指针是天然可以接收子类的指针或者引用的,那么如果是将父类给子类呢?

 可以看到如果我们不加类型转化的话连编译都过不了,那么我们用类型转换再试试:

 可以看到经过类型转换后是没问题的,并且dynamic_cast在转换中会保证安全性。下面我们看看如果不用dynamic_cast转化会出现什么情况:

class A
{
public:
	virtual void f() {}
	int _a = 0;
};
class B : public A
{
public:
	int _b = 0;
};
void Func(A* ptr)
{
	B* btr = (B*)ptr;
	cout << btr << endl;
	btr->_a++;
	btr->_b++;
	cout << btr->_a << endl;
	cout << btr->_b << endl;
}
int main()
{
	A aa;
	B bb;
	Func(&aa);
	Func(&bb);
	return 0;
}

 我们可以看到直接出错了,下面我们用安全转换试一下:

我们可以看到当子类接收父类指针造成越界的时候安全转化会检查能否成功转化,对于不能成功转化的直接返回0就像上图一样。

总结:

Func中的ptr如果是指向子类对象,那么转回子类类型是没问题的。 

ptr如果是指向父类对象,那么转回子类类型是存在越界风险的。

注意
强制类型转换关闭或挂起了正常的类型检查 ,每次使用强制类型转换前,程序员应该仔细考虑是
否还有其他不同的方法达到同一目的,如果非强制类型转换不可,则应限制强制转换值的作用
域,以减少发生错误的机会。 强烈建议:避免使用强制类型转换

以上就是C++类型转换的全部内容了,下面我们进入IO流的学习。

二、C++IO流

C 语言的输入与输出:
C 语言中我们用到的最频繁的输入输出方式就是 scanf () printf() scanf(): 从标准输入设备 (
) 读取数据,并将值存放在变量中 printf(): 将指定的文字 / 字符串输出到标准输出设备 ( 屏幕 )
注意宽度输出和精度输出控制。 C 语言借助了相应的缓冲区来进行输入与输出。
输入输出缓冲区 的理解:
1. 可以 屏蔽掉低级 I/O 的实现 ,低级 I/O 的实现依赖操作系统本身内核的实现,所以如果能够屏
蔽这部分的差异,可以 很容易写出可移植的程序
2. 可以 使用这部分的内容实现 读取的行为 ,对于计算机而言是没有 这个概念,有了这
部分,就可以定义 的概念,然后解析缓冲区的内容,返回一个
流是什么:
即是流动的意思,是物质从一处向另一处流动的过程 ,是对一种 有序连续 具有方向性
其单位可以是bit,byte,packet )的 抽象描述。
C++ 流是指信息从外部输入设备(如键盘)向计算机内部(如内存)输入和从内存向外部输出设
备(显示器)输出的过程。这种输入输出的过程被形象的比喻为
它的 特性 是: 有序连续 具有方向性
为了实现这种流动, C++ 定义了 I/O 标准类库,这些每个类都称为流 / 流类,用以完成某方面的功
能。
C++IO流:
C++ 系统实现了一个庞大的类库,其中 ios 为基类,其他类都是直接或间接派生自 ios 类。

 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 可以直接输入和输出内置类型数据,原因: 标准库已经将所有内置类型的输入和
输出全部重载了 :

下面我们看看OJ中的循环输入:

int main()
{
	string str;
	while (cin >> str)
	{
		cout << str << endl;
	}
	return 0;
}

不知道我们是否会有疑问>>符号是如何像逻辑判断操作符那样在循环体中进行判断的呢?

 我们可以看到>>符号的返回值是istream&,也不是bool类型,下面我们看文档:

 其实在文档中我们发现不管是C++11还是C++98都重载了operator bool,重载的目的就是支持自定义类型隐式转换为自定义类型,也就是说支持将istream&转化为bool类型。当然其实日常使用中我们看到最多的是内置类型隐式转换成自定义类型,比如下面这样:

class A
{
public:
	A(int a)
		:_a1(1)
		, _a2(2)
	{}
private:
	int _a1;
	int _a2;
};
int main()
{
	A aa = 1;
	return 0;
}

 上图中我们的aa就是自定义类型,1是内置类型,将1给aa的过程中发生了隐式类型转化,由内置类型转化为自定义类型。

 上图中我们可以看到,正常情况下我们是不能将自定义类型转化为内置类型的,但是当我们重载了int的转换后就可以了:

    operator int()
	{
		return _a1 + _a2;
	}

 下面我们将日期类实现为像>>一样可以判断的:

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 true;
		}
		else
		{
			return false;
		}
	}
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;
}

int main()
{
	Date d1(0, 5, 30);
	while (d1)
	{
		cout << d1 << endl;
	}
	return 0;
}

我们重载bool类型的时候直接用year做判断了这里只是为了演示,对于年份为0的日期进入while循环后会直接退出,如果是年份非0的日期则会死循环的打印。

C++文件IO流:

C++ 根据文件内容的数据格式分为 二进制文件 文本文件 。采用文件流对象操作文件的一般步
骤:
1. 定义一个文件流对象
ifstream ififile( 只输入用 )
ofstream ofifile( 只输出用 )
fstream iofifile( 既输入又输出用 )
2. 使用文件流对象的成员函数打开一个磁盘文件,使得文件流对象和磁盘文件之间建立联系
3. 使用提取和插入运算符对文件进行读写操作,或使用成员函数进行读写
4. 关闭文件
struct ServerInfo
{
 char _address[32];
 int _port;
 Date _date;
};
struct ConfigManager
{
public:
 ConfigManager(const char* filename)
 :_filename(filename)
 {}
 void WriteBin(const ServerInfo& info)
 {
 ofstream ofs(_filename, ios_base::out | ios_base::binary);
 ofs.write((const char*)&info, sizeof(info));
 }
 void ReadBin(ServerInfo& info)
 {
 ifstream ifs(_filename, ios_base::in | ios_base::binary);
 ifs.read((char*)&info, sizeof(info));
 }
 void WriteText(const ServerInfo& info)
 {
 ofstream ofs(_filename);
 ofs << info._address << " " << info._port<< " "<<info._date;
 }
 void ReadText(ServerInfo& info)
 {
 ifstream ifs(_filename);
 ifs >> info._address >> info._port>>info._date;
 }
private:
 string _filename; // 配置文件
};

上面是一个文件管理的类,里面封装了二进制读写和文本读写的接口,由于C++IO流属于了解性的内容所以我们就不再详细的讲解每个函数,有不会的接口大家自行查文档即可。下面我们用一个实例使用一下以上的接口:

int main()
{
 ServerInfo winfo = { "192.0.0.1", 80, { 2022, 4, 10 } };
 
    // 二进制读写
 ConfigManager cf_bin("test.bin");
 cf_bin.WriteBin(winfo);
 ServerInfo rbinfo;
 cf_bin.ReadBin(rbinfo);
 cout << rbinfo._address << " " << rbinfo._port <<" "
<<rbinfo._date << endl;
    // 文本读写
 ConfigManager cf_text("test.text");
 cf_text.WriteText(winfo);
 ServerInfo rtinfo;
 cf_text.ReadText(rtinfo);
 cout << rtinfo._address << " " << rtinfo._port << " " <<
rtinfo._date << endl;
 return 0;
}
C++ 文件流的优势就是可以对内置类型和自定义类型,都使用 一样的方式,去流插入和流提取数据,当然这里自定义类型Date 需要重载 >> << 。
istream& operator >> (istream& in, Date& d)
ostream& operator << (ostream& out, const Date& d)
stringstream 的简单介绍
C 语言中,如果想要将一个整形变量的数据转化为字符串格式,如何去做?
1. 使用 itoa() 函数
2. 使用 sprintf() 函数
但是两个函数在转化时,都得 需要先给出保存结果的空间 ,那空间要给多大呢,就不太好界定,
而且 转化格式不匹配时,可能还会得到错误的结果甚至程序崩溃
C++ 中,可以使用 stringstream 类对象来避开此问题。
在程序中如果想要使用 stringstream ,必须要包含头文件 。在该头文件下,标准库三个类:
istringstream ostringstream stringstream ,分别用来进行流的输入、输出和输入输出操
作,下面主要介绍 stringstream
stringstream 主要可以用来:

1. 将数值类型数据格式化为字符串

#include<sstream>
int main()
{
 int a = 12345678;
 string sa;
 // 将一个整形变量转化为字符串,存储到string类对象中
 stringstream s;
 s << a;
 s >> sa;
    // clear()
    // 注意多次转换时,必须使用clear将上次转换状态清空掉
    // stringstreams在转换结尾时(即最后一个转换后),会将其内部状态设置为badbit
    // 因此下一次转换是必须调用clear()将状态重置为goodbit才可以转换
    // 但是clear()不会将stringstreams底层字符串清空掉
    
    // s.str("");
 // 将stringstream底层管理string对象设置成"", 
 // 否则多次转换时,会将结果全部累积在底层string对象中
    
 s.str("");
 s.clear();   // 清空s, 不清空会转化失败
 double d = 12.34;
 s << d;
 s >> sa;
 string sValue;
 sValue = s.str();   // str()方法:返回stringsteam中管理的string类型
 cout << sValue << endl; 
 return 0;
}

2. 字符串拼接

int main()
{
 stringstream sstream;
 // 将多个字符串放入 sstream 中
 sstream << "first" << " " << "string,";
 sstream << " second string";
 cout << "strResult is: " << sstream.str() << endl;
 // 清空 sstream
 sstream.str("");
 sstream << "third string";
 cout << "After clear, strResult is: " << sstream.str() << endl;
 return 0;
}

3. 序列化和反序列化结构数据

struct ChatInfo
{
 string _name; // 名字
 int _id;      // id
 Date _date;   // 时间
 string _msg;  // 聊天信息
};
int main()
{
 // 结构信息序列化为字符串
 ChatInfo winfo = { "张三", 135246, { 2022, 4, 10 }, "晚上一起看电影吧"
};
 ostringstream oss;
 oss << winfo._name << " " << winfo._id << " " << winfo._date << " "
<< winfo._msg;
 string str = oss.str();
 cout << str << endl<<endl;
 // 我们通过网络这个字符串发送给对象,实际开发中,信息相对更复杂,
    // 一般会选用Json、xml等方式进行更好的支持
 // 字符串解析成结构信息
 ChatInfo rInfo;
 istringstream iss(str);
 iss >> rInfo._name >> rInfo._id >> rInfo._date >> rInfo._msg;
 cout << "-------------------------------------------------------"
<< endl;
 cout << "姓名:" << rInfo._name << "(" << rInfo._id << ") ";
 cout <<rInfo._date << endl;
 cout << rInfo._name << ":>" << rInfo._msg << endl;
 cout << "-------------------------------------------------------"
<< endl;
 return 0;
}
注意:
1. stringstream 实际是在其底层维护了一个 string 类型的对象用来保存结果
2. 多次数据类型转化时,一定要用 clear() 来清空,才能正确转化 ,但 clear() 不会将
stringstream 底层的 string 对象清空。
3. 可以 使用 s. str("") 方法将底层 string 对象设置为 "" 空字符串
4. 可以 使用 s.str() 将让 stringstream 返回其底层的 string 对象
5. stringstream 使用 string 类对象代替字符数组,可以避免缓冲区溢出的危险,而且其会对参
数类型进行推演,不需要格式化控制,也不会出现格式化失败的风险 ,因此使用更方便,更
安全。


总结

C++IO流这部分知识大多都是偏了解性的知识,所以我们没有在细细的讲解,实际上学到这一部分很多人对C++都基本入门了,这个时候是完全有能力通过官方文档来自己运用这部分内容。

猜你喜欢

转载自blog.csdn.net/Sxy_wspsby/article/details/131276150