本文主要是介绍【C++ 第二十一章】特殊类的设计(学习思路),希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!
1.请设计一个类,不能被拷贝
设计思路
拷贝只会使用在两个场景中:拷贝构造函数以及赋值运算符重载,因此想要让一个类禁止拷贝,只需让该类不能调用拷贝构造函数以及赋值运算符重载即可。
C++98 的做法
将拷贝构造函数与赋值运算符重载只声明不定义,并且将其访问权限设置为私有即可。(不定义:则拷贝操作无法实际的实现;设置成私有:避免公有被调用出来实现)
class A
{
private:A(const A&);A& operator=(const A&);
};
原因:
- 设置成私有:如果只声明没有设置成private,用户自己如果在类外定义了,就可以不
能禁止拷贝了 - 只声明不定义:不定义是因为该函数根本不会调用,定义了其实也没有什么意义,不写反而还简单,而且如果定义了就不会防止成员函数内部拷贝了。
C++11 的做法
C++11 扩展 delete 的用法,delete 除了释放 new 申请的资源外,如果在默认成员函数后加上 =delete,表示让编译器删除掉该默认成员函数。
class A
{A(const A&) = delete;A& operator=(const A&) = delete;
};
2.请设计一个类,只能在堆上创建对象
方法一:构造函数私有化
- 构造、拷贝和赋值私有化:将类的构造函数私有,拷贝构造和赋值声明成私有。防止别人调用拷贝在栈上生成对象。或者将构造私有化,将拷贝和赋值 delete 禁用
- 同一提供对外接口:提供一个静态的成员函数,在该静态成员函数中完成堆对象的创建
这个有点封装的味道了,将一些功能封装起来,自己提供对外接口,就可以控制外界可以使用的功能(控制权限)
为什么要设置成静态函数:
关于为什么对外功能接口要设置成静态函数?
思路:
1、首先,我们将构造函数私有化,拷贝构造和赋值 delete 禁用掉,外界就不可以调用这几个函数在栈上构造一个对象
2、其次,该对外功能接口函数 也是成员函数,调用一个成员函数需要一个对象来调用,但是我们这里都没有创建对象,正等着该功能函数来创建对象呢?何来一个对象?
3、这里就产生:先有鸡,还是先有蛋的问题
4、因此就需要避免使用通过对象调用的方式
:可以设置成 静态成员,在外部通过类域指定调用(这时静态成员的特性,就无需通过对象调用)
class HeapOnly
{
public:// 设置成静态函数:static HeapOnly* CreateObj() {return new HeapOnly;}// 将拷贝与赋值重载都使用 delete 禁用掉HeapOnly(const HeapOnly&) = delete;HeapOnly& operator=(const HeapOnly&) = delete;
private:// 将构造函数设置成私有:避免外部调用,在栈上构造对象HeapOnly() {};
};int main() {HeapOnly* p = HeapOnly::CreateObj(); // 通过类域调用类的静态成员return 0;
}
方法二:析构函数私有化
注释都解释清楚了
// 析构函数私有化
class HeapOnly
{
public:void Destroy() {delete this;}
private:// 将析构函数设置成私有~HeapOnly() {};
};int main() {HeapOnly obj; // 报错:创建一个类对象,销毁时会自动调用析构,但是这里调用不了(因为析构函数被"禁用"了),因此也不允许这个对象被创建出来HeapOnly* p = new HeapOnly(); // 不报错:new 出来的对象,不会自动调用析构,需要手动 deletedelete p; //报错:调用不了析构 p->Destroy(); // 通过类中的功能接口销毁对象return 0;
}
3.请设计一个类,只能在栈上创建对象
这个比限制只能在堆上,还要麻烦一些些
实现第一步:构造函数私有化
class StackOnly
{
public:// 对外功能接口:该函数可以调用私有成员,构造函数,创建对象,返回匿名对象static StackOnly CreateObj() {return StackOnly();}private:// 将构造函数设置成私有:避免外部通过 new,在堆上创建对象StackOnly() {};
};int main() {StackOnly obj1 = StackOnly::CreateObj();StackOnly* p_obj2 = new StackOnly(obj1); return 0;
}
禁掉了 构造函数,还可以走拷贝构造的路:通过CreateObj() 函数,先创建一个对象出来,再 拷贝+new 生成一个新对象
因此,还要完善
实现第二步:拷贝与赋值用 delete 禁用掉
class StackOnly
{
public:// 对外功能接口:该函数可以调用私有成员,构造函数,创建对象,返回匿名对象static StackOnly CreateObj() {return StackOnly();}// 将拷贝与赋值重载都使用 delete 禁用掉StackOnly(const StackOnly&) = delete;
private:// 将构造函数设置成私有:避免外部通过 new,在堆上创建对象StackOnly() {};
};int main() {StackOnly obj1 = StackOnly::CreateObj();//StackOnly* p_obj2 = new StackOnly(obj1); return 0;
}
但是!
将 拷贝构造禁用掉:会导致 CreateObj 函数失效,因为该成员函数返回的是局部对象,需要拷贝生成临时对象,而拷贝构造失效,导致生成失败
因此,还要完善
实现第三步:operator new 用 delete 禁用掉
可以尝试从 new 的本质入手:new = 全局函数 operator new(malloc+抛异常)+ 构造
我们若自己显式实现 operator new ,则 new 优先使用我们自己的
因此可以在这里将 new 的 operator new 禁掉,使得 new 无法调用
// 只能在栈上创建对象
class StackOnly
{
public:// 该函数可以调用私有成员:构造函数,创建对象,返回匿名对象static StackOnly CreateObj() {return StackOnly();}// 可以使用/*void* operator new(size_t size) {return malloc(size*sizeof(StackOnly));}*/void* operator new(size_t size) = delete;void operator delete(void* p) = delete;
private:// 将构造函数设置成私有:避免外部通过 new,在堆上创建对象StackOnly() {};
};int main() {StackOnly obj1 = StackOnly::CreateObj();StackOnly* p_obj2 = new StackOnly(obj1); // 报错return 0;
}
但是问题又回来了:
只要没有将 拷贝构造禁用掉,还是可以通过拷贝构造创建一个在静态区的对象
StackOnly obj1 = StackOnly::CreateObj();static StackOnly obj3(obj1); // 不报错
同时,还可以通过 移动构造 创建静态区的对象
StackOnly obj1 = StackOnly::CreateObj();static StackOnly obj4(move(obj1));
这里为什么创建静态区的对象? 仅仅是将栈区对象区别开
实现最终大法:直接使用返回对象进行操作
既然我们的目的是设计一个只能在栈上对象的类,
我们直接从这里思考,我们先将拷贝、赋值、移动构造私有化 或 delete 禁用。
既然直接将 CreateObj() 函数返回的匿名对象拷贝给新对象会触发拷贝或移动构造
StackOnly obj1 = StackOnly::CreateObj(); // 这里会触发拷贝或移动构造
干脆别拷贝给新对象,而是直接使用这个 匿名对象进行操作
(其实这个方法有点取巧,但是不也是达到了题目要求吗?😎)
int main() {StackOnly::CreateObj().Print(); // 直接使用该返回对象进行操作//StackOnly obj1 = StackOnly::CreateObj(); // 会触发拷贝或移动构造//StackOnly* p_obj2 = new StackOnly(obj1); // 报错:operator new 和 拷贝构造 不能用了//StackOnly obj3(obj1); // 报错:拷贝构造 不能用了//static StackOnly obj4(move(*obj1)); // 报错:拷贝构造 和 移动构造 不能用了//static StackOnly obj2(obj1);return 0;
}
4.请设计一个类,不能被继承
C++98 方式
C++98 中构造函数私有化,派生类中调不到基类的构造函数,则无法继承
class A
{
private:A() {};
};
C++11 方式
使用 final 关键字
final 关键字,final修饰类,表示该类不能被继承。
class A final
{
private:A() {};
};
5.请设计一个类,只能创建一个对象(单例模式)
5.1 设计模式:
设计模式(Design Pattern)是一套被反复使用、多数人知晓的、经过分类的、代码设计经验的总结。为什么会产生设计模式这样的东西呢?就像人类历史发展会产生兵法。最开始部落之间打仗时都是人拼人的对砍。后来春秋战国时期,七国之间经常打仗,就发现打仗也是有套路的,后来孙子就总结出了《孙子兵法》。孙子兵法也是类似。
使用设计模式的目的:为了代码可重用性、让代码更容易被他人理解、保证代码可靠性。 设计模式使代码编写真正工程化;设计模式是软件工程的基石脉络,如同大厦的结构一样。
我们之前其实已经接触过一些设计模式了,比如迭代器模式、适配器/配接器模式
下面我们要学习的是设计模式中的 单例模式
5.2 单例模式:
一个类只能创建一个对象,即单例模式,该模式可以保证系统中该类只有一个实例,并提供一个访问它的全局访问点,该实例被所有程序模块共享。比如在某个服务器程序中,该服务器的配置信息存放在一个文件中,这些配置数据由一个单例对象统一读取,然后服务进程中的其他对象再通过这个单例对象获取这些配置信息,这种方式简化了在复杂环境下的配置管理。
单例模式有两种实现模式:饿汉模式 和 懒汉模式
5.3 饿汉模式
就是说不管你将来用不用,程序启动时就创建一个唯一的实例对象。(即程序一开始就实例化一个该类对象给你了)
为什么叫做饿汉?:饿汉就好比你放学饿着肚子回家,在你回家前妈妈就已经准备好饭菜给你了
设计思路:
1、构造函数私有化。
2、将 拷贝构造、移动构造、赋值重载 封死:确保我们只有 GetInstance() 可以放出去唯一一个实例化对象。
3、创建一个自己这个类的静态成员对象:一个类的静态成员只能创建一个,而且static数据会在程序启动时创建好(刚好符合 饿汉模式 的理念)
// 单例模式:一个类只能创建一个对象,即单例模式,该模式可以保证系统中该类只有一个实例,并提供一个访问它的全局访问点,该实例被所有程序模块共享。
class InfoMgr
{
public:// 只有这个函数可以向外提供唯一一个实例化对象static InfoMgr& GetInstance() {return _ins;}void Print() {cout << _ip << '\n';cout << _port << '\n';cout << _buffSize << '\n';}// 将 拷贝构造、移动构造、赋值重载 封死:确保我们只有 GetInstance() 可以放出去唯一一个实例化对象InfoMgr(const InfoMgr&) = delete;InfoMgr(InfoMgr&&) = delete;InfoMgr& operator=(const InfoMgr&) = delete;
private:// 将构造函数私有化:外部无法直接构造该类对象InfoMgr() {cout << "InfoMgr()" << '\n';}private:string _ip = "127.0.0.1";int _port = 80;size_t _buffSize = 1024 * 1024;// 这不能说是在类中创建一个自己,否则就套娃乱透了// 静态成员不存储在一个类对象里面static InfoMgr _ins;
};
InfoMgr InfoMgr::_ins;int main() {// 调试程序可以发现:调试还没有开始走就已经打印 "InfoMgr()" ,说明 在main函数程序执行前,对象就已经构造好了(这是因为该对象是 static,全局域)InfoMgr::GetInstance().Print();return 0;
}
饿汉模式的缺陷
如果这个单例对象在多线程高并发环境下频繁使用,性能要求较高,那么显然使用饿汉模式来避免资源竞争,提高响应速度更好。
由于饿汉模式的对象在 main 函数前就被创建,所以它不存在线程安全问题,但是它也存在一些缺点:
1、多个饿汉模式的单例,某个对象初始化内容较多(读文件),会导致程序启动慢
2、A 和 B 两个饿汉,对象初始化存在依赖关系,要求A先初始化,B再初始化,饿汉无法保证其初始化顺序
5.4 懒汉模式
如果单例对象构造十分耗时或者占用很多资源,比如加载插件啊, 初始化网络连接啊,读取文件啊等等,而有可能该对象程序运行时不会用到,那么也要在程序一开始就进行初始化,就会导致程序启动时非常的缓慢,
这些就是 饿汉模式的缺陷。 所以这种情况使用懒汉模式(延迟加载)更好。
饿汉模式的特点就是先创建好对象,这也容易引发一些问题
懒汉模式 可以解决这个问题:不先创建对象,而是需要时再创建对象
这样就可以按需创建,即你要吃的东西,我不提前给你准备好,只会在你需要吃时再做,这就是懒汉
懒汉模式写法一:定义类对象指针
在 main 函数中,程序一般都会按顺序执行(不像懒汉模式中全局变量执行顺序不定),而且按需调用即可,这样也可以解决 懒汉模式中的 依赖关系的先后问题
// 懒汉模式
class InfoMgr
{
public:// 若对象指针为 nullptr,就给你 new 一个对象// 若不为空,就返回该对象给你static InfoMgr& GetInstance() {if (_pIns == nullptr) {_pIns = new InfoMgr();}return *_pIns;}void Print() {cout << _ip << '\n';cout << _port << '\n';cout << _buffSize << '\n';}// 将 拷贝构造、移动构造、赋值重载 封死:确保我们只有 GetInstance() 可以放出去唯一一个实例化对象InfoMgr(const InfoMgr&) = delete;InfoMgr(InfoMgr&&) = delete;InfoMgr& operator=(const InfoMgr&) = delete;
private:// 将构造函数私有化:外部无法直接构造该类对象InfoMgr() {cout << "InfoMgr()" << '\n';}private:string _ip = "127.0.0.1";int _port = 80;size_t _buffSize = 1024 * 1024;// 这不能说是在类中创建一个自己,否则就套娃乱透了// 静态成员不存储在一个类对象里面static InfoMgr* _pIns;
};
InfoMgr* InfoMgr::_pIns;int main() {// 调试程序可以发现:类里面的那个对象是在 GetInstance() 函数调用时创建的InfoMgr::GetInstance().Print();return 0;
}
有没有发现 懒汉模式存在一个问题:该模式中的对象是 new 出来的,就需要手动 delete 释放
而我们上面的类中,默认的析构只会将 _pIns 这个指针置空,而不会 delete 指向的资源,相当于 ”浅析构“,会造成内存泄漏
实际上,只有单例对象内存泄漏问题并没有这么严重
如果想要delete,这里有个很好的方法:定义内部类对象,当本项目程序结束后,该对象销毁会调用自己的析构函数,我们就可以在析构函数里面设置 delete 相关程序
这其实是一种解决问题的 思想:自己类无法做到的事,可以定义内部类,利用类的特性间接完成一些功能
// 定义一个内部类:用于析构单例对象
class DestroyIns
{public:~DestroyIns() {if (InfoMgr::_pIns != nullptr) {delete InfoMgr::_pIns;cout << "delete InfoMgr::_pIns;" << '\n';}}
};InfoMgr::DestroyIns desIns; // 全局对象:程序结束后会销毁,自动调用析构函数,则会执行析构函数里面 delete 的程序
应用进去
// 懒汉模式
class InfoMgr
{
public:// 若对象指针为 nullptr,就给你 new 一个对象// 若不为空,就返回该对象给你static InfoMgr& GetInstance() {if (_pIns == nullptr) {_pIns = new InfoMgr();}return *_pIns;}void Print() {cout << _ip << '\n';cout << _port << '\n';cout << _buffSize << '\n';}// 将 拷贝构造、移动构造、赋值重载 封死:确保我们只有 GetInstance() 可以放出去唯一一个实例化对象InfoMgr(const InfoMgr&) = delete;InfoMgr(InfoMgr&&) = delete;InfoMgr& operator=(const InfoMgr&) = delete;// 定义一个内部类:用于析构单例对象class DestroyIns{public:~DestroyIns() {if (InfoMgr::_pIns != nullptr) {delete InfoMgr::_pIns;cout << "delete InfoMgr::_pIns;" << '\n';}}};private:// 将构造函数私有化:外部无法直接构造该类对象InfoMgr() {cout << "InfoMgr()" << '\n';}private:string _ip = "127.0.0.1";int _port = 80;size_t _buffSize = 1024 * 1024;// 这不能说是在类中创建一个自己,否则就套娃乱透了// 静态成员不存储在一个类对象里面static InfoMgr* _pIns;
};
InfoMgr* InfoMgr::_pIns;
InfoMgr::DestroyIns desIns;int main() {// 调试程序可以发现:类里面的那个对象是在 GetInstance() 函数调用时创建的InfoMgr::GetInstance().Print();return 0;
}
懒汉模式二:利用 局部静态变量(推荐写这个)
局部静态变量
函数内的静态变量也称为局部静态变量,其作用域只限于函数内部,别的函数不能访问。
局部静态变量存储在全局数据区,只允许初始化一次,但它的生命周期和全局变量一样,自它们被定义时就一直存在,直到程序结束时才会被销毁。不会随着函数的结束而被销毁,会一直存在
特性:只允许初始化一次
作用域:在函数内部
存储区:全局静态区
生命周期:全局,不会随着函数的结束而被销毁,程序结束时才会被销毁
由于局部静态变量的特性,也可以达到 第一次调用 GetInstance() 函数,就定义一个类对象,其他时候调用不会重新定义,只允许定义一次 的目的
同时,程序结束时会该对象也会自动销毁,不用再定义内部类对齐处理了!!
这个写法简单明了,相比前一种写法更加巧妙
// 懒汉模式二:利用 局部静态变量
class InfoMgr
{
public:// 若对象指针为 nullptr,就给你 new 一个对象// 若不为空,就返回该对象给你static InfoMgr& GetInstance() {static InfoMgr pIns;return pIns;}void Print() {cout << _ip << '\n';cout << _port << '\n';cout << _buffSize << '\n';}// 将 拷贝构造、移动构造、赋值重载 封死:确保我们只有 GetInstance() 可以放出去唯一一个实例化对象InfoMgr(const InfoMgr&) = delete;InfoMgr(InfoMgr&&) = delete;InfoMgr& operator=(const InfoMgr&) = delete;private:// 将构造函数私有化:外部无法直接构造该类对象InfoMgr() {cout << "InfoMgr()" << '\n';}private:string _ip = "127.0.0.1";int _port = 80;size_t _buffSize = 1024 * 1024;
};int main() {// 调试程序可以发现:类里面的那个对象是在 GetInstance() 函数调用时创建的InfoMgr::GetInstance().Print();return 0;
}
这篇关于【C++ 第二十一章】特殊类的设计(学习思路)的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!