第二十三讲:特殊类和类型转换

目录

1、特殊类设计

1.1、不能被拷贝

1.2、只能在堆上创建

1.3、只能在栈上创建

1.4、不能被继承

1.5、单例模式

1.5.1、饿汉模式

1.5.2、懒汉模式

2、类型转换

2.1、C语言中的类型转换

2.2、C++四种类型转换

2.2.1、static_cast

2.2.2、reinterpret_cast

2.2.3、const_cast

2.2.4、dynamic_cast

3、RTTI


1、特殊类设计

1.1、不能被拷贝

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

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

1、设置成私有:如果只声明没有设置成private,用户自己如果在类外定义了,就可以拷贝了。

2、只声明不定义:不定义是因为该函数根本不会调用,定义了其实也没有什么意义,不写反而还简单,而且如果声明了就不会生成默认的拷贝和赋值重载函数了。

C++11:在默认成员函数后跟上=delete,表示让编译器删除掉该默认成员函数。

不再举例,因为在智能指针这一部分已经举例了。

1.2、只能在堆上创建

第一种方式是:将析构函数私有化。例如:

cpp 复制代码
class HeapOnly
{
private:
	~HeapOnly()
	{
		cout << "~HeapOnly" << endl;
	}
};

int main()
{
	HeapOnly hp1; // 会报错
	static HeapOnly hp2; // 会报错

	return 0;
}

这样就只能在堆上创建对象了,如果要释放在堆上创建的该对象,直接使用delete是不行的,可以写一个销毁函数来解决这个问题,如下:

cpp 复制代码
class HeapOnly
{
public:
	static void Destory(HeapOnly* ptr)
	{
		delete ptr;
	}
private:
	~HeapOnly()
	{
		cout << "~HeapOnly" << endl;
	}
};

int main()
{	
	HeapOnly* ptr = new HeapOnly;
	HeapOnly::Destory(ptr);

	return 0;
}

或者,也可以像下面这样写:

cpp 复制代码
class HeapOnly
{
public:
	void Destory()
	{
		delete this;
	}
private:
	~HeapOnly()
	{
		cout << "~HeapOnly" << endl;
	}
};

int main()
{	
	HeapOnly* ptr = new HeapOnly;
	ptr->Destory();

	return 0;
}

第二种方式:将类的构造函数私有,拷贝构造声明成私有。防止别人调用拷贝构造在栈上生成对象。例如:

cpp 复制代码
class HeapOnly
{
private:
	HeapOnly()
	{
		cout << "HeapOnly" << endl;
	}

	HeapOnly(const HeapOnly& hp)
	{
		cout << "HeapOnly(const HeapOnly& hp)" << endl;
	}
};

int main()
{
	// 下面的这些都会报错
	HeapOnly hp1;
	static HeapOnly hp2;
	HeapOnly* ptr = new HeapOnly;
	HeapOnly copy(ptr);

	return 0;
}

如果我们此时想要创建对象的话,就可以像下面这样做:

cpp 复制代码
class HeapOnly
{
public:
	static HeapOnly* CreateObj()
	{
		return new HeapOnly;
	}
private:
	HeapOnly()
	{
		cout << "HeapOnly" << endl;
	}

	HeapOnly(const HeapOnly& hp)
	{
		cout << "HeapOnly(const HeapOnly& hp)" << endl;
	}
};

int main()
{
	HeapOnly* ptr = HeapOnly::CreateObj();

	return 0;
}

1.3、只能在栈上创建

可以将构造函数私有化,并禁掉new和delete函数。例如:

cpp 复制代码
class StackOnly
{
public:
	static StackOnly CreateObj()
	{
		return StackOnly();
	}

