【C++学习笔记】C++特殊类设计!你绝对不能错过的干货!

[本节目标]

  • 掌握常见特殊类的设计方式

1.请设计一个类,不能被拷贝

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

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

class CopyBan
{
    // ...

private:
    CopyBan(const CopyBan&);
    CopyBan& operator=(const CopyBan&);
    //...
};

原因:

⭐1. 设置成私有:如果只声明没有设置成private,用户自己如果在类外定义了,就不能禁            止拷贝了

⭐2. 只声明不定义:不定义是因为该函数根本不会调用,定义了其实也没有什么意义,不写         反而还简单,而且如果定义了就不能防止成员函数内部拷贝了。

C++11:C++11扩展delete的用法,delete除了释放new申请的资源外,如果在默认成员函数后跟上 =delete,表示让编译器删除掉该默认成员函数。

class CopyBan
{
    // ...
    CopyBan(const CopyBan&) = delete;
    CopyBan& operator=(const CopyBan&) = delete;
    //...
};

在c++标准库中对于IO流的对象不能拷贝就是采用的c++11的方式

2. 请设计一个类,只能在堆上创建对象

实现方式:

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

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

class HeapOnly
{
private:
	int _x;
	vector<int> _a;
};

int main()
{
	HeapOnly ho1;//栈上开辟
	HeapOnly* ho2 = new HeapOnly;//堆上开辟
	return 0;
}

如果我们没有任何设置,此时在内存任意地方我们就可以创建对象,但是我们要知道一点,无论我们在哪个地方创建对象,都要去调用默认的构造函数(如果我们没有显示实现),当我们把构造函数私有时,此时就不能创建对象了。

此时就能阻止在类外栈上创建对象,但是此时也阻止了在类外堆上开辟空间,但是我们现在的场景是只能在堆上开辟,怎么做呢?但是咱们在类里面还是可以创建对象的,类是我们自己的,我们可以写死,只写在堆上开辟空间的代码。

class HeapOnly
{
public:
	// 模板的可变参数
	template <class... Args>
	HeapOnly* CreatObj(Args&&... args)
	{
		return new HeapOnly(args...);//调用构造函数
	}
private:
	HeapOnly()// 构造私有化
	{}
	int _x;
	int _y;

	HeapOnly(int x, int y)
		: _x(x)
		, _y(y)
	{}
	vector<int> _a;
};

但是此时我们又会发现一个问题,此时我们调用CreatObj调不动。

但是我们就是想通过CreatObj来创建对象,但是Creatobj又需要我们的对象去调用,所以会出现报错。我们可以设置成静态成员函数,并且静态成员里面调用构造函数不需要this,因为我们是直接new出来的,直接会去调用构造函数,而静态成员函数通过类名加上域访问限定符即可调用。

class HeapOnly
{
public:
	// 模板的可变参数
	template <class... Args>
	static HeapOnly* CreatObj(Args&&... args)
	{
		return new HeapOnly(args...);//调用构造函数
	}
private:
	HeapOnly()// 构造私有化
	{}
	int _x;
	int _y;

	HeapOnly(int x, int y)
		: _x(x)
		, _y(y)
	{}
	vector<int> _a;
};

int main()
{
	HeapOnly* ho1 = HeapOnly::CreatObj();
	HeapOnly* ho2 = HeapOnly::CreatObj(1,2);
	return 0;
}

那我们当前的设计有没有什么漏洞呢?

我还要封掉拷贝构造函数和赋值重载,虽然赋值重载是堆上开辟的空间,但是此时是默认的赋值重载,进行的是浅拷贝,有时候我们不期望值拷贝,所以我们也封掉。

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

方法二:把析构函数封掉

// 只能在堆上
class HeapOnly
{
public:
	HeapOnly()
	{}

	HeapOnly(int x, int y)
		:_x(x)
		,_y(y)
	{}

	void Destroy()
	{
		delete this;
	}
	HeapOnly(const HeapOnly&) = delete;
	HeapOnly& operator=(const HeapOnly&) = delete;
private:
	// 析构函数私有
	~HeapOnly()
	{
		cout << "~HeapOnly()" << endl;
	}
	int _x;
	int _y;
	vector<int> _a;
};

此时我们还可以把堆上开辟的空间交给智能指针来管理。

3. 请设计一个类,只能在栈上创建对象

同上将构造函数私有化,然后设计静态方法创建对象返回即可

class StackOnly
{
public:
	// 模板的可变参数
	template <class... Args>
	static StackOnly CreatObj(Args&&... args)
	{
		return StackOnly(args...);//调用拷贝构造函数 + 构造函数
		// 返回栈上面的匿名对象,传值返回
	}
	//StackOnly(const StackOnly&) = delete;//不能封死了
	StackOnly& operator=(const StackOnly&) = delete;
private:
	StackOnly()// 构造私有化
	{}
	int _x;
	int _y;

