菜鸟笔记
提升您的技术认知

数组越界

计算机内存中每个存储单元都有其存储地址,根据存储地址即可准确地找到该内存单元。通常把这个地址称为指针。在C语言中,指针变量就是用来存放内存单元地址的变量类型,简称指针。灵活的使用指针可以表示各种数据结构,动态地分配内存,有效地处理数组,从而编写出精炼而又高效的程序。但是,在指针这种直接访问内存的方便有效的同时,也很容易因使用的错误而给系统带来隐患。

1 C语言中常见的指针错误使用

1.1 指针变量未赋初始值

指针变量未赋初始值容易产生野指针。野指针是指指向不可用内存区域的指针。它不像空指针能够通过if语句比较容易的进行判断,从而避免非法的访问。通常对野指针进行操作的话,将会产生不可预知的错误,甚至使程序崩溃。若指针没有被赋予其初始值,则它们的值不会自动初始化,而是随机的,从而无法判断指针是否指向了合理的内存空间。因此,在创建指针变量的时候应当及时对其进行初始化,方法是将指针设置为NULL,或者将它指向合法的内存。

1.2 指针指向的内存已经被释放

在使用指针进行动态内存分配操作时,指针p被free或者delete之后,指针变量本身并没有被删除掉,若没有置为NULL,会让人误以为p是个合法的指针而在以后的程序中错误的使用它。这时候通常又会使用语句if (p != NULL)进行判断以避免发生错误。但是,此时,p虽不是NULL指针,但它也不指向合法的内存块,所以不会收到防错的效果。例如:

void text()
{
    char *p = (char *) malloc(50);
    if(p != NULL)
    strcpy(p, “hello”);
    free(p); // p指向不合法的内存
    if(p != NULL) // 不会收到防错的效果
    strcpy(p, “world”); //发生错误
}

在这个程序中,应该在第四行之后加上p=NULL; 即在p释放之后就直接将

P置为NULL,就不会发生后续错误。

1.3 指针引起的内存泄露

C语言中内存的申请与释放均是由程序员控制,并不像Java中自带有自动垃圾回收机制,虚拟机会释放已经没有用的内存。当程序员在使用完所申请的内存之后,却并没有释放它们的时候,就出现了内存泄露。一旦泄露达到一定的程度,就会导致系统崩溃。

1.4 指针被重复释放

在上面我们讨论了指针没有及时释放而造成内存泄露的情况,但是,反之,若对于已申请的内存进行了重复性释放,同样会使可执行程序产生致命的错误。例如[2]:

void text1 () {
    int *p = new int (10) ;
    text2 (p) ;
}

void text2 (int * p) {
    if (…)
        delete p;
    else
        text3 (p) ;
}
void text3 (int * p) {
    if (…)
        delete p;
    else
        text4 (p) ;
}

此时函数text1中指针p申请了内存,并传递给text2。然后函数text2中进行判断1(第6行),若条件成立则释放指针p,否则将指针p继续传递给函数text3。函数text3进行与text2一样的操作,先进行判断2(第12行),若成立则释放p,否则继续传递给其他函数。

然而,若判断1和判断2在某些情况下会同时成立时,就出现了重复释放指针p的情况,也就造成了内存的重复释放。重复释放内存会导致程序异常终止而发生难以预知的错误,并且往往这种错误难以发现。这时,我们可以采用在释放内存前先检查内存是否为空,然后释放内存的同时,对指针p进行赋空值的操作,这样就会避免这种情况的发生。

1.5 指针的使用超出其作用范围

C语言中变量可分为局部变量和全局变量,而局部变量只能在其生成期有效。而当对局部变量使用指针时,指针应当使用在局部变量的作用域内来进行对局部变量的访问。否则,该指针也指向了不合法的内存而发生错误。这种情况下,我们应该避免使用作用域大的指针变量指向作用域小的变量。

2 指针与数组

在C语言中,数组是一种数据单元的序列。在访问数组元素时,除了可以用下标访问以外,还可以通过指针来访问数组变量。

2.1 指针与一维数组

一维数组是一个线性表,它是按顺序存放在一片连续的存储单元中。数组元素由数组名和整数表示的下标表示。其中,数组名是数组变量在内存中的起始地址。在C语言中对数组元素进行访问时,都是通过数组名加上起始地址的相对量(即下标)来得到要访问元素的地址,然后再实现对其内容的访问。实际上,在编译系统中,是将数组元素的形式首先转换为*(a+j),然后才进行其地址的计算得到要访问元素的地址。由此可见,C语言对数组的处理,实际上是转换成指针地址的运算。

例如:

void main()

{
    int a[5]={1,2,3,4,5};
    int *p,i;
    p=a;//指针p指向数组第一个元素,等价于p=&a[0]
    for(i=0; i <5; i++)
    {
        printf(“%d ”,*p);//输出指针指向的内容
        p++;
    }
}


此程序运行结果为:

1 2 3 4 5

2.2指针与多维数组

多维数组均可分解为多个一维数组来处理,以多维数组的列数作为这些一维数组的长度。

这里我们以二维数组为例。二维数组是按行存放的,所以也可以把二维数组看成是特殊的一维数组。如图1所示。

例如:

int a[2][3]={{1,3,5},{2,4,6}};

我们可以假设数组a在内存中的分配情况如表1所示[3]。

当用指针变量指向这个二维数组时,引用元素a[i][j]的地址将换算为*(a+i)+j ,该元素的值为*(*(a+i)+j)。还需要注意的是,若p=a[0],在上述例子中的操作p++此时不是指向a[0][1],而是指向a[1],即p的增值是以一维数组的长度为单位,此处一维数组的长度就是指二维数组的列数。

3 数组越界

数组访问越界是指数组下标变量的取值超过了初始定义时的大小, 导致对数组元素的访问出现在数组的范围之外, 这类错误是C语言程序中的常见错误[4]。在C语言中数组的两端都有可能越界而使其他变量的数据甚至程序代码被破坏。因为在C语言中,数组必须是静态的。换而言之,数组的大小必须在程序运行前就应当被确定下来。由于C语言并不具有Java中现有的静态分析工具的功能[5],可以对程序中数组下标取值范围进行严格检查,一旦发现数组上溢或下溢,都会抛出异常而终止程序,因此,数组下标的取值范围只能进行预先推断一个值来确定数组的维数。因此对于C语言来说,数组的边界检验是程序员的职责。

3.1 数组越界错误的分类

数组越界错误主要包括数组下标取值越界和指向数组的指针的指向范围越界。数组下标取值越界主要是指访问数组的时候,下标的取值不在已定义好的数组的取值范围,而访问的是无法获取的内存地址。例如:int a[10],则此时数组a的下标取值范围是[0,9]。若取值不在这个范围,则就出现了越界错误。而指向数组的指针的指向范围越界与前面所讲述的指针与数组有一定联系。只是当定义的指针p若指向了数组的首地址时(即p=a),若对其不断进行操作p++,则最后会导致指针p指向大于该数组的范围的上界,从而也是使程序访问了数组以外的存储单元,造成数组越界。

3.2 数组越界的环境模拟

对于上述中的数组越界错误,我们经过详细分析,模拟出一种对数组下标是否越界的检测方法。具体代码如下:

void text1(int x)
{
    int a;
    printf("请输入待测下标(a):");
    scanf("%d",&a);
    if(ax-1) printf("数组越界\n");
    else printf("使用正确\n");
}

void text2(int x, int y)
{
    int a,b;
    printf("请输入待测下标(a,b):");
    flushall();//清空缓冲区
    scanf("%d%d",&a,&b);
    if(ax-1 || by-1) printf("数组越界\n");
    else printf("使用正确\n");
}
void text3(int x, int y, int z)
{
    int a,b,c;
    printf("请输入待测下标(a,b,c):");
    flushall();
    scanf("%d%d%d",&a,&b,&c);
    if(ax-1 || by-1 || cz-1) printf("数组越界\n");
    else printf("使用正确\n");
}
void main()
{
    int n,i,j,k;
    char m;
L:
    printf("请输入所要检测数组的维度");
    scanf("%d",&n);

    switch(n)
{
    case 1:printf("请输入数组下标(i): ");

    scanf("%d",&i);

    text1(i);break;

    case 2:printf("请输入数组下标(i,j):");

    scanf("%d%d",&i,&j);

    text2(i,j);break;

    case 3:printf("请输入数组下标(i,j,k):");

    scanf("%d%d%d",&i,&j,&k);

    text3(i,j,k);break;
}
printf("是否继续检测(Y/N):");
flushall();
scanf("%c",&m);
switch(m)
{
    case 'Y':goto L; break;
    case 'y':goto L; break;
    default: exit;
}

}

检测结果如图2所示。

4 结束语

指针时C语言中广泛使用的一种类型,灵活正确的掌握指针在C语言中的运用,是学好C语言的关键的一步,能否正确理解和使用指针是掌握C语言的一个重要标志。本文总结了C语言中指针使用的常见错误,并分析提出了改进方法。不过这些还远远不够,对于指针在结构类型、函数等方面的运用还有待研究。