文章目录
特殊类设计
1.请设计一个类,只能在堆上创建对象
我想大家对于这个问题第一步想到的肯定是将构造函数私有
class HeapOnly
{
private:
HeapOnly()
{}
};
int main()
{
HeapOnly p1;
HeapOnly* p2 = new HeapOnly;
return 0;
}
运行结果:
我们将构造函数私有之后,虽然不能在栈上创建对象,但是同样也不能在堆上创建对象了,这种就属于杀敌一千自损八百
那我们有没有什么办法可以让我们不能再栈上创建对象,但是可以在堆上创建对象呢?
答案是有的。
class HeapOnly
{
public:
HeapOnly* CreatObj()
{
return new HeapOnly;
}
private:
HeapOnly()
{}
};
大家可能认为这样子就可以了,其实不是的,请大家仔细的想一想:调用一个类的普通成员函数是不是需要对象去调用,那我们在堆上申请的对象又正好需要调用这个函数来生成,可是我们选择都没有对象我们如何去调用这个函数呢?所以这就纯纯是先有鸡还是先有蛋的问题了
为了解决这个问题,我们可以将这个成员函数设置成静态的。
class HeapOnly
{
public:
static HeapOnly* CreatObj()
{
return new HeapOnly;
}
private:
HeapOnly()
{}
};
int main()
{
HeapOnly p1;
HeapOnly* p2 = HeapOnly::CreatObj();
return 0;
}
运行结果:
可以看到这一次我们就实现了我们的目标。
那我再来问大家一个问题:这个代码还有没有什么不足的地方?
答案是有的——拷贝构造还可以用
class HeapOnly
{
public:
static HeapOnly* CreatObj()
{
return new HeapOnly;
}
private:
HeapOnly()
{}
};
int main()
{
HeapOnly p1;
HeapOnly* p2 = HeapOnly::CreatObj();
HeapOnly p3 = (*p2);
return 0;
}
运行结果:
我们可以看到,虽然我们这里不能够直接的在栈上创建对象,但是我们可以在堆上申请一个对象之后再调用一次拷贝构造然后间接的就在栈上申请对象了。
因此下面我们要做的就是将拷贝构造给禁掉或者让它私有
class HeapOnly
{
public:
static HeapOnly* CreatObj()
{
return new HeapOnly;
}
private:
HeapOnly()
{}
//C++98 --防拷贝
//拷贝构造私有,并且只声明不实现(实现也是可以的,但是没人用所以没必要实现)
//HeapOnly(const HeapOnly&);
//C++11
HeapOnly(const HeapOnly&) = delete;
};
int main()
{
HeapOnly p1;
HeapOnly* p2 = HeapOnly::CreatObj();
HeapOnly p3 = (*p2);
return 0;
}
运行结果:
如此一来便完成了我们的任务——设计一个类,只能在堆上创建对象
2.请设计一个类,只能在栈上创建对象
有了上面只能在堆上创建对象的经验之后,下面我们再来设计这个只能在栈上创建的对象的类就会轻车熟路了。
方法一: 同上将构造函数私有化,然后设计静态方法创建对象返回即可。
class StackOnly
{
public:
static StackOnly CreateObj()
{
return StackOnly();
}
private:
//构造函数私有
StackOnly()
{}
};
int main()
{
StackOnly p1 = StackOnly::CreateObj();
StackOnly* p2 = new StackOnly;
static StackOnly p3;
return 0;
}
运行结果:
方法二: 屏蔽new
因为new在底层调用void* operator new(size_t size)函数,只需将该函数屏蔽掉即可。注意:也要防止定位new
class StackOnly
{
public:
StackOnly()
{}
private:
void* operator new(size_t size);
void operator delete(void* p);
};
int main()
{
StackOnly p1;
StackOnly* p2 = new StackOnly;
//缺陷:还可以在静态区上创建对象
static StackOnly p3;
return 0;
}
运行结果:
如此一来便完成了我们的目标,这两种方法任选一种即可,但是第二种方法有一点点小缺陷。
3.请设计一个类,不能被拷贝
我们需要明白的是拷贝只会放生在两个场景中:拷贝构造函数以及赋值运算符重载,因此想要让一个类禁止拷贝,只需让该类不能调用拷贝构造函数以及赋值运算符重载即可。
-
C++98
将拷贝构造函数与赋值运算符重载只声明不定义,并且将其访问权限设置为私有即可。
class CopyBan { //... private: //C++98 CopyBan(const CopyBan&); CopyBan& operator=(const CopyBan&); };
原因:
- 设置成私有: 如果只声明但是没有设置成private,用户可以自己在类外定义,这样就不能禁止拷贝了
- 只声明不定义: 不定义是因为该函数根本不会调用,所以定义了也没有声明意义,不写反而还简单,而且如果定义了就不会防止成员函数内部拷贝了。
-
C++11
C++11扩展delete的用法,delete除了释放new申请的资源外,
如果在默认成员函数后跟上=delete,表示让编译器删除掉该默认成员函数。
class CopyBan { //... private: //C++11 CopyBan(const CopyBan&) = delete; CopyBan& operator=(const CopyBan&) = delete; };
4.请设计一个类,不能被继承
大家对于这个有什么想法呢?
大家可能会这么想—— 一个派生类如果要实例化对象,首先要去调用父类的构造函数,然后再去调用自己的构造函数,既然如此那我直接把父类的构造函数私有,这样你子类实例不出对象你基本上就废了。但是我父类自己又想实例对象,那我可以定义一个静态的成员函数去帮助我自己实例化对象。
- C++98方式
//请设计一个类,不能被继承
// C++98中构造函数私有化,派生类中调不到基类的构造函数。则无法继承
class NonInherit
{
public:
static NonInherit GetInstance()
{
return NonInherit();
}
private:
NonInherit()
{}
};
这种方法是可以的,但是C++98中这个不能被继承的方式不够彻底,实际是可以继承的,只不过它限制的是子类继承后不能实例化对象
- C++11方式
final关键字,final修饰类,表示该类不能被继承。
class A final
{
//...
};
class B :public A
{
};
运行结果:
5.请设计一个类,只能创建一个对象(单例模式)
设计模式
设计模式(Design Pattern)是一套被反复使用、多数人知晓的、经过分类的、代码设计经验的总结。
为什么会产生设计模式这样的东西呢?
- 就像人类历史发展会产生兵法。最开始部落之间打仗时都是人拼人的对砍。后来春秋战国时期,七国之间经常打仗,就发现打仗也是有套路的,后来孙子就总结出了《孙子兵法》。孙子兵法也是类似。
使用设计模式的目的:
- 为了代码可重用性、让代码更容易被他人理解、保证代码可靠性。
- 设计模式使代码编写真正工程化;设计模式是软件工程的基石脉络,如同大厦的结构一样。
单例模式
一个类只能创建一个对象,即单例模式,该模式可以保证系统中该类只有一个实例,并提供一个访问它的全局访问点,该实例被所有程序模块共享。
比如在某个服务器程序中,该服务器的配置信息存放在一个文件中,这些配置数据由一个单例对象统一读取,然后服务进程中的其他对象再通过这个单例对象获取这些配置信息,这种方式简化了在复杂环境下的配置管理。
单例模式有两种实现模式:饿汉模式
与懒汉模式
饿汉模式
就是说不管你将来用不用,程序启动时就创建一个唯一的实例对象。
对于单例模式我们首先需要解决一个问题:
如果保证全局(一个进程中)只有一个唯一实例对象
经过上面的学习我们就可以知道,要想保证全局只有一个唯一实例对象我们需要做以下两步操作
- 构造函数私有定义。拷贝构造和赋值防拷贝禁掉
- 提供一个GetInstance获取单例对象
// 饿汉模式 -- 程序开始main执行之前就创建单例对象
// 提供一个静态指向单例对象的成员指针,初始化时new一个对象给它
class Singleton
{
public:
static Singleton* GetInstance()
{
return _inst;
}
void Print()
{
cout << "Print()" << _a << endl;
}
private:
Singleton()
:_a(0)
{}
C++98 防拷贝
//Singleton(const Singleton&);
//Singleton& operator=(const Singleton&);
//C++11 防拷贝
Singleton(const Singleton&) = delete;
Singleton& operator=(const Singleton&) = delete;
int _a;
static Singleton* _inst;
};
// 在程序入口之前就完成单例对象的初始化
Singleton* Singleton::_inst = new Singleton;
int main()
{
cout << Singleton::GetInstance() << endl;
cout << Singleton::GetInstance() << endl;
cout << Singleton::GetInstance() << endl;
Singleton::GetInstance()->Print();
return 0;
}
运行结果:
饿汉模式适用场景:
- 如果这个单例对象在多线程高并发环境下频繁使用,性能要求较高,那么显然使用饿汉模式来避免资源竞争,提高响应速度更好。
饿汉模式不适用场景:
- 如果单例类构造函数中,要做很多配置初始化工作,导致程序启动非常慢,这个时候我们使用饿汉就不合适了,这时我们应该使用下面的
懒汉模式
懒汉模式
如果单例对象构造十分耗时或者占用很多资源,比如加载插件啊, 初始化网络连接啊,读取文件啊等等,而有可能该对象程序运行时不会用到,那么也要在程序一开始就进行初始化,就会导致程序启动时非常的缓慢。 所以这种情况使用懒汉模式(延迟加载
)更好。
大家了解了懒汉模式之后可能会这么去写懒汉:
//懒汉模式
class Singleton
{
public:
static Singleton* GetInstance()
{
if (_inst == nullptr)
{
_inst = new Singleton;
}
return _inst;
}
void Print()
{
cout << "Print()" << _a << endl;
}
private:
Singleton()
:_a(0)
{}
C++98 防拷贝
//Singleton(const Singleton&);
//Singleton& operator=(const Singleton&);
//C++11 防拷贝
Singleton(const Singleton&) = delete;
Singleton& operator=(const Singleton&) = delete;
int _a;
static Singleton* _inst;
};
Singleton* Singleton::_inst = nullptr;
但是这么写是会存在一些问题的。
在多线程场景下会存在线程安全的问题,因为我们的懒汉式要用的时候才会去调用GetInstance函数
再去创建那个唯一的对象。
假如说现在有两个线程,分别是线程A与线程B,然后我线程A与线程B都调用了GetInstance函数,线程A与线程B都进入了if条件判断里面,此时我们线程A如果先执行实例化对象的代码,然后返回。因为我们的线程B不知道线程A已经实例化了唯一对象,此时线程B再去调用实例化对象的代码,就会导致前面的唯一对象的地址被覆盖掉,线程A的数据会丢失,并且会有内存泄漏的风险,更严重的是此时已经产生了两个实例对象,违反了单例模式的设计思想。
那我们应如何解决上面的问题呢?我们需要进行加锁
但是不是单单的加锁,而是双检查加锁
//懒汉模式
class Singleton
{
public:
static Singleton* GetInstance()
{
// 保护第一次需要加锁,后面都不需要加锁的场景,可以使用双检查加锁
// 特点:第一次加锁,后面不加锁,保护线程安全,同时提高了效率
if (_inst == nullptr)
{
_mtx.lock();
if (_inst == nullptr)
{
_inst = new Singleton;
}
_mtx.unlock();
}
return _inst;
}
static void DelInstance()
{
_mtx.lock();
if (_inst)
{
delete _inst;
_inst = nullptr;
}
_mtx.unlock();
}
void Print()
{
cout << "Print()" << _a << endl;
}
private:
Singleton()
:_a(0)
{
// 假设单例类构造函数中,要做很多配置初始化
}
~Singleton()
{
// 程序结束时,需要处理一下,持久化保存一些数据
}
Singleton(const Singleton&) = delete;
Singleton& operator=(const Singleton&) = delete;
// 实现一个内嵌垃圾回收类
class CGarbo {
public:
~CGarbo()
{
if (_inst)
{
delete _inst;
_inst = nullptr;
}
}
};
int _a;
static Singleton* _inst;
static std::mutex _mtx;
static CGarbo _gc;
};
Singleton* Singleton::_inst = nullptr;
std::mutex Singleton::_mtx;
Singleton::CGarbo Singleton::_gc;
给大家解释一下为什么是双检查加锁
我们需要知道我们想加锁的地方只是第一次 _inst为空的时候,我们想实例化一个唯一对象时才需要加锁,
但是如果到了后面我们已经实例化出了对象,但是我们照样加锁了之后再去判断 _inst是否为空,然后在多线程场景下频繁的加锁解锁会导致效率降低,并且也不推荐这样做。
而我们的双检查加锁里面的第二个if检查就是我先进行加锁,然后进行判断第一次_inst是否为空,然后实例化一个唯一对象,等到下一次某个线程再调用GetInstance的时候,我们的第一个if检查发现inst不为空,就不会进入第一个if条件判断的里面,因此也就不存在多线程场景下频繁加锁解锁,保护线程安全,同时提高了效率。
其他版本的懒汉模式: 局部静态对象
class Singleton
{
public:
static Singleton* GetInstance()
{
static Singleton inst;
return &inst;
}
void Print()
{
cout << "Print()" << _a << endl;
}
private:
Singleton()
:_a(0)
{
// 假设单例类构造函数中,要做很多配置初始化
}
~Singleton()
{
// 程序结束时,需要处理一下,持久化保存一些数据
}
Singleton(const Singleton&) = delete;
Singleton& operator=(const Singleton&) = delete;
int _a;
};
这一种版本的懒汉模式我们了解一下即可。
缺点:
- 单例对象在静态区,如果单例对象太大,不太好,不合适。
- 想主动释放单例对象,无法主动控制。
饿汉模式与懒汉模式对比
饿汉
优点:
- 简单
缺点:
-
如果单例对象构造函数工作比较多,会导致程序启动慢,迟迟进不了入口main函数
-
如果有多个单例对象,他们之间有初始化依赖关系,饿汉模式也会有问题。
比如有A和B两个单例类,要求A单例先初始化,B必须在A之后初始化。那么饿汉无法保证。这种场景下面用懒汉就可以,懒汉可以先调用A::GetInstance(),再调用B::GetInstance().
懒汉
优点:
- 解决上面饿汉的缺点。因为他是第一次调用GetInstance时创建初始化单例对象
缺点:
- 相对饿汉,复杂一点点。