C++特殊类设计

目录

不能被拷贝的类

只能在堆上创建对象的类

构造函数私有化:

析构函数私有化:

只能在栈上创建对象的类

不能被继承的类

只能创建一个对象的类(单例模式)

设计模式:

单例模式:

饿汉模式:

懒汉模式:

线程不安全的懒汉模式:

线程安全的懒汉模式:

静态对象:

不能被拷贝的类

要想设计一个不能被拷贝的类,首先需要知道哪些场景中会发生类的拷贝。拷贝只会发生在两个场景拷贝构造时赋值时,因此想要让一个类不能被拷贝,只需让该类不能调用拷贝构造函数以及赋值运算符重载即可。

C++98 的标准中,需要把拷贝构造函数和赋值重载函数只声明不定义且设置为私有,此时该类就无法调用拷贝构造函数和赋值重载函数了。

cpp 复制代码
class NoCopy
{
public:
	NoCopy()
	{}
	~NoCopy()
	{}
private:
	//拷贝构造和赋值重载只声明不定义,且设置为私有的
	NoCopy(const NoCopy& nc);
	NoCopy& operator=(const NoCopy& nc);
};

int main()
{
	NoCopy n1;
	NoCopy n2(n1);//调用拷贝构造
	NoCopy n3;
	n3 = n1;//调研赋值重载

	return 0;
}

上述代码中,定义了一个NoCopy类,该类的拷贝构造和赋值重载只声明不定义,且设置为私有的。然后再main函数中尝试调用NoCopy类的拷贝构造和赋值重载。尝试编译运行一下:

可见把类的拷贝构造和赋值重载只声明不定义,且设置为私有的这种做法确实可以让一个类无法被拷贝。

注意:只声明不定义设置为私有两个条件缺一不可

  • 设置为私有:如果没有设置成private,那么用户就可以在类外定义了,就不能禁止拷贝了。
  • 只声明不定义:不定义是因为该函数根本不会调用,定义了也没有什么意义,不写反而省事了,而且如果定义了,就可能会在成员函数内部进行拷贝了。

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

此时再想设计一个不能被拷贝的类,只需要把该类的拷贝构造函数和赋值重载函数使用delete强删除即可,且即使设置为公有的也不影响

cpp 复制代码
class NoCopy
{
public:
	NoCopy()
	{}
	~NoCopy()
	{}
	
	//使用delete强制删除拷贝构造函数和赋值重载函数
	NoCopy(const NoCopy& nc) = delete;
	NoCopy& operator=(const NoCopy& nc) = delete;
	
};

int main()
{
	NoCopy n1;
	NoCopy n2(n1);//调用拷贝构造
	NoCopy n3;
	n3 = n1;//调研赋值重载

	return 0;
}

上边的代码中,NoCopy类的拷贝构造函数和赋值重载函数被使用delete强制删除了,且是在public区域中,在main函数中尝试调用拷贝构造函数和赋值重载函数。尝试编译运行:

可见把类的拷贝构造函数和赋值重载函数使用delete强制删除,是可以让一个类无法被拷贝的。

只能在堆上创建对象的类

构造函数私有化:

创建一个类对象,只能创建在两个位置,要么是创建在栈上,要么是创建在堆上。而要创建一个类对象,必会调用构造函数或拷贝构造函数。而想要在堆上创建对象,就必须通过new/malloc等实现。因此要想实现只能在堆上创建对象,首先要将构造函数私有化,同时拷贝构造和赋值重载需要强制删除(或者只声明不定义且设置为私有 ),提供一个static成员函数,在该函数中完成对象的创建并返回对象指针

cpp 复制代码
class HeapOnly
{
public:
	static HeapOnly* CreateObject()
	{
		return new HeapOnly;
	}
    //通过类成员函数返回对象
	/*HeapOnly test()
	{
		return *this;
	}*/
private:
	//构造函数
	HeapOnly() {}

	//拷贝构造和赋值重载
	//C++98:只声明不定义,且设置为私有
	//HeapOnly(const HeapOnly&);
	//HeapOnly& operator=(const HeapOnly& h);
	//C++11:使用delete强制删除
	HeapOnly(const HeapOnly&) = delete;
	HeapOnly& operator=(const HeapOnly& h) = delete;

};

int main()
{
	HeapOnly* hp1 = HeapOnly::CreateObject();
	//如果不把拷贝构造函数和赋值重载函数删除,就可以通过下边的方法在栈上创建对象
	/*HeapOnly hp2(*hp1);
	HeapOnly* hp3 = HeapOnly::CreateObject();
	HeapOnly hp4(*hp3);
	hp4 = *hp1;
	HeapOnly hp5 = hp3->test();*/
	delete hp1;
	return 0;
}

