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

C语言间接访问操作符的使用与技巧

2023-08-037.2k 阅读

C语言间接访问操作符基础概念

在C语言中,间接访问操作符,也就是星号 *,在指针的使用中扮演着关键角色。指针是一种特殊的变量类型,它存储的是内存地址。而间接访问操作符 * 允许我们通过指针来访问该指针所指向的内存位置中的数据。

例如,假设有一个整型变量 num

#include <stdio.h>

int main() {
    int num = 10;
    int *ptr; // 声明一个整型指针
    ptr = &num; // 将指针ptr指向num的地址
    printf("通过指针间接访问的值: %d\n", *ptr);
    return 0;
}

在上述代码中,int *ptr 声明了一个名为 ptr 的指针,它指向 int 类型的数据。ptr = &num 这行代码将 ptr 指向了变量 num 的内存地址。然后,通过 *ptr 我们就可以间接访问到 num 的值,即 10。这里的 * 就是间接访问操作符,它解引用了指针 ptr,获取到了指针所指向的实际数据。

间接访问操作符在函数参数传递中的应用

  1. 传递指针以修改实参值 在C语言中,函数参数传递默认是值传递。这意味着当我们将一个变量传递给函数时,函数内部得到的是该变量的一个副本,对副本的修改不会影响到原始变量。然而,通过传递指针,我们可以利用间接访问操作符来修改原始变量的值。

例如,下面的代码实现了一个交换两个整数的函数:

#include <stdio.h>

void swap(int *a, int *b) {
    int temp;
    temp = *a;
    *a = *b;
    *b = temp;
}

int main() {
    int num1 = 5;
    int num2 = 10;
    printf("交换前: num1 = %d, num2 = %d\n", num1, num2);
    swap(&num1, &num2);
    printf("交换后: num1 = %d, num2 = %d\n", num1, num2);
    return 0;
}

swap 函数中,参数 ab 是指向 int 类型的指针。通过 *a*b,我们间接访问到了 num1num2 的值,并进行了交换。在 main 函数中,我们将 num1num2 的地址传递给 swap 函数,这样函数内部对指针解引用后所做的修改,就会反映到原始变量上。

  1. 通过指针传递数组 在C语言中,数组名本质上是一个指向数组首元素的指针。当我们将数组作为函数参数传递时,实际上传递的是数组首元素的地址。在函数内部,我们可以使用间接访问操作符来访问数组中的元素。

例如,下面的函数计算数组元素的总和:

#include <stdio.h>

int sum(int *arr, int size) {
    int total = 0;
    for (int i = 0; i < size; i++) {
        total += *(arr + i);
    }
    return total;
}

int main() {
    int arr[] = {1, 2, 3, 4, 5};
    int size = sizeof(arr) / sizeof(arr[0]);
    int result = sum(arr, size);
    printf("数组元素总和: %d\n", result);
    return 0;
}

sum 函数中,arr 是一个指向 int 类型的指针,它指向数组的首元素。通过 *(arr + i),我们间接访问到了数组的第 i 个元素,并累加到 total 中。这种方式等同于 arr[i],因为在C语言中,arr[i] 被编译器解释为 *(arr + i)

多级指针与间接访问

  1. 二级指针(指向指针的指针) 除了普通指针,C语言还支持多级指针。二级指针是指向指针的指针。也就是说,二级指针存储的是一个指针的地址,而这个指针又指向实际的数据。

声明一个二级指针的语法如下:

int **pptr;

这里的 pptr 是一个二级指针,它指向一个 int * 类型的指针。

下面是一个使用二级指针的示例:

#include <stdio.h>

int main() {
    int num = 10;
    int *ptr = &num;
    int **pptr = &ptr;

    printf("通过二级指针间接访问的值: %d\n", **pptr);
    return 0;
}

在上述代码中,num 是一个整型变量,ptr 是指向 num 的指针,而 pptr 是指向 ptr 的二级指针。通过 **pptr,我们进行了两次间接访问,最终获取到了 num 的值。

  1. 多级指针的应用场景 多级指针在一些复杂的数据结构中有着重要的应用,比如链表的链表。假设有一个链表,每个节点存储的是另一个链表的头指针。在这种情况下,我们可以使用二级指针来操作外部链表的节点,因为外部链表节点中的指针(也就是内部链表的头指针)需要被修改时,就需要通过二级指针来实现。

