Bootstrap

【C++】特殊类设计和C++的类型转换

🌈个人主页:秦jh_-CSDN博客
🔥 系列专栏:https://blog.csdn.net/qinjh_/category_12575764.html?spm=1001.2014.3001.5482

 9efbcbc3d25747719da38c01b3fa9b4f.gif​ 

目录

 不能被拷贝的类

只能在堆上创建对象的类

只能在栈上创建对象的类

 不能被继承的类

 只能创建一个对象的类(单例模式)

单例模式: 

饿汉模式

懒汉模式 

C语言中的类型转换 

为什么C++需要四种类型转换 

C++强制类型转换 

 static_cast(隐式类型转换)

 reinterpret_cast(显式类型转换)

const_cast

dynamic_cast 

RTTI(了解)


前言

    💬 hello! 各位铁子们大家好哇。

             今日更新了C++特殊类和强制类型转换的相关内容
    🎉 欢迎大家关注🔍点赞👍收藏⭐️留言📝

 不能被拷贝的类

拷贝只会发生在两个场景中:拷贝构造函数以及赋值运算符重载,因此想要让一个类禁止拷贝, 只需让该类不能调用拷贝构造函数以及赋值运算符重载即可。

C++98 将拷贝构造函数与赋值运算符重载只声明不定义,并且将其访问权限设置为私有即可。

C++11 扩展delete的用法,delete除了释放new申请的资源外,如果在默认成员函数后跟上 =delete,表示让编译器删除掉该默认成员函数。 

只能在堆上创建对象的类

实现方式:

  1. 将类的构造函数私有,拷贝构造声明成私有。防止别人调用拷贝在栈上生成对象。
  2. 提供一个静态的成员函数,在该静态成员函数中完成堆对象的创建 

 

 如上图,此时不能在栈上创建对象了。但是调用成员函数需要对象,所以就把成员函数设置成静态的,通过类域名就能调用了。如下图:

注意,还需要把拷贝构造和赋值重载封死。

只能在栈上创建对象的类

同上将构造函数私有化,然后设计静态方法创建对象返回即可。 

 new会调用构造,而拷贝构造也是构造,就可以间接new一个对象,所以要把operator new禁用。new会调用operator new和构造,默认会调用全局的operator new,但是我们可以重载一个类专属的operator new,此时new的时候不会调用全局的,而是我们重载的。

 不能被继承的类

C++98中构造函数私有化,派生类中调不到基类的构造函数。则无法继承 

C++11方法:f inal关键字,final修饰类,表示该类不能被继承。 

class A  final
 {
 // ....
 };

 只能创建一个对象的类(单例模式)

设计模式:

设计模式(Design Pattern)是一套被反复使用、多数人知晓的、经过分类的、代码设计经验的总结。

使用设计模式的目的:为了代码可重用性、让代码更容易被他人理解、保证代码可靠性。 设计模 式使代码编写真正工程化;

单例模式: 

一个类只能创建一个对象,即单例模式,该模式可以保证系统中该类只有一个实例,并提供一个 访问它的全局访问点,该实例被所有程序模块共享。比如在某个服务器程序中,该服务器的配置 信息存放在一个文件中,这些配置数据由一个单例对象统一读取,然后服务进程中的其他对象再 通过这个单例对象获取这些配置信息,这种方式简化了在复杂环境下的配置管理。

单例模式有两种实现模式:

饿汉模式

就是说不管你将来用不用,程序启动时就创建一个唯一的实例对象。(main函数前就创建对象)

// 饿汉模式 : 一开始就(main函数之前)就创建对象
// 问题1:如果有很多单例类,都是饿汉模式,有些单例对象初始化资源很多,导致程序启动慢,迟迟进不了main函数
// 如果两个单例类有初始化依赖关系,饿汉也无法解决。比如A类和B类是单例,A单例要连接数据库,B单例要用A单例访问数据库
class ConfigInfo
{
public:
	static ConfigInfo* GetInstance()
	{
		return &_sInfo;
	}

	string GetIp()
	{
		return _ip;
	}

	void SetIp(const string& ip)
	{
		_ip = ip;
	}

private:
	ConfigInfo()
	{
		cout << "ConfigInfo()" << endl;
	}

	ConfigInfo(const ConfigInfo&) = delete;
	ConfigInfo& operator=(const ConfigInfo&) = delete;
private:
	string _ip = "127.0.0.1";
	int _port = 80;
	//...

	// 声明 
	static ConfigInfo _sInfo;
};

// 定义
ConfigInfo ConfigInfo::_sInfo;
int main()
{
	cout << ConfigInfo::GetInstance() << endl;
	cout << ConfigInfo::GetInstance() << endl;
	cout << ConfigInfo::GetInstance() << endl;

	ConfigInfo::GetInstance()->SetIp("192.33.3.22");

	cout << ConfigInfo::GetInstance()->GetIp() << endl;

	return 0;
}

如果这个单例对象在多线程高并发环境下频繁使用,性能要求较高,那么显然使用饿汉模式来避免资源竞争,提高响应速度更好。

懒汉模式 

 如果单例对象构造十分耗时或者占用很多资源,比如加载插件啊, 初始化网络连接啊,读取文件啊等等,而有可能该对象程序运行时不会用到,那么也要在程序一开始就进行初始化, 就会导致程序启动时非常的缓慢。 所以这种情况使用懒汉模式(延迟加载)更好。

优点:第一次使用实例对象时,创建对象。进程启动无负载。多个单例实例启动顺序自由控制。

缺点:复杂 

