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

C语言指针变量内容剖析

2023-05-013.5k 阅读

指针变量基础概念

在C语言中,指针变量是一种特殊的变量类型,它存储的是内存地址。内存地址可以理解为计算机内存中每个字节单元的编号,通过这个编号,我们可以准确地访问内存中的数据。

指针变量声明的一般形式为:类型说明符 *变量名; 例如:

int *ptr;
float *fp;

这里,ptr 是一个指向 int 类型数据的指针变量,fp 是一个指向 float 类型数据的指针变量。需要注意的是,* 在这里用于声明一个指针变量,而不是乘法运算符。

指针变量的初始化

指针变量在使用前最好进行初始化,否则它可能指向一个未知的内存位置,这可能会导致程序出现不可预测的行为。初始化指针变量有两种常见方式:

  1. 让指针指向一个已存在的变量:
#include <stdio.h>
int main() {
    int num = 10;
    int *ptr = &num; // 让指针ptr指向变量num
    printf("变量num的地址是:%p\n", &num);
    printf("指针ptr的值是:%p\n", ptr);
    return 0;
}

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

  1. 初始化为 NULL
#include <stdio.h>
int main() {
    int *ptr = NULL;
    if (ptr == NULL) {
        printf("指针ptr初始化为NULL\n");
    }
    return 0;
}

NULL 是一个宏定义,通常被定义为0,它表示指针不指向任何有效的内存地址。这样初始化可以避免指针成为野指针(指向未知或已释放内存的指针)。

指针变量与内存访问

指针变量的主要用途之一是通过它来访问内存中的数据。通过指针变量,我们可以间接地访问和修改其所指向的内存位置的数据。

间接访问运算符 *

在指针变量声明时,* 用于声明指针变量。而在使用指针变量时,* 是间接访问运算符(也称为解引用运算符),它用于访问指针所指向的内存位置的值。

#include <stdio.h>
int main() {
    int num = 10;
    int *ptr = &num;
    printf("变量num的值是:%d\n", num);
    printf("通过指针ptr访问的值是:%d\n", *ptr);
    *ptr = 20; // 通过指针修改num的值
    printf("修改后变量num的值是:%d\n", num);
    return 0;
}

在上述代码中,*ptr 表示访问指针 ptr 所指向的内存位置的值,也就是 num 的值。通过 *ptr = 20; 语句,我们可以修改 num 的值。

指针变量的算术运算

指针变量可以进行一些算术运算,主要包括加法、减法和比较运算。指针的算术运算与普通变量的算术运算有很大不同,因为指针的算术运算是基于其所指向的数据类型的大小的。

  1. 指针的加法运算: 当指针加上一个整数 n 时,实际上指针移动的字节数是 n * sizeof(指针指向的数据类型)。例如:
#include <stdio.h>
int main() {
    int arr[5] = {1, 2, 3, 4, 5};
    int *ptr = arr;
    printf("ptr指向的值是:%d\n", *ptr);
    ptr = ptr + 2; // 指针向后移动2个int类型的位置
    printf("移动后ptr指向的值是:%d\n", *ptr);
    return 0;
}

在这段代码中,ptr 最初指向数组 arr 的第一个元素。当 ptr = ptr + 2; 执行后,ptr 移动了 2 * sizeof(int) 个字节,指向了数组的第三个元素。

  1. 指针的减法运算: 指针的减法运算通常用于计算两个指针之间的距离,结果是两个指针之间相隔的元素个数。两个指针相减的前提是它们指向同一个数组中的元素。
#include <stdio.h>
int main() {
    int arr[5] = {1, 2, 3, 4, 5};
    int *ptr1 = &arr[0];
    int *ptr2 = &arr[3];
    int distance = ptr2 - ptr1;
    printf("两个指针之间的距离是:%d\n", distance);
    return 0;
}

在上述代码中,ptr2 - ptr1 的结果是 3,表示 ptr2ptr1 之间相隔3个 int 类型的元素。

  1. 指针的比较运算: 指针可以进行比较运算,如 ==!=<> 等。指针比较通常用于判断两个指针是否指向同一个内存位置,或者判断指针在数组中的相对位置。
#include <stdio.h>
int main() {
    int arr[5] = {1, 2, 3, 4, 5};
    int *ptr1 = &arr[0];
    int *ptr2 = &arr[2];
    if (ptr1 < ptr2) {
        printf("ptr1在ptr2之前\n");
    }
    return 0;
}

