指针与数组

发布时间 2023-06-19 00:09:06作者: comsoi

数组和指针可以相互转换,但它们不是等价的。

指针与数组


数组的名字被当做指针使用

在 C 和 C++ 中,指针与数组有非常紧密的联系。实际上,使用数组的时候,编译器通常都是在操作指针。这里我们从两个角度说明数组名在很多时候被当做是一个指针。

int nums[] = {1, 2, 3};
int *p = &(nums[0]);
if (p == nums) {
    printf("true!\n");
}
size_t i = 0;
for (i = 0; i != 3; ++i) {
    printf("%d\n", p[i]);
}

如果你执行这一小段代码,那么,不出意外的话,程序会在终端上打印 true!,以及 nums 中的三个数字。这预示着,指针变量 p 保存的内容(也就是 nums[0] 的地址)和 nums 保存的内容是完全一样的;同时,编译器以相同的方式去解释 pnums。显然 p 是一个指向 int 型变量的指针,那么 nums 也就是一个指针了。

C++11 标准引入了 auto 关键字,它能够在定义变量时,探测初始值的类型,并为新定义的变量设置合适的类型。我们看看 auto 关键字作用于数组名字的时候,会发生什么。

int nums[] = {1, 2, 3};
auto what = nums;
int val = 42;
what = &val;

这份代码在 C++11 标准中,可以顺利通过。这说明 what 的类型,经由 auto 检测,是 int *

这两个例子,足以说明:当数组名字被当做是一个值来使用的时候,它就相当于是一个指针。

当然,也不是全部时候,数组名字都被当做是简单的指针。比如在数组名字被传入 sizeof() 运算符的时候,它会被当做是一个真实的数组来看待。

用指针引用数组元素

引用数组元素可以用“下标法”,除了这种方法之外还可以用指针,即通过指向某个数组元素的指针变量来引用数组元素。

数组包含若干个元素,元素就是变量,变量都有地址。所以每一个数组元素在内存中都占有存储单元,都有相应的地址。指针变量既然可以指向变量,当然也就可以指向数组元素。同样,数组的类型和指针变量的基类型一定要相同。

# include <stdio.h>
int main(void)
{
    int a[] = {1, 2, 3, 4, 5};
    int *p = &a[0];
    int *q = a;
    printf("*p = %d, *q = %d\n", *p, *q);
    return 0;
}
输出结果是:
*p = 1, *q = 1

程序中定义了一个一维数组 a,它有 5 个元素,即 5 个变量,分别为 a[0]、a[1]、a[2]、a[3]、a[4]。所以 p=&a[0] 就表示将 a[0]
的地址放到指针变量 p 中,即指针变量 p 指向数组 a 的第一个元素 a[0]。而 C 语言中规定,“数组名”是一个指针“常量”,表示数组第一个元素的起始地址。所以
p=&a[0] 和 p=a 是等价的,所以程序输出的结果 *p 和 *q 是相等的,因为它们都指向 a[0],或者说它们都存放 a[0] 的地址。

数组 a 是 int 型的,所以数组 a 中每一个元素都占 4 字节的内存单元。而每字节都有一个地址,所以每个元素都有 4 个地址。那么
p=&a[0] 是把这 4 个地址中的第一个地址放到了 p 中。

注意,数组名不代表整个数组,q=a 表示的是“把数组 a 的第一个元素的起始地址赋给指针变量 q”,而不是“把数组 a 的各个元素的地址赋给指针变量
q”。

数组下标与指针加减

上面提到,数组指针可以进行加减运算:数组指针与整数的加减,实际是将指针沿着数组进行移动,得到的结果还是一个指针。既然结果是指针,那么就可以解引用,访问数组中的元素。因此有

int nums[] = {0,1,2,3,4};
size_t len = sizeof(nums) / sizeof(nums[0]);
int *p = nums;
size_t i = 0;
for (i = 0; i != len; ++i) {
    if (nums[i] == *(p + i)) {
        printf("true!\n");
    }
}