// 懒汉:第一次调用GetInstance时创建单例对象
// 懒汉完美解决上面饿汉的问题
class ConfigInfo
{
public:
	static ConfigInfo* GetInstance()
	{
		// C++11之前,多线程调用GetInstance
		// 局部静态单例对象构造初始化,无法保证线程安全
		// 也就是说,这种写法C++11之后才能用
		static ConfigInfo info;

		return &info;
	}

	string GetIp()
	{
		return _ip;
	}

	void SetIp(const string& ip)
	{
		_ip = ip;
	}

private:
	ConfigInfo()
	{
		cout << "ConfigInfo()" << endl;
	}

	ConfigInfo(const ConfigInfo&) = delete;
	ConfigInfo& operator=(const ConfigInfo&) = delete;
private:
	string _ip = "127.0.0.1";
	int _port = 80;
	//...
};

C++11后,上面的创建实例对象才能用

class ConfigInfo
{
public:
	static ConfigInfo* GetInstance()
	{
		// C++11之前也能保证线程安全
		// 多线程调用需要考虑线程安全问题

		// 双检查加锁
		// t1 t2
		if (_spInfo == nullptr)      // 性能
		{
			unique_lock<mutex> lock(_mtx);
			if (_spInfo == nullptr)  // 线程安全
			{
				_spInfo = new ConfigInfo;
			}
		}

		return _spInfo;
	}

	string GetIp()
	{
		return _ip;
	}

	void SetIp(const string& ip)
	{
		_ip = ip;
	}

private:
	ConfigInfo()
	{
		cout << "ConfigInfo()" << endl;
	}

	ConfigInfo(const ConfigInfo&) = delete;
	ConfigInfo& operator=(const ConfigInfo&) = delete;
private:
	string _ip = "127.0.0.1";
	int _port = 80;
	//...

	static ConfigInfo* _spInfo;
	static mutex _mtx;
};

ConfigInfo* ConfigInfo::_spInfo = nullptr;
mutex ConfigInfo::_mtx;

int main()
{
	cout << ConfigInfo::GetInstance() << endl;
	cout << ConfigInfo::GetInstance() << endl;
	cout << ConfigInfo::GetInstance() << endl;

	ConfigInfo::GetInstance()->SetIp("192.33.3.22");

	cout << ConfigInfo::GetInstance()->GetIp() << endl;

	return 0;
}

C++11之前,多线程需要考虑线程安全问题,需要加锁解决。

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);

	return 0;
}

 缺陷: 转换的可视性比较差,所有的转换形式都是以一种相同形式书写,难以跟踪错误的转换

为什么C++需要四种类型转换 

C风格的转换格式很简单,但是有不少缺点的:

  1. 隐式类型转化有些情况下可能会出问题:比如数据精度丢失
  2. 显式类型转换将所有情况混合在一起,代码不够清晰 因此C++提出了自己的类型转化风格,注意因为C++要兼容C语言,所以C++中还可以使用C语言的转化风格。 

C++强制类型转换 

标准C++为了加强类型转换的可视性,引入了四种命名的强制类型转换操作符:  static_cast、reinterpret_cast、const_cast、dynamic_cast 

 static_cast(隐式类型转换)

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

int main()
 {
     double d = 12.34;
     int a = static_cast<int>(d);
     cout<<a<<endl;
     return 0;
 }

 reinterpret_cast(显式类型转换)

 reinterpret_cast操作符通常为操作数的位模式提供较低层次的重新解释,用于将一种类型转换为另一种不同的类型

int main()
{
	double d = 12.34;
	int a = static_cast<int>(d);
	// 这里使用static_cast会报错,应该使用reinterpret_cast
	//int *p = static_cast<int*>(a);
	int* p = reinterpret_cast<int*>(a);

	return 0;
}

const_cast

const_cast最常用的用途就是删除变量的const属性,方便赋值 

 如上图,a被const修饰,不能直接修改,但可以通过指针间接修改,但是上面修改后,为什么a打印出来还是1?

通过监视窗口,看到a的值已经变成2了。原因:编译器进行了优化,编译器觉得a被const修饰了,不会改变了,他就把a存到寄存器,直接到寄存器访问,虽然内存里改变了,但是寄存器的还没变。监视窗口看到的是内存里的值。 

此时需要用 volatile 修饰a。volatile的作用是:告诉编译器不要优化,每次都去内存取a。

c++增加了 const_cast 。

作用:时刻提醒你这个变量的const属性被去掉了,变得不稳定了,这个变量需要被 volatile 修饰。

如果不用volatile修饰,结果还是会被优化。 

dynamic_cast 

如上图, 形参是B类型时,因为fun用A接收,就只能看到B里的_a1部分,然后又被转回B类型,此时又能看到B类型的_b1了。

如果传A类型对象,fun里又被转成B类型,此时就会越界,因为访问了A里没有_b1,但是又访问了_b1。所以上面运行到fun(&a)是就崩溃了。

dynamic_cast用于将一个父类对象的指针/引用转换为子类对象的指针或引用(动态转换)

向上转型:子类对象指针/引用->父类指针/引用(不需要转换,赋值兼容规则)

向下转型:父类对象指针/引用->子类指针/引用(用dynamic_cast转型是安全的)

注意:

  1. dynamic_cast只能用于父类含有虚函数的类
  2. dynamic_cast会先检查是否能转换成功,能成功则转换,不能则返回0 

前面的转换是无差别转换,会有越界风险。

c++设计了 dynamic_cast 进行动态转换。

dynamic_cast会先检查是否能转换成功,能成功则转换,不能则返回 

RTTI(了解)

RTTI:Run-time Type identification的简称,即:运行时类型识别。

C++通过以下方式来支持RTTI:

  1.  typeid运算符
  2. dynamic_cast运算符
  3.  decltype 
;