C语言-深入理解指针(2)

完结操作符之后,我们紧接着继续理解有关指针的相关知识。本章的内容能让我们更加深入理解数组与指针之间的关系,我们马上进入正题~

目录

1.对数组名的理解

1.1数组名代表数组首元素的地址

1.2数组名代表整个数组

2.指针访问数组

2.1各种变量类型的判断

2.2指针访问一维数组的本质

2.2.1一维数组与指针的关系

2.2.2实现利用指针访问一维数组

2.3指针访问二维数组的本质

 2.3.1二维数组在内存中的存储

2.3.2二维数组和指针的关系

2.3.3实现利用指针访问二维数组

3.一维数组传参的本质

4.冒泡排序

4.1冒泡排序过程

4.2冒泡排序代码实现

5.二级指针

6.指针数组

6.1指针数组的概念

6.2指针数组及其元素类型

6.3利用指针数组模拟二维数组


1.对数组名的理解

我们学习了数组之后,在各个地方都会利用到它,那在不同的场景下它的数组名其实代表的意义是不同的,大家看下面这几个例子:

int arr[] = { 0,1,2,3,4,5,6,7,8,9 };

int ret = sizeof(arr);

int sur = sizeof(arr + 0);

int mov = sizeof(&arr);

如果你还很难分辨上述的四个代码的具体含义以及结果,相信看完下面内容你会获得新的知识。 

1.1数组名代表数组首元素的地址

其实如果大家有学习过C语言,在学到数组这一章节肯定都会听过数组名代表首元素的地址,这确实也没错,但并不是所有的都是。我们先来看看下面代码:

int arr[] = { 0,1,2,3,4,5,6,7,8,9 };

int* p = arr;

int* s = &arr[0];

在上面的代码中,定义整型指针指向数组arr,不管是p还是s,arr都代表数组首元素的地址,这也是大多数的情况。我们可以验证:

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

	printf("%p\n", p);
	printf("%p\n", s);

	return 0;
}

 其代码运行结果为:

说明两者效果确实是一样的,表明数组确实是地址,而且还是数组第一个元素的地址。

1.2数组名代表整个数组

然而,还有一些情况下数组名其实代表的是整个数组,但这些情况我们可以总结如下:

Rule 1:数组名单独在sizeof()内部,此时数组名代表整个数组,功能为计算整个数组的大小。

Rule 2:数组名在取地址操作符&后,此时数组名代表整个数组,取出的是整个数组的地址,和首元素地址的起始位置相同,但+-整数时的增量不同。

如果已经看明白了,尝试理解以下代码:

#include<stdio.h>
int main()
{
	int arr[10] = { 0 };
	printf("arr     = %p\n", arr);//数组首元素的地址
	printf("&arr[0] = %p\n", &arr[0]);//数组首元素的地址
	printf("&arr    = %p\n", &arr);//整个数组的地址

	printf("&arr[0] + 1 = %p\n", &arr[0] + 1);//类型int*,+1跳过一个整型
	printf("&arr + 1    = %p\n", &arr + 1);//整个数组的地址,+1跳过整个数组

	printf("%zd\n", sizeof(arr));//40

	return 0;
}

所以我们可以总结,数组名代表首元素的地址,但是有两个例外。

2.指针访问数组

2.1各种变量类型的判断

有些人看到这里可能会说,这不是很简单吗?我三下五除二就能判断出来:

int arr[10] = { 1,2,3,4,5,6,7,8,9,10 };//数组arr为int[10]类型,数组中的元素为int类型

char a = 'A';//变量a为char类型

unsigned long length = 100;//变量length为unsigned long类型

看似好像是这样的,那如果换换看呢?

int(*pf)(int, int) = Add;//pf为函数指针,为int(*)(int ,int)类型

int(*p)[10] = &arr;//p为数组指针,为int(*)[10]类型

int(*pfarr[4])(int, int) = { add,sub,mul,div };//pfarr为函数指针数组,为int

