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

C语言指针的指针原理

2022-04-135.8k 阅读

指针基础回顾

在深入探讨指针的指针之前,先来简单回顾一下指针的基本概念。在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 through ptr is %d\n", *ptr);

    return 0;
}

在上述代码中,int *ptr声明了一个指向int类型的指针变量ptr。通过ptr = &num,将num变量的地址赋值给了ptr*ptr则用于访问ptr所指向的内存地址中的值,也就是num的值。

指针的指针定义

指针的指针,也称为二级指针,是一个指针变量,它存储的是另一个指针变量的内存地址。也就是说,一级指针指向普通变量,而二级指针指向一级指针。其声明方式如下:

data_type **pointer_variable;

其中,data_type是最终指向的变量的数据类型,**表示这是一个二级指针,pointer_variable是指针变量名。

例如,声明一个指向int类型指针的指针:

int **ptr_to_ptr;

指针的指针使用场景

  1. 动态二维数组 在C语言中,动态分配二维数组时,指针的指针非常有用。假设我们要创建一个mn列的二维数组。
#include <stdio.h>
#include <stdlib.h>

int main() {
    int m = 3;
    int n = 4;
    int **matrix;

    // 分配行指针
    matrix = (int **)malloc(m * sizeof(int *));

    // 为每一行分配内存
    for (int i = 0; i < m; i++) {
        matrix[i] = (int *)malloc(n * sizeof(int));
    }

    // 初始化矩阵
    for (int i = 0; i < m; i++) {
        for (int j = 0; j < n; j++) {
            matrix[i][j] = i * n + j;
        }
    }

    // 打印矩阵
    for (int i = 0; i < m; i++) {
        for (int j = 0; j < n; j++) {
            printf("%d ", matrix[i][j]);
        }
        printf("\n");
    }

    // 释放内存
    for (int i = 0; i < m; i++) {
        free(matrix[i]);
    }
    free(matrix);

    return 0;
}

在这段代码中,int **matrix声明了一个二级指针matrix。首先为m个行指针分配内存,然后为每一行分配nint类型的内存空间。这样就创建了一个动态的二维数组。

  1. 传递指针参数 当需要在函数中修改指针本身,而不仅仅是修改指针所指向的值时,就需要使用指针的指针。例如,实现一个函数来动态分配内存并将指针返回:
#include <stdio.h>
#include <stdlib.h>

void allocate_memory(int **ptr) {
    *ptr = (int *)malloc(sizeof(int));
    if (*ptr == NULL) {
        printf("Memory allocation failed\n");
        return;
    }
    **ptr = 42;
}

int main() {
    int *ptr;
    allocate_memory(&ptr);

    if (ptr != NULL) {
        printf("The value is %d\n", *ptr);
        free(ptr);
    }

    return 0;
}

allocate_memory函数中,int **ptr接收一个指向指针的指针。通过*ptr = (int *)malloc(sizeof(int))为指针ptr分配内存,然后通过**ptr = 42为分配的内存空间赋值。在main函数中,通过allocate_memory(&ptr)传递ptr的地址,使得函数可以修改ptr本身,为其分配内存。

指针的指针原理剖析

  1. 内存布局 以如下代码为例:
#include <stdio.h>

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

    printf("The value of num is %d\n", num);
    printf("The address of num is %p\n", &num);
    printf("The value of ptr (address of num) is %p\n", ptr);
    printf("The address of ptr is %p\n", &ptr);
    printf("The value of ptr_to_ptr (address of ptr) is %p\n", ptr_to_ptr);
    printf("The value of num accessed through ptr is %d\n", *ptr);
    printf("The value of num accessed through ptr_to_ptr is %d\n", **ptr_to_ptr);

    return 0;
}