	// 禁掉operator new可以把下面用new 调用拷贝构造申请对象给禁掉
	// StackOnly obj = StackOnly::CreateObj();
	// StackOnly* ptr3 = new StackOnly(obj);
	void* operator new(size_t size) = delete;
	void operator delete(void* p) = delete;

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

int main()
{
	StackOnly obj = StackOnly::CreateObj();
	StackOnly* ptr = new StackOnly; // 报错
	StackOnly* ptr = new StackOnly(obj); // 报错

	return 0;
}

1.4、不能被继承

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

C++11:使用final关键字,final修饰类,表示该类不能被继承。

这个在之前就已经提到过了,不再举例。

1.5、单例模式

设计模式是一套被反复使用、多数人知晓的、经过分类的、代码设计经验的总结。为什么会产生设计模式这样的东西呢?就像人类历史发展会产生兵法。最开始部落之间打仗时都是人与人的对砍。后来春秋战国时期,七国之间经常打仗,就发现打仗也是有套路的,后来孙子就总结出了《孙子兵法》,设计模式也是类似。

使用设计模式的目的:为了代码可重用性、让代码更容易被他人理解、保证代码可靠性。 设计模 式使代码编写真正工程化;设计模式是软件工程的基石脉络,如同大厦的结构一样。

注:设计模式有很多,常见的设计模式就几个,下面的单例模式就是常见的一种设计模式。

一个类只能创建一个对象,即单例模式,该模式可以保证系统中该类只有一个实例,并提供一个 访问它的全局访问点,该实例被所有程序模块共享。单例模式有两种实现模式: 饿汉模式和懒汉模式。

1.5.1、饿汉模式

饿汉模式就是说不管你将来用不用,程序启动时就创建一个唯一的实例对象。

例如:

cpp 复制代码
// 饿汉模式
class A
{
public:
	static A* GetInstance()
	{
		return &_inst;
	}

	void Add(const string& key, const string& value)
	{
		_dict[key] = value;
	}

	void Print()
	{
		for (auto kv : _dict)
		{
			cout << kv.first << " : " << kv.second << endl;
		}
		cout << endl;
	}

private:
	A()
	{}

	A(const A& aa) = delete;
	A& operator=(const A& aa) = delete;

	map<string, string> _dict;
	int _n = 0;

	static A _inst;
};
A A::_inst; // 在执行main函数前就会被创建好。

int main()
{
	A::GetInstance()->Add("sort", "分类、排序");
	A::GetInstance()->Add("left", "左边");
	A::GetInstance()->Add("right", "右边");

	A::GetInstance()->Print();

	return 0;
}

优点:相比下面的懒汉模式实现会更加简单。

缺点:可能会导致进程启动变慢(指从程序启动到开始执行main函数);另外一个就是如果一个项目中有多个单例,这多个单例类对象实例启动顺序不确定,饿汉模式是无法控制实例化的顺序的,此时就需要考虑下面的懒汉模式了。

1.5.2、懒汉模式

如果单例对象构造十分耗时或者占用很多资源,而有可能该对象程序运行时不会用到,那么也要在程序一开始就进行初始化,就会导致程序启动时非常的缓慢;此外,如果有顺序的要求,也不能使用饿汉模式。所以在这些情况下使用懒汉模式更好。

例如:

cpp 复制代码
// 懒汉模式
class B
{
public:
	static B* GetInstance()
	{
		if (_inst == nullptr)
		{
			_inst = new B; // 懒汉对象一般不需要释放,因为程序运行过程中,还需要使用它(这种不属于内存泄漏)。
			               // 等到进程正常结束,它也就自然而然的释放了。
		}
		return _inst;
	}

	void Add(const string& key, const string& value)
	{
		_dict[key] = value;
	}

	void Print()
	{
		for (auto kv : _dict)
		{
			cout << kv.first << " : " << kv.second << endl;
		}
		cout << endl;
	}

	static void DelInstance() // 如果有需要的话,可以调用该函数销毁懒汉对象。
	{
		if (_inst != nullptr)
		{
			delete _inst;
			_inst = nullptr;
		}
	}

private:
	B()
	{}

	~B()
	{
		// 如果需要做一些动作的话,比如:持久化(将一些数据写到文件)
		cout << "数据写到文件" << endl;
	}

	B(const B& aa) = delete;
	B& operator=(const B& aa) = delete;

	map<string, string> _dict;
	int _n = 0;

	static B* _inst;

	class gc
	{
	public:
		~gc()
		{
			DelInstance();
		}
	};

