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

C语言指针实例分析

2021-03-166.3k 阅读

指针基础概念

在 C 语言中,指针是一种极为重要且强大的概念。简单来说,指针是一个变量,其值为另一个变量的内存地址。通过指针,我们可以直接访问和操作内存中的数据,这在很多复杂的编程场景中提供了极大的灵活性。

指针变量的声明与初始化

声明一个指针变量的语法如下:

数据类型 *指针变量名;

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

int *ptr;

这里的 * 用于表明 ptr 是一个指针变量,它指向 int 类型的数据。需要注意的是,指针变量在使用前必须初始化,即让它指向一个实际存在的变量。

初始化指针变量有两种常见方式。一种是通过已存在的变量地址来初始化:

#include <stdio.h>

int main() {
    int num = 10;
    int *ptr = &num;
    printf("num 的地址是: %p\n", (void*)&num);
    printf("ptr 存储的地址是: %p\n", (void*)ptr);
    return 0;
}

在上述代码中,& 是取地址运算符,&num 获取了变量 num 的内存地址,并将其赋值给指针 ptr

另一种方式是使用 malloc 函数动态分配内存并让指针指向这块内存:

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

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

这里使用 malloc 函数分配了一块能存储一个 int 类型数据的内存空间,并将返回的内存地址赋值给 ptr。同时,我们使用 free 函数在使用完动态分配的内存后释放它,以避免内存泄漏。

通过指针访问数据

一旦指针变量被正确初始化,我们就可以通过它来访问和修改其所指向的变量的值。在 C 语言中,使用 * 运算符来解引用指针,即获取指针所指向的变量的值。

#include <stdio.h>

int main() {
    int num = 30;
    int *ptr = &num;
    printf("通过变量直接访问: %d\n", num);
    printf("通过指针访问: %d\n", *ptr);
    *ptr = 40;
    printf("通过指针修改后,变量的值: %d\n", num);
    return 0;
}

在这段代码中,*ptr 表示访问 ptr 所指向的内存地址处存储的值。当我们修改 *ptr 时,实际上是修改了 num 的值,因为 ptr 指向 num 的内存地址。

指针与数组

指针和数组在 C 语言中有着紧密的联系。事实上,数组名在大多数情况下可以被看作是一个指向数组首元素的指针常量。

数组名作为指针

考虑以下代码:

#include <stdio.h>

int main() {
    int arr[5] = {1, 2, 3, 4, 5};
    int *ptr = arr;
    for (int i = 0; i < 5; i++) {
        printf("arr[%d] = %d, 通过指针访问: %d\n", i, arr[i], *(ptr + i));
    }
    return 0;
}

在上述代码中,arr 是数组名,它代表数组首元素的地址,因此可以直接将其赋值给指针 ptr。通过 *(ptr + i) 这种方式,我们可以像使用数组下标一样访问数组元素。ptr + i 表示从 ptr 所指向的地址开始,偏移 i 个元素的地址,然后通过 * 运算符解引用获取该地址处的值。

指针运算与数组遍历

指针可以进行一些运算,这在数组遍历中非常有用。除了上述的指针偏移运算 ptr + i,指针还可以进行减法运算。例如,两个指向同一数组的指针相减,其结果是它们之间的元素个数。

#include <stdio.h>

int main() {
    int arr[5] = {1, 2, 3, 4, 5};
    int *ptr1 = &arr[0];
    int *ptr2 = &arr[3];
    int diff = ptr2 - ptr1;
    printf("ptr2 和 ptr1 之间的元素个数: %d\n", diff);
    return 0;
}

在这段代码中,ptr2 - ptr1 的结果为 3,因为 ptr2 指向的元素比 ptr1 指向的元素在数组中往后 3 个位置。

另外,指针还可以进行自增(++)和自减(--)运算。当指针指向数组元素时,自增运算会使指针指向下一个元素,自减运算则相反。

#include <stdio.h>

int main() {
    int arr[5] = {1, 2, 3, 4, 5};
    int *ptr = arr;
    while (ptr < &arr[5]) {
        printf("%d ", *ptr);
        ptr++;
    }
    return 0;
}

在这个例子中,通过 ptr++ 不断移动指针,从而遍历整个数组。