上边的代码实现了一个只能在堆上创建对象的类HeapOnly,到那时还存在一个问题,CreateObject()方法返回的是一个原生指针 ,HeapOnly类虽然禁止了类对象的拷贝和赋值,但是原生指针之间还是可以互相赋值的,这就会导致多个指针指向同一份资源,释放资源时就会出问题。

cpp 复制代码
int main()
{
	HeapOnly* hp1 = HeapOnly::CreateObject();
	HeapOnly* hp2 = hp1;
	delete hp1;
	delete hp2;
	return 0;
}

上边的代码先通过CreateObject()方法创建了一个对象并把对象指针返回给后hp1,然后又把hp1赋值给hp2,编译运行代码:

可以发现程序运行崩溃了,就是以为重复释放了同一份资源导致的。

要解决这个问题就需要用到以前说过的智能指针了,通过智能指针可以更加安全有效的管理资源。

cpp 复制代码
class HeapOnly
{
public:
	static shared_ptr<HeapOnly> CreateObject()
	{
		return shared_ptr<HeapOnly>(new HeapOnly);
	}
	/*static HeapOnly* CreateObject()
	{
		return new HeapOnly;
	}*/
private:
	//构造函数
	HeapOnly() {}

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

};

int main()
{
	shared_ptr<HeapOnly> hp1 = HeapOnly::CreateObject();
	shared_ptr<HeapOnly> hp2 = hp1;
	return 0;
}

上述代码通过修改CreateObject()函数的返回值,实现了对资源的安全管理,可以正常编译运行。

shared_ptr可以让多个对象共享同一块资源,而unique_ptr是独占资源,可以根据不同的场景灵活使用。

需要注意的是CreateObject()函数在返回对象时不能使用make_shared<HeapOnly>()

make_shared<HeapOnly>()

  • make_shared 需要直接调用 HeapOnly的构造函数。

  • 但 make_shared的模板实例化代码位于标准库中(<memory> 头文件),不在 HeapOnly类的成员函数作用域内

  • 因此,make_shared无法访问 HeapOnly的私有构造函数,导致编译错误。

  • 要想使用make_shared,需要在HeapOnly类中声明make_shared为友元:

    cpp 复制代码
    class HeapOnly {
    public:
        template <typename T, typename... Args>
        friend shared_ptr<T> std::make_shared(Args&&... args);
        // 其他代码...
    };

shared_ptr(new HeapOnly)

  • new HeapOnly的调用发生在 CreateObject()成员函数内部。
  • 成员函数可以访问类的私有成员(包括私有构造函数)。
  • 因此,new HeapOnly合法,shared_ptr可以安全接管指针。

析构函数私有化:

上边通过构造函数私有化实现了一个只能在堆上创建的类,此外还可以通过将析构函数私有化实现一个只能在堆上创建的类。

cpp 复制代码
class HeapOnly
{
public:
	//构造函数
	HeapOnly() {}

	void del()
	{
		delete this;
	}
private:
	//析构函数
	~HeapOnly()
	{}

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

};

int main()
{
	HeapOnly* hp1 = new HeapOnly;
	hp1->del();
	HeapOnly hp2;
	
	return 0;
}

上述代码中定义了一个HeapOnly类,该类将析构函数进行了私有化处理,在main函数中通过new在堆上创建一个对象,又直接在栈上定义了一个对象,编译运行:

编译直接报错,这是因为编译器在编译阶段会检查对象的 完整生命周期包括析构函数的可访问性,即使构造函数是公有的,如果析构函数不可访问,编译器会直接拒绝栈对象的定义。

new操作符仅依赖构造函数的可访问性与析构函数无关。堆对象的销毁必须通过显式调用del()函数,不能直接使用delete操作符,因为delete操作符会尝试调用析构函数,而析构函数是私有的,无法在外部直接调用;而del()是类的成员函数,可以直接访问私有的析构函数。

只能在栈上创建对象的类

有了只能在堆上创建对象的经验,要设计一个只能在栈上创建对象的类就很简单了。要想在堆上创建对象,必须要调用new操作符,当使用new动态分配对象时,编译器会先调用operator new分配内存,再调用构造函数 。因此只需要把operator new强制删除,就可以禁止在堆上创建对象了。

cpp 复制代码
class StackOnly
{
public:
	static StackOnly CreateObj()
	{
		return StackOnly();
	}
	StackOnly(){}
	
