前言
在实际的应用场景中,不免会有一些特殊的设计要求存在。在C++中,由于三种不同的域,以及地址空间的大小或者申请方式不同,就衍生出了一些特殊的设计类方法。
何为特殊呢?即区别于普通类的设计。
上一篇C++笔记传送门~
【C++】通过哈希表实现map和set_柒海啦的博客-CSDN博客_c++ set 哈希
复习复习C++的类与对象吧~
『吉他与孤独与蓝色星球』
目录
1.设计一个类,不可被拷贝
我们知道,在类的默认六个成员函数中(C++11中新增两个,一共八个:构造函数,拷贝构造函数,析构函数,赋值重载函数,取地址重载函数、const取地址重载函数,移动拷贝函数,移动赋值函数),拷贝相关的就是拷贝构造和赋值重载相关的函数了。
我们要设计的类不可被拷贝的话,那我们只要让此类的拷贝构造和赋值重载失效不就好了嘛。
C++98方式:
(由于此时没有给默认成员函数删除的关键字,所以C++98和C++11是不同的方式)
因为在类外访问类中的方法都是访问的public区域的。所以,我们将两个拷贝相关的函数放在私有域外界不就可以访问不到了嘛:
class NoCopy
{
public:
NoCopy(int flag = 0)
:_flag(flag)
{}
private:
int _flag;
NoCopy(const NoCopy& e)
:_flag(e._flag)
{}
NoCopy& operator= (const NoCopy& e)
{
_flag = e._flag;
}
};
C++11方式:
由于C++11新增了一个关键字多用,即delete关键字,可以让编译器不再默认生成此默认成员函数,而是删除掉。
我们删除掉拷贝构造和赋值重载这两个默认成员函数即可。
class NoCopy
{
public:
NoCopy(int flag = 0)
:_flag(flag)
{}
NoCopy(const NoCopy&) = delete;
NoCopy& operator= (const NoCopy&) = delete;
private:
int _flag;
};
另外,有的小伙伴会不会担心移动赋值和移动构造会不会出问题呢?你可以针对上面两种情况利用move函数转为右值试一试,是不会出现问题的。原因就是在C++98的实现方法里面,已经声明实现了拷贝构造、赋值重载函数了,根据移动的默认生成规则,就不会生成。C++11里面delete后也类似于显示写了,也不会。
移动构造和移动赋值相关默认生成可以详细的看这篇博客哦~
【C++】c++11学习-常用特性总结_柒海啦的博客-CSDN博客
2.设计一个类,只可在堆上创建
首先,先来区分一下在栈上创建和堆上创建的区别。栈上创建也就是在函数里直接声明定义的;而堆上创建即就是通过malloc或者new等函数或者关键字进行申请的,在C++中需要手动释放的空间。(程序地址空间可以参考这一篇文章:【C++】内存管理到用new申请堆内存_柒海啦的博客-CSDN博客_c++申请堆空间)
现在,我们想要只能在堆上申请,也就是必须杜绝栈上创建。那么我们就可以先观察类的实例化对象在栈上的表现:创建对象时会调用构造函数(在向堆申请的时候new也要调用构造函数),生命周期结束(栈帧结束)会自动调用析构函数(堆上需要显示调用delete方可调用析构函数)。
根据上述表现我们可以找到一个区别:那就是结束的时候栈上的对象是自动调用析构函数,而堆上则是要手动去调用delete才会去调用对象的析构函数的。利用这一个特点,我们就可以用来做手脚。
限制析构函数:
我们把析构函数放入私有域,那么在栈上创建的对象在生命周期结束后就无法访问析构函数,自然无法在栈上创建。但是堆上虽然可以申请,那么堆上对象如何去释放呢?类中可以提供一个接口,在类里释放即可:
class OnlyHeep1
{
public:
void Delete()
{
delete this;
}
private:
~OnlyHeep1()
{}
int _a;
};
限制构造函数:
虽然将两者之间的不同点找到可以,但是我们也可以一开始就一棍子打死,限制构造函数(放在私有域),那么这样一来都无法在类外实例化出此类的对象了。所以类里提供一个静态函数(即不需要实例化对象就可以访问的函数),专门返回new出来的对象即可:
但是需要注意,虽然此接口提供,但是如果不把拷贝构造和赋值重载函数禁用了的话还是可以在栈上创建的,所以还需要对这两个默认成员函数特殊处理:
class OnlyHeep2
{
public:
static OnlyHeep2* New()
{
return new OnlyHeep2;
}
// 注意细节,拷贝函数相关的就要删掉或者私有化哦
OnlyHeep2(const OnlyHeep2&) = delete;
OnlyHeep2& operator=(const OnlyHeep2&) = delete;
private:
OnlyHeep2()
:_a(0)
{}
int _a;
};
测试代码:
void test1()
{
OnlyHeep1* a = new OnlyHeep1;
a->Delete();
OnlyHeep2* b = OnlyHeep2::New();
delete b;
}
3.设计一个类,只可在栈上创建
和上面那个问题反了过来。由于栈上创建对象没有比堆上多余的创建条件,那么我们也只能限制构造函数,只提供一个返回栈上对象的接口即可。但是注意此时返回的是一个临时对象,所以不可以禁用拷贝构造函数。
这样就会引来一个新的问题,如果不被禁用的话,堆上上创建就可以借此调用拷贝构造函数。所以我们就需要将new操作符进行重载,删除掉即可(也可以放入私有域屏蔽掉即可)。
// 构造函数私有化- 但是注意不可禁止拷贝 ,因为返回栈上对象的话由于生命周期问题,所以只能返回拷贝对象。
class OnlyStack
{
public:
static OnlyStack New()
{
return OnlyStack();
}
void* operator new(size_t) = delete;
private:
OnlyStack()
{}
int _a = 0;
};
但是注意上面虽然可以防住堆上创建,但是防不住static创建哦,注意静态变量可不属于栈上的空间哦。所以上面代码是防不住static变量创建的。
4.设计一个类,不可被继承
构造函数私有化:
我们知道继承的话,子类是要调用父类的构造函数来对父类域的成员进行初始化的。(详细可以参考这篇文章:【C++】继承- 赋值兼容转换、虚基表_柒海啦的博客-CSDN博客)那么我们将此类的构造函数私有化,这样继承到子类子类对父类的构造函数就是不可见状态,不可见那么也就无法初始化父类域中的成员,就会出现问题。
class NotInheritable2 // C++98
{
public:
private:
NotInheritable2()
:_a(0)
{}
int _a;
};
此时发生继承就会存在问题。当然上面的方法存在瑕疵,意思就是我也无法实例化出此类的对象了。
final关键字:
C++11中,增加了关键字final在类后表示此类为最终类,即不可被继承。但是可以正常实例化对象:
class NotInheritable1 final // C++11
{
public:
private:
int _a;
};
5.单例模式
存在一种说法叫做设计模式。也就是说在平时写程序中使用较多的一类写法,就会被设计为一种模式:
软件设计模式(Design pattern),又称设计模式,是一套被反复使用、多数人知晓的、经过分类编目的、代码设计经验的总结。使用设计模式是为了可重用代码、让代码更容易被他人理解、保证代码可靠性、程序的重用性。
详细请看百度百科:软件设计模式_百度百科 (baidu.com)
在C++中,迭代器和适配器就是一种设计模式。当然,现在的单例模式就是设计模式中的一种,适用于在一个进程中唯一存在的实例对象。
针对在一个程序中只能实例化出一个对象的类,我们有如下两种设计模式:饿汉和懒汉模式。
饿汉模式:
顾名思义,饿汉,就是想不顾一切的吃东西。那么它就要率先的去干满足它的事情。
所以在C++单例模式里的意思就是,在主程序开始前就创建出一个实例对象。由于是在主程序一开始前创造出一个对象,那么一开始就要定义此对象的成员。因为只能实例化一次对象,所以应该将构造函数私有化(既然构造函数私有化,也就会出现上面的拷贝问题,所以也要将这两个禁用掉),提供一个返回此对象的接口,因为是在没有实例化的情况下,返回对象自然就是一个静态成员,方法同样也是静态方法。初始化在外面初始化即可。
// 1饿汉模式 - main开始前就创建对象
class HungryModel
{
public:
static HungryModel* GetInstance()
{
return _pinst;
}
private:
HungryModel() // 构造函数私有化
{}
// 防止拷贝
HungryModel(const HungryModel&) = delete;
HungryModel& operator= (const HungryModel&) = delete;
static HungryModel* _pinst;
};
HungryModel* HungryModel::_pinst = new HungryModel;
饿汉模式优缺点:
优点:简单、没有线程安全问题。
缺点:1、一个程序中有多个单例,并且有先后创建初始化顺序要求时,饿汉无法控制。(比如:程序中两个单例类 A 和 B,要求A先创建初始化,B在创建初始化。 - 静态变量是不可确定的(多个文件的情况下)和静动态库)。2、饿汉单例类,初始化时任务多,影响程序启动速度。
懒汉模式:
顾名思义,懒汉就是很懒,只有叫到他的时候他才会动一动回应你。
所以在C++单例模式里的意思就是可在使用接口的时候才进行实例化,但是只能实例化一次。之后就不可进行实例化出对象了。那么实际上也就是从上面的饿汉模式做出一些变化(返回堆上的对象,构造函数进行私有化),在其余不变的条件下,不在外面进行初始化,而是在使用向外提供的接口第一次的时候返回new对象,之后就不可返回对象即可。
第一次的判断根据成员是否为null进行判断即可:
(下面代码由于博主还没有学习多线程,所以多线程相关安全问题还暂时没有考虑)
// 2懒汉模式 - 第一次使用对象时创建实例
class LazyModel
{
public:
static LazyModel* GetInstance()
{
if (_pinst == nullptr)
{
_pinst = new LazyModel;
}
return _pinst;
}
private:
LazyModel() // 还是要构造函数私有化
{}
// 防止拷贝
LazyModel(const LazyModel&) = delete;
LazyModel& operator= (const LazyModel&) = delete;
static LazyModel* _pinst;
};
LazyModel* LazyModel::_pinst = nullptr;
懒汉模式优缺点:
优点:1.控制顺序。2.不影响启动速度。
缺点:1.相对复杂。2.线程安全问题要处理好。
未完待续~~~