C++:特殊类的设计(无线程)

目录

一、设计一个不能拷贝类

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

方法一:析构函数私有化

方法二:构造函数私有化

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

四、设计一个类不能被继承

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

1.设计模式

2.单例模式

饿汉模式

懒汉模式

一、设计一个不能拷贝类

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

  • C++98

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

cpp 复制代码
class CopyBan
{
    // ...
private:
    CopyBan(const CopyBan&);
    CopyBan& operator=(const CopyBan&);
    //...
};

原因:

  1. 设置成私有:如果只声明没有设置成private,用户自己如果在类外定义了,还是能拷贝。

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

  • C++11

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

cpp 复制代码
class CopyBan
{
    // ...
    CopyBan(const CopyBan&)=delete;
    CopyBan& operator=(const CopyBan&)=delete;
    //...
};

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

方法一:析构函数私有化

那么我们怎么创建对象呢?其实这里不能直接创建对象,只能用指针间接创建一个对象,如下所示。

可以直接用delete吗?

不行,因为delete释放对象的资源会调用对象的析构函数,而析构函数不可访问。这该怎么办呢?

既然析构函数被设置为私有,只能够在类内调用,那我们设置一个public函数调用析构函数可以吗?如下所示:

cpp 复制代码
class HeapOnly
{
public:
	void Destroy()
	{
		this->~HeapOnly();
	}
private:
	~HeapOnly()
	{
		cout << "~HeapOnly" << endl;
	}
};

//方法一:析构函数私有化
//因为没有析构函数,就只能用new申请资源赋值给类类型的指针。
int main()
{
	//HeapOnly hp1;
	HeapOnly* hp2 = new HeapOnly;
	//delete hp2;
	hp2->Destroy();
	return 0;
}

运行一下:

调用了析构函数没有问题。

我们还可以用delete调用析构函数,

但这种写法有些繁琐,我们可以将Destroy设置成静态的类成员函数。

这样调用就简单一点。

注意:类的静态成员函数不能使用非静态成员变量,因为使用非静态成员变量要this,而静态成员函数中没有this。上面的静态成员函数调用的变量是ptr,ptr不是成员变量,更不是非静态成员变量,不需要this调用。delete ptr,是delete调用ptr指向空间的析构函数。

方法二:构造函数私有化

因为构造函数私有了,类外部不能直接访问,所以不能直接实例化,也不能通过new赋值给类类型指针了。

同析构函数一样,我们也可以用间接调用构造函数的方式,创造对象指针,如下

但是这里报错 :CreateObj显示未定义,这是为什么呢?因为CreateObj是成员函数,调用成员函数要先创建对象或对象的指针,而我们又要用CreateObj创建对象。

怎么解决这个问题呢?我们可以使用静态成员函数,

这样就行解决了。注意:这里的new调用构造函数,构造函数不需要this调用。

但这种写法有个致命缺陷,就是可以通过拷贝构造创建栈上的变量。如下所示:

由于是浅拷贝,所以hp4和hp3指向同一块空间。我添加一个私有成员变量check_i来核验一下,

所以我们要把拷贝构造禁掉,

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

因为只能在栈上创建对象,所以要限制对象的创建方式,我们先将构造函数设置为私有,这样就只能通过类内部的某个函数调用构造函数,在这个函数中,我们通过在栈上创建变量,然后返回该变量, 同时要将这个函数设置为静态的,因为非静态成员要用隐式的this调用。

cpp 复制代码
//设计一个只能在栈中创建对象的类
class StackOnly
{
public:
	static StackOnly CreateObj()
	{
		StackOnly obj;
		return obj;
	}
private:
	StackOnly()
	{
		cout << "StackOnly()" << endl;
	}
	int check_i = 666;
};
int main()
{
	StackOnly obj = StackOnly::CreateObj();
	return 0;
}

运行,

这样只限制了构造函数,但可以通过new拷贝构造的对象申请堆上的空间,如下

那可以把拷贝构造delete吗?

会报错,因为传值返回会调用拷贝构造。

我们知道new分为两个步骤,一个是调用operator new ,一个是调用构造函数。

我们可以从operator new出手。对于每一个类,如果我们不显示实现类专属的operator new,它就会调用全局的operator new。如果我们实现类专属的operator new,如下

cpp 复制代码
class StackOnly
{
public:
	static StackOnly CreateObj()
	{
		StackOnly obj;
		return obj;
	}
	//StackOnly(const StackOnly& obj) = delete;
	void* operator new(size_t size)
	{
		cout << "void* operator new(size_t size)" << endl;
		return malloc(size);
	}
private:
	StackOnly()
	{
		cout << "StackOnly()" << endl;
	}
	int check_i = 666;
};
int main()
{
	StackOnly obj = StackOnly::CreateObj();
	StackOnly* ptr = new StackOnly(obj);
	return 0;
}