在内存中,num变量占据一块内存空间存储值10ptr指针变量存储的是num的内存地址。而ptr_to_ptr存储的是ptr的内存地址。通过*ptr可以访问num的值,通过**ptr_to_ptr同样可以访问num的值。这是因为ptr_to_ptr指向ptrptr又指向num,所以**ptr_to_ptr先通过ptr_to_ptr找到ptr,再通过ptr找到num

  1. 间接访问 指针的指针实现了多层间接访问。每一层*操作符都表示一次间接访问。例如,*ptr_to_ptr得到的是ptr,因为ptr_to_ptr指向ptr。而**ptr_to_ptr则得到num的值,因为*ptr_to_ptr得到ptr,再对ptr应用*操作符就得到num的值。

指针的指针与数组

  1. 指针数组与数组指针 指针数组是一个数组,数组中的每个元素都是指针。例如:
int *array_of_pointers[5];

这是一个包含5个int类型指针的数组。

数组指针是一个指针,它指向一个数组。例如:

int (*pointer_to_array)[5];

这是一个指向包含5个int类型元素数组的指针。

  1. 指针的指针与二维数组 在C语言中,二维数组在内存中是按行存储的连续内存块。可以将二维数组名看作是一个指向数组的指针,也就是一个一级指针。而指针的指针可以用来模拟二维数组的动态分配。例如前面提到的动态二维数组的例子,int **matrix通过先分配行指针,再为每一行分配内存,实现了类似二维数组的结构。

当使用指针的指针来操作类似二维数组的结构时,matrix[i][j]实际上等价于*(*(matrix + i) + j)。这里matrix + i得到第i行的指针,*(matrix + i)得到第i行首元素的指针,*(matrix + i) + j得到第i行第j列元素的指针,最后*(*(matrix + i) + j)得到该元素的值。

指针的指针注意事项

  1. 内存管理 在使用指针的指针时,尤其是在动态分配内存的情况下,内存管理至关重要。例如在动态二维数组的例子中,不仅要释放行指针所指向的内存,还要释放最外层的指针数组。如果忘记释放内层的行指针,就会导致内存泄漏。

  2. 指针类型匹配 在使用指针的指针时,指针类型必须严格匹配。例如,不能将一个指向char类型指针的指针赋值给一个指向int类型指针的指针,否则会导致未定义行为。

  3. NULL指针检查 在使用指针的指针进行间接访问之前,一定要检查指针是否为NULL。例如,在前面动态分配内存的例子中,在调用**ptr之前,先检查了*ptr是否为NULL,以避免空指针引用错误。

综合示例

下面来看一个综合示例,展示指针的指针在链表操作中的应用。链表是一种常见的数据结构,每个节点包含数据和指向下一个节点的指针。在某些情况下,需要通过指针的指针来操作链表,以便在函数中修改链表的头指针。

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

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

// 创建新节点
Node* create_node(int value) {
    Node *new_node = (Node *)malloc(sizeof(Node));
    new_node->data = value;
    new_node->next = NULL;
    return new_node;
}

// 在链表头部插入节点
void insert_at_head(Node **head, int value) {
    Node *new_node = create_node(value);
    new_node->next = *head;
    *head = new_node;
}

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

// 释放链表内存
void free_list(Node *head) {
    Node *current = head;
    Node *next_node;
    while (current != NULL) {
        next_node = current->next;
        free(current);
        current = next_node;
    }
}

int main() {
    Node *head = NULL;

    insert_at_head(&head, 3);
    insert_at_head(&head, 2);
    insert_at_head(&head, 1);

    print_list(head);

    free_list(head);

    return 0;
}

在这个示例中,insert_at_head函数接收一个指向链表头指针的指针Node **head。通过*head = new_node,可以在函数中修改链表的头指针,实现向链表头部插入新节点的功能。

通过以上内容,详细介绍了C语言指针的指针的原理、使用场景、与数组的关系以及注意事项,并通过丰富的代码示例进行了说明。希望读者对指针的指针有更深入的理解和掌握。在实际编程中,合理运用指针的指针可以解决很多复杂的数据结构和内存管理问题。