总结:特殊类创建流程 = 私有构造函数+静态成员
创建一个只能在堆上创建对象的类
方法:先把构造函数私有,这样就不能让用户随意创建对象了。然后我们再用静态成员来进行操作。
class HeapOnly
{
public:
static HeapOnly* createObj()
{
return new HeapOnly();
}
private:
HeapOnly() {}
HeapOnly(const HeapOnly&) = delete;
};
int main()
{
HeapOnly* foo = HeapOnly::createObj();
HeapOnly cp(*foo);
}
讲几个点:
1.HeapOnly(const HeapOnly&) = delete;是C++11的语法,表示这个函数不能被使用。且写函数声明即可,不用写实现。
2.这里为什么要禁用掉拷贝构造呢?可能有这种写法:
HeapOnly cp(*foo);这样就被钻漏洞了。
3.赋值运算符重载不需要禁用。没办法用这个来钻漏洞。
创建一个只能在栈上创建对象的类
还是先私有构造,然后用静态成员返回对象。
class StackOnly
{
public:
static StackOnly createObj()
{
return StackOnly();
}
private:
StackOnly() {};
};
int main()
{
StackOnly foo(StackOnly::createObj());
}
讲几个点:
1.这里的createObj的返回值不能传引用,因为StackOnly()是局部对象。
2.由于要拷贝构造一个返回值对象,因此不能禁用掉拷贝构造
创建一个不允许拷贝对象的类
把拷贝构造和赋值运算符重载私有即可(或者这两个函数写成delete)
class NoCopy
{
public:
NoCopy() {};
NoCopy(const NoCopy&) = delete;
NoCopy& operator=(NoCopy) = delete;
};
创建一个不允许被继承的类
之前讲继承的时候讲过final关键字,就是不允许被继承。
class NoExtends final
{
public:
NoExtends() {};
};
还有一种写法是把构造函数私有了,这样就不能被子类调用父类的构造函数来初始化子类对象了。
但是这样写其实不太好,因为要在类外面创建父类对象就必须写两个静态成员,一个是可以在栈上创建对象的createObj,一个是可以在堆上创建对象的createObj
单例模式
只能创建一个对象的类,叫单例模式。
创建步骤如下:
1.先私有构造函数,再把拷贝构造和赋值禁掉(禁不禁不影响最后结果,只影响效率,因为同一个对象进行拷贝是无用的)
2.写一个getInstance返回对象
3.单例对象的指针都是静态成员,只有静态的才能使对象只有一份。
饿汉单例
指程序开始main执行之前就创建单例对象(因为单例对象是静态成员,main之前就初始化了)。要用的时候直接用。
饿汉单例不好,因为如果单例对象太大,迟迟进入不了main函数。程序员无法判定是服务出bug了还是单例加载太慢了。
代码:
class Singleton
{
public:
static Singleton* getInstance()
{
return ptr;
}
private:
Singleton() {};
static Singleton* ptr;
};
Singleton* Singleton:: ptr = new Singleton;
懒汉单例
懒汉单例指你要使用这个单例模式的时候再创建单例对象。
优点:可以让程序跑起来再创建单例,不会让服务卡死。
代码:
class Singleton
{
public:
static Singleton* getinstance()
{
if (ptr == nullptr)
{
mtx.lock();
if (ptr == nullptr)
{
ptr = new Singleton();
}
mtx.unlock();
}
return ptr;
}
private:
Singleton() {};
Singleton(const Singleton&) = delete;
Singleton& operator=(const Singleton&) = delete;
static Singleton* ptr;
static mutex mtx;
};
Singleton* Singleton::ptr = nullptr;
mutex Singleton::mtx;
上面是单例的核心代码。私有构造,禁用拷贝和赋值都是常规操作。懒汉的最大特点就是double-check + mutex
1.为什么需要加锁呢?
因为这有线程安全问题。
其实由于对象只有一个,这个对象你可以看成临界资源。两个线程对临界资源进行写操作肯定要加锁。
2.至于为什么要double-check?
原因是要提升效率。我们可以看到,如果不double-check。在第一次创建好单例对象之后,第二次再次进入,由于此时ptr一定不为空,此次判断是没有意义的。但是却要为这次无用的判断加锁。
我们知道加锁,要从用户态中断陷入(trap指令)到内核态,成本很高,会影响效率,因此我们要进行double-check。
如果先判断了ptr不为空, 线程就不要加锁了,直接返回单例对象的地址即可。如果判断了ptr为空, 证明有可能单例对象没有创建好,就去申请锁再次判断一下。
double-check的思想可以广泛用于只需要加锁一次的情景!(如果这个资源访问时要频繁加锁,就不能用)
3.加锁在linux下是用POSIX库的,但是由于windows下不支持这个库,你要使用就要下载这个库。因此C++为了支持跨平台,C++11用oop思想封装了一个线程库。
封装原理用到了条件编译:
#ifdef _WIN32
// windows 提供多线程api
#else
// linux pthread
#endif // _WIN32
4.可以为懒汉模式加上一个GC(garbage collection)。但这不是必须的,由于单例模式的对象不析构也没什么关系,一般经常使用的对象才会用单例模式,像httpServer这样的对象。
写一个内部类GC,当GC的生命周期结束的时候,它会析构掉单例对象。
class GC
{
public:
~GC()
{
if (ptr)
{
delete ptr;
ptr = nullptr;
}
}
};
总体代码:
class Singleton
{
public:
static Singleton* getinstance()
{
if (ptr == nullptr)
{
mtx.lock();
if (ptr == nullptr)
{
ptr = new Singleton();
}
mtx.unlock();
}
return ptr;
}
private:
Singleton() {};
Singleton(const Singleton&) = delete;
Singleton& operator=(const Singleton&) = delete;
class GC
{
public:
~GC()
{
if (ptr)
{
delete ptr;
ptr = nullptr;
}
}
};
static Singleton* ptr;
static mutex mtx;
static GC gc;
};
Singleton* Singleton::ptr = nullptr;
mutex Singleton::mtx;
Singleton::GC Singleton::gc;
讲一下:
其实为了对象唯一,因此我们可以看到基本所有成员变量都是static的。
所以以后我们写代码想写只有唯一对象的时候,也可以用static成员变量。(其实就和写算法题的全局变量没什么区别)。