【C语言】指针和数组的深入理解(第三期)

2年前 (2022) 程序员胖胖胖虎阿
242 0 0
【C语言】指针和数组的深入理解(第三期)
篮球哥温馨提示:编程的同时不要忘记锻炼哦!

眼里藏星河,笑里带月亮.


目录

1、指针变量在口语中为什么会跟指针混用?

2、指针数组和数组指针 

2.1 什么是指针数组? 

2.2 什么是数组指针?

2.3 指针数组和数组指针的布局

3、多维数组和多维指针

3.1 二维数组

3.2 二级指针

3.3 一道面试题


1、指针变量在口语中为什么会跟指针混用?

这里我们先阐述一个问题,为什么很多人会混用指针变量和指针呢?其实说法不是完全错误,为什么呢?请看如下代码:

int main() 
{
	int a = 10; //当 a 做左值,a 是变量
	int b = a; //当 a 做右值,a本质就是10

	int* p1 = &a; //当 p1 做左值,p1是一个变量
	int* p2 = p1; //当 p1 做右值 p1 等价于 &a,也就是a的地址,即指针
	return 0;
}

看以上代码相信大家就能明白了,当指针做右值的时候,他本质上就是一个地址,地址就是指针,所以在这种情况下,指针变量可以理解成指针,但不建议这么理解,之所以在前两期我没有强调这个问题,是怕初学者会弄混淆了,有了前两期的学习,在来看这个问题就很轻松了。

但是我们心里面还是要把指针和指针变量区分开来,知道什么时候是混用什么时候不是混用的,为了书面表达以及名词阐述,后面的内容在口语上可能会混用,但是你们自己要有一个谱,还是建议要区分清楚这两个概念。


2、指针数组和数组指针 

2.1 什么是指针数组? 

很多小伙伴在接触指针数组和数组指针的时候是非常头疼的,总会混淆两个的概念, 那么本期会以一个很清晰讲解带大家深入理解:

我们再来回顾一遍数组的定义:具有相同类型元素的集合,我们称为数组

指针是类型吗?不是,指针只是地址,那 int* 是类型吗?double* 是类型吗?是的!分别是整型指针类型,双精度浮点型指针类型,假设我有十个整型指针,也就对应其中十个变量的地址,我们可以把它们放到一个数组里吗?可以的!这就是指针数组:存放指针的数组,每个指针的类型都是一样。

它的语法格式是这样的:int* p[10]  意思是这个数组有十个元素,每个元素类型是 int*,那么指针数组是数组还是指针?数组!存放指针的数组!

2.2 什么是数组指针?

上期我们也学习过 &arr,说它要放在一个数组指针里,我们今天就来探讨下什么是数组指针:

前面我们也了解过,int* p 它是一个可以指向整型数据的指针变量,float* p 它是一个可以指向单精度浮点数数据的指针变量。那么可以理解 *p 表示他是个指针变量,int 就是它对应指向的数据类型,那我如果要指向一个数组呢?

数组的类型是什么?int arr[10] 其实在C语言中这样定义数组在阅读上是有点变扭的,像Java中int[10] arr, 是定义数组,上期也讲过,数组拿 int arr[10] 来说,他的类型就是 int[10],一个数组十个元素,每个元素是 int 类型。所以我们要拿指针去指向int[10] 这种类型该如何写呢?

int (*p)[10] 这样就是我们的数组指针,因为 [ ] 的优先级比 * 高,所以我们先 p 跟 * 结合,表示它是一个指针变量,指向的类型是 int[10],刚好跟我们 int* p 的说法吻合。

数组指针是指针!是一个存放数组地址的指针变量。(有时候为了说法方便,会混用指针和指针变量,大家要自己注意区分)

2.3 指针数组和数组指针的布局

为了方便大家的理解,请看以下图片,但是这并不是真实的内存布局。就好似指针变量并没有一根线去指向一个地址,他只是一个变量存放着地址。

