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

C语言间接访问操作符详解

2024-02-041.9k 阅读

一、C语言中的指针与间接访问操作符概述

在C语言里,指针是一种强大且独特的数据类型,它存储的是变量在内存中的地址。而间接访问操作符(*)则是与指针紧密相连的一个关键元素,用于访问指针所指向的内存位置中的值。

从本质上讲,计算机内存就像一个巨大的数组,每个内存单元都有一个唯一的地址。变量在内存中占据一定数量的连续内存单元,指针变量就是专门用来存储其他变量地址的变量。间接访问操作符允许我们通过指针来获取或修改该指针所指向的变量的值。

例如,考虑以下代码:

#include <stdio.h>

int main() {
    int num = 10;
    int *ptr;
    ptr = &num;
    printf("The value of num is: %d\n", num);
    printf("The address of num is: %p\n", &num);
    printf("The value stored in ptr (which is the address of num) is: %p\n", ptr);
    printf("The value of num accessed indirectly through ptr is: %d\n", *ptr);
    return 0;
}

在这段代码中,我们首先声明了一个整型变量num并赋值为10。接着声明了一个指向整型的指针变量ptr。通过ptr = &num;语句,我们将num的地址赋给了ptr。这里的&是取地址操作符,用于获取变量的内存地址。然后,我们使用间接访问操作符*ptr来获取ptr所指向的内存位置的值,也就是num的值。

(一)间接访问操作符的语法

间接访问操作符*紧跟在指针变量名之前。其语法形式如下:

data_type *pointer_variable;
// 使用间接访问操作符访问指针指向的值
data_type value = *pointer_variable;

这里data_type是指针所指向的数据类型,比如intfloatchar等。

二、间接访问操作符在不同场景下的使用

(一)基本变量的间接访问

  1. 整型变量 如前面的例子,对于整型变量,通过指针和间接访问操作符可以方便地获取和修改其值。假设我们想通过指针来修改num的值:
#include <stdio.h>

int main() {
    int num = 10;
    int *ptr;
    ptr = &num;
    *ptr = 20;
    printf("The new value of num is: %d\n", num);
    return 0;
}

在这段代码中,*ptr = 20;语句通过指针ptr间接修改了num的值。因为ptr指向num的内存地址,*ptr就代表了num这个变量本身,所以对*ptr的赋值操作实际上就是对num的赋值操作。

  1. 浮点型变量 同样的原理适用于浮点型变量。
#include <stdio.h>

int main() {
    float f = 3.14;
    float *fptr;
    fptr = &f;
    printf("The value of f is: %f\n", f);
    printf("The value of f accessed indirectly through fptr is: %f\n", *fptr);
    *fptr = 2.71;
    printf("The new value of f is: %f\n", f);
    return 0;
}

这里我们声明了一个浮点型变量f和一个指向浮点型的指针fptr。通过*fptr我们可以获取和修改f的值。

(二)数组与间接访问操作符

  1. 一维数组 在C语言中,数组名在大多数情况下会被自动转换为指向数组首元素的指针。这使得我们可以使用间接访问操作符来访问数组元素。
#include <stdio.h>

int main() {
    int arr[5] = {1, 2, 3, 4, 5};
    int *ptr;
    ptr = arr;
    printf("The first element of the array is: %d\n", *ptr);
    // 访问数组的第二个元素
    printf("The second element of the array is: %d\n", *(ptr + 1));
    return 0;
}

在这段代码中,ptr = arr;ptr指向了数组arr的首元素。*ptr就等同于arr[0],而*(ptr + 1)等同于arr[1]。这是因为在C语言中,数组元素在内存中是连续存储的,指针的算术运算(如ptr + 1)会根据指针所指向的数据类型的大小来移动相应的字节数。对于整型数组,ptr + 1会使指针移动4个字节(假设int类型占4个字节),从而指向数组的下一个元素。

  1. 二维数组 二维数组可以看作是数组的数组。当我们使用指针和间接访问操作符来访问二维数组时,情况会稍微复杂一些。