	static gc _gc;
};
B* B::_inst = nullptr; // 在执行main函数前就会被创建好。
B::gc B::_gc;

int main()
{
	B::GetInstance()->Add("sort", "分类、排序");
	B::GetInstance()->Add("left", "左边");
	B::GetInstance()->Add("right", "右边");
	B::GetInstance()->Print();

	B::GetInstance()->Add("right", "xxx");
	B::GetInstance()->Print();

	return 0;
}

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

缺点:实现比较复杂,懒汉模式是有线程安全问题的,等后面的文章讲线程时再说。

2、类型转换

2.1、C语言中的类型转换

在C语言中,如果赋值运算符左右两侧类型不同,或者形参与实参类型不匹配,或者返回值类型与 接收返回值类型不一致时,就需要发生类型转化,C语言中总共有两种形式的类型转换:隐式类型 转换显式类型转换

1、隐式类型转化:编译器在编译阶段自动进行,能转就转,不能转就编译失败。

2、显式类型转化:需要用户自己处理。

例如:

cpp 复制代码
void Test()
{
	int i = 1;
	// 隐式类型转换
	double d = i; // 因为int和double都是用来表示数据大小的,所以可以进行隐式类型转换。

	int* p = &i;
	// 显示的强制类型转换
	int address = (int)p; // 因为int和int*的含义是不一样的,一个表示地址,一个表示数据大小,因此不能隐式类型转换。
						 // 之所以可以强制类型转化是因为两者之间还有一定的关联性。

	// 注:完全不相关的类型之间是无法进行互相转换的。
}

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

1、隐式类型转化有些情况下可能会出问题:比如数据精度丢失。

2、显式类型转换代码不够清晰。

因此C++提出了自己的类型转化风格,注意因为C++要兼容C语言,所以C++中还可以使用C语言的 转化风格。

2.2、C++四种类型转换

标准C++为了加强类型转换的可视性以及可读性,引入了四种命名的强制类型转换操作符,如下:

2.2.1、static_cast

static_cast是静态转换,编译器隐式执行的任何类型转换都可用 static_cast,但它不能用于两个不相关的类型进行转换。对应的就是C语言的隐式类型转换。例如:

cpp 复制代码
int main()
{
	double d = 12.34;
	int a = static_cast<int>(d);
	cout << a << endl;

	int* ptr = static_cast<int*>(a); // 会报错
	return 0;
}
2.2.2、reinterpret_cast

reinterpret_cast操作符通常用于将一种类型转换为另一种不同的类型。对应C语言的强制类型转换。例如:

cpp 复制代码
int main()
{
	int a = 0;
	int* ptr = reinterpret_cast<int*>(a); 
	return 0;
}
2.2.3、const_cast

const_cast最常用的用途就是删除变量的const属性,方便赋值。对应与C语言的强制类型转换,例如:

cpp 复制代码
int main()
{
	volatile const int a = 2;
	int* p = const_cast<int*>(&a);
	*p = 3;

	cout << a << endl; // 上面不加volatile最终会导致这里的a的值的打印为2。
	cout << *p << endl;

	cout << (void*)&a << endl; // 之所以进行了转换,是因为cout无法正确识别该类型。
	cout << p << endl;

	return 0;
}
2.2.4、dynamic_cast

dynamic_cast用于将一个父类对象的指针或者引用转换为子类对象的指针或引用。这里的dynamic_cast针对的是向下转型。

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

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

注意:

1、dynamic_cast只能用于父类含有虚函数的类。

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

例如:

cpp 复制代码
class A
{
public:
	virtual void f() {}
};
class B : public A
{
};

void fun(A* pa)
{
	//dynamic_cast会先检查是否能转换成功,能成功则转换,不能则返回。
	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;
}

运行结果为:

3、RTTI

RTTI:运行时类型识别。C++通过以下方式来支持RTTI:

1、typeid运算符。

2、dynamic_cast运算符。

3、decltype。

上面的这三个在之前的文章中都讲过,不再举例说明。

相关推荐
笨蛋少年派2 小时前
JAVA基础语法
java·开发语言
渡我白衣2 小时前
深入剖析:boost::intrusive_ptr 与 std::shared_ptr 的性能边界和实现哲学
开发语言·c++·spring
爱吃小胖橘3 小时前
Lua语法
开发语言·unity·lua
怀旧,3 小时前
【C++】26. 智能指针
开发语言·c++·算法
Aevget3 小时前
PHP智能开发工具PhpStorm v2025.2全新上线——支持PHPUnit 12等
开发语言·ide·php·phpstorm
东方芷兰3 小时前
JavaWeb 课堂笔记 —— 20 SpringBootWeb案例 配置文件
java·开发语言·笔记·算法·log4j·intellij-idea·lua
许商3 小时前
【stm32】bash自动配置buildenv
开发语言·bash
reembarkation3 小时前
自定义分页控件,只显示当前页码的前后N页
开发语言·前端·javascript
楼田莉子3 小时前
vscode搭建C/C++配置开发环境
c语言·开发语言·c++·vscode·学习·编辑器