【软件开发底层知识修炼】二十七 C/C++中的指针与数组是不同的

  • 上几篇文章学习了ABI-应用程序二进制接口:【软件开发底层知识修炼】二十六 ABI-应用程序二进制接口 学习总结文章目录
  • 本篇文章就指针与数组的联系与区别来学习学习

文章目录

  • 1 疑问
  • 2 指针与数组是不相等的
  • 3 解决疑问
  • 4 总结

1 疑问

在具体用文字理论来说明指针与数组的区别之前,先看一下下面的代码例子,这两个程序输出的结果是一样的么?不一样的话,分别输出什么?

  • main.c
#include <stdio.h>extern char* g_name;int main()
{define_print();printf("main() : %s\n", g_name);return 0;
}
  • define.c
#include <stdio.h>char g_name[] = "D.T.Software";void define_print()
{printf("define_print() : %s\n", g_name);
}

将上述两个程序放到同一文件夹下进行编译运行:

  • gcc -g main.c define.c -o test.out
  • .test.out

运行结果如下:
在这里插入图片描述

  • 但是如果我把main.c中的extern char* g_name; 换成extern char g_name[]; 的话,程序运行就可以通过,并且可以得到预期的结果。

对于这个结果,我想并不是很多人可以理解的。这个问题放到后面解释。下面我们先来看看指针与数组的一些基本概念。

2 指针与数组是不相等的

  • 指针
  • 指针的本质就是一个变量,它保存的目标值是一个内存地址。这个内存地址是另一个变量或者不管什么东西的地址
  • 指针运算与 * 操作符配合使用能够模拟数组的行为
  • 数组
  • 数组是一段连续的内存空间的别名
  • 数组名可看做指向数组第一个元素的常量指针。

在C语言中指针与数组在某些层面是具有等价关系的,注意这里说的是某层面。比如下面的代码层面,指针与数组的操作就是相等的:

在这里插入图片描述

那么,既然我们已经学习了那么多汇编的知识,上面的指针与数组的操作在汇编层面(或者叫做二进制层面)是否相等?我们以实际的例子来说明,编译下面代码,并生成汇编代码,查看test函数的汇编代码:

#include <stdio.h>int test()
{int a[3] = {0};int* p = a;p[0] = 1;  // a[0] = 1p[1] = 2;  // a[1] = 2a[2] = 3;  // p[2] = 3
}int main()
{test();return 0;
}
  • gcc -g test.c -o test.out
  • objdump -S test.out > test.s 生成test.s反汇编代码

查看test.s中的test函数中的汇编代码,如下:

int test()
{8048394:	55                   	push   %ebp8048395:	89 e5                	mov    %esp,%ebp8048397:	83 ec 10             	sub    $0x10,%espint a[3] = {0};    804839a:	c7 45 f0 00 00 00 00 	movl   $0x0,-0x10(%ebp)  //a[0]的值80483a1:	c7 45 f4 00 00 00 00 	movl   $0x0,-0xc(%ebp)80483a8:	c7 45 f8 00 00 00 00 	movl   $0x0,-0x8(%ebp)int* p = a;     //指针p指向数组a的第一个元素,4字节80483af:	8d 45 f0             	lea    -0x10(%ebp),%eax80483b2:	89 45 fc             	mov    %eax,-0x4(%ebp)p[0] = 1;  // a[0] = 1  由于是在第一个位置,没必要使用add    $0x0,%eax80483b5:	8b 45 fc             	mov    -0x4(%ebp),%eax80483b8:	c7 00 01 00 00 00    	movl   $0x1,(%eax)p[1] = 2;  // a[1] = 2  可以看出有两次寻址的过程80483be:	8b 45 fc             	mov    -0x4(%ebp),%eax //首先把指针p存的地址取出来传给eax寄存器80483c1:	83 c0 04             	add    $0x4,%eax  //然后将eax+480483c4:	c7 00 02 00 00 00    	movl   $0x2,(%eax) //最后将数值2传给eax寄存器中存的地址所在的内存处,注意这句话的理解。a[2] = 3;  // p[2] = 380483ca:	c7 45 f8 03 00 00 00 	movl   $0x3,-0x8(%ebp) //可以看出如果是数组的话,直接将值赋值给对应内存处,而不用像指针那样进行两次地址的操作
}80483d1:	c9                   	leave  80483d2:	c3                   	ret    

