做dm素材网站,wordpress如何应用ssl,个人备案的公司网站,做网站的公司利润上几篇文章学习了ABI-应用程序二进制接口#xff1a;【软件开发底层知识修炼】二十六 ABI-应用程序二进制接口 学习总结文章目录本篇文章就指针与数组的联系与区别来学习学习 文章目录1 疑问2 指针与数组是不相等的3 解决疑问4 总结1 疑问
在具体用文字理论来说明指针与数组的… 上几篇文章学习了ABI-应用程序二进制接口【软件开发底层知识修炼】二十六 ABI-应用程序二进制接口 学习总结文章目录本篇文章就指针与数组的联系与区别来学习学习 文章目录1 疑问2 指针与数组是不相等的3 解决疑问4 总结 1 疑问
在具体用文字理论来说明指针与数组的区别之前先看一下下面的代码例子这两个程序输出的结果是一样的么不一样的话分别输出什么
main.c
#include stdio.hextern char* g_name;int main()
{define_print();printf(main() : %s\n, g_name);return 0;
}define.c
#include stdio.hchar 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.hint 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.outobjdump -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 //然后将eax480483c4: 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; 这段话看上面的汇编代码很明显就是直接进行一次内存操作。这显而易见。
由此我们可以粗略的得出以下结论
指针与数组不管在真么情况下在二进制层面是完全不同的。尽管在语言书写的时候等效但是效率是相差很大的指针操作是先寻址然后再对内存单元进行操作数组是直接对内存单元进行操作
然后就是在大多数情况下编译器做了很多的工作它让程序员可以更高效的写代码所以在很多情况中指针和数组在语言编写层面是一样的就像上线的示例代码一样。
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链接起来具体如何链接呢请看以下图示
刚开始define.c中的g_name就是一个数组的首地址如下图所示 当将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 总结 在进行总结前这里务必再次将声明与定义的区别说明一下 声明只是告诉编译器目标存在可使用定义是为目标分配内存变量或确定执行流函数理论上任何目标都需要先声明再使用C/C允许声明与定义的统一 下面是针对本文的指针与数组的区别的总结
C/C语言中的指针与数组在某些语言层面上的使用时等价的指针与数组在二进制层面是完全不等的C/C编译器忽略了源码之间的依赖关系如果一定要使用跨文件之间的全局变量的话最好将全局变量放到一个统一的头文件global.h中然后其他源文件包含global.h即可 对于上面的分析如果没有懂可以加左侧群进群进行交流。