问题: 数组名可以当作常量指针使用,那么指针是否也可以当作数组名来使用呢?

数组的访问方式

  • 以下标的形式访问数组中的元素
void code_1()
{
    int a[5] = {0};
    
    a[1] = 3;
    a[2] = 5;
}
  • 以指针的形式访问数组中的元素
void code_2()
{
    int a[5] = {0};
    
    *(a + 1) = 3;
    *(a + 2) = 5;
}

下标形式 VS 指针形式

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

a[n] <--> *(a + n) <--> *(n + a) <--> n[a]

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

编程实验: 数组的访问方式

#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;
}
输出:
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

编程实验: 数组和指针不同

test.c

#include <stdio.h>

int main()
{
    extern int* a;
    
    printf("&a = %p\n", &a);
    printf("a = %p\n", a);
    printf("*a = %d\n", *a);
}

ext.c

int a[] = {1, 2, 3, 4, 5};
输出:【编译无警告,无错误】
&a = 0x804a014
a = 0x1
段错误

分析:
  • 为什么编译没有警告,没有报错呢?

编译器中,在编译阶段进行语法、语义检查,进行警告和错误提示。但是各个文件在编译阶段是互相独立的,文件之间没有关联。在 test.c 文件中, 编译器发现 extern int* a; 就会把 a 当作一个外部定义的 int*指针变量进行解析使用,不再做其它任何处理;
在链接阶段,发现 a 符号确实是存在的,而连接器没有能力去检查 a 的属性,因此链接也无警告和错误提示,导致正常编译生成最终文件。

  • 运行过程中发生了什么呢?
  1. ext.c中,编译器发现 int a[] = {1, 2, 3, 4, 5}; 便会分配 20 字节空间,空间名为a,其中包含 5 个 int 类型元素,并进行初始化;
  2. test.c中, 编译器发现 extern int* a; 此后 a 在当前文件中就会被当作 int* 指针变量进行解析;
  3. &a 当前文件中,意图访问 int* 指针变量的地址,实质结果为访问数组a[5]的数组名a的地址,即访问数组首元素的地址&a[0],&a[0] 对数组操作是有效的,因此正常输出0x804a014;
  4. a 当前文件中,意图访问 int* 指针变量存储的地址值,此时 a 地址为 0x804a014,实质结果为访问 0x804a014 处存储的数值, 即 数组a[5] 中 *a ==> a[0] ==> 0x01;
  5. *a 当前文件中,意图访问 int* 指针变量指向的地址处空间,a 存储的地址值为 0x01, 就会访问 0x01 地址处的内容,造成段错误。

a 与 &a 的区别

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

a + 1 ==> (unsigned int)a + sizeof(*a)
&a + 1 ==> (unsigned int)(&a) + sizeof(*&a) ==> (unsigned int)(&a) + sizeof(a)

实例分析: 指针运算经典问题

#include <stdio.h>

int main()
{
    int a[5] = {1, 2, 3, 4, 5};
    int* p1 = (int*)(&a + 1);
    int* p2 = (int*)((int)a + 1);
    int* p3 = (int*)(a + 1);
    
    printf("%d, %d, %d\n", p1[-1], p2[0], p3[1]);
    
    return 0;
}
输出:【linux 小端模式】
5, 33554432, 3

分析:
p1[-1] : p1 = (int*)(&a + 1) ==> p1 = (int*)(&a + sizeof(*&a)) ==> p1 = (int*)(&a + sizeof(a)) ==> p1 = (int*)(&a[0 + 5]) ==> p1 = (int*)(&a[5])
         p1[-1] = a[5 - 1] = a[4] = 5
-----
p3[1] :  p3 = (int*)(a + 1) ==> p3 = (int*)(&a[0 + 1]) ==> (int*)(&a[1]) 
         p3[1] = a[1 + 1] = a[2] = 3
-----
p2[0] :  p2 = (int*)((int)a + 1);

clipboard.png

数组参数

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

void f(int a[]); <--> void f(int* a);
void f(int a[5]); <--> void f(int* a);

结论: 一般情况下,当定义的函数中有数组参数时,需要定义另一个参数来标识数组的大小。

实例分析: 虚幻的数组参数

#include <stdio.h>

void func1(char a[5])
{
    printf("In func1: sizeof(a) = %d\n", sizeof(a));  // 请注意打印输出
    
    *a = 'a';
    
    a = NULL;      // 请注意打印输出
}

void func2(char b[])
{
    printf("In func1: sizeof(b) = %d\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;
}
输出:【编译无警告,无错误】
In func1: sizeof(a) = 4
array[0] = a
In func1: sizeof(b) = 4
array[0] = b

小结

  • 数组名和指针仅一定情况下使用方式相同

    • 数组名的本质不是指针
    • 指针的本质不是数组
  • 数组名并不是数组的地址,而是数组首元素的地址
  • 函数的数组参数退化为指针

补充:以上实验在32位机器中运行。

当指针大小占用 8 字节时(64位机器), 使用 (unsigned int) 强制类型转换将发生数据截断,导致得到不正常的结果。

以上内容参考狄泰软件学院系列课程,请大家保护原创!


TianSong
734 声望138 粉丝

阿里山神木的种子在3000年前已经埋下,今天不过是看到当年注定的结果,为了未来的自己,今天就埋下一颗好种子吧