在C语言中指针和数组的使用方法几乎是一样的,但他们的确是完全不同的两个东西,如果不能很好的区分,则可能会带来一些问题,想要发现其中的区别,我们则需要从汇编层面观察。
头文件中的声明对于编译器是一个非常重要的检查手段,但有时候为了方便,我们会直接在C文件中声明外部的符号,这样做编译器将无法判断声明和定义是否一致,可能会带来隐患,非常不推荐这样做。
下面两个文件演示了在一个C文件中定义了一个数组,而在另一个C文件中不小心将它声明成了指针,可以发现该程序编译正常,但运行的时候出现了段错误。
a.c
#include
volatile uint8_t share_memory[128];
- 1
- 2
b.c
#include
extern uint8_t *share_memory; int main(void) { share_memory[0] = 0xAA; return 0; }
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
编译运行,编译正常,运行失败
$ gcc a.c b.c && ./a.out Segmentation fault (core dumped)
- 1
- 2
避免该错误的方式就是定义一个a.h
头文件,在头文件中进行声明,然后两个C文件都包含该头文件。这样做,当声明和定义不一致的时候编译器就会报错。
a.h
#ifndef _A_H_ #define _A_H_ extern uint8_t *share_memory; #endif /* _A_H_ */
- 1
- 2
- 3
- 4
- 5
- 6
a.c
#include
#include "a.h" volatile uint8_t share_memory[128];
- 1
- 2
- 3
b.c
#include
#include "a.h" int main(void) { share_memory[0] = 0xAA; return 0; }
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
编译运行,编译失败
$ gcc a.c b.c && ./a.out a.c:3: error: conflicting types for 'share_memory' # 编译器提示类型冲突 a.h:4: note: previous declaration of 'share_memory' was here
- 1
- 2
- 3
我对csky的指令集比较熟悉,下面就采用csky-abiv2-elf-gcc
编译的结果来查看指针和数组的区别。
下面这个文件分别定义了指针和数组,又定义了4个函数分别返回指针指向的第一个元素以及数组的第一个成员。
main.c
int *ptr; int array[1]; int func_ptr1(void) { return *ptr; } int func_ptr2(void) { return ptr[0]; } int func_array1(void) { return *array; } int func_array2(void) { return array[0]; }
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
编译,并反汇编,也可以直接汇编,但汇编的结果不够简洁
$ csky-abiv2-elf-gcc -c main.c -S -O2 -mcpu=ck802 -o main.asm # 这句话是直接汇编,main.asm中保存了汇编的内容 $ csky-abiv2-elf-gcc -c main.c -O2 -mcpu=ck802 -o main.o && csky-abiv2-elf-objdump -S main.o > main.asm # 这句话是编译然后反汇编,main.asm中保存了汇编的内容 $ cat main.asm # 摘录部分内容 ... <func_ptr1>: /* func_ptr2和这个是一样的 */ lrw r3, 0x0 // 8 <func_ptr1+0x8> /* 将ptr这个指针变量的地址加载到r3寄存器 */ ld.w r3, (r3, 0x0) /* 将r3寄存器的值作为地址,读取该地址中的数据,放到r3中,即指针的值 */ ld.w r0, (r3, 0x0) /* 将r3寄存器的值作为地址,读取该地址中的数据,放到r0中,即指针指向的值 */ jmp r15 /* r0存储着返回值,这句话让函数返回 */ ... <func_array1>: /* func_array2和这个是一样的 */ lrw r3, 0x0 // 8 <func_array1+0xc> /* 将array这个数组的地址加载到r3寄存器 */ ld.w r0, (r3, 0x0) /* 将r3寄存器的值作为地址,读取该地址中的数据,放到r0中,即数组的第一个值 */ jmp r15 /* r0存储着返回值,这句话让函数返回 */ ...
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
通过上面的汇编发现,指针引用数据会比数组索引数据多一条指令,这也是解引用
这个名字的由来。指针本质是一个变量,占有存储空间,汇编需要先访问这个变量,再将变量的值作地址,去访问其指向的值。而数组名本质是一个符号,代表一个常数值(地址值),不占用空间,访问这个地址自然拿到的就是其中的成员。
C语言自由灵活,也存在许多陷阱,有些陷阱需要通过汇编来解释,C语言开发,需要掌握一些基本的汇编知识。