C 语言指针 “多维迷宫”:二维数组、指针数组与指向指针的指针大揭秘(三)

一、二维数组

1、二维数组的存储本质与指针关系

  • 存储布局:在C语言中,二维数组在内存中是按行存储的。例如,对于二维数组int a[2][3],它的存储顺序是先存储第一行的元素a[0][0]a[0][1]a[0][2],然后再存储第二行的元素a[1][0]a[1][1]a[1][2]。从内存角度看,它可以看作是一个连续的内存块,总共有2 * 3 = 6int类型的元素。
  • 指针表示:二维数组名本身是一个指向数组的指针。对于int a[2][3]a是一个指向包含3个int元素的数组的指针。可以把a看作是一个指针,它指向a[0]a[0]是一个包含3个int元素的数组)。如果将a的值(它是一个地址)赋给一个指针变量,例如int (*p)[3]=a;,这里p是一个指向包含3个int元素的数组的指针,pa在类型上是相同的。

2、通过指针访问二维数组元素

  • 行指针方式
    • 利用指向数组的指针来访问元素。例如,对于int a[2][3]int (*p)[3]=a;,可以通过(*(p + i))[j]来访问a[i][j]。这里p + i使指针p移动i个“包含3个int元素的数组”的单位,*(p + i)得到第i行数组,然后[j]操作访问该行数组中的第j个元素。
    • 例如,假设a[0][0]=1; a[0][1]=2; a[0][2]=3; a[1][0]=4; a[1][1]=5; a[1][2]=6;,如果int (*p)[3]=a;,那么(*(p + 1))[2]的值就是6(即a[1][2])。
  • 普通指针方式
    • 可以将二维数组看作是一个一维数组,其中每个元素又是一个指针(指向每行的数组)。例如,对于int a[2][3],可以定义一个指针int *q=(int *)a;。此时,访问a[i][j]可以通过*(q + i * 3 + j)来实现。
    • 解释一下这个公式,i * 3是因为每行有3个元素,先跳过i行,然后再加上j就可以访问到a[i][j]这个元素。例如,对于前面定义的数组a,如果int *q=(int *)a;,那么*(q + 1 * 3+2)的值就是6(即a[1][2])。

3、二维数组作为函数参数与指针的应用

  • 传递二维数组参数
    • 当二维数组作为函数参数时,函数形参可以声明为指向数组的指针。例如,一个函数用于打印二维数组的元素:
      void printArray(int (*arr)[3], int rows) {
          for (int i = 0; i < rows; i++) {
              for (int j = 0; j < 3; j++) {
                  printf("%d ", (*(arr + i))[j]);
              }
              printf("\n");
          }
      }
      
    • 可以这样调用这个函数:int a[2][3] = { {1, 2, 3}, {4, 5, 6} }; printArray(a, 2);。在函数调用时,二维数组名a会被隐式转换为指向包含3个int元素的数组的指针,传递给printArray函数的arr参数。
  • 灵活的参数传递方式
    • 也可以将二维数组参数转换为指针数组来处理。例如,定义一个函数,它的参数是一个指针数组和行数、列数:
      void printArray2(int **arr, int rows, int cols) {
          for (int i = 0; i < rows; i++) {
              for (int j = 0; j < cols; j++) {
                  printf("%d ", arr[i][j]);
              }
              printf("\n");
          }
      }
      
    • 调用这个函数需要先将二维数组转换为指针数组的形式。例如:
      int a[2][3] = { {1, 2, 3}, {4, 5, 6} };
      int *p[2];
      for (int i = 0; i < 2; i++) {
          p[i]=a[i];
      }
      printArray2(p, 2, 3);
      
    • 这种方式在处理动态分配的二维数组(如使用malloc分配的二维数组)或者不规则的二维数组(每行元素个数可能不同)时比较有用。

通过以上从存储本质、元素访问和函数参数等多个角度对二维数组和指针关系的剖析,可以更深入地理解C语言中二维数组的工作原理,并且能够更加灵活地运用指针来操作二维数组。

二、指针数组

1、概念

  • 指针数组是一个数组,其元素是指针类型。也就是说,数组中的每个元素都存储着一个内存地址。在C语言中,它的定义形式为类型 *数组名[数组大小]。例如int *arr[5];,这就定义了一个包含5个元素的指针数组arr,每个元素都是一个指向int类型的指针。

