18. C语言 -- 指针数组和数组指针

本博客主要内容为 “小甲鱼” 视频课程《带你学C带你飞》【第一季】 学习笔记,文章的主题内容均来自该课程,在这里仅作学习交流。在文章中可能出现一些错误或者不准确的地方,如发现请积极指出,十分感谢。
也欢迎大家一起讨论交流,如果你觉得这篇文章对你有所帮助,记得评论、点赞哦 ~(。・∀・)ノ゙

1. 指针和数组的区别

  指针是左值,而数组名只是一个地址常量,它不可以被修改,所以数组名不是左值。其中的左值在 《12. C语言 – 拾遗》 中的 1.1 部分有讲,lvalue 指用于识别或定位一个存储位置的标识符,同时还必须是可改变的。

  比如说在下面的这段程序中

#include 

int main()
{
	char str[] = "I love LunZiGongChang!";
	int count = 0;

	while (*str++ != '\0'){
		count++;
	}

	printf("总共有%d个字符!\n", count);

	return 0;
}

执行代码会得到如下的错误
在这里插入图片描述

通过错误提醒可以知道,自加运算符 ++ 需要一个左值,虽然数组名是数组第一个元素的地址,但是他是不可变的,不满足左值的要求,即不是一个左值。但是指针是一个左值,所以我们只需要初始化一个指针为数组第一个元素的地址,就可以解决这个问题,具体代码如下

#include 

int main()
{
	char str[] = "I love LunZiGongChang!";
	char *p= str;
	int count = 0;

	while (*p++ != '\0'){
		count++;
	}

	printf("总共有%d个字符!\n", count);

	return 0;
}

其中还有一点需要注意的就是,自加运算符 ++ 与取值运算符 * 相比,++ 的优先级要更高,所以*p++ 相当于先将指针指向当前位置的下一个位置,然后再取出当前地址的值,实际上就是在逐个的取出字符串中的值。执行上面的代码可以得到如下的结果

总共有22个字符!

这个地方还可以引申出一个问题:c 中while(*p++);与while(*p){p++;}有什么区别?

区别在于退出循环后, p的值不一样。

while( *p++ ); //当*p=0时,退出循环,此时p++仍然执行了
while( *p ) p++; //当*p=0时,退出循环,此时p++不再被执行

例如 char *p="ABCD"; 执行完第一个while循环后,p指向的是’\0’后面的一个字节,p的结果是未知的
而如果是执行第二个循环,则p指向的是’\0’,也就是’D’后面的一字节,即
p=’\0’。如果忘记了可以回顾一下之前的文章《12. C语言 – 拾遗》中第三部分 “自增自减运算符” 中的内容。

  上面这段代码是不是和 《17. C语言 – 指针和数组的关系》 第三部分 指针的运算 很相似。在指针的运算中,我们是使用指针的方式定义了一个数组,因为指针中存放的是数组中第一个元素的地址,而数组中第一个元素的地址又是数组名,所以对于指针定义的数组,既可以使用数组的形式访问,又可以使用指针运算的方式访问。但是对于直接用数组形式定义的数组,由于数组名虽然和数组中第一个元素的地址相等,但是并不是一个左值,所以只可以数组的形式访问数组中元素,不可以使用指针的形式访问,除非向上面那样新定义一个指针。

2. 指针数组

  指针数组,从名字来理解,很容易看出它是一个数组,里面装的是指针。比如下面的这段代码

int *p1[5];

它就是一个指针数组,我们可以从运算符的优先级和结合性进行分析。数组下标的优先级要比取值运算符的优先级高,所以先入为主,p1 被定义为具有 5 个元素的数组。那么数组元素的类型呢?是整型吗?显然不是,因为还有一个星号,所以它们应该是指向整型变量的指针。所示上述代码所定义的数组如下所示
在这里插入图片描述

即指针数组是一个数组,数组中的元素是指针变量。

  比如说下面这段代码

#include 

int main()
{
        char *p1[5] = {
                "轮子工厂厂长招亲!",
                "身高不限",
                "肤色不限",
                "身材不限",
                "只要你不嫌弃厂长丑帅丑帅的~"
        };
        int i;

        for (i = 0; i < 5; i++){
                printf("%s\n", p1[i]);
        }

        return 0;
}

我们将指针数组中的每个元素初始化为一个字符串,这里之所以可以这样写是因为一个指针可以使用 char *p = "sss" 的方式进行初始化,所以如果想初始化一个指针数组,就可以通过上面的方式进行。在打印输出中使用 p1[i] 而不是 *p1[i]*p1[i] 将取出的是字符串中的第一个字符,而不能打印整个字符串。执行上面的代码会得到如下的结果

