C++之特殊类设计及类型转换

目录

一、设计一个不能被拷贝的类

二、设计一个只能在堆上创建对象的类

三、设计一个只能在栈上创建对象的类

四、设计一个不能被继承的类

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

六、C语言中的类型转换

七、C++中的三类类型转换

八、C++强制类型转换

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

8.2、static_cast

8.3、reinterpret_cast

8.4、const_cast

8.5、dynamic_cast

九、RTTI


一、设计一个不能被拷贝的类

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

C++98:

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

原因:

  • 设置成私有:如果只声明没有设置成private,用户自己如果在类外定义了,就可以不被禁止拷贝了
  • 只声明不定义:不定义是因为该函数根本不会调用,定义了其实也没有什么意义,不写反而还简单,而且如果定义了就不会防止成员函数内部拷贝了。

示例代码:

cpp 复制代码
class CopyBan
{
private:
	CopyBan(const CopyBan&);
	CopyBan& operator=(const CopyBan&);

	//...
};

C++11:

C++11扩展delete的用法,delete除了释放new申请的资源外,如果在默认成员函数后跟上

=delete,表示让编译器删除掉该默认成员函数。

示例代码:

cpp 复制代码
class CopyBan
{
	// ...

	CopyBan(const CopyBan&) = delete;
	CopyBan& operator=(const CopyBan&) = delete;

	//...

};

二、设计一个只能在堆上创建对象的类

实现方式:

  1. 将类的构造函数私有,拷贝构造声明成私有。防止别人调用拷贝在栈上生成对象。

  2. 提供一个静态的成员函数,在该静态成员函数中完成堆对象的创建

方法一:

cpp 复制代码
class HeapOnly
{
public:
    //提供在堆上创建对象的方法
	static HeapOnly* CreateObj()
	{
		return new HeapOnly;
	}

	//将拷贝构造和赋值重载也禁止
	HeapOnly(const HeapOnly&) = delete;
	HeapOnly& operator=(const HeapOnly&) = delete;
private:
	//将构造私有,防止外界直接创建对象
	HeapOnly()
	{}
};

int main()
{
	//静态区上创建对象
	//static HeapOnly hp0;
	
	// 栈上创建对象
	//HeapOnly hp1;
	
	//堆上创建对象
	//HeapOnly* hp2 = new HeapOnly;

	HeapOnly* hp3 = HeapOnly::CreateObj();
	
	//要防止别人通过这种方式在栈上创建对象
	//通过禁止拷贝构造来防止这种方式
	//HeapOnly hp4(*hp3);

	//手动释放堆上的资源
	delete hp3;

	return 0;
}

**解释:**上面代码是通过私有构造函数的方式来阻止外界自己创建对象,并提供一个在堆上创建对象的方法,使得外界只能在堆上创建对象,将拷贝构造和赋值重载禁止是防止别人像图中那样通过这两个方法在栈上创建对象。

方法二:

cpp 复制代码
class HeapOnly
{
public:
	void Destroy()
	{
		delete this;
	}

private:
	//析构函数私有化
	~HeapOnly()
	{}
};

int main()
{
	//static HeapOnly hp0;
	//HeapOnly hp1;
	HeapOnly* hp2 = new HeapOnly;
	//delete hp2;
	hp2->Destroy();

	return 0;
}

**解释:**该方法是通过私有析构函数的方式使外界无法自动调用析构函数,进而无法创建对象,只能在堆上创建对象,因为在堆上申请的空间需要自己主动释放,不会自动调用析构。这种实现的方法无需禁止拷贝构造和赋值重载,因为通过这两种方式创建出来的栈上的对象仍会因为无法调用析构而无法创建。

三、设计一个只能在栈上创建对象的类

方法一:同上将构造函数私有化,然后设计静态方法创建对象返回,并禁止掉重载的new和delete。

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

	//StackOnly(const StackOnly& s) = delete;
	void* operator new(size_t size) = delete;
	void operator delete(void* p) = delete;
private:
	StackOnly()
		:_a(0)
	{}
private:
	int _a;
};