2、本质

  • 从内存角度看,指针数组在内存中是连续存储各个指针元素的。这些指针元素本身存储的是其他变量或者数组等的地址。它和普通数组一样,有自己的基地址,通过这个基地址和索引可以访问到每个指针元素。
  • 本质上,指针数组是一种数据结构,用于集中管理多个指针,这些指针可以指向不同的内存区域,并且可以根据需要灵活地操作这些指针所指向的数据。

3、应用场景和示例

  • 字符串数组(字符指针数组)
    • 在C语言中,没有专门的字符串类型,字符串通常是以字符数组或者字符指针的形式存在。当处理多个字符串时,使用指针数组是一种很方便的方式。
    • 例如:
    #include <stdio.h>
    int main() {
        char *fruits[] = {"apple", "banana", "cherry"};
        int size = sizeof(fruits)/sizeof(fruits[0]);
        for (int i = 0; i < size; i++) {
            printf("%s\n", fruits[i]);
        }
        return 0;
    }
    
    • 在这个例子中,fruits是一个指针数组,它的每个元素(fruits[0]fruits[1]fruits[2])都是一个字符指针,分别指向存储字符串"apple""banana""cherry"的内存区域。当使用printf("%s\n", fruits[i]);时,%s格式说明符会根据指针fruits[i]所指向的字符数组(字符串)的起始地址,从这个地址开始逐个字符地打印,直到遇到字符串结束符'\0'
  • 函数指针数组
    • 用于存储多个函数的指针,这样可以根据不同的条件调用不同的函数,增加程序的灵活性。
    • 例如,有两个函数用于计算两个数的和与差:
    #include <stdio.h>
    int add(int a, int b) {
        return a + b;
    }
    int subtract(int a, int b) {
        return a - b;
    }
    int main() {
        int (*funcPtrs[])(int, int) = {add, subtract};
        int a = 5, b = 3;
        printf("加法结果:%d\n", funcPtrs[0](a, b));
        printf("减法结果:%d\n", funcPtrs[1](a, b));
        return 0;
    }
    
    • 在这个例子中,int (*funcPtrs[])(int, int)定义了一个函数指针数组funcPtrs,它的元素是指向返回值为int,参数为两个int类型的函数的指针。数组funcPtrs的第一个元素funcPtrs[0]指向add函数,第二个元素funcPtrs[1]指向subtract函数。通过funcPtrs[0](a, b)funcPtrs[1](a, b)这样的方式可以调用相应的函数。
  • 指向动态分配内存的指针数组
    • 可以用于管理多个动态分配的内存区域。
    • 例如,动态分配多个int类型的数组:
    #include <stdio.h>
    #include <stdlib.h>
    int main() {
        int *nums[3];
        for (int i = 0; i < 3; i++) {
            nums[i] = (int *)malloc(sizeof(int) * 5);
            if (nums[i] == NULL) {
                printf("内存分配失败!\n");
                return 1;
            }
        }
        // 对动态分配的数组进行赋值等操作
        for (int i = 0; i < 3; i++) {
            for (int j = 0; j < 5; j++) {
                nums[i][j] = i * 5 + j;
            }
        }
        // 打印动态分配数组的值
        for (int i = 0; i < 3; i++) {
            for (int j = 0; j < 5; j++) {
                printf("%d ", nums[i][j]);
            }
            printf("\n");
        }
        // 释放动态分配的内存
        for (int i = 0; i < 3; i++) {
            free(nums[i]);
        }
        return 0;
    }
    
    • 在这里,nums是一个指针数组,通过循环为每个指针元素分配了一个包含5个int元素的动态内存空间。然后可以像使用二维数组一样(通过nums[i][j])对这些动态分配的内存进行赋值和访问。最后,通过循环释放每个动态分配的内存区域,避免内存泄漏。

