C++ : 智能指针的补充和特殊类的设计
目录
前言
定制删除器:
一、请设计一个类,不能被拷贝
二、 请设计一个类,只能在堆上创建对象
三、请设计一个类,只能在栈上创建对象
四、设计一个类不能被继承
五、 请设计一个类,只能创建一个对象(单例模式)
5.1饿汉模式
5.2懒汉模式
总结
前言
定制删除器:
补充上节关于未讲到的智能指针的一些实践中经常出现的问题
int main()
{txf::shared_ptr<A> sp1(new A(1));return 0;
}
这样写是没问题的,他会构造和析构,但如果这样写呢?
int main()
{txf::shared_ptr<A> sp1(new A(1));txf::shared_ptr<A> sp2(new A[10]);return 0;
}
- DeleteArray<A>() : 匿名对象
由于这里的虚构函数是写死的 ,这就会导致,这里有九个对象,还没有释放
甚至如果说这里是malloc
int main()
{txf::shared_ptr<A> sp1(new A(1));txf::shared_ptr<A> sp2(new A[10]);// DeleteArray<A>()匿名对象txf::shared_ptr<A> sp3((A*)malloc(sizeof(A)));return 0;
}
- 我们之前就说过malloc匹配free, new要匹配delete,这就明显是不匹配的,不匹配就会出问题
- 那库里面是怎么解决这个问题的呢?->
- 这个问题也叫做定制删除器 (依靠仿函数来解决)
template<class D>shared_ptr(T* ptr,D del):_ptr(ptr) ,_pcount(new int(1)){}
- 这是库里解决的方法,在构造函数加了一个,模板参数D,他要传D类型的仿函数给他,那我们肯定也要接收这个仿函数,用谁接收呢?那就想到在类里面要定义一个仿函数来接收
template<class D>shared_ptr(T* ptr,D del):_ptr(ptr) ,_pcount(new int(1)),_del(del){}
private:T* _ptr;int* _pcount;D _del;
- 直接增加一个D类型的仿函数,这个D类型能在这里用吗?
- D是构造函数的模板参数,不是类模板的模板参数,不能这么写;我们要在析构函数用到del,是del在构造函数怎么办?我们可以用它来创建一个成员变量,但是他类型是什么?因为D这个是专门在构造函数,才能用的,不是构造函数,不能写D类型,不能明确他的类型,那我们怎样创建一个成员中间?如果说del成员专门写成仿函数类型,那如果传的是函数指针或者lambda怎么办呢?这里我们就要用前面讲的包装器,包装器可以接收他们仨个类型中的任意一个类型
private:T* _ptr;int* _pcount;//D _del //D是构造函数的模板参数,不是类模板的模板参数,不能这么写;//我们要在析构函数用到del,但是del在构造函数怎么办?// 我们可以用它来创建一个成员变量,但是他类型是什么?// 因为D这个是专门在构造函数,才能用的,不是构造函数,不能写D类型,// 不能明确他的类型,那我们怎样创建一个成员中间?// 如果说del成员专门写成仿函数类型,那如果传的是函数指针或者lambda怎么办呢?// 这里我们就要用前面讲的包装器,包装器可以接收他们仨个类型中的任意一个类型function<void(T*)> _del//void : 被调用函数的返回类型// T* :被调用函数的形参//int _pcount 如果是这样,每个对象的_pcount都是独立的
- void : 被调用函数的返回类型
- T* :被调用函数的形参
我们写一个专门处理多个对象的仿函数传给他,这样就能在析构函数中用这个仿函数,
template<class T>
struct DeleteArray
{void operator()(T* ptr){delete[] ptr;}
};
int main()
{txf::shared_ptr<A> sp1(new A(1));//由于这里的虚构函数是写死的 如果这样写呢?txf::shared_ptr<A> sp2(new A[10], DeleteArray<A>());// DeleteArray<A>()匿名对象//那这里有九个对象,还没有释放//甚至如果说这里是malloc txf::shared_ptr<A> sp3((A*)malloc(sizeof(A)), [](A* ptr) {free(ptr); });//我们之前就说过malloc匹配free, new要匹配delete,这就明显是不匹配的,不匹配就会出问题//那库里面是怎么解决这个问题的呢?->定制删除器(仿函数)return 0;
}
~shared_ptr(){if (--(*_pcount) == 0){cout << "delete:" << _ptr << endl;_del(_ptr);delete _pcount;}}
这样是不是看起来没问题了?其实不是的,我们看似是解决了多个对象销毁的问题,但是我们好像又不能处理单个对象销毁的问题,单个对象销毁需要的是delete而不是delete[] , 所以我们要给包装器加个缺省值,
private:T* _ptr;int* _pcount;function<void(T*)> _del = [](T* ptr) {delete ptr; };//要先给一个lambda,要不然这个就专门设计成了处理多个对象的了,反而处理当个对象就不行了,所以还要再写一个拷贝函数//void : 被调用函数的返回类型// T* :被调用函数的形参};
- 要先给一个lambda,要不然这个就专门设计成了处理多个对象的了,反而处理当个对象就不行了,所以还要再写一个构造函数,
一个构造是构造单个对象的一个构造函数是处理多个对象的 ,要处理多个对象,我们就要传反函数给他,这样两个构造函数就能构成重载
shared_ptr(T* ptr):_ptr(ptr), _pcount(new int(1)){}template<class D>shared_ptr(T* ptr,D del):_ptr(ptr) ,_pcount(new int(1)),_del(del){}
以上就是关于定制删除器的问题
一、请设计一个类,不能被拷贝
拷贝只会放生在两个场景中:拷贝构造函数以及赋值运算符重载,因此想要让一个类禁止拷贝,只需让该类不能调用拷贝构造函数以及赋值运算符重载即可
- C++98的做法
class A
{// ...private:A(const A&);A& operator=(const A&);//...};
将拷贝构造函数与赋值运算符重载只声明不定义,并且将其访问权限设置为私有即可
- 设置成私有:如果只声明没有设置成private,用户自己如果在类外定义了,就可以不
能禁止拷贝了 - 只声明不定义:不定义是因为该函数根本不会调用,定义了其实也没有什么意义,不写反而还简单,而且如果定义了就不会防止成员函数内部拷贝了
- C++11的做法
class A
{// ...A(const A&)=delete;A& operator=(const A&)=delete;//...};
- C++11扩展delete的用法,delete除了释放new申请的资源外,如果在默认成员函数后跟上=delete,表示让编译器删除掉该默认成员函数
二、 请设计一个类,只能在堆上创建对象
设计一个类,只能在堆上创建对象 ->什么意思 : 只能显式申请堆来创建对象,禁止掉一切除该方法外创建对象的行为如 :int a;
int main()
{HeapOnly hp1;//创建在栈上static HeapOnly hp2;//创建在静态区上HeapOnly* hp3 = new HeapOnly;//创建在堆上return 0;
}
看到以上的区别后,那该怎么做呢?
由于在栈上和静态区上创建的对象,他们都是能够自动调用析构函数去释放空间的,
而创建在堆上的对象的需要手动的去调用析构函数去释放空间,
所以我们把能自动释放的析构函数给禁掉,这样系统就不允许你在栈上和静态区上创建对象,因为没有析构函数
这样只能在堆上创建对象,但是在堆上创建对象,你也需要去释放对象
而我们把析构函数给私有化之后,我们可以再创建一个公共函数,调用析构函数
私有化限定的是类外面,而不会限定类里面
class HeapOnly
{
public:void Destroy(){cout << " deletec : " << this << endl;delete this;}
private:~HeapOnly(){}
};
那还有没有其他方法呢?有的
刚才是把析构函数给禁掉,我们还可以把构造函数给私有化,
我们在那里面把构造函数给私有化,但是,
你可以创建一个函数来创建对象,当然,这是针对于堆上创建对象
这样你就不能调用构造函数,
你就不能在栈上或者在静态区上创建对象,
只能手动的去调用函数创建对象,
函数里面写成在堆里面创建对象,
所以这样写就只能在堆上创建对象
class HeapOnly
{
public:void Destroy(){cout << " deletec : " << this << endl;delete this;}HeapOnly* CreateObj(){}private:~HeapOnly(){}HeapOnly(){}
};
- static让 CreateObj() 脱离对象,变成“类名::函数”这种普通全局入口 , 让它变成静态区的一个函数,变成一个用类名调用函数的形式,声明类就可以调用,这样不用非得要一个对象才能调用这个函数
- 但是此时面临个问题他不像是析构函数,使用已经创建好的对象去调用一个函数,他这里是需要创建一个对象去给一个指针,但是谁来调用这个函数呢?我们并没有对象,我们还要靠这个函数来创造对象,但是又需要一个对象来调用这个函数这就有点像是先有鸡还是先有蛋的问题
static让 CreateObj() 脱离对象,变成“类名::函数”这种普通全局入口
让它变成静态区的一个函数,变成一个用类名调用函数的形式,声明类就可以调用,
这样不用非得要一个对象才能调用这个函数
static HeapOnly* CreateObj()//static让 CreateObj() 脱离对象,变成“类名::函数”这种普通全局入口
{ //让它变成静态区的一个函数,变成一个用类名调用函数的形式,声明类就可以调用,// 这样不用非得要一个对象才能调用这个函数return new HeapOnly;//但是此时面临个问题//他不像是析构函数,使用已经创建好的对象去调用一个函数,// 他这里是需要创建一个对象去给一个指针,但是谁来调用这个函数呢?// 我们并没有对象,我们还要靠这个函数来创造对象,// 但是又需要一个对象来调用这个函数//这就有点像是先有鸡还是先有蛋的问题
}
此时我们并没有🈲掉他的拷贝构造函数,他依旧能够拷贝构造一个对象,这个创建的对象在栈上
HeapOnly hp4(*hp3);
所以还要把拷贝构造给封一下
private:~HeapOnly(){}HeapOnly(){}HeapOnly(const HeapOnly& hp) = delete;HeapOnly& operator=(const HeapOnly& hp) = delete;
三、请设计一个类,只能在栈上创建对象
请设计一个类,只能在栈上创建对象
什么叫“设计一个类只能在栈上创建对象”
允许 A a; 、 A a(args);
绝对禁止 new A 、 new A[10]
把 operator new 全家桶全部封杀(含全局、数组、 placement 等)
把析构函数留在公有,否则栈对象离开作用域无法自动析构
(可选)把构造函数公有,否则连栈实例也建不了。一句话总结 “只能在栈上创建”=把通往堆的所有后门(operator new)全部焊死
class StackOnly
{
public:static StackOnly CreateObj(){StackOnly st;return st;}private:StackOnly(){ }
};int main()
{StackOnly sp1= StackOnly::CreateObj();return 0;
}
- 🈲掉构造函数,只给一个CreateObj函数,让外界调用它来初始化,
在CreateObj函数里面限制,只从栈上面创造对象,这样,你怎么样都不可能从堆上创建对象,
在CreateObj函数内创建一个对象返回它
但是该方法还是存在问题
StackOnly* sp2 = new StackOnly(sp1);
- 这样拷贝对象建立出来的对象还是在栈上面
new 的时候可以调构造,还可以调拷贝构造
new 表达式” = operator new(拿内存) + 构造函数(初始化),operator new相当于全家桶
我们可以重载一个类的专属的operator new,这样他就不会调用全局,而会调用我们重载的operator new ,让void* operator new(size_t size) = delete; 把通往堆的所有后门(operator new)全部焊死
class StackOnly
{
public:static StackOnly CreateObj(){StackOnly st;return st;}private:StackOnly(){ }void* operator new(size_t size) = delete;//把通往堆的所有后门(operator new)全部焊死
};int main()
{StackOnly sp1= StackOnly::CreateObj();//StackOnly* sp2 = new StackOnly(sp1); //这样就不能拷贝了return 0;
}
四、设计一个类不能被继承
- C++98的方式
class NonInherit
{
public:static NonInherit GetInstance(){return NonInherit();}
private:NonInherit(){}
};
- C++98中构造函数私有化,派生类中调不到基类的构造函数。则无法继承
- C++11的方法
class A final
{// ....
};
- final关键字,final修饰类,表示该类不能被继承
五、 请设计一个类,只能创建一个对象(单例模式)
5.1饿汉模式
单例模式:一个类只能创建一个对象,即单例模式(用这个类定义出来的对象,不管名字是否相同,都是同一个对象) ,该模式可以保证系统中该类只有一个实例,并提供一个
访问它的全局访问点,该实例被所有程序模块共享。比如在某个服务器程序中,该服务器的配置
信息存放在一个文件中,这些配置数据由一个单例对象统一读取,然后服务进程中的其他对象再通过这个单例对象获取这些配置信息,这种方式简化了在复杂环境下的配置管理
class singleton
{
public://步骤二//提供获取单例对象的接口函数static singleton& GetInstance(){return _sinst;}// singleton sinst;//一般单例不用释放void Add(const pair<string, string>& kv){_dict[kv.first] = kv.second;}private://单例模式步骤一//构造函数私有map<string, string> _dict;singleton(){}//步骤三//防拷贝singleton (const singleton& s) = delete;singleton& operator=(const singleton& s) = delete;static singleton _sinst;//声明一个成员变量
};//类的静态成员变量需要在类外初始化singleton singleton:: _sinst;//实例化变量//虽然存储在静态区,但由于在singleton类中声明了,所以它依然能享用到构造函数,而且在静态区,所以只用声明在哪个类域int main()
{//singleton s1;//不能随意创建对象//singleton s2;//if (&s1 == &s2)//{// cout << "a" << endl;//}//else//{// cout << " b";//}//cout << &singleton::GetInstance() << endl;//cout << &singleton::GetInstance() << endl;//cout << &singleton::GetInstance() << endl;//还要把拷贝给禁掉,要不然还能拷贝//singleton copy(singleton::GetInstance());return 0;
}
- 单例模式步骤一 : 构造函数私有
class singleton
{
public:private://单例模式步骤一//构造函数私有singleton(){}
}:
- 步骤二 : 提供获取单例对象的接口函数
class singleton
{
public://步骤二//提供获取单例对象的接口函数static singleton& GetInstance(){return _sinst;}
private://单例模式步骤一//构造函数私有singleton(){}
}:
- 首先,我们把构造函数私有化之后,我们要创建一个静态成员函数来获取单例对象,如果不是静态的,那么要想调用这个函数,就必须要一个对象,而我们就是要构造出一个对象,所以这就死循环了,但是如果把它设置为静态的,它就存储在静态区,这样就可以用类::的方式来调用获取单例对象的函数,而不用非得要创建一个对象,才能调用这个函数,
- 其次,我们正常来说一个类是不可能只实例化出一份对象的,所以我们要想到全局对象或者静态对象在作用域内是只有一份的,
- 所以我们要把对象设置为全局或者是静态的,由于构造函数是私有的,在类外面是无法创建对象的,所以我们要想办法让创建对象可以放到单例类的内部,用于调用构造函数,所以我们要用静态成员变量,在单例类的内部,声明一个静态成员对象,他这个对象是纯属在静态区的,是不属于这个类的,所以在这里不会套娃式的不断在内部创造,而是只会实例化一次,并且存储在静态区,这样这个静态成员对象就可以访问构造函数,但是类的静态成员变量是需要在类外面初始化的,所以我们就在单例内的外部,初始化定义这个静态的单一类对象即可
- 步骤三 : 防拷贝
还要把拷贝给禁掉,要不然还能拷贝
singleton copy(singleton::GetInstance());
完整代码:
class singleton
{
public://步骤二//提供获取单例对象的接口函数static singleton& GetInstance(){return _sinst;}// singleton sinst;//一般单例不用释放void Add(const pair<string, string>& kv){_dict[kv.first] = kv.second;}private://单例模式步骤一//构造函数私有map<string, string> _dict;singleton(){}//步骤三//防拷贝singleton (const singleton& s) = delete;singleton& operator=(const singleton& s) = delete;static singleton _sinst;//声明一个成员变量
};//类的静态成员变量需要在类外初始化singleton singleton:: _sinst;//实例化变量//虽然存储在静态区,但由于在singleton类中声明了,所以它依然能享用到构造函数,而且在静态区,所以只用声明在哪个类域
以上这种模式也叫做饿汉模式,
就是说不管你将来用不用,程序启动时就创建一个唯一的实例对象,在一开始(main函数)之前就创建单例对象
不过他也有缺陷,以下是饿汉模式的缺陷 :
- 如果单例对象要初始化的内容很多,那么就会影响进程的启动速度
- 如果两个单例类,互相有依赖关系,例如:有两个单例类A,B。要求A先创建,B再创建,B的初始化依赖于A。倘若单例类A,B不在同一个文件,那么我们无法保证编译器会去先执行哪一个文件,那么就无法保证A始终是先被创建的,那么B的初始化工作可能就无法运行,这就很坑
下面介绍另一种模式,专门解决饿汉模式的缺陷 : 懒汉模式
5.2懒汉模式
懒汉模式:用来解决饿汉模式的问题 (_sinst 改成指针,这样就不用先创建对象导致启动慢,等到要调用对象的时候,再创建对象
懒汉模式中的静态成员变量,不是一个单例对象 ,而是一个单例对象的指针,我们可以将这个单例对象的指针初始化为nullptr,这样也就不会调用构造函数了,等到真正使用单例对象的时候,判断单例对象的指针是否为空,如果单例对象的指针为空,那么就说明此时单例对象还未调用构造函数创建,此时我们new一个单例对象给单例对象的指针即可,这样就调用了单例类的构造函数进行的初始化单例对象,因为一开始仅仅创建一个空指针没有什么消耗,所以不会影响进程的启动速度, 当第一次真正需要使用单例对象的时候才会进行new调用构造函数实例化
namespace lazy
{class Singleton{public:static Singleton& GetInstace(){if (_sinst == nullptr){_sinst = new Singleton();}return *_sinst;}private:Singleton(){}Singleton(const Singleton& _sinst) = delete;Singleton& operator=(const Singleton& _sinst) = delete;static Singleton* _sinst;};Singleton* Singleton::_sinst = nullptr;
}
总结
以上就是今天要讲的内容,本文仅仅简单介绍了几个特殊类,还有相关的单例模式,说实话在写的时候,小编并未完全弄清楚单例模式以及相关的优化场景,所以就只是简单的介绍了一下,小编要下去再好好弄清楚,弄清楚后再来补充