不出意外的话,这一小段代码会连续打印五行 true!。这提供了另一种访问数组内元素的方法;而事实上,在使用下标访问数组元素的时候,编译器都会转换成类似 *(nums + i) 的形式。也就是说,通过指针运算和解引用来访问数组元素,其实是更加本质的方式。

指针的移动

那么如何使指针变量指向一维数组中的其他元素呢?比如,如何使指针变量指向 a[3] 呢?

同样可以写成 p=&a[3]。但是除了这种方法之外,C 语言规定:如果指针变量 p 已经指向一维数组的第一个元素,那么 p+1
就表示指向该数组的第二个元素。

注意,p+1 不是指向下一个地址,而是指向下一个元素。“下一个地址”和“下一个元素”是不同的。比如 int 型数组中每个元素都占 4
字节,每字节都有一个地址,所以 int 型数组中每个元素都有 4 个地址。如果指针变量 p 指向该数组的首地址,那么“下一个地址”表示的是第一个元素的第二个地址,即
p 往后移一个地址。而“下一个元素”表示 p 往后移 4 个地址,即第二个元素的首地址。下面写一个程序验证一下:

# include <stdio.h>
int main(void)
{
    int a[] = {1, 2, 3, 4, 5};
    int *p = a;
    printf("p = %d, p + 1 = %d\n", p, p+1);    //用十进制格式输出
    printf("p = %#X, p + 1 = %#X\n", p, p+1);  //也可以用十六进制格式输出
    printf("p = %p, p + 1 = %p\n", p, p+1);    //%p是专门输出地址的输出控制符
    return 0;
}

输出结果是:
p = 1638196, p + 1 = 1638200
p = 0X18FF34, p + 1 = 0X18FF38
p = 0018FF34, p + 1 = 0018FF38

我们看到,p+1 表示的是地址往后移 4 个。但并不是所有类型的数组 p+1 都表示往后移 4 个地址。p+1
的本质是移到数组下一个元素的地址,所以关键是看数组是什么类型的。比如数组是 char 型的,每个元素都占一字节,那么此时 p+1
就表示往后移一个地址。所以不同类型的数组,p+1 移动的地址的个数是不同的,但都是移向下一个元素。

知道元素的地址后引用元素就很简单了。如果 p 指向的是第一个元素的地址,那么 *p 表示的就是第一个元素的内容。同样,p+i 表示的是第
i+1 个元素的地址,那么 *(p+i) 就表示第 i+1 个元素的内容。即 p+i 就是指向元素 a[i] 的指针,*(p+i) 就等价于 a[i]

小结一下:

  • 在指针前面使用*运算符可以得到该指针所指向对象的值。

  • 指针加1,指针的值递增它所指向类型的大小(以字节为单位)。

下面的等式体现了C语言的灵活性:

dates + 2 == &dates[2]       // 相同的地址
*(dates + 2) == dates[2]    // 相同的值

不要混淆*(dates+2)*dates+2 。间接运算符(*)的优先级高于+ ,所以*dates+2 相当于(*dates)+2

指针的数组表示

那么现在有一个问题:“数组名 a 表示数组的首地址,a[i] 表示的是数组第 i+1 个元素。那么如果指针变量 p
也指向这个首地址,可以用 p[i] 表示数组的第 i 个元素吗?”可以。

# include <stdio.h>
int main(void)
{
    int a[] = {2, 5, 8, 7, 4};
    int *p = a;
    printf("*(p+3) = %d, *(a+3) = %d\n", *(p+3), *(a+3));
    return 0;
}

输出结果是:
*(p+3) = 7, *(a+3) = 7

实际上系统在编译时,数组元素 a[i] 就是按 *(a+i) 处理的。即首先通过数组名 a 找到数组的首地址,然后首地址再加上i就是元素 a[i]
的地址,然后通过该地址找到该单元中的内容。

