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

一.特殊类设计

1.只能在堆上创建对象的类

只能在堆上创建对象,也就是只能通过new操作符创建对象,方式如下:

  1. 将构造函数设置为私有,防止外部直接调用构造函数在栈上创建对象。
  2. 向外部提供一个获取对象的static接口,该接口在堆上创建一个对象并返回。
  3. 将拷贝构造函数设置为私有,并且只声明不实现,防止外部调用拷贝构造函数在栈上创建对象。

代码如下:

class HeapOnly
{
    
    
public:
	//2、提供一个获取对象的接口,并且该接口必须设置为静态成员函数
	static HeapOnly* CreateObj()
	{
    
    
		return new HeapOnly;
	}
private:
	//1、将构造函数设置为私有
	HeapOnly()
	{
    
    }
	//3、将拷贝构造函数设置为私有,并且只声明不实现
	//C++98
	HeapOnly(const HeapOnly&);
	//C++11
	//HeapOnly(const HeapOnly&) = delete;
};

说明一下:

  1. 向外部提供的CreateObj函数必须设置为静态成员函数,因为外部调用该接口就是为了获取对象的,而非静态成员函数必须通过对象才能调用,这就变成鸡生蛋蛋生鸡的问题了。
  2. C++98通过将拷贝构造函数声明为私有以达到防拷贝的目的,C++11可以在拷贝构造函数后面加上=delete,表示让编译器将拷贝构造函数删除,此时也能达到防拷贝的目的。

2.只能在栈上创建对象的类

方法一

方式如下:

  1. 将构造函数设置为私有,防止外部直接调用构造函数在堆上创建对象。
  2. 向外部提供一个获取对象的static接口,该接口在栈上创建一个对象并返回。

代码如下:

class StackOnly
{
    
    
public:
	//2、提供一个获取对象的接口,并且该接口必须设置为静态成员函数
	static StackOnly CreateObj()
	{
    
    
		return StackOnly();
	}
private:
	//1、将构造函数设置为私有
	StackOnly()
	{
    
    }
};

但该方法有一个缺陷就是,无法防止外部调用拷贝构造函数创建对象。

StackOnly obj1 = StackOnly::CreateObj();
static StackOnly obj2(obj1); //在静态区拷贝构造对象
StackOnly* ptr = new StackOnly(obj1); //在堆上拷贝构造对象

但是我们不能将构造函数设置为私有,也不能用=delete的方式将拷贝构造函数删除,因为CreateObj函数当中创建的是局部对象,返回局部对象的过程中势必需要调用拷贝构造函数。

方法二

方式如下:

  • 屏蔽operator new函数和operator delete函数。

代码如下:

class StackOnly
{
    
    
public:
	StackOnly()
	{
    
    }
private:
	//C++98
	void* operator new(size_t size);
	void operator delete(void* p);
	//C++11
	//void* operator new(size_t size) = delete;
	//void operator delete(void* p) = delete;
};

new和delete的原理:

  • new在堆上申请空间实际分为两步,第一步是调用operator new函数申请空间,第二步是在申请的空间上执行构造函数,完成对象的初始化工作。
  • delete在释放堆空间也分为两步,第一步是在该空间上执行析构函数,完成对象中资源的清理工作,第二步是调用operator delete函数释放对象的空间。

new和delete默认调用的是全局的operator new函数和operator delete函数,但如果一个类重载了专属的operator new函数和operator delete函数,那么new和delete就会调用这个专属的函数。所以只要把operator new函数和operator delete函数屏蔽掉,那么就无法再使用new在堆上创建对象了。

但该方法也有一个缺陷,就是无法防止外部在静态区创建对象。

static StackOnly obj; //在静态区创建对象

当然,也可以将方法一和方法二进行结合,结合之后就只是无法防止在静态区拷贝构造对象了。

3.设计一个不能被拷贝的类

要让一个类不能被拷贝,就要让该类不能调用拷贝构造函数和赋值运算符重载函数,因此直接将该类的拷贝构造函数和赋值运算符重载函数设置为私有,或者用C++11的方式将这两个函数删除即可。

代码如下:

class CopyBan
{
    
    
public:
	CopyBan()
	{
    
    }
private:
	//C++98
	CopyBan(const CopyBan&);
	CopyBan& operator=(const CopyBan&);
	//C++11
	//CopyBan(const CopyBan&) = delete;
	//CopyBan& operator=(const CopyBan&) = delete;
};

4.设计一个不能被继承的类

方法一:C++98

