【C++ 第二十一章】特殊类的设计(学习思路)

2024-09-06 00:44

本文主要是介绍【C++ 第二十一章】特殊类的设计(学习思路),希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!




在这里插入图片描述



1.请设计一个类,不能被拷贝

设计思路

拷贝只会使用在两个场景中:拷贝构造函数以及赋值运算符重载,因此想要让一个类禁止拷贝,只需让该类不能调用拷贝构造函数以及赋值运算符重载即可。

C++98 的做法

将拷贝构造函数与赋值运算符重载只声明不定义,并且将其访问权限设置为私有即可。(不定义:则拷贝操作无法实际的实现;设置成私有:避免公有被调用出来实现)

class A
{
private:A(const A&);A& operator=(const A&);
};

原因:

  1. 设置成私有:如果只声明没有设置成private,用户自己如果在类外定义了,就可以不
    能禁止拷贝了
  2. 只声明不定义:不定义是因为该函数根本不会调用,定义了其实也没有什么意义,不写反而还简单,而且如果定义了就不会防止成员函数内部拷贝了。



C++11 的做法

C++11 扩展 delete 的用法,delete 除了释放 new 申请的资源外,如果在默认成员函数后加上 =delete,表示让编译器删除掉该默认成员函数。

class A
{A(const A&) = delete;A& operator=(const A&) = delete;
};



2.请设计一个类,只能在上创建对象


方法一:构造函数私有化

  1. 构造、拷贝和赋值私有化:将类的构造函数私有,拷贝构造和赋值声明成私有。防止别人调用拷贝在栈上生成对象。或者将构造私有化,将拷贝和赋值 delete 禁用
  2. 同一提供对外接口:提供一个静态的成员函数,在该静态成员函数中完成堆对象的创建

这个有点封装的味道了,将一些功能封装起来,自己提供对外接口,就可以控制外界可以使用的功能(控制权限)

为什么要设置成静态函数:

关于为什么对外功能接口要设置成静态函数?

思路:

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++ 第二十一章】特殊类的设计(学习思路)的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/1140519

相关文章

HarmonyOS学习(七)——UI(五)常用布局总结

自适应布局 1.1、线性布局(LinearLayout) 通过线性容器Row和Column实现线性布局。Column容器内的子组件按照垂直方向排列,Row组件中的子组件按照水平方向排列。 属性说明space通过space参数设置主轴上子组件的间距,达到各子组件在排列上的等间距效果alignItems设置子组件在交叉轴上的对齐方式,且在各类尺寸屏幕上表现一致,其中交叉轴为垂直时,取值为Vert

Ilya-AI分享的他在OpenAI学习到的15个提示工程技巧

Ilya(不是本人,claude AI)在社交媒体上分享了他在OpenAI学习到的15个Prompt撰写技巧。 以下是详细的内容: 提示精确化:在编写提示时,力求表达清晰准确。清楚地阐述任务需求和概念定义至关重要。例:不用"分析文本",而用"判断这段话的情感倾向:积极、消极还是中性"。 快速迭代:善于快速连续调整提示。熟练的提示工程师能够灵活地进行多轮优化。例:从"总结文章"到"用

不懂推荐算法也能设计推荐系统

本文以商业化应用推荐为例,告诉我们不懂推荐算法的产品,也能从产品侧出发, 设计出一款不错的推荐系统。 相信很多新手产品,看到算法二字,多是懵圈的。 什么排序算法、最短路径等都是相对传统的算法(注:传统是指科班出身的产品都会接触过)。但对于推荐算法,多数产品对着网上搜到的资源,都会无从下手。特别当某些推荐算法 和 “AI”扯上关系后,更是加大了理解的难度。 但,不了解推荐算法,就无法做推荐系

【前端学习】AntV G6-08 深入图形与图形分组、自定义节点、节点动画(下)

【课程链接】 AntV G6:深入图形与图形分组、自定义节点、节点动画(下)_哔哩哔哩_bilibili 本章十吾老师讲解了一个复杂的自定义节点中,应该怎样去计算和绘制图形,如何给一个图形制作不间断的动画,以及在鼠标事件之后产生动画。(有点难,需要好好理解) <!DOCTYPE html><html><head><meta charset="UTF-8"><title>06

学习hash总结

2014/1/29/   最近刚开始学hash,名字很陌生,但是hash的思想却很熟悉,以前早就做过此类的题,但是不知道这就是hash思想而已,说白了hash就是一个映射,往往灵活利用数组的下标来实现算法,hash的作用:1、判重;2、统计次数;

【C++ Primer Plus习题】13.4

大家好,这里是国中之林! ❥前些天发现了一个巨牛的人工智能学习网站,通俗易懂,风趣幽默,忍不住分享一下给大家。点击跳转到网站。有兴趣的可以点点进去看看← 问题: 解答: main.cpp #include <iostream>#include "port.h"int main() {Port p1;Port p2("Abc", "Bcc", 30);std::cout <<

C++包装器

包装器 在 C++ 中,“包装器”通常指的是一种设计模式或编程技巧,用于封装其他代码或对象,使其更易于使用、管理或扩展。包装器的概念在编程中非常普遍,可以用于函数、类、库等多个方面。下面是几个常见的 “包装器” 类型: 1. 函数包装器 函数包装器用于封装一个或多个函数,使其接口更统一或更便于调用。例如,std::function 是一个通用的函数包装器,它可以存储任意可调用对象(函数、函数

C++11第三弹:lambda表达式 | 新的类功能 | 模板的可变参数

🌈个人主页: 南桥几晴秋 🌈C++专栏: 南桥谈C++ 🌈C语言专栏: C语言学习系列 🌈Linux学习专栏: 南桥谈Linux 🌈数据结构学习专栏: 数据结构杂谈 🌈数据库学习专栏: 南桥谈MySQL 🌈Qt学习专栏: 南桥谈Qt 🌈菜鸡代码练习: 练习随想记录 🌈git学习: 南桥谈Git 🌈🌈🌈🌈🌈🌈🌈🌈🌈🌈🌈🌈🌈�

【C++】_list常用方法解析及模拟实现

相信自己的力量,只要对自己始终保持信心,尽自己最大努力去完成任何事,就算事情最终结果是失败了,努力了也不留遗憾。💓💓💓 目录   ✨说在前面 🍋知识点一:什么是list? •🌰1.list的定义 •🌰2.list的基本特性 •🌰3.常用接口介绍 🍋知识点二:list常用接口 •🌰1.默认成员函数 🔥构造函数(⭐) 🔥析构函数 •🌰2.list对象

06 C++Lambda表达式

lambda表达式的定义 没有显式模版形参的lambda表达式 [捕获] 前属性 (形参列表) 说明符 异常 后属性 尾随类型 约束 {函数体} 有显式模版形参的lambda表达式 [捕获] <模版形参> 模版约束 前属性 (形参列表) 说明符 异常 后属性 尾随类型 约束 {函数体} 含义 捕获:包含零个或者多个捕获符的逗号分隔列表 模板形参:用于泛型lambda提供个模板形参的名