运算符优先级

# include <stdio.h>
int main(void)
{
    int a[] = {2, 5, 8, 7, 4};
    int *p = a;
    printf("*p++ = %d, *++p = %d\n", *p++, *++p);
    return 0;
}

*p++ = 5, *++p = 5

因为指针运算符 *和自增运算符“++”的优先级相同,而它们的结合方向是从右往左,所以 *p++ 就相当于 *(p++)*++p
就相当于 *(++p)。但是为了提高程序的可读性,最好加上括号。

在程序中如果用循环语句有规律地执行 ++p,那么每个数组元素就都可以直接用指针指向了,这样读取每个数组元素时执行效率就大大提高了。

用数组的下标形式引用数组元素时,每次都要重新计算数组名 a 表示的首地址,然后再加上下标才能找到该元素。而有规律地使用 ++p,则每次
p 都是直接指向那个元素的,不用额外的计算,所以访问速度就大大提高了。

函数打印数组的元素

# include <stdio.h>
int main(void)
{
    int a[5] = {1, 2, 3, 4, 5};
    int i;
    for (i=0; i<5; ++i)
    {
        printf("%d\n", a[i]);
    }
    return 0;
}

指针方法实现

# include <stdio.h>
int main(void)
{
    int a[] = {1, 2, 3, 4, 5};
    int *p = NULL;  //先初始化, 好习惯
    for (p=a; p<(a+5); ++p)
    {
        printf("%d\n", *p);
    }
    return 0;
}

两个参数确定一个数组

在函数调用时如果要将一个数组从主调函数传到被调函数,只需要传递两个参数就能知道整个数组的信息。即一维数组的首地址(数组名)和数组元素的个数(数组长度)。

下面来写一个程序,把“输出一维数组所有元素”的功能写成函数,然后在主函数中进行调用

# include <stdio.h>
void Output(int *p, int cnt);  //声明一个输出数组的函数
int main(void)
{
    int a[] = {1, 2, 3, 4, 5};
    int b[] = {-5, -9, -8, -7, -4};
    Output(a, 5);
    Output(b, 5);
    printf("\n");
    return 0;
}
/*定义一个输出数组的函数*/
void Output(int *p, int cnt)  /*p用来接收首地址, cnt用来接收数组元素的个数*/
{
    int *a = p;
    for (; p<(a+cnt); ++p)  //数组地址作为循环变量
    {
        printf("%d  ", *p);
    }
}

指针与任意类型

定义一个 Entry struct

typedef struct {
    int key;
    int value;
} Entry;

Entry* arr 在是一个指针,它指向 Entry 结构体类型的数据。当你将它作为数组来使用时,arr 就像是指向 Entry 类型数组的指针。

相当于将这个内存地址解释为一个数组,这个数组的每个元素都是 Entry 类型的数据。

C 语言中的数组在内存中是连续存储的。所以当你有一个指向数组的指针,你可以通过增加指针的值来访问数组中的其他元素。

例如:

Entry arr[10];
Entry* p = arr;

在这个例子中,p 是一个指向 Entry 类型的指针,arr 是一个 Entry
类型的数组。因为数组名在大多数情况下是数组的首地址,所以 p = arr 就相当于 p 指向了 arr 数组的首元素,你可以通过 p
来操作数组。

例如,*(p+1) 就相当于 arr[1],都是指数组的第二个元素。同时,你也可以用 p[1] 来访问数组的第二个元素,因为数组访问符 []
在指针上也是可用的。

int cmp(const void* a, const void* b) {
    const Entry* entryA = (const Entry*)a;
    const Entry* entryB = (const Entry*)b;
    return entryA->key - entryB->key;
}