将该类的构造函数设置为私有即可,因为子类的构造函数被调用时,必须调用父类的构造函数初始化父类的那一部分成员,但父类的私有成员在子类当中是不可见的,所以在创建子类对象时子类无法调用父类的构造函数对父类的成员进行初始化,因此该类被继承后子类无法创建出对象。

代码如下:

class NonInherit
{
    
    
public:
	static NonInherit CreateObj()
	{
    
    
		return NonInherit();
	}
private:
	//将构造函数设置为私有
	NonInherit()
	{
    
    }
};

方法二:C++11

C++98的这种方式其实不够彻底,因为这个类仍然可以被继承(编译器不会报错),只不过被继承后无法实例化出对象而已。于是C++11中提供了final关键字,被final修饰的类叫做最终类,最终类无法被继承,此时就算继承后没有创建对象也会编译出错。

代码如下:

class NonInherit final
{
    
    
	//...
};

二.单例模式

什么是单例模式?

  • 单例模式是一种设计模式(Design Pattern),设计模式就是一套被反复使用、多数人知晓的、经过分类编目的、代码设计经验的总结。使用设计模式的目的就是为了可重用代码、让代码更容易被他人理解、保证代码可靠性程序的重用性。
  • 单例模式指的就是一个类只能创建一个对象,该模式可以保证系统中该类只有一个实例,并提供一个访问它的全局访问点,该实例被所有程序模块共享。
  • 比如在某个服务器程序中,该服务器的配置信息存放在一个文件中,这些配置数据由一个单例对象同一读取,然后服务进程中的其他对象再通过这个单例对象获取这些配置信息,这种方式简化了在复杂环境下的配置管理。

单例模式有两种实现方式,分别是饿汉模式和懒汉模式:

饿汉模式

单例模式的饿汉实现方式如下:

  1. 将构造函数设置为私有,并将拷贝构造函数和赋值运算符重载函数设置为私有或删除,防止外部创建或拷贝对象。
  2. 提供一个指向单例对象的static指针,并在程序入口之前完成单例对象的初始化。
  3. 提供一个全局访问点获取单例对象。

代码如下:

class Singleton
{
    
    
public:
	//3、提供一个全局访问点获取单例对象
	static Singleton* GetInstance()
	{
    
    
		return _inst;
	}
private:
	//1、将构造函数设置为私有,并防拷贝
	Singleton()
	{
    
    }
	Singleton(const Singleton&) = delete;
	Singleton& operator=(const Singleton&) = delete;
	//2、提供一个指向单例对象的static指针
	static Singleton* _inst;
};

//在程序入口之前完成单例对象的初始化
Singleton* Singleton::_inst = new Singleton;

懒汉模式

单例模式的懒汉实现方式如下:

  1. 将构造函数设置为私有,并将拷贝构造函数和赋值运算符重载函数设置为私有或删除,防止外部创建或拷贝对象。
  2. 提供一个指向单例对象的static指针,并在程序入口之前先将其初始化为空。
  3. 提供一个全局访问点获取单例对象。

代码如下:

class Singleton
{
    
    
public:
	//3、提供一个全局访问点获取单例对象
	static Singleton* GetInstance()
	{
    
    
		//双检查
		if (_inst == nullptr)
		{
    
    
			_mtx.lock();
			if (_inst == nullptr)
			{
    
    
				_inst = new Singleton;
			}
			_mtx.unlock();
		}
		return _inst;
	}
private:
	//1、将构造函数设置为私有,并防拷贝
	Singleton()
	{
    
    }
	Singleton(const Singleton&) = delete;
	Singleton& operator=(const Singleton&) = delete;
	//2、提供一个指向单例对象的static指针
	static Singleton* _inst;
	static mutex _mtx; //互斥锁
};

//在程序入口之前先将static指针初始化为空
Singleton* Singleton::_inst = nullptr;
mutex Singleton::_mtx; //初始化互斥锁

饿汉模式和懒汉模式对比

  • 饿汉模式的优点就是简单,但是它的缺点也比较明显。饿汉模式在程序运行主函数之前就会创建单例对象,如果单例类的构造函数中所做的工作比较多,就会导致程序迟迟无法进入主函数,在外部看来就好像是程序卡住了。
  • 此外,如果有多个单例类需要创建单例对象,并且它们之间的初始化存在某种依赖关系,比如单例对象A的创建必须在单例对象B之后,此时饿汉模式也会存在问题,因为我们无法保证这多个单例对象中的哪个对象先创建。
  • 而懒汉模式就能很好的解决上述饿汉模式的缺点,因为懒汉模式并不是一开始就完成单例对象的创建,因此不会导致程序迟迟无法进入主函数,并且懒汉模式中各个单例对象创建的顺序是由各个单例类中的GetInstance函数第一次被调用的顺序决定,因此是可控制的。
  • 懒汉模式的缺点就是,在编码上比饿汉模式复杂,在创建单例对象时需要考虑线程安全的问题。