	// 禁用 new 操作符
	void* operator new(size_t) = delete;
	void operator delete(void*) = delete;
	// 禁用 new[] 和 delete[]
	void* operator new[](size_t) = delete;
	void operator delete[](void*) = delete;
private:
	
};

int main()
{
	StackOnly so1=StackOnly::CreateObj();
	StackOnly so2;
	//StackOnly* so3 = new StackOnly;//报错

	return 0;
}

不能被继承的类

在C++的继承体系中,子类的构造函数 必须调用父类的构造函数初始化父类的那一部分成员,如果父类没有默认的构造函 数,则必须在子类构造函数的初始化列表阶段显示调用 。因此如果把父类的构造函数设置为私有的,那么在子类构造函数中将无法调用父类的构造函数,这就实现了父类不可被继承。

cpp 复制代码
class NonInherit
{
public:
	static NonInherit GetInstance()
	{
		return NonInherit();
	}
private:
	NonInherit(){}
};

class A:public NonInherit
{
public:
	A(){}

};

int main()
{
	A a;
	return 0;
}

上述代码中定义了一个NoInherit类,NoInherit类的构造函数设置为了私有的,有一个A类继承自NoInherit类,然后再main函数中定义A类对象,编译运行:

可以发现A类将无法定义对象,也就实现了NoInherit类不可被继承,因为只要继承了NoInherit类的子类都无法定义对象。

除了上述方法,C++11引入了final关键字来修饰类,表示该类不可被继承。

cpp 复制代码
class NonInherit final
{
public:
	NonInherit(){}
};

只能创建一个对象的类(单例模式)

设计模式:

设计模式是解决软件设计中常见问题的可复用方案,它们可以分为三大类:​创建型结构型行为型。

创建型模式:控制对象的创建过程,解耦对象的创建与使用,提高灵活性和可维护性。主要包括:单例模式、工厂模式等。

结构型模式 组织类和对象的结构,通过组合或继承实现更灵活的设计。主要包括:适配器模式、装饰器模式等。

行为型模式:​管理对象间的交互和职责分配,优化通信流程。主要包括:观察者模式、命令模式等。

实用设计模式的目的:提高代码可重用性让代码更容易被他人理解保证代码可靠性。使代码编写真正工程化。

单例模式:

一个类只能创建一个对象,这种模式叫做单例模式。单例模式确保一个类只有一个实例并提供全局访问点该实例被所有程序模块共享,需要通过静态对象实现。主要应用场景:数据库连接池、全局配置管理器。

单例模式有两种实现模式:饿汉模式懒汉模式

饿汉模式:

饿汉模式的设计思想是不管是否使用在程序启动时(main函数之前)就直接创建一个唯一的静态实例对象

优点:

  • 简单,没有线程安全的问题。

缺点:

  • 如果单例对象数据较多,构造初始化成本较高,那么会影响程序启动的速度。迟迟进不了main函数。
  • 如果多个单例类有初始化启动依赖关系,饿汉模式无法控制。假设有A和B两个单例,B类依赖于A类,要求A先初始化,B再初始化,此时饿汉模式将无法保证初始化顺序。
cpp 复制代码
//饿汉模式
namespace Hunger
{
	//单例类
	class Singleton
	{
	public:
		static Singleton* GetInstance()
		{
			return &_sing;
		}

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

	private:
		//将拷贝构造和赋值重载强制删除,防止通过它们破坏单例的唯一性
		Singleton(Singleton const&) = delete;
		Singleton& operator=(Singleton const&) = delete;

		//构造函数私有化,防止随意创建对象
		Singleton(int x, int y, const vector<string>& vstr)
			:_x(x)
			, _y(y)
			, _vstr(vstr)
		{}

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

		// 静态成员对象,不存在对象中,存在静态区,只有一份,相当于全局的,定义在类中,受类域限制
		static Singleton _sing;
	};
	//类外定义静态成员
	//只能在此处进行初始化设置数据
	Singleton Singleton::_sing(1, 2, { "hello","haha" });
}

int main()
{
	Hunger::Singleton* hs=Hunger::Singleton::GetInstance();
	hs->Print();
	hs->AddStr("nihao");
	hs->Print();
	return 0;
}

上述代码中实现了一个饿汉模式的单例类Hunger::Singleton,并在main函数中进行了相关调用,编译运行:

饿汉模式由于提前加载资源,适用于实例小且频繁使用的情况,可避免资源竞争,提高响应速度。

懒汉模式:

懒汉模式 的设计思想是在第一次使用实例对象时,创建对象

优点:

  • 第一次使用实例对象时,创建对象。程序启动无负载。多个单例实例启动顺序自由控制

缺点:

  • 复杂,存在线程安全的问题。
