【C++】特殊类设计

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

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

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

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

原因:

  1. 设置成私有:如果只声明没有设置成private,用户自己如果在类外定义了,就可以不能禁止拷贝了
  2. 只声明不定义:不定义是因为该函数根本不会调用,定义了其实也没有什么意义,不写反而还简单,而且如果定义了就不会防止成员函数内部拷贝了。

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

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

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

第一种方法是:将析构函数私有,这样保证了只能在堆上创建,而局部和静态变量无法创建,因为它们在销毁时会自动调用析构,私有后无法调用析构,但是当delete堆上创建的对象时也会报错,因为会先去调用它的析构,因此需要专门写一个释放函数,防止内存泄漏。

class HeapOnly {
   
public:
	HeapOnly() {
   }
	void del() {
   
		delete this;
	}
private:
	~HeapOnly() {
   }
	HeapOnly(const HeapOnly&);
	HeapOnly& operator=(const HeapOnly&);
};

int main() {
   
	//下面两种定义报错
	/*HeapOnly hp1;
	static 	HeapOnly hp2;*/
	HeapOnly* hp3 = new HeapOnly;
	hp3->del();
	return 0;
}

第二种方法是:将构造函数私有,然后公有出一个返回堆上对象指针的静态成员函数即可。

class HeapOnly {
   
public:
	static HeapOnly* createrObj() {
   
		return new HeapOnly;
	}
private:
	HeapOnly() {
   }
	HeapOnly(const HeapOnly&);
	HeapOnly& operator=(const HeapOnly&);
};

int main() {
   
	HeapOnly* hp3 = HeapOnly::createrObj();
	return 0;
}

若不声明为静态成员函数会出现歧义,需要对象才能调用该函数,而就是因为没有对象才需要调用该函数创建对象,经典先有鸡还是先有蛋的问题,因此必须声明为静态的,这样无需对象,通过类名可以直接调用。

注意:不管哪种方法都需要把两个拷贝函数私有或者delete,这样完全只能在堆上创建对象,避免了通过拷贝而在栈上创建对象。

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

做法与上面类似,也是将构造函数私有,然后公有出一个静态成员函数,该函数内部创建一个对象然后返回。

还有第一点,禁了构造但没有禁拷贝构造,因此new可以调用拷贝构造创建一个对象,而又不能直接将拷贝构造声明为删除函数,因为传值返回或者外部会用到它创建对象,那该怎么办呢?

由于new是由两部分组成,operator new + 构造(拷贝构造),其中operator new是一个全局函数,可以在类中对它重载一个该类专属的operator new,若重载了,new该类对象时就不会去调用全局,而是去调用类中的,既然在类中又可以将其声明为删除函数,这样new就无法调用operator new了,进而无法在堆上创建对象了。

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

int main() {
   
	StackOnly hp3 = StackOnly::createrObj();;
	return 0;
}

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

C++98方式:构造函数私有化,派生类中调不到基类的构造函数。则无法继承,因为语法规定,派生类中基类的成员必须要调用基类的构造函数进行初始化,而把构造函数私有了之后对于派生类而言是不可见的,这样就完成了基类无法被继承。

class NonInherit
{
   
public:
	static NonInherit GetInstance()
	{
   
		return NonInherit();
	}
private:
	NonInherit()
	{
   }
};

C++11方式:在类名的后面加上final关键字,表示该类无法被继承。

class A  final
{
   
	// ....
};

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

设计模式:

设计模式(Design Pattern)是一套被反复使用、多数人知晓的、经过分类的、代码设计经验的总结。为什么会产生设计模式这样的东西呢?就像人类历史发展会产生兵法。最开始部落之间打仗时都是人拼人的对砍。后来春秋战国时期,七国之间经常打仗,就发现打仗也是有套路的,后来孙子就总结出了《孙子兵法》。孙子兵法也是类似。

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

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

单例模式分为两种:

  1. 饿汉模式
  2. 懒汉模式

5.1 饿汉模式

该模式的特点是,不管你后续用不用,在一开始(main函数之前)就创建一个唯一的全局实例对象

模拟实现:

class Singleton {
   
public:
	//2. 提供一个获取单例对象的接口 
	static Singleton& GetInstance()
	{
   
		return m_instance;
	}

	void add(const pair<string, string>& kv) {
   
		_dict[kv.first] = kv.second;
	}

	void print() {
   
		for (auto& kv : _dict) {
   
			cout << kv.first << ':' << kv.second << endl;
		}
	}
private:
	//1. 构造函数私有
	Singleton() {
   };

	//3. 将拷贝函数删除,防拷贝
	Singleton(Singleton const&) = delete;
	Singleton& operator=(Singleton const&) = delete;

	map<string, string> _dict;
	//静态成员变量类中声明
	//类外定义
	static Singleton m_instance;
	//类中可以定义自己类型的对象吗?
	//可以,但必须是静态的
	//因为静态成员变量只会初始化一次存储在静态区
	//不存储在任何一个对象中,
	//且整个类的所有对象共享这一个成员
	//若不是静态的就会出现循环问题:
	//m_instance对象中有一个m_instance成员
	//m_instance成员本身是个对象,该对象中有一个m_instance成员
	//...
};
Singleton Singleton::m_instance;
//可以用全局变量吗?
//不行,因为构造函数私有,外部无法调用。
int main() {
   
	Singleton::GetInstance().add({
    "xxx", "111" });
	Singleton::GetInstance().add({
    "yyy", "222" });
	Singleton::GetInstance().add({
    "zzz", "333" });
	Singleton::GetInstance().add({
    "ddd", "444" });

	Singleton::GetInstance().print();
	return 0;
}