编译器就会优先调用显示实现的operator new,我们执行一下,

确实如此。

所以我们可以把operator new禁掉,这样new就会报错。

四、设计一个类不能被继承

C++98:

C++11:

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

1.设计模式

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

使用设计模式的目的:增强代码的可重用性、可理解性、可靠性,促使代码编写工程化。

2.单例模式

一个类只能创建一个对象,即单例模式,该模式可以保证系统中该类只有一个实例,并提供一个访问它的全局访问点,该实例被所有程序模块共享。

比如在某个服务器程序中,该服务器的配置信息存放在一个文件中,这些配置数据由一个单例对象统一读取,然后服务进程中的其他对象再通过这个单例对象获取这些配置信息,这种方式简化了在复杂环境下的配置管理。

单例模式有两种实现模式:

饿汉模式

程序启动时就创建一个唯一的实例对象。 (main函数启动前)

但凡是对对象有限制的,比如只能在堆上、栈上创建,第一步,就是将构造函数私有化。

如果不私有化构造函数(因为要创建对象,所以不可能delete构造函数),那么就可以通过构造函数构造多个对象。如下,我们先将构造函数私有化:

cpp 复制代码
class A
{
public:
	
private:
	A()
	{}
	map<string, string> _dict;
	int _n = 0;
};
int main()
{

}

现在我们要创建一个在main()函数调用前就存在的对象,还只能创建一个。我们把问题分解为两个小问题"main函数前创造"和"只能创造一个",我们先思考前一个问题,什么变量在main()调用前就创造好了你?好像只能是全局变量。

我们试试创建A类型的全局变量_inst。

发现报错,我们没办法直接在类外面定义一个全局变量,那我们可以在类内声明一个变量再在类外定义吗?这不就是类的静态成员变量吗?

类的静态成员只能在类内声明类外定义,我们试试,

cpp 复制代码
class A
{
public:

private:
	A()
	{}
	map<string, string> _dict;
	int _n = 0;
	static A _inst;
};
A A::_inst;
int main()
{
	return 0;
}

我们运行一下程序,发现没有问题。

现在我们只要通过一个类的静态成员函数拿出_inst就行。(注意,一定要是静态的,不然访问非静态的成员函数需要对象或对象的指向,就需要构造对象)

cpp 复制代码
class A
{
public:
	static A* GetInstance()
	{
		return &_inst;
	}
private:
	A()
	{}
	map<string, string> _dict;
	int _n = 0;
	static A _inst;
};
A A::_inst;

可以选择返回指针,也可以返回引用。

因为我们私有了构造函数,同时只创建一个类的静态成员变量,所以不修改当前代码,该类通过构造函数只有这一个对象(后面还要禁拷贝构造)。(同样的,如果定义了两个静态成员变量,该类就可以有两个对象)

那么我们怎么给这个对象添加数据呢?

这本质就是访问其他成员变量(非静态的成员变量),由于A中的成员变量都是私有的,所以我们只要间接的通过成员函数访问私有成员变量即可。

cpp 复制代码
class A
{
public:
	static A* GetInstance()
	{
		return &_inst;
	}
	void Add(const string& key,const string& value)
	{
		_dict[key] = value;
	}

private:
	A()
	{}
	map<string, string> _dict;
	int _n = 0;
	static A _inst;
};
A A::_inst;

我们还可以写一个"打印函数",

cpp 复制代码
class A
{
public:
	static A* GetInstance()
	{
		return &_inst;
	}
	void Add(const string& key,const string& value)
	{
		_dict[key] = value;
	}
	void Print()
	{
		for (auto& kv : _dict)
		{
			cout << kv.first << ":" << kv.second << endl;
		}
		cout << endl;
	}
private:
	A()
	{}
	map<string, string> _dict;
	int _n = 0;
	static A _inst;
};
A A::_inst;

我们开始测试一下这个类。

cpp 复制代码
int main()
{
	A::GetInstance()->Add("sort", "排序");
	A::GetInstance()->Add("left", "左边");
	A::GetInstance()->Add("right", "右边");
	A::GetInstance()->Print();
	return 0;
}

输出:

我们还需要禁掉拷贝构造,不然可以通过拷贝构造创建对象,如下所示,

这样就创造了两个不同的对象。 所以我们要把拷贝构造禁掉,如果不需要自己给自己赋值,赋值重载也可以禁掉。

饿汉模式的优点:实现简单。