	StackOnly(int x, int y)
		: _x(x)
		, _y(y)
	{}
	vector<int> _a;
};

int main()
{
	StackOnly so1 = StackOnly::CreatObj();
	StackOnly so2 = StackOnly::CreatObj(1,1);

	return 0;
}

但是此时又出现一个漏洞了,我们拷贝构造没有封掉

还记得我们的new会干什么嘛?它会调用operator new + 构造函数,拷贝构造函数也是构造函数的一种,我们既然不能封掉拷贝构造,那么我们来封掉operator new,但是operator new是一个全局的,所以我们在类里面自己定义就不会调用全局的,然后我们再封掉自己写的operator new即可。

class StackOnly
{
public:
	// 模板的可变参数
	template <class... Args>
	static StackOnly CreatObj(Args&&... args)
	{
		return StackOnly(args...);//调用拷贝构造函数 + 构造函数
		// 返回栈上面的匿名对象,传值返回
	}
	//StackOnly(const StackOnly&) = delete;//不能封死了
	StackOnly& operator=(const StackOnly&) = delete;

	// 重载一个类专属的operator new
	void* operator new(size_t n) = delete;
private:
	StackOnly()// 构造私有化
	{}
	int _x;
	int _y;

	StackOnly(int x, int y)
		: _x(x)
		, _y(y)
	{}
	vector<int> _a;
};

int main()
{
	StackOnly so1 = StackOnly::CreatObj(); //在栈上开辟
	StackOnly so2 = StackOnly::CreatObj(1,1); //在栈上开辟

    //error
	StackOnly* so3 = new StackOnly(so1);//在堆上开辟
	//调用不了构造函数,可以调用拷贝构造函数啊

	return 0;
}

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

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

class Person
{
public:
	
private:
	Person()//构造函数私有化
	{}
	int _sex;//性别
};

class Student : public Person
{
public:
	Student()// error:无法访问 private 成员(在“Person”类中声明)
	{}
private:
	string _name;
	int _num;//学号
};

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

class Person final
{
public:
	
private:
	Person()//构造函数私有化
	{}
	int _sex;//性别
};

//error:"Student": 无法从 "Person" 继承,因为它已声明为 "final"
class Student : public Person 
{
public:
	Student()
	{}
private:
	string _name;
	int _num;//学号
};

5. 请设计一个类,只能创建一个对象(单例模式)

设计模式:

设计模式(Design Pattern)是一套被反复使用、多数人知晓的、经过分类的、代码设计经验的

总结:为什么会产生设计模式这样的东西呢?就像人类历史发展会产生兵法。最开始部落之间打 仗时都是人拼人的对砍。后来春秋战国时期,七国之间经常打仗,就发现打仗也是有套路的,后 来孙子就总结出了《孙子兵法》。孙子兵法也是类似。

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

单例模式:

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

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

我们首先要做的就是将构造函数私有,在类外不让你能创建对象。

那我们肯定就要在类里面完成,我们可以按照之前的方式,来一个CreatObj,我这里取名字为GetInstance,但是按照之前那样写我们依然是可以创建多个对象,怎么办呢?我们这里可以使用一个全局变量的对象,GetInstance的时候就直接返回这个全局对象,这样就能保证只有一个对象,但是这个全局变量我们定义在类外又不行了,构造函数私有化了呀,我们不能初始化这个全局变量呀,我们可以把这个全局变量定义在类里面,加上static修饰,静态成员变量不存在对象中,在静态区,而相当于全局变量,只不过此时受类域限制,我们来手撕一下。

// 饿汉
class Singleton
{
public:
	// 饿汉:一开始(main之前)就创建出对象
	static Singleton* GetInstance()
	{
		return &_sint;
	}

	// 获取数据
	void Print()
	{
		cout << _x << endl;
		cout << _y << endl;

		for (auto& e : _vstr)
		{
			cout << e << " ";
		}
		cout << endl;
	}

	void AddStr(const string& s)
	{
		_vstr.push_back(s);
	}

	Singleton(Singleton const&) = delete;
	Singleton& operator=(Singleton const&) = delete;
private:
	Singleton(int x = 0, int y = 0, const vector<string>& vstr = { "yyyyy","xxxx" })
		: _x(x)
		, _y(y)
		, _vstr(vstr)
	{}

	// 想让一些数据,当前程序只有一份,那就可以把这些数据放到这个类里面
	// 再把这个类设计成单例,这个数据就只有一份了
	int _x;
	int _y;
	vector<string> _vstr;