轮子工厂厂长招亲!
身高不限
肤色不限
身材不限
只要你不嫌弃厂长丑帅丑帅的~

3. 指针的步长

  要理解数组指针,首先要加深对指针的理解。指针的类型决定了指针的视野,指针的视野决定了指针的步长。我们必须清楚一个指针变量将告诉我们两个信息:某个数据结构的起始地址,以及该结构的跨度。比如 int p = &a; 说明该指针指向变量 a 的起始地址,以及它的跨度为 sizeof(int)。所以 p + 1 == p + sizeof(int)。

  因此不同的数组有着不同的指针步长。在这里我们首先从数组名的角度进行考虑,对于一个一维数组 int array[3]={1,2,3}; ,数组名是数组的首地址,是一个 int 型的指针,这点很明显。对于一个二维数组 int array[2][3]={{1,2,3},{4,5,6}}; 来讲,由于内存中的二维数组是以一维数组的形式存放的,所以二维数组是嵌套定义的 ,这个二维数组就是由两个一维数组array[0]和array[1]组成的。其中

  • array[0]:是第一个一维数组的数组名,这个一维数组是{1,2,3};
  • array[1]:是第二个一维数组的数组名,这个一维数组是{4,5,6};

这个时候数组从 array[0] 到 array[1] 虽然只变换了一个位置,但实际上跳过了整个第一行,因此

  • array数组的元素类型为: int (*)[3]类型 //步长为 3 的指针
  • array[0]数组的元素类型为: int *类型
  • array[1]数组的元素类型为: int *类型

  同理对于一个三维数组

int Sarray[2][2][3] = {
{ { 1, 2, 3 }, { 4, 5, 6 } },
{ { 7, 8, 9 }, { 3, 6, 8 } }
};
  • Sarray: 是指向 int (*)[2][3]类型的指针;//步长为 2行3列的指针。
  • Sarray[0]:是指向int *[3]类型的指针;
  • Sarray[0][0]:是指向int *类型的指针;

因此可以看到,对于数组名来讲,他的确是数组第一个元素的地址,但是他的步长是根据他的第一个元素的数量确定的,无论是多少维的数组,把它看成一个一维数组,里面包含着怎样的子数组,就有怎样的步长

4. 数组指针

  数组指针,顾名思义,是一个指向数组的指针,比如说下面这个

int (*p2)[5];

从运算符的优先级和结合性进行分析,因为圆括号和数组下标位于同一个优先级队列,所以我们就要看先来后到的问题了。由于它们的结合性都是从左到右,所以 p2 先被定义为一个指针变量。那么它指向谁?还能有谁?后边还紧跟着一个具有 5 个元素的数组,p2 指向的就是它。由于指针变量的类型事实上就是它所指向的元素的类型,所以这个 int 就是定义数组元素的类型为整型。即如下图所示
在这里插入图片描述

  所以数组指针是一个指针,它指向的是一个数组。那么数组名,数组第一个元素的地址,数组指针这三者之间是什么关系呢?我们看下面的这段程序,它将打印出这3个指针的值,并且通过指针的方式打印出指针所指向的下一个元素的地址。

#include 

int main()
{
	int temp[5] = {1, 2, 3, 4, 5};
	int (*p)[5] = &temp;
	int *pp = temp;

	printf("%p\n", temp);
	printf("%p\n", &temp[0]);
	printf("%p\n", &temp);
	printf("----------------\n");
	printf("%p\n", pp);
	printf("%p\n", pp+1);
	printf("%p\n", p);
	printf("%p\n", p+1);
	printf("%p\n", *p+1);

	return 0;
}

输出的结果如下

0x7ffe2758e710
0x7ffe2758e710
0x7ffe2758e710
----------------
0x7ffe2758e710
0x7ffe2758e714
0x7ffe2758e710
0x7ffe2758e724
0x7ffe2758e714

  可以看到数组名,数组第一个元素的地址和数组指针这三个指针指向的都是同一个地址,那么是否意味着三这是完全等价的呢?在上面的代码中可以看到 pp 指针指向数组的首地址,他的下一个指向的数组中第二个元素的地址;而作为数组指针的 p 指针虽然也指向数组的首地址,但是他的下一个,却指向数组外面的位置(与数组首地址相差了 20,16 进制的 14 是 20),只用通过 *p+1 才会得到数组中第二个原始的地址,这就涉及到指针步长这个概念了。

  就像刚刚所讲的,实际上这里 &temp 对数组取址就是将整个数组看作是一个元素,那么指针 int (*p)[5] = &temp; 的跨度就很明显是 5 啦~这也就解释了为什么指向数组首地址的指针的下一个是数组中的第二个元素的地址,而数组指针的下一个会指向数组的最后(实际上是数组最后一个元素的后一个位置的地址)。

  根据上面的知识可以知道,下面的代码明显是错误的。

