概述
在 c 语言中,一个数组变量可以理解成定义了一个指向数组的指针。大多数情况下这样的理解是没有问题的,但是还是要弄清楚他们的区别,否则会导致错误。本文会从因为混淆他们所导致的错误,揭示一个 C 语言鲜为人知的特点。
问题实例
// 在 test.c 文件定义一个全局的数组变量
char g_name[] = {'Y','u','n','\0'};
#include<stdio.h>
// 用指针的引用 test.c 定义的全局变量
extern char* g_name;
int main()
{
printf("%c\n",g_name[0]);
return 0;
}
对于这样的用法,对程序进行编译的时候没有出现错误,但是程序运行的时候程序就会奔溃。
修正
#include<stdio.h>
// 用数组的方式,引用 test.c里面的数组
extern char g_name[];
int main()
{
printf("%c\n",g_name[0]);
return 0;
}
能够正常运行
问题分析
数组的内存模型
#include <stdio.h>
int main()
{
char name[] = {'Y','u','n','\0'};
printf(" Addr of name:%p\n",name);
printf(" Addr of name[0]:%p\n",&name[0]);
printf("Content of name[0]:0x%x (%c)\n",name[0],name[0]);
printf(" Addr of name[1]:%p\n",&name[1]);
printf("Content of name[1]:0x%x (%c)\n",name[1],name[1]);
printf(" Addr of name[2]:%p\n",&name[2]);
printf("Content of name[2]:0x%x (%c)\n",name[2],name[2]);
printf(" Addr of name[3]:%p\n",&name[3]);
printf("Content of name[3]:0x%x (%c)\n",name[3],name[3]);
return 0;
}
Addr of name:0022CCD0
Addr of name[0]:0022CCD0
Content of name[0]:0x59 (Y)
Addr of name[1]:0022CCD1
Content of name[1]:0x75 (u)
Addr of name[2]:0022CCD2
Content of name[2]:0x6e (n)
Addr of name[3]:0022CCD3
Content of name[3]:0x0 ( )
从上面的结果可以看出C 语言数组的变量名可认为代表数组的开始地址,从数组的开始地址处每个数组的地址都是如下的顺序出现的
指针的内存模型
#include <stdio.h>
int main()
{
char name[] = {'Y','u','n','\0'};
char *p_name = &name[0];
printf(" Addr of name:%p\n",name);
printf(" Addr of pname:%p\n",&p_name);
printf("Content of pname:0x%x \n",p_name);
printf("Content of pname[0]:0x%x (%c)\n",p_name[0],p_name[0]);
printf("Content of pname[1]:0x%x (%c)\n",p_name[1],p_name[1]);
printf("Content of pname[2]:0x%x (%c)\n",p_name[2],p_name[2]);
printf("Content of pname[3]:0x%x (%c)\n",p_name[3],p_name[3]);
return 0;
}
Addr of name:0022cccc
Addr of pname:0022ccd0
Content of pname:0x0022ccd0
Content of pname[0]:0x59 (Y)
Content of pname[1]:0x75 (u)
Content of pname[2]:0x6e (n)
Content of pname[3]:0x0 ( )
问题成因
当从test.c的角度看 g_name 是定义在这一文件中的字符数组。由于 g_name 是定义好的全局变量,所以它的内存分配是在 .data 段。但是在外部以指针的方式引用的时候,从它的角度而言,g_name是一个在其他文件定义的指针变量,并且它只是引用这一变量,当他们的.o文件连接的时候变量会被当成同一个。由于在 wrong.c 里面,它被定义成指针那么它回去访问 0x006e7559 这个地址,但是这个地址可能是不合法的所以可能会出现段错误的情况。最主要的一点的原因是:当一个文件被编译成目标文件的时候,目标文件中将不会包含C语言的任何信息。
链接的过程就是将所有的同名符号合成一个。链接器发现不了目标文件中符号不匹配的问题
预防措施
如果链接器不能发现符号不匹配问题,那就只能依靠编译器了。在编写程序的时候应当将一个外部需要引用的变量或者函数放在一个头文件中,然后在定义和引用的源文件中包含它,这种包含就能够建立起联系。有了这种联系编译器就能够发现问题。关于声明和引用不匹配的问题同样会发生在函数的使用上面,当我们进行 extern 使用的时候,要格外的注意。
总结
通过这个问题,回顾了指针和数组的内存模型,并揭示了链接器并不关系 C 语法的这一事实。
为了避免产生混淆指针与数组产生的问题,我们需要养成总是将头文件作为(变量和函数的)定义和引用的桥梁这一编程习惯。