线程不安全的懒汉模式:
cpp 复制代码
namespace Lazy
{
	class Singleton
	{
	public:
		//获取单例对象
		static Singleton* GetInstance()
		{
			//静态成员变量只会创建一次
			if (_psing == nullptr)//在第一次调用时创建
			{
				_psing = new Singleton;
			}
			
			return _psing;
		}

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

		//删除单例对象
		static void DelInstance()
		{
			std::cout << "static void DelInstance()" << std::endl;
			if (_psing)
			{
				delete _psing;
				_psing = nullptr;
			}
		}
		~Singleton() {}
	private:
		Singleton(Singleton const&) = delete;
		Singleton& operator=(Singleton const&) = delete;

		//构造函数私有化,防止随意创建对象
		Singleton(int x = 0, int y = 0, const vector<string>& vstr = { "xxxxxxx" })
			:_x(x)
			, _y(y)
			, _vstr(vstr)
		{}

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

		// 静态成员对象,不存在对象中,存在静态区,相当于全局的,定义在类中,受类域限制
		static Singleton* _psing;

		//内部类,帮助删除单例对象
		class GC
		{
		public:
			~GC()
			{
				Singleton::DelInstance();
			}
		};
		static GC _gc;
	};
	//静态成员类内声明,类外定义
	Singleton* Singleton::_psing = nullptr;
	Singleton::GC Singleton::_gc;
}

int main()
{
	Lazy::Singleton* ls = Lazy::Singleton::GetInstance();
	ls->Print();
	ls->AddStr("zaijian");
	ls->Print();
	//ls->DelInstance();手动调用
	return 0;
}

上述代码中实现了一个基础的懒汉模式的单例类Lazy::Singleton,由于静态成员变量是一个指针,无法转动销毁对象资源,需要提供一个静态成员方法DelInstance(),用来释放对象资源,为防止忘记手动调用DelInstance()方法,设计一个内部类GC,专门用于调用DelInstance()方法释放对象资源。编译运行:

可以发现,DelInstance()方法自动调用了。

这种基础的懒汉模式的单例类,存在一个很大的问题,它是线程不安全 的,多线程下可能创建多个实例(竞态条件)

单例模式的核心是确保一个类只有一个实例,并在全局提供访问点。在单线程环境下,因为GetInstance()会检查_psing是否为nullptr,如果是,则创建新实例,如果不是,则直接返回。但在多线程环境下,可能会有多个线程同时进入GetInstance()导致多个实例被创建,从而破坏单例的唯一性

具体来说,线程不安全的原因在于没有同步机制。当两个或多个线程同时执行if (_psing==nullptr)时,它们都可能发现_psing为nullptr,从而各自执行_psing = new Singleton,导致多次实例化。

cpp 复制代码
void test()
{
	Lazy::Singleton* ls = Lazy::Singleton::GetInstance();
	std::cout << ls << std::endl;
	
}
int main()
{
	/*Lazy::Singleton* ls = Lazy::Singleton::GetInstance();
	ls->Print();
	ls->AddStr("zaijian");
	ls->Print();*/
	//ls->DelInstance();手动调用
	vector<thread> threads;

	// 启动多个线程
	for (int i = 0; i < 10; ++i) 
	{
		threads.emplace_back(test);
	}

	// 等待所有线程结束
	for (auto& t : threads) 
	{
		t.join();
	}

	return 0;
}

上边的代码中,test函数每次都会获取实例,并打印地址,main函数中,创建了10个线程都去执行test函数,观察现象:

可以发现,这是个线程打印的地址多数都是不同的,说明进行了多次实例化。

线程不安全的问题并不容易复现,因为线程的调度是操作系统控制的,可能在某些情况下测试通过,但在其他情况下失败。为了增加竞态发生的概率 ,可以在if条件判断后和实际创建实例前插入一些延迟 (this_thread::sleep_for(chrono::milliseconds(100))),模拟线程切换 的情况,以增加复现概率

线程安全的懒汉模式:

为了保证线程安全,可以通过加锁的方法来实现。

cpp 复制代码
namespace Lazy
{
	class Singleton
	{
	public:
		//获取单例对象
		static Singleton* GetInstance()
		{
			//静态成员变量只会创建一次
			//双检查加锁,保证线程安全
			if (_psing == nullptr)
			{
				unique_lock<mutex> lock(_mutex);//加锁
				if(_psing ==nullptr)//在第一次调用时创建
				{ 
					this_thread::sleep_for(chrono::milliseconds(100));//手动延时
					_psing = new Singleton;
				}
			}
			return _psing;
		}

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

