MK
摩柯社区 - 一个极简的技术知识社区
AI 面试

C语言数组与指针常见错误及避免方法

2024-08-113.4k 阅读

C语言数组与指针常见错误及避免方法

数组越界错误

  1. 错误本质
    • 在C语言中,数组的下标是从0开始的。当访问数组元素时,如果使用的下标超出了数组定义的有效范围,就会发生数组越界错误。这种错误在运行时可能不会立即导致程序崩溃,但会引发未定义行为(Undefined Behavior),即程序的行为是不可预测的。它可能看似正常运行,也可能产生奇怪的结果,甚至导致程序崩溃。
    • 例如,定义一个数组int arr[5];,其有效的下标范围是0到4。如果访问arr[5],就超出了数组的边界。
  2. 代码示例
#include <stdio.h>

int main() {
    int arr[5] = {1, 2, 3, 4, 5};
    // 尝试访问越界元素
    printf("%d\n", arr[5]);
    return 0;
}
  • 在上述代码中,尝试访问arr[5],这是越界访问。不同的编译器和运行环境对这种未定义行为的处理可能不同。有些可能会给出运行时错误,有些可能会输出一个看似随机的值。
  1. 避免方法
    • 边界检查:在访问数组元素之前,确保下标在有效范围内。例如,如果要遍历数组,可以使用如下代码:
#include <stdio.h>

int main() {
    int arr[5] = {1, 2, 3, 4, 5};
    int i;
    for (i = 0; i < 5; i++) {
        printf("%d ", arr[i]);
    }
    return 0;
}
  • 使用宏定义数组大小:如果数组大小在程序中可能会改变,使用宏定义数组大小,这样在需要修改数组大小时,只需要修改宏定义的值,而不需要在多个地方修改数组的大小和相关的访问边界判断。
#include <stdio.h>

#define ARRAY_SIZE 5

int main() {
    int arr[ARRAY_SIZE] = {1, 2, 3, 4, 5};
    int i;
    for (i = 0; i < ARRAY_SIZE; i++) {
        printf("%d ", arr[i]);
    }
    return 0;
}

指针未初始化错误

  1. 错误本质
    • 指针是一种特殊的变量,它存储的是内存地址。如果一个指针变量没有被初始化就使用,它会指向一个不确定的内存位置。访问这个不确定位置的数据会导致未定义行为,可能会破坏其他数据,或者导致程序崩溃。
    • 例如,定义一个指针int *ptr;,此时ptr的值是未定义的,直接使用*ptr(解引用指针)就是错误的。
  2. 代码示例
#include <stdio.h>

int main() {
    int *ptr;
    // 未初始化就解引用指针
    *ptr = 10;
    printf("%d\n", *ptr);
    return 0;
}
  • 在上述代码中,ptr没有初始化就进行解引用并赋值操作,这是非常危险的。运行这段代码很可能会导致程序崩溃,因为ptr指向的是未知的内存地址,对该地址进行写入操作可能会违反内存访问规则。
  1. 避免方法
    • 初始化指针:在定义指针时,就给它一个合法的初始值。可以让指针指向一个已定义的变量,或者使用malloc等函数分配内存后让指针指向分配的内存。
    • 指向已定义变量
#include <stdio.h>

int main() {
    int num = 10;
    int *ptr = &num;
    printf("%d\n", *ptr);
    return 0;
}
  • 使用malloc分配内存
#include <stdio.h>
#include <stdlib.h>

int main() {
    int *ptr = (int *)malloc(sizeof(int));
    if (ptr!= NULL) {
        *ptr = 20;
        printf("%d\n", *ptr);
        free(ptr);
    }
    return 0;
}
  • 在使用malloc分配内存后,要检查返回值是否为NULL,以确保内存分配成功。并且在使用完动态分配的内存后,要使用free函数释放内存,防止内存泄漏。

