C++ 特殊类的设计


前言: 在本文中,我们掌握几种常见的特殊类的设计。


1. 设计一个只能在堆上创建对象的类

大部分情况下,我们创建的对象都是在栈上,你普通定义一个对象 那么它一般都在在栈上。 要求对象只能在堆上创建,所以 要对 类的构造函数 做些调整。

思路:

  • 将构造函数设置为私有,那么 普通的创建对象方式 就给封死了。
  • 提供一个静态成员函数,此函数返回一个 在堆上创建的对象 。
  • 拷贝构造 它得销毁掉。

代码实现:

class Heap_class
{
private:
	int _a;
	Heap_class(int a = 0)
		:_a(a)
	{}
public:
	static Heap_class* Creat_object(int n)
	{
		return new Heap_class(n);
	}
	Heap_class(const Heap_class&) = delete;
};

使用:

Heap_class* hc= Heap_class::Creat_object(2);

这就是简单的使用,可以看到这个类中有一个私有成员 _a ,所以设计 出构造这个对象,返回的是一个匿名对象,用传的参数n来初始化_a。


如果 不把拷贝构造销毁掉,会出现这样的情况:

Heap_class* hc= Heap_class::Creat_object(2);
	 
Heap_class hh(*hc);

hh就是在栈上开辟的,所以 还是把拷贝构造封掉。


还有就是 有人可能对 设置一个静态成员函数有问题,为啥要设置为静态呢?

静态成员函数 没有 this指针,它不受对象的管理。简言之,没有对象 它依旧可以调用;如果是普通的成员函数,有this指针,调用普通成员函数,必须先有对象,然而对象 还没创建。这就类似先有鸡后有蛋的问题。


2. 设计一个只能在栈上创建对象的类

上面是只能在堆上,现在要求只能在栈上。和上面的思路如出一辙。

还是得控制 构造函数,如何控制?或者 不需要控制 构造函数,而是去控制 new 、delete

  1. 设置静态成员函数,返回一个在栈上创建的对象
  2. 屏蔽new

代码实现:

  1. 第一个版本:
class stack_class
{
private:
	int _a;
	stack_class(int a=0)
		:_a(a)
	{}
public:
	static stack_class&& Creat_object(int n)
	{
		return stack_class(n);
	}
};

使用:

 stack_class s= stack_class::Creat_object(3);
  1. 第二个版本:
class stack_class1
{
private:
	int _a;
	void* operator new(size_t n) = delete;
	void operator delete(void* ptr) = delete;

public:
	stack_class1(int a=0)
		:_a(a)
	{}
};

使用: 正常使用就行、

 stack_class1 s1(2);

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

这就更简单了啊,直接将拷贝构造和赋值重载 delete掉:

class A
{
private:
	int _a;
public:
	A(int a = 0)
		:_a(a)
	{}
	A(const A&) = delete;
	A& operator=(A&) = delete;
};


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

这个给出两种方法:

  1. 控制构造函数,将基类的构造函数设置为私有,就会导致继承此基类的子类,无非初始化基类的东西,从而导致构造子类对象失败。这就可以理解为 基类不能被构造,这是C++98的玩法。
  2. 利用关键字 final 修饰类,这是c++11 才有的。

代码实现:

class B
{
private:
	B(){}
};

class s: public B
{
public:
	s(){}
};

这里其实就已经编译不通过了,因为 基类的构造函数无法访问。

在这里插入图片描述
这里如果不懂为什么,建议去看看我之前的博客,有一篇是专门讲继承的。


class B final
{
public:
	B(){}
};

class s: public B
{
public:
	s(){}
};

在这里插入图片描述


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

其实这里就涉及到设计模式了,一个类只能创建一个对象,那就是单例模式。单例模式,又有两种实现方式:懒汉,饿汉。

思路:

慢慢来讲这个;首先我提个问题:全局变量 在多个源程序中如何使用?问的有点别扭,我们来看代码:

头文件 声明 还有 一个配套的源程序 定义 ;再来一个源程序有来测验:

hc.h:

int a;

hc.cpp:

#include"hc.h"
int a = 1;

test.cpp:

#include<iostream>
#include"hc.h"
using namespace std;

int main()
{
 cout<<a<<endl;
}

像上面那样写,行嘛?看结果:

在这里插入图片描述
怎么办?说明一个问题:头文件处对 a变量也是定义,而不是声明,声明一个 变量前 要加 extern

hc.h:

extern int a;

但是怎么说呢,上面对于一个全局变量 让其他程序共用,用的这种办法是C语言的玩法;到了C++ 我们可以利用面向对象的思想,去设计。

首先,全局变量 让所有程序共用;对应到C++里就是,所有程序,共用一个对象。那么这个类必须 有且只能创建一个对象,也就是单例模式。


  1. 饿汉模式

hc.h:

class A
{
private:
 	int _a;
    static A my_A;
	A(int a = 0):_a(a)
	{}
public:
	void add_a()
	{
		_a++;
	}

	void del_a()
	{
		_a--;
	}
	A(const A&) = delete;
	A& operator=(const A) = delete;

public:
	static A& use_myA()
	{
		return my_A;
	}
};
A A::my_A; /// 程序入口,就已经创建好了对象

使用:

#include"hc.h"
int main()
{
	A::use_myA().add_a();
	A::use_myA().add_a();
	A::use_myA().add_a();
	A::use_myA().del_a();
	A::use_myA().del_a();
}

通过调试看看结果:

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

分析一下饿汉模式:类中有一个静态对象,在程序入口时,已经初始化完成;构造函数依旧给成私有,表明 有且只有一个静态对象供使用;给了两类函数接口,第一个类是static A& use_myA():它是返回了静态对象 ,第二类是void add_a(),void del_a() 操作对象中的成员。


  1. 懒汉模式

懒汉模式,它不会一上来就创建对象,而是 当用到这个对象时,才会去创建对象,供使用。

class B
{
private:
	int _b;
	B(int b=0)
		:_b(b)
	{}

	static B* my_B;
public:
	static B& use_myB()
	{
		if (my_B == nullptr)
		{
			my_B = new B;
		}
		return *my_B;
	}
	void add_b()
	{
		_b++;
	}

	void del_b()
	{
		_b--;
	}
	B(const B&) = delete;
	B& operator=(const B) = delete;

};

B* B::my_B = nullptr; // 先给成空指针

懒汉就是 给的不是对象 而是一个对象指针,它为空时,对象不会构建;当使用它时,判断其为 空,那么构造一个对象。

使用也很简单:

    B::use_myB().add_b();
	B::use_myB().add_b();
	B::use_myB().del_b();

但是懒汉模式 还需要 进一步完善,那就是需要考虑线程安全问题,所以需要 使用互斥锁 。
出现线程安全问题的地方:

       if (my_B == nullptr)
		{
			my_B = new B;
		}

如果多线程,进来,就有可能导致 构造出多个对象。比如:一个线程判断为空,被打断,另一个线程 进来构建了一个对象,切回到上一个线程,然而 第一个线程已经进入 if语句,所以 它又构造了一个对象。

所以需要在这里加上锁。

还有一点要改善的地方在于,在最后 要回收 静态对象,因为它是在堆上创建的,所以需要内嵌一个回收类。

完整版本:

class B
{
private:
	int _b;
	B(int b=0)
		:_b(b)
	{}

	static B* my_B;
	static mutex _mutex;
public:
	static B& use_myB()
	{
		if (my_B == nullptr)
		{
			unique_lock<mutex>(_mutex);
			if(my_B == nullptr)
			my_B = new B;
		}
		return *my_B;
	}
	void add_b()
	{
		_b++;
	}

	void del_b()
	{
		_b--;
	}
	B(const B&) = delete;
	B& operator=(const B) = delete;

	class reclaim
	{
	public:
		~reclaim()
		{
			if (my_B)
			{
				delete my_B;
				my_B = nullptr;
			}
		}
	};

	static reclaim rm_myb;

};

B* B::my_B = nullptr;
mutex B::_mutex;
B::reclaim rm_myb;

可以看到这就是完整版本的了,有内嵌类型,并且还有一个细节就是 上锁那块。

一般情况下,我们上锁是这样的:

   static B& use_myB()
	{
	    unique_lock<mutex>(_mutex);
		if (my_B == nullptr)
		{
			my_B = new B;
		}
		return *my_B;
	}

但是这样上锁,会导致 线程进来都去竞争锁资源。其实只有在 my_B 为空的时候,才需要竞争锁资源去创建一个对象。当对象创建成功,就不需要再竞争锁资源了。因为单例模式嘛,共用一个对象的。

可以优化的 这里:

    static B& use_myB()
	{
		if (my_B == nullptr)
		{
			unique_lock<mutex>(_mutex);
			if(my_B == nullptr)
			my_B = new B;
		}
		return *my_B;
	}

但是 必须是 双判断 ,如果 单判断 其实 就相当于没加这个锁:

比如:

    static B& use_myB()
	{
		if (my_B == nullptr)
		{
			unique_lock<mutex>(_mutex);
			my_B = new B;
		}
		return *my_B;
	}

上面这种,就是 单判断 相当于没加锁;给出一种场景,第一个线程判断为空 并竞争上锁资源,创建了一个对象;但在没创建对象成功前,另一个进程 也判断为空成功,并进入了if语句,但是竞争锁资源失败了。第一个线程 创建对象成功后,释放锁资源,第二线程紧接着获取锁资源,然后又创建了一个对象。 嗯 ,问题就出现了。

综上 ,这里需要用到双判断。

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

动名词

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值