对于上面的汇编代码,应该并不是很多人都可以理解。不理解也无所谓,能够看出我们的问题所在即可。

  • 首先看上面,对于p[0]=1; p[1]=2; 这两段代码,它们所对应的汇编代码,由于p[0]比较特殊,所以看p[2]的。上线的注释也是比较详细了,由此我们知道如果将指针当做数组来使用,首先需要取出指针所存储的地址,然后将地址值+4,然后在加了4的地址处赋值,这很明显是两次寻址操作。一次是从指针中取出地址,二是根据这个地址再找到相应的内存然后进行赋值。
  • 但是对于 a[2] = 3; 这段话,看上面的汇编代码,很明显,就是直接进行一次内存操作。这显而易见。

由此我们可以粗略的得出以下结论:

  1. 指针与数组不管在真么情况下,在二进制层面是完全不同的。尽管在语言书写的时候等效,但是效率是相差很大的
  2. 指针操作是先寻址,然后再对内存单元进行操作
  3. 数组是直接对内存单元进行操作

然后就是,在大多数情况下,编译器做了很多的工作,它让程序员可以更高效的写代码,所以在很多情况中,指针和数组在语言编写层面,是一样的,就像上线的示例代码一样。

3 解决疑问

上一节内容我们学会了指针与数组的一些区别,现在就来看看最开始的疑问,最开始main.c和define.c编译运行后,为什么会产生错误,并且为什么是段错误呢?下面就一点点揭开迷雾。

  • 首先我们要知道的前提知识点,C/C++编译器的天生缺陷
  • C/C++编译器由4个子部件组成,分别是预处理器,编译器,汇编器,链接器
  • 每个子部件之间独立工作,相互之间没有通信
  • 对于语法的检查与规范只在编译器(是指第二个子部件的编译器)编译阶段有效(如:类型约束和保护成员)
  • 编译器认为,每一个源文件都是相互独立的,对各个源文件单独进行编译(当然最后是需要将各个单独编译后的文件进行链接的)。这个是导致上面错误代码的直接原因。具体还看下面的分析。

那么对于上面的几条知识点,我们使用下面的图解进行说明:

在这里插入图片描述

  • 上面图示中说了在两个文件中类型不一致导致运行时错误,当然这是表面原因,并且如果是其他的类型(不是指针的类型),有可能就不会出错。所以我们还需要深挖这其中的错误。
  • 针对我们的代码的话,就是在main.c中将g_name声明为指针,那么编译器进行编译的时候,就是单独编译main.c文件,并且将g_name按照指针的方式进行编译。那么由第二节的内容知道,指针的操作是需要两次寻址的。 这里我我们先记住,下面的分析会用上。

为了能够更加清楚的说清楚问题,下面我们针对上述的main.c与define.c的编译的过程简单的用图表示一下:

在这里插入图片描述

  • 上面最后将define.c中的数组g_name的首地址与main.c中代表的指针g_name链接起来,具体如何链接呢?请看以下图示:
  1. 刚开始define.c中的g_name就是一个数组的首地址,如下图所示:

在这里插入图片描述

  1. 当将main.c中的指针g_name与上面的define.c中的g_name进行链接后,由于g_name是指针,占4字节,所以链接后如下图:

在这里插入图片描述

上面的图示分析如果能看懂的话,就知道g_name 是一个占有4字节的指针,而g_name 是一个指向数组首地址的值。如果我们注意到前面所说的指针作为数组是需要两次寻址操作的话,我们就应该知道,如果使用g_name 的话,首先将它存的地址:“D.T.” 取出来,可以看到,它本身应该存的是地址,但是现在是一串字符。然后用这个“地址”来寻址另一个内存地址处。到这里,就明了了,上面的一串字符所代表的地址处是一个未定义的,是一个野地址!!!也就是说在运行的时候,此时g_name是一个野指针!!!这必然会产生段错误了!!!

  • 这就是为什么,产生的错误是段错误。真正的原因归根结底是野指针的原因。

对于上面存在的问题,我们尽量使用以下的方法来解决:

  • 尽可能不使用跨文件的全局变量,也就是非static的全局变量
  • 当必须使用时,在统一固定的头文件中声明global.h
  • 其他源文件包含上述global.h即可

4 总结

  • 在进行总结前,这里务必再次将声明与定义的区别说明一下:
  1. 声明只是告诉编译器,目标存在,可使用
  2. 定义,是为目标分配内存(变量)或确定执行流(函数)
  3. 理论上,任何目标都需要先声明,再使用
  4. C/C++允许声明与定义的统一

