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

C 语言指针的运算与注意事项

2023-11-153.9k 阅读

C 语言指针的运算

在 C 语言中,指针是一个强大的特性,它允许程序员直接操作内存地址。指针的运算主要包括算术运算、关系运算和赋值运算。

指针的算术运算

  1. 指针与整数的加法和减法 指针与整数的加法和减法是指针算术运算中最常见的操作。当一个指针加上或减去一个整数时,实际上是在内存地址上进行相应的偏移。偏移量的大小取决于指针所指向的数据类型的大小。

例如,假设有一个指向 int 类型的指针 pint 类型在某系统中占用 4 个字节。如果 p 的值为 0x1000,那么 p + 1 的值将是 0x1004,因为 int 类型每个元素占用 4 个字节。

以下是代码示例:

#include <stdio.h>

int main() {
    int arr[] = {10, 20, 30, 40, 50};
    int *p = arr;

    printf("指针 p 的初始地址: %p\n", (void *)p);
    p = p + 2; // 指针 p 向后移动 2 个 int 元素的位置
    printf("指针 p 移动后的地址: %p\n", (void *)p);
    printf("指针 p 移动后指向的值: %d\n", *p);

    return 0;
}

在上述代码中,p + 2 使指针 p 向后移动了两个 int 类型元素的位置。注意,在使用 printf 输出指针值时,需要将指针强制转换为 void * 类型,以避免编译器警告。

  1. 指针之间的减法 只有当两个指针指向同一数组中的元素时,它们之间的减法才有意义。指针相减的结果是两个指针之间的元素个数,而不是它们地址之间的差值。

例如:

#include <stdio.h>

int main() {
    int arr[] = {10, 20, 30, 40, 50};
    int *p1 = &arr[0];
    int *p2 = &arr[3];

    int diff = p2 - p1;
    printf("p2 和 p1 之间的元素个数: %d\n", diff);

    return 0;
}

在这个例子中,p2 - p1 的结果为 3,因为 p2 指向的元素与 p1 指向的元素之间相隔 3 个 int 类型的元素。

指针的关系运算

指针可以进行关系运算,如 ==!=<><=>=。这些运算用于比较两个指针的值(即内存地址)。

通常,关系运算在指向同一数组元素的指针之间进行才有实际意义。例如,判断一个指针是否指向数组的末尾元素等。

代码示例:

#include <stdio.h>

int main() {
    int arr[] = {10, 20, 30, 40, 50};
    int *p1 = &arr[0];
    int *p2 = &arr[2];

    if (p1 < p2) {
        printf("p1 的地址小于 p2 的地址\n");
    }

    if (p1!= p2) {
        printf("p1 和 p2 的地址不相等\n");
    }

    return 0;
}

在上述代码中,通过关系运算比较了两个指针的地址。

指针的赋值运算

指针的赋值运算用于将一个指针的值赋给另一个指针。需要注意的是,赋值的两个指针类型必须兼容,否则可能会导致未定义行为。

例如:

#include <stdio.h>

int main() {
    int num = 10;
    int *p1 = &num;
    int *p2;

    p2 = p1; // 将 p1 的值赋给 p2
    printf("p1 指向的值: %d\n", *p1);
    printf("p2 指向的值: %d\n", *p2);

    return 0;
}

在这个例子中,p2 = p1 使得 p2 指向了与 p1 相同的内存地址,因此 *p1*p2 的值相同。

C 语言指针运算的注意事项

指针运算的边界问题

  1. 数组越界访问 在进行指针的算术运算时,很容易发生数组越界访问的问题。例如,当指针超出数组的有效范围时,可能会访问到未分配的内存或者其他程序的数据,导致程序崩溃或产生未定义行为。

以下是一个可能导致数组越界的示例:

#include <stdio.h>

int main() {
    int arr[] = {10, 20, 30, 40, 50};
    int *p = arr;

    // 这里指针 p 超出了数组 arr 的范围
    p = p + 10;
    printf("指针 p 指向的值: %d\n", *p);

    return 0;
}

在上述代码中,p = p + 10 使指针 p 超出了数组 arr 的范围,访问 *p 会导致未定义行为。为了避免这种情况,在进行指针运算时,必须确保指针始终在数组的有效范围内。

  1. 内存释放后的指针使用 当使用 free 函数释放了一块动态分配的内存后,指向该内存的指针变成了悬空指针(dangling pointer)。如果继续使用这个悬空指针进行运算或访问内存,同样会导致未定义行为。

例如:

#include <stdio.h>
#include <stdlib.h>

int main() {
    int *p = (int *)malloc(sizeof(int));
    *p = 10;

    free(p);
    // 这里 p 成为悬空指针,继续使用会导致未定义行为
    printf("指针 p 指向的值: %d\n", *p);

    return 0;
}

