【C++11】特殊类设计 | 类型转换

目录

前言:

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

1.方法一(私有化构造函数)

2.方法二(私有化析构函数)

二、设计只能在栈上创建对象的类

三、单例模式

1.饿汉模式

2.懒汉模式

[2.1 方法一(使用指针)](#2.1 方法一(使用指针))

[2.2 方法二(使用静态成员)](#2.2 方法二(使用静态成员))

四、C++的类型转换

1.内置类型之间的转换

2.内置类型转换为自定义类型

3.自定义类型转换为内置类型

4.自定义类型直接的转换

五、C++强制类型转换

1.static_cast

2.reinterpret_cast

3.const_cast

4.dynamic_cast

[六、 RTTI](#六、 RTTI)

总结:


前言:

不知不觉,已经学习了很多新知识了,但是学海无涯,我们已经掌握了大部分C++11的新语法,本篇我们要学习在一些特定的情况下设计出特殊需求的类。还有之前没有讲解过的类型转换等知识,做好准备。go go go出发了!

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

1.方法一(私有化构造函数)

我们平时这样写,可以在任意内存空间上创建对象:

cpp 复制代码
class HeapOnly
{

};

int main()
{
	HeapOnly hp1; //栈上开辟
	HeapOnly* hp2 = new HeapOnly; //堆上开辟

	return 0;
}

我们先把构造函数封死,写成私有的:

cpp 复制代码
class HeapOnly
{
private:
	HeapOnly()
	{}
};

但是这样也无法在堆上创建对象了,我们开放一个方法使其可以构造对象。

cpp 复制代码
public:
	HeapOnly* CreateObj()
	{
		return new HeapOnly;
	}

但是调用这个函数要有对象,这就矛盾了。是否还记得static方法无需通过对象调用?所以使其变成静态的成员方法,加上static。

类中static修饰的成员或方法 ,无需实例化对象,只需要指定类域即可使用

对于静态成员,需要在类外单独定义(分配存储空间)。访问权限仍然受类域限制。

静态成员变量一般存储在全局/静态存储区。

静态成员函数一般存储在代码区。

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

private:
	HeapOnly()
	{}
};

int main()
{
	//HeapOnly hp1; //栈上开辟
	//HeapOnly* hp2 = new HeapOnly; //堆上开辟
	HeapOnly* hp3 = HeapOnly::CreateObj();

	return 0;
}

但是这样就无法在栈上创建对象了吗?我们可以通过拷贝构造创建对象。

cpp 复制代码
HeapOnly hp4(*hp3);  //创建对象依旧在栈上

所以我们平时会把拷贝构造和赋值重载都给禁用掉:

cpp 复制代码
HeapOnly(const HeapOnly&) = delete;
HeapOnly& operator=(const HeapOnly&) = delete;

所以我们平时会把拷贝构造和赋值重载都给禁用掉。析构函数直接delete即可。

2.方法二(私有化析构函数)

还有第二种方式,把析构写成私有的。

但是我们使用delete也无法释放hp2,因为delete由析构函数和operator delete构成。

可以像之前一样,提供一个公共的方法专门释放空间。

这里在里面也就调用了析构函数。

二、设计只能在栈上创建对象的类

我们还是像之前一样私有化构造方法:

此时我们还是通过提供的静态方法来创建对象:

cpp 复制代码
StackOnly s4 = StackOnly::CreateObj();

还有问题,我们可以调用拷贝构造在堆上创建对象:

我们不能把拷贝构造封死,因为Create返回的是StackOnly,会调用拷贝构造,这样甚至无法创建对象了。

C++规定,如果一个类重载了operator new,必须使用重载的new。

补充一个知识:new和operator new的关系。

new:

完整的内存分配 + 对象构造

new 是一个高级运算符,完成两步操作:

分配内存:调用底层 operator new 分配原始内存。

构造对象:在分配的内存上调用构造函数初始化对象。

operator new:

仅负责内存分配

是 new 的第一步操作,只分配指定大小的原始内存(不涉及对象构造)。

比如这条语句:

cpp 复制代码
StackOnly* s5 = new StackOnly(s4);

这条语句中,执行顺序是严格分两步的,具体流程如下:先分配内存(调用 operator new)。 再调用拷贝构造函数(在分配的内存上构造对象)。

所以为了防止这样的情况发生,我们把operator new和operator delete都禁止掉。

cpp 复制代码
void* operator new(size_t size) = delete;
void operator delete(void* p) = delete; 

我们使用拷贝构造直接在栈上创建对象没有问题,但是还可以在静态区继续创建对象:

cpp 复制代码
StackOnly s6(s4); //在栈上创建对象

static StackOnly s7(s6); //在静态区创建对象

我们可以把拷贝构造禁用掉,之后提供移动构造。

cpp 复制代码
//移动构造
StackOnly(StackOnly&& s)
{}

//禁用拷贝构造
StackOnly(const StackOnly& s) = delete;

但是还是可以在静态区上创建对象,比如:

cpp 复制代码
static StackOnly s6(move(s4));

所以很难十全十美。

三、单例模式

设计一个类,只能创建一个对象。

比如服务器信息,有IP、端口号等等,我们可以把它设计成一个类,但是只能有一个实例对象。

1.饿汉模式

饿汉模式(Eager Initialization) 是一种单例模式的实现方式,其核心特点是在程序启动时(早于 main 函数执行)就提前初始化单例对象,无论后续是否真正用到该实例。它的设计思想是"提前准备好资源",因此得名"饿汉"。

有些抽象,它的意思是在类中定义了一个该对象。

C++类的大小不包括静态成员变量的大小。静态成员属于类本身,不是类实例

cpp 复制代码
class InfoMgr
{
public:
	static InfoMgr& GetInstance()
	{
		return _ins;
	}

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

private:
	InfoMgr()
	{}

private:
	string _ip = "127.0.0.1";
	int _port = 80;
	size_t _buffSize = 1024 * 1024;

	//静态对象
	static InfoMgr _ins;
};

//类外面定义
InfoMgr InfoMgr::_ins;

在类中声明了一个静态成员变量 _ins,并在类外实例化了它。

InfoMgr 类通过静态成员 _ins 确保整个程序只有唯一一个实例对象(单例模式)。这个实例是全局唯一的,因为 _ins 是静态的,且构造函数是私有的(防止外部创建新对象)。

因为没有禁用拷贝构造,这样会发生浅拷贝,并在栈上创建一个InfoMgr对象。所以我们把拷贝构造和赋值重载都禁用掉:

cpp 复制代码
InfoMgr(const InfoMgr&) = delete;
InfoMgr& operator=(const InfoMgr&) = delete;

但是饿汉模式有缺陷,当有多个饿汉模式的单例,某个对象初始化内容较多(读文件),导致程序启动慢。

饿汉模式(Eager Initialization)的单例实现之所以可能导致程序启动变慢,是因为它在程序初始化阶段(早于 main 函数执行)就完成了单例对象的构造。如果单例的构造函数涉及复杂操作(如加载资源、建立连接、初始化大量数据等),会直接拖慢启动时间。

A和B两个饿汉,对象初始化存在依赖关系,要求A先初始化,B再初始化,饿汉无法保证。

比如在一个文件中定义了一个饿汉,另一个文件中也定义了饿汉,无法保证谁先初始化。

2.懒汉模式

2.1 方法一(使用指针)

我们将其对象定义为指针,并初始化为空,当第一次使用静态方法创建对象时,判断为空,为空就new一个对象出来,这样能保证在调用时创建对象且是唯一一个。

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

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

	InfoMgr()
	{}

private:
	string _ip = "127.0.0.1";
	int _port = 80;
	size_t _buffSize = 1024 * 1024;

	//静态对象
	static InfoMgr* _pins;
};

//类外面定义
InfoMgr* InfoMgr::_pins = nullptr;

单例对象一般也不需要释放,因为只有一个,当主函数调用完毕释放。 但是我们来实现一个释放空间的函数:

cpp 复制代码
static void DelInstance()
{
	delete _pins;
	_pins = nullptr;
}

2.2 方法二(使用静态成员)

我们可以不使用指针,在静态函数内部调用时实例化一个对象出来:

支持C++11和之后版本

四、C++的类型转换

1.内置类型之间的转换

内置类型之间的转换分为两种:

  • 隐式类型的转换
  • 显示类型的转换

整形系列,整形和浮点数直接。显示类型是一些类型有一些关联,但是不是非常紧密,需要强制转换,比如指针和整形,不同类型指针之间:

cpp 复制代码
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;
}

