C++进阶篇10---特殊类设计

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

有人可能会觉得,这不是很简单吗,直接把拷贝构造ban掉,不就行了,但事实真的如此吗?

class A
{
public:
	A(){}
	A(const A& tmp) = delete;
    // ...
};

int main()
{
	A a;
	// A b = a; // 该形式的拷贝被ban了
	A b;
	b = a; // 可以通过赋值重载来变相的完成拷贝
	return 0;
}

所以,正确的做法是将赋值重载和拷贝构造都ban掉,代码如下

class A
{
public:
	A(){}
    // C++11 的做法 --- 直接ban掉
	A(const A& tmp) = delete;
	A& operator=(const A& tmp) = delete;
    // ...
private:
    // C++98 的做法 --- 声明为私有,只声明不实现的原因是类外不能调用,实现了也没用
    // A(const A& tmp);
	// A& operator=(const A& tmp);
};

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

在考虑只能在堆上建立对象之前,我们来想想如何禁止在栈上创建对象?而创建对象跟构造函数有关,所以问题变成如何调整构造函数,从而ban掉在栈上创建对象?

其实我们只要将构造函数的访问权限设置为private,那么我们就无法在外部调用构造函数,也就无法在栈上创建对象,代码如下

class A
{
private:
	A(){} // 将构造函数放在private中,不让外部使用
};

但是,现在我们也不能在堆上创建对象,因为new内部也要调用A的构造函数,如何解决?既然不能在外部创建对象,那么我们只能在类内提供接口,因为类内是允许访问成员函数的,同时,我们也需要让外部在没有对象的情况下能调用到该接口,显然该接口得是静态的成员函数,代码如下

class A
{
public:
	static A* HeapOnly()
	{
		return new A;
	}
private:
	A(){} // 将构造函数放在private中,不让外部使用,只能在类内提供接口
};

int main()
{
	A* a = A::HeapOnly();
	return 0;
}

 但是,现在的代码还是存在问题,结合第一个设计样例的问题,我们很容易发现,当我们在对申请出来的堆上的对象进行拷贝操作时,我们就避开了构造函数,在栈上创建出了对象,如下

int main()
{
	A* a = A::HeapOnly();
	A b = *a; // 拷贝构造
    // A c; c=*a; // 不行,因为赋值的前提是得先有对象
	return 0;
}

所以,正确的做法是还要将拷贝构造ban掉,代码如下

class A
{
public:
	static A* HeapOnly()
	{
		return new A;
	}
private:
	A(){} // 将构造函数放在private中,不让外部使用,只能在类内提供接口
	// C++11 的做法
	A(const A& tmp) = delete;
	// C++98 的做法
	// A(const A& tmp);
};

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

和设计一个只能在堆上创建对象的类的思路大致相同,我们同样需要将构造函数设为私有,并且对外提供一个静态的成员函数用来返回在栈上创建的对象,代码如下

class A
{
public:
	static A StackOnly()
	{
		return A();
	}
private:
	A(){}
};

但是我们如何禁止它在堆上创建对象呢?可能有人会觉得上面这个类,已经无法在堆上创建对象了,因为new无法调用到构造函数,但是,new可以调用拷贝构造呀,你不是能创建栈上的对象吗,我就拿你创建的对象进行拷贝构造,如下

int main()
{
	A a = A::StackOnly();
	A* p = new A(a);
	return 0;
}

那么我们该如何做?将new和delete直接ban掉,从根源上解决问题,一劳永逸,代码如下

class A
{
public:
	static A StackOnly()
	{
		return A();
	}
	void* operator new(size_t) = delete;
	void operator delete(void*) = delete;
private:
	A(){}
};

四、设置一个类,不能被继承

这个就很简单,直接在给类加上 final 关键字即可(C++11加的),代码如下

class A final
{
    // ...
};

当然,我们也能通过将该类的构造函数私有化来实现,因为子类对象的创建需要先创建它的父类部分,而显然它如法调用父类的构造函数,故无法被继承,代码如下

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

扩展:结合不能被继承的思路,如果我们不想一个类能被拷贝 / 赋值,除了将该类的赋值重载和拷贝构造ban掉,我们还可以如何做?

我们可以让该类继承一个被ban掉了赋值重载和拷贝构造的基类,这样由于基类成员无法被拷贝和赋值,就会导致它也无法被拷贝和赋值,从而实现了该类也无法被拷贝和赋值的操作

class nocopy
{
public:
	nocopy(){}
	nocopy(const nocopy& tmp) = delete;
	nocopy& operator=(const nocopy& tmp) = delete;
};

class A :public nocopy
{
	//...
};
int main()
{
	A a;
	A b = a; // 报错
	A c; c = a; // 报错
	return 0;
}

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

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

1、饿汉模式

提前(main函数启动时)创建好实例对象

如何实现?

1、该对象只能有一个,我们肯定需要将构造函数设置为私有,不让我们能在外面创建对象

2、该对象得是个全局的,且不能在类外创建,它只能是类的静态成员变量,并且类的成员变量一般都设置为私有,所以我们还得给它提供一个静态成员函数,让外界能访问到它

