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

ops/2024/9/23 9:43:39/




在这里插入图片描述



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;
}

http://www.ppmy.cn/ops/106356.html

相关文章

数据结构————内核链表

内核链表是Linux内核中广泛使用的一种数据结构&#xff0c;它具有以下特点&#xff1a; 1.双向循环链表&#xff1a;每个节点包含两个指针&#xff0c;一个指向前驱节点&#xff08;prev&#xff09;&#xff0c;另一个指向后继节点&#xff08;next&#xff09;&#xff0c;…

Docker(完整实验版)

目录 一 Docker 1.1 Docker简介 1.1.1 什么是docker&#xff1f; 1.1.2 docker在企业中的应用场景 1.1.3 docker与虚拟化的对比 1.1.4 docker的优势 1.2 部署docker 1.2.1 配置软件仓库 二 Docker的基本操作 2.1 Docker镜像管理 2.1.1 搜索镜像 2.1.2 拉取镜像 2…

25版王道数据结构课后习题详细分析 第八章 8.1 排序的基本概念

一、单项选择题 ———————————————————— ———————————————————— 解析&#xff1a;拓扑排序是将有向图中所有结点排成一个线性序列&#xff0c;虽然也是在内存中进行的&#xff0c;但它不属于我们这里所提到的内部排序范畴&#xff0c;也…

在仿真数据检查器中查看数据

目录 查看记录的数据 从工作区或文件导入数据 查看复数数据 查看字符串数据 查看基于帧的数据 查看基于事件的数据 可以使用仿真数据检查器来可视化您在整个设计过程中生成的数据。您在 Simulink 模型中记录的仿真数据会记录到仿真数据检查器中。您还可以将测试数…

产业生态构建,产业运营服务如何促进上下游协同?

在当今竞争激烈的市场环境中&#xff0c;产业生态的构建成为了企业发展的关键。而产业运营服务作为推动产业生态发展的重要力量&#xff0c;在促进上下游协同方面发挥着至关重要的作用。 首先&#xff0c;产业运营服务通过搭建交流合作平台&#xff0c;促进上下游企业之间的沟通…

Postman注册使用

文章目录 介绍下载安装官网&#xff1a;[Postman API Platform | Sign Up for Free](https://www.postman.com/) 使用过程 介绍 Postman是一款功能强大的网页调试与发送网页HTTP请求的Chrome插件。 Postman原是Chrome浏览器的插件&#xff0c;可以模拟浏览器向后端服务器发起…

k8s集群环境搭建(一主二从--kubeadm安装)

前置条件 版本&#xff1a;CentOS Linux release 7.5.1804 (Core) 内存&#xff1a;2G CPU&#xff1a;2 主机名解析 vim /etc/hosts 192.168.109.100 master 192.168.109.101 node1 192.168.109.102 node2时间同步&#xff0c;这里直接使用chronyd服务从网络同步时间syste…

SpringSecurity Oauth2 - 访问令牌续期

文章目录 1. 访问令牌的续期2. CustomUserDetailsService3. 配置 AuthorizationServerEndpointsConfigurer4. 测试项目 1. 访问令牌的续期 在Spring Security OAuth2中&#xff0c;访问令牌的续期通常是通过使用**刷新令牌&#xff08;Refresh Token&#xff09;**来实现的。当…