下面是一个简单的示例,展示如何使用二级指针来管理链表:

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

// 定义链表节点结构体
typedef struct Node {
    int data;
    struct Node *next;
} Node;

// 向链表头部插入节点的函数,使用二级指针
void insertAtHead(Node **head, int value) {
    Node *newNode = (Node *)malloc(sizeof(Node));
    newNode->data = value;
    newNode->next = *head;
    *head = newNode;
}

// 打印链表的函数
void printList(Node *head) {
    Node *current = head;
    while (current != NULL) {
        printf("%d -> ", current->data);
        current = current->next;
    }
    printf("NULL\n");
}

int main() {
    Node *head = NULL;
    insertAtHead(&head, 3);
    insertAtHead(&head, 2);
    insertAtHead(&head, 1);
    printList(head);
    return 0;
}

insertAtHead 函数中,参数 head 是一个二级指针。因为我们需要修改 main 函数中 head 指针的值(也就是链表的头指针),如果只使用一级指针,对指针的修改不会影响到 main 函数中的 head。通过二级指针 **head,我们可以间接访问并修改 head 指针本身,使其指向新插入的节点。

间接访问操作符与内存动态分配

  1. 使用 malloc 分配内存并通过指针访问 在C语言中,malloc 函数用于在堆上动态分配内存。malloc 函数返回一个指向分配内存起始地址的指针。我们可以使用间接访问操作符来访问这块内存。

例如,下面的代码动态分配了一个整数大小的内存,并存储一个值:

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

int main() {
    int *ptr;
    ptr = (int *)malloc(sizeof(int));
    if (ptr == NULL) {
        printf("内存分配失败\n");
        return 1;
    }
    *ptr = 20;
    printf("通过指针访问动态分配内存的值: %d\n", *ptr);
    free(ptr);
    return 0;
}

在上述代码中,malloc(sizeof(int)) 分配了一个 int 类型大小的内存块,并返回指向该内存块的指针 ptr。通过 *ptr,我们将值 20 存储到这块内存中。最后,使用 free(ptr) 释放了动态分配的内存,以避免内存泄漏。

  1. 动态分配数组并访问元素 我们也可以动态分配一个数组大小的内存,并通过指针和间接访问操作符来访问数组元素。

例如:

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

int main() {
    int n = 5;
    int *arr;
    arr = (int *)malloc(n * sizeof(int));
    if (arr == NULL) {
        printf("内存分配失败\n");
        return 1;
    }
    for (int i = 0; i < n; i++) {
        *(arr + i) = i * 2;
    }
    for (int i = 0; i < n; i++) {
        printf("arr[%d] = %d\n", i, *(arr + i));
    }
    free(arr);
    return 0;
}

在这个例子中,malloc(n * sizeof(int)) 分配了一个能容纳 nint 类型元素的内存块。通过 *(arr + i),我们可以像访问普通数组一样访问动态分配数组的元素,并进行赋值和读取操作。

间接访问操作符与结构体

  1. 结构体指针与间接访问 当我们有一个结构体变量时,我们可以声明一个指向该结构体的指针。通过结构体指针,我们可以使用间接访问操作符来访问结构体的成员。

例如,定义一个简单的结构体 Point

#include <stdio.h>

typedef struct Point {
    int x;
    int y;
} Point;

int main() {
    Point p = {10, 20};
    Point *ptr = &p;
    printf("通过结构体指针间接访问: x = %d, y = %d\n", (*ptr).x, (*ptr).y);
    return 0;
}

在上述代码中,ptr 是指向结构体 p 的指针。通过 (*ptr).x(*ptr).y,我们间接访问到了结构体 p 的成员 xy

为了方便,C语言提供了 -> 操作符,它等价于 (*ptr).。所以上述代码可以改写为:

#include <stdio.h>

typedef struct Point {
    int x;
    int y;
} Point;

int main() {
    Point p = {10, 20};
    Point *ptr = &p;
    printf("通过结构体指针间接访问: x = %d, y = %d\n", ptr->x, ptr->y);
    return 0;
}
  1. 动态分配结构体内存并通过指针访问 和普通数据类型一样,我们也可以动态分配结构体内存,并通过指针来访问其成员。

