【C语言进阶剖析】29、指针和数组分析(下)


在开始之前,先思考一个问题:数组名可以当作常量指针使用,那么指针是否也可以当作数组名来使用呢?

1 数组的访问方式

1.1 两种数组访问方式

访问数组中的元素有两种访问方式,通过下标访问和通过指针访问数组
在这里插入图片描述

1.2 下标形式和指针形式对比

下标形式和指针形式基本是等价的,但是效率略有区别

  • 指针以固定增量在数组中移动时,效率略高于下标形式
  • 指针增量为 1 且硬件具有硬件增量模型时,效率更高

注意:现代编译器的生成代码优化率已大大提高,在固定增量时,下标形式的效率已经和指针形式相当;但从可读性和代码维护的角度来看,下标形式更优

下标形式与指针形式之间可以相互转换,转换如下:
在这里插入图片描述
这里 a[n] 可是可以转换为 n[a] 的哦

下面通过一个例子说明数组的指针访问和下标访问

// 29-1.c
#include<stdio.h>
int main()
{
    int a[5] = {0};
    int* p = a;
    int i = 0;
    for (i = 0; i < 5; i++)
    {
        p[i] = i + 1;
    }
    for (i = 0; i < 5; i++)
    {
        printf("a[%d] = %d\n", i, *(a + i));
    }
    printf("\n");

    for (i = 0; i < 5; i++)
    {
        i[a] = i + 10;
    }
    for (i = 0; i < 5; i++)
    {
        printf("p[%d] = %d\n", i, p[i]);
    }
    return 0;
}

编译运行结果如下:

$ gcc 29-1.c -o 29-1
$ ./29-1
a[0] = 1
a[1] = 2
a[2] = 3
a[3] = 4
a[4] = 5

p[0] = 10
p[1] = 11
p[2] = 12
p[3] = 13
p[4] = 14

从结果我们可以看到,通过指针和数组都可以实现访问数组中的元素,可以把数组名当作常量指针使用,也可以把指针当作数组名来使用,a[i] 和 i[a] 是等效的

指针和数组的概念不是完全相等的,二着是有区别的,我们通过一个例子来说明指针数组和指针的不同

// 29-2.c
#include<stdio.h>
int main()
{
    extern int a[];
    printf("&a = %p\n", &a);
    printf("a = %p\n", a);
    printf("*a = %d\n", *a);
    return 0;
}
// ext.c
int a[] = {1, 2, 3, 4, 5};

编译运行结果如下:

$ gcc 29-2.c ext.c -o 29-2
$ ./29-2
&a = 0x560167697010
a = 0x560167697010
*a = 1

a 是数组首元素的地址,&a 是整个数组的地址,在数值上二者是相同的,*a 表示取元素第一个元素。

如果把 29-2.c 的第 6 行 extern int a[]; 改为 extern int *a; 代码更改如下:

// 29-2.c
#include<stdio.h>
int main()
{
    extern int *a;				// a[] 改为 *a
    printf("&a = %p\n", &a);
    printf("a = %p\n", a);
    printf("*a = %d\n", *a);
    return 0;
}

再次编译运行,结果完全不一样了

$ gcc 29-2.c ext.c -o 29-2
$ ./29-2
&a = 0x562f0c0ba010
a = 0x200000001
段错误 (核心已转储)

为什么会出现这个运行结果呢?编译器编译 ext.c 后,数组 a 的地址就是 0x562f0c0ba010,等价于 a 的地址就是 0x562f0c0ba010。编译 29-2.c 时,碰见 extern int* a; 就寻找其他位置定义的 a 的值,a 的地址为 0x562f0c0ba010,所以 &a 就是取 a 的地址,就是 0x562f0c0ba010,a 的值是该地址下对应的数据,为 0x200000001。*a 表示取地址值为 0x200000001 的数据,不合法,是不允许访问的,返回段错误。
下面来说明一下为什么地址 0x562f0c0ba010 对应的数据为 0x200000001,我们打印的是 %p,%p 代表的是:按十六进制输出数据,长度为 8 个字节,Linux 是小端序系统,低地址存放低位,数组存储方式如下,取 8 字节数据为 0x200000001

在这里插入图片描述

2 a 和 &a 的区别

  • a 为数组首元素的地址
  • &a 为整个数组的地址
  • a 和 &a 的区别在于指针运算

在这里插入图片描述
a + 1 表示指向下一个元素,a + 1是越过一个元素,&a + 1 表示指向这个数组最后一个元素后面的位置,&a + 1 是越过一个数组,可以从下图清晰的看到二者的指向
在这里插入图片描述

下面看一个例子,更好的区分 a 和 &a

// 29-3.c
#include<stdio.h>
int main()
{
    int a[5] = {1, 2, 3, 4, 5};
    int* p1 = (int*)(&a + 1);
    int* p2 = (int*)((long long)a + 1);
    int* p3 = (int*)(a + 1);
    printf("%d, %d, %d\n", p1[-1], p2[0], p3[1]);
    return 0;
}
$ gcc 29-3.c -o 29-3
$ ./29-3
5, 33554432, 3
  • p1 指向整个数组最后一个元素后面的位置, p1[-1] ==> *(p1 -1),所以 p1[-1] 指向数组最后一个元素。
  • (int*)((long long)a + 1); 先把指针转换为 long long 再相加,这已经不是指针运算了,就是基本数据类型之间的运算,再次解引用,等于从数组首元素的第二个字节开始,取 4 个字节的数据,结果如下,linux 系统是小端序,取到的数据为 0x02000000,等于十进制的 33554432。
  • p3 指向数组第二个元素

在这里插入图片描述

3 数组参数

  • 数组作为函数参数时,编译器将其编译成对应的指针

在这里插入图片描述
结论:将数组名作为参数传递给函数时,数组名退化为指针,一般情况下,当定义的函数中有数组参数时,需要定义另一个参数来表示数组的大小

下面通过例子来验证我们的理论

// 29-4.c
#include<stdio.h>
void func1(char a[5])
{
    printf("In func1: sizeof(a) = %ld\n", sizeof(a));
    *a = 'a';
    a = NULL;
}
void func2(char b[])
{
    printf("In func2: sizeof(b) = %ld\n", sizeof(b));
    *b = 'b';
    b = NULL;
}
int main()
{
    char array[10] = {0};
    func1(array);
    printf("array[0] = %c\n", array[0]);
    func2(array);
    printf("array[0] = %c\n", array[0]);
    return 0;
}

在 func1() 函数中,,打印 sizeof(a),数组退化成指针,所以不管数组长度如何,打印的都是指针的长度,由于退化成指针,所以允许 修改指针 a = NULL; 我们是不能修改数组名的指向的。func1() 函数是一样的,只是没有指定数组长度。

编译运行结果如下:

$ gcc 29-4.c -o 29-4
$ ./29-4
In func1: sizeof(a) = 8
array[0] = a
In func2: sizeof(b) = 8
array[0] = b

4 小结

1、数组名和指针仅使用方式相同,数组名本质不是指针,指针也不是数组名
2、数组名并不是数组的地址,而是数组首元素的地址
3、函数的数组参数退化为指针

发布了248 篇原创文章 · 获赞 115 · 访问量 9万+

猜你喜欢

转载自blog.csdn.net/happyjacob/article/details/103321499