2.内置类型转换为自定义类型

其实我们之前就已经见到过这种方法了,它是支持的。它们是通过自定义类型构造函数决定的,会走隐式类型转换:

cpp 复制代码
class A
{
public:
	A(int a)
		: _a1(a)
		, _a2(a)
	{}

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

private:
	int _a1 = 1;
	int _a2 = 1;
};

int main()
{
	string s1 = "1111";
	A aa1 = 1;        //单参数隐式类型转换
	A aa2 = { 2, 2 }; //多参数隐式类型转换
	return 0;
}

如果不想支持隐式类型转换,可以在构造函数前加上explicit声明。

3.自定义类型转换为内置类型

自定义类型能否转换为内置类型?这里我们需要去重载,其实可以重载(),但是它被仿函数占用了,于是有了新语法:

cpp 复制代码
//()被仿函数占用了
// operator + 类型  无返回值 
operator int()
{
	return _a1 + _a2;
}

智能指针的operator bool就使用了这个东西,所以可以做判断,具体使用方法看文档。

4.自定义类型直接的转换

再写一个B类,如果A类需要转换成B,需要在B类中添加构造方法:

cpp 复制代码
class B
{
public:
	B(int b)
		: _b1(b)
	{}

	//使A转换成B
	B(const A& aa)
		: _b1(aa.get())
	{}

private:
	int _b1 = 1;
};