3、根据上面设计的经验,我们还要考虑赋值重载和拷贝构造的问题,这里我们全部ban掉,理由同上 (ps:防止有人整什么奇怪的操作)

如果有没明白的地方,可以看看代码,代码如下

// 假设我们需要一个单例字典
class A
{
public:
	static A* GetInstance()
	{
		return &_inst;
	}

	void push(const string& src, const string& dest)
	{
		_mp[src] = dest;
	}

	void print()
	{
		for (auto it : _mp)
		{
			cout << it.first << "  " << it.second << endl;
		}
	}

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

private:
	A(){}
private:
	map<string, string>_mp;
	// A _a; // 这种写法是错误的,A类中不能包含A类对象
	static A _inst; // 这样是可以的,因为静态变量不属于类对象,属于整个类,存放在静态区
	// 1、静态成员属于类,所以能调用构造函数
	// 2、静态成员在整个类中只有一份
    // 3、静态成员能通过 类域 访问,即可以全局访问,只要能找到这个类,当然这里我们需要通过接口间接得到
	// 完美卡上所有我们需要的条件
};

A A::_inst;

int main()
{
	A::GetInstance()->push("left", "左边");
	A::GetInstance()->push("right", "右边");
	A::GetInstance()->push("apple", "苹果");
	A::GetInstance()->push("zxws", "帅");// (doge)
	A::GetInstance()->print();
	return 0;
}

优点:实现简单
缺点:
1、可能导致进程启动满,因为它在main函数之前就要被初始化好
2、如果两个单例启动有先后顺序,那么 饿汉模式 无法控制

2、懒汉模式

第一次使用时,才创建,和饿汉模式的代码很相似,如下

class A
{
public:
	static A* GetInstance()
	{
		if (_inst == nullptr)
		{
			_inst = new A;
		}
		return _inst;
	}

	void push(const string& src, const string& dest)
	{
		_mp[src] = dest;
	}

	void print()
	{
		for (auto it : _mp)
		{
			cout << it.first << "  " << it.second << endl;
		}
	}

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

private:
	A() {}
private:
	map<string, string>_mp;
	static A* _inst;
};

A* A::_inst = nullptr;

int main()
{
	A::GetInstance()->push("left", "左边");
	A::GetInstance()->push("right", "右边");
	A::GetInstance()->push("apple", "苹果");
	A::GetInstance()->push("zxws", "帅");// (doge)
	A::GetInstance()->print();
	return 0;
}

一般来说,如果没有什么特殊要求,这样写就可以了(进程正常结束会自动释放空间),但是如果我们需要在释放单例之前做一些操作,就需要我们去调用析构函数,同时我们希望它能在程序结束时被自动调用,如何做?代码如下

class A
{
public:
	static A* GetInstance()
	{
		if (_inst == nullptr)
		{
			_inst = new A;
		}
		return _inst;
	}

	void push(const string& src, const string& dest)
	{
		_mp[src] = dest;
	}

	void print()
	{
		for (auto it : _mp)
		{
			cout << it.first << "  " << it.second << endl;
		}
	}

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

	static void DelInstance() // 提供手动释放的接口
	{
		if (_inst)
		{
			// ...
			delete _inst;
			_inst = nullptr;
		}
	}
private:
	A() {}
private:
	map<string, string>_mp;
	static A* _inst;

	class gc
	{
	public:
		~gc()
		{
			DelInstance();
		}
	};
	static gc _gc; // 用一个类来控制资源,类似智能指针
};

A* A::_inst = nullptr;
A::gc A::_gc;

除此之外,懒汉模式在创建和销毁时,还会涉及线程安全问题,即当多个线程同时创建/销毁进程时,会导致空间泄露 / 多次销毁同一段空间 的问题,所以我们需要给它加锁,代码如下

class A
{
public:
	static A* GetInstance()
	{
		if (_inst == nullptr) // 当单例创建完,后续线程在访问时,没必要在去申请锁去判断是否要创建
		{
			lock_guard<mutex> lock(_mutex);
			if (_inst == nullptr)
			{
				_inst = new A;
			}
		}
		return _inst;
	}

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

	static void DelInstance()
	{
		if (_inst) // 当单例释放完,后续线程在释放时,没必要在去申请锁去判断是否要释放
		{
			lock_guard<mutex>lock(_mutex);
			if (_inst)
			{
				delete _inst;
				_inst = nullptr;
			}
		}
	}
private:
	A() {}
private:

	static A* _inst;

	class gc
	{
	public:
		~gc()
		{
			DelInstance();
		}
	};
	static gc _gc;
	static mutex _mutex;
};

A* A::_inst = nullptr;
A::gc A::_gc;
mutex A::_mutex;

这里说明一点,上面线程安全的单例,只保证在创建和销毁单例时是线程安全的,并不保证在使用该单例的资源时也是线程安全的,需要我们自己去维护。

最简单的懒汉模式

class A
{
public:
	static A* GetInstance()
	{
        // C++11后保证静态变量的创建是线程安全的
        // 静态变量存放在静态区,只在第一次调用该函数的时候初始化
		static A inst; 
		return &inst;
	}

	A(const A& tmp) = delete;
	A& operator=(const A& tmp) = delete;
private:
	A() {}
};
  • 23
    点赞
  • 18
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值