C++相关概念和易错语法(31)(特殊类的设计、new和delete底层调用分析)

本文主要是介绍C++相关概念和易错语法(31)(特殊类的设计、new和delete底层调用分析),希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

特殊类的设计

在实践过程中,我们难免会接触到一些需要实现特定功能的类。像之前提过的unique_ptr就是直接delete拷贝构造和赋值函数。下面会分享一些常见的特殊类的实现

1、防拷贝和防赋值

通过封死拷贝构造和赋值函数来保护对象里面内容不被复制。如果对象里面的内容是指针,对析构次数有严格要求的话(如unique_ptr)就通常采用这种处理方法。

注意拷贝构造和移动拷贝为一体,赋值重载和移动赋值为一体,两两为一组,若其中之一被delete掉了,另一个就算满足自动生成条件(析构、移动、拷贝赋值未手动生成)也没有办法自动生成。

注意封析构不会影响拷贝和赋值的自动生成

所以我们实现防拷贝时,只需要封死拷贝和赋值那一块,而移动拷贝和赋值就不需要我们过多担心了,它们也不会自动生成。因为我们是想要防止拷贝,所以直接delete比起私有化函数更干净利落。

2、只能在栈区创建对象(new和delete底层分析)

如果我们想要只允许在栈区创建对象的话,就要想办法封死堆区和静态区创建的方式。

我先说静态区,静态区无法被封死,因为它的创建、拷贝模式和栈区的几乎没有区别,你可以封死构造,自己写一个构造(栈区值返回,调用构造或移动),但是拦不住静态区也利用构造或移动来拷贝构造。

想要封死堆区创建的方式从操作来讲非常简单,因为我们知道new会去调用operator new,delete会去调用operator delete,所以我们在中间拦截就能实现这个禁用new和delete

但是对new和delete有一定了解的人马上就能找到漏洞,直接跳过中间的拦截,用malloc、calloc、realloc都可以实现在堆区开辟空间

malloc、calloc、realloc是无法拦截的,为什么?以及删除operator new和operator delete为什么会禁止掉new?我们需要深入new和delete的调用规则才能一探究竟。

我们在new和delete混用分析就说过,new主要分为以下阶段:new -> operator new -> malloc,delete阶段分为delete -> operator delete -> free,其中operator new和operator delete都是全局函数,但其实更准确的是在operator new里malloc,在operator delete里面free,operator new之后调用构造函数,而析构函数是在operator delete之前就调用了的。

对于大多数情况,operator new和operator delete都是在全局进行调用的,调用operator new之前就会计算好应该开辟空间的大小

如在这里num接收的就是应当malloc的字节大小num。

关键点来了,C++规定operator new和operator delete可以在类里面自己实现。虽然operator这个标志词让人一下就联想到了重载这个概念,但全局函数和类里面的成员函数首先在作用域上就不相同,其次operator new和operator delete对函数名、参数、返回值都有严格要求,并不能被定义为重载,一般我们可以理解为重定义或者替换。

当在类里面重定义这两个函数时,new这个类或者delete这个类实例化的对象时,当进行到调用operator new或调用operator delete这一步时,都会直接去调用类里面的替换的函数,而不会去调用全局的。

看看下面的代码会如何打印

class A
{
public:A(){cout << "A()" << endl;}~A(){cout << "~A()" << endl;}void* operator new(size_t num){cout << "void* operator new(size_t num)" << endl;return malloc(sizeof(A));}void operator delete(void* p){cout << "void operator delete(void* p)" << endl;free(p);}	};int main()
{A* a = new A;delete a;return 0;
}

结果是

这个结果也可以进一步验证我所说的new和delete的执行步骤,注意malloc(num)中num的具体含义是指开辟的空间大小以及重定义的函数的形式必须完全统一。

如果我们显式删除了operator new和operator delete,就算我们没有显式定义这两个函数,编译器也会解读为我们不希望new这个类的时候调用operator new这个函数,也不会去调用全局的函数,所以在开辟空间这里就卡死了。

这里可以理解为一层特殊处理,但也很符合我们的逻辑,如果你想要调用全局的operator new那就什么都不写,想自己实现就自己写,编译器也会调用(注意格式功能正确),不想自己写也不想调用全局的就直接delete掉这个函数。

还有人知道new[ ]和delete[ ],这两个也是从new和delete中衍生出来的

new[ ] -> operator new[ ] (malloc包含在内),delete[ ]阶段分为delete[ ] -> operator delete[ ](free包含在内)

