当前位置: 首页 > news >正文

盘一盘C++的类型描述符(二)

先序文章请看
盘一盘C++的类型描述符(一)

稍微组合一下的复杂类型

数组指针类型的数组类型

数组的指针类型我们已经了解了,那么,以这种类型作为元素的数组类型怎么搞?

using type = int (*)[3];
// 元素类型是数组指针的数组类型
type arr[4]; // 直接写是什么样呢?

为了说明问题,这里用了using语句,后面章节会详细介绍。对于arr,它应该是什么类型呢?其实应该是:

int (*arr[4])[3];

同理,从里向外,最内层的括号里出现了*[4],表示arr是拥有4个元素的数组类型,并且数组元素为指针(没有括号分辨内外的情况下,切记,左为外,右为内)。而外层的int [3]就表示指针的解类型,是一个数组类型。所以我们说arr是一个数组,其元素是指针类型,指针的解类型为另一种数组类型。

以此为引子我们还能拓展出很多其他更BT的组合,不过在继续发散之前,我们需要先来看另一个问题。

没有变量名,单说类型

如果我问,上一节示例中的arr是什么类型呢?很简单,去掉变量名,剩下的部分就是它的「类型描述符」,也就是int (*[4])[3]。正推很容易,但是反过来可能就让人有点摸不着头脑,比如说,请问下面类型表示什么含义:

int (**)[3];
int *(*)[3];
int **[3];

当去掉变量名后,这个单纯的类型描述符就显得很诡异,尤其是这个小括号,搞得乍一看跟什么奇怪的密文一样。遇到这种情况其实也不用慌,我们需要「脑补」一个变量名,再就好解释了。

变量名添加的位置一定是最内层,我们找到最内层的部分,分别把变量名补进去,再来解释它的类型就好:

int (**p1)[3];
int *(*p2)[3];
int **p3[3];

所以,p1是指针类型,其类型为另一个指针类型,其解类型为拥有3个int元素的数组类型。或者说,p1是「(数组的指针) 的指针」类型(数组的二级指针)。

p2是指针类型,其解类型是一个数组,含有3个指针类型元素,其解类型是int。或者说,p2是「(指针数组) 的指针」类型(数组的一级指针)。

p3是数组类型,拥有3个指针类型的元素,其解类型是另一个指针类型,其解类型是int。或者说,p3是「(指针的指针) 的数组」类型(二级指针的数组)。

typedef与using

类型复杂了以后可读性会直线下降,所以这时候更加推荐在合适的层级进行类型重命名。typedef是从C语言继承来的语法,但它有一个缺点,就是新的类型名在类型描述符中处于「变量名」的位置,看起来会有些奇怪,比如说:

typedef int type1[3]; // type1其实就是int[3]类型
typedef int (*type2)[2]; // type2其实就是int (*)[2]类型
typedef int *(*type3[4])[5]; // type3其实就是int *(*[4])[5]类型

using则是完全把新的类型名从原本的类型说明符中提取出来了,看上去会更直观一些:

using type1 = int [3];
using type2 = int (*)[2];
using type3 = int *(*[4])[5];

需要注意的是,两种用法的区别仅仅在于描述方式,其效果是完全等价的。实际使用中不用太过纠结,选哪一种都行。

多维数组类型

前面我们介绍了多级指针,其本质并没有几级,而是说指针的解类型还是一个指针罢了。

同理,所谓的「多维数组」并不会真的给出一个多维空间, 其本质是「数组的数组」,或者说「元素为数组类型的数组类型」。

举例来说:

int arr1[2][3];

我们说,arr1是一个拥有2个元素的数组类型(最内层才是本质),元素类型是一个有3个int元素的数组类型(外层表示的是数组的元素类型)。如果我们拆开来写,应该是这样的:

using ele_t = int [3];
ele_t arr1[2];

所以,我们搞清它的本质,那么其他的问题就容易解释了,比如说:

auto p = arr1;  // 请问p是什么类型?