【C语言】指针和数组的深入理解(第三期)

所以看到这里,你应该更能理解上一期取地址数组名与数组名的区别了吧!

2.4 数组指针的使用

简单的使用:

void printArr1(int arr[3][5], int row, int col)
{
    for (int i = 0; i < row; i++)
    {
        for (int j = 0; j < col; j++)
        {
            printf("%d ", arr[i][j]);
        }
            printf("\n");
    }
}
void printAarr2(int(*arr)[5], int row, int col)
{
    for (int i = 0; i < row; i++)
    {
        for (int j = 0; j < col; j++)
        {
            printf("%d ", arr[i][j]);
        }
        printf("\n");
    }
}
int main()
{
    int arr[3][5] = { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 };
    printArr1(arr, 3, 5);
    printAarr2(arr, 3, 5);
    return 0;
}

在我们打印数组中,可以用数组指针来接收,数组名是 arr,表示首元素地址,但是二维数组的首元素的第一行,所以我们这里传递的首元素地址本质是第一行一维数组的地址,所以我们可以用数组指针来接受,因为数组指针是一个指向数组的指针。 


3、多维数组和多维指针

3.1 二维数组

二维数组,其实也是个麻烦的地方,很多书中都把二维数组画成一个几行几列的图形,这样图确实可以帮助我们理解,但是我们也要清楚一点,这些是示意图,并非真正的内存布局。下面我们来通过代码来看看二维数组在内存中如何布局的:

【C语言】指针和数组的深入理解(第三期)

通过打印每个元素的地址,我们也能发现,二维数组在内存中的布局是连续的。如果我们要画比较真实的内存布局的话,应该这么画:

【C语言】指针和数组的深入理解(第三期)

数组的定义是:具有相同类型元素的集合,数组当中可以保存任何类型,这里的二维数组图是不是相当于数组保存了数组,一个数组有三个元素,每个元素保存了三个数组类型 int[3],其实我们完全可以理解成,这些二维数组可以当成一维数组来看,只不过它内部的元素也是数组而已,这里就能说明,既然一维数组是线性递增的,而一维数组每个元素又保存的数组,所以整体二维数组也是线性递增的,所以:二维数组可以被看作内部元素是一维数组的数组。

那既然是连续的,我们第一次打印数组每个元素的内存时,是不是可以换种方法打印?

int main() 
{
	int arr[3][3] = { 0 };
	int* p = (int*)arr;
	for (int i = 0; i < 3 * 3; i++)
	{
		printf("%p\n", p + i);
	}
	return 0;
}

但是我们通常也不会这么去做,下面有一道关于二维数组的题,如果你能看懂,并且能自己分析,那就证明对数组的理解没有问题了,一定要注意数组名是什么的问题!