在这段代码中,通过比较 ptr1ptr2 的值,判断 ptr1 是否在 ptr2 之前。

指针变量与数组

在C语言中,指针和数组有着密切的关系。数组名在很多情况下可以被看作是一个指针常量,它指向数组的第一个元素。

数组名作为指针

#include <stdio.h>
int main() {
    int arr[5] = {1, 2, 3, 4, 5};
    int *ptr = arr; // 数组名arr作为指针,指向数组第一个元素
    for (int i = 0; i < 5; i++) {
        printf("arr[%d] = %d, *(ptr + %d) = %d\n", i, arr[i], i, *(ptr + i));
    }
    return 0;
}

在上述代码中,arr 可以当作一个指向 arr[0] 的指针。通过指针 ptr 也可以像使用数组下标一样访问数组元素,*(ptr + i) 等同于 arr[i]

指针与多维数组

对于多维数组,情况稍微复杂一些。以二维数组为例,二维数组可以看作是数组的数组。

#include <stdio.h>
int main() {
    int arr[3][4] = {
        {1, 2, 3, 4},
        {5, 6, 7, 8},
        {9, 10, 11, 12}
    };
    int (*ptr)[4] = arr; // ptr是一个指向包含4个int类型元素的数组的指针
    for (int i = 0; i < 3; i++) {
        for (int j = 0; j < 4; j++) {
            printf("arr[%d][%d] = %d, *(*(ptr + %d) + %d) = %d\n", i, j, arr[i][j], i, j, *(*(ptr + i) + j));
        }
    }
    return 0;
}

在这段代码中,int (*ptr)[4] 声明了一个指针 ptr,它指向一个包含4个 int 类型元素的数组。*(ptr + i) 指向二维数组的第 i 行,*(*(ptr + i) + j) 则访问第 i 行第 j 列的元素,等同于 arr[i][j]

指针变量与函数

指针在函数中有着广泛的应用,它可以用于传递大型数据结构以提高效率,也可以用于函数返回多个值等。

指针作为函数参数

通过将指针作为函数参数,可以在函数内部修改调用函数中变量的值。

#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 类型的指针作为参数,通过指针在函数内部交换了 num1num2 的值。

函数返回指针

函数也可以返回一个指针。需要注意的是,返回的指针所指向的内存空间在函数结束后仍然有效。

#include <stdio.h>
int *createArray(int size) {
    int *arr = (int *)malloc(size * sizeof(int));
    for (int i = 0; i < size; i++) {
        arr[i] = i + 1;
    }
    return arr;
}
int main() {
    int *ptr = createArray(5);
    for (int i = 0; i < 5; i++) {
        printf("arr[%d] = %d\n", i, ptr[i]);
    }
    free(ptr); // 释放动态分配的内存
    return 0;
}

在这段代码中,createArray 函数动态分配了一块内存,并返回指向这块内存的指针。在 main 函数中使用完这块内存后,需要使用 free 函数释放内存,以避免内存泄漏。

指向函数的指针

在C语言中,函数也有地址,我们可以定义指向函数的指针。指向函数的指针可以用于将函数作为参数传递给其他函数,或者根据不同的条件调用不同的函数。

#include <stdio.h>
int add(int a, int b) {
    return a + b;
}
int subtract(int a, int b) {
    return a - b;
}
void operate(int a, int b, int (*func)(int, int)) {
    int result = func(a, b);
    printf("运算结果是:%d\n", result);
}
int main() {
    int num1 = 10, num2 = 5;
    operate(num1, num2, add);
    operate(num1, num2, subtract);
    return 0;
}

在上述代码中,int (*func)(int, int) 定义了一个指向函数的指针 func,该函数接受两个 int 类型参数并返回一个 int 类型值。operate 函数接受两个整数和一个指向函数的指针作为参数,并调用相应的函数进行运算。

指针变量的高级话题

多级指针

多级指针是指针的指针,即一个指针变量存储的是另一个指针变量的地址。

#include <stdio.h>
int main() {
    int num = 10;
    int *ptr1 = &num;
    int **ptr2 = &ptr1;
    printf("num的值是:%d\n", num);
    printf("通过ptr1访问num的值是:%d\n", *ptr1);
    printf("通过ptr2访问num的值是:%d\n", **ptr2);
    return 0;
}