既然,数组类型可以隐式转换为首元素的指针,那么,这里的p应该就是「(数组的元素)的指针 类型」。那么arr1的元素应该是ele_t类型,所以,p应该是这种类型的指针类型,也就是ele_t *类型,也就是int [3]的指针类型,也就是int (*)[3]类型。

再比如说:

auto p2 = &arr1; // 请问p2是什么类型?

这里并不是用数组直接转换,而是取了地址,那么p2就应该是一个二维数组的指针类型了。因为arr1ele_t [2]类型,那么p2就应该是它的指针类型,也就是ele_t (*)[2]类型,也就是int (*)[2][3]类型。

希望读者可以搞清指针和数组的类型描述方法,对于由他们组合的复杂类型也就能见招拆招,迎刃而解了。

函数类型和函数指针类型

对于纯C语言程序员来说,可能都不容易意识到「函数类型」这种类型,毕竟你不会用一个函数的类型去定义另一个函数,也不会关心它的大小之类的事情。但是有了C++的模板以后,这件事就变得值得关注了。

「函数类型」也是一种类型,它跟数组类型的描述比较类似,包括两部分,函数返回值类型和函数的参数类型。

int f1(int, double); // f1是函数类型,返回值类型为int,接收2个参数,分别为int、double类型
void f2(); // f2是函数类型,返回值类型为空,参数为空
int f3(int); // f3是函数类型,返回值类型为int,接收1个参数,是int类型

有一个需要注意的是,如果函数无返回值,那么必须要用void占位,而不可以省略,在早期版本标准里,返回值为int是可以省略的,也就是说:

f();
// 相当于
int f();

不过这种标准已经废除,也不被推荐,但自始至终void都不能省略的。

而如果一个函数不接收参数,那么小括号中可以空着,或者写上void,也就是说:

void f(); 
// 相当于
void f(void);

函数类型之所以也能成为一个类型,主要是由于冯·诺依曼体系的计算机中,在存储上并不区分「数据」和「指令」。函数在编译后其实就会成为一段指令,同样会加载到内存中,同样会拥有地址。那么从本质上来说,它就跟变量是一样的,这就是函数类型。

有了前面复杂类型的洗礼,那么这里我们就可以稍微添加一点难度了。如果要写一个返回值为数组指针类型的函数,要怎么办?我们观察到,在函数类型的说明符里,返回值在左边,参数在右边。而前面我们已经介绍过,没有括号区分的情况下,左边是外边,右边是里面,也就是说,返回值应该写在外面,参数应该写在里面。那么既然返回值也是一个复杂类型,那么就应当把这个类型套在函数类型的外面,用于表示返回值类型:

int (*f())[3]; // 表示f是一个函数,无参数,返回值类型是一个指针,指针的解类型是int [3]
// 分开来写就是
using ret_t = int (*)[3];
ret_t f();

通俗来讲就是说,f是一个返回值为「数组指针」类型的函数。

既然函数类型也是一种数据类型,函数也拥有内存地址,那么自然我们就可以取这种类型的指针,也就是我们常说的「函数指针」类型。

书写的思路一样,本身是一个指针,所以星号在最内层,紧贴变量名,解类型是函数类型,那么就把函数类型的描述符写在外层:

void (*p)(int); // 表示p是一个指针,解类型是函数类型void (int)
// 分开来写就是
using func_t = void (int);
func_t *p;

不过有一点比较特殊的是,函数类型可以隐式转换为函数指针类型,而函数取地址也能得到函数指针类型。这样就会造成下面这种很有意思的情况:

void f();void Demo() {auto p1 = f; // p1是void (*)()类型auto p2 = &f; // p2也是void (*)()类型auto p3 = *f; // p3也是void (*)()类型auto p4 = *****************f; // p4也是void (*)()类型auto p5 = &f; // p5是void (**)()类型「二级指针」
}

离谱归离谱,但……C++嘛,DDDD~

上面也展示了函数指针的指针了,那我们再来个难一点的,一个函数的返回值是函数指针的情况,应该怎么写呢?

using ret_t = void (*)();ret_t f(); // f的类型是?