例如:

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

typedef struct Book {
    char title[50];
    char author[50];
    int year;
} Book;

int main() {
    Book *bookPtr;
    bookPtr = (Book *)malloc(sizeof(Book));
    if (bookPtr == NULL) {
        printf("内存分配失败\n");
        return 1;
    }
    sprintf(bookPtr->title, "C Programming");
    sprintf(bookPtr->author, "Brian W. Kernighan");
    bookPtr->year = 1988;
    printf("书名: %s\n作者: %s\n年份: %d\n", bookPtr->title, bookPtr->author, bookPtr->year);
    free(bookPtr);
    return 0;
}

在这个例子中,malloc(sizeof(Book)) 动态分配了一个 Book 结构体大小的内存块,并通过 bookPtr 指针来访问和设置结构体的成员。

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

  1. 空指针解引用 在使用间接访问操作符时,最常见的错误之一是空指针解引用。当一个指针的值为 NULL 时,对其进行解引用操作会导致未定义行为,通常会引发程序崩溃。

例如:

#include <stdio.h>

int main() {
    int *ptr = NULL;
    printf("%d\n", *ptr); // 空指针解引用,未定义行为
    return 0;
}

为了避免这种情况,在解引用指针之前,一定要确保指针不是 NULL

  1. 野指针 野指针是指向一块已经释放或者未初始化内存的指针。使用野指针同样会导致未定义行为。

例如:

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

int main() {
    int *ptr = (int *)malloc(sizeof(int));
    *ptr = 10;
    free(ptr);
    printf("%d\n", *ptr); // 野指针解引用,未定义行为
    return 0;
}

在上述代码中,ptrfree(ptr) 之后成为了野指针,因为内存已经被释放。再次解引用 ptr 是危险的。为了避免野指针问题,在释放内存后,可以将指针赋值为 NULL

  1. 指针类型不匹配 当使用间接访问操作符时,指针的类型必须与它所指向的数据类型匹配。如果类型不匹配,会导致未定义行为。

例如:

#include <stdio.h>

int main() {
    int num = 10;
    char *ptr = (char *)&num; // 指针类型不匹配
    printf("%d\n", *ptr); // 未定义行为
    return 0;
}

在上述代码中,ptr 是一个 char * 类型的指针,却指向了一个 int 类型的变量 num,这种类型不匹配会导致未定义行为。

间接访问操作符与函数指针

  1. 函数指针的定义与间接访问 在C语言中,函数指针是一种指向函数的指针类型。函数在内存中也有一个起始地址,函数指针可以存储这个地址。我们可以使用间接访问操作符来通过函数指针调用函数。

声明一个函数指针的语法如下:

return_type (*function_pointer_name)(parameter_list);

例如,假设有一个简单的加法函数:

#include <stdio.h>

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

int main() {
    int (*ptr)(int, int);
    ptr = add;
    int result = (*ptr)(3, 5);
    printf("通过函数指针调用函数的结果: %d\n", result);
    return 0;
}

在上述代码中,int (*ptr)(int, int) 声明了一个函数指针 ptr,它指向一个返回 int 类型,接受两个 int 类型参数的函数。ptr = addptr 指向了 add 函数。通过 (*ptr)(3, 5),我们间接调用了 add 函数,并得到结果。

  1. 函数指针作为函数参数 函数指针可以作为函数的参数,这在实现回调函数时非常有用。回调函数是一种通过函数指针调用的函数,它允许我们将函数作为参数传递给另一个函数,使得另一个函数可以在适当的时候调用这个传递进来的函数。

例如,下面的代码展示了一个使用函数指针作为参数的示例:

#include <stdio.h>

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

int subtract(int a, int b) {
    return a - b;
}

void calculate(int a, int b, int (*operation)(int, int)) {
    int result = (*operation)(a, b);
    printf("计算结果: %d\n", result);
}

int main() {
    calculate(5, 3, add);
    calculate(5, 3, subtract);
    return 0;
}

calculate 函数中,int (*operation)(int, int) 是一个函数指针参数。通过传递不同的函数(如 addsubtract),calculate 函数可以执行不同的操作。在函数内部,通过 (*operation)(a, b) 间接调用了传递进来的函数。