在这段代码中,ptr2 是一个二级指针,它指向 ptr1**ptr2 最终访问到 num 的值。

指针数组与数组指针

  1. 指针数组:指针数组是一个数组,数组中的每个元素都是一个指针。
#include <stdio.h>
int main() {
    int num1 = 10, num2 = 20, num3 = 30;
    int *arr[3] = {&num1, &num2, &num3};
    for (int i = 0; i < 3; i++) {
        printf("arr[%d]指向的值是:%d\n", i, *arr[i]);
    }
    return 0;
}

在上述代码中,arr 是一个指针数组,每个元素都是一个指向 int 类型变量的指针。

  1. 数组指针:数组指针是一个指针,它指向一个数组。前面在多维数组部分已经有过相关示例,如 int (*ptr)[4] 就是一个指向包含4个 int 类型元素数组的指针。

野指针与悬空指针

  1. 野指针:野指针是指向一个未知或未初始化内存位置的指针。例如:
#include <stdio.h>
int main() {
    int *ptr;
    *ptr = 10; // ptr是野指针,未初始化就使用
    return 0;
}

这段代码中,ptr 未初始化就试图通过它修改内存值,这会导致未定义行为。

  1. 悬空指针:悬空指针是指向曾经分配过内存,但现在该内存已经被释放的指针。例如:
#include <stdio.h>
#include <stdlib.h>
int main() {
    int *ptr = (int *)malloc(sizeof(int));
    *ptr = 10;
    free(ptr);
    *ptr = 20; // ptr成为悬空指针,所指向内存已释放
    return 0;
}

在这段代码中,free(ptr) 释放了 ptr 所指向的内存,之后再试图通过 ptr 修改内存值,会导致未定义行为。

为了避免野指针和悬空指针,在使用指针前一定要确保其已正确初始化,并且在释放内存后将指针赋值为 NULL

指针与内存管理

指针在动态内存分配和管理中起着关键作用。通过 malloccallocrealloc 等函数可以动态分配内存,并返回指向分配内存块的指针。

#include <stdio.h>
#include <stdlib.h>
int main() {
    int *ptr = (int *)malloc(5 * sizeof(int));
    if (ptr == NULL) {
        printf("内存分配失败\n");
        return 1;
    }
    for (int i = 0; i < 5; i++) {
        ptr[i] = i + 1;
    }
    for (int i = 0; i < 5; i++) {
        printf("ptr[%d] = %d\n", i, ptr[i]);
    }
    free(ptr);
    return 0;
}

在上述代码中,malloc 函数分配了一块能容纳5个 int 类型数据的内存,并返回指向这块内存的指针。使用完内存后,通过 free 函数释放内存,以避免内存泄漏。

calloc 函数与 malloc 类似,但它会将分配的内存初始化为0:

#include <stdio.h>
#include <stdlib.h>
int main() {
    int *ptr = (int *)calloc(5, sizeof(int));
    if (ptr == NULL) {
        printf("内存分配失败\n");
        return 1;
    }
    for (int i = 0; i < 5; i++) {
        printf("ptr[%d] = %d\n", i, ptr[i]);
    }
    free(ptr);
    return 0;
}

realloc 函数用于调整已分配内存块的大小:

#include <stdio.h>
#include <stdlib.h>
int main() {
    int *ptr = (int *)malloc(3 * sizeof(int));
    if (ptr == NULL) {
        printf("内存分配失败\n");
        return 1;
    }
    for (int i = 0; i < 3; i++) {
        ptr[i] = i + 1;
    }
    ptr = (int *)realloc(ptr, 5 * sizeof(int));
    if (ptr == NULL) {
        printf("内存重新分配失败\n");
        return 1;
    }
    for (int i = 3; i < 5; i++) {
        ptr[i] = i + 1;
    }
    for (int i = 0; i < 5; i++) {
        printf("ptr[%d] = %d\n", i, ptr[i]);
    }
    free(ptr);
    return 0;
}

在这段代码中,首先使用 malloc 分配了3个 int 类型的内存,然后使用 realloc 将其调整为5个 int 类型的内存。

通过深入理解指针变量的这些特性和应用,我们能够在C语言编程中更加灵活高效地管理内存和操作数据,编写出更健壮、更优化的程序。无论是在系统级编程、嵌入式开发还是其他领域,指针变量的熟练运用都是C语言程序员必备的技能。