单例对象的释放

单例对象创建后一般在整个程序运行期间都可能会使用,所以我们可以不考虑单例对象的释放,程序正常结束时会自动将资源归还给操作系统。

如果要考虑单例对象的释放,可以参考以下两种方式:

  1. 在单例类中编写一个DelInstance函数,在该函数中进行单例对象的释放动作,当不再需要该单例对象时就可以主动调用DelInstance释放单例对象。

代码如下:

static void DelInstance()
{
    
    
	_mtx.lock();
	if (_inst != nullptr)
	{
    
    
		delete _inst;
		_inst = nullptr;
	}
	_mtx.unlock();
}
  1. 在单例类中实现一个内嵌的垃圾回收类,在垃圾回收类的析构函数中完成单例对象的释放。在单例类中定义一个静态的垃圾回收类对象,当该对象被消耗时就会调用其析构函数,这时便对单例对象进行了释放。

代码如下:

//垃圾回收类
class CGarbo
{
    
    
public:
	~CGarbo()
	{
    
    
		if (_inst != nullptr)
		{
    
    
			delete _inst;
			_inst = nullptr;
		}
	}
};

三.类型转换

1.C语言中的类型转换

C语言和C++都是强类型语言,如果赋值运算符左右两侧变量的类型不同,或形参与实参的类型不匹配,或返回值类型与接收返回值的变量类型不一致,那么就需要进行类型转换。

C语言中有两种形式的类型转换,分别是隐式类型转换和显式类型转换:

  • 隐式类型转换:编译器在编译阶段自动进行,能转就转,不能转就编译失败。
  • 显式类型转换:需要用户自己处理,以(指定类型)变量的方式进行类型转换。

需要注意的是,只有相近类型之间才能发生隐式类型转换,比如int和double表示的都是数值,只不过它们表示的范围和精度不同。而指针类型表示的是地址编号,因此整型和指针类型之间不会进行隐式类型转换,如果需要转换则只能进行显式类型转换。比如:

int main()
{
    
    
	//隐式类型转换
	int i = 1;
	double d = i;
	cout << i << endl;
	cout << d << endl;

	//显式类型转换
	int* p = &i;
	int address = (int)p;
	cout << p << endl;
	cout << address << endl;
	return 0;
}

2.C++类型转换

C风格的转换格式虽然很简单,但也有很多缺点:

  • 隐式类型转换在某些情况下可能会出问题,比如数据精度丢失。
  • 显式类型转换将所有情况混合在一起,转换的可视性比较差。

因此C++为了加强类型转换的可视性,引入了四种命名的强制类型转换操作符,分别是static_castreinterpret_castconst_castdynamic_cast

①static_cast

static_cast用于相近类型之间的转换,编译器隐式执行的任何类型转换都可用static_cast,但它不能用于两个不相关类型之间转换。比如:

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

	int* p = &a;
	// int address = static_cast<int>(p); //error
	return 0;
}

②reinterpret_cast

reinterpret_cast用于两个不相关类型之间的转换。比如:

int main()
{
    
    
	int a = 10;
	int* p = &a;
	int address = reinterpret_cast<int>(p);
	cout << address << endl;
	return 0;
}

reinterpret_cast还有一个非常bug的用法,比如在下面的代码中将带参带返回值的函数指针转换成了无参无返回值的函数指针,并且还可以用转换后函数指针调用这个函数。

typedef void(*FUNC)();
int DoSomething(int i)
{
    
    
	cout << "DoSomething: " << i << endl;
	return 0;
}
int main()
{
    
    
	FUNC f = reinterpret_cast<FUNC>(DoSomething);
	f();
	return 0;
}

说明一下: 用转换后的函数指针调用该函数时没有传入参数,因此这里打印出参数i的值是一个随机值

③ const_cast

const_cast用于删除变量的const属性,转换后就可以对const变量的值进行修改。比如:

int main()
{
    
    
	const int a = 2;
	int* p = const_cast<int*>(&a);
	*p = 3;
	cout << a << endl;  //2
	cout << *p << endl; //3
	return 0;
}

说明一下

  • 代码中用const_cast删除了变量a的地址的const属性,这时就可以通过这个指针来修改变量a的值。
  • 由于编译器认为const修饰的变量是不会被修改的,因此会将const修饰的变量存放到寄存器当中,当需要读取const变量时就会直接从寄存器中进行读取,而我们修改的实际上是内存中的a的值,因此最终打印出a的值是未修改之前的值。
  • 如果不想让编译器将const变量优化到寄存器当中,可以用volatile关键字对const变量进行修饰,这时当要读取这个const变量时编译器就会从内存中进行读取,即保持了该变量在内存中的可见性。