间接访问操作符在复杂数据结构中的应用

  1. 二叉树中的指针与间接访问 二叉树是一种常见的树状数据结构,每个节点最多有两个子节点。在实现二叉树时,指针和间接访问操作符起着关键作用。

例如,定义一个简单的二叉树节点结构体:

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

typedef struct TreeNode {
    int data;
    struct TreeNode *left;
    struct TreeNode *right;
} TreeNode;

// 创建新节点的函数
TreeNode *createNode(int value) {
    TreeNode *newNode = (TreeNode *)malloc(sizeof(TreeNode));
    newNode->data = value;
    newNode->left = NULL;
    newNode->right = NULL;
    return newNode;
}

// 插入节点到二叉树的函数
TreeNode *insertNode(TreeNode *root, int value) {
    if (root == NULL) {
        return createNode(value);
    }
    if (value < root->data) {
        root->left = insertNode(root->left, value);
    } else if (value > root->data) {
        root->right = insertNode(root->right, value);
    }
    return root;
}

// 中序遍历二叉树的函数
void inorderTraversal(TreeNode *root) {
    if (root != NULL) {
        inorderTraversal(root->left);
        printf("%d ", root->data);
        inorderTraversal(root->right);
    }
}

int main() {
    TreeNode *root = NULL;
    root = insertNode(root, 50);
    insertNode(root, 30);
    insertNode(root, 20);
    insertNode(root, 40);
    insertNode(root, 70);
    insertNode(root, 60);
    insertNode(root, 80);
    printf("中序遍历结果: ");
    inorderTraversal(root);
    return 0;
}

在上述代码中,TreeNode 结构体包含一个 int 类型的数据成员 data,以及两个指向 TreeNode 类型的指针 leftright,分别指向左子节点和右子节点。通过这些指针和间接访问操作符,我们可以构建、插入节点以及遍历二叉树。例如,在 insertNode 函数中,root->leftroot->right 通过间接访问操作符来访问和修改节点的子节点指针。

  1. 哈希表中的指针与间接访问 哈希表是一种根据关键码值(Key value)而直接进行访问的数据结构。在实现哈希表时,指针和间接访问操作符用于管理哈希桶中的链表(如果使用链地址法解决哈希冲突)。

例如,下面是一个简单的哈希表实现示例:

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

#define TABLE_SIZE 10

typedef struct HashNode {
    char key[50];
    int value;
    struct HashNode *next;
} HashNode;

typedef struct HashTable {
    HashNode *table[TABLE_SIZE];
} HashTable;

// 哈希函数
unsigned long hashFunction(const char *key) {
    unsigned long hash = 5381;
    int c;
    while ((c = *key++)) {
        hash = ((hash << 5) + hash) + c; // hash * 33 + c
    }
    return hash % TABLE_SIZE;
}

// 插入键值对到哈希表
void insert(HashTable *hashTable, const char *key, int value) {
    unsigned long index = hashFunction(key);
    HashNode *node = hashTable->table[index];
    if (node == NULL) {
        hashTable->table[index] = (HashNode *)malloc(sizeof(HashNode));
        strcpy(hashTable->table[index]->key, key);
        hashTable->table[index]->value = value;
        hashTable->table[index]->next = NULL;
    } else {
        while (node->next != NULL) {
            node = node->next;
        }
        node->next = (HashNode *)malloc(sizeof(HashNode));
        strcpy(node->next->key, key);
        node->next->value = value;
        node->next->next = NULL;
    }
}

// 根据键获取值
int get(HashTable *hashTable, const char *key) {
    unsigned long index = hashFunction(key);
    HashNode *node = hashTable->table[index];
    while (node != NULL) {
        if (strcmp(node->key, key) == 0) {
            return node->value;
        }
        node = node->next;
    }
    return -1; // 未找到
}

int main() {
    HashTable hashTable;
    memset(&hashTable, 0, sizeof(HashTable));
    insert(&hashTable, "apple", 10);
    insert(&hashTable, "banana", 20);
    printf("apple的值: %d\n", get(&hashTable, "apple"));
    printf("cherry的值: %d\n", get(&hashTable, "cherry"));
    return 0;
}