	// 静态成员对象,不存在对象中,存在静态区,相当于全局的,定义在类中,受类域限制
	// 此时就可以访问构造函数
	static Singleton _sint;
};

// 在程序入口之前就完成单例对象的初始化
Singleton Singleton::_sint(1, 1, { "武汉" ,"黄石" });

光这样说获取都是一个对象的证据还是不太足够,我们直接打印地址看看。

⭐问题:
    // 1、如果当前单例对象数据较多,构造初始化成本较高,那么会影响程序启动的速度。迟迟进不了main函数
    // 2、多个单例类有初始化启动依赖关系,饿汉无法控制。假设:A和B两个单例,假设要求A先初始化,B再初始化,饿汉无法保证,因为都是全局变量,而且还在不同的文件

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

懒汉模式

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

// 懒汉
class Singleton
{
public:
	static Singleton* GetInstance()
	{
		// 第一次调用时,创建单例对象
		// 此时调用构造函数
		// 线程安全问题,需要加锁
		if(_psint == nullptr)
			_psint = new Singleton;
		return _psint;
	}

	// 获取数据
	void Print()
	{
		cout << _x << endl;
		cout << _y << endl;

		for (auto& e : _vstr)
		{
			cout << e << " ";
		}
		cout << endl;
	}

	void AddStr(const string& s)
	{
		_vstr.push_back(s);
	}

	Singleton(Singleton const&) = delete;
	Singleton& operator=(Singleton const&) = delete;
private:
	Singleton(int x = 0, int y = 0, const vector<string>& vstr = { "yyyyy","xxxx" })
		: _x(x)
		, _y(y)
		, _vstr(vstr)
	{}

	
	int _x;
	int _y;
	vector<string> _vstr;

	
	static Singleton* _psint;
};

Singleton* Singleton::_psint = nullptr;

我们此时调用的是默认的构造函数,那我们能不能使用传参的构造函数呢?怎么使用呢?

但是此时我们的单例对象是new出来的,我们就需要对这个资源进行释放,此时依然要使用静态成员函数,为什么呢?

// 懒汉
class Singleton
{
public:
	static Singleton* GetInstance()
	{
		// 第一次调用时,创建单例对象
		// 此时调用构造函数
		// 线程安全问题,需要加锁
		if(_psint == nullptr)
			_psint = new Singleton;
		return _psint;
	}

	static void DelInstance()
	{
		if (_psint)
		{
			delete _psint;
			_psint = nullptr;
		}
	}

	// 获取数据
	void Print()
	{
		cout << _x << endl;
		cout << _y << endl;

		for (auto& e : _vstr)
		{
			cout << e << " ";
		}
		cout << endl;
	}

	void AddStr(const string& s)
	{
		_vstr.push_back(s);
	}

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

	~Singleton()
	{
		// 把数据写到文件
		cout << "~Singleton()" << endl;
	}
private:
	Singleton(int x = 0, int y = 0, const vector<string>& vstr = { "yyyyy","xxxx" })
		: _x(x)
		, _y(y)
		, _vstr(vstr)
	{}

	
	int _x;
	int _y;
	vector<string> _vstr;

	
	static Singleton* _psint;
};

Singleton* Singleton::_psint = nullptr;

运行一下:

如果我们没有显示调用呢?或者忘记咋办?再借助一个内部类来帮我们释放,为啥要内部类,内部类只能定义类的人来操作,如果我们放在外面,就能定义多个gc类对象,虽然按照逻辑不会错,但是没必要定义这么多,但是此时和上面有一个区别,上面是显示调用析构函数,而下面是程序结束的时候借助gc对象调用析构函数。

// 懒汉
class Singleton
{
public:
	static Singleton* GetInstance()
	{
		// 第一次调用时,创建单例对象
		// 此时调用构造函数
		// 线程安全问题,需要加锁
		if(_psint == nullptr)
			_psint = new Singleton;
		return _psint;
	}

	static void DelInstance()
	{
		if (_psint)
		{
			delete _psint;
			_psint = nullptr;
		}
	}

	// 获取数据
	void Print()
	{
		cout << _x << endl;
		cout << _y << endl;

		for (auto& e : _vstr)
		{
			cout << e << " ";
		}
		cout << endl;
	}

	void AddStr(const string& s)
	{
		_vstr.push_back(s);
	}

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

	~Singleton()
	{
		// 把数据写到文件
		cout << "~Singleton()" << endl;
	}
private:
	Singleton(int x = 0, int y = 0, const vector<string>& vstr = { "yyyyy","xxxx" })
		: _x(x)
		, _y(y)
		, _vstr(vstr)
	{}

	
	int _x;
	int _y;
	vector<string> _vstr;