为了避免悬空指针问题,在释放内存后,应立即将指针赋值为 NULL,这样可以防止意外地使用悬空指针。修改后的代码如下:

#include <stdio.h>
#include <stdlib.h>

int main() {
    int *p = (int *)malloc(sizeof(int));
    *p = 10;

    free(p);
    p = NULL;

    // 这里 p 为 NULL,避免了悬空指针问题
    if (p!= NULL) {
        printf("指针 p 指向的值: %d\n", *p);
    }

    return 0;
}

指针类型兼容性

  1. 不同类型指针的赋值 在进行指针赋值运算时,必须保证两个指针的类型兼容。例如,不能将一个指向 int 类型的指针直接赋给一个指向 char 类型的指针,除非进行适当的类型转换。

以下是一个不兼容指针赋值的示例:

#include <stdio.h>

int main() {
    int num = 10;
    int *p1 = &num;
    char *p2;

    // 这行代码会导致编译错误,因为类型不兼容
    p2 = p1;

    return 0;
}

如果确实需要进行类型转换,可以使用强制类型转换,但要谨慎操作,因为不同数据类型在内存中的表示和对齐方式可能不同,不当的转换可能会导致数据错误。

例如:

#include <stdio.h>

int main() {
    int num = 10;
    int *p1 = &num;
    char *p2;

    p2 = (char *)p1;
    // 这里虽然可以进行强制类型转换,但可能会导致数据访问错误
    printf("通过 p2 访问的值: %c\n", *p2);

    return 0;
}

在这个例子中,将 int * 类型的指针 p1 强制转换为 char * 类型的指针 p2,但由于 intchar 类型的大小和表示方式不同,访问 *p2 可能会得到不正确的结果。

  1. 函数指针类型兼容性 函数指针也有类型兼容性的问题。当将一个函数指针赋值给另一个函数指针时,两个函数指针的参数列表和返回类型必须完全匹配。

例如:

#include <stdio.h>

int add(int a, int b) {
    return a + b;
}

// 函数指针类型声明
typedef int (*FuncPtr)(int, int);

int main() {
    FuncPtr p1 = add;
    // 定义一个不同参数列表的函数
    int sub(int a, int b, int c) {
        return a - b - c;
    }
    FuncPtr p2;

    // 这行代码会导致编译错误,因为函数参数列表不匹配
    p2 = sub;

    return 0;
}

在上述代码中,由于 sub 函数的参数列表与 FuncPtr 类型要求的参数列表不匹配,将 sub 函数的地址赋给 p2 会导致编译错误。

指针与 const 关键字

  1. 指向常量的指针 当一个指针被声明为指向常量时,不能通过该指针修改所指向的数据,但指针本身的值可以改变。

例如:

#include <stdio.h>

int main() {
    const int num = 10;
    const int *p = &num;

    // 这行代码会导致编译错误,不能通过 p 修改 num 的值
    // *p = 20;

    int anotherNum = 20;
    p = &anotherNum; // 指针 p 可以指向其他常量

    return 0;
}

在这个例子中,const int *p 声明了 p 是一个指向常量 int 类型的指针,不能通过 p 修改其所指向的数据,但 p 可以指向其他常量。

  1. 常量指针 常量指针是指指针本身的值不能改变,即它始终指向同一个内存地址,但可以通过该指针修改所指向的数据(如果数据本身不是常量)。

例如:

#include <stdio.h>

int main() {
    int num1 = 10;
    int num2 = 20;
    int *const p = &num1;

    // 这行代码会导致编译错误,p 是常量指针,不能改变其值
    // p = &num2;

    *p = 30; // 可以通过 p 修改 num1 的值

    return 0;
}

在上述代码中,int *const p 声明了 p 是一个常量指针,它只能指向 num1,不能再指向其他地址,但可以通过 p 修改 num1 的值。

  1. 指向常量的常量指针 结合上述两种情况,可以声明指向常量的常量指针,即指针本身的值不能改变,且不能通过该指针修改所指向的数据。

例如:

#include <stdio.h>

int main() {
    const int num = 10;
    const int *const p = &num;

    // 这行代码会导致编译错误,不能通过 p 修改 num 的值
    // *p = 20;

    // 这行代码也会导致编译错误,p 是常量指针,不能改变其值
    // p = &anotherNum;

    return 0;
}

在这个例子中,const int *const p 声明了 p 是一个指向常量 int 类型的常量指针,既不能改变 p 所指向的地址,也不能通过 p 修改其所指向的数据。

多级指针的运算与注意事项

  1. 多级指针的概念 多级指针是指指针指向的是另一个指针,例如二级指针是指向指针的指针。声明二级指针的方式如下:
int num = 10;
int *p1 = &num;
int **p2 = &p1;