在这个哈希表实现中,HashNode 结构体通过 next 指针形成链表来解决哈希冲突。在 insertget 函数中,通过间接访问操作符(如 node->nextnode->value)来操作链表节点,实现键值对的插入和获取。

总结间接访问操作符的技巧

  1. 利用指针别名优化代码 指针别名是指多个指针指向同一块内存区域。在一些情况下,合理利用指针别名可以优化代码性能。例如,在处理大型数组时,我们可以通过多个指针指向数组的不同部分,以并行的方式对数组进行操作。
#include <stdio.h>

void processArray(int *arr, int size) {
    int *ptr1 = arr;
    int *ptr2 = arr + size / 2;
    for (int i = 0; i < size / 2; i++) {
        *ptr1 += 1;
        *ptr2 += 2;
        ptr1++;
        ptr2++;
    }
}

int main() {
    int arr[10] = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
    processArray(arr, 10);
    for (int i = 0; i < 10; i++) {
        printf("%d ", arr[i]);
    }
    return 0;
}

在上述代码中,ptr1ptr2 指向数组 arr 的不同部分,通过并行操作提高了处理效率。

  1. 使用指针数组简化代码逻辑 指针数组是一个数组,其元素都是指针。在处理多个同类型数据结构时,指针数组可以简化代码逻辑。

例如,假设有多个结构体变量,我们可以使用指针数组来管理它们:

#include <stdio.h>

typedef struct Student {
    char name[50];
    int age;
} Student;

int main() {
    Student s1 = {"Alice", 20};
    Student s2 = {"Bob", 21};
    Student s3 = {"Charlie", 22};
    Student *students[3] = {&s1, &s2, &s3};
    for (int i = 0; i < 3; i++) {
        printf("姓名: %s, 年龄: %d\n", students[i]->name, students[i]->age);
    }
    return 0;
}

通过指针数组 students,我们可以方便地遍历和操作多个 Student 结构体变量。

  1. 注意指针的生命周期与作用域 在使用指针和间接访问操作符时,要特别注意指针的生命周期和作用域。局部指针在其所在函数结束时会被销毁,如果不小心返回局部指针的地址,会导致悬空指针问题。

例如:

#include <stdio.h>

int *badFunction() {
    int num = 10;
    return &num; // 返回局部变量的地址,悬空指针
}

int main() {
    int *ptr = badFunction();
    printf("%d\n", *ptr); // 未定义行为
    return 0;
}

为了避免这种情况,应该确保返回的指针指向的内存不会在函数结束时被销毁,例如通过动态分配内存并返回指针。

  1. 利用常量指针和指针常量 常量指针是指向常量的指针,它保证不能通过该指针修改所指向的数据。指针常量是指针本身是常量,不能改变指针的指向。合理使用常量指针和指针常量可以增强代码的健壮性。

例如:

#include <stdio.h>

int main() {
    const int num = 10;
    const int *ptr1 = &num; // 常量指针
    int value = 20;
    int *const ptr2 = &value; // 指针常量
    // *ptr1 = 20; // 错误,不能通过常量指针修改数据
    // ptr2 = &num; // 错误,不能修改指针常量的指向
    return 0;
}

通过使用常量指针和指针常量,可以在编译时捕获一些潜在的错误,提高代码的安全性。

  1. 调试时关注指针状态 在调试包含指针和间接访问操作符的代码时,要密切关注指针的状态。可以使用调试工具(如GDB)来查看指针的值、指针所指向的内存内容等。例如,在GDB中,可以使用 p 命令来打印指针的值,使用 x 命令来查看指针所指向的内存。
#include <stdio.h>

int main() {
    int num = 10;
    int *ptr = &num;
    // 假设在这里设置断点
    *ptr = 20;
    return 0;
}

在GDB中,可以在设置断点后,使用 p ptr 查看指针 ptr 的值,使用 x/xw ptr 查看 ptr 所指向的内存内容(xw 表示以十六进制、字(4字节)的格式显示)。

通过掌握这些关于间接访问操作符的使用技巧,可以更高效、安全地编写C语言代码,尤其是在处理复杂数据结构和内存管理时。同时,深入理解间接访问操作符的本质和应用场景,有助于我们优化代码性能,减少错误发生的概率。在实际编程中,不断实践和总结经验,将有助于我们更好地运用指针和间接访问操作符来实现各种功能。