	static Singleton* _psint;

	// 内部类 - 以防在外面被定义多个
	class GC // 单纯为了析构单例对象
	{
	public:
		~GC()
		{
			Singleton::DelInstance();
		}
	};
	static GC gc;
};

Singleton* Singleton::_psint = nullptr;
Singleton::GC Singleton::gc;//出了作用域就析构

那咱们学了智能指针,咱不得用起来呀!

class Singleton
{
public:
	static Singleton* GetInstance()
	{
		// 第一次调用时,创建单例对象
		// 此时调用构造函数
		// 线程安全问题,需要加锁
        // unique_lock<mutex> lock(mtx); 写在这里会频繁加锁解锁
        // 实际上我们只想在第一次加锁,后面条件不满足直接返回即可
        // 双检查来解决
        if (_psint == nullptr) // 提效
        {
            unique_lock<mutex> lock(mtx);
            if (_psint == nullptr) // 保证单例线程安全
			    _psint = new Singleton;
        }  
        return _psint;
		
	}

	static void DelInstance()
	{
		if (_psint)
		{
			delete _psint;
			_psint = nullptr;
		}
	}

	// 获取数据
	void Print()
	{
		cout << _x << endl;
		cout << _y << endl;

		for (auto& e : _vstr)
		{
			cout << e << " ";
		}
		cout << endl;
	}

	void AddStr(const string& s)
	{
		_vstr.push_back(s);
	}

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

	~Singleton()
	{
		// 把数据写到文件
		cout << "~Singleton()" << endl;
	}
private:
	Singleton(int x = 0, int y = 0, const vector<string>& vstr = { "yyyyy","xxxx" })
		: _x(x)
		, _y(y)
		, _vstr(vstr)
	{}


	int _x;
	int _y;
	vector<string> _vstr;


	static Singleton* _psint;
    static mutex mtx;

	static shared_ptr<Singleton> ptr;//需要在类里面,否则无法访问_psint

};

Singleton* Singleton::_psint = nullptr;
mutex Singleton::mtx;
shared_ptr<Singleton>  Singleton::ptr(Singleton::_psint, [](Singleton* ptr1) { ptr1->DelInstance(); });

int main()
{
	cout << Singleton::GetInstance() << endl;
	// 获取的都是同一个对象
	Singleton::GetInstance()->Print();
	Singleton::GetInstance()->Print();
	cout << Singleton::GetInstance() << endl;
	Singleton::GetInstance()->AddStr("黄石");
	Singleton::GetInstance()->Print();
	cout << Singleton::GetInstance() << endl;
}

运行一下,看看此时有没有问题。

太复杂啦!有没有更简单的方法。

class Singleton
{
public:
	static Singleton* GetInstance()
	{
		// 局部的静态对象,第一次调用函数时构造初始化
		// C++11及之后这样写才可以
		// C++11之前无法保证这里的构造初始化是线程安全
		// 此时我们没有通过new申请资源,也就不用进行释放资源的动作
		// 静态变量只会初始化一次,后面这个代码就不执行了
		static Singleton _sinst;

		return &_sinst;
	}

	// 获取数据
	void Print()
	{
		cout << _x << endl;
		cout << _y << endl;

		for (auto& e : _vstr)
		{
			cout << e << " ";
		}
		cout << endl;
	}

	void AddStr(const string& s)
	{
		_vstr.push_back(s);
	}

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

	~Singleton()
	{
		// 把数据写到文件
		cout << "~Singleton()" << endl;
	}
private:
	Singleton(int x = 0, int y = 0, const vector<string>& vstr = { "yyyyy","xxxx" })
		: _x(x)
		, _y(y)
		, _vstr(vstr)
	{}


	int _x;
	int _y;
	vector<string> _vstr;
};

这里我们要区分和饿汉的区别,饿汉是把static Singleton _sinst定义成了全局变量,main函数之前我们就创建好了对象,而懒汉是把static Singleton _sinst定义成局部的,但是它的生命周期同样是全局的,需要我们自己去显示调用才会创建这个对象,而且我们定义成静态只会初始化一次,也满足只有一个对象的情况,我们可以对比他们的区别,先来懒汉。

我们再来看看饿汉

懒汉不仅解决了在单例数据过多,导致迟迟不能加载到main函数,同时对于初始化顺序又需要的,我们可以通过显示调用构造函数来保持顺序要求。

  • 18
    点赞
  • 12
    收藏
    觉得还不错? 一键收藏
  • 5
    评论
评论 5
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值