饿汉模式的缺点:1.可能会导致进程启动慢;

  1. 如果有两个单例,A类单例和B类单例,分别在不同的文件中,且要区分启动的先后顺序,饿汉模式无法控制启动的先后顺序。

懒汉模式

既然懒汉模式是需要时才用,那我们可以将对象的定义到函数中,调用该函数才会真正创建对象。

为了保证只创建一个对象,我们可以用指针的接受new的对象,这样在函数可以根据指针是否为空,判断是否要创建对象,代码如下,

cpp 复制代码
//懒汉模式:第一次使用时再创建(现吃现用)
class B
{
public:
	static B* GetInstance()
	{
		if (_inst == nullptr)
		{
			_inst = new B;
		}
		return _inst;
	}
	void Add(const string& key, const string& value)
	{
		_dict[key] = value;
	}
	void Print()
	{
		for (auto& kv : _dict)
		{
			cout << kv.first << ":" << kv.second << endl;
		}
		cout << endl;
	}
private:
	B()
	{}
	B(const B& a) = delete;
	B& operator=(const B& a) = delete;
	map<string, string> _dict;
	int _n = 0;
	static B* _inst;
};
B* B::_inst = nullptr;
int main()
{
	B::GetInstance()->Add("sort", "排序");
	B::GetInstance()->Add("left", "左边");
	B::GetInstance()->Add("right", "右边");
	B::GetInstance()->Print();
	return 0;
}

执行,

既然我们是new出一个对象,该资源怎么释放呢?其实new的懒汉对象一般不需要释放,进程正常结束会释放资源。(这里是指系统在进程结束后释放资源)

那么如果进程不结束,或者要提前释放资源,或者要在main()函数结束后释放资源呢?

我们可以定义一个释放资源的函数,外部可以直接调用释放的,

cpp 复制代码
static void DelInstance()
{
	if (_inst)
	{
		delete _inst;
		_inst = nullptr;
	}
}

这样我们可以随时调用这个函数释放资源。

同时我们可以定义一个内部类,以及声明一个该类的静态成员变量,然后在类外定义这个变量。内部类的析构函数封装DelInstance(),这样在main()函数运行结束后,编译器会调用内部类静态全局变量的析构函数,也可以达到清理外部类申请资源的作用。

cpp 复制代码
class B
{
public:
	static B* GetInstance()
	{
		if (_inst == nullptr)
		{
			_inst = new B;
		}
		return _inst;
	}
	void Add(const string& key, const string& value)
	{
		_dict[key] = value;
	}
	void Print()
	{
		for (auto& kv : _dict)
		{
			cout << kv.first << ":" << kv.second << endl;
		}
		cout << endl;
	}
	static void DelInstance()
	{
		if (_inst)
		{
			delete _inst;
			_inst = nullptr;
			cout << "delete" << endl;
		}
	}
private:
	B()
	{}
	B(const B& a) = delete;
	B& operator=(const B& a) = delete;
	map<string, string> _dict;
	int _n = 0;
	static B* _inst;
	class gc
	{
	public:
		~gc()
		{
			DelInstance();
		}
	};
	static gc _gc;
};
B* B::_inst = nullptr;
B::gc B::_gc;
int main()
{
	B::GetInstance()->Add("sort", "排序");
	B::GetInstance()->Add("left", "左边");
	B::GetInstance()->Add("right", "右边");
	B::GetInstance()->Print();
	return 0;
}

运行,

我们没有显示调用 DelInstance(),但还是释放资源了。

相关推荐
明月看潮生6 分钟前
青少年编程与数学 02-003 Go语言网络编程 15课题、Go语言URL编程
开发语言·网络·青少年编程·golang·编程与数学
南宫理的日知录16 分钟前
99、Python并发编程:多线程的问题、临界资源以及同步机制
开发语言·python·学习·编程学习
逊嘘33 分钟前
【Java语言】抽象类与接口
java·开发语言·jvm
van叶~35 分钟前
算法妙妙屋-------1.递归的深邃回响:二叉树的奇妙剪枝
c++·算法
Half-up36 分钟前
C语言心型代码解析
c语言·开发语言
knighthood20011 小时前
解决:ros进行gazebo仿真,rviz没有显示传感器数据
c++·ubuntu·ros
Source.Liu1 小时前
【用Rust写CAD】第二章 第四节 函数
开发语言·rust
monkey_meng1 小时前
【Rust中的迭代器】
开发语言·后端·rust
余衫马1 小时前
Rust-Trait 特征编程
开发语言·后端·rust
monkey_meng1 小时前
【Rust中多线程同步机制】
开发语言·redis·后端·rust