#include <stdio.h>

int main() {
    int arr[2][3] = {{1, 2, 3}, {4, 5, 6}};
    int (*ptr)[3];
    ptr = arr;
    // 访问第一行第一列的元素
    printf("The element at (0, 0) is: %d\n", *(*ptr));
    // 访问第二行第二列的元素
    printf("The element at (1, 1) is: %d\n", *(*(ptr + 1) + 1));
    return 0;
}

这里int (*ptr)[3];声明了一个指向包含3个int类型元素的数组的指针。ptr指向二维数组arr的第一行。*ptr则是指向第一行首元素的指针,所以*(*ptr)等同于arr[0][0]。而*(ptr + 1)指向第二行,*(*(ptr + 1) + 1)等同于arr[1][1]

(三)结构体与间接访问操作符

  1. 结构体变量的间接访问 结构体是一种自定义的数据类型,它可以包含不同类型的成员。当我们使用指针来操作结构体变量时,间接访问操作符也起着重要作用。
#include <stdio.h>

struct student {
    char name[20];
    int age;
    float grade;
};

int main() {
    struct student stu = {"Alice", 20, 3.5};
    struct student *stu_ptr;
    stu_ptr = &stu;
    printf("Name: %s\n", (*stu_ptr).name);
    printf("Age: %d\n", (*stu_ptr).age);
    printf("Grade: %f\n", (*stu_ptr).grade);
    // 使用 -> 操作符等效访问
    printf("Name (using ->): %s\n", stu_ptr->name);
    printf("Age (using ->): %d\n", stu_ptr->age);
    printf("Grade (using ->): %f\n", stu_ptr->grade);
    return 0;
}

在这段代码中,我们首先定义了一个student结构体。然后声明了一个student类型的变量stu和一个指向student类型的指针stu_ptr。通过(*stu_ptr).name(*stu_ptr).age(*stu_ptr).grade我们可以访问结构体stu的成员。同时,C语言还提供了->操作符,stu_ptr->name等效于(*stu_ptr).name,它使得代码更加简洁易读。

  1. 结构体数组的间接访问 当我们有一个结构体数组,并使用指针来访问其元素时,间接访问操作符同样适用。
#include <stdio.h>

struct student {
    char name[20];
    int age;
    float grade;
};

int main() {
    struct student students[2] = {{"Alice", 20, 3.5}, {"Bob", 21, 3.2}};
    struct student *stu_ptr;
    stu_ptr = students;
    printf("Name of first student: %s\n", (*stu_ptr).name);
    printf("Age of second student: %d\n", (*(stu_ptr + 1)).age);
    return 0;
}

这里stu_ptr指向结构体数组students的首元素。*stu_ptr代表数组的第一个元素,*(stu_ptr + 1)代表数组的第二个元素。通过间接访问操作符,我们可以方便地访问结构体数组中每个元素的成员。

三、间接访问操作符与函数

(一)函数参数中的指针与间接访问

  1. 传递基本类型指针 当我们将指针作为函数参数传递时,函数可以通过间接访问操作符修改调用函数中变量的值。
#include <stdio.h>

void increment(int *num) {
    (*num)++;
}

int main() {
    int num = 5;
    printf("Before increment, num = %d\n", num);
    increment(&num);
    printf("After increment, num = %d\n", num);
    return 0;
}

在这段代码中,increment函数接受一个指向int类型的指针num。在函数内部,通过(*num)++语句,我们对调用函数中num变量的值进行了递增操作。注意,这里的(*num)++*num++是不同的。(*num)++是先对*num的值进行递增,而*num++是先返回*num的值,然后再对num本身(即指针)进行递增。

  1. 传递数组指针 我们可以将数组名(本质是指向数组首元素的指针)传递给函数,然后在函数中使用间接访问操作符来访问数组元素。
