盘一盘C++的类型描述符(一)
前言
C++的类型描述方式是从C语言继承来的,并且进行了扩充(例如引用、非静态成员函数、模板实参等)。但由于C语言中的类型描述方式就略微有点「反人类」,再经C++扩展后就有点「反碳基生物」了~
是的,当我第一次看到这种描述符的时候,我也觉得能写出这玩意的肯定不是碳基生物……没准是用偏硅酸盐合成的新型物种……
void (Test::*(Test::*const &)() const)() const
更离谱的是,上面这种类型如果通过type_traits以后会变成什么?
using type = void (Test::*(Test::*const &)() const)() const;
std::remove_const_t<type>; // 这又是个啥类型呢?
好吧,但愿这个引子可以让读者产生兴趣,而不是劝退(才怪……)。
但,真的理解了以后,emmm…确实也是人类能理解的吧(或许当我理解它的那一刻,我的体内就已经在合成SiO32−SiO_3^{2-}SiO32−了吧……【手动捂脸】)
因此,本篇就来盘一盘C++的类型描述符,带读者由浅入深,一步一步征服它。
先从指针类型说起
指针类型其实是指针的默认解类型
首先我们应当知道,「指针类型」本身,指的就是「用于保存内存地址的变量类型」。而对于内存地址来说,都是一样的(不存在XXX类型的内存地址这种说法)。所以,照理说,「指针类型」应该就都是一种类型,表示这种类型的数据,应当解释为内存地址。我们这里暂且把这种指针类型起名为ptr_t:
void Demo() {using ptr_t = void *; // 可以先忽略这一行int a;double b;ptr_t p1 = nullptr; // 空地址ptr_t p2 = &a; // 用于保存a的地址ptr_t p3 = &b; // 用于保存b的地址ptr_t p4 = &p2; // 用于保存p2的地址
}
只是,通常情况下,我们仅仅拿到一个内存地址是没什么意义的(难道只是为了把它打印出来吗?),拿这个内存肯定是为了操作这个内存上的数据,而我们只知道这个数据的地址是不够的,我们还得知道,要用什么样的方式来解释存在这里的数据,也就是「指针的解类型」。例如,我们「用int的方式来解p2指针」,也就是从p2的值所表示的地址处开始,向高地址方向取sizeof(int)个字节的数据,按照小端序组装起来,并把首位认为是符号位,然后读出(或者写入)这个整数。比如说把读出来的这个值赋给另一个变量y,代码写作:
int y = *(int *)p2; // 表示把p2这个指针,按照int方式解出来,得到的值赋值给y
但如果每次都去指定指针的解类型,会很麻烦,所以我们就希望能给这个指针添加一个「默认解类型」,也就是说,在定义这个指针类型的时候,给它指定一个默认的解类型,如果后续不指定类型,直接解指针的话,就用这种默认的解类型。
从C++的语法上来说,类型+星号,表示定义一个指定了默认解类型的指针类型。比如说:
int *p5 = &a; // p5是指针类型,默认解类型是intint z = *p5; // 没有指定解类型的时候,选用默认的解类型,也就是int类型
同理,如果「默认解类型」是「一个指针类型」的话,也是一样的:
ptr_t *p6 = &p5; // p6的默认解类型是指针类型ptr_t p7 = *p6; // 解出来就是ptr_t类型,所以p7也是指针类型
// 但是因为ptr_t是未指定解类型的指针类型,所以解的时候必须要指定解类型
int w = *(int *)p7;
那么,如果我还希望解出来的指针类型也含有默认解类型的话,就可以用「默认解类型」是「一个指定了『默认解类型』的指针」的指针类型:
int **p8 = &p5; // p8的默认解类型是int *类型
// 所以对p8直接解指针后,得到的就应该是int *类型
int *p9 = *p8;
// 而p9是默认解类型为int的指针类型,解指针后得到int类型
int t = *p9;
上面的例子想表明的是:
- 只要是指针类型,都是用来保存内存地址的,也就是说它的值仅仅表示地址。
- 指针类型中,星号前面的部分表示的「指针的默认解类型」。
- 多级指针本质是「默认解类型为『指针类型』的指针类型」,所以无论前面的类型多么复杂,它都属于这个指针的默认解类型,而不影响这个指针本身。
泛型指针类型其实是无默认解类型的指针类型
再回头来看看刚才这个ptr_t,刚才有一句我让大家先忽略的那一行定义:
using ptr_t = void *;
我们希望表示的是「不含默认解类型」的指针类型,按照语法,默认解类型是T的指针类型就是T *,那么「不含」默认解类型的,就可以理解为默认解类型是「空」的,自然就是void *。
所以我们常说的「泛型」指针,之所以能「泛」,其实就是因为,没有指定默认解类型而已,并没有什么稀奇的。
指针类型转换本质是指针默认解类型转换
既然我们知道了指针类型其实表示的是它的默认解类型,那么指针类型的转换自然是表示它默认解类型的转换了:
void *p = &a;
int *p2 = (int *)p; // void *转换为int *,其实就是默认解类型从空变为int
所以前面例子中我们「指定解类型」的解指针方式,本质就是把指针转换为「默认解类型是对应类型」的指针类型,再去解指针:
int y = *(int *)p; // 其实是把p转换为int *类型,再解指针,自然得到int类型
那么,把「含有默认解类型的指针类型」转换为「不含默认解类型的指针类型」应当是一种较为安全的静态转换,因此,我们使用static_cast来代替之前C风格的转换:
int *p = &a;
void *p2 = static_cast<void *>(p); // int * → void *
另外,上面这种转换也支持隐式转换:
int *p = &a;
void *p2 = p; // int * → void *
同理,给「不含默认解类型的指针类型」赋予一个默认解类型,变为「含有默认解类型的指针类型」也是一种较为安全的静态转换,所以同样使用static_cast:
void *p = &a;
int *p2 = static_cast<int *>(p); // void * → int *
不过这种转换不支持隐式转换,必须强转。
而「默认解类型为A的指针类型」转换为「默认解类型是B的指针类型」(这里的A,B都不是void)则被认为是一种相对不安全的转换,因为改变指针的默认解类型相当于「重新解释了」指针所指数据的含义。因此,这里要使用reinterpret_cast:
int *p = &a;
char *p2 = reinterpret_cast<char *>(p); // int * → char *
const修饰的指针
const关键字在C/C++中并不是代表真正的常量,而是应当理解为read-only,也就是只读。用const修饰的类型不可被修改,只能读取。
而对于指针来说,指针本身既然是一种数据类型,那么也就存在「只读的指针类型」。另一方面,指针的默认解类型也可能是一种只读类型,所以,我们主要是要区分这个const修饰的是指针类型本身,还是指针的默认解类型中的类型。
int *p1; // 指针本身可变,默认解类型是int
const int *p2; // 指针本身可变,默认解类型是const int
int *const p3; // 指针本身不可变,默认解类型是int
const int *const p4; // 指针本身不可变,默认解类型是const int
当我们理解了何为指针的解类型后,其实就很好判断了。如果const出现在解类型中,那么它与指针本身是否可变无关,只有在修饰指针本身的时候,才表示这个指针变量是个只读变量。
而在指针类型的表达式中,我们首先应当找到那个「特殊的星号」,由这个星号隔开,其余的内容都是解类型。
比如在int *const p3中,星号只有一个,自然就是那个特殊的(或者说最内层的),星号前面(外面)的都是解类型,而这个const出现在特殊型号的右边(内部),因此,它修饰的是这个变量本身,那么我们就说p3是只读类型。而剩下的部分,是它的解类型,也就是int。
同理,在const int *p2中,也只出现了一个星号,它就是特殊的那个。星号后面没有const修饰,所以p2是可变的,而它的解类型是const int,也就是说这里的const修饰的是解类型。
C++中提供了一个模板工具std::remove_const,用于去掉类型的const修饰,这里要注意的是,它去掉的是类型本身的const,而跟解类型是完全没有关系的,会原样保留,比如说:
std::remove_const_t<const int *>; // const int *
std::remove_const_t<int *const>; // int *
std::remove_const_t<const int *const>; // const int *
那么,对于多级指针呢?同理,我们需要找到特殊的星号(最内层的星号),由他隔离开,外边都是解类型。
int *const *p1; // p1可变,解类型是int *const
int **const *p2; // p2可变,解类型是int **const
const int **p3; // p3可变, 解类型是const int *
int *const *const p4; // p4不可变,解类型是int *const
所以,它们如果去掉const也是同理,只会去掉那个修饰变量本身的const,而解类型不会改变:
std::remove_const_t<int *const *>; // int * const *
std::remove_const_t<int **const *>; // int **const *
std::remove_const_t<const int **>; // const int **
std::remove_const_t<int *const *const>; // int *const *
总结就是一句,找到最内层的星号(目前的例子其实都是最右边的星号),由它分隔,外面(目前例子都是左边)都表示解类型,与变量本身无关,里面(目前例子都是右边)才是修饰变量本身的,如果出现了const,就表示变量本身不可变。
后面的章节将会介绍真正的「里面」和「外面」并不符合前面的「右边」和「左边」规律的例子。
数组类型
单纯的数组类型
笔者采访过一些C++程序员(以C++为主要开发语言的从业者),惊奇地发现有一多半的人都不了解「数组类型」。尽管他们可能天天见、天天使用,但从来没有意识到这种类型的存在形态。
举个例子来说:
int arr[] {1, 2, 3};
请问arr是什么类型?数组类型?指针类型?int *类型?
揭晓答案,arr是int [3]类型,解释为,含有3个int元素的数组类型。我相信大家对「数组类型」肯定不陌生,也能解释清楚它的元素类型、个数等。但是乍一看到这个int [3]类型,还是有很多人会懵圈的。
的确,我们并不容易注意到arr的类型就是int [3],这主要是因为,C++的数组类型通常情况下只会在定义的时候用到,之后就全部改用指针和偏移量去操作了。
那么现在就请读者知晓,数组类型本身包含了「元素类型」和「元素个数」这两个因素的。它是独立存在一种类型,并不是指针/结构体/整数等的语法糖。只不过,数组类型可以隐式转换为首元素的指针类型:
auto p = arr; // p是int *类型
// 也就是等价于
int *p = (int *)arr;
所以我们一定要清楚,数组是数组,指针是指针,这是两种不同的类型,只是可以隐式转换而已。要想验证也很简单,用std::is_same来验证,或者直接通过sizeof也可以间接验证:
int arr[] {1, 2, 3};
auto p = arr;std::is_same_v<decltype(arr), decltype(p)>; // false
std::is_same_v<int [3], int *>; // false// 假设64位环境
sizeof(arr); // 12
sizeof(p); // 8
sizeof(int [3]); // 12
sizeof(int *); // 8
识别清数组类型,会对我们在模板实例化时避坑有很大帮助。比如说下面的写法就是有问题的:
std::shared_ptr<int *> p = new int[5];
因为p被识别为int *类型的智能指针,那么在p析构时,只会调用delete方法,而不是delete [],使得这片堆空间没有被正确释放。
正确的写法是:
std::shared_ptr<int[]> p = new int[5]; // 要用数组类型,而不是指针类型
再比如,模板的自动类型推导中,如果传入数组也会被识别为数组类型:
template <typename T>
struct Test {Test(const T &t) {}
};void Demo() {int arr[] {1, 2, 3};auto p = arr;Test t1{arr}; // t1是Test<int[3]>类型Test t2{p}; // t2是Test<int *>类型
}
const数组类型
那么,是否存在不可变数组类型呢?我们知道,数组一旦确定,它的元素类型不可变,元素个数也不可变,所以但从数组的两个因素来讲,所有的数组都是不可变的,因此也就不存在所谓可变还是不可变数组类型。
那么对于数组来说,唯一可以控制是否可变的就是元素类型,因此,只存在const T [N]类型,而不存在类似于T (const) [N]之类的。注意,T const [N]和const T [N]等价,const都是修饰元素类型的。
既然const是修饰元素类型的,那么它隐式转换为指针后,这个const也一定修饰的是解类型,而不是指针本身:
const int arr[] {1, 2, 3};
auto p = arr; // p的类型是const int *
数组指针类型
数组指针类型其实就是指「默认解类型是数组类型的指针类型」。一定要注意,这跟「数组首元素指针类型」是不同的!数组类型不能转化成它,而是要通过取地址运算得到:
int arr[] {1, 2, 3};
auto p = &arr; // p的类型是int (*)[3]
这里我们不得不引出C/C++中类型描述符的一大绕不开的「缺陷」了,那就是类型描述符并不一定是从左向右,而是可能从里向外。前面章节我们提到过「内部」和「外部」的说法,也是为了跟这种类型描述符的特点相对应。
从「数组类型」开始,就已经符合这种由内向外的描述方式了:
int arr[3];
arr是int [3]类型,但并没有写作int[3] arr而是写作了int arr[3]。我们注意到,变量名被夹在了类型描述符的中间。对于更复杂的这种类型描述方式来说,我们需要由内向外来解释,首先要找到变量名,然后逐层向外来阅读。例如:
int (*p)[3];
首先找到变量名p,由括号限定的最内层有一个型号,表示p本身是一个指针。那么再向外一层则表示指针的解类型,这里它的解类型是int [3]。所以综合来说,p是一个解类型为int [3]类型的指针,也就是我们通常所说的「数组指针」类型。
与之对应的一个容易搞混的是:
int *q[3];
同样先找到变量名q,向外一层则是数组,右边表示数组元素个数,左边表示数组元素类型。所以q是数组,元素类型是int *,也就是我们通常说的「指针数组」。
【第二篇待更】
相关文章:
盘一盘C++的类型描述符(一)
前言 C的类型描述方式是从C语言继承来的,并且进行了扩充(例如引用、非静态成员函数、模板实参等)。但由于C语言中的类型描述方式就略微有点「反人类」,再经C扩展后就有点「反碳基生物」了~ 是的,当我第一次看到这种描…...
Peppol的发展史和基本框架
Peppol(Pan-European Public Procurement Online)是欧洲区域内的一个跨境公共采购电子商务平台试点项目,由欧盟委员会和Peppol联盟成员国共同资助建立,旨在通过制定标准化框架,推动欧盟成员国在公共采购相关的电子目录…...
Linux-GCC介绍+入门级Makefile使用
前言(1)我们都知道,在Linux中编译.c文件需要使用gcc -o .c文件的指令来将C文件变成可执行文件。但是我们有没有发现,如果我们需要编译大一点的工程,后面需要加上的.c文件是不是太多了?感觉非常的麻烦。&…...
iOS(一):Swift纯代码模式iOS开发入门教程
Swift纯代码模式iOS开发入门教程项目初始化(修改为纯代码项目)安装第三方库(以SnapKit库为例)桥接OC库(QMUIKit)封装视图并进行导航跳转示例:使用 TangramKit 第三方UI布局库应用国际化添加 R.s…...
IDEA+Python+Selenium+360浏览器自动化测试
环境配置前提,见文章https://mp.csdn.net/mp_blog/creation/editor/new?spm1001.2101.3001.4503下载360浏览器,并下载对应版本的chromedriver.exe,下载地址http://chromedriver.storage.googleapis.com/index.html下载好360浏览器࿰…...
运输层概述及web请求
运输层 运输层概述 运输层向高层用户屏蔽了下面网络核心的细节(如网络拓扑、所采用的路由选择协议等)它使应用进程看见的就好像是在两个运输层实体之间有一条端到端的逻辑通信信道; 根据需求不同,运输层提供两种运输协议 面向连…...
python与pycharm从零安装
python(解释器)下载地址:Welcome to Python.orgpycharm(编译器)下载地址:PyCharm: the Python IDE for Professional Developers by JetBrains一、python的下载与安装到官网后根据步骤下载安装包后…...
叠氮试剂943858-70-6,Azidobutyric acid NHS ester,叠氮-C3-活性酯
1、试剂基团反应特点(Reagent group reaction characteristics):Azidobutyric acid NHS ester具有叠氮化物和NHS酯端基。西安凯新生物科技有限公司供应的叠氮化物可以与炔烃、DBCO和BCN进行铜催化的点击化学反应。NHS酯可以与胺基反应&#x…...
pycharm激活虚拟环境时报错:无法加载文件activate.ps1,因为在此系统上禁止运行脚本,Windows10系统
问题: ii_env\Scripts\activate : 无法加载文件 F:\gitlab\AutoFrame\ii_env\Scripts\Activate.ps1,因为在此系统上禁止运行脚本。 有关详细信息,请参阅 https:/go.microsoft.com/fwlink/?LinkID135170 中的 about_Execution_Policies。 所在…...
刷题小抄4-数组
在Python中数组的功能由列表来实现,本文主要介绍一些力扣上关于数组的题目解法 寻找数组中重复的数字 题目链接 题目大意: 给出一个数组,数组长度为n,数组里的数字在[0,n-1]范围以内,数字可以重复,寻找出数组中任意一个重复的数字,返回结果 解法一 该题最基础的思路是使用字…...
Hbase安装
目录 上传压缩包 解压 改名 修改 Hbase 配置文件 修改base-env.sh 修改hbase-site.xml 配置环境变量 修改zookeeper配置文件 复制配置文件 修改zoo.cfg配置文件 修改myid 配置环境变量 刷新配置文件 启动Hbase 进入Hbase 查看版本号 查看命名空间 查看命名空…...
面向对象设计模式:结构型模式之代理模式
一、引入 访问 FB:代理服务器 二、代理模式 aka Surrogate 2.1 Intent 意图 Provide a surrogate (代理) or placeholder for another object to control access to it. 为另一个对象提供一个代理或占位符,以控制对它的访问。代理模式给某一个对象提…...
CCF大数据专家委员会十周年纪念庆典纪实:拥抱数字时代,展望科技未来
山河远阔,奋进十年,作为国内大数据领域最权威的学术组织,CCF大数据专家委员会(以下简称“大专委”)不忘初心,凝心聚力,见证并推动了过去10年来大数据技术生态在中国的建立、发展和成熟。 2023年…...
Qt学习3-Qt Creator四则运算计算器(哔站视频学习记录)
计算器中的“”按钮这部分的代码解释 目录 制作计算器中的“”按钮这部分的代码解释 一、代码部分 二、解释 三、思路 四、死循环! 一、代码部分 void Widget::on_equalButton_clicked() {QStack<int> s_num,s_opt; //声明两个int类型变量char opt[128…...
学习 Python 之 Pygame 开发魂斗罗(九)
学习 Python 之 Pygame 开发魂斗罗(九)继续编写魂斗罗1. 在子弹类中修改敌人发射子弹的位置2. 创建显示敌人子弹的函数3. 解决敌人不会向下掉落的问题4. 给敌人碰撞体组增加碰撞体5. 解决敌人叠加在一起的问题继续编写魂斗罗 在上次的博客学习 Python 之…...
最简单的SpringBoot+MyBatis多数据源实现
最简单的SpringBootMyBatis多数据源实现1.数据库准备2.环境准备3.代码部分3.1多数据源配置2.测试随着应用用户数量的增加,相应的并发请求的数量也会跟着不断增加,慢慢地,单个数据库已经没有办法满足频繁的数据库操作请求了,在某些…...
Spring Boot 3.0系列【8】核心特性篇之SpringApplication
有道无术,术尚可求,有术无道,止于术。 本系列Spring Boot版本3.0.3 源码地址:https://gitee.com/pearl-organization/study-spring-boot3 文章目录 前言1. 启动应用2. 自定义 Banner3. 应用参数传递参数获取参数4. ApplicationRunner、CommandLineRunner5. 事件发布和监听…...
Nginx的搭建与核心配置
目录 一.Nginx是什么? 1.Nginx概述 2.Nginx模块与作用 3.Nginx三大作用:反向代理、负载均衡、动静分离 二.Nginx和Apache的差异 三.安装Nginx 1.编译安装 2.yum安装 四.Nginx的信号使用 五.Nginx的核心配置指令 1.访问状态统计配置 2.基于授…...
Java学习笔记 --- jQuery
一、jQuery介绍 jQuery,顾名思义,也就是JavaScript和查询(Query),它就是辅助JavaScript开发的js类库。它的核心思想是write less,do more(写得更少,做得更多),…...
华为OD机试题,用 Java 解【字符串加密】问题
华为Od必看系列 华为OD机试 全流程解析+经验分享,题型分享,防作弊指南)华为od机试,独家整理 已参加机试人员的实战技巧华为od 2023 | 什么是华为od,od 薪资待遇,od机试题清单华为OD机试真题大全,用 Python 解华为机试题 | 机试宝典使用说明 参加华为od机试,一定要注意不…...
第一篇:Agent2Agent (A2A) 协议——协作式人工智能的黎明
AI 领域的快速发展正在催生一个新时代,智能代理(agents)不再是孤立的个体,而是能够像一个数字团队一样协作。然而,当前 AI 生态系统的碎片化阻碍了这一愿景的实现,导致了“AI 巴别塔问题”——不同代理之间…...
【单片机期末】单片机系统设计
主要内容:系统状态机,系统时基,系统需求分析,系统构建,系统状态流图 一、题目要求 二、绘制系统状态流图 题目:根据上述描述绘制系统状态流图,注明状态转移条件及方向。 三、利用定时器产生时…...
基于matlab策略迭代和值迭代法的动态规划
经典的基于策略迭代和值迭代法的动态规划matlab代码,实现机器人的最优运输 Dynamic-Programming-master/Environment.pdf , 104724 Dynamic-Programming-master/README.md , 506 Dynamic-Programming-master/generalizedPolicyIteration.m , 1970 Dynamic-Programm…...
JVM 内存结构 详解
内存结构 运行时数据区: Java虚拟机在运行Java程序过程中管理的内存区域。 程序计数器: 线程私有,程序控制流的指示器,分支、循环、跳转、异常处理、线程恢复等基础功能都依赖这个计数器完成。 每个线程都有一个程序计数…...
windows系统MySQL安装文档
概览:本文讨论了MySQL的安装、使用过程中涉及的解压、配置、初始化、注册服务、启动、修改密码、登录、退出以及卸载等相关内容,为学习者提供全面的操作指导。关键要点包括: 解压 :下载完成后解压压缩包,得到MySQL 8.…...
云原生周刊:k0s 成为 CNCF 沙箱项目
开源项目推荐 HAMi HAMi(原名 k8s‑vGPU‑scheduler)是一款 CNCF Sandbox 级别的开源 K8s 中间件,通过虚拟化 GPU/NPU 等异构设备并支持内存、计算核心时间片隔离及共享调度,为容器提供统一接口,实现细粒度资源配额…...
数据分析六部曲?
引言 上一章我们说到了数据分析六部曲,何谓六部曲呢? 其实啊,数据分析没那么难,只要掌握了下面这六个步骤,也就是数据分析六部曲,就算你是个啥都不懂的小白,也能慢慢上手做数据分析啦。 第一…...
华为OD机考- 简单的自动曝光/平均像素
import java.util.Arrays; import java.util.Scanner;public class DemoTest4 {public static void main(String[] args) {Scanner in new Scanner(System.in);// 注意 hasNext 和 hasNextLine 的区别while (in.hasNextLine()) { // 注意 while 处理多个 caseint[] arr Array…...
篇章一 论坛系统——前置知识
目录 1.软件开发 1.1 软件的生命周期 1.2 面向对象 1.3 CS、BS架构 1.CS架构编辑 2.BS架构 1.4 软件需求 1.需求分类 2.需求获取 1.5 需求分析 1. 工作内容 1.6 面向对象分析 1.OOA的任务 2.统一建模语言UML 3. 用例模型 3.1 用例图的元素 3.2 建立用例模型 …...
DOM(文档对象模型)深度解析
DOM(文档对象模型)深度解析 DOM 是 HTML/XML 文档的树形结构表示,提供了一套让 JavaScript 动态操作网页内容、结构和样式的接口。 一、DOM 核心概念 1. 节点(Node)类型 类型值说明示例ELEMENT_NODE1元素节点<div>, <p>TEXT_NODE3文本节点元素内的文字COMMEN…...