④dynamic_cast

dynamic_cast用于将父类的指针(或引用)转换成子类的指针(或引用)。

向上转型与向下转型

  • 向上转型: 子类的指针(或引用)→ 父类的指针(或引用)。
  • 向下转型: 父类的指针(或引用)→ 子类的指针(或引用)。

其中,向上转型就是所说的切割/切片,是语法天然支持的,不需要进行转换,而向下转型是语法不支持的,需要进行强制类型转换。

向下转型的安全问题

向下转型分为两种情况:

  1. 如果父类的指针(或引用)指向的是一个父类对象,那么将其转换为子类的指针(或引用)是不安全的,因为转换后可能会访问到子类的资源,而这个资源是父类对象所没有的。
  2. 如果父类的指针(或引用)指向的是一个子类对象,那么将其转换为子类的指针(或引用)则是安全的。

使用C风格的强制类型转换进行向下转型是不安全的,因为此时无论父类的指针(或引用)指向的是父类对象还是子类对象都会进行转换。而使用dynamic_cast进行向下转型则是安全的,如果父类的指针(或引用)指向的是子类对象那么dynamic_cast会转换成功,但如果父类的指针(或引用)指向的是父类对象那么dynamic_cast会转换失败并返回一个空指针。比如:

class A
{
    
    
public:
	virtual void f()
	{
    
    }
};
class B : public A
{
    
    };
void func(A* pa)
{
    
    
	B* pb1 = (B*)pa;               //不安全
	B* pb2 = dynamic_cast<B*>(pa); //安全

	cout << "pb1: " << pb1 << endl;
	cout << "pb2: " << pb2 << endl;
}
int main()
{
    
    
	A a;
	B b;
	func(&a);
	func(&b);
	return 0;
}

上述代码中,如果传入func函数的是子类对象的地址,那么在转换后pb1和pb2都会有对应的地址,但如果传入func函数的是父类对象的地址,那么转换后pb1会有对应的地址,而pb2则是一个空指针。

说明一下: dynamic_cast只能用于含有虚函数的类,因为运行时类型检查需要运行时的类型信息,而这个信息是存储在虚函数表中的,只有定义了虚函数的类才有虚函数表。

explicit

explicit用来修饰构造函数,从而禁止单参数构造函数的隐式转换。比如:

class A
{
    
    
public:
	explicit A(int a)
	{
    
    
		cout << "A(int a)" << endl;
	}
	A(const A& a)
	{
    
    
		cout << "A(const A& a)" << endl;
	}
private:
	int _a;
};
int main()
{
    
    
	A a1(1);
	//A a2 = 1; //error
	return 0;
}

在语法上,代码中的A a2 = 1等价于以下两句代码:

A tmp(1);  //先构造
A a2(tmp); //再拷贝构造

所以在早期的编译器中,当编译器遇到A a2 = 1这句代码时,会先构造一个临时对象,再用这个临时对象拷贝构造a2。但是现在的编译器已经做了优化,当遇到A a2 = 1这句代码时,会直接按照A a2(1)的方式进行处理,这也叫做隐式类型转换。

但对于单参数的自定义类型来说,A a2 = 1这种代码的可读性不是很好,因此可以用explicit修饰单参数的构造函数,从而禁止单参数构造函数的隐式转换。

3.RTTI

RTTI(Run-Time Type Identification)就是运行时类型识别。

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

  1. typeid:在运行时识别出一个对象的类型。
  2. dynamic_cast:在运行时识别出一个父类的指针(或引用)指向的是父类对象还是子类对象。
  3. decltype:在运行时推演出一个表达式或函数返回值的类型。

4.常见面试题

  1. C++中的4种类型转换分别是:____ 、____ 、____ 、____。

    分别是static_cast、reinterpret_cast、const_cast和dynamic_cast。

  2. 说说4种类型转换的应用场景。

    • static_cast用于相近类型的类型之间的转换,编译器隐式执行的任何类型转换都可用static_cast。
    • reinterpret_cast用于两个不相关类型之间的转换。
    • const_cast用于删除变量的const属性,方便赋值。
    • dynamic_cast用于安全的将父类的指针(或引用)转换成子类的指针(或引用)。

本文到此结束,码文不易,还请多多支持哦!!!

猜你喜欢

转载自blog.csdn.net/weixin_67401157/article/details/132264711