1.类型转换
什么是类型转换?我们知道有些数字类型可以相互转换,如double类型可以转换为int类型,这样的转换会发生切割将double类型的小数部分切割掉丢失精度;还有在前面的多态那块有一个虚函数指针表,这个虚函数指针表存储在类最前面的4到8个字节;我们想获得这个地址于是用指针强制转换类的地址为虚函数的函数指针类型,从而获得虚函数指针表的首地址;这也是一种类型转换,但这样的类型转换是需要带()强转的;
1.1 C语言类型转换
1.隐式类型转换
上面说到了我们以往的类型转换一般都是直接将某个类型的值赋给另一个类型,这样就完成了类型转换,这其实是编译器底层进行了隐式类型转换,让具有相似概念的数据可以转换;
double a=10.21;
int b=a;
//这样的转换伴随着精度的降低
这样的代码在编译器上是可以直接编译通过的,是编译器进行了隐式类型转换;
2.显式类型转换(强转)
还有一些类型是不能直接转换的,就需要进行显式类型转换(强转):
int a = 10;
int b = (int)&a;//这样的转换使得&a地址数据被截断了
原本a的地址是没有办法传递给b的但是由于,()中的强制类型转换,使得b强行获得了a的地址,但由于a地址数据大于int,所以截断a地址数据赋值给b;
类型的转换有时是存在问题的,我们应该严谨的对待类型的转换,防止出现不必要的错误;
1.2 C++类型转换
C++继承了C的所有内容,自然上面的转换C++也是拥有的,但是C++祖师爷也研发了C++自己的类型转换方式,这些转换方式有作用,但作用不是非常大,其目的重要之处是为了规范程序员编写代码的行为,在有些管理严格的计算机公司会要求程序员在类型转换时按照必须下面的四种方式进行转换;不要使用如()的不明确的转换;
1.2.1 static_cast
当转换类型与被转换数据类型有相似的意义时,如不同类型指针的转换,整数与浮点数之间的转换,这些转换都是具有相似意义的转换;
double a = 10.21;
int b = static_cast<int>(a);在linux线程函数中经常用到将空指针转换为某种数据类型的指针:
void*args;
threadData* data=static_cast< threadData*>(a);
1.2.2 reinterpret_cast
这个转换代表着将有一定的关联但是没有相似意义的两个类型进行转换,通常用来表示两个完全不同类型的转换:
int a = 10;
int b = reinterpret_cast<int>(&a);double c = 10.11;
char d = reinterpret_cast<int>(&c);
1.2.3 const_cast
这个转换一般用来删除具有常性数据的常性:
//去除优化,防止常量替换
volatile const int a = 10;
int* b = const_cast<int*>(&a);//const_cast<>中的类型必须是指针或者引用
测试时发现的小问题:
在vs2022与linux下都存在常量替换的优化所以用volatile去掉优化效果;
另外在测试的过程中还出现了cout<<&a输出1的<<输出流类型匹配不明确的问题;
1.2.4 dynamic_cast
这个转换是用来对父类引用或指针进行安全转换的,有下面几个需要注意的地方;
1.被转换的父类必须是指针或者引用;
2.被转换的父类指针与引用接收的是子类时才可以成功转换,否则会返回空指针或者抛出std::bad_cast异常;(向下转换)
3.父类成员函数中必须有虚函数;
//测试dynamic_cast
class A {
public:virtual void fun()//使用dynamic_cast转化的父类必须包含虚函数{cout << "A()" << endl;}
private:int _a;
};class B :public A
{
public:void fun(){cout << "B()" << endl;}
private:int _b;
};void getfun(A* a)
{B* b = dynamic_cast<B*>(a);if (b){cout << "success" << endl;}else{cout << "fail" << endl;}
}void test5()
{A a;B b;getfun(&a);getfun(&b);
}
上面就是C++四大类型转换的基本内容
2.特殊类设计
2.1只能在堆上创建的类
法1:将构造函数设置为私有成员,将拷贝构造与赋值重载删去防止使用其在栈上构造,最后创建一个静态成员函数,使得可以调用此接口在堆上创建对象;
法2:将析构函数设置为私有成员,剩余步骤同上;
下面使用的是法1:
//只能在堆上创建的类
class onlyHeap {
public:static onlyHeap* getonlyHeap(int a = 10){return new onlyHeap(a);}void deleteData() {//进行析构cout << "~onlyHeap()" << endl;}
private:onlyHeap(int a = 10):_a(a){cout << "onlyHeap()" << endl;}onlyHeap(const onlyHeap& a) = delete;onlyHeap& operator =(const onlyHeap& a) = delete;int _a;
};
2.2只能在栈上创建类
1.也是一样将构造函数私有化,并写一个静态的创建类对象的接口即可。
2.最为保险是将类中的operator new与operator delete也禁掉 ;
//设计只能在栈上创建的类
class onlyStack {
public:static onlyStack getonlyStack(int a = 10){onlyStack s;return s;}~onlyStack() {//进行析构cout << "~onlyStack()" << endl;}//可以直接禁掉下面两个函数也可做到只能在栈上创建void* operator new(size_t size) = delete;void operator delete(void * d) = delete;
private:onlyStack(int a = 10):_a(a){cout << "onlyStack()" << endl;}int _a;
};void test7()
{onlyStack s=onlyStack::getonlyStack();//下面无法创建//onlyStack s1 = new onlyStack;
}
2.3让一个类无法被继承
1.使用final关键字,在类名后声明,使得这个类无法再被继承;
2.私有化构造函数,让子类无法调用
2.4让一个类禁用拷贝构造
1.delete删除默认拷贝构造与赋值重载;
2.声明并私有
2.5单例模式与懒汉饿汉方式
这其实在我前面的linux博客中就有说明过,可以联系前面linux的博客理解;
单例模式是编程设计模式中23种中的一种;
饿汉单例:
//C++下实现单例饿汉模式
class hangry{
public:static hangry* getHangry(){return &_h;}hangry(const hangry& h) = delete;hangry& operator=(const hangry& h) = delete;~hangry(){cout << "~hangry()" << endl;}
private:hangry(){cout << "hangry()" << endl;}static hangry _h;//静态变量在程序创建时一同创建
};
hangry hangry::_h;//这里得初始化一下,不然没法创建int main()
{return 0;
}
懒汉单例在前面linux那实现过完成的了,这里就不是实现了;
3.C++中的IO流
3.1 C++中庞大的IO流体系
什么是流?我们可以理解为具有方向,进行连续传递的数据叫做流;
在我们C++库中有一个非常大的体系是关于IO流,数据可以通过这个IO流的封装进行流动,下面是这个体系的大致构成:
ios是io流体系中所有类的基类,所有类都继承与ios类(ios_base是由于不同国家编码格式和一些设置存在所以为基类的基类);我们经常使用是iostream类是继承istrem和ostrem,这里有一个菱形继承,存在数据冗余和二义性的问题,要通过虚基表来解决;后面的文件流类fstream也是继承与istream与ostream;字符串流同理;下面说一些值得注意的地方:
1.上面的这些类有很多接口,我们虽然不常用但这些接口是真正存在的:
比如istream类,它有一个我们经常用到的对象cin,它是istream中实例化出的一个全局对象,istream中有非常多的接口,我们的cin也就可以调用这些接口:
2.我们经常使用的cin,cout,cerro这些都是实例化出的全局对象,是标准库的实现自动完成
3.缓冲区问题,我们输入输出的数据会先进入缓冲区再放入内存中的文件或者我们开辟的数据空间中,而cin,cout,cerro这些在读取数据时会通过一些分割符来读取,当遇到回车或者空格时会刷新缓冲区将数据读入;所以由此同理C语言也是有这样的缓冲区的是与C++的缓冲区独立的,一般我们程序会自动维护C语言与C++缓冲区同步,我们可以调用iostream类中的sync_with_stdio接口关闭同步提高效率,但是这样也可能会出现数据不同步导致的问题,当只使用C或C++接口时可以调用sync_with_stdio(false)关闭,提高效率;
3.2运算符重载与类型转换运算符(类型重载)
3.2.1 运算符重载
在我们很早之前刚开始学习C++的时候,我们就学过了运算符重载,我们知道可以通过重载运算符的行为来实现我们想要的动作,其实不仅仅我们是这么做的,库中也是这么做的,我们定义的是自定义类型,输出我们自定义的数据,而库中定义的是内置类型,输出的是内置类型的数据;而想要对这些数据用运算符进行操作,也就调用是库中所重载的行为,所以其实内置类型也不是什么很高大上的存在,也和我们实现的内置类型一般,重载了运算符的操作,不过是库来实现重载的罢了,我们不知道它底层如何实现的而已;
重载实现与调用的过程 :
上面我们说完了运算符重载我们下面谈一谈类型重载,在上面的内容中,我们学到了C++的四大类型转换方式,这些转换方式是让转换更加清晰明了;这样的转换可以进行的原因是为什么呢?
3.2.2类型转换
1.自定义类型<——内置类型
2.自定义类型<——自定义类型
上面这两种转换为什么可行,这样的转换是因为我们实现了自定义类型的构造函数,让后边的这些被转换类型作为参数给自定义类型,从而调用自定义类型的构造函数例如:
// 类型转换与类型重载
class test_string
{
public:test_string(const char* str){_str = new char[10];strcpy(_str,str);}char* _str;
};void test8()
{test_string s = "hello";cout << s._str << endl;
}
test_string s = "hello";
上面的const char*内置类型转换为了test_string自定义类型就是因为构造函数的存在,使得内置类型找到了自自定义类型的构造,从而调用;那么自定义类型转换为自定义类型也是同理的;
可以还有两种转换下面这样的类型是如何进行转换的呢:
1.内置类型<——内置类型
2.内置类型<——自定义类型
while(sacnf)输出的小例子:
我们在最前面学习c语言的时候题目时是不是会遇到这样的重复输入:
while(scanf("%d",&a)!=EOF){}
当一个程序有这样的代码时,我们哪个时候怎么输入都无法正常退出,除非我们关闭进程;后来随着我们慢慢的学习知道了ctrl+c和ctrl+z(linux下为ctrl+d)可以退出我们的while循环结束进行,但还是不知道其正常退出的原因,再后来我们学习了linux信号,知道了ctrl+c是发送信号杀死进程;而现在我们将解开ctrl+z的面纱;ctrl+z其实就是我们上面while的判断条件EOF,当我们按下ctrl+z时会发送EOF文件结束符到内存中,while识别到了EOF自然就退出进程了;
小tip:
我在vs2022下操作发现,输入三次ctrl+z后再输入回车才正常退出,我也很好奇,为什么要输入三次才判断退出,非常奇怪,我在codeblock和linux下进行都只需要输入一次就会正常退出;
那上面的操作和我们的类型转换有什么关系呢?
int scanf(const char *format, ...);
int是scanf的返回值,当scanf读取到EOF时也会返回EOF从而让while的条件判断为假,从而退出;而我们c++中while(cin>>a) 这也是具有相同效果的而cin>>的返回值是一个istream对象,这个istream对象为什么可以作为bool内置类型的值来进行判断空与非空呢?
3.2.3 类型转换运算符
这里就是问题的关键所在,因为istream类中存在一个类型转换的重载:
将istream的类型直接转换为了bool,从而进行判断;
我们的内置类型数字0为falsee非0为true,指针空指针为false,非空指针为true,它们其实在内部也是进行了类型重载的,可以将我们的类型重载为bool类型,从而进行空与非空的判断;
看下面的代码实现:
// 类型重载
struct A {A(int a=0):_a(a){}operator bool(){if (_a == 0)return false;elsereturn true;}operator int(){return _a;}int _a;
};
void test9()
{A a(100);if (a)cout << "true" << endl;elsecout << "false" << endl;int b = a;cout << b << endl;
}
上面的现象中自定义类型也可以作为bool值来进行判断了,这就是类型重载;
同理转换为其他内置类型也是可以的:
这就是类型的重载
3.3 C++文件流
我们前面学过C语言的文件操作,也学习了linux下底层的文件操作,现在我们来看看C++面向对象语言的文件操作是怎么样的:
C++中文件流依然是被封装成为了三个类,ifstream和ofstream以及iofstream;
3.3.1 二进制读写操作
// 文件流操作
void test10()
{ofstream of("text.txt", ios_base::out | ios_base::binary);const char* str = "你好!看得到我吗?";of.write(str, strlen(str));of.close();//一定一定记得要关闭上面的文件,否则会对文件进行上锁,使得无法再次以读形式打开ifstream in("text.txt", ios_base::in | ios_base::binary);char buffer[100] = { 0 };in.read(buffer, sizeof(buffer));cout << buffer << endl;
}
这样我们成功的读到了数据:
但是二进制的读写存在一些问题,如果读写的是一个容器,或者是堆上开辟的空间,传递的是指针,会发生这样的问题:
改写代码,写入数据有容器string
myData类实现
class myData {
public:myData(const string& str = ""):_str(str){}~myData(){cout << "~myData()" << endl;printf("%p\n", _str.c_str());}string _str;
};
ostream& operator<<(ostream& of,const myData& d)
{return of << d._str;}istream& operator>>(istream& in, myData& d)
{return in >> d._str;
}
void test11()
{myData d("hellohellohellohellohellohellohellohellohellohellohellohellohellohello");ofstream of("text.txt", ios_base::out | ios_base::binary);of.write((char*)&d, sizeof(d));of.close();//一定一定记得要关闭上面的文件,否则会对文件进行上锁,使得无法再次以读形式打开ifstream in("text.txt", ios_base::in | ios_base::binary);myData d1;in.read((char*) & d1, sizeof(d1));cout << d1._str << endl;
}
这下面之所以要给d这么长的字符,是因为string在vs2022的底层实现下是存在一个buffer的,这个buffer是在数据量小的时候就会在buffer中创建数据而不是通过指针在堆上创建,buffer是直接存在于string类对象中的,如果不写这么长的字符,就无法看到两次析构的现象;
所以在有容器写入时我们一定要注意要进行文本读写;
3.3.2 文本读写操作
//文本读写操作
void test12()
{myData d("hellohellohellohellohellohellohellohellohellohellohello");ofstream of("text.txt");//默认以文本形式写入of << d;of.close();ifstream in("text.txt", ios_base::in | ios_base::binary);myData d1;in >> d1;cout << d1 << endl;
}
总结文件流操作:
1.C++文件流操作中创建ofstream与ifstream对象,它们默认创建都是以文本读或者文本写的形式创建的,如果想切换为二进制读写需要向上面那样带上两个设置的 |(与)
2.我们在进行文件操作的时候,如果创建了一个文件对象,我们使用完之后一定要调用close接口来关闭使用完了的对象,否则对象占用文件会导致其他对象无法打开这个文件完成操作;
3.文件写入操作要记得带上分割符,我们可以自己设置也可以使用默认的空格,回车,制表符;但是需要注意一旦我们是定义的自己设置的分割符,那么,我们在文本读的时候要使用getline来设置第三个参数作为分割符,否则会发生想不到的错误,下面的stringstream也有写当没有分隔符时所产生的现象;
4.对于容器的读写只能使用文本读写,使用二进制读写会发生野指针的问题;
5.文本读写有时候放在记事本中会有乱码,这是因为记事本的编码格式和你编译器编译的可执行程序编码格式不同,就像vs2022是utf-8编码而记事本一般是utf-16,改写编码格式即可;
3.4 stringstream类
3.4.1 将各种数据类型转换为字符串类型
//将各种数据类型转换为字符串类型
void test14()
{int a = 10;double b = 10.12;bool c = true;char d = 'a';stringstream ss;ss << a << b << c << d;cout << ss.str() << endl;
}
现象:
如果想把放入了stringstream中的数据再放回原有类型,要记得str("")置空stringstream底层的string字符串与clear()清空某些控制才能让 stringstream多次进行转换;
stringstream ss;
。。。。。转换操作
ss.str(""); //多次转换操作中间都必须得有这两步操作;
ss.clear();
。。。。。转换操作
3.4.2 序列化与反序列化
stringstream是字符串流类,在C语言中对应的是sprintf和sscanf,用来将数据转换为字符串格式;我们也将数据转换为字符串称为序列化,将字符串转化为数据称之为反序列化;
不过序列化和反序列的操作有更好的方法来处理,stringstream只适用与简单的数据处理,可以用xml和json来处理,这里我并没有学过,只是听过所以还需要后续的学习来验证
接下来我们用stringstream来完成一下简单的序列化和反序列化操作:
//使用stringstream完成简单的序列化反序列化操作
class conmuni {
public:friend ostream& operator<<(ostream& of, const conmuni& d);friend istream& operator>>(istream& in, conmuni& d);conmuni(const string& id = "", double time = 0, const string& msg = ""):_id(id), _time(time), _msg(msg){}
private:string _id;double _time;string _msg;
};ostream& operator<<(ostream& of, const conmuni& d)
{//这里这些回车一起写入stringstream的底层中//当进行读时,这些回车就会作为分隔符读入//可以删除这些endl去调试看一下现象//of << d._id << d._time << d._msg;of << d._id << endl << d._time << endl << d._msg << endl;return of;
}istream& operator>>(istream& in, conmuni& d)
{in >> d._id >> d._time >> d._msg;return in;
}void test13()
{conmuni a("张三", 9.12, "今天真的好累哇!");//让回车作为分割符stringstream ss;conmuni a1;ss << a;cout << "序列化数据获得:" << endl << ss.str() << endl;cout << "反序列化获得:" << endl;ss >> a1;cout << a1;
}
现象:
我们由此可知ss底层中的string是
string="张三\n9.12\n今天真的好累哇!\n\0";
我们上面的operator<<操作如果去掉所有的endl我们调式会看到这样的现象:
of << d._id << d._time << d._msg;
关于>>与<<重载的思考:
在上面的ss>>与ss<<操作上,其实我花了很长的时间思考为什么ss可以调用到我们写的
istream& operator>>(istream& in, conmuni& d)重载
ostream& operator<<(ostream& of, const conmuni& d)重载
1.明明我们写的重载是iostream类的对象呀;为什么stringstream类调用了这个重载呢?
其实这就是继承所带来的优势,因为stringstream是iostream的子类,所以stringstream作为子类对象赋值给父类的引用,从而找到了这个两个符合条件的函数,
2.之后为什么内部的实现又是stringstream对象完成的呢明明第一个参数是父类的引用呀?
这就涉及到了运算符重载的另一个内容重载解析,编译器对于运算符重载函数会通过这个iostream&中实际的对象来寻找匹配的函数;而iostream&中实际的对象是stringstream,所以调用的内部的实现是stringstream的>>与<<;
通过上面的两步操作成功的实现了类似与多态的行为,一样的操作可以调用不同的函数重载;