指针运算错误

  1. 错误本质
    • 指针运算在C语言中有特定的规则。指针的加减法运算通常是基于其所指向的数据类型的大小。错误的指针运算可能会导致访问到不应该访问的内存位置,从而引发未定义行为。
    • 例如,对指针进行不恰当的算术运算,如指针加上一个与数组元素大小不匹配的值,或者在指针算术运算中使用了错误的逻辑。
  2. 代码示例
#include <stdio.h>

int main() {
    int arr[5] = {1, 2, 3, 4, 5};
    int *ptr = arr;
    // 错误的指针运算,加2跳过了两个整数的位置
    ptr = ptr + 2;
    // 这里假设只想访问下一个元素,应该加1
    printf("%d\n", *ptr);
    return 0;
}
  • 在上述代码中,原本可能希望ptr指向数组的下一个元素,但错误地加了2,导致ptr跳过了两个整数的位置,访问到了不期望的内存位置。
  1. 避免方法
    • 理解指针运算规则:指针加1,实际上是指针移动到下一个同类型数据的位置,其移动的字节数等于所指向数据类型的大小。例如,int类型指针加1,移动sizeof(int)个字节。
    • 仔细检查指针运算逻辑:在进行指针运算时,要确保运算的结果是指向期望的内存位置。如果是在数组环境中,通常指针的加减法运算应该基于数组元素的索引。
#include <stdio.h>

int main() {
    int arr[5] = {1, 2, 3, 4, 5};
    int *ptr = arr;
    // 正确的指针运算,指向数组的下一个元素
    ptr = ptr + 1;
    printf("%d\n", *ptr);
    return 0;
}

数组名与指针的混淆

  1. 错误本质
    • 在C语言中,数组名在很多情况下会被隐式转换为指向数组首元素的指针。然而,数组名和指针并不是完全等同的概念。数组名有其自身的特性,如它有固定的内存地址和大小,而指针是一个变量,可以指向不同的位置。混淆两者的特性会导致错误。
    • 例如,试图对数组名进行赋值操作,就像对指针一样,这是错误的,因为数组名是一个常量指针,其指向的地址不能被改变。
  2. 代码示例
#include <stdio.h>

int main() {
    int arr[5] = {1, 2, 3, 4, 5};
    // 错误:不能对数组名赋值
    arr = arr + 1;
    return 0;
}
  • 在上述代码中,尝试对数组名arr进行赋值操作,这是不允许的。数组名在表达式中会被转换为指针常量,不能被修改。
  1. 避免方法
    • 明确区分概念:记住数组名是一个常量指针,它指向数组的首元素,并且其地址是固定的。而普通指针是变量,可以重新赋值指向不同的内存位置。
    • 使用指针变量进行灵活操作:如果需要对数组进行灵活的指针操作,定义一个指针变量并让它指向数组,而不是直接对数组名进行不恰当的操作。
#include <stdio.h>

int main() {
    int arr[5] = {1, 2, 3, 4, 5};
    int *ptr = arr;
    // 对指针变量进行操作
    ptr = ptr + 1;
    printf("%d\n", *ptr);
    return 0;
}

动态内存分配与指针相关错误

  1. 内存泄漏
    • 错误本质:当使用malloccalloc等函数动态分配内存后,如果没有使用free函数释放这些内存,就会发生内存泄漏。随着程序的运行,未释放的内存会逐渐积累,最终耗尽系统内存,导致程序性能下降甚至崩溃。
    • 代码示例
#include <stdio.h>
#include <stdlib.h>

int main() {
    int *ptr;
    int i;
    for (i = 0; i < 10; i++) {
        ptr = (int *)malloc(sizeof(int));
        if (ptr!= NULL) {
            *ptr = i;
        }
    }
    // 没有释放分配的内存
    return 0;
}
  • 在上述代码中,每次循环都分配了内存,但没有在使用完后释放,导致内存泄漏。
  • 避免方法:在使用完动态分配的内存后,一定要调用free函数释放内存。并且要确保free函数只被调用一次,避免重复释放内存导致错误。
