特殊类设计(只在堆/栈上创建对象,单例模式),完整版代码+思路
目录
类不能被拷贝
拷贝有两种方式,拷贝构造和赋值拷贝,所以只需要让创建出的对象不能使用这两个成员函数即可
"不能使用"可以有两种方式
- 设置为私有
- 私有后,一般只声明不定义,如果定义了,可能会在类内部使用拷贝操作
- 使用关键字"delete"
- c++11后扩展了delete的用法
- delete除了释放new申请的资源外,如果在默认成员函数后跟上
=delete,表示让编译器删除掉该默认成员函数 既然已经删除了,也就无法使用了
类不能被继承
之前就有提到过,如果构造函数是私有的,则无法创建对象
那在继承概念中,如果基类无法被创建,自然子类也创建不出来,也就没有继承的说法了
也可以使用c++11引入的final关键字,表示该类不能被继承
只在堆上创建对象
- 也就是说,只能通过申请资源的方式创建对象,那么申请到的都是由指针指向的一块空间,也就是说,需要我们返回指针
- 但构造函数显然无法满足这个条件,所以我们干脆禁用构造函数,直接给一个接口函数
- 禁用就和前面不能拷贝类似,有两种方式
- 注意!!!要把这个接口函数设置成静态的
- 这个函数本来就是用来创建对象的,但普通成员函数的调用又需要一个对象,这就形成了先有鸡还是先有蛋的问题
- 所以,直接设置成静态的,就可以用类域调用了
class HeapOnly { public: static HeapOnly* CreateObject() //一定要是静态的嗷!!! { return new HeapOnly; } private: // C++98 // 1.只声明,不实现,本身就不需要实现 // 2.声明成私有 HeapOnly() {} HeapOnly(const HeapOnly&); C++11 //HeapOnly(const HeapOnly&) = delete; };
只在栈上创建对象
也就是要禁止申请空间,首先可以采用上面的方式,直接给接口,构造禁掉
class StackOnly { public: static StackOnly CreateObject() { return StackOnly(); } private: StackOnly() {} StackOnly(const StackOnly&); };
类对象在new的时候,实际上会先调用operator new,然后再调用构造函数
operator new
- 是C++中用于动态分配内存的内置运算符
- 主要作用是分配一块连续的内存空间,以便在其中存储对象或数据
- 可以被重载
所以我们可以声明一个删除的operator new函数,这样外部就不能用new申请资源了
class StackOnly { public: StackOnly(){} StackOnly(const StackOnly& tmp) {} private: void* operator new(size_t size) = delete; int _a = 1; };
还可以禁掉释放资源的函数,这样申请到的资源也就无法释放,那编译器就不会允许你在堆上申请
还记得前面的operator new吗,类似的,delete也会先调用operator delete
operator delete
- 和operator new配套的运算符,用于释放动态分配内存的内置运算符
- 通过传递要释放的内存块的指针,它将该内存块返回给系统或内存管理器,以便将其重新分配给其他用途
- 允许重载
所以,和上面操作类似
class StackOnly { public: StackOnly(){} StackOnly(const StackOnly& tmp) {} private: //void* operator new(size_t size) = delete; void operator delete(void* p) = delete; int _a = 1; };
只能创建一个对象
设计模式
介绍
- 设计模式是一种用于解决软件设计问题的经验和最佳实践的复用方案
- 它们提供了在特定情境下的通用解决方案,有助于创建更可维护、灵活和可扩展的软件
- 设计模式是从实践中总结出来的,并被广泛接受和使用,以解决常见的设计问题
常见的设计模式
这里只介绍一下单例模式
单例模式
介绍
单例模式是一种创建型设计模式,其目的是确保一个类只有一个实例,并提供一个全局访问点来访问该实例
应用
- 当需要共享某个资源,例如配置信息、日志记录、数据库连接、线程池等,单例模式可以确保全局只有一个资源实例,避免资源的浪费和冲突
- 当需要维护全局状态,例如应用程序的状态或设置,单例模式可以提供一个中心点来管理和访问这些状态
- 也就是程序运行过程中,只需要一份/只能有一份的时候,单例模式可以防止创建出多份对象
饿汉模式
介绍
- 也称为预先实例化模式,是一种单例模式的实现方式
- 在饿汉模式中,单例实例在类加载时就被创建,因此在整个程序生命周期中,该实例都是唯一的
实现
思路
- 因为只能有一个,所以构造/拷贝构造/赋值拷贝都必须禁用
- 为了确保只有一份实例,我们的接口函数必须返回的是同一个对象
- 并且这个对象应该是静态的,不然怎么实现返回的都是一个对象,必须要让它的作用域是全局,而不是某个对象
- 由于是预先实例化,所以提前在程序开始前实例化,也就是在全局中实例化
代码
class Singleton {
public:
static Singleton& GetInstance() { //每次调用接口都只返回那一个对象
return _instance;
}
void add(int t) {
_arr.push_back(t);
}
void print() {
for (auto t : _arr) {
std::cout << t << " ";
}
}
private:
Singleton() {}; //注意这里要有函数体(也就是要定义它),因为我们的_instance需要被初始化
Singleton(const Singleton&);
Singleton& operator=(const Singleton&);
static Singleton _instance;
std::vector<int> _arr;
};
Singleton Singleton::_instance;
使用
int main() {
Singleton::GetInstance().add(1);
//使用类域调用对象接口,并且用返回值直接调用函数,因为对象是私有的,不能被显式定义
Singleton::GetInstance().add(2);
Singleton::GetInstance().add(3);
Singleton::GetInstance().print();
return 0;
}
懒汉模式
引入
- 饿汉模式存在很多缺陷,如果类很大,那程序启动所需的时间将会很长
- 或者如果某个类需要依赖其他类,但我们无法保证究竟哪个类先被实例化
- 所以,为了解决这些问题,懒汉模式就被引入了
介绍
- 懒汉模式推迟了实例的创建,直到首次访问该实例时才进行初始化
- 避免在程序启动时需要大量资源初始化时,产生不必要的开销
- 但是,在多线程环境下不是线程安全的
实现
思路
- 既然需要在首次访问时创建对象,那么就在第一次调用接口时才创建对象,之后返回该对象即可
- 该如何判断是否为第一次呢?
- 可以考虑用一个指针,如果该指针有指向的对象,就说明不是第一次了
- 但是,这个对象必须得是动态开辟出来的,不然构造出来的是个右值对象,没法取地址
- 所以,就要面临释放资源的问题
- 但是,由于我们的变量是个指针,没法自动析构,所以要定义一个接口del来手动释放
代码
namespace lazy {
class Singleton {
public:
static Singleton& GetInstance() { //每次调用接口都只返回那一个对象
if (_p==nullptr) {
//_p = &Singleton(); //如果_p指向普通对象,这里就是右值了,无法取地址
_p = new Singleton;//所以必须要动态申请,这样的话就需要释放资源了
//但其实一般单例模式不需要释放,随程序结束就自己释放了
//但我们可能中途需要释放,那析构函数就没有用了,_p是静态对象,不会在中途自己调用析构
//所以需要定义一个接口
}
return *_p;
}
static void DelInstance() { //因为我们的对象是私有的,在外部访问不到,定义成static方便一点
if (_p) {
delete _p;//这里会调用析构
_p = nullptr;
}
}
void add(int t) {
_arr.push_back(t);
}
void print() {
for (auto t : _arr) {
std::cout << t << " ";
}
}
private:
Singleton() {};
Singleton(const Singleton&);
Singleton& operator=(const Singleton&);
~Singleton() //_p离开作用域并不会调用析构,所以需要手动调用,也就是在del接口中
{
std::cout << "~Singleton()" << std::endl;
//其他操作
}
std::vector<int> _arr;
static Singleton* _p;//和_instance一样,要定义成静态的,保证_p的唯一性
};
Singleton* Singleton::_p = nullptr;
}
使用
- 使用其实和饿汉模式差不多,只不过懒汉的构造调用时间不同,并且增加了释放资源的接口
- 注意!!!因为我们定义的是个指针,所以不会自动析构
- 所以,唯一的接口就是那个del,在它里面可以调用析构
- 我们可以手动调用del
- 也可以改造一下让他具有析构函数的性质(离开作用域自动调用)
显式析构
像上面那样写,可以让我们显式调用del函数来释放资源,并且在释放前完成某些操作
int main() { lazy::Singleton::GetInstance().add(1); lazy::Singleton::GetInstance().add(2); lazy::Singleton::GetInstance().add(3); lazy::Singleton::GetInstance().print(); lazy::Singleton::DelInstance();//显式调用 lazy::Singleton::GetInstance().add(4); lazy::Singleton::GetInstance().print(); return 0; }
注意,这里最后程序没有自动调用析构,因为我们的静态成员是_p指针,它来指向被申请的空间,而不是直接创建一个对象
隐式析构
- 如果我们想要在程序结束前也完成某些操作,就可能不太方便,需要我们手动调用接口,况且我们可能也不知道什么时候程序结束
- 所以,可以利用智能指针的特性,将del接口放在某个类的析构函数中
- 这样随着类析构,也就自动调用了del函数
namespace lazy { class Singleton { public: static Singleton& GetInstance() { //每次调用接口都只返回那一个对象 if (_p==nullptr) { //_p = &Singleton(); //如果_p指向普通对象,这里就是右值了,无法取地址 _p = new Singleton;//所以必须要动态申请,这样的话就需要释放资源了 //但其实一般单例模式不需要释放,随程序结束就自己释放了 //但我们可能中途需要释放,那析构函数就没有用了,_p是静态对象,不会在中途自己调用析构 //所以需要定义一个接口 } return *_p; } static void DelInstance() { //因为我们的对象是私有的,在外部访问不到,定义成static方便一点 if (_p) { delete _p;//这里会调用析构 _p = nullptr; } } class func { //用于程序结束自动调用del接口 public: ~func() { Singleton::DelInstance(); } }; void add(int t) { _arr.push_back(t); } void print() { for (auto t : _arr) { std::cout << t << " "; } } private: Singleton() {}; //注意这里要有函数体(也就是要定义它),因为我们的_instance需要被初始化 Singleton(const Singleton&); Singleton& operator=(const Singleton&); ~Singleton() //vs2019下,静态对象好像不会自动调析构,而是直接释放资源了 { std::cout << "~Singleton()" << std::endl; //其他操作 } std::vector<int> _arr; static Singleton* _p; static func _f;//定义一个静态对象 //得是静态的,不然一个对象一个_f,会对_p析构多次 }; Singleton* Singleton::_p = nullptr; Singleton::func Singleton::_f; }
- 这样,即使我们没有显式调用del,也可以在程序结束前自动调用del