本文主要介绍C语言数组越界的详细解释及其避免方法。通过示例代码进行了非常详细的介绍,对大家的学习或工作有一定的参考价值。有需要的朋友下面和边肖一起学习。
所谓数组越界,简单来说就是指索引组下标变量的值超过了初始定义的大小,导致对数组元素的访问出现在数组的范围之外。这种错误也是C语言程序中最常见的错误之一。
在C语言中,数组必须是静态的。换句话说,数组的大小必须在程序运行之前确定。由于C语言不具备类似于Java等语言中已有的静态分析工具的功能,可以严格检查程序中数组下标值的范围。一旦发现数组溢出或下溢,程序将通过抛出异常来终止。也就是说,C语言不检查数组边界,数组两端都有可能越界,从而破坏其他变量的数据甚至程序代码。
所以数组下标的范围只能预先推断出一个值来确定数组的维数,检查数组的边界是程序员的职责。
一般来说,数组的越界错误有两种:数组下标的越界值和指针指向数组的越界范围。
数组下标值超出界限
数组下标越界值主要是指访问数组时,下标的值不在定义的数组范围内,而是访问了无法获取的内存地址。比如对于数组int a[3],它的下标范围是[0,2](即a[0],a[1],a[2])。如果我们的值不在这个范围内(比如a[3]),就会出现越界错误。代码如下:
int a[3];
int I=0;
for(I=0;i4;我)
{
a[I]=I;
}
for(I=0;i4;我)
{
printf('a[%d]=%d\n ',I,a[I]);
}
显然,在上面的示例程序中,访问a[3]是非法的,并且会发生越界错误。因此,我们应该将上述代码修改为以下形式:
int a[3];
int I=0;
for(I=0;i3;我)
{
a[I]=I;
}
for(I=0;i3;我)
{
printf('a[%d]=%d\n ',I,a[I]);
}
指向数组的指针超出范围。
当指向数组的指针超出范围时,意味着在定义数组时,会返回一个指向第一个变量的头指针。加上或减去这个指针可以将这个指针向前或向后移动,然后访问数组中的所有变量。但是在移动指针的时候,如果不注意移动的次数和位置,就会使指针指向数组之外的位置,导致数组越界错误。下面的示例代码是在移动指针时不考虑移动的次数和数组的范围,使程序访问数组外的存储单元。
int I;
int * p;
int a[5];
/*数组A的头指针被赋给指针p*/
p=a;
for(I=0;i10我)
{
/*指针p指向的变量*/
* p=i 10
/*指针p下一个变量*/
p;
}
在上面的示例代码中,for循环将指针P向后移动10次,并为指针每次指向的单元格赋值。但是,这里数组A的下标范围是[0,4](即a[0],a[1],a[2],a[3]和a[4])。因此,后五个操作会将值赋给未知的内存区域,而这种对未知内存区域的赋值会导致系统错误。正确的操作应该是指针移动的次数与数组中变量的数量一样多,如下面的代码所示:
int I;
int * p;
int a[5];
/*数组A的头指针被赋给指针p*/
p=a;
for(I=0;i5;我)
{
/*指针p指向的变量*/
* p=i 10
/*指针p下一个变量*/
p;
}
为了加深大家对数组越界的理解,这里用一个完整的数组越界的例子来演示编程中数组越界会导致什么问题。
#定义密码' 123456 '
int测试(char *str)
{
int标志;
char buffer[7];
flag=strcmp(str,PASSWORD);
strcpy(缓冲区,str);
返回标志;
}
int main(void)
{
int flag=0;
char string[1024];
while(1)
{
Printf('请输入密码:');
scanf('%s ',str);
flag=Test(str);
if(标志)
{
printf('密码错误!\ n’);
}
其他
{
Printf('密码正确!\ n’);
}
}
返回0;
}
上面的示例代码模拟了一个密码验证的例子,将用户输入的密码与宏定义中的密码“123456”进行比较。显然,这个例子中最大的设计缺陷是Test()函数中的strcpy(buffer,str)调用。
因为程序将用户输入的字符串原封不动地复制到Test()函数的数组char buffer[7]中。因此,当用户的输入大于7个字符的缓冲区时,就会发生数组越界错误,这也就是所谓的缓冲区溢出漏洞。但需要注意的是,如果我们根据此时缓冲区溢出的具体情况来填充缓冲区,不仅会防止程序崩溃,还会影响程序的执行进程,甚至会使程序执行缓冲区中的代码。运行结果如下:
请输入密码:12345。
密码错误!
请输入密码:123456。
密码正确!
请输入密码:1234567。
密码正确!
请输入密码:aaaaaaa
密码正确!
请输入密码:0123456。
密码错误!
请输入密码:
在示例代码中,flag变量实际上是一个标志变量,它的值将决定程序是进入“密码错误”(0以外)的过程还是进入“密码正确”(0)的过程。当我们输入错误的字符串“1234567”或“aaaaaaa”时,程序也会输出“密码正确”。但当输入“0123456”时,程序输出“密码错误”。为什么?
其实原因很简单。当调用Test()函数时,系统会给它分配一个连续的内存空间,变量char buffer[7]和int flag并排存放,用户输入的字符串会被复制到buffer[7]中。这时,如果我们输入的字符串个数超过6个(注意有一个字符串截断字符,算一个),那么超出的部分就会破坏与之相邻的标志变量的内容。
当输入的密码不是宏定义的“123456”时,字符串比较将返回1或-1。众所周知,内存中的数据是以4字节(DWORD)的逆序存储的,所以当flag为1时,内存中存储的是0x01000000。如果我们输入了一个7个字符的错误密码,比如“aaaaaaa”,那么字符串截断0x00就会被写入flag变量,这样溢出数组的一个字节0x00000000就正好把逆序存储的flag变量改成0x000000。函数返回后,一旦主函数的标志为0,就会输出“密码正确”。这样,我们用错误的密码来获得正确密码的运行效果。
至于“0123456”,因为比“123456”小,flag的值为-1,所以负数会按照补码存储在内存中,所以实际存储的不是0x01000000,而是0xffffffff。然后,字符串被截断后,符号0x00变成0x00ffffff,或者不为0,所以没有进入正确的分支。
这个例子实际上只是用一个字节淹没了相邻变量,导致程序以正确的密码进入处理流程,使设计的验证函数失效。
尝试显式指定数组的边界。
在C语言中,为了提高运行效率,给程序员更多的空间,给指针操作带来更多的方便,C语言本身并不检查数组下标表达式的值是否在合法的范围内,也不检查指向数组元素的指针是否移出了数组的合法区域。因此,在编程中使用数组时,一定要格外谨慎,读写数组时要进行检查,避免数组上的操作超出数组边界而导致缓冲区溢出漏洞。
为了避免数组越界引起的程序错误,我们需要从数组的边界定义开始。尝试显式指定数组的边界,即使它已由初始化值列表隐式指定。代码如下:
int a[]={1,2,3,4,5,6,7,8,9,10 };
显然,对于上面的数组a[],虽然编译器可以根据初始化值列表计算出数组的长度。但是,如果我们显式指定这个数组的长度,例如:
int a[10]={1,2,3,4,5,6,7,8,9,10 };
不仅使程序可读性更强,而且当数组长度小于初始化值列表长度时,大多数编译器都会给出相应的警告。
当然,也可以用宏的形式显式指定数组的边界(事实上,这也是最常用的指定方法),如下面的代码所示:
#定义最多10个
…
int a[MAX]={1,2,3,4,5,6,7,8,9,10 };
此外,在C99标准中,我们可以使用单个指示器为数组的两个段“分配”空间,如下面的代码所示:
int a[MAX]={1,2,3,4,5,[MAX-5]=6,7,8,9,10 };
在上面的a[MAX]数组中,如果MAX大于10,数组中间会用0值元素填充(填充的个数是MAX-10,0值填充从a[5]开始);如果MAX小于10,则“[MAX-5]”之前的五个元素(1,2,3,4,5)中的几个将被“[MAX-5]”之后的五个元素(6,7,8,9,10)覆盖。示例代码如下:
#定义最多10个
#定义最大值1 15
#定义最大值2 6
int main(void)
{
int a[MAX]={1,2,3,4,5,[MAX-5]=6,7,8,9,10 };
int b[MAX1]={1,2,3,4,5,[MAX1-5]=6,7,8,9,10 };
int c[MAX2]={1,2,3,4,5,[MAX2-5]=6,7,8,9,10 };
int I=0;
int j=0;
int z=0;
printf(' a[MAX]:\ n ');
for(I=0;iMAX我)
{
printf('a[%d]=%d ',I,a[I]);
}
printf(' \ nb[max 1]:\ n ');
for(j=0;jMAX1j)
{
printf('b[%d]=%d ',j,b[j]);
}
printf(' \ NC[max 2]:\ n ');
for(z=0;zMAX2z)
{
printf('c[%d]=%d ',z,c[z]);
}
printf(' \ n ');
返回0;
}
运行结果是:
a[MAX]:
a[0]=1a[1]=2a[2]=3a[3]=4a[4]=5a[5]=6a[6]=7a[7]=8a[8]=9a[9]=10
b[MAX1]:
b[0]=1 b[1]=2 b[2]=3 b[3]=4 b[4]=5 b[5]=0 b[6]=0 b[8]=0 b[9]=0 b[10]=6 b[11]=7 b[12]=8 b[13]=9 b[14]=10
c[MAX2]:
c[0]=1 c[1]=6 c[2]=7 c[3]=8 c[4]=9 c[5]=10
检查数组是否越界,以确保索引值在合法范围内。
为了避免数组的边界,除了如上所述显式指定数组的边界之外,还可以在使用数组之前检查数组的边界和字符串(也存储为数组)的结尾,以确保数组的索引值在合法范围内。比如写一个处理数组的函数,一般要有一个range参数;总是检查在处理字符串时是否遇到空字符' \ 0 '。
看一下下面的代码示例:
#定义数组编号10
int *TestArray(int num,int value)
{
int * arr=NULL
arr=(int *)malloc(sizeof(int)* ARRAY _ NUM);
如果(arr!=空)
{
arr[num]=值;
}
其他
{
/*句柄arr==NULL*/
}
返回arr
}
从上面的“int*TestArray(int num,int value)”函数不难看出,有一个明显的问题,就是不能保证num参数越界(即num=ARRAY_NUM时)。因此,num参数应该被检查到边界之外,示例代码如下:
int *TestArray(int num,int value)
{
int * arr=NULL
/*跨境检查(跨境)*/
if(numARRAY_NUM)
{
arr=(int *)malloc(sizeof(int)* ARRAY _ NUM);
如果(arr!=空)
{
arr[num]=值;
}
其他
{
/*句柄arr==NULL*/
}
}
返回arr
}
这样,“if(numARRAY_NUM)”语句用于越界检查,从而保证NUM参数不会越过这个数组的上限。现在看来,TestArray()函数应该没有问题,不会出现越界错误。
但是仔细检查的话,TestArray()函数还是有一个致命的问题,就是没有检查数组的下界。由于这里的num参数类型是int类型,所以它可能是负数。如果num参数传递的值是负数,将导致在arr引用的内存边界之外写入。
当然,您可以通过向“if(numARRAY_NUM)”语句添加另一个条件来测试它,如下面的代码所示:
if(num=0numARRAY_NUM)
{
}
但是这样的函数形式对调用者是不友好的(因为是int类型,所以还是可以传递负数给调用者,但是在函数中如何处理就是另一回事了)。因此,最好的解决方案是将num参数声明为size_t类型,从根本上防止它传递负数。示例代码如下:
int *TestArray(size_t num,int value)
{
int * arr=NULL
/*跨境检查(跨境)*/
if(numARRAY_NUM)
{
arr=(int *)malloc(sizeof(int)* ARRAY _ NUM);
如果(arr!=空)
{
arr[num]=值;
}
其他
{
/*句柄arr==NULL*/
}
}
返回arr
}
获取数组长度时,不要对指针应用sizeof运算符。
在C语言中,sizeof这个丑家伙经常让无数程序员叫苦不迭。同时也是各大公司必备的面试题目。简单来说,sizeof是单目算子,不是函数。它的功能是返回一个操作数占用的内存字节数。其中,操作数可以是表达式,也可以是用括号括起来的类型名,操作数的存储大小由操作数的类型决定。例如,对于数组int a[5],可以使用“sizeof(a)”来获取数组的长度,使用“sizeof(a[0])”来获取数组元素的长度。
但需要注意的是,sizeof运算符不能用于函数类型、不完整类型(存储大小未知的数据类型,如存储大小未知的数组类型、内容未知的结构或并集类型、void类型等。)和位字段。例如,以下都是不正确的形式:
/*如果此时max定义为int max();*/
尺寸(最大值)
/*如果arr被定义为char arr[MAX]并且MAX未知*/
sizeof(数组)
/*不能用于void类型*/
sizeof(空)
/*不能用于位域*/
结构S
{
无符号整数f1:1;
无符号整数F2:5;
无符号int F3:12;
};
sizeof(s . f1);
理解sizeof运算符后,现在来看下面的示例代码:
void Init(int arr[])
{
size _ t I=0;
for(I=0;isizeof(arr)/sizeof(arr[0]);我)
{
arr[I]=I;
}
}
int main(void)
{
int I=0;
int a[10];
init(a);
for(I=0;i10我)
{
printf('%d\n ',a[I]);
}
返回0;
}
从表面上看,上面代码的输出结果应该是“0,1,2,3,4,5,6,7,8,9”,但实际结果却出乎我们的意料,如图1所示。
是什么导致了这个结果?
显然,上面的示例代码在“void Init(int arr[])”函数中接收了一个“int arr[]”类型的形参,并在main函数中向其传递了一个“a[10]”参数。同时,在Init()函数中,这个数组元素的个数和初始化值是由“sizeof(arr)/sizeof(arr[0])决定的。
这里有一个很大的问题:由于arr参数是形参,所以是指针类型,结果是“sizeof(arr)=sizeof(int*)”。在IA-32中,“sizeof(arr)/sizeof(arr[0])的结果是1。因此,最终结果如图1所示。
对于上面的示例代码,我们可以通过传入数组的长度来解决这个问题。示例代码如下:
void初始化(int arr[],size_t arr_len)
{
size _ t I=0;
for(I=0;iarr _ len我)
{
arr[I]=I;
}
}
int main(void)
{
int I=0;
int a[10];
Init(a,10);
for(I=0;i10我)
{
printf('%d\n ',a[I]);
}
返回0;
}
除此之外,我们还可以借助指针来解决上述问题。示例代码如下:
void Init(int (*arr)[10])
{
size _ t I=0;
for(I=0;I sizeof(* arr)/sizeof(int);我)
{
(* arr)[I]=I;
}
}
int main(void)
{
int I=0;
int a[10];
init(a);
for(I=0;i10我)
{
printf('%d\n ',a[I]);
}
返回0;
}
现在,Init()函数中的arr参数是一个指向“arr[10]”类型的指针。需要注意的是“void Init(int(*arr)[])”在这里绝对不能用来声明函数,但是必须指明要传入的数组的大小,否则无法计算出“sizeof(*arr)”。但是,在这种情况下,通过sizeof计算数组大小是没有意义的,因为数组大小已经被指定为10。
关于解释C语言数组越界以及如何避免越界的文章到此为止。有关C语言数组越界的更多信息,请搜索我们以前的文章或继续浏览下面的相关文章。希望大家以后能多多支持我们!
郑重声明:本文由网友发布,不代表盛行IT的观点,版权归原作者所有,仅为传播更多信息之目的,如有侵权请联系,我们将第一时间修改或删除,多谢。