缺点:

  1. 若该对象的初始化内容较多,可能会导致进程启动慢。
  2. 由于多个单例类对象实例时,创建顺序会不确定,若单例对象的创建有依赖关系时,比如:要求A先创建,B的创建依赖与A,那么这种情况顺序不确定可能会导致程序出现问题。

针对上述的两个缺点,可以使用懒汉模式来解决。

5.2 懒汉模式

它的特点是,在第一次使用的时候才初始化,若有多个实例且存在依赖,可以自由调整实例化顺序。

大体结构与饿汉的实现是相似的,模拟实现:

class Singleton {
   
public:
	//2. 提供一个获取单例对象的接口 
	static Singleton& GetInstance()
	{
   
		//第一次调用才实例出单例对象
		if (m_instance == nullptr) {
   
			m_instance = new Singleton;
		}
		return *m_instance;
	}

	//与饿汉不同,它不存在释放问题
	//而懒汉对象是new出来的,就需要考虑释放的问题
	//但一般而言单例对象是不需要释放的
	//因为进程结束,资源会自动回收
	//但可能一些特殊情况,比如:
	//1. 中途不会在使用了想要显式地释放
	//2. 进程结束需要对象中的数据需要持久化保存(或者其它操作)
	//因此是有必要写个释放逻辑的
	static void DelInstance() {
   
		//不为空就释放
		if (m_instance) {
   
			delete m_instance;
			m_instance = nullptr;
		}
	}

	void add(const pair<string, string>& kv) {
   
		_dict[kv.first] = kv.second;
	}

private:
	//1. 构造函数私有
	Singleton() {
   }
	
	~Singleton() {
   
		//持久化
		FILE* fin = fopen("map.txt", "w");
		for (auto& kv : _dict) {
   
			fputs(kv.first.c_str(), fin);
			fputs(": ", fin);
			fputs(kv.first.c_str(), fin);
			fputs("\n", fin);
		}
		//...或者其它操作
	}

	//3. 将拷贝函数删除,防拷贝
	Singleton(Singleton const&) = delete;
	Singleton& operator=(Singleton const&) = delete;

	map<string, string> _dict;
	//这里声明一个对象的指针
	static Singleton* m_instance;
};

//在main函数之前仅仅只初始化一个指针
//相较于饿汉模式可以减少程序的启动时间
Singleton* Singleton::m_instance = nullptr;

int main() {
   
	Singleton::GetInstance().add({
    "xxx", "111" });
	Singleton::GetInstance().add({
    "yyy", "222" });
	Singleton::GetInstance().add({
    "zzz", "333" });
	Singleton::GetInstance().add({
    "ddd", "444" });
	
	Singleton::GetInstance().print();
	//显式释放
	Singleton::DelInstance();
	return 0;
}

还有一个问题:若进程结束的地方有很多要在每个地方都显式地写释放吗?

其实不必,有两种做法:

class GC {
   
public:
	~GC() {
   
		Singleton::DelInstance();
	}
};
GC gc;

定义GC类和GC类的全局对象gc,当进程结束时,gc的生命周期也结束了,然后它会调用它的析构函数,函数内部刚好完成了对单例对象的释放工作。

若有多个单例对象需要释放时,也可以统一写在里面。
2.

class Singleton {
   
public:
	//...
private:
	//...
	class GC {
   
	//内部类
	public:
		~GC() {
   
			Singleton::DelInstance();
		}
	};
	//在内部定义
	static GC _gc;
}
Singleton::GC Singleton::_gc;

在单例类内部声明GC类,然后定义一个静态GC类对象,这样当进程结束后会调用_gc的析构函数,效果与第一种方法是一样的。

相关推荐

  1. C++特殊设计

    2024-01-22 06:54:03       35 阅读
  2. C++】特殊设计

    2024-01-22 06:54:03       31 阅读
  3. C++特殊设计

    2024-01-22 06:54:03       29 阅读
  4. C++特殊设计

    2024-01-22 06:54:03       29 阅读
  5. c++特殊设计

    2024-01-22 06:54:03       19 阅读

最近更新

  1. TCP协议是安全的吗?

    2024-01-22 06:54:03       18 阅读
  2. 阿里云服务器执行yum,一直下载docker-ce-stable失败

    2024-01-22 06:54:03       19 阅读
  3. 【Python教程】压缩PDF文件大小

    2024-01-22 06:54:03       18 阅读
  4. 通过文章id递归查询所有评论(xml)

    2024-01-22 06:54:03       20 阅读

热门阅读

  1. 8-Docker网络命令之ls

    2024-01-22 06:54:03       29 阅读
  2. P8738 [蓝桥杯 2020 国 C] 天干地支

    2024-01-22 06:54:03       32 阅读
  3. 实时流媒体传输开源库——Live555

    2024-01-22 06:54:03       28 阅读
  4. SpringBoot整理-Spring Boot与Spring MVC的区别

    2024-01-22 06:54:03       36 阅读
  5. Apache Hive(二)

    2024-01-22 06:54:03       29 阅读
  6. for...in、for...of、for...Each的详细区别!

    2024-01-22 06:54:03       33 阅读
  7. 设计模式-命令模式

    2024-01-22 06:54:03       38 阅读