下面是针对本文的指针与数组的区别的总结

  • C/C++语言中的指针与数组在某些语言层面上的使用时等价的
  • 指针与数组在二进制层面是完全不等的
  • C/C++编译器忽略了源码之间的依赖关系
  • 如果一定要使用跨文件之间的全局变量的话,最好将全局变量放到一个统一的头文件global.h中
  • 然后其他源文件包含global.h即可

对于上面的分析,如果没有懂,可以加左侧群,进群进行交流。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/422873.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

微软MIX11大会第一天主旨以及新产品发布总结

期盼已久的MIX11终于开幕了&#xff0c;虽然没有去现场&#xff0c;但是心情还是蛮激动的。 MIX11第一天Keynote实况大概1个多小时&#xff0c;其中介绍了下一代微软浏览器&#xff0c;新工具更新以及新产品发布等&#xff0c;下面总结一下MIX11第一天的主要话题。 1. Internet…

【Git、GitHub、GitLab】三 Git基本命令之创建仓库并向仓库中添加文件

前两篇文章已经学会了Git的基本命令与创建仓库的命令&#xff0c;点击链接查看上一篇文章&#xff1a;【Git、GitHub、GitLab】二 Git基本命令之建立Git仓库&#xff0c;本篇文章就来创建一个有模有样的仓库。该仓库中的代码是一个显示静态页面的小工程代码。 文章目录0 本文所…

Paul Graham:撼动硅谷的人(译文)

Paul Graham&#xff1a;撼动硅谷的人&#xff08;译文&#xff09; 作者&#xff1a; 阮一峰 日期&#xff1a; 2010年12月19日 为《黑客与画家》写"译者序"&#xff0c;遇到一个棘手的问题。 "应该如何介绍Paul Graham&#xff0c;才能让中国读者了解&#xf…

【软件开发底层知识修炼】二十八 C/C++中volatile的作用

上一篇文章学习了C/C中的指针与数组的区别&#xff0c;点击链接进行查看&#xff1a;【软件开发底层知识修炼】二十七 C/C中的指针与数组是不同的本篇文章将学习volatile关键字在C/C中的作用 文章目录1 实例代码分析2 问题分析3 解决方案4 拓展&#xff1a; const和volatile4 总…

计算char,short,int,long类型变量的取值范围

源自《The C Programming Language》P28 pr2-1&#xff1a; 编写一个程序以确定分别由signed及unsigned限定的char&#xff0c;short&#xff0c;int&#xff0c;long类型变量的取值范围。 参考代码&#xff1a; main.c 1 #include <stdio.h>2 #include <limits.h>…

EtherCAT主站实时性分析

转载自&#xff1a;https://blog.csdn.net/ethercat_i7/article/details/54018036 一、实时性的意义 在主从DC同步模式下&#xff0c;主站需要以非常精准的时间发送过程数据&#xff0c;如下图所示&#xff1a; 二、实时性的关键 如下图所示&#xff0c;影响实时性的关键因素是…

VNC源码研究(一)

VNC采用RFB通信协议。RFB ("remote 帧缓存 ") 是一个远程图形用户的简单协议&#xff0c;因为它工作在帧缓存级别上&#xff0c;所以它可以应用于所有的窗口系统&#xff0c;例如&#xff1a;X 11,Windows 和 Mac 系统。 独特的计算环境。 RFB 协议可进行可靠的传输…

枚举的一些常用操作

本章将介绍以下几点&#xff1a; 1、如何把其它类型转换为枚举类型&#xff1f; 2、如何把枚举中的值添加到下拉菜单中&#xff1f; 一、如何把其它类型转换为枚举类型&#xff1f; 我们回顾一下有关字符串与数字之间的转换&#xff0c;如&#xff1a; string strValue&quo…

10个开源免费的电子商务平台(转自伯乐在线)

如今&#xff0c;人们几乎可以在网络上购买到绝大部分东西&#xff0c;从电子产品、衣服&#xff0c;到机票预订和订餐。购物已转移到互联网&#xff0c;你所做的&#xff0c;只是需要付钱。当然&#xff0c;消费者会 非常注重网站的用户体验。所以&#xff0c;一个整洁安全的平…

【Git、GitHub、GitLab】五 git中裸仓库.git下的内容

上一篇文章学习了git的文件重命名与git -log 的系列命令的使用方法。点击链接查看上一篇文章&#xff1a;【Git、GitHub、GitLab】四 Git文件重命名的简单方法以及使用git log查看版本演变历史 本篇文章学习git中&#xff0c;在没有远端服务器的情况下&#xff0c;裸仓库.git中…