思路都没有变,返回值套在外面就好,不再赘述了:

void (*f())(); // 外层的void ()表示指针的解类型,内层的*表示f的返回值是指针,f后面的()是f的参数列表。

非静态成员函数类型

这个标题已经比较自洽了,所谓「非静态成员函数(non-static member function)」又有地方管它叫「方法(method)」,指的就是类(或结构体/共合体)中的成员函数,并且没有用static修饰的。举例来说:

struct Test {void f();
};

此时的f就是一个非静态成员函数,它的类型是:

void (Test::)();

先别急,我们来慢慢解释~~

首先我们要明确一件事,所谓非静态成员函数之所以是一个独立的类型,主要是因为它会隐含一个函数参数,也就是this指针所指的对象。非静态成员函数不能够直接调用,而是要通过一个「对象」作为发起方。用上面的例子来说:

void Demo() {Test::f(); // 这种调用方法是错误的Test t;t.f(); // 必须要有发起方
}

那么既然有「发起方」,那么这个发起方一定有自己的类型。发起方的类型需要与所调用的函数所在类型相匹配,也就是说,一个对象只能调用自己类,或自己的父类(包括间接父类)中的非静态成员函数,并且当符合默认情况时可以省略类名,我们来看一个具体的例子:

struct Base {void f1();
};struct Test1 : Base {void f2();
};struct Test2 {};void Demo() {Test1 t1;Test2 t2;t1.Base::f1(); // 调用父类的成员函数,OKt1.Test1::f2(); // 调用自己类的成员函数,OKt1.f1(); // 省略类名,则会向上查找到继承链中最近的方法实现,这里相当于t1.Base::f1()t1.f2(); // 省略类名,同理,这里相当于t1.Test1::f2();t2.f1(); // 报错,应为Test2的继承链中找不到f1函数t2.Base::f1(); // 同样报错,因为t2不属于Base类的继承链中的类型,不能够用它来调用Base类的成员函数
}

所以,对于非静态成员函数来说,它的发起方类型(也就是隐含参数的类型)也应当体现在它的类型描述符中。因此,对于「一个Test1类中的成员函数,返回值为void,参数为空(其实是有一个隐含参数的,这里说的是不包括隐含的,只看明面的情况,它是空)」这种类型的函数,其类型描述符是:void (Test1::)()

或者也可以从另一个角度来解读,就是隐含参数是Test1类型,而隐含参数的类型要放在函数名的前面(也就是最内层的部分)。总之,这里希望读者知道的是,非静态成员函数的类型描述符,需要出现这个类型名,用以表示隐含参数的类型(当然对于继承链下游的类型也同样支持,这种情况我们认为做了一次static_cast就好,也可以解释)。

再多啰嗦一句,我们刚才说的都是非静态成员函数的情况,而静态成员函数由于不含隐含参数,因此它的描述方式跟普通函数是一样的:

struct Test {static void f1(); // f1的类型是void ()void f2(); // f1的类型是void (Test::)()
};

那么,如果非静态成员函数还含有一些其他的属性(比如说const&noexcept)的话,它的声明位置同样在类型的最后,也就是参数列表的后面:

struct Test {void f1() const;void f2(const int *) &&;int f3() noexcept;void f4(int) & noexcept;
};

上面例子中,f1~f4的类型分别是:

void (Test::)() const; // f1的类型
void (Test::)(const int *) &&; // f2的类型
int (Test::)() noexcept; // f3的类型
void (Test::)(int) &noexcept; // f4的类型

那么,如果一个非静态成员函数的返回值是一个复杂类型呢?同理,放到外层就好了(只要把握住内外层,那么所有的问题都能迎刃而解),比如说:

using type1 = int (*)[3];
using type2 = void (*)(int);
struct Test {type1 f1() const;type2 f2(double) noexcept;
};

对应的类型是:

int (*(Test::)() const)[3]; // f1的类型
void (*(Test::)(double) noexcept)(int); // f2的类型

不过需要大家知道的是,「非静态成员函数」类型只是一个概念上的类型,实际情况下我们是没法用这种类型来定义变量的,甚至都没法直接定义这种类型:

using type = void (Test::)(); // 报错
void (Test::f)(); // 报错

而能够使用的类型则是它的指针类型,下一节来介绍。

非静态成员函数指针类型

既然非静态成员函数基本可以等价于一个隐藏了调用方这个参数的函数类型,那么它本质还是一个函数,也就是一个代码段,自然也是要入内存的。所以,它同样含有内存地址,也就含有对应的指针类型,也就是非静态成员函数指针类型。

同样,在最内层加一个指针符号即可表示函数指针类型:

struct Test {void f();
};void Demo() {auto fp = &Test::f;// fp的类型就是非静态成员函数指针类型,等价于:void (Test::*fp)() = &Test::f;
}

在上面的例子中,fp的类型是void (Test::*)()类型。

照例,我们还是做一些组合,下面直接给出一些例子,就不再赘述:

struct Test {void f1() const;int f2() noexcept;int f3(double, int) &;
};void Demo() {using type1 = void (Test::*)() const;using type2 = int (Test::*)(double, int) &;using type3 = decltype(&Test::f2);type1 *p1; // void (Test::**)() consttype3 p2; // int (Test::*)() noexcept;type2 arr[2]; // int (Test::*[2])() noexcept;type3 (*p3)(int); // int (Test::*(*)(int))(double, int) &;
}

顺便啰嗦一句,非静态成员函数指针的大小为2个指针的大小,在64位环境中应该是16个字节(读者可以自行sizeof来验证),这部分详细解释可以看深入C++成员函数及虚函数表。

【第三篇待更】

相关文章:

盘一盘C++的类型描述符(二)

先序文章请看 盘一盘C的类型描述符(一) 稍微组合一下的复杂类型 数组指针类型的数组类型 数组的指针类型我们已经了解了,那么,以这种类型作为元素的数组类型怎么搞? using type int (*)[3]; // 元素类型是数组指针…...

慎投,Frontiers这本期刊显示on hold中

什么是“On Hold”? 该期刊因为质量问题正在被进行重新评估;在重新评估过程中,不会检索新发表的文章。该期刊因为质量问题正在被进行重新评估;在重新评估过程中,不会检索新发表的文章。根据选择标准,在最严…...

Winform控件开发(21)——ProgressBar(史上最全)

一、属性 1、Name 用于获取控件对象 2、Anchor 锚定控件对于父控件的位置 3、BackColor 背景色 4、ContextMenuStrip 关联的上下文菜单 5、Cursor 鼠标移动到控件上显示的光标 6、Dock 停靠在父控件的位置 7、Enabled 是否启动该控件,false时事件都不能触发 8、…...

校招失败后,在外包公司熬了 2 年终于进了字节跳动,竭尽全力....

其实两年前校招的时候就往字节投了一次简历,结果很明显凉了,随后这个理想就被暂时放下了,但是这个种子一直埋在心里这两年除了工作以外,也会坚持写博客,也因此结识了很多优秀的小伙伴,从他们身上学到了特别…...

UniApp + SpringBoot 实现接入支付宝支付功能和退款功能

一、支付宝开放平台设置 注册支付宝支付功能需要个体工商户或企业才可以!需要有营业执照才能去申请哦! 1、登录到控制台 进入支付宝开放平台 控制台 2、开发设置 3、产品绑定APP支付 如果没有绑定APP支付就会报商家订单参数异常,请重新发起…...

初识进程

文章目录一、进程的概念1. 进程是什么及进程的管理2. Linux 下的 pcb3. 系统调用接口 getpid 和 getppid4. 系统调用接口 fork一、进程的概念 1. 进程是什么及进程的管理 在 Linux下 ./binaryfile 运行一个程序或者在 Windows下双击运行一个程序时,程序就变成了一个…...

SOAP传输协议

一.HTTP传输协议 超文本传输协议(HyperText Transfer Protocol,缩写:HTTP),它是基于请求-响应的模式协议,客户端发出请求,服务器端给出响应并返回请求内容。方法如下,HTTP传输协议常…...

