C++——类型转换与特殊类设计

我们在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转型是安全的)
注意:

  1. dynamic_cast只能用于父类含有虚函数的类
    1. 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;
}
相关推荐
萝卜兽编程9 分钟前
优先级队列
c++·算法
Bruce小鬼10 分钟前
QT文件基本操作
开发语言·qt
2202_7544215415 分钟前
生成MPSOC以及ZYNQ的启动文件BOOT.BIN的小软件
java·linux·开发语言
我只会发热22 分钟前
Java SE 与 Java EE:基础与进阶的探索之旅
java·开发语言·java-ee
懷淰メ32 分钟前
PyQt飞机大战游戏(附下载地址)
开发语言·python·qt·游戏·pyqt·游戏开发·pyqt5
博风41 分钟前
设计模式:6、装饰模式(包装器)
设计模式
A_cot42 分钟前
理解设计模式与 UML 类图:构建稳健软件架构的基石
microsoft·设计模式·简单工厂模式·工厂方法模式·uml
君败红颜44 分钟前
设计模式之创建模式篇
设计模式
hummhumm1 小时前
第 22 章 - Go语言 测试与基准测试
java·大数据·开发语言·前端·python·golang·log4j
宁静@星空1 小时前
006-自定义枚举注解
java·开发语言