深入剖析 C 语言数组类型差异:指针视角下的奇妙世界

文章目录

一、普通数组与指针数组

  • 普通数组
    • 例如int arr[5];,这是一个简单的一维数组,它包含5个int类型的元素。从类型角度看,arr的类型是int [5],表示一个包含5个int元素的数组。在内存中,这些元素是连续存储的,数组名arr在大多数情况下代表数组的首地址。
    • 当我们使用arr作为表达式时,在很多情况下它会"衰减"为一个指向数组首元素的指针,类型变为int *。例如,当把arr传递给一个函数时,函数接收到的实际是一个int *类型的指针,指向数组的首元素。
  • 指针数组
    • 定义形式为类型 *数组名[数组大小];,例如int *pArr[3];。这里pArr是一个数组,它的元素类型是int *,即每个元素都是一个指向int类型的指针。
    • 假设我们有三个int变量a = 1, b = 2, c = 3,可以这样使用pArrint *pArr[3] = {&a, &b, &c};,此时pArr[0]指向apArr[1]指向bpArr[2]指向c。这种数组主要用于存储多个指针,方便对这些指针所指向的对象进行操作。

二、二维数组与数组指针(指向数组的指针)

  • 二维数组
    • 例如int twoDArr[3][4];,这是一个二维数组,它的类型是int [3][4],可以看作是一个包含3个元素的数组,每个元素又是一个包含4个int元素的数组。在内存中,它的存储方式是按行存储,即先存储第一行的4个元素,再存储第二行的元素,以此类推。
    • 当我们使用twoDArr作为表达式时,它也会衰减,不过它衰减后的类型是int (*)[4](指向包含4个int元素的数组的指针)。例如,在函数参数传递中,如果有一个函数接收一个二维数组作为参数,形式参数可以写成void func(int (*p)[4]),这里p就是一个数组指针,它可以接收二维数组twoDArr作为实参。
  • 数组指针(指向数组的指针)
    • 定义形式为类型 (*指针变量名)[数组大小];,如int (*p)[4];。这种指针是专门用于指向一个包含特定数量元素(这里是4个int元素)的数组。
    • 假设我们有一个二维数组int arr[3][4],可以通过以下方式使用数组指针:
c 复制代码
int (*p)[4];
p = arr;
for(int i = 0; i < 3; i++){
    for(int j = 0; j < 4; j++){
        // 访问二维数组元素
        printf("%d ",(*(p + i))[j]);
    }
    printf("\n");
}
 - 这里`p`指向`arr`的首行(即一个包含4个`int`元素的数组),`p + i`指向第`i`行,`(*(p + i))[j]`就相当于`arr[i][j]`,
 - 通过这种方式可以使用数组指针来访问二维数组。

三、字符数组与字符指针

  • 字符数组
    • 例如char str1[] = "hello";,这是一个字符数组,它的类型是char [6](包括字符串结束符\0)。字符数组在内存中有自己的存储空间,用于存储字符序列。
    • 可以通过下标来访问数组中的字符,如str1[0]表示字符'h'。而且字符数组可以通过strcpy等函数来修改其中的内容。
  • 字符指针
    • 定义形式为char *str2;,如果str2 = "world";,这里str2是一个字符指针,它指向一个字符串常量(在C语言中,字符串常量存储在只读数据区)。其类型是char *
    • 虽然可以通过str2来访问字符串中的字符,如printf("%c", str2[0]);会输出'w',但是不能通过str2来修改字符串常量的内容(在标准C语言环境下,这样做是未定义行为)。如果想修改字符串内容,需要通过字符数组来实现。例如,char str3[] = "test";,可以通过str3[0]='T';来修改。

四、数组名和&数组名的区别

1、数组名

  • 代表首元素地址(大部分情况) :在C和C++语言中,数组名在大多数情况下会"衰减"为指向数组首元素的指针。例如,对于int arr[5];arr通常等价于&arr[0],它们的类型都是int*。这意味着可以使用数组名来访问数组中的元素,就像使用指针一样。例如,可以通过*(arr + 1)来访问arr[1]
  • 类型信息(数组类型) :尽管数组名在很多操作中表现得像指针,但它本身的类型是"数组类型"。对于int arr[5];arr的类型是int [5]。这个类型信息在一些特定的场景下会体现出来,比如在使用sizeof运算符时,sizeof(arr)返回的是整个数组所占用的字节数(在这个例子中是5 * sizeof(int)),而不是指针的大小。
  • 作为函数参数 :当数组名作为函数参数传递时,它会被转换为指向数组首元素的指针。例如,有一个函数void func(int a[]),实际上它等价于void func(int *a)。这是因为在函数调用过程中,传递的是数组的首元素地址,而不是整个数组的副本。