int main()
{
	int arr[3][3] = { 0 };
	printf("%d\n", sizeof(arr)); //36->计算的是整个数组的大小

	printf("%d\n", sizeof(arr[0][0])); //4->算的是第一个元素的大小

	printf("%d\n", sizeof(arr[0])); //12
	//arr[0]相当于第一行一维数组的数组名,单独放在sizeof内部
	//所以算出的是第一行一位数组的大小

	printf("%d\n", sizeof(arr[0] + 1)); //4
	//arr[0]相当于第一行一维数组的数组名,数组名表示首元素地址,+1则跳过一个数组元素类型大小
	//数组名没有单独放在sizeof内部,也没有取地址,所以求的是第一行第二个元素的大小

	printf("%d\n", sizeof(*(arr[0] + 1))); //4
	//arr[0] + 1相当于第二行数组名,对第二行数组名解引用,没有&,也没有单独放在sizeof内部
	//所以求的是第二行第一个元素的大小->数组名代表首元素地址

	printf("%d\n", sizeof(arr + 1)); //4或8
	//arr表示数组首元素地址,+1表示跳过一个元素,所以求得是第二个元素地址的大小
	//地址的大小在32位平台下为4字节,64位平台为8字节

	printf("%d\n", sizeof(*(arr + 1))); //12
	//数组名代表首元素地址,arr表示数组首元素,也就是第一行的一维数组
	//arr+1就是第二行的一维数组的地址,对一维数组的地址解引用,访问的是整个数组,所以是12

	printf("%d\n", sizeof(&arr[0] + 1)); //4或8
	//arr[0]表示数组首元素,也就是第一行的一维数组,取地址取出的是一维数组的地址
	//对数组+1本质跳过一个数组,也就表示第二行的地址,但它并没有单独出现在sizeof内部
	//表示的是第二行首元素的地址,地址大小是4或8字节

	printf("%d\n", sizeof(*(&arr[0] + 1))); //12
	//&arr[0] + 1取出的是第二行一维数组的地址,对整个一维数组的地址解引用,访问的是整个数组

	printf("%d\n", sizeof(*arr)); //12
	//数组名代表首元素地址,解引用访问第一个元素,二维数组的第一个元素是第一行的整个一维数组
	//所以*arr也就是第一行的数组名即arr[0],单独放在了sizeof内部,计算的是整个一维数组的大小

	printf("%d\n", sizeof(arr[2])); //12
	//arr[2]表示二维数组的第二个元素也就是第二行的数组名,同上,计算的是整个一维数组的大小

	return 0;
}

3.2 二级指针

在学习二级指针之前,我们要了解,指针变量是一个变量,既然是变量就有地址,如果我们要保存一个一级指针变量的地址该如何做呢?用二级指针变量来存放一级指针变量的地址!

简单使用:

【C语言】指针和数组的深入理解(第三期)

二级指针, 当 pp 第一次解引用,访问的是 p 指针变量,如果此时进行赋值,则是修改了 p 里面存放的地址,当 pp 第二次解引用,本质是先访问 p 指针变量,然后对 p 指针变量解引用,最后也就是访问 p 保存地址对应的内容。

至于超过二维的数组,和超过二级的指针,一般用的很少,感兴趣的可以按照上面分析的方法下来自己研究研究。

3.3 一道面试题

int main()
{
	int arr[5][5];
	int(*p)[4];
	p = arr;
	printf("%p,%d\n", &p[4][2] - &arr[4][2], &p[4][2] - &arr[4][2]);
	return 0;
}

我们结合图解,再来分析这道题的解法:

【C语言】指针和数组的深入理解(第三期)

这道题,首先要找出 &arr[4][2] 所对应的地址,这个很简单,可是 &p[4][2] 对应的地址如何找呢?首先我们知道 指针+1 跳过的是对应类型的大小,而 p 的类型是 int[4] 的类型,所以他 +1 可以跳过四个整型,而 p[4][2] 又可以写成 *(*(p+4)),这样一来 p + 4 先是跳过了 16 个整型,因为是二维数组,解引用也就是相当于找到了一维数组,站在 p 的角度 +4 之后解引用可以访问后续的一维数组,在进行 +2 解引用,则跳过了两个整型,就来到了图中的位置。

首先我们来看图,&arr[4][2] 的地址肯定是要高于 &p[4][2] 的,我们也知道,指针相减,得到的是之间的元素个数,所以 &arr[4][2] - &p[4][2] 得到的肯定是 -4,那如果以 %p 打印的话是打印无符号进制,我们将 -4 的补码当成无符号数打印出来就是:0xFFFFFFFC


【C语言】指针和数组的深入理解(第三期)

下期预告:【C语言】指针和数组的深入理解(第四期)

版权声明:程序员胖胖胖虎阿 发表于 2022年11月12日 上午6:24。
转载请注明:【C语言】指针和数组的深入理解(第三期) | 胖虎的工具箱-编程导航

相关文章

暂无评论

暂无评论...