设计一个不能被拷贝的类
由于拷贝只会发生在两个场景中:拷贝构造函数以及赋值运算符重载,因此想要让一个类禁止拷贝,只需禁用该类的拷贝构造函数以及赋值运算符重载即可。
在c++98中的做法通常是将拷贝构造函数与赋值运算符重载只声明不定义,并且将其访问权限设置为私有即可,原因如下:
- 只声明不定义:声明是为了不让编译器生成默认的拷贝构造函数与赋值运算符重载,不定义是因为该函数根本不会调用,定义了其实也没有什么意义,不写反而还简单,而且如果定义了内部成员函数就有机会进行调用达到拷贝的目的。
- 设置成私有:如果只声明没有设置成private,用户就可以在类外定义这2个函数从而进行拷贝。
class BanCopy
{
private:
BanCopy(const BanCopy& bc);
BanCopy& operator=(const BanCopy& bc);
};
c++11拓展了关键字delete的用法,其除了可以释放new申请的资源外,还可以在默认成员函数后跟上=delete,表示让编译器删除掉该默认成员函数,因此在c++11中不让一个类进行拷贝的做法很简单,只需要在拷贝构造函数以及赋值运算符重载后面跟上=delete即可:
class BanCopy
{
BanCopy(const BanCopy& bc)=delete;
BanCopy& operator=(const BanCopy& bc)=delete;
};
设计一个只能在堆上创建对象的类
方法一:
我们只需要提供一个在堆上创建对象的成员函数,让用户能且仅能通过该成员函数创建对象。
实现方式:
- 将类的所有构造函数声明成私有。
- 提供一个静态的成员函数,在该静态成员函数中完成堆对象的创建
将类的所有构造函数声明成私有是防止别人调用构造函数在栈上生成对象(拷贝构造函数可以不处理,因为用户想要调用该函数就必须要有一个直接的CreateOnHeapOnly对象,但用户只有指向该对象的指针自然就无法调用该函数在栈上创建对象,为了以防万一,也可以对其进行处理,同理也不需要对赋值运算符重载函数进行处理)将创建对象的函数设为静态是因为倘若我们将其设为普通成员函数,我们就必须需要通过对象才可以去调用这个成员函数(隐含的this指针),但我们又还没有创建出对象,所以我们将该函数设为静态,指明类域就可以直接调用,不需要通过对象调用(静态成员函数没有隐含的this指针)。
class CreateOnHeapOnly
{
public:
static CreateOnHeapOnly* CreateObject();
private:
CreateOnHeapOnly()
{}
};
CreateOnHeapOnly* CreateOnHeapOnly::CreateObject()
{
return new CreateOnHeapOnly;
}
方法二:
由于编译器在为类对象分配栈空间时,会先检查该类的析构函数是否可以访问,如若不能访问则拒绝在栈空间上创建该对象,因此我们也可以通过将析构函数私有化达到目的,同时为了避免内存泄漏需要提供一个public权限的函数让用户手动调用去释放资源。
class CreateOnHeapOnly
{
public:
void MyCreateOnHeapOnly()
{
this->~CreateOnHeapOnly();
}
private:
~CreateOnHeapOnly()
{}
};
设计一个只能在栈上创建对象的类
同上面一样,我们想用一个静态方法在栈上创建对象,同时为了防止用户通过new在堆上创建对象,我们需要私有化所有构造函数,但不能对拷贝构造函数进行处理,因为我们在接收静态成员函数返回的对象时需要拷贝构造函数,但这样用户就可以在栈区创建出一个对象cs1后这样干:
CreateOnStackOnly* cs2 = new CreateOnStackOnly(cs1);
我们只能对new进行处理:由于new在底层分为2步,第一步是先调用operator new函数在堆上面开好空间,第二步是调用构造函数在该空间上创建对象,我们可以通过禁用该类的operator new函数,这样就无法通过new在堆上面创建对象了。最后我们发现其实只要禁用该类的operator new用户就无法使用new在堆上开辟空间了。与此对应我们也可以禁用operator delete函数,这样编译器就会检查到开辟的资源会无法释放,就不会允许用户在栈上创建该对象。
class CreateOnStackOnly
{
void* operator new(size_t size) = delete;
//或者void operator delete(void* p)=delete;
//或者void* operator new(size_t size);
//或者void operator delete(void* p);
};
设计一个不能被继承的类
在c++11中实现的方式比较简单,只需要使用关键字final对类进行修饰即可:
class NonInherit final
{
// ....
};
设计一个只能创建一个对象的类(单例模式)
一个类只能创建一个对象,即单例模式,该模式可以保证系统中该类只有一个实例,并提供一个访问它的全局访问点,该实例被所有程序模块共享。比如在某个服务器程序中,该服务器的配置信息存放在一个文件中,这些配置数据由一个单例对象统一读取,然后服务进程中的其他对象再通过这个单例对象获取这些配置信息,这种方式简化了在复杂环境下的配置管理。
单例模式有两种实现模式: 饿汉模式和懒汉模式。
饿汉模式
饿汉模式的思路是不管用户是否需要,在程序启动时就实例化出一个对象,具体做法是:
- 提供一个静态成员函数让用户可以获取该唯一实例化对象
- 私有化所有构造函数并禁止该类的拷贝
- 在全局中实例化出该对象
class Singleton
{
public:
static Singleton* GetInstance()
{
return &m_instance;
}
private:
// 构造函数私有
Singleton(){};
// C++11防拷贝
Singleton(Singleton const&) = delete;
Singleton& operator=(Singleton const&) = delete;
static Singleton m_instance;
};
Singleton Singleton::m_instance; // 在程序入口(main函数)之前就完成单例对象的初始化
将获取该唯一实例化对象的函数设为静态是因为用户一开始无法得到该唯一实例化对象,无法调用普通成员函数;私有化所有构造函数并禁止该类的拷贝是为了阻止用户实例化出第2个对象,当然由于我们提供的GetInstance()函数返回的是指针,所以可以不对拷贝构造函数进行处理;将m_instance设为静态是为了保证GetInstance()返回的是同一个对象,否则编译也不会让你通过; 在全局中实例化出该对象是因为我们希望该类预先实例化出对象。
如果该单例对象在多线程高并发环境下频繁使用,这对性能要求较高,显然使用饿汉模式可以避免资源竞争,提高响应速度。
优点:简单
缺点:可能会导致进程启动慢,且如果有多个单例类对象实例启动顺序是不确定的。
懒汉模式
如果单例对象构造十分耗时或者占用很多资源,比如加载插件、初始化网络连接,读取文件等等,而且或许该对象在程序运行中根本就没有用到,但却要在程序一开始就进行初始化,这明显会降低程序启动速度,在这种情况下使用懒汉模式(延迟加载)就会更好。
实现方式是:
- 提供一个静态成员函数让用户可以获取该唯一实例化对象
- 私有化所有构造函数并禁止该类的拷贝
- 实现一个内嵌垃圾回收类
class Singleton
{
public:
static Singleton* GetInstance()
{
if(nullptr==m_pInstance)
{
m_pInstance = new Singleton();
}
return m_pInstance;
}
// 实现一个内嵌垃圾回收类
class MyDelete
{
public:
~MyDelete()
{
if (Singleton::m_pInstance)
{
delete Singleton::m_pInstance;
}
}
};
// 定义一个静态成员变量,程序结束时,系统会自动调用它的析构函数从而释放单例对象
static MyDelete md;
private:
// 构造函数私有
Singleton(){};
// 防拷贝
Singleton(Singleton const&)=delete;
Singleton& operator=(Singleton const&)=delete;
// 单例对象指针
static Singleton* m_pInstance;
};
Singleton* Singleton::m_pInstance = nullptr;
Singleton::MyDelete md;
由于m_pInstance是一个指针,编译器在释放资源时不会调用Singleton类的析构函数,因此我们需要实现一个垃圾回收类对开辟的资源进行释放:我们定义了一个内部类MyDelete,由该内部类的析构函数对开辟的资源进行释放,利用该内部类定义一个静态成员变量md,当程序退出释放md时调用其析构函数就完成了对资源的释放。md要设为静态是因为在其析构中调用delete释放m_pInstance时,如果md是普通成员,由于m_pInstance包含成员md,delete就又会调用md的析构函数,引发无穷递归;如果md是静态成员,其不属于任何对象,delete就又不会再调用md的析构函数,这样资源就可以正常释放了。
优点:只有在第一次使用实例对象时创建对象,意味着进程启动时无负载,同时多个单例实例启动顺序自由控制。
缺点:复杂