		//删除单例对象
		static void DelInstance()
		{
			std::cout << "static void DelInstance()" << std::endl;
			if (_psing)
			{
				delete _psing;
				_psing = nullptr;
			}
		}
		~Singleton(){}
	private:
		Singleton(Singleton const&) = delete;
		Singleton& operator=(Singleton const&) = delete;

		//构造函数私有化,防止随意创建对象
		Singleton(int x = 0, int y = 0, const vector<string>& vstr = {"xxxxxxx"})
			:_x(x)
			, _y(y)
			, _vstr(vstr)
		{}

		

		int _x;
		int _y;
		vector<string> _vstr;
		static mutex _mutex;//锁

		// 静态成员对象,不存在对象中,存在静态区,相当于全局的,定义在类中,受类域限制
		static Singleton* _psing;
		
		//内部类,帮助调用单例类的析构
		class GC
		{
		public:
			~GC()
			{
				Singleton::DelInstance();
			}
		};
		static GC _gc;
	};

	//静态成员类内声明,类外定义
	Singleton* Singleton::_psing = nullptr;
	mutex Singleton::_mutex;
	Singleton::GC Singleton::_gc;
}

void test()
{
	Lazy::Singleton* ls = Lazy::Singleton::GetInstance();
	std::cout << ls << std::endl;
	
}
int main()
{
	vector<thread> threads;

	// 启动多个线程
	for (int i = 0; i < 10; ++i) 
	{
		threads.emplace_back(test);
	}

	// 等待所有线程结束
	for (auto& t : threads) 
	{
		t.join();
	}

	return 0;
}

上边的代码通过加锁,保证了线程安全。

前边所说的懒汉模式的代码,都必须通过一个静态方法来释放对象资源,为防止忘记手动调用,还需要一个内部类GC来实现自动释放资源,因为单例对象是一个静态指针,无法自动释放资源,那可不可以直接使用静态对象来实现单例模式,这样就不用再单独考虑资源释放的问题了。

答案是可以的,但是需要在C++11之后才可以,因为C++11之前静态对象的创建无法保证线程安全,C++11之后保证了静态对象创建是线程安全的

静态对象:
cpp 复制代码
namespace Lazy
{
    class Singleton
	{
	public:
		//获取单例对象
		static Singleton* GetInstance()
		{
			// 局部的静态对象,第一次调用函数时构造初始化
			// C++11及之后这样写才可以
			// C++11之前无法保证这里的构造初始化是线程安全的
            this_thread::sleep_for(chrono::milliseconds(100));
			static Singleton sint;
            this_thread::sleep_for(chrono::milliseconds(100));
			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);
		}

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

		//构造函数私有化,防止随意创建对象
		Singleton(int x = 0, int y = 0, const vector<string>& vstr = { "xxxxxxx" })
			:_x(x)
			, _y(y)
			, _vstr(vstr)
		{}

		~Singleton()
		{
			cout << "~Singleton()" << endl;
		}

		int _x;
		int _y;
		vector<string> _vstr;
	};
}
void test()
{
	Lazy::Singleton* ls = Lazy::Singleton::GetInstance();
	std::cout << ls << std::endl;
	
}
int main()
{
	vector<thread> threads;

	// 启动多个线程
	for (int i = 0; i < 10; ++i) 
	{
		threads.emplace_back(test);
	}

	// 等待所有线程结束
	for (auto& t : threads) 
	{
		t.join();
	}

	return 0;
}

上边的代码通过静态对象实现了一个线程安全的懒汉模式单例类。

相关推荐
夏天的味道٥2 小时前
使用 Java 执行 SQL 语句和存储过程
java·开发语言·sql
IT、木易3 小时前
大白话JavaScript实现一个函数,将字符串中的每个单词首字母大写。
开发语言·前端·javascript·ecmascript
Mr.NickJJ4 小时前
JavaScript系列06-深入理解 JavaScript 事件系统:从原生事件到 React 合成事件
开发语言·javascript·react.js
Dream it possible!4 小时前
LeetCode 热题 100_字符串解码(71_394_中等_C++)(栈)
c++·算法·leetcode
Archer1945 小时前
C语言——链表
c语言·开发语言·链表
My Li.5 小时前
c++的介绍
开发语言·c++
功德+n5 小时前
Maven 使用指南:基础 + 进阶 + 高级用法
java·开发语言·maven
达斯维达的大眼睛5 小时前
qt小项目,简单的音乐播放器
开发语言·qt
面会菜.5 小时前
C语言(队列)
c语言·开发语言
香精煎鱼香翅捞饭6 小时前
java通用自研接口限流组件
java·开发语言