#include 

int main()
{

	int (*p2)[5] = {1, 2, 3, 4, 5};
	int i;

	for (i = 0; i < 5; i++)
	{
		printf("%d\n", *(p2 + i));
	}

	return 0;
}

它的本来用意是想用指针法的形式将数组中的每一个元素打印出来,但是却得到如下的结果
18. C语言 -- 指针数组和数组指针_第1张图片

从 warning 的提示信息可以看出是第六行指针的定义及初始化的错误。

  在上面的程序中,数组指针是指向数组的指针,但是 int (*p2)[5] = {1, 2, 3, 4, 5}; 表示指针变量 p2 是指向数组中的第一个元素的地址,并不是指向数组的地址。数组指针和数组的头地址虽然在数值上相等,但是两者的步长却是不等的,这上面 warningd 的原因,也是结果错误的原因。

  因此我们对代码进行了如下的修改,将数组指针初始化为整个数组的地址(即代码中的 5 6 行)。

#include 

int main()
{
	int temp[5] = {1, 2, 3, 4, 5};
	int (*p)[5] = &temp;
	int i;

	for (i = 0; i < 5; i++){
		printf("%d\n", *(p + i));
	}

	return 0;
}

执行之后发现程序依然会报错,如下所示
18. C语言 -- 指针数组和数组指针_第2张图片

很明显又错了,从 warning 的信息来看 printf 使用一个 %d 作为占位符,但是传入的确实一个整型指针。这个很好理解,因为 p 指针还是一个指向数组首地址,步长为5的指针。

  将程序修改为如下的形式便可以正常执行了

#include 

int main()
{
	int temp[5] = {1, 2, 3, 4, 5};
	int (*p)[5] = &temp;
	int i;

	for (i = 0; i < 5; i++){
		printf("%d\n", *(*p + i));
	}

	return 0;
}

  结果就是输出 1 2 3 4 5 ,这里就不截图了。需要强调的是在打印输出的过程中使用了 printf("%d\n", *(*p2 + i)); ,在 p2 的前面增加了一个 * ,有两种方式理解这种改动。一种是 temp 前面多了一个取址运算符,p2 前面就要对应着增加一个取值运算符;另一种理解方式是,temp 是数组名,实际上就是数组中第一个元素的地址,对地址再进行取址,所以 p2 代表的是数组自一个元素地址的地址,这个时候 *p2 代表数组第一个元素的地址,*p2 + i 就是后面第 i 个元素的地址,然后再取值就可以获得数组中的元素了。

  我们可以从下面这个例子加深自己对数组指针的理解

#include 

int main()
{
        int array[10] = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9};
        int *p = (int *)(&array + 1);

        printf("%d\n", *(p - 6));

        return 0;
}

如果将数组指针与数组第一个元素的地址等价起来的话,上面的代码很明显是越界了的,但实际上编译执行之后输出了结果 4。实际上是因为虽然 array 和 &array 的值相同,但步长是不一样的。

  因此,&array + 1 指向的就是整个数组最后的位置(第二个 array 数组的起始位置),然后 (int *) 将其强制转换为一个整型地址(指针),所以指针变量 p 初始化后,指向的地址应该是 array[10](第 11 个元素),所以 *(p - 6) == array[10 - 6] == array[4] == 4。

  重点强调:array 是数组第一个元素的地址,所以 array + 1 指向数组第二个元素;&array 是整个数组的地址,所以 &array + 1 指向整个数组最后的位置,也就是两者指针的步长是不一样的。

  上面的例子是从数组指针的角度进行考虑的,下面的这个例子将从指针数组的角度进行考虑,比如下面的这段代码会输出和上面一样的结果。

#include 

int main()
{
        int array[10] = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9};
        int (*p)[10] = &array;

        printf("%d\n", *(*(p+1)-6));

        return 0;
}

参考

[1] “小甲鱼” 视频课程《带你学C带你飞》【第一季】P23
[2] 凉凉猫 CSDN博客 《二维数组及多维数组的指针总结》
[3] 百度知道 《c++中while(*p++);与while(*p){p++;}有什么区别?》
欢迎大家关注我的知乎号(左侧)和经常投稿的微信公众号(右侧)

18. C语言 -- 指针数组和数组指针_第3张图片

你可能感兴趣的:(C,从零入门C语言!!!)