我们一定要注意operator new[ ]和operator new,operator delete和operator delete[ ]完全独立,没有任何关系

operator new[ ]专门处理开辟数组的情况, 不会去调用operator new。注意size_t num依然意味着要开辟空间的大小,编译器会提前计算好,将真真实实需要开辟的字节数传过来作为num。

在混用分析那里我特地强调了operator new和operator new[ ]的区别,operator new[ ]调用前就会计算要开辟空间的大小(包括多开辟的),会多开辟空间用于存放数组元素个数的信息,返回的时候会将返回的地址二次处理,通过检测new[ ]的元素个数,记录并进行地址的错位返回。只有delete[ ]会在调用operator delete[ ]前进行矫正,将矫正的地址赋给p。

根据上面的规则,结合下面的代码,仔细体会并试图回答为什么不能用operator new[ ]替代new[ ]?为什么不能用operator delete[ ]替代delete[ ]?


class A
{
public:A(){cout << "A()" << endl;}~A(){cout << "~A()" << endl;}void* operator new(size_t num){cout << "void* operator new(size_t num) : num == " << num << endl;return malloc(num);}void operator delete(void* p){cout << "void operator delete(void* p)" << endl;free(p);}void* operator new[](size_t num){cout << "void* operator new[](size_t num) : num == " << num << endl;return malloc(num);}void operator delete[](void* p){cout << "void operator delete[](void* p)" << endl;free(p);}};int main()
{cout << "sizeof(A) == " << sizeof(A) << endl << endl;A* a1 = new A[1];delete[] a1;	cout << endl;A* a2 = new A;delete a2;return 0;
}

结果是

这个大小9是编译器自己的处理方式,不用纠结数字如何来的。

由于new和new[ ]、delete和delete[ ]调用的函数不一样,所以当我们删除operator new时,operator new[ ]并不会受到任何影响,依然遵循优先重定义函数其次全局函数的规则,所以我们如果要封死的话,要注意4个函数都delete,不要只写两个

我们只能尽最大努力,控制new、new[ ]、delete、delete[ ]的行为,但malloc、calloc、realloc不支持重定义这套规则,也没办法delete掉这些函数(显式delete的会被认为是成员函数,仍会调用全局的,编译器不会像operator new那样解释)。因此,从某种意义上说,只能在栈区定义的类难以实现,但我们可以在很多层面作出限制,毕竟使用C++一般都不会使用malloc这种C语言语法了,一定程度上起到了规范作用。

3、只能在堆区创建对象

(1)私有化构造函数

只有在堆区创建对象的话,我们要先私有化构造函数,只能以我们的规定的方式来定义函数,这也需要借助静态成员函数来实现。注意我们要分清什么使用该私有化函数,什么时候该delete函数。私有化函数是防止外部调用,但内部可以调用,delete就是完全不可调用。在这里只能私有化,即只能用规定方式创建对象,创建时内部调用构造。

这里我们需要仔细体会,静态成员函数属于整个类而不是某个对象,因此静态成员函数只需要我们指定类域即可,它再调用构造函数就能实现对象的初始化。而如果我们写的是非静态成员函数,那么这就陷入了先有鸡还是先有蛋的问题。非静态成员函数本来就需要先实例化出对象才能调用的,但这个函数的功能又是实例化出对象。

我们上面的实现有个漏洞,即拷贝和移动可以轻松绕过限制,我们在实现特殊类时一定不能忽略拷贝、赋值这两个函数可能带来的漏洞。

因此我们需要针对我们的需求delete拷贝构造或是私有化,对外提供接口

赋值其实在这里是没有必要封的,因为赋值的本质是进行值的覆盖,是对该空间的值的重写。当我们把构造、拷贝函数私有化了之后,我们就只能按照对外的接口来创建空间,显然赋值重载只是将掌管堆区空间的指针进行转移,并不会导致在其他区域开辟了新空间。

(2)私有化析构函数

这是一个很巧妙的办法,利用了非堆区对象出生命周期自动调用析构函数这个特征来禁止调用。

堆区对象的特点就是不主动释放就不析构,最后程序结束时不调用析构直接回收空间。

但上面这个操作明显导致了内存泄漏,因此当我们不用堆区空间,就利用接口来释放空间,这比栈区静态区灵活多了。

很多人这个时候回想:我可以先在栈区构造,用了之后显式析构,这样析构私有化就失效了啊,但事实真的如此吗?