【Git、GitHub、GitLab】六 GIT中commit、tree和blob三个对象之间的关系

上一篇文章学习了git裸仓库.git中的内容&#xff0c;点击查看上一篇文章&#xff1a;【Git、GitHub、GitLab】五 git中裸仓库.git下的内容 本篇文章记录学习git中commit、tree和blob三个对象之间的关系。 首先需要会使用下面的命令&#xff1a; cat 命令&#xff0c; 功能&am…

【Git、GitHub、GitLab】十 将git仓库备份到本地

上一篇文章学习记录了工作中常用的一些git命令&#xff0c;点击链接查看&#xff1a;【Git、GitHub、GitLab】九 工作中非常重要的一些git用法 文章目录1 git的传输协议2 如何将git仓库备份到本地2.1 使用哑协议备份2.2 使用智能协议备份1 git的传输协议 哑协议与智能协议的区别…

java-XML

XML不再多说&#xff0c;XML 约束也不用说了&#xff0c;这里讲讲java如何对XML操作。 java中使用XML&#xff0c;目前常用的就是Jaxp(sun)和dom4j了&#xff0c;这里先讲讲java自带的Jaxp包 JAXP 开发包是J2SE的一部分&#xff0c;它由javax.xml、org.w3c.dom 、org.xml.sax 包…

【C语言进阶深度学习记录】一 数据类型的本质与变量的本质

今天学习C语言中的数据类型的本质与变量的本质 文章目录1 什么是数据类型2 变量的本质3 数据类型与变量的关系4 自定义数据类型与创建变量5 总结1 什么是数据类型 数据类型可以理解为固定内存大小的别名数据类型是创建变量的模子 如同下面的图示&#xff0c;各个数据类型是代…

使用第三方Markdown编辑器编辑为知笔记

前言 为知笔记默认的编辑器并没有预览功能&#xff0c;而提供的MD编辑器插件也并不是很好用&#xff0c;但为知笔记提供了可以使用第三方编辑器的功能&#xff0c;Typora编辑器是目前最优秀的Markdown编辑器之一&#xff0c;可以很好作为第三方编辑器。 Typora介绍 Typora是…

【C语言进阶深度学习记录】二 有符号与无符号

今天学习C语言中的有符号与无符号 文章目录1 计算机中的符号位1.1 有符号数的表示法1.2 无符号数的表示法1.3 signed 和 unsigned2 实验-当有符号数与无符号数进行运算3 错误的使用了unsigned4 总结1 计算机中的符号位 C语言中&#xff0c;数据类型的最高位&#xff0c;用于标…

【C语言进阶深度学习记录】三 浮点数(float) 在内存中的表示方法

相信大多数人知道整形数在内存中的分布方式&#xff0c;而且也能很容易写出其二进制的形式&#xff0c;但是对于浮点数&#xff0c;估计知道的人并不是很多今天学习在C语言中浮点数在内存中的表示方法 文章目录1 浮点数在内存中的存储方式1.1 浮点数的转换步骤1.2 浮点数的转换…

【C语言进阶深度学习记录】四 C语言中的类型转换

今天学习C语言中的类型转换&#xff0c;包括隐式类型转换和显示类型转换 文章目录1 C语言中的数据类型转换1.1 强制类型转换1.11 强制类型转换代码分析1.&#xff12; 隐式类型转换1.21 隐式类型转换代码分析2 总结1 C语言中的数据类型转换 C语言中&#xff0c;可以进行数据类…

【C语言进阶深度学习记录】五 C语言中变量的属性

上一篇文章学习了C语言中的类型转换&#xff0c;点击链接查看&#xff1a;【C语言进阶深度学习记录】四 C语言中的类型转换. 文章目录1 C语言的变量属性1.1 auto关键字1.2 register关键字1.3 static 关键字1.4 代码案例分析1.5 extern 关键字1.6 代码案例分析2 总结1 C语言的变…

【C语言进阶深度学习记录】六 C语言中的分支语句

文章目录1 if 语句的分析1.1 if 语句中零值比较的注意点2 switch 语句的分析3 if 与switch语句使用代码案例分析4 if语句与switch语句的互换5 总结1 if 语句的分析 if 语句根据条件选择执行语句else 不能独立存在&#xff0c;且总是与距离它最近的if匹配else 语句可以连接其他…