一、设计一个类,不能被拷贝
有人可能会觉得,这不是很简单吗,直接把拷贝构造ban掉,不就行了,但事实真的如此吗?
class A
{
public:A(){}A(const A& tmp) = delete;// ...
};int main()
{A a;// A b = a; // 该形式的拷贝被ban了A b;b = a; // 可以通过赋值重载来变相的完成拷贝return 0;
}
所以,正确的做法是将赋值重载和拷贝构造都ban掉,代码如下
class A
{
public:A(){}// C++11 的做法 --- 直接ban掉A(const A& tmp) = delete;A& operator=(const A& tmp) = delete;// ...
private:// C++98 的做法 --- 声明为私有,只声明不实现的原因是类外不能调用,实现了也没用// A(const A& tmp);// A& operator=(const A& tmp);
};
二、设计一个类,只能在堆上创建对象
在考虑只能在堆上建立对象之前,我们来想想如何禁止在栈上创建对象?而创建对象跟构造函数有关,所以问题变成如何调整构造函数,从而ban掉在栈上创建对象?
其实我们只要将构造函数的访问权限设置为private,那么我们就无法在外部调用构造函数,也就无法在栈上创建对象,代码如下
class A
{
private:A(){} // 将构造函数放在private中,不让外部使用
};
但是,现在我们也不能在堆上创建对象,因为new内部也要调用A的构造函数,如何解决?既然不能在外部创建对象,那么我们只能在类内提供接口,因为类内是允许访问成员函数的,同时,我们也需要让外部在没有对象的情况下能调用到该接口,显然该接口得是静态的成员函数,代码如下
class A
{
public:static A* HeapOnly(){return new A;}
private:A(){} // 将构造函数放在private中,不让外部使用,只能在类内提供接口
};int main()
{A* a = A::HeapOnly();return 0;
}
但是,现在的代码还是存在问题,结合第一个设计样例的问题,我们很容易发现,当我们在对申请出来的堆上的对象进行拷贝操作时,我们就避开了构造函数,在栈上创建出了对象,如下
int main()
{A* a = A::HeapOnly();A b = *a; // 拷贝构造// A c; c=*a; // 不行,因为赋值的前提是得先有对象return 0;
}
所以,正确的做法是还要将拷贝构造ban掉,代码如下
class A
{
public:static A* HeapOnly(){return new A;}
private:A(){} // 将构造函数放在private中,不让外部使用,只能在类内提供接口// C++11 的做法A(const A& tmp) = delete;// C++98 的做法// A(const A& tmp);
};
三、设计一个类,只能在栈上创建对象
和设计一个只能在堆上创建对象的类的思路大致相同,我们同样需要将构造函数设为私有,并且对外提供一个静态的成员函数用来返回在栈上创建的对象,代码如下
class A
{
public:static A StackOnly(){return A();}
private:A(){}
};
但是我们如何禁止它在堆上创建对象呢?可能有人会觉得上面这个类,已经无法在堆上创建对象了,因为new无法调用到构造函数,但是,new可以调用拷贝构造呀,你不是能创建栈上的对象吗,我就拿你创建的对象进行拷贝构造,如下
int main()
{A a = A::StackOnly();A* p = new A(a);return 0;
}
那么我们该如何做?将new和delete直接ban掉,从根源上解决问题,一劳永逸,代码如下
class A
{
public:static A StackOnly(){return A();}void* operator new(size_t) = delete;void operator delete(void*) = delete;
private:A(){}
};
四、设置一个类,不能被继承
这个就很简单,直接在给类加上 final 关键字即可(C++11加的),代码如下
class A final
{// ...
};
当然,我们也能通过将该类的构造函数私有化来实现,因为子类对象的创建需要先创建它的父类部分,而显然它如法调用父类的构造函数,故无法被继承,代码如下
// C++98中构造函数私有化,派生类中调不到基类的构造函数。则无法继承
class A
{
public:static A GetInstance(){return A();}
private:A(){}
};
扩展:结合不能被继承的思路,如果我们不想一个类能被拷贝 / 赋值,除了将该类的赋值重载和拷贝构造ban掉,我们还可以如何做?
我们可以让该类继承一个被ban掉了赋值重载和拷贝构造的基类,这样由于基类成员无法被拷贝和赋值,就会导致它也无法被拷贝和赋值,从而实现了该类也无法被拷贝和赋值的操作
class nocopy { public:nocopy(){}nocopy(const nocopy& tmp) = delete;nocopy& operator=(const nocopy& tmp) = delete; };class A :public nocopy {//... }; int main() {A a;A b = a; // 报错A c; c = a; // 报错return 0; }
五、设计一个类,只能创建一个对象(单例模式)
一个类只能创建一个对象,即单例模式,该模式可以保证系统中该类只有一个实例,并提供一个访问它的全局访问点,该实例被所有程序模块共享。比如在某个服务器程序中,该服务器的配置信息存放在一个文件中,这些配置数据由一个单例对象统一读取,然后服务进程中的其他对象再通过这个单例对象获取这些配置信息,简化了在复杂环境下的配置管理
1、饿汉模式
提前(main函数启动时)创建好实例对象
如何实现?
1、该对象只能有一个,我们肯定需要将构造函数设置为私有,不让我们能在外面创建对象
2、该对象得是个全局的,且不能在类外创建,它只能是类的静态成员变量,并且类的成员变量一般都设置为私有,所以我们还得给它提供一个静态成员函数,让外界能访问到它
3、根据上面设计的经验,我们还要考虑赋值重载和拷贝构造的问题,这里我们全部ban掉,理由同上 (ps:防止有人整什么奇怪的操作)
如果有没明白的地方,可以看看代码,代码如下
// 假设我们需要一个单例字典
class A
{
public:static A* GetInstance(){return &_inst;}void push(const string& src, const string& dest){_mp[src] = dest;}void print(){for (auto it : _mp){cout << it.first << " " << it.second << endl;}}A(const A& tmp) = delete;A& operator=(const A& tmp) = delete;private:A(){}
private:map<string, string>_mp;// A _a; // 这种写法是错误的,A类中不能包含A类对象static A _inst; // 这样是可以的,因为静态变量不属于类对象,属于整个类,存放在静态区// 1、静态成员属于类,所以能调用构造函数// 2、静态成员在整个类中只有一份// 3、静态成员能通过 类域 访问,即可以全局访问,只要能找到这个类,当然这里我们需要通过接口间接得到// 完美卡上所有我们需要的条件
};A A::_inst;int main()
{A::GetInstance()->push("left", "左边");A::GetInstance()->push("right", "右边");A::GetInstance()->push("apple", "苹果");A::GetInstance()->push("zxws", "帅");// (doge)A::GetInstance()->print();return 0;
}
优点:实现简单
缺点:
1、可能导致进程启动满,因为它在main函数之前就要被初始化好
2、如果两个单例启动有先后顺序,那么 饿汉模式 无法控制
2、懒汉模式
第一次使用时,才创建,和饿汉模式的代码很相似,如下
class A
{
public:static A* GetInstance(){if (_inst == nullptr){_inst = new A;}return _inst;}void push(const string& src, const string& dest){_mp[src] = dest;}void print(){for (auto it : _mp){cout << it.first << " " << it.second << endl;}}A(const A& tmp) = delete;A& operator=(const A& tmp) = delete;private:A() {}
private:map<string, string>_mp;static A* _inst;
};A* A::_inst = nullptr;int main()
{A::GetInstance()->push("left", "左边");A::GetInstance()->push("right", "右边");A::GetInstance()->push("apple", "苹果");A::GetInstance()->push("zxws", "帅");// (doge)A::GetInstance()->print();return 0;
}
一般来说,如果没有什么特殊要求,这样写就可以了(进程正常结束会自动释放空间),但是如果我们需要在释放单例之前做一些操作,就需要我们去调用析构函数,同时我们希望它能在程序结束时被自动调用,如何做?代码如下
class A
{
public:static A* GetInstance(){if (_inst == nullptr){_inst = new A;}return _inst;}void push(const string& src, const string& dest){_mp[src] = dest;}void print(){for (auto it : _mp){cout << it.first << " " << it.second << endl;}}A(const A& tmp) = delete;A& operator=(const A& tmp) = delete;static void DelInstance() // 提供手动释放的接口{if (_inst){// ...delete _inst;_inst = nullptr;}}
private:A() {}
private:map<string, string>_mp;static A* _inst;class gc{public:~gc(){DelInstance();}};static gc _gc; // 用一个类来控制资源,类似智能指针
};A* A::_inst = nullptr;
A::gc A::_gc;
除此之外,懒汉模式在创建和销毁时,还会涉及线程安全问题,即当多个线程同时创建/销毁进程时,会导致空间泄露 / 多次销毁同一段空间 的问题,所以我们需要给它加锁,代码如下
class A
{
public:static A* GetInstance(){if (_inst == nullptr) // 当单例创建完,后续线程在访问时,没必要在去申请锁去判断是否要创建{lock_guard<mutex> lock(_mutex);if (_inst == nullptr){_inst = new A;}}return _inst;}A(const A& tmp) = delete;A& operator=(const A& tmp) = delete;static void DelInstance(){if (_inst) // 当单例释放完,后续线程在释放时,没必要在去申请锁去判断是否要释放{lock_guard<mutex>lock(_mutex);if (_inst){delete _inst;_inst = nullptr;}}}
private:A() {}
private:static A* _inst;class gc{public:~gc(){DelInstance();}};static gc _gc;static mutex _mutex;
};A* A::_inst = nullptr;
A::gc A::_gc;
mutex A::_mutex;
这里说明一点,上面线程安全的单例,只保证在创建和销毁单例时是线程安全的,并不保证在使用该单例的资源时也是线程安全的,需要我们自己去维护。
最简单的懒汉模式
class A
{
public:static A* GetInstance(){// C++11后保证静态变量的创建是线程安全的// 静态变量存放在静态区,只在第一次调用该函数的时候初始化static A inst; return &inst;}A(const A& tmp) = delete;A& operator=(const A& tmp) = delete;
private:A() {}
};