指针与函数

指针在函数中有着广泛的应用,主要体现在函数参数传递和函数返回值方面。

指针作为函数参数

通过将指针作为函数参数,可以实现函数对实参的直接修改,而不仅仅是传递实参的副本。这在很多情况下是非常必要的,比如需要在函数内部改变调用函数中变量的值。

#include <stdio.h>

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

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

swap 函数中,int *aint *b 是指针参数。通过传递 num1num2 的地址,函数内部可以直接修改这两个变量的值。如果不使用指针参数,仅传递 num1num2 的副本,函数内部的交换操作不会影响到调用函数中的原始变量。

函数返回指针

函数也可以返回一个指针。不过需要注意,返回的指针所指向的内存空间必须在函数结束后仍然有效。常见的做法是返回动态分配的内存地址或者指向全局变量的指针。

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

int *createArray(int size) {
    int *arr = (int*)malloc(size * sizeof(int));
    if (arr == NULL) {
        printf("内存分配失败\n");
        return NULL;
    }
    for (int i = 0; i < size; i++) {
        arr[i] = i + 1;
    }
    return arr;
}

int main() {
    int *result = createArray(5);
    if (result != NULL) {
        for (int i = 0; i < 5; i++) {
            printf("%d ", result[i]);
        }
        free(result);
    }
    return 0;
}

createArray 函数中,通过 malloc 动态分配内存并返回指向这块内存的指针。在 main 函数中,接收返回的指针并使用它,最后记得使用 free 释放动态分配的内存,以防止内存泄漏。

多级指针

除了一级指针,C 语言还支持多级指针。多级指针是指针的指针,即一个指针变量的值是另一个指针变量的地址。

二级指针

最常见的多级指针是二级指针。声明一个二级指针的语法如下:

数据类型 **指针变量名;

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

int **ptr2;

下面通过一个例子来演示二级指针的使用:

#include <stdio.h>

int main() {
    int num = 10;
    int *ptr1 = &num;
    int **ptr2 = &ptr1;
    printf("num 的值: %d\n", num);
    printf("通过一级指针访问 num: %d\n", *ptr1);
    printf("通过二级指针访问 num: %d\n", **ptr2);
    return 0;
}

在这段代码中,ptr2 是一个二级指针,它指向 ptr1。通过 **ptr2 可以访问到 num 的值。首先,*ptr2 得到 ptr1 的值,即 num 的地址,然后再对这个地址进行解引用 *(*ptr2) 也就是 **ptr2,就可以得到 num 的值。

多级指针的应用场景

多级指针在一些复杂的数据结构中有着重要的应用,比如链表的链表或者树结构等。以链表的链表为例,假设我们有一个链表,每个节点又指向另一个链表。这时,就可以使用二级指针来操作外层链表的节点指针,因为在插入或删除外层链表节点时,可能需要修改指向节点的指针(即指针的指针)。

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

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

// 定义外层链表节点结构
typedef struct OuterNode {
    InnerNode *innerList;
    struct OuterNode *next;
} OuterNode;

// 在内层链表中插入节点
InnerNode* createInnerNode(int data) {
    InnerNode *newNode = (InnerNode*)malloc(sizeof(InnerNode));
    newNode->data = data;
    newNode->next = NULL;
    return newNode;
}

// 在外层链表中插入节点
void insertOuterNode(OuterNode **head, int innerData) {
    OuterNode *newOuterNode = (OuterNode*)malloc(sizeof(OuterNode));
    newOuterNode->innerList = createInnerNode(innerData);
    newOuterNode->next = *head;
    *head = newOuterNode;
}

// 打印链表
void printLists(OuterNode *head) {
    OuterNode *currentOuter = head;
    while (currentOuter != NULL) {
        InnerNode *currentInner = currentOuter->innerList;
        while (currentInner != NULL) {
            printf("%d ", currentInner->data);
            currentInner = currentInner->next;
        }
        printf("\n");
        currentOuter = currentOuter->next;
    }
}

int main() {
    OuterNode *head = NULL;
    insertOuterNode(&head, 1);
    insertOuterNode(&head, 2);
    insertOuterNode(&head, 3);
    printLists(head);
    return 0;
}