//(*[4])(int, int)类型,数组元素为int(*)(int, int)类型

 是不是瞬间变为了你看不懂的样子?呐,现在还好高骛远不?还不乖乖听我娓娓道来。

其实方法也没什么神秘的,也很简单,就是将定义时等号左边的变量去掉就行。不信你试试,不管是简单的int,char,unsigned long,还是更难的函数指针、数组指针、函数指针数组,都是同样的处理方法。大家可要记住了,很重要的。

2.2指针访问一维数组的本质

2.2.1一维数组与指针的关系

我们在深入理解指针(1)中已经理解了指针的基础知识,那么对于用指针和一维数组的关系,我们以下面这个数组来举例:

int arr[10] = { 1,2,3,4,5,6,7,8,9,10 };

我们来看下面这张图:

这张图就很清晰地展示出了一维数组和指针的关系,可以发现,我们对指针进行一次解引用就可以得到其元素。

上述蓝色字体写错了,大家可以看完1.1和1.2后自行修正。

2.2.2实现利用指针访问一维数组

我们在深入理解指针(1)中已经理解了指针的基础知识,那么对于用指针访问数组,其实非常简单了。这里补充一个我对下标访问符[ ]的理解,其实我认为下标访问就自带指针的解引用功能,例如下面代码其实都是一个意思:

 arr[i]  *(arr+i)   i[arr]   *(i+arr)

一个下标访问符,arr[i],实际就是 *(arr+i),那arr[i]和i[arr]其实不都是这个意思吗,所以理论上都是可行的,所以,我们可以得到:*(arr+x)=arr[x],若p=arr,则也有*(p+x)=p[x]对于一维数组,我们就可以对应地使用解引用一次就能得到数组元素的指针来访问。

int main()
{
    int arr[10] = { 1,2,3,4,5,6,7,8,9,10 };
    int* p = arr;
    int i = 0;
    for (i = 0; i <= 10; i++)
    {
        //printf("%d ", *p++);
        printf("%d ",*(p + i));
    }
 
    return 0;
}

arr是数组首元素的地址,将arr赋值给p,p就指向了arr,*p就是arr[0],*(p+1)就是arr[1],*(p+2)就是arr[2],以此类推,*(p+i)就是arr[i],就能够将数组用指针方式访问了。

2.3指针访问二维数组的本质

 2.3.1二维数组在内存中的存储

我们首先要理解,二维数组到底是怎么定义的?二维数组是将一维数组作为元素所得到的数组。这点很关键,它在内存中的存储如下图所示:

虽然图片中将二维数组写成了三行,每行5个元素,但其实大家要知道,二维数组也是数组,在内存中依然是连续存放并且地址依次递增。

2.3.2二维数组和指针的关系

2.2中我们讨论了下标访问符[ ]自带解引用作用,因而进行一维数组访问的指针解引用一次就能得到具体的数组元素。那么同理,二维数组用到了两个下标访问符,说明指向二维数组的指针解引用两次才能得到具体某行某列的元素。我们以下面这个数组来举例:

int arr[3][5] = { 1,2,3,4,5, 2,3,4,5,6, 3,4,5,6,7 };

我们来看下面这张图:

由此图我们就能理解, 二维数组是将一维数组作为元素所得到的数组,他的三个元素分别为arr[0],arr[1]和arr[2],指针形式分别是*(arr+0),*(arr+1)和*(arr+2)。我们接着继续看:

我们能看到,arr是数组名,代表数组首元素的地址,即第一行一维数组arr[0] 的地址,也可以写成&arr[0]。那么,arr+1应该跳过一个元素的大小,即arr+1是第二行的地址,arr+2是第三行的地址。这是因为一维数组作为二维数组的元素,类型为int[5](不会判断的返回去看2.1),所以+1跳过5个整型。但是大家注意蓝字,我们对数组名进行一次解引用,得到的应该是具体的元素,即一维数组。所以*arr或arr[0]也是和arr指向同一个位置,但是他们的增量是不同的,此时一维数组的元素是int类型,+1只跳过一个字节,所以*arr+1或arr[0]+1表示的是第一行第二列元素的地址,而并非第二行的地址了。这里稍有些复杂,大家反复观看,看懂了再继续;