void qsort(void* base, size_t num, size_t size, int (*compar)(const void*, const void*)) {
    Entry* arr = (Entry*)base;
    for (int i = 0; i < num; i++) {
        for (int j = 0; j < num - i - 1; j++) {
            if (compar(&arr[j], &arr[j + 1]) > 0) {
                Entry temp = arr[j];
                arr[j] = arr[j + 1];
                arr[j + 1] = temp;
            }
        }
    }
}

Entry* arr = (Entry*)base; 这句代码的含义就是创建一个 Entry 类型的指针 arr,并让它指向 base
指向的位置。然后通过这个 arr 指针,你就可以像操作数组一样操作内存中的这段数据。

指针与数组与动态分配内存

在上面的内容中,我们已经知道了如何使用指针来操作数组。但是在上面的例子中,数组的大小是固定的,如果我们需要一个动态大小的数组,怎么办呢?

在 C 语言中,我们可以通过 malloc 函数来动态地分配内存。malloc 函数会在堆内存中申请一段连续的内存,并返回这段内存的首地址。这就为我们创建了一个动态的数组。

Entry *prime_dict = (Entry *) malloc(sizeof(Entry) * len_n_list);

这句代码的含义是,我们动态地在内存中创建了一个大小为 len_n_listEntry
类型的数组,并将这个数组的首地址赋给了 prime_dict 指针。

(Entry *) malloc(sizeof(Entry) * len_n_list);
这部分代码的作用是申请一块内存,大小为 sizeof(Entry) * len_n_listsizeof(Entry) 返回 Entry 类型的大小,len_n_list
则是我们需要的数组元素个数,所以 sizeof(Entry) * len_n_list 就是我们需要的内存大小。

然后,我们通过 (Entry *) 将申请到的内存的地址转换为 Entry 类型的指针,这样我们就可以像操作 Entry 类型的数组一样来操作这块内存。

(Entry *) 是一个类型转换操作,也称为强制类型转换。在 C 语言中,malloc 函数的返回类型是 void *
,也就是无类型指针。这意味着,malloc 返回的指针仅仅代表了一块内存的地址,而没有明确这块内存应该如何解释,即这块内存是什么类型的数据。
因此,当我们用 malloc
分配内存以后,通常需要对其返回的指针进行类型转换,使其成为一个具有特定类型的指针。在这里,(Entry *) 就是将 void *
类型的指针转换为 Entry * 类型的指针。
转换后的 Entry *
类型指针,不仅包含了内存地址的信息,还包含了如何解读这块内存的信息。也就是说,我们知道了这块内存是由 Entry
类型的数据组成的。这使得我们能够像操作 Entry 类型的数组一样,通过指针来操作这块动态分配的内存。

例如,prime_dict[0] 就代表了这块内存的第一个元素,prime_dict[1] 就代表了这块内存的第二个元素,依此类推。

指针和多维数组

int zippo[4][2]; /* 内含int数组的数组 */
指针 数组
保存数据的地址,任何存入变量的数据都会被当做地址来处理。变量本身的地址由编译器另外存储,我们并不知道在哪里。 保存数据。数组名代表的是数组第一个元素的地址,而不是数组的地址;&数组名才是整个数组的地址。a 本身的地址由编译器另外存储,我们并不知道在哪里。
间接访问数据:访问是完全匿名的。首先取得变量的内容,把它作为地址,然后根据这个地址提取或写入数据。 直接访问数据:数组名就是整个数组的名字,数组内每个元素没有名字。只能通过具名+匿名的方式来访问某个元素,不能把整个数组当做一个整体进行读写操作。
通常用于动态数据结构,相关的函数有 malloc 和 free。 通常用于存储固定数目且数据类型相同的元素,隐式分配和销毁。
通常指向匿名数据(也可以指向具名数据) 自身为具名数据(数组名)
初始化时编译器并不为指针所指向的对象分配空间,只分配指针本身,除非在定义时同时赋值给指针一个字符串常量。 数组在初始化时为对象分配了空间。