#include <stdio.h>
#include <stdlib.h>

int main() {
    int *ptr;
    int i;
    for (i = 0; i < 10; i++) {
        ptr = (int *)malloc(sizeof(int));
        if (ptr!= NULL) {
            *ptr = i;
            free(ptr);
        }
    }
    return 0;
}
  1. 悬空指针
    • 错误本质:当一个指针指向的内存被释放后,该指针仍然保留着原来的内存地址,此时它就成为了悬空指针。如果继续使用这个悬空指针,就会导致未定义行为,因为该指针指向的内存已经不再是有效的数据区域。
    • 代码示例
#include <stdio.h>
#include <stdlib.h>

int main() {
    int *ptr = (int *)malloc(sizeof(int));
    if (ptr!= NULL) {
        *ptr = 10;
        free(ptr);
        // ptr现在是悬空指针
        printf("%d\n", *ptr);
    }
    return 0;
}
  • 在上述代码中,free(ptr)释放了ptr指向的内存,之后再访问*ptr就是使用悬空指针,会导致未定义行为。
  • 避免方法:在释放内存后,将指针赋值为NULL。这样,当再次试图使用该指针时,程序会因为访问NULL指针而崩溃,从而更容易发现错误。
#include <stdio.h>
#include <stdlib.h>

int main() {
    int *ptr = (int *)malloc(sizeof(int));
    if (ptr!= NULL) {
        *ptr = 10;
        free(ptr);
        ptr = NULL;
        // 这里访问ptr不会导致未定义行为,因为ptr是NULL
        if (ptr!= NULL) {
            printf("%d\n", *ptr);
        }
    }
    return 0;
}

函数参数中数组与指针的错误理解

  1. 错误本质
    • 在C语言中,当数组作为函数参数传递时,实际上传递的是指向数组首元素的指针。这意味着在函数内部,对数组参数的修改会影响到原数组。同时,在函数内部无法通过sizeof运算符获取原数组的真实大小,因为此时数组已经退化为指针。如果不理解这些特性,可能会导致错误的数组操作。
  2. 代码示例
#include <stdio.h>

void printArray(int arr[]) {
    // 这里的sizeof(arr)得到的是指针的大小,而不是数组的大小
    printf("Size of arr in function: %zu\n", sizeof(arr));
}

int main() {
    int arr[5] = {1, 2, 3, 4, 5};
    printArray(arr);
    return 0;
}
  • 在上述代码中,在printArray函数中使用sizeof(arr)得到的是指针的大小,而不是原数组的大小。如果在函数中依赖sizeof(arr)来进行数组相关操作,可能会导致错误。
  1. 避免方法
    • 传递数组大小参数:为了在函数内部正确处理数组,除了传递数组指针外,还应该传递数组的大小作为参数。
#include <stdio.h>

void printArray(int arr[], int size) {
    int i;
    for (i = 0; i < size; i++) {
        printf("%d ", arr[i]);
    }
    printf("\n");
}

int main() {
    int arr[5] = {1, 2, 3, 4, 5};
    printArray(arr, 5);
    return 0;
}
  • 使用结构体封装数组和大小:可以定义一个结构体,将数组和其大小封装在一起,然后传递结构体对象,这样在函数内部可以方便地获取数组大小并进行操作。
#include <stdio.h>

typedef struct {
    int data[100];
    int size;
} ArrayStruct;

void printArray(ArrayStruct arr) {
    int i;
    for (i = 0; i < arr.size; i++) {
        printf("%d ", arr.data[i]);
    }
    printf("\n");
}

int main() {
    ArrayStruct arr = {{1, 2, 3, 4, 5}, 5};
    printArray(arr);
    return 0;
}

多级指针错误

  1. 错误本质
    • 多级指针是指针的指针,例如int **ptr;。使用多级指针时,需要正确地初始化和操作。常见的错误包括没有正确分配内存给多级指针所指向的指针,或者在解引用多级指针时出现层次错误。
    • 例如,没有为二级指针指向的一级指针分配内存就进行解引用操作,会导致未定义行为。
  2. 代码示例
