探讨C语言中数组、元素内存地址之间的关系

小至尖尖优化案例 / 2023-08-24 / 原文

最近一直在研究C语言,总结出一个结论:C开发者就是和内存与数据结构在打交道。

这篇文章先整理一下内存这块学习到的知识以免后面忘记了。

 

我们先讨论下数组和指针之间的关系,代码如下:

 

#include <stdio.h>

int main() {

    int arr[3] = {77777,88888,99999};                           // 定义一个长度为3 的数组,取名为:arr
    int *pa = arr;                                              // 创建一个指针变量指向 arr 数组
    printf("arr 数组的指向:%p , pa变量的指向:%p \n" , arr,pa);
  
  return 0;  
}

 

可以看到 arr 和 pa 两个变量是指向一个内存地址。

 

继续看代码:

int main() {

    int arr[3] = {77777, 88888, 99999};           // 定义一个长度为3 的数组,取名为:arr
    int *pa = arr;                                         // 创建一个指针变量指向 arr 数组
    printf("arr 数组的指向:%p , pa变量的指向:%p \n", arr, pa);
    printf("数组第一个元素指的地址 = %p , 第一个元素值 = %d \n", &pa[0], *(pa));           // 如果p是个指针变量,则:p[i] 永远等价于 *(p+i) 
    printf("数组第二个元素指的地址 = %p , 第二个元素值 = %d \n", &pa[1], *(pa + 1));
    printf("数组第三个元素指的地址 = %p , 第三个元素值 = %d \n", &pa[2], *(pa + 2));
    return 0
}

 可以看到 数组 arr 变量的指向 和 数组内第一个元素的内存指向的内存空间是一样的,我们可以总结出以下结论:

 

    • 数组是一段连续的内存空间,用于存储同一类型的元素

 

    • 数组名作为指针:当数组名被用作指针时,它会退化成指向数组第一个元素的指针
    • 使用指针操作数组:可以使用指针和偏移量来访问和操作数组元素,对于一个 int* 指针 p(假设 int 是4字节),p + 1 会指向4字节后的地址(下一个元素),而不是紧接着的下一个字节。

 

回到代码继续探究一下数组内元素地址和内存地址之间的关系:

#include <stdio.h>

int main() {

    int arr[3] = {77777, 88888, 99999};           // 定义一个长度为3 的数组,取名为:arr
    int *pa = arr;                                         // 创建一个指针变量指向 arr 数组

    printf("arr 数组的指向:%p , pa变量的指向:%p \n", arr, pa);
    printf("数组第一个元素指的地址 = %p , 第一个元素值 = %d \n", &pa[0], *(pa));
    printf("数组第二个元素指的地址 = %p , 第二个元素值 = %d \n", &pa[1], *(pa + 1));
    printf("数组第三个元素指的地址 = %p , 第三个元素值 = %d \n", &pa[2], *(pa + 2));
    printf("\n");

    int *p = &pa[0];                           // 将数组下标为0的第一个元素内存地址赋值给 p指针变量
    printf("p指针:%p  \n" , p);


    return 0;
}

 这里将数组下标为0的第一个元素内存地址赋值给 p指针变量,可以看到 p 指针变量和 arr变量 、pa 指针变量指向的都是一块内存地址。

而这块内存地址保存着正是数值 77777  这个元素。

我们都知道在传统 x86 / x64 系统中,像 C/C++、JAVA 这类编译语言中 , int 类型一般是占用 4个字节,每个字节 8位,既32位保存一个 int 类型的数字。

像 int 类型占 4个字节,我们称为多字节变量而像 char 这种字符类型,只需要 1个字节即8位可以保存一个元素,我们称为单字节变量。

 

我们现在直接说结论:

  • 变量地址:每个变量都存储在内存中,而且每个变量都有一个起始地址,这通常是该变量的第一个字节的地址,例如,如果你有一个 int 类型的变量 x,那么&x将给出这个变量的起始地址。
  • 多字节变量:对于只需要一个字节的数据类型(例如 char),变量的地址与其唯一的字节的地址相同。但对于需要多个字节的数据类型(例如 int, float, double 等),变量的地址是这些连续字节的第一个字节的地址。