我们可以看到,如果解引用次数+下标引用次数=1时,只会访问到一维数组,当解引用次数+下标引用次数=2时,将具体访问到一维数组的元素,其解引用和下标引用在形式上可以相互转化(如上图),其中&和*两个操作符功能可以相互抵消,如*&a就是a。

2.3.3实现利用指针访问二维数组

那有了上述知识作为基础,我们就可以很轻松地利用指针实现二维数组了:

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

	return 0;
}

这时候可能有人问:我们再写一维数组的时候又定义了指针变量p指向一维数组,那此时我们能不能也定义指针变量p指向二维数组呢?答案当然是可以的,这个我们放在下一篇目说明,本章掌握到上面的代码即可。

3.一维数组传参的本质

我们再学习函数的时候说过,函数的参数(形参)只是实参的一份临时拷贝,对形参的修改不会影响实参。但是当形参是数组时,却可以对数组直接进行修改,如下面这段代码,这是为什么呢?

#include<stdio.h>
void test1(int arr[])//参数写成数组形式,本质上还是指针
{
		printf("%d\n", sizeof(arr));
}
void test2(int* arr)//参数写成指针形式
{
	printf("%d\n", sizeof(arr));//计算⼀个指针变量的大小
}
int main()
{
	int arr[10] = { 1,2,3,4,5,6,7,8,9,10 };
	test1(arr);
	test2(arr);

	return 0;
}

上述有两个test函数,其功能用来计算数组的大小,当然两个test的结果肯定是相同的。那看到test2,我们就应该有所启发了。我们说函数的参数(形参)只是实参的一份临时拷贝,对形参的修改不会影响实参,所以我们要进行传址调用,也就是将变量的地址传过去,然后解引用再使用。那对于数组来说,数组名本身就是首元素的地址,直接传当然没问题,能写成指针形式也自然理所当然。所以我们总结:

conclusion 1:数组传参的本质实际上是传递首元素的地址

conclusion 2:⼀维数组传参,形参的部分可以写成数组的形式,也可以写成指针的形式

4.冒泡排序

在C语言中有许多有趣的排序,如冒泡排序、选择排序、插入排序、希尔排序等等,其中冒泡排序是最简单的。冒泡排序的核心思想就是:两两相邻的元素进行比较,将当前数组中最大的元素放到最后面,然后循环。冒泡排序是基于交换的排序

4.1冒泡排序过程

我再怎么用文字说明,都不如直接观看动态过程直观,所以大家直接看下面视频来直观感受冒泡排序是怎样进行的:

4.2冒泡排序代码实现

相信有动态图片的理解,大家就能轻松理解冒泡排序了,下面是它的代码实现:

#include<stdio.h>

void bubSort(int arr[], int length)
{
	while (length-- )//每次将最大的数放在末尾,就不用重复管他
	{
		for (int i = 0; i < length; i++)
		{
			if (arr[i + 1] < arr[i])//比较相邻两元素大小
			{
				int temp = arr[i + 1];//交换
				arr[i + 1] = arr[i];
				arr[i] = temp;
			}
		}
	}
}
int main()
{
	int arr[10] = { 1,2,3,4,5,6,7,8,9,10 };
	int sz = sizeof(arr) / sizeof(arr[0]);
	bubSort(arr, sz);
	for (int i = 0; i < sz; i++)
	{
		printf("%d ", arr[i]);
	}

	return 0;
}

但是这段代码有一个可以优化的地方,就是如果数组中元素是2,1,3,3,4,6,7,8,9,那只需要将2,1交换就行了,但是上面代码比完1,2数组全部有序后,又会从第一个元素再比到第七个元素,然后再从第一个元素比到第六个元素,一直等到length为0。这是很低效率的一件事情,所以我们可以加一个指标flag,只有再一趟交换中有元素被交换,flag才等于1,否则没有元素交换,即数组已经有序,就不需要交换了,flag=0,而只有flag=1才进行循环,这样就规避了这个低效率的问题:

void bubSort(int arr[], int length)
{
	int flag = 1;
	while (length-- && flag)//flag为1才进入循环开始一趟排序
	{
		flag = 0;//进入后flag默认为0
		for (int i = 0; i < length; i++)
		{
			if (arr[i + 1] < arr[i])
			{
				int temp = arr[i + 1]
				arr[i + 1] = arr[i];
				arr[i] = temp;
				flag = 1;//只有有元素交换flag才为1
			}
		}
	}
}

5.二级指针

我们之前学习的指针,指的是指针变量,即地址,指向了改地址存储的变量。那指针变量也是变量,是变量就会有地址,我们将指向指针变量的地址称之为二级指针变量,简称二级指针。

我们来看看下面的代码:

//二级指针
int main()
{
	int a = 10;
	int* p = &a;//p是一级指针,p指向的内容a是int*类型
	int** pp = &p;//pp是二级指针,pp指向的内容p是int*类型

	return 0;
}

我们来直接上图:

首先a是整型变量,0x0012ff40是a的指向a的地址,将其存放在一级指针变量pa中,0x00112ff38是ppa的地址,将其存放在二级指针变量ppa中。对于ppa,解引用一次得到pa,再次解引用才得到a。

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

	return 0;
}

6.指针数组

6.1指针数组的概念

先问大家一个问题:指针数组是指针还是数组?

答案自然是数组。那这是个什么样的数组呢?我们可以类比,整型数组是存放整型的数组,字符数组是存放字符的数组,那指针数组就是存放指针的数组

6.2指针数组及其元素类型

我们先从含有五个整型的整型数组int arr[5]和含有五个字符的字符数组char arr[5]开始:

我们能看到,整型数组的元素都是int类型,字符数组的元素都是char类型,这很简单。再根据我们2.1所说,将变量名去掉,得到整型数组arr的类型为int [5],char数组的类型为char [5]。 

我们再来看指针数组:

对于一个指针数组int*  arr[5],数组中的每个元素都是int*类型的指针,数组arr为int* [5],每一个元素都是地址,都可以指向另外的区域。

6.3利用指针数组模拟二维数组

二级指针可以用来模拟二维数组,我们来看看下面的代码:

//指针数组
int main()
{
	int arr1[] = { 1,2,3,4,5 };
	int arr2[] = { 2,3,4,5,6 };
	int arr3[] = { 3,4,5,6,7 };
	int* parr[] = { arr1,arr2,arr3 };
	for (int i = 0; i < 3; i++)
	{
		for (int j = 0; j < 5; j++)
		{
			printf("%d ", parr[i][j]);//arr1[j]  *(arr1+j)  *(*(parr+i)+j)  
		}
		printf("\n");
	}

	return 0;
}
//本质都是指针运算

我们能看到,我们定义了指针数组parr,其中有arr1,arr2,arr3三个指针,分别指向了三个数组。可能parr[ ][ ]的写法会让人困惑,这都没有二维数组,怎么会有两个下标引用?会有这样困惑的人是思维定式地认为一维数组就应该arr[ ],二维数组就应该arr[ ][ ],建议重复观看上面指针访问数组的本质。arr[i]表示这个数组是arr1还是arr2还是arr3,因为[ ]相当于解引用了一次,所以我们就会直接得到指针数组中的具体哪一个数组,此时我们再+-整数进行解引用,得到的自然就是一维数组中的具体值了,如arr[1][2],相当于*(*(parr+1)+2)。

不过再怎么说只是将二维数组模拟出来而已,并不是真正的二维数组,在本质上有着很大的区别,关键在于模拟出来的数组在内存中不是连续存放的:

好了,上述就是本篇的所有内容了。如果感觉有收获就点个赞吧,有错误欢迎指出~ 

  • 25
    点赞
  • 25
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值