调用A类中的get,所以在A类中添加get方法:

cpp 复制代码
int get() const
{
	return _a1 + _a2;
}

所以自定义类型之间可以互相转换,使用对应的构造函数支持。

我们再举一个例子,我们之前实现过list,使用const迭代器对非const对象进行遍历就会报错。

但是,据我们所学,这里是权限的缩小,应该可以遍历。我们先看标准库是否可以这样遍历。

可以发现没有问题。

权限的缩小和放大,仅限于指针和引用。我们这里是自定义类型,必须在内部处理才能实现。

所以这里是类型转换,而不是权限缩小。

同一个类模板给不同的参数,会实例化不同的类型,所以iterator和const_iterator是两个完全不同的类型。

所以为了实现这个类型转换,我们刚才说自定义类型之间的转换是要通过构造函数的,所以写一个拷贝构造函数,用于 允许 ListIterator<T, T&, T*>(普通迭代器)向 ListIterator<T, const T&, const T*>(const 迭代器)的隐式转换。

cpp 复制代码
ListIterator(const ListIterator<T, T&, T*>& it)
	: _head(it._head)
{}

注意这里的const迭代器是关注我们在list中声明的const_iterator中const修饰的哪一部分,我们这里修饰的是指向内容,所以可以被++。

五、C++强制类型转换

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

  • static_cast
  • reinterpret_cast
  • const_cast
  • dynamic_cast

1.static_cast

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

2.reinterpret_cast

我们之前说过,对于关联性强的可以直接隐式类型转换,而关联性不强的需要用到强制转换,比如执行转指针,所以要用到reinterpret_cast(reinterpret重释):

3.const_cast

对于const类型,我们需要对指针加上const修饰才能取其地址,但是const_cast可以把这个限制去掉:

这里是因为编译器的优化,所以我们加上关键字volatile(易变的;挥发的;无定性的):

4.dynamic_cast

dynamic_cast用于将一个父类对象的指针/引用转换为子类对象的指针或引用(动态转换) 向上转型:子类对象指针/引用->父类指针/引用(不需要转换,赋值兼容规则) 向下转型:父类对象指针/引用->子类指针/引用(用dynamic_cast转型是安全的) 注意: 1. dynamic_cast只能用于父类含有虚函数的类。

我们一般就是向上转型,也就是子转父 ,这样比较安全;但是可能有特殊需求需要父转子 ,我们可以强制转换 ,但是这种方式是不安全 的,所以要使用dynamic_cast来转换。

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

class B : public A
{};

void fun(A* pa)
{
	// dynamic_cast会先检查是否能转换成功,能成功则转换
	// 不能则返回nullptr
	//B* pb1 = static_cast<B*>(pa);
	B* pb2 = dynamic_cast<B*>(pa);
	//cout << "pb1:" << pb1 << endl;
	
	if (pb2)
	{
		cout << "pb2:" << pb2 << endl;
	}
	else
	{
		cout << "转换失败" << endl;
	}
}

int main()
{
	A a;
	B b;
	fun(&a); //父类指针转换为子类指针失败
	fun(&b); //子类指针转换为子类指针成功
	return 0;
}

需要父类中有虚函数,没有虚函数报错。

六、 RTTI

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

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

  • typeid运算符
  • dynamic_cast运算符
  • decltype

总结:

有一些知识点可能过得比较快,但用法也确实简单,这篇我们学习的内容和之前相比很简单,我们学的可能也并不深入,但是至少要有所了解,以后工作中很有可能会使用到,所以需要把这部分学习一遍。加油吧少年!

相关推荐
加成BUFF2 小时前
C++入门详解2:数据类型、运算符与表达式
c语言·c++·计算机
徐行code2 小时前
std::bind()和lambda的区别
c++
小老鼠不吃猫3 小时前
C++20 STL <numbers> 数学常量库
开发语言·c++·c++20
程序员zgh3 小时前
C++常用设计模式
c语言·数据结构·c++·设计模式
im_AMBER3 小时前
Leetcode 80 统计一个数组中好对子的数目
数据结构·c++·笔记·学习·算法·leetcode
尘诞辰3 小时前
【C语言】数据在内存中的储存
c语言·开发语言·数据结构·c++
无敌最俊朗@3 小时前
STL-关联容器(面试复习4)
开发语言·c++
无限进步_4 小时前
【C语言】栈(Stack)数据结构的实现与应用
c语言·开发语言·数据结构·c++·后端·visual studio
闻缺陷则喜何志丹4 小时前
【计算几何 SAT轴】P6732 「Wdsr-2」方分|普及+
c++·数学·计算几何·sat轴·凸多边形分离