4、注意事项

  • 内存管理:如果指针数组中的指针指向动态分配的内存,那么需要注意正确地释放这些内存。在上面动态分配内存的例子中,必须逐个释放每个指针所指向的内存区域,否则会导致内存泄漏。
  • 指针初始化:在使用指针数组时,要确保数组中的指针被正确初始化。如果指针没有初始化就进行解引用操作,可能会导致程序崩溃或者出现不可预测的行为。例如,在定义了int *arr[5];后,如果没有给arr中的指针元素赋值,直接访问*arr[0]是错误的。
  • 指针类型匹配:指针数组中的指针类型要与所指向的数据类型相匹配。例如,不能将一个指向double类型的指针存储在一个定义为int *类型的指针数组中。

三、二级指针

1、概念

  • 在C语言中,指针是用来存储变量地址的变量。二级指针是指向指针的指针,即它存储的是另一个指针变量的地址。如果一级指针(普通指针)可以理解为间接访问一个变量,那么二级指针就是通过两级间接访问来操作变量。

2、本质

  • 从内存角度看,二级指针本身也占用内存空间,其内容是另一个指针的内存地址。例如,对于一个int类型的变量a,有一个一级指针p指向a*p = a),那么二级指针pp可以指向p**pp = a,通过*pp可以访问p,通过**pp可以访问a)。
  • 二级指针本质上是对指针的更高层次的间接引用,用于在需要操作指针本身的地址或者构建更复杂的数据结构(如指针数组)时使用。

3、应用

  • 函数参数传递指针的地址:当需要在函数内部修改指针变量本身时,就需要传递指针的地址,也就是使用二级指针作为函数参数。
  • 例如,动态内存分配函数malloc返回一个指向所分配内存块的指针。如果要在一个函数中分配内存,并让调用者能够获取这个新分配的指针,就可以使用二级指针。
#include <stdio.h>
#include <stdlib.h>
void allocateMemory(int **ptr) {
    *ptr = (int *)malloc(sizeof(int));
    if (*ptr == NULL) {
        printf("内存分配失败!\n");
        return;
    }
    **ptr = 10;
}
int main() {
    int *p = NULL;
    allocateMemory(&p);
    printf("分配的内存中的值为:%d\n", *p);
    free(p);
    return 0;
}
  • 在这个例子中,allocateMemory函数接受一个二级指针int **ptr。在函数内部,通过*ptr = (int *)malloc(sizeof(int));int类型分配内存,并将分配后的内存地址赋值给*ptr(也就是main函数中的p)。然后通过**ptr = 10;将分配的内存中的值设置为10。在main函数中,调用allocateMemory(&p),将p的地址传递给函数,这样函数就可以修改p本身的值。
  • 处理指针数组:二级指针可以方便地用于处理指针数组。
  • 例如,假设有一个字符串数组(实际上是一个字符指针数组),可以使用二级指针来遍历和操作这个数组。
#include <stdio.h>
void printStringArray(char **arr, int size) {
    for (int i = 0; i < size; i++) {
        printf("%s\n", arr[i]);
    }
}
int main() {
    char *strings[] = {"Hello", "World", "C Language"};
    int size = sizeof(strings)/sizeof(strings[0]);
    printStringArray(strings, size);
    return 0;
}
  • 在这个例子中,printStringArray函数接受一个二级指针char **arr和数组大小sizestrings数组是一个字符指针数组,当传递给printStringArray函数时,会发生隐式转换,将strings(类型为char *[])转换为char **。在函数内部,通过arr[i](等同于*(arr + i))来访问每个字符串指针,然后通过printf("%s\n", arr[i]);打印出字符串内容。

4、注意事项

  • 解引用操作顺序:使用二级指针时,要清楚解引用的顺序。例如,对于二级指针pp*pp得到它所指向的一级指针,**pp得到一级指针所指向的变量的值。如果解引用顺序错误,可能会导致程序出错,如段错误(访问非法内存地址)。
  • 内存管理:当使用二级指针进行动态内存分配时,要注意内存的释放。如果在一个函数中分配了内存,需要确保在合适的地方(通常是调用者)正确地释放内存。否则可能会导致内存泄漏。在上面的allocateMemory例子中,在main函数中通过free(p)正确地释放了在allocateMemory函数中分配的内存。
  • 初始化:二级指针在使用前应该进行正确的初始化。如果没有初始化,它可能包含一个随机的内存地址,在解引用时可能会导致不可预测的结果。例如,在定义一个二级指针int **pp;后,最好先将其赋值为NULL或者使其指向一个有效的一级指针。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值