在这个例子中,insertOuterNode 函数使用二级指针 OuterNode **head 来操作外层链表的头指针。因为在插入新节点时,头指针可能会发生改变,通过二级指针可以直接修改调用函数中的头指针。

指针与字符串

在 C 语言中,字符串通常是以 \0 结尾的字符数组来表示,而指针在处理字符串时非常方便和高效。

字符指针与字符串常量

一个字符指针可以指向一个字符串常量。字符串常量在内存中存储为一个字符数组,并且编译器会自动在末尾添加 \0 作为结束标志。

#include <stdio.h>

int main() {
    char *str = "Hello, World!";
    printf("字符串: %s\n", str);
    return 0;
}

在上述代码中,str 是一个字符指针,它指向字符串常量 "Hello, World!" 的首字符地址。使用 printf 函数的 %s 格式化输出可以打印整个字符串,直到遇到 \0 为止。

需要注意的是,虽然可以通过字符指针访问字符串常量,但不能通过该指针修改字符串常量的内容,因为字符串常量存储在只读内存区域。如果尝试修改,会导致程序运行时错误。

动态分配内存的字符串

有时候我们需要动态分配内存来存储字符串,以便根据实际需求调整字符串的长度。这可以通过 malloccalloc 函数来实现。

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

int main() {
    char *str = (char*)malloc(20 * sizeof(char));
    if (str == NULL) {
        printf("内存分配失败\n");
        return 1;
    }
    strcpy(str, "Dynamic String");
    printf("字符串: %s\n", str);
    free(str);
    return 0;
}

在这段代码中,首先使用 malloc 分配了一块能存储 20 个字符的内存空间,然后使用 strcpy 函数将字符串 "Dynamic String" 复制到这块内存中。最后,使用 free 释放动态分配的内存。

指针相关的常见错误与陷阱

在使用指针时,很容易出现一些错误和陷入一些陷阱,以下是一些常见的情况。

未初始化指针

使用未初始化的指针是非常危险的,因为它可能指向任何内存地址,导致程序崩溃或出现未定义行为。

#include <stdio.h>

int main() {
    int *ptr;
    printf("%d\n", *ptr); // 未初始化指针,行为未定义
    return 0;
}

在这个例子中,ptr 没有被初始化就尝试解引用它,这会导致程序出现未定义行为。在使用指针前,一定要确保它指向一个有效的内存地址。

野指针

野指针是指向已释放内存或从未分配内存的指针。当动态分配的内存被释放后,如果没有将指向该内存的指针设置为 NULL,这个指针就变成了野指针。

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

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

为了避免野指针问题,在释放内存后,应将指针设置为 NULL,这样可以防止意外地再次访问已释放的内存。

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

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

指针越界

在使用指针访问数组元素或动态分配的内存块时,如果指针偏移超出了合法的范围,就会发生指针越界。这可能导致程序访问到其他变量的内存空间,破坏数据或引发程序崩溃。

#include <stdio.h>

int main() {
    int arr[5] = {1, 2, 3, 4, 5};
    int *ptr = arr;
    for (int i = 0; i < 6; i++) {
        printf("%d ", *(ptr + i)); // 越界访问,行为未定义
    }
    return 0;
}

在这个例子中,数组 arr 只有 5 个元素,但循环尝试访问第 6 个元素,这会导致指针越界。在编写代码时,一定要确保指针的偏移在合法的范围内。

通过深入理解指针的各种特性、应用场景以及常见错误,我们能够更加熟练和安全地使用指针,充分发挥 C 语言的强大功能,编写出高效、灵活的程序。在实际编程中,不断实践和积累经验,有助于更好地掌握指针这一重要的概念。同时,要养成良好的编程习惯,如初始化指针、及时释放动态分配的内存等,以避免因指针使用不当而引发的各种问题。在处理复杂的数据结构和算法时,指针的正确运用往往是关键所在,它能够让我们以更加优雅和高效的方式管理内存和数据,提升程序的性能和可维护性。无论是简单的变量访问优化,还是构建复杂的链表、树等数据结构,指针都发挥着不可或缺的作用。在后续的编程学习和实践中,随着对 C 语言理解的不断深入,指针的重要性和实用性将愈发凸显。