<Linux>进程控制

进程控制 文章目录进程控制一、进程创建1.fork函数认识2.写时拷贝3.fork常规用法4.fork调用失败的原因二、进程终止1.进程退出场景2.进程退出码3.进程退出的方式三、进程等待1.进程等待是什么?2.进程等待的必要性3.进程等待的方法3.1.wait函数3.2.waitpid函数4.如何…...

有手就行 -- 搭建图床(PicGo+腾讯云)

🍳作者:贤蛋大眼萌,一名很普通但不想普通的程序媛\color{#FF0000}{贤蛋 大眼萌 ,一名很普通但不想普通的程序媛}贤蛋大眼萌,一名很普通但不想普通的程序媛🤳 🙊语录:多一些不为什么的…...

“蓝桥杯”递推和递归(一)——取数位

1. 算法简介 递推和递归虽然叫法不同,但它们的基本思想是一致的,在很多程序中,这两种算法可以通用,不同的是递推法效率更高,递归法更方便阅读。 (1)递推法 递推法是一种重要的数学方法&#…...

蓝桥杯·3月份刷题集训Day02

本篇博客旨在记录自已打卡蓝桥杯3月份刷题集训,同时会有自己的思路及代码解答希望可以给小伙伴一些帮助。本人也是算法小白,水平有限,如果文章中有什么错误之处,希望小伙伴们可以在评论区指出来,共勉💪。 文…...

python --获取内网IP地址

方法一 import socketdef get_local_ip_address():ip_address try:# 获取本机主机名hostname socket.gethostname()# 获取本机IPip_address socket.gethostbyname(hostname)except:passreturn ip_address方法二 import subprocessdef get_local_ip_address():ip_address …...

如何衡量你的Facebook广告活动的成功

投入大量资金和资源在Facebook广告上并不总能带来预期的回报,这很可能是由于缺乏恰当的衡量广告活动成功的方法。在这篇文章中,我们将介绍一些关键的指标,帮助你更好地了解如何衡量你的Facebook广告活动的成功。1.费用每次点击(CP…...

Linux对一个目录及其子目录所有文件添加权限

1、chmod指令 chmod是一个改变用户拥有指定文件的权限的命令.r:只读,w:写,x执行.也可以用数字 -rw------- (600) -- 只有属主有读写权限。 -rw-r--r-- (644) -- 只有属主有读写权限;而属组用户和其他用户只有读权限。 -rwx------ (700) -- 只有属主有读、写、执…...

宝刀未老?低代码何德何能受大厂们的推崇

风口之下,低代码蓬勃发展,本文从国内低代码的走红现象引入,浅析低代码发展中的变化趋势,重点探讨如此趋势之下,国内大厂如何通过低代码实现了良性发展。 一、国内爆火的低代码 据Gartner最新报告显示,到2…...

智能扑克牌识别软件(Python+YOLOv5深度学习模型+清新界面)

摘要:智能扑克牌识别软件利用视觉方法检测和识别日常扑克牌具体花色与数字,快速识别牌型并标注结果,帮助计算机完成扑克牌对战的前期识别步骤。本文详细介绍基于深度学习的智能扑克牌识别软件,在介绍算法原理的同时,给…...

SQL优化13连问,收藏好!

1.日常工作中,你是怎么优化SQL的? 大家可以从这几个维度回答这个问题: 分析慢查询日志 使用explain查看执行计划 索引优化 深分页优化 避免全表扫描 避免返回不必要的数据(如select具体字段而不是select*) 使用…...

【小技巧】公式从docx文件复制到doc文件变成了图片怎么办?

文章目录0、word文件后缀命名1、docx和doc默认的公式编辑方式2、MathTpye公式编辑器3、MathType 运行时错误‘53’:文件未找到:MathPage.WLL4、结束语0、word文件后缀命名 1997-2003的旧版本文件名后缀是.doc   从2007版以后,后缀名是.docx…...

Python3入门与进阶笔记(六):初识类

目录 一些解释 属性 类名建议首字母大写,通常用驼峰规则命名。变量名建议小写,下划线隔开。类最基本的作用是封装。 写在类内非方法中的语句在类加载的时候会执行,且只会执行一次,例如下面的print语句,类加载时就会…...

Prometheus监控实战系列九:主机监控

Prometheus使用各种Exporter来监控资源。Exporter可以看成是监控的agent端,它负责收集对应资源的指标,并提供接口给到Prometheus读取。不同资源的监控对应不同的Exporter,如node-exporeter、mysql-exporter、kafka-exporter等,在这…...

JVM知识整理

JVM知识整理 JVM的主要组成部分 JVM包含两个两个子系统(类加载子系统和执行引擎)和两个组件(运行时数据区与和本地库接口) 类加载子系统:根据给定的全限定类名来加载class文件到运行时数据区域中的方法区。执行引擎&a…...

【C++】二叉搜索树

A:你长大后想要做什么? B:写下“快乐”…… A:不,你理解错我的意思了,我是说 B:不,是你理解错了人生…… 文章目录一、二叉搜索树的实现1.struct TreeNode{}2.迭代版本2.1 Insert()插入结点(解决链接的问题&#xff09…...

leetcode -- 21. 合并两个有序链表

🐨目录📑1. 题目🛶2. 解法- 头插到新链表🐬2.1 思路🐬2.1 代码实现⛵3. 解法优化 - 带哨兵位🐋3.1 思路🐋3.2 代码实现🚤4. 题目链接📑1. 题目 将两个升序链表合并为一个…...

计算机组成原理|第四章(笔记)

目录第四章 存储器4.1 概述4.1.1 存储器分类4.1.2 存储器的层次结构4.2 主存储器4.2.1 概述4.2.2 半导体存储芯片简介4.2.3 随机存取存储器(RAM)4.2.4 只读存储器(ROM)4.2.5 存储器与CPU的连接4.2.6 存储器的校验4.2.7 提高访存速…...

【Unity3D-BUG记录】Unity3D中出现“动画片段必须标记为Legacy的警告”消除方法

推荐阅读 CSDN主页GitHub开源地址Unity3D插件分享简书地址我的个人博客 大家好,我是佛系工程师☆恬静的小魔龙☆,不定时更新Unity开发技巧,觉得有用记得一键三连哦。 一、前言 在开发中可能会遇到下面的警告: The AnimationClip…...

Spring Bean的定义(含创建Bean的三种方式)

🏆 文章目标:复习和理解下Spring Bean的定义 🍀 Spring Bean的定义(含创建Bean的三种方式) ✅ 创作者:Jay… 🎉 个人主页:Jay的个人主页 🍁 展望:若本篇讲解内…...

vue的路由-vue router(一)

vue的路由-vue router一、路由的基本使用HTMLrouter-linkrouter-viewJavaScript二、带参数的动态路由匹配三、嵌套路由四. 编程式导航导航到不同的位置替换当前位置横跨历史篡改历史五. 命名路由六. 命名视图嵌套命名视图七. 重定向和别名重定向别名八. 将 props 传递给路由组件…...

DevOps流水线搭建-PHP版本

一、介绍流水线发布代码1、官网https://www.jenkins.io/zh2、kubesphere里的介绍https://kubesphere.io/zh/docs/v3.3/devops-user-guide/how-to-use/pipelines/choose-jenkins-agent/3、git仓库可以自己写点测试代码,提交,待会测试用https://gitee.com/…...

C语言之按位取反~(七十一)

计算机存储数据基本知识计算机中二进制数包括(正数和负数)是以补码形式存储。符号位:补码的最左侧首位是符号位,0表示正数,1表示负数。二进制有三种形式:原码、反码、补码。正数的补码和反码:是…...

C# 委托

Action 在 C# 中,Action 是一个预定义的委托类型,表示一个不带参数和返回值的方法。它的定义如下: public delegate void Action();Action 委托可以用于表示任何没有参数和返回值的方法。例如: // 定义一个不带参数和返回值的方…...