一、不能被拷贝的类
设计思路:
拷贝只会发生在两个场景中:拷贝构造和赋值重载,因此想要让一个类禁止拷贝,只需让该类不能调用拷贝构造以及赋值重载即可。
C++98方案:
将拷贝构造与赋值重载只声明不定义,并且将其访问权限设置为私有即可。
class CopyBan
{
// ...
private:
CopyBan(const CopyBan&);
CopyBan& operator=(const CopyBan&);
//...
};
原因:
-
设置成私有:如果只声明没有设置成private,用户自己如果在类外定义了,就不能禁止拷贝了。
-
只声明不定义:不定义是因为该函数根本不会调用,定义了其实也没有什么意义,不写反而还简单,而且如果定义了就不会防止成员函数内部拷贝了。
C++11方案:
C++11扩展delete的用法,delete除了释放new申请的资源外,如果在默认成员函数后跟上=delete
,表示让编译器删除掉该默认成员函数。
class CopyBan
{
// ...
CopyBan(const CopyBan&)=delete;
CopyBan& operator=(const CopyBan&)=delete;
//...
};
二、只能在堆上创建的类
思路一:将构造、拷贝构造函数私有
- 将类的构造、拷贝构造声明成私有。
- 提供一个静态的成员函数,在该静态成员函数中使用new申请堆空间并调用构造函数完成堆对象的初始化,最后返回该对象的指针。
class HeapOnly
{
int _val;
// 把构造和拷贝构造设置成私有
HeapOnly(int val = 0)
: _val(val)
{
}
// 一定要把拷贝构造也设为私有
HeapOnly(const HeapOnly &obj);
public:
// 提供一个静态的成员函数,使用new申请堆空间并调用构造函数完成堆对象的创建。
static HeapOnly *CreateObj(int val = 0)
{
return new HeapOnly(val);
}
};
int main()
{
// HeapOnly obj;
HeapOnly *pobj1 = HeapOnly::CreateObj(10);
// HeapOnly obj(*pobj1);
return 0;
}
思路二:将析构函数私有
编译器在为类对象分配栈空间时,会先检查类的构造和析构函数的访问性。由于栈的创建和释放都需要由系统完成的,所以若是无法调用构造或者析构函数,自然会报错。如果类的析构函数是私有的,则编译器将报错。
当然为了我们能够释放动态创建的对象,我们必须提供一个公有函数,该函数的唯一功能就是删除堆对象。
- 将类的析构函数声明成私有。
- 提供一个公有的成员函数,执行
delete this
调用析构函数清理对象资源并释放堆空间。
class HeapOnly
{
int _val;
// 把析构设置成私有
~HeapOnly()
{
cout << "~HeapOnly()" << endl;
}
public:
HeapOnly(int val = 0)
: _val(val)
{
}
// 提供一个公有的成员函数,执行delete this调用析构函数清理对象资源并释放堆空间
void DestroyObj()
{
delete this;
}
};
int main()
{
// HeapOnly obj;
HeapOnly *pobj = new HeapOnly(10);
// HeapOnly obj(*pobj);
// delete pobj;
pobj->DestroyObj();
return 0;
}
三、只能在栈上创建的类
思路:重载operator new
我们还可以将new操作符重载并设置为私有访问。
class StackOnly
{
int _val;
void* operator new(size_t t);
public:
StackOnly(int val = 0)
: _val(val)
{
}
StackOnly(const StackOnly &obj)
: _val(obj._val)
{
}
};
int main()
{
StackOnly obj(10);
StackOnly obj1(obj);
// StackOnly *pobj = new StackOnly(10);
// StackOnly *pobj1 = new StackOnly(obj);
return 0;
}
四、不能被继承的类
C++98方案:将构造函数私有
派生类中调不到基类的构造函数,则无法继承。
class NonInherit
{
public:
static NonInherit CreatObj()
{
return NonInherit();
}
private:
NonInherit()
{}
};
C++11方案:final关键字
final修饰类,表示该类不能被继承。
class A final
{
// ....
};
五、单例模式
5.1 设计模式
设计模式(Design Pattern)是一套被反复使用、多数人知晓的、经过分类的代码设计经验总结。
使用设计模式的目的:
为了代码可重用性、让代码更容易被他人理解、保证代码可靠性。 设计模式使代码编写真正工程化;设计模式是软件工程的基石脉络,如同大厦的结构一样。
常用的设计模式:
- 适配器模式:对已有的类进行适配包装形成具有全新功能和性质的类,如:栈、队列、优先级队列、function包装器。
- 迭代器模式:几乎所有容器通用的遍历访问方式,可以封装隐藏容器的底层结构,以类似指针的使用方式访问容器中的数据。如:数组(vector)、链表(list)、哈希表(unordered_map)、树(map)的迭代器。
- 单例模式:接下来的内容
- 工厂模式:工厂模式是一种创建对象的设计模式,它通过定义一个工厂类来封装对象的创建过程,并通过调用工厂类的方法来创建对象,从而将对象的创建与使用分离。
- 观察者模式:观察者模式是一种对象间的一对多依赖关系,当一个对象的状态发生变化时,它的所有依赖者都会得到通知并自动更新。
单例模式:
- 一个类只能创建一个对象,即单例模式。该模式可以保证系统中(进程中)该类只有一个实例,并提供一个访问它的全局访问点,该实例被所有程序模块(线程及函数)共享。
- 比如在某个服务器程序中,该服务器的配置信息存放在一个文件中,这些配置数据由一个单例对象统一读取,然后服务进程中的其他对象再通过这个单例对象获取这些配置信息,这种方式简化了在复杂环境下的配置管理。
- 比如空间配置器一般也是单例模式。
- 单例模式有两种实现模式:饿汉模式和懒汉模式。
5.2 饿汉模式
所谓饿汉模式,就是说不管你将来用不用,程序启动时(main函数之前)就创建一个唯一的实例对象。
方法一:在堆区创建单例
设计思路:
- 私有构造、拷贝构造和析构,保证系统中该类只有一个实例;
- 包含一个该类的静态指针并在类外使用new创建单例,提供一个访问单例的全局访问点;
- 包含一个互斥锁成员,保证多线程互斥访问该单例;
- 提供一个用于获取全局访问点(静态指针)的静态成员函数;
- 包含一个静态的内部类对象,该对象析构时会顺便析构单例,自动释放。
class Singleton
{
// 成员变量
vector<string> _dir;
// 该类的静态指针,提供一个访问单例的全局访问点
static Singleton *s_ins;
// 互斥锁成员,保证多线程互斥访问该单例
mutex s_mtx;
// 静态的内部类对象,该对象析构时会顺便析构单例,自动释放
struct GC
{
~GC()
{
if (s_ins != nullptr)
{
delete s_ins;
s_ins = nullptr;
}
}
};
static GC s_gc;
// 私有构造、拷贝构造和析构,保证系统中该类只有一个实例
Singleton()
{
cout << "Singleton()" << endl;
};
Singleton(const Singleton &st);
~Singleton()
{
// 单例对象的析构一般会做一些持久化操作(数据落盘)
// ......
cout << "~Singleton()" << endl;
}
public:
// 提供一个静态成员函数,用于获取全局访问点(静态指针)
static Singleton *GetInstance()
{
return s_ins;
}
void Add(const string &name)
{
s_mtx.lock();
_dir.push_back(name);
s_mtx.unlock();
}
void Print()
{
s_mtx.lock();
for (auto &name : _dir)
{
cout << name << endl;
}
s_mtx.unlock();
}
};
// 程序启动时(main函数之前)创建
Singleton *Singleton::s_ins = new Singleton;
Singleton::GC Singleton::s_gc;
int main()
{
// 系统中该类只有一个实例,不允许通过任何方式实例化
// Singleton st;
// static Singleton st1;
// Singleton* pst = new Singleton;
// Singleton st(*(Singleton::GetInstance()));
// 单线程场景
// Singleton::GetInstance()->Add("张三");
// Singleton::GetInstance()->Add("李四");
// Singleton::GetInstance()->Add("王五");
// Singleton::GetInstance()->Print();
// 多线程场景
int n = 6;
srand((unsigned int)time(nullptr));
thread t1([n]() mutable
{
while(n--)
{
Singleton::GetInstance()->Add("线程1:" + to_string(rand()));
this_thread::sleep_for(chrono::milliseconds(10));
} });
thread t2([n]() mutable
{
while(n--)
{
Singleton::GetInstance()->Add("线程2:" + to_string(rand()));
this_thread::sleep_for(chrono::milliseconds(10));
} });
t1.join();
t2.join();
Singleton::GetInstance()->Print();
}
运行结果(多线程场景):
方法二:在静态区创建单例
设计思路:
- 私有构造、拷贝构造和析构,保证系统中该类只有一个实例;
- 包含一个该类的静态对象并在类外定义,提供一个访问单例的全局访问点;
- 包含一个互斥锁成员,保证多线程互斥访问该单例;
- 提供一个用于获取全局访问点(静态对象的引用)的静态成员函数;
- 由于单例是在静态区创建的,进程结束时,系统会自动调用单例析构释放其资源。
// 饿汉模式2
class Singleton
{
// 成员变量
vector<string> _dir;
// 该类的静态对象,提供一个访问单例的全局访问点
static Singleton s_ins;
// 互斥锁成员,保证多线程互斥访问该单例
mutex s_mtx;
// 私有构造、拷贝构造和析构,保证系统中该类只有一个实例
Singleton()
{
cout << "Singleton()" << endl;
};
Singleton(const Singleton &st);
// 由于单例是在静态区创建的,进程结束时,系统会自动调用单例析构释放其资源。
~Singleton()
{
// 单例对象的析构一般会做一些持久化操作(数据落盘)
// ......
cout << "~Singleton()" << endl;
}
public:
// 提供一个静态成员函数,用于获取全局访问点(静态对象的引用)
static Singleton &GetInstance()
{
return s_ins;
}
void Add(const string &name)
{
s_mtx.lock();
_dir.push_back(name);
s_mtx.unlock();
}
void Print()
{
s_mtx.lock();
for (auto &name : _dir)
{
cout << name << endl;
}
s_mtx.unlock();
}
};
// 程序启动时(main函数之前)创建
Singleton Singleton::s_ins;
运行结果:同上
饿汉模式的缺点:
- 由于单例对象是在main函数之前创建的,如果单例对象很大,很复杂,其创建和初始化所占用的时间较多。会拖慢程序的启动速度。
- 如果当前进程暂时不需要使用该单例对象,而饿汉模式在启动时创建单例占用了空间和时间资源。
- 如果具有依赖关系的两个单例都是饿汉模式,需要先创建单例1再创建单例2。饿汉模式无法控制其创建和初始化顺序。
提示:饿汉模式的全局访问点除了定义静态指针还可以直接定义成静态对象。如果是静态对象,进程在退出时会自动调用其析构函数。
5.3 懒汉模式
如果单例对象的构造十分耗时或者占用很多资源,比如加载插件、 初始化网络连接、读取文件等等。而且有可能程序运行时不会用到该对象,如果也在程序一开始就进行初始化,就会导致程序启动时非常的缓慢。 所以这种情况使用懒汉模式(延迟加载)更好。
所谓懒汉模式,就是在任意程序模块第一次访问单例时实例化对象。
方法一:在堆区创建单例
设计思路:
- 私有构造、拷贝构造和析构,保证系统中该类只有一个实例;
- 包含一个该类的静态指针并在类外初始化为nullptr,提供一个访问单例的全局访问点;
- 包含一个静态互斥锁并在类外定义,保证多线程互斥地创建和访问该单例;
- 提供一个静态成员函数,用于首次调用创建单例(注意双检查加锁)和获取全局访问点(静态指针);
- 包含一个静态的内部类对象,该对象析构时会顺便析构单例,自动释放。
// 懒汉模式
class Singleton
{
// 成员变量
vector<string> _dir;
// 该类的静态指针,提供一个访问单例的全局访问点
static Singleton *s_ins;
// 静态互斥锁,保证多线程互斥地创建和访问该单例
static mutex s_mtx;
// 静态的内部类对象,该对象析构时会顺便析构单例,自动释放
struct GC
{
~GC()
{
if (s_ins != nullptr)
{
delete s_ins;
s_ins = nullptr;
}
}
};
static GC gc;
// 私有构造、拷贝构造和析构,保证系统中该类只有一个实例
Singleton()
{
cout << "Singleton()" << endl;
};
Singleton(const Singleton &st);
~Singleton()
{
// 单例对象的析构一般会做一些持久化操作(数据落盘)
// ......
cout << "~Singleton()" << endl;
}
public:
static Singleton *GetInstance()
{
// 懒汉模式:在第一次访问实例时创建
// 双检查加锁
if (s_ins == nullptr) // 第一道检查:提高效率,不需要每次获取单例都加锁解锁
{
s_mtx.lock();
if (s_ins == nullptr) // 第二道检查:保证线程安全和只new一次
{
s_ins = new Singleton;
}
s_mtx.unlock();
}
return s_ins;
}
void Add(const string &name)
{
s_mtx.lock();
_dir.push_back(name);
s_mtx.unlock();
}
void Print()
{
s_mtx.lock();
for (auto &name : _dir)
{
cout << name << endl;
}
s_mtx.unlock();
}
// 一般单例对象的生命周期随进程,系统会在进程退出时释放其内存,不需要中途析构单例对象
// 不过在一些特殊场景下,可能需要进行显示手动释放
static void DelInstance()
{
s_mtx.lock();
if (s_ins != nullptr)
{
delete s_ins;
s_ins = nullptr;
}
s_mtx.unlock();
}
};
// 静态成员要在类外定义
Singleton *Singleton::s_ins = nullptr;
mutex Singleton::s_mtx;
Singleton::GC Singleton::gc;
运行结果(多线程场景):
方法二:在静态区创建单例(C++11)
设计思路:
- 私有构造、拷贝构造和析构,保证系统中该类只有一个实例;
- 提供一个静态成员函数,用于首次调用创建单例(创建静态局部对象)和获取全局访问点(静态对象的指针);
- 包含一个互斥锁成员,保证多线程互斥访问该单例;
- 由于单例是在静态区创建的,进程结束时,系统会自动调用单例析构释放其资源。
// 懒汉模式2
class Singleton
{
// 成员变量
vector<string> _dir;
// 互斥锁成员,保证多线程互斥访问该单例
mutex s_mtx;
// 私有构造、拷贝构造和析构,保证系统中该类只有一个实例
Singleton()
{
cout << "Singleton()" << endl;
};
Singleton(const Singleton &st);
~Singleton()
{
// 单例对象的析构一般会做一些持久化操作(数据落盘)
// ......
cout << "~Singleton()" << endl;
}
public:
static Singleton *GetInstance()
{
// C++11之前,这里不能保证初始化静态对象的线程安全问题
// C++11之后,这里可以保证初始化静态对象的线程安全问题
static Singleton s_ins; //首次调用时创建局部静态对象
return &s_ins;
}
void Add(const string &name)
{
s_mtx.lock();
_dir.push_back(name);
s_mtx.unlock();
}
void Print()
{
s_mtx.lock();
for (auto &name : _dir)
{
cout << name << endl;
}
s_mtx.unlock();
}
};
运行结果:同上
懒汉模式模式完美解决了饿汉模式的问题,就是相对复杂一些。