#include <stdio.h>

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

int main() {
    int arr[5] = {1, 2, 3, 4, 5};
    print_array(arr, 5);
    return 0;
}

print_array函数中,arr是指向数组首元素的指针。通过*(arr + i)我们可以访问数组的第i个元素,并将其打印出来。

(二)函数返回指针与间接访问

  1. 返回基本类型指针 函数可以返回一个指向基本类型的指针。但是需要注意,返回的指针所指向的内存空间必须在函数调用结束后仍然有效。
#include <stdio.h>

int *create_number() {
    static int num = 10;
    return &num;
}

int main() {
    int *ptr = create_number();
    printf("The value of the number is: %d\n", *ptr);
    return 0;
}

在这段代码中,create_number函数返回一个指向静态局部变量num的指针。因为静态局部变量在函数调用结束后仍然存在,所以*ptr可以正确获取到num的值。如果num不是静态变量,而是普通的局部变量,那么在函数返回后,num所占据的内存空间可能会被释放,*ptr就会指向一个无效的内存位置,导致未定义行为。

  1. 返回结构体指针 函数也可以返回一个指向结构体的指针。
#include <stdio.h>

struct student {
    char name[20];
    int age;
    float grade;
};

struct student *create_student() {
    static struct student stu = {"Alice", 20, 3.5};
    return &stu;
}

int main() {
    struct student *stu_ptr = create_student();
    printf("Name: %s\n", stu_ptr->name);
    printf("Age: %d\n", stu_ptr->age);
    printf("Grade: %f\n", stu_ptr->grade);
    return 0;
}

这里create_student函数返回一个指向静态结构体变量stu的指针。通过stu_ptr->name等操作,我们可以访问结构体的成员。同样,为了保证指针所指向的内存有效,这里使用了静态变量。

四、间接访问操作符的注意事项

(一)指针未初始化

使用未初始化的指针是一种常见的错误。当指针未初始化时,它的值是不确定的,可能指向任何内存位置。使用间接访问操作符访问未初始化指针所指向的值会导致未定义行为。

#include <stdio.h>

int main() {
    int *ptr;
    // 未初始化指针,下面这行代码会导致未定义行为
    printf("The value is: %d\n", *ptr);
    return 0;
}

在实际编程中,一定要在使用指针之前对其进行初始化,使其指向一个有效的内存位置。

(二)空指针

空指针是一个特殊的指针值,它不指向任何有效的内存位置。在C语言中,我们可以通过将指针赋值为NULL(在<stdio.h>等头文件中定义)来创建一个空指针。使用间接访问操作符访问空指针同样会导致未定义行为。

#include <stdio.h>

int main() {
    int *ptr = NULL;
    // 下面这行代码会导致未定义行为
    printf("The value is: %d\n", *ptr);
    return 0;
}

在使用指针之前,应该先检查指针是否为NULL,以避免这种错误。

#include <stdio.h>

int main() {
    int *ptr = NULL;
    if (ptr!= NULL) {
        printf("The value is: %d\n", *ptr);
    } else {
        printf("The pointer is NULL.\n");
    }
    return 0;
}

(三)指针越界

指针越界是指指针访问了不属于其有效范围的内存位置。在数组中,当我们使用指针算术运算超出了数组的边界时,就会发生指针越界。

#include <stdio.h>

int main() {
    int arr[5] = {1, 2, 3, 4, 5};
    int *ptr = arr;
    // 这里ptr + 5会导致指针越界,下面这行代码会导致未定义行为
    printf("The value is: %d\n", *(ptr + 5));
    return 0;
}

为了避免指针越界,在进行指针算术运算时,一定要确保不会超出有效内存范围。

(四)内存释放后使用指针

当我们使用free函数(在<stdlib.h>中定义)释放了动态分配的内存后,指向该内存的指针就变成了悬空指针。如果继续使用间接访问操作符通过这个悬空指针来访问内存,会导致未定义行为。

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

