我们在C语言中经常会使用到强制类型转换,例如指针和整形之间的转换是最为常见的,但是
在C++中,C++设计师认为这种强制类型转换是不安全的,所以在C++标准中加入了四种强制
类型转换风格,这就是我将要介绍的强制类型转换。
在某些场景中,我们可能需要一些特殊的类来让我们的代码能够更加符合场景,比如只能在栈
上创建对象,或者只能在堆上常见对象等等场景,而其中尤为出名的一种特殊类,也被纳入设计
模式中,那就是单例模式。
1. 特殊类设计
a. 不能能被拷贝的类
拷贝只会放生在两个场景中:拷贝构造函数以及赋值运算符重载,因此想要让一个类禁止拷贝,只需让该类不能调用拷贝构造函数以及赋值运算符重载即可。
由于这两个函数是默认成员函数,你不写它会自动生成,所以我们需要显式地写出来,并且是具有私有属性:
cpp
class A
{
public:
A(int a = 0)
:_a(a)
{}
private:
A(const A& a);
A& operator=(const A& a);
int _a;
};
而在C++11中delete作用于函数有着新的作用,那就是不生成该函数,所以:
cpp
class A
{
public:
A(int a = 0)
:_a(a)
{}
A(const A& a) = delete;
A& operator=(const A& a) = delete;
private:
int _a;
};
这个的话私有共有无所谓了。
b. 只能在堆上创建的对象
我们想要创建一个对象无非就这两种方式:
cpp
int main()
{
A aa;//栈上
A* paa = new(A);//堆上
return 0;
}
现在要让我们不能在栈上创建对象,一定是从构造函数找出发点,首先构造函数不能直接地被调用,说明他应该是私有成员,但是他是私有成员后,我们new一个对象的时候也需要构造函数,这样也不能在堆上创建对象了,这个时候我们就需要在类中再写一个函数,由于类内是没有私有公有一说的,所以类内成员函数可以随意的调用类内的任意成员函数,所以:
cpp
class A
{
public:
static A* getObj(int a = 0)
{
A* pa = new A(a);
return pa;
}
private:
A(int a = 0) :_a(a){};
int _a;
};
这样写完之后我们发现,我们要使用这个函数先得需要一个对象,但是我们又创建不了对象,自相矛盾了,所以我们需要将这个函数设置为静态的:
cpp
class A
{
public:
static A* getObj(int a = 0)
{
A* pa = new A(a);
return pa;
}
private:
A(int a = 0) :_a(a){};
int _a;
};
还有问题,如果是这样呢:
所以还需要禁用拷贝构造和赋值重载:
cpp
class A
{
public:
static A* getObj(int a = 0)
{
A* pa = new A(a);
return pa;
}
private:
A(int a = 0) :_a(a){};
A(const A& a) = delete;
A& operator=(const A& a) = delete;
int _a;
};
c. 只能在栈上创建对象
我们肯定还是在构造函数上找突破口:
cpp
class A
{
public:
static A getObj()
{
A a;
return a;
}
private:
A(int a = 0) :_a(a) {};
int _a;
};
而对于这样的方式,有人会说会不会效率变慢了,如果对象需要在堆上开辟空间的话,不要忘了我们可是有右值引用的:
这个现象需要在VS2019或者更低版本的编译器上进行,VS2022优化有点高,看不到这种现象。
cpp
#include <iostream>
using namespace std;
class A
{
public:
static A getObj()
{
A a;
return a;
}
A(A&& a)
{
cout << "右值拷贝构造" << endl;
swap(_a, a._a);
}
private:
A(int a = 0) :_a(new int[a]) {};
int* _a;
};
int main()
{
A a = A::getObj();
//A* pa = new A;
return 0;
}
d. 只能创建一个对象(单例模式)
关于这一个设计,这是一种设计模式,因为它经常出现在各种编程场景中,被人们广泛使用。
设计模式:
设计模式(Design Pattern)是一套被反复使用、多数人知晓的、经过分类的、代码设计
经验的总结。为什么会产生设计模式这样的东西呢?就像人类历史发展会产生兵法。最开
始部落之间打仗时都是人拼人的对砍。后来春秋战国时期,七国之间经常打仗,就发现打
仗也是有套路的,后来孙子就总结出了《孙子兵法》。孙子兵法也是类似。
使用设计模式的目的:
为了代码可重用性、让代码更容易被他人理解、保证代码可靠性。 设计模式使代码编写真
正工程化;设计模式是软件工程的基石脉络,如同大厦的结构一样。
单例模式:
一个类只能创建一个对象,即单例模式,该模式可以保证系统中该类只有一个实例,并提
供一个访问它的全局访问点,该实例被所有程序模块共享。比如在某个服务器程序中,该
服务器的配置信息存放在一个文件中,这些配置数据由一个单例对象统一读取,然后服务
进程中的其他对象再通过这个单例对象获取这些配置信息,这种方式简化了在复杂环境下
的配置管理。
而关于单例模式的设计有两种方法应对于不同的场景:
1). 饿汉模式
饿汉模式,他的意思就是在进入main函数之前,这个对象就已经被创建好了。
我们首先要想想,什么变量是在进入main函数之前就已经被创建好了呢?那肯定是全局变量啊,但是全局变量你要知道,它使用起来是有风险的,在多文件的时候尤为明显,可能会产生重定义的风险。所以我们是这样设计的:
cpp
class Singleton
{
public:
static Singleton& getExa()
{
return sl;
}
void Add(const pair<string, int> kv)
{
sl._hash.insert(kv);
}
void Print()
{
for (auto& e : sl._hash)
{
cout << e.first << " : " << e.second << endl;
}
cout << endl;
}
private:
map<string, int> _hash;
Singleton() {};
Singleton(const Singleton& sl) = delete;
Singleton& operator=(const Singleton& sl) = delete;
static Singleton sl;
};
Singleton Singleton::sl;
int main()
{
Singleton::getExa().Add({ "hello", 2 });
Singleton::getExa().Add({ "world", 1 });
Singleton::getExa().Add({ "xxx", 1 });
Singleton::getExa().Add({ "yyy", 10 });
Singleton::getExa().Print();
return 0;
}
2). 懒汉模式
这种设计模式就是在对象被调用的时候再创建:
cpp
class Singleton
{
public:
static Singleton* getExa()
{
if (sl == nullptr)
sl = new Singleton;
return sl;
}
void Add(const pair<string, int> kv)
{
sl->_hash.insert(kv);
}
void Print()
{
for (auto& e : sl->_hash)
{
cout << e.first << " : " << e.second << endl;
}
cout << endl;
}
private:
map<string, int> _hash;
Singleton() {};
Singleton(const Singleton& sl) = delete;
Singleton& operator=(const Singleton& sl) = delete;
static Singleton* sl;
};
Singleton* Singleton::sl = nullptr;
int main()
{
Singleton::getExa()->Add({ "hello", 2 });
Singleton::getExa()->Add({ "world", 1 });
Singleton::getExa()->Add({ "xxx", 1 });
Singleton::getExa()->Add({ "yyy", 10 });
Singleton::getExa()->Print();
return 0;
}
在饿汉模式中,对象的销毁在进程结束之后会自动销毁,并且由于是在栈上,所以无法手动销毁对象,但是在懒汉模式中该对象是从堆上开辟而来,有人就会想到,那这个对象究竟什么时候释放呢?程序也不会自动调用析构函数释放啊,其实我们不需要担心它内存泄露的问题,因为进程如果是正常退出的话,操作系统会帮我们做这一件事的,如果进程是异常退出的话,那需要担心的地方也不是这里了,而是为什么进程异常退出了。但是我们非要设计进程结束,释放资源的功能也是可以的:
cpp
class Singleton
{
public:
static Singleton* getExa()
{
if (sl == nullptr)
sl = new Singleton;
return sl;
}
void Add(const pair<string, int> kv)
{
sl->_hash.insert(kv);
}
void Print()
{
for (auto& e : sl->_hash)
{
cout << e.first << " : " << e.second << endl;
}
cout << endl;
}
//创建一个内嵌的自定义类,并且创建该自定义类的栈上的对象,那么在进程结束的
//时候该对象会调用它的析构函数,我们在它的析构函数中释放sl即可。
class GC
{
public:
~GC()
{
if (sl)
{
delete sl;
sl = nullptr;
}
}
};
private:
map<string, int> _hash;
Singleton() {};
Singleton(const Singleton& sl) = delete;
Singleton& operator=(const Singleton& sl) = delete;
static Singleton* sl;
static GC gc;
};
Singleton* Singleton::sl = nullptr;
Singleton::GC Singleton::gc;
要注意:自定义类型的静态成员变量在类内声明,类外定义
3). 两者的场景
饿汉模式
如果这个单例对象在多线程高并发环境下频繁使用,性能要求较高,那么显然使用饿汉
模式来避免资源竞争,提高响应速度更好。
懒汉模式
如果单例对象构造十分耗时或者占用很多资源,比如加载插件啊, 初始化网络连接啊,
读取文件啊等等,而有可能该对象程序运行时不会用到,那么也要在程序一开始就进行
初始化,就会导致程序启动时非常的缓慢。 所以这种情况使用懒汉模式(延迟加载)更
好。
在这里要说明一点,饿汉模式不需要担心线程安全问题,而懒汉模式需要,需要加锁,关于这一方面的问题,日后再谈(因为我也不会...)。
1. C语言类型转换
我们在编写C/C++代码的过程中肯定会不可避免地使用到强制类型转换,例如自己计算结构体某一成员偏移量,指针之间,指针与整形之间转换的场景我们都是这么写:
cpp
// 偏移量
#define offsetof( type,member) (char *)&((type *)0->member)
它的类型转换分为两种,一种是隐式的:
cpp
double j = 1.1;
int a = j;
但是很明显这种转换会丢失精度,还有就是显式的:
cpp
int main()
{
int a = 0;
int* pa = &a;
int b = (int)pa;
return 0;
}
对于以上类型转换会存在一些很明显的缺陷:
1. 转换的可视性比较差,所有的转换形式都是以一种相同形式书写,难以跟踪错误的转换
2. 隐式类型转化有些情况下可能会出问题:比如数据精度丢失
3. 显式类型转换将所有情况混合在一起,代码不够清晰
4. 隐式类型的转换在出现错误后一般在编译时才出现
所以基于这种情况C++标准提出了四种类型转换格式。
2. C++类型转换
a. static_cast
static_cast用于非多态类型的转换(静态转换),编译器隐式执行的任何类型转换都可用static_cast,但它不能用于两个不相关的类型进行转换:
cpp
int main()
{
double d = 12.34;
int a = static_cast<int>(d);
cout<<a<<endl;
return 0;
}
b. reinterpret_cast
reinterpret_cast操作符通常为操作数的位模式提供较低层次的重新解释,用于将一种类型转换为另一种不同的类型:
cpp
int main()
{
double d = 12.34;
int a = static_cast<int>(d);
cout << a << endl;
// 这里使用static_cast会报错,应该使用reinterpret_cast
//int *p = static_cast<int*>(a);
int *p = reinterpret_cast<int*>(a);
return 0;
}
c. const_cast
const_cast最常用的用途就是删除变量的const属性,方便赋值:
cpp
void Test ()
{
const int a = 2;
int* p = const_cast< int*>(&a );
*p = 3;
cout<<a <<endl;
}
d. dynamic_cast
dynamic_cast用于将一个父类对象的指针/引用转换为子类对象的指针或引用(动态转换)
向上转型:子类对象指针/引用->父类指针/引用(不需要转换,赋值兼容规则)
向下转型:父类对象指针/引用->子类指针/引用(用dynamic_cast转型是安全的)
注意:
- dynamic_cast只能用于父类含有虚函数的类
-
- dynamic_cast会先检查是否能转换成功,能成功则转换,不能则返回0
cpp
class A
{
public :
virtual void f(){}
};
class B : public A
{};
void fun (A* pa)
{
// dynamic_cast会先检查是否能转换成功,能成功则转换,不能则返回0
B* pb1 = static_cast<B*>(pa);
B* pb2 = dynamic_cast<B*>(pa);
cout<<"pb1:" <<pb1<< endl;
cout<<"pb2:" <<pb2<< endl;
}
int main ()
{
A a;
B b;
fun(&a);
fun(&b);
return 0;
}