int main()
{
	//static StackOnly s1;
	//StackOnly s2;
	//StackOnly* s3 = new StackOnly;

	StackOnly s4 = StackOnly::CreateObj();

	//StackOnly* s5 = new StackOnly(s4);
	static StackOnly s6(s4);

	return 0;
}

**解释:**私有构造函数,并提供创建对象的方法,这样外界无法自己创建对象,只能使用提供的方法在栈上创建对象。但如果只是这样外界可以通过拷贝构造在堆上或在静态区创建对象,可我们不能禁止掉拷贝构造,因为在栈上创建对象并返回,会用到拷贝构造,如上述代码中s4接收返回的栈上的对象就是将栈上的对象拷贝给s4的,所以我们重载new和delete,C++中如果我们重载了这两个方法,那么我们调用这两个方法时会优先调用我们自己的而不是库的,我们再将这两个方法禁止,这样就阻止别人在堆上创建对象了。但是在静态区禁止不了。

**方法二:**同上将构造函数私有化,然后设计静态方法创建对象返回,并禁止掉拷贝构造,但提供移动构造。

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

	StackOnly(const StackOnly&& s)
	{
		//......
	}

	StackOnly(const StackOnly& s) = delete;
private:
	StackOnly()
		:_a(0)
	{}
private:
	int _a;
};

int main()
{
	StackOnly s4 = StackOnly::CreateObj();

	//StackOnly* s5 = new StackOnly(s4);
	//static StackOnly s6(s4);

	//这种方式禁止不掉
	StackOnly* s5 = new StackOnly(move(s4));
	static StackOnly s6(move(s4));

	return 0;
}

**解释:**提供的在栈上创建对象的方法返回的是匿名对象,是右值,可以通过移动构造赋值出去,这样外界用事先创建好的对象再通过拷贝的方式在堆上或者在静态区创建对象就创建不了了,但如果有人将左值move成右值再去拷贝,那就阻止不了了。

四、设计一个不能被继承的类

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

示例代码:

cpp 复制代码
// C++98中构造函数私有化,派生类中调不到基类的构造函数。则无法继承
class NonInherit
{
public:
	static NonInherit GetInstance()
	{
		return NonInherit();
	}

private:
	NonInherit()
	{}
};

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

示例代码:

cpp 复制代码
class A final
{
	// ....
};

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

设计模式:

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

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

单例模式:

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

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

  • 饿汉模式

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

cpp 复制代码
// 饿汉模式
// 1、多个饿汉模式的单例,某个对象初始化内容较多(读文件),会导致程序启动慢
// 2、A和B两个饿汉,对象初始化存在依赖关系,要求A先初始化,B再初始化,饿汉无法保证
class InfoMgr
{
public:
	static InfoMgr& GetInstance()
	{
		return _ins;
	}

	void Print()
	{
		cout << _ip << endl;
		cout << _port << endl;
		cout << _buffSize << endl;
	}
private:
	InfoMgr(const InfoMgr&) = delete;
	InfoMgr& operator=(const InfoMgr&) = delete;

	InfoMgr()
	{
		cout << "InfoMgr()" << endl;
	}
private:
	string _ip = "127.0.0.1";
	int _port = 80;
	size_t _buffSize = 1024 * 1024;
	//...

	static InfoMgr _ins;
};

InfoMgr InfoMgr::_ins;


int main()
{
	InfoMgr::GetInstance().Print();
	//InfoMgr copy(InfoMgr::GetInstance());

	return 0;
}

**解释:**首先单例模式只允许一个类创建一个对象,所以还是将构造函数,拷贝构造,赋值重载全部禁掉,防止外界自己创建对象,然后再类里面添加一个私有的静态的类的对象,这个对象因为是静态的,不会存在类中,存在静态区,所以不会造成类里面有类对象,类对象里面又有类对象这种无穷套娃的问题,这里将该类的对象放到类中是为了让它受到类域的限制,不让外界随意访问,其实就相当于静态的全局变量(但被类域限制着),当程序一启动,这个对象就会被创建,我们再提供一个方法供外界获取这个对象即可。