这就陷入了编译时逻辑和运行时逻辑的漏洞,编译器在编译的时候可不会管Destroy()什么意思,只要在栈区实例化出对象,它就会去找析构函数,访问不了就报错,所以虽然运行时逻辑没有问题,但编译都报错了,运行时逻辑还有意义吗?

这里只是特殊类的一些例子,用到的知识已经综合化了,这也可以帮助我们加深语法的印象,拔高我们的思维。

这篇关于C++相关概念和易错语法(31)(特殊类的设计、new和delete底层调用分析)的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



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

相关文章

MySQL 内存使用率常用分析语句

《MySQL内存使用率常用分析语句》用户整理了MySQL内存占用过高的分析方法,涵盖操作系统层确认及数据库层bufferpool、内存模块差值、线程状态、performance_schema性能数据... 目录一、 OS层二、 DB层1. 全局情况2. 内存占js用详情最近连续遇到mysql内存占用过高导致

Mysql中设计数据表的过程解析

《Mysql中设计数据表的过程解析》数据库约束通过NOTNULL、UNIQUE、DEFAULT、主键和外键等规则保障数据完整性,自动校验数据,减少人工错误,提升数据一致性和业务逻辑严谨性,本文介绍My... 目录1.引言2.NOT NULL——制定某列不可以存储NULL值2.UNIQUE——保证某一列的每一

C++11范围for初始化列表auto decltype详解

《C++11范围for初始化列表autodecltype详解》C++11引入auto类型推导、decltype类型推断、统一列表初始化、范围for循环及智能指针,提升代码简洁性、类型安全与资源管理效... 目录C++11新特性1. 自动类型推导auto1.1 基本语法2. decltype3. 列表初始化3

深度解析Nginx日志分析与499状态码问题解决

《深度解析Nginx日志分析与499状态码问题解决》在Web服务器运维和性能优化过程中,Nginx日志是排查问题的重要依据,本文将围绕Nginx日志分析、499状态码的成因、排查方法及解决方案展开讨论... 目录前言1. Nginx日志基础1.1 Nginx日志存放位置1.2 Nginx日志格式2. 499

C++11右值引用与Lambda表达式的使用

《C++11右值引用与Lambda表达式的使用》C++11引入右值引用,实现移动语义提升性能,支持资源转移与完美转发;同时引入Lambda表达式,简化匿名函数定义,通过捕获列表和参数列表灵活处理变量... 目录C++11新特性右值引用和移动语义左值 / 右值常见的左值和右值移动语义移动构造函数移动复制运算符

C++中detach的作用、使用场景及注意事项

《C++中detach的作用、使用场景及注意事项》关于C++中的detach,它主要涉及多线程编程中的线程管理,理解detach的作用、使用场景以及注意事项,对于写出高效、安全的多线程程序至关重要,下... 目录一、什么是join()?它的作用是什么?类比一下:二、join()的作用总结三、join()怎么

在MySQL中实现冷热数据分离的方法及使用场景底层原理解析

《在MySQL中实现冷热数据分离的方法及使用场景底层原理解析》MySQL冷热数据分离通过分表/分区策略、数据归档和索引优化,将频繁访问的热数据与冷数据分开存储,提升查询效率并降低存储成本,适用于高并发... 目录实现冷热数据分离1. 分表策略2. 使用分区表3. 数据归档与迁移在mysql中实现冷热数据分

Olingo分析和实践之EDM 辅助序列化器详解(最佳实践)

《Olingo分析和实践之EDM辅助序列化器详解(最佳实践)》EDM辅助序列化器是ApacheOlingoOData框架中无需完整EDM模型的智能序列化工具,通过运行时类型推断实现灵活数据转换,适用... 目录概念与定义什么是 EDM 辅助序列化器?核心概念设计目标核心特点1. EDM 信息可选2. 智能类

Olingo分析和实践之OData框架核心组件初始化(关键步骤)

《Olingo分析和实践之OData框架核心组件初始化(关键步骤)》ODataSpringBootService通过初始化OData实例和服务元数据,构建框架核心能力与数据模型结构,实现序列化、URI... 目录概述第一步:OData实例创建1.1 OData.newInstance() 详细分析1.1.1

Olingo分析和实践之ODataImpl详细分析(重要方法详解)

《Olingo分析和实践之ODataImpl详细分析(重要方法详解)》ODataImpl.java是ApacheOlingoOData框架的核心工厂类,负责创建序列化器、反序列化器和处理器等组件,... 目录概述主要职责类结构与继承关系核心功能分析1. 序列化器管理2. 反序列化器管理3. 处理器管理重要方