在上述代码中,p2 是一个二级指针,它指向 p1,而 p1 指向 num

  1. 多级指针的运算 多级指针的运算与一级指针类似,但需要注意指针的层次。例如,当对二级指针进行算术运算时,同样是在内存地址上进行偏移,不过偏移量的计算要考虑到所指向的指针类型的大小。

假设 int * 类型在某系统中占用 4 个字节,以下是一个简单的二级指针运算示例:

#include <stdio.h>

int main() {
    int num1 = 10;
    int num2 = 20;
    int *p1 = &num1;
    int *p2 = &num2;
    int **pp = &p1;

    printf("pp 初始指向的地址: %p\n", (void *)pp);
    pp = pp + 1; // pp 向后移动一个 int * 类型的位置
    printf("pp 移动后指向的地址: %p\n", (void *)pp);
    pp = &p2;
    printf("通过 pp 间接访问的值: %d\n", **pp);

    return 0;
}

在上述代码中,pp = pp + 1 使二级指针 pp 向后移动了一个 int * 类型元素的位置。

  1. 多级指针的注意事项 使用多级指针时,容易出现指针层次混淆的问题。例如,错误地解引用指针层次。同时,在释放内存时,如果涉及多级指针指向的动态分配内存,需要按照正确的顺序释放,否则可能会导致内存泄漏。

例如,假设通过二级指针动态分配了一个二维数组:

#include <stdio.h>
#include <stdlib.h>

int main() {
    int **arr = (int **)malloc(3 * sizeof(int *));
    for (int i = 0; i < 3; i++) {
        arr[i] = (int *)malloc(4 * sizeof(int));
    }

    // 使用完后释放内存
    for (int i = 0; i < 3; i++) {
        free(arr[i]);
    }
    free(arr);

    return 0;
}

在这个例子中,先释放了每一行的内存,然后释放了指向行指针的数组内存。如果顺序错误,例如先释放 arr,再释放 arr[i],就会导致内存泄漏。

指针与数组的关系及注意事项

  1. 指针与数组的紧密联系 在 C 语言中,数组名在大多数情况下可以被看作是一个指向数组首元素的指针常量。例如:
#include <stdio.h>

int main() {
    int arr[] = {10, 20, 30, 40, 50};
    int *p = arr;

    printf("通过数组名访问第一个元素: %d\n", arr[0]);
    printf("通过指针访问第一个元素: %d\n", *p);

    return 0;
}

在上述代码中,arr 可以当作指向 arr[0] 的指针,p 指向 arr[0],因此通过 arr[0]*p 都能访问到数组的第一个元素。

  1. 指针与数组的区别 虽然数组名和指针有紧密联系,但它们也有区别。数组名是一个常量指针,其值不能改变,而普通指针的值是可以改变的。

例如:

#include <stdio.h>

int main() {
    int arr[] = {10, 20, 30, 40, 50};
    int *p = arr;

    // 这行代码会导致编译错误,数组名 arr 是常量,不能改变其值
    // arr = arr + 1;

    p = p + 1; // 指针 p 的值可以改变

    return 0;
}

此外,使用 sizeof 运算符时,对数组名和指针的结果不同。sizeof(arr) 返回整个数组的大小,而 sizeof(p) 返回指针本身的大小(通常为 4 或 8 字节,取决于系统的指针大小)。

例如:

#include <stdio.h>

int main() {
    int arr[] = {10, 20, 30, 40, 50};
    int *p = arr;

    printf("数组 arr 的大小: %zu\n", sizeof(arr));
    printf("指针 p 的大小: %zu\n", sizeof(p));

    return 0;
}

在这个例子中,sizeof(arr) 返回数组 arr 占用的总字节数,而 sizeof(p) 返回指针 p 本身的大小。

  1. 传递数组给函数时的指针行为 当将数组作为参数传递给函数时,实际上传递的是数组首元素的地址,即一个指针。在函数内部,对数组参数的操作就像对指针的操作一样。

例如:

#include <stdio.h>

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

int main() {
    int arr[] = {10, 20, 30, 40, 50};
    printArray(arr, 5);

    return 0;
}

在上述代码中,printArray 函数的参数 arr 实际上是一个指针,在函数内部可以像操作指针一样访问数组元素。但需要注意的是,在函数内部无法通过 sizeof(arr) 获取数组的实际大小,因为此时 arr 只是一个指针,需要额外传递数组的大小参数。

综上所述,C 语言指针的运算功能强大,但在使用过程中需要特别注意各种边界问题、类型兼容性以及与其他概念(如数组、const 关键字等)的关系,以编写健壮、安全的程序。通过深入理解指针运算及其注意事项,程序员能够更好地利用 C 语言的指针特性,进行高效的内存管理和复杂的数据结构操作。