#include <stdio.h>

int main() {
    int **ptr;
    int num = 10;
    // 错误:没有为*ptr分配内存
    *ptr = &num;
    printf("%d\n", **ptr);
    return 0;
}
  • 在上述代码中,ptr是二级指针,没有为*ptr(即一级指针)分配内存就直接赋值,这是错误的。
  1. 避免方法
    • 正确初始化多级指针:在使用多级指针之前,要确保每一级指针都有合法的内存地址。对于二级指针,首先要为其分配内存指向一个一级指针,然后再为一级指针分配内存指向实际的数据。
#include <stdio.h>
#include <stdlib.h>

int main() {
    int **ptr = (int **)malloc(sizeof(int *));
    if (ptr!= NULL) {
        int num = 10;
        *ptr = &num;
        printf("%d\n", **ptr);
        free(ptr);
    }
    return 0;
}
  • 仔细处理解引用层次:在解引用多级指针时,要清楚每一级解引用的作用和顺序,确保按照正确的层次进行解引用操作。

指针与数组在字符串处理中的错误

  1. 字符串数组与字符指针的混淆
    • 错误本质:在C语言中,字符串可以用字符数组或字符指针表示。字符数组在内存中是一块连续的存储空间,用于存储字符串的各个字符以及字符串结束符'\0'。而字符指针可以指向一个字符串常量或动态分配的字符串内存。混淆两者的特性可能导致错误,例如试图修改字符串常量。
    • 代码示例
#include <stdio.h>

int main() {
    char *str = "Hello";
    // 错误:试图修改字符串常量
    str[0] = 'h';
    return 0;
}
  • 在上述代码中,str是一个字符指针指向字符串常量"Hello"。字符串常量存储在只读内存区域,试图修改它会导致未定义行为。
  • 避免方法:如果需要修改字符串,使用字符数组。如果只是需要指向一个字符串常量进行读取操作,可以使用字符指针。
#include <stdio.h>

int main() {
    char str[] = "Hello";
    str[0] = 'h';
    printf("%s\n", str);
    return 0;
}
  1. 字符串处理函数中的指针与数组参数错误
    • 错误本质:C语言提供了许多字符串处理函数,如strcpystrcat等。这些函数的参数通常是字符指针或字符数组。如果传递的参数不符合函数要求,或者没有正确处理字符串的结束符,会导致错误。例如,目标数组空间不足时使用strcpy函数会导致缓冲区溢出。
    • 代码示例
#include <stdio.h>
#include <string.h>

int main() {
    char dest[5];
    char src[] = "Hello";
    // 错误:dest数组空间不足,会导致缓冲区溢出
    strcpy(dest, src);
    printf("%s\n", dest);
    return 0;
}
  • 在上述代码中,dest数组的大小为5,不足以容纳src字符串(包括结束符'\0'),使用strcpy会导致缓冲区溢出,这是非常危险的,可能会导致程序崩溃或安全漏洞。
  • 避免方法:确保目标数组有足够的空间来存储源字符串。可以使用snprintf函数代替strcpysnprintf会防止缓冲区溢出。
#include <stdio.h>
#include <string.h>

int main() {
    char dest[6];
    char src[] = "Hello";
    snprintf(dest, sizeof(dest), "%s", src);
    printf("%s\n", dest);
    return 0;
}
  • 在使用snprintf时,第二个参数指定了目标数组的大小,它会确保不会写入超过这个大小的数据,从而避免缓冲区溢出。

通过对以上C语言数组与指针常见错误的深入理解和掌握避免方法,可以编写出更健壮、可靠的C语言程序,减少因这些错误导致的程序漏洞和崩溃。在实际编程中,要养成良好的编程习惯,仔细检查数组和指针的操作,确保程序的正确性和稳定性。