演示代码:

 

#include <stdio.h>

int main() {

    int arr[3] = {77777, 88888, 99999};           // 定义一个长度为3 的数组,取名为:arr
    int *pa = arr;                                         // 创建一个指针变量指向 arr 数组

    printf("arr 数组的指向:%p , pa变量的指向:%p \n", arr, pa);
    printf("数组第一个元素指的地址 = %p , 第一个元素值 = %d \n", &pa[0], *(pa));
    printf("数组第二个元素指的地址 = %p , 第二个元素值 = %d \n", &pa[1], *(pa + 1));
    printf("数组第三个元素指的地址 = %p , 第三个元素值 = %d \n", &pa[2], *(pa + 2));
    printf("\n");

    int *p = &pa[0];                           // 将数组下标为0的第一个元素内存地址赋值给 p指针变量
    printf("p指针:%p  \n" , p);



    char* charPer = (char*)p;           // 将数组第一个元素 77777 指针转换成 char 指针,4个字节 转换成 1个字节,对每个字节进行遍历


    printf("指针变量p的值: %d\n", *p);
    printf("指针变量p的地址(起始字节的地址): %p\n", charPer);

    printf("\n遍历指针变量p的每个字节:\n");

    // 遍历int变量的每一个字节
    for (size_t i = 0; i < sizeof(*p); i++) {
        printf("字节 %zu 地址: %p -> 值: %x\n", i+1, charPer + i, (unsigned char)charPer[i]);
    }

    printf("\n" );
    // 输出 x 16进制的内存地址
    printf("指针变量p地址16进制: = %X" ,*p);

    printf("\n" );

    return 0;
}

 

通过上面代码可以发现,int 类型的元素内存占4个字节,变量指向的是第一个字节的内存地址。

本篇博文总结出一个主要的观点:

  • 在C语言中,数组是一块连续的内存空间,数组变量名指向第一个元素的内存地址。
  • 数组第一个元素指又指向第一个字节的内存地址。
  • 即数组内存地址 = 数组第一个元素的第一个字节的内存地址。 

 

扩展内容,上面遍历每个字节的内存地址是在小端序Linux系统上输出的结果:

 

遍历指针变量p的每个字节:
字节 1 地址: 00000068a7fffa74 -> 值: d1
字节 2 地址: 00000068a7fffa75 -> 值: 2f
字节 3 地址: 00000068a7fffa76 -> 值: 1
字节 4 地址: 00000068a7fffa77 -> 值: 0

指针变量p地址16进制: = 12FD1

 

如果是在像 AIX 这种大端序的系统上输出的结果可能正好相反

模拟 AIX系统上输入的结果:

字节 1 地址: [相应的内存地址] -> 值: 0
字节 2 地址: [相应的内存地址 + 1] -> 值: 1
字节 3 地址: [相应的内存地址 + 2] -> 值: 2f
字节 4 地址: [相应的内存地址 + 3] -> 值: d1


字节 1 地址: 00000068a7fffa74 -> 值: 0
字节 2 地址: 00000068a7fffa75 -> 值: 1
字节 3 地址: 00000068a7fffa76 -> 值: 2f
字节 4 地址: 00000068a7fffa77 -> 值: d1

指针变量p地址16进制: = 12FD1

在小端序系统(Linux系统)中,数据的最低有效字节存储在最低的内存地址,而最高有效字节存储在最高的内存地址

而在大端序系统中,情况正好相反:数据的最高有效字节存储在最低的内存地址,而最低有效字节存储在最高的内存地址。

之前我知道这个系统有分大小端字节序之间的区别,但是一直不理解这个内存字节高低位置是个什么意思,通过学习C语言,也算是搞清楚这里面的逻辑。