引言
最近在查一个bug,查到最后发现是数组越界导致的。数组只有30个字节,代码却向这个数组填充了35个数据,这个bug还是偶现的,查到它确实废了一番功夫。我就突然想到:C语言为什么不检查数组下标呢???先来个demo验证下。
#include<stdio.h>
#include<stdlib.h>
int main()
{
int data[5]={0};
for(int i=0;i<8;++i)
{
printf("%d ",data[i]);
}
printf("\n");
return 0;
}
结果显示,C语言还真的不检查数组的下标。不仅没有报错,而且运行正常。
思考
这就让我陷入了思考,C语言为什么不检查下标呢?想上文这么简单的,data数据组就5个数据,编译器是知道的,为什么是访问第8个数据时,编译器来个报错也没有呢?我想到了之前的文章《指针与数组》中有如下示例代码:
void main()
{
int data[4] = {0, 1, 2, 3};
int *p;
p = data +2;
printf("p[-1] is %d\n",p[-1]);
printf("*(p-1) is %d\n",*(p-1));
}
运行结果如下:
不仅可以编译通过,还能正确的输出结果为1。这表明,C的下标引用和间接访问表达式是一样的。
这让我突然意识到,数组的这些特性,如数组名本质上是一个常量指针(不懂的同学看之前的推文《指针与数组》)C语言很难检查下标合法性的。
如果C语言检查数组是否越界,因为当数组出现在表达式中的时候,它会立刻被解读成指针。此外,使用其他的指针变量也可以指向数组的任意元素,并且这个指针可以随意进行加减运算。引用数组元素的时候,虽然你可以写成a[i],但是它只不过是*(a+i)的一种表达,C语言本身的语法是无法检查的,只能通过编译器检查。
那么编译器将加入额外的代码用于检测数组是否越界,C的下标检查所涉及的开销比你开始想象的要多。编译器必须在程序中插入指令,证实下标的结果所引用的元素和指针表达式所指向的元素属于同一个数组,可能仅仅是个小功能,生成的程序的数组检查占有大量的代码空间,这必将影响程序的运行效率。
这也让我意识到一个事情:数组的标识符(也就是数组名),它只包含并没有包含数组的长度的信息,它只是个地址信息,也就是上面说的数组名本质上是个常量指针。读到这里,请你想一下,C语言有提供数组长度的底层函数吗???
答案是否定的,一般情况下,我们获取一个数组的长度,我们可以获取数组所占的内存大小,然后除以单个元素的内存大小计算数组长度。
int a[8];
printf("%d",sizeof(a)/sizeof(a[0]));
为什么不修复“漏洞”
既然我们发现了上述问题,那么那些C语言的大神为什么不修复这个“漏洞”呢?其他编程语言会吸取“教训”吗?学过JAVA的同学可以看下面代码:
int [][] array = {{1,2,3},{1,4}};
System.out.println(array[1][2]);
这也是一个数组越界访问的例子,但是JAVA的控制台会打印如下信息:
Exception in thread "main" java.lang.ArrayIndexOutOfBoundsException: 2
at demo.Array.main(Array.java:31)。
会明确告诉你数组下标越界了,是的,高级语言JAVA是支持的。
那么我们就来讲讲C语言的设计目标:提供一种能以简易的方式编译、处理低级存储器、仅产生少量的机器码以及不需要任何运行环境支持便能运行的编程语言。
如果C语言加入了类似下标检查,实现一个简单的数组数据写入,需要大量指令检查下标是否正确,那么还符合C语言设计目标吗?如果C语言有大量的这样设计,操作系统内核还会使用C语言编写吗?单片机等实时系统还会使用C语言吗?
所以C语言给了程序员更大空间,C语言执行效率高,可以直接访问硬件,具有非常好的可移植性,所以世界上绝大部分的操作系统内核都是用C语言编写的。
那么问题来了,JAVA都检查了数组下标,C语言难道一点进步也没有吗?其实也不然,微软在这一方面也做了贡献。
在早期的CRT函数中也不对字符串指针或数组进行越界检查,都是要求程序员确保空间足够,因此也才也才有了在VS2005之后微软提供的安全的CRT函数版本。(CRT函数不是本文的重点,不懂的同学请面向百度编程)。
总结
C语言为什么不检查数组下标???答案一个字:快。