int main() {
    int *ptr = (int *)malloc(sizeof(int));
    if (ptr!= NULL) {
        *ptr = 10;
        printf("The value is: %d\n", *ptr);
        free(ptr);
        // 下面这行代码会导致未定义行为,因为ptr已经是悬空指针
        printf("The value is: %d\n", *ptr);
    }
    return 0;
}

为了避免这种情况,在释放内存后,可以将指针赋值为NULL

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

int main() {
    int *ptr = (int *)malloc(sizeof(int));
    if (ptr!= NULL) {
        *ptr = 10;
        printf("The value is: %d\n", *ptr);
        free(ptr);
        ptr = NULL;
    }
    return 0;
}

五、间接访问操作符与内存管理

(一)动态内存分配与间接访问

  1. malloc函数与间接访问 malloc函数(在<stdlib.h>中定义)用于在堆上动态分配内存。它返回一个指向分配内存起始地址的指针。我们可以使用间接访问操作符来操作这块内存。
#include <stdio.h>
#include <stdlib.h>

int main() {
    int *ptr = (int *)malloc(sizeof(int));
    if (ptr!= NULL) {
        *ptr = 20;
        printf("The value is: %d\n", *ptr);
        free(ptr);
    } else {
        printf("Memory allocation failed.\n");
    }
    return 0;
}

在这段代码中,malloc(sizeof(int))分配了足够存储一个int类型数据的内存空间,并返回一个指向该空间的指针ptr。通过*ptr我们可以对这块内存进行赋值和取值操作。最后,使用free(ptr)释放了分配的内存。

  1. calloc函数与间接访问 calloc函数(也在<stdlib.h>中定义)用于分配内存并将其初始化为0。它的使用方式与malloc类似,但在初始化方面有所不同。
#include <stdio.h>
#include <stdlib.h>

int main() {
    int *ptr = (int *)calloc(5, sizeof(int));
    if (ptr!= NULL) {
        for (int i = 0; i < 5; i++) {
            printf("Element %d: %d\n", i, *(ptr + i));
        }
        free(ptr);
    } else {
        printf("Memory allocation failed.\n");
    }
    return 0;
}

这里calloc(5, sizeof(int))分配了5个连续的int类型大小的内存空间,并将它们初始化为0。通过*(ptr + i)我们可以访问每个元素。

(二)内存释放与间接访问的关系

如前面提到的,在使用完动态分配的内存后,必须使用free函数进行释放。如果不释放,会导致内存泄漏。同时,在释放内存后,一定要注意不要再通过原来的指针进行间接访问,因为此时指针已经成为悬空指针。

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

int main() {
    int *ptr = (int *)malloc(sizeof(int));
    if (ptr!= NULL) {
        *ptr = 30;
        printf("The value is: %d\n", *ptr);
        free(ptr);
        // 错误,ptr已成为悬空指针,下面这行代码会导致未定义行为
        printf("The value is: %d\n", *ptr);
    } else {
        printf("Memory allocation failed.\n");
    }
    return 0;
}

为了防止这种错误,在释放内存后,可以将指针赋值为NULL,这样如果不小心再次使用该指针进行间接访问,程序会因为访问NULL指针而报错,从而更容易发现问题。

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

int main() {
    int *ptr = (int *)malloc(sizeof(int));
    if (ptr!= NULL) {
        *ptr = 30;
        printf("The value is: %d\n", *ptr);
        free(ptr);
        ptr = NULL;
    } else {
        printf("Memory allocation failed.\n");
    }
    return 0;
}

通过对C语言中间接访问操作符的详细了解,我们可以更好地掌握指针的使用,编写出更高效、更健壮的C语言程序。无论是在基本变量操作、数组处理、结构体应用,还是函数调用和内存管理等方面,间接访问操作符都扮演着至关重要的角色。同时,我们也要时刻注意避免指针相关的错误,确保程序的正确性和稳定性。