2、取地址数组名(&数组名)

  • 代表整个数组的地址 :对于int arr[5];&arr返回的是整个数组的地址。它的类型是int (*)[5],这是一个指向包含5个int元素的数组的指针。与arr(大部分情况下等价于&arr[0])不同,&arr的地址值在数值上虽然和&arr[0]相同,但它们的类型和含义是不同的。
  • 在指针运算中的差异 :因为&arr是整个数组的地址,所以在指针运算中会有不同的行为。例如,如果p是一个指向整个数组的指针(类型为int (*)[5]),那么p + 1会跳过整个数组的长度(即5 * sizeof(int)字节),而如果q是一个指向数组首元素的指针(类型为int*),那么q + 1只会跳过一个元素的长度(即sizeof(int)字节)。
  • 在函数参数传递中的特殊用途 :当想要传递整个数组(而不是仅仅传递数组的首元素地址)给一个函数时,可以使用指向数组的指针作为参数类型。例如,void func(int (*p)[5])这个函数可以接收一个指向包含5个int元素的数组的指针。在调用这个函数时,可以使用func(&arr),这里&arr就是整个数组的地址。这种方式在处理多维数组等复杂情况时非常有用,能够更准确地操作数组。

3、举例

  1. 示例代码
c 复制代码
#include <stdio.h>
int main() {
    int arr[5] = {1, 2, 3, 4, 5};
    int *p1 = arr; 
    int (*p2)[5] = &arr;

    // 输出数组名和取地址数组名的值(地址值)
    printf("arr: %p\n", (void *)arr);
    printf("&arr: %p\n", (void *)&arr);

    // 指针运算的区别
    printf("p1 + 1: %p\n", (void *)(p1 + 1));
    printf("p2 + 1: %p\n", (void *)(p2 + 1));

    // 通过数组名和取地址数组名访问数组元素
    printf("*(arr + 2): %d\n", *(arr + 2));
    printf("(*(&arr))[2]: %d\n", (*(&arr))[2]);
    return 0;
}
  • 运行结果:
  1. 详细讲解
    • 指针定义部分
      • int *p1 = arr;:这里将p1定义为一个int类型的指针,并将arr赋值给p1。由于数组名arr在这种情况下会衰减为指向首元素的指针,所以p1现在指向arr的第一个元素1
      • int (*p2)[5] = &arr;p2被定义为一个指向包含5个int元素的数组的指针,然后将&arr(整个数组的地址)赋值给p2
    • 输出地址值部分
      • printf("arr: %p\n", (void *)arr);printf("&arr: %p\n", (void *)&arr);:这两行代码分别输出数组名arr的值(也就是首元素的地址)和取地址数组名&arr(整个数组的地址)。在很多情况下,这两个地址值在数值上是相同的,但它们的类型不同。
    • 指针运算部分
      • printf("p1 + 1: %p\n", (void *)(p1 + 1));:因为p1是指向int类型的指针,所以p1 + 1会使指针向后移动一个int类型元素的大小。在这个例子中,如果int类型占4个字节,那么p1 + 1指向的地址会比p1的地址增加4个字节,也就是指向数组中的第二个元素2
      • printf("p2 + 1: %p\n", (void *)(p2 + 1));p2是指向包含5个int元素的数组的指针,所以p2 + 1会使指针向后移动一个包含5个int元素的数组的大小。如果int类型占4个字节,那么p2 + 1指向的地址会比p2的地址增加5 * 4 = 20个字节,这就跳过了整个arr数组。
    • 访问数组元素部分
      • printf("*(arr + 2): %d\n", *(arr + 2));:这里利用数组名arr衰减为指向首元素的指针的特性,arr + 2表示指向数组中第三个元素的指针,然后通过*(arr + 2)来获取这个元素的值,即3
      • printf("(*(&arr))[2]: %d\n", (*(&arr))[2]);:首先&arr是整个数组的地址,*(&arr)就相当于arr(整个数组),然后(*(&arr))[2]就相当于arr[2],所以输出的值也是3
相关推荐
高一学习c++会秃头吗10 分钟前
string
开发语言·c++
yuanbenshidiaos38 分钟前
c语言-----数组
c语言·开发语言·算法
蓝天扶光1 小时前
C++算法第十一天
开发语言·c++·算法
从以前1 小时前
python练习:“互联网 +” 时代的出租车资源配置的数学建模(一)
开发语言·javascript·人工智能·python·算法·数学建模
池鱼ipou2 小时前
算法小白的救星:一步步教你玩转栈与最小栈
前端·javascript·算法
蹉跎x3 小时前
力扣45. 跳跃游戏 II
算法·leetcode·游戏
Wils0nEdwards3 小时前
Leetcode 环形子数组的最大和
java·算法·leetcode
sweetheart7-73 小时前
LeetCode1143. 最长公共子序列(2024冬季每日一题 36)
算法·动态规划·力扣·dp·最长公共子序列
木向3 小时前
leetcode17:电话号码的字母组合
开发语言·c++·算法
机器学习之心3 小时前
轻量级+鲸鱼优化!WOA-LightGBM鲸鱼优化算法优化轻量级梯度提升机分类预测Matlab实现
算法·matlab·分类·鲸鱼优化算法优化·woa-lightgbm·轻量级梯度提升机分类预测