**注意点:**首先,多个饿汉模式的单例,某个对象初始化内容较多(如需要读文件),会导致程序启动慢。其次,A和B两个饿汉,对象初始化存在依赖关系,要求A先初始化,B再初始化,饿汉无法保证,因为都是全局变量,谁先初始化无法保证。

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

  • 懒汉模式

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

cpp 复制代码
// 懒汉模式
class InfoMgr
{
public:
	static InfoMgr& GetInstance()
	{
		// 第一次调用时创建单例对象
		// 有线程安全的风险
		if (_pins == nullptr)
		{
			_pins = new InfoMgr;
		}

		return *_pins;
	}

	void Print()
	{
		cout << _ip << endl;
		cout << _port << endl;
		cout << _buffSize << endl;
	}

	static void DelInstance()
	{
		delete _pins;
		_pins = nullptr;
	}

private:
	InfoMgr(const InfoMgr&) = delete;
	InfoMgr& operator=(const InfoMgr&) = delete;

	InfoMgr()
	{
		cout << "InfoMgr()" << endl;
	}
private:
	string _ip = "127.0.0.1";
	int _port = 80;
	size_t _buffSize = 1024 * 1024;
	//...

	static InfoMgr* _pins;
};

InfoMgr* InfoMgr::_pins = nullptr;

int main()
{
	InfoMgr::GetInstance().Print();
	InfoMgr::GetInstance().Print();

	return 0;
}

**解释:**和饿汉思路类似,只不过懒汉不能一开始就创建好这个唯一的类对象,只有当需要的时候才会创建,不过这里懒汉的对象是在堆上创建的,可以对外提供一个释放资源的方法。

**注意点:**懒汉模式创建对象时有线程风险问题,这里只是演示一下基本思路,所以代码并没有做的非常严谨,如果想解决这个问题可以加锁。

方法二:(此方法适用C++11之后)

cpp 复制代码
class InfoMgr
{
public:
	static InfoMgr& GetInstance()
	{
		// 第一次调用时创建单例对象
		// C++11之后
		static InfoMgr ins;
		return ins;
	}

	void Print()
	{
		cout << _ip << endl;
		cout << _port << endl;
		cout << _buffSize << endl;
	}
private:
	InfoMgr(const InfoMgr&) = delete;
	InfoMgr& operator=(const InfoMgr&) = delete;

	InfoMgr()
	{
		cout << "InfoMgr()" << endl;
	}
private:
	string _ip = "127.0.0.1";
	int _port = 80;
	size_t _buffSize = 1024 * 1024;
	//...
};

int main()
{
	InfoMgr::GetInstance().Print();
	InfoMgr::GetInstance().Print();

	cout << &InfoMgr::GetInstance() << endl;
	cout << &InfoMgr::GetInstance() << endl;

	return 0;
}

**解释:**这里提供一个局部的静态变量,而不是全局的,这样只有第一次需要的时候才会创建,后面因为是静态变量,再去申请对象时使用的是前面创建好的静态对象。

六、C语言中的类型转换

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

  1. 隐式类型转化:编译器在编译阶段自动进行,能转就转,不能转就编译失败
  2. 显式类型转化:需要用户自己处理

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

七、C++中的三类类型转换

内置类型之间:

  • 隐式类型转换 整形之间/整形和浮点数之间
  • 显示类型的转换 指针和整形、指针之间

示例代码:

cpp 复制代码
// a、内置类型之间
// 1、隐式类型转换    整形之间/整形和浮点数之间
// 2、显示类型的转换  指针和整形、指针之间

int main()
{
	int i = 1;
	// 隐式类型转换
	double d = i;
	printf("%d, %.2f\n", i, d);

	int* p = &i;
	// 显示的强制类型转换
	int address = (int)p;
	printf("%p, %d\n", p, address);

	return 0;
}

内置类型和自定义类型之间:

  • 自定义类型 = 内置类型 ->构造函数支持
  • 内置类型 = 自定义类型 ->operator重载支持

示例代码:

cpp 复制代码
// b、内置类型和自定义类型之间
// 1、自定义类型 = 内置类型  ->构造函数支持
// 2、内置类型 = 自定义类型
class A
{
public:
	//explicit A(int a) //explicit关键字可以禁止隐式转换,必须显示转换
	A(int a)
		:_a1(a)
		,_a2(a)
	{}

	A(int a1, int a2)
		:_a1(a1)
		, _a2(a2)
	{}

	//自定义类型 -> 内置类型
	// ()被仿函数占用了,不能用
	// operator 类型实现,有返回值,无返回类型
	// 默认返回类型就是要转换的类型
 	//explicit operator int()
	operator int()
	{
		return _a1 + _a2;
	}
private:
	int _a1 = 1;
	int _a2 = 1;
};
int main()
{
	//内置类型转换自定义类型
	string s1 = "1111111";

	A aa1 = 1;
	//A aa1 = (A)1;

	A aa2 = { 2,2 };
	const A& aa3 = { 2,2 };

	//自定义类型转换内置类型
	int z = aa1.operator int();
	//int x = (int)aa1;
	int x = aa1;
	int y = aa2;
	cout << x << endl;
	cout << y << endl;

	//库里的shared_ptr提供了转换为bool类型的重载函数
	std::shared_ptr<int> foo;
	std::shared_ptr<int> bar(new int(34));

	//这里本质是转换为了bool类型
	//if (foo.operator bool())
	if (foo)
		std::cout << "foo points to " << *foo << '\n';
	else 
		std::cout << "foo is null\n";

	if (bar)
		std::cout << "bar points to " << *bar << '\n';
	else
		std::cout << "bar is null\n";

	return 0;
}

自定义类型和自定义类型之间:

  • 对应的构造函数支持

示例代码:

cpp 复制代码
// c、自定义类型和自定义类型之间 -- 对应的构造函数支持
class A
{
public:
	A(int a)
		:_a1(a)
		, _a2(a)
	{}

	A(int a1, int a2)
		:_a1(a1)
		, _a2(a2)
	{}

	int get() const
	{
		return _a1 + _a2;
	}
private:
	int _a1 = 1;
	int _a2 = 1;
};

class B
{
public:
	B(int b)
		:_b1(b)
	{}

	B(const A& aa)
		:_b1(aa.get())
	{}

private:
	int _b1 = 1;
};

#include"List.h"
#include<list>

int main()
{
	A aa1(1);
	B bb1(2);

	bb1 = aa1;
	B& ref1= bb1;
	const B& ref2 = aa1;

	bit::list<int> lt = { 1,2,3,4 };
	// 权限的缩小?权限缩小和放大,仅限于const的指针和引用
	// 不是权限缩小,这里类型转换
	bit::list<int>::const_iterator cit = lt.begin();
	while (cit != lt.end())
	{
		cout << *cit << " ";
		++cit;
	}
	cout << endl;

	return 0;
}

**解释:**上面代码中涉及到一个从普通迭代器到const迭代器的转换问题,这不属于权限缩小,权限问题只有在指针和引用中存在,这里无法转换是因为迭代器的实现使用了模版,模版实例化后普通迭代器和const迭代器本就是不同的类型,所以这里是类型不同导致相互之间无法赋值,解决办法如下图。

**解释:**我们需要再迭代器中增加一个方法,这个方法的形参必须是普通迭代器类型,当该迭代器模版实例化为普通迭代器后,这个函数在普通迭代器中就是拷贝构造,当该迭代器模板实例化为const迭代器后,这个函数在const迭代器中就能够将传入的普通迭代器转换为const迭代器。

八、C++强制类型转换

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

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

  1. 隐式类型转化有些情况下可能会出问题:比如数据精度丢失
  2. 显式类型转换将所有情况混合在一起,代码不够清晰

因此C++提出了自己的类型转化风格,注意因为C++要兼容C语言,所以C++中还可以使用C语言的 转化风格。标准C++为了加强类型转换的可视性,引入了四种命名的强制类型转换操作符:

  • static_cast
  • reinterpret_cast
  • const_cast
  • dynamic_cast

8.2、static_cast

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

示例代码:

cpp 复制代码
int main()
{
	// 对应隐式类型转换 -- 数据的意义没有改变
	double d = 12.34;
	int a = static_cast<int>(d);
	cout << a << endl;

	return 0;
}

8.3、reinterpret_cast

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

示例代码:

cpp 复制代码
int main()
{
	// 对应隐式类型转换 -- 数据的意义没有改变
	double d = 12.34;
	int a = static_cast<int>(d);
	cout << a << endl;
	
	// 对应强制类型转换 -- 数据的意义已经发生改变
	int* p1 = reinterpret_cast<int*>(a);;

	return 0;
}

8.4、const_cast

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

示例代码:

cpp 复制代码
int main()
{
	// 对应强制类型转换中有风险的去掉const属性
	//volatile const int b = 2;
	const int b = 2;
	int* p2 = const_cast<int*>(&b);
	*p2 = 3;

	cout << b << endl;
	cout << *p2 << endl;

	return 0;
}

**解释:**上面代码中存在一个问题这里我们确实将b的值改变了,但是如果我们直接打印b的值会发现打印出来的值还是变化前的,这是因为编译器可能直接将b当做一个常量(2)输出出来了,或者从寄存器中直接获取的b的值,而没有重新上内存中获取新的值,导致使用时还是旧值。我们可以通过关键字volatile解决这个问题,被该关键字修饰后每次都会上内存中去取值,确保拿到更新后的新值。

8.5、dynamic_cast

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

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

注意:

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

示例代码:

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

	int _a = 1;
};

class B : public A
{
public:
	int _b = 2;
};

void fun(A* pa)
{
	// dynamic_cast会先检查是否能转换成功(指向子类对象),能成功则转换,
	// (指向父类对象)不能则返回NULL
	// 指向父类转换时有风险的,后续访问存在越界访问的风险
	// 指向子类转换时安全
	B* pb1 = dynamic_cast<B*>(pa);
	if (pb1)
	{
		cout << "pb1:" << pb1 << endl;
		cout << pb1->_a << endl;
		cout << pb1->_b << endl;
		pb1->_a++;
		pb1->_b++;
		cout << pb1->_a << endl;
		cout << pb1->_b << endl;
	}
	else
	{
		cout << "转换失败" << endl;
	}
}

int main()
{
	A a;
	B b;
	fun(&a);
	fun(&b);

	return 0;
}

**解释:**子类转父类没有问题,主要是父类转子类,当父类指针指向子类对象时可以转换成功,当父类指针指向父类对象时会转换失败。

**注意:**强制类型转换关闭或挂起了正常的类型检查,每次使用强制类型转换前,程序员应该仔细考虑是否还有其他不同的方法达到同一目的,如果非强制类型转换不可,则应限制强制转换值的作用域,以减少发生错误的机会。强烈建议:避免使用强制类型转换

九、RTTI

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

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

  1. typeid运算符
  2. dynamic_cast运算符
  3. decltype
相关推荐
星星火柴9364 分钟前
观 察 者 模 式
笔记·设计模式
机器视觉知识推荐、就业指导13 分钟前
Qt/C++面试【速通笔记五】—子线程与GUI线程安全交互
c++·qt·面试·gui·子线程
沐知全栈开发15 分钟前
AJAX 实例
开发语言
边缘常驻民27 分钟前
北京工业大学25计专上岸经验分享
经验分享·计算机考研·北京工业大学
green_pine_32 分钟前
CSS学习笔记12——CSS3新增特性
前端·css·笔记·学习
njsgcs35 分钟前
硬盘对游戏性能没多大影响,对加载速度有影响的游戏有影响
经验分享
大魔王(已黑化)41 分钟前
LeetCode —— 94. 二叉树的中序遍历
数据结构·c++·算法·leetcode·职场和发展
Wenhao.44 分钟前
Go-web开发之帖子功能
开发语言·前端·golang
南玖yy1 小时前
解锁 C++26 的未来:从语言标准演进到实战突破
开发语言·数据库·c++·人工智能·c++23·c++基础语法
恋喵大鲤鱼1 小时前
Golang 身份证号码校验
开发语言·后端·golang