C语言必学之指针——从入门到入坑——part2(chapter13)

前言:

大家好呀,我是Humble,今天我们来到指针的第二篇博客

我们要在第一篇的基础上拓展一些内容(注意:我们后面所有的内容都是基于前面的基础,还没看过一的小伙伴记得从我的专栏进入阅读哦,也欢迎各位订阅我的专栏)

那么,废话不多说,开始指针的第二篇吧

一.数组名的理解

在上一篇博客中,我们在使用指针访问数组的内容时,有这样的代码

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

这里我们使用 &arr[0] 的方式拿到了数组第一个元素的地址,但是其实数组名本来就是地址,而且 是数组元素的地址,我们来做个测试,看一下下面代码的测试结果

int main()
{
 int arr[10] = { 1,2,3,4,5,6,7,8,9,10 };
 printf("&arr[0] = %p\n", &arr[0]);
 printf("arr = %p\n", arr);
 return 0;
}

44c5d1e1d6f94e5597c01687cec41be8.png

我们发现数组名和数组首元素的地址打印出的结果一模一样,数组名就是数组首元素(第一个元素)的地址

这时候有同学会有疑问?数组名如果是数首元素的地址,那下面的代码怎么理解呢?

int main()
{
 int arr[10] = { 1,2,3,4,5,6,7,8,9,10 };
 printf("%d\n", sizeof(arr));
 return 0;
}

这个代码的结果无疑是40,如果arr是数组首元素的地址,那在x64的环境下输出应该的应该是8才对

其实数组名就是数组首元素(第一个元素)的地址是对的,但是有两个例外:

1. sizeof(数组名),sizeof中单独放数组名,这里的数组表示整个数组,计算的是整个数组的大小, 单位是字节

2. &数组名,这里的数组名表示整个数组,取出的是整个数组的地址(整个数组的地址和数组首元素 的地址是有区别的)

除此之外,任何地方使用数组名,数组名都表示首元素的地址

这时有好奇的同学,可能会试一下下面这个代码

int main()
{
 int arr[10] = { 1,2,3,4,5,6,7,8,9,10 };
 printf("&arr[0] = %p\n", &arr[0]);
 printf("arr = %p\n", arr);
 printf("&arr = %p\n", &arr);
 return 0;
}

0ef3f100c01149a89d8ce85d614442a6.png

三个打印结果一模一样,这时候又纳闷了,那arr和&arr有啥区别呢?

其实,我们看一下下面的代码就会理解了

int main()
{
 int arr[10] = { 1,2,3,4,5,6,7,8,9,10 };
 printf("&arr[0] = %p\n", &arr[0]);
 printf("&arr[0]+1 = %p\n", &arr[0]+1);
 printf("arr = %p\n", arr);
 printf("arr+1 = %p\n", arr+1);
 printf("&arr = %p\n", &arr);
 printf("&arr+1 = %p\n", &arr+1);
 return 0;
}

f3acc0c402274a4abaf8c993ad9faaf9.png

这里我们发现&arr[0]和&arr[0]+1相差4个字节,arr和arr+1 相差4个字节,是因为&arr[0] 和 arr 都是首元素的地址,+1就是跳过一个元素。

但是&arr 和 &arr+1相差40个字节,这就是因为&arr是数组的地址,+1 操作是跳过整个数组的。 到这里大家应该搞清楚数组名的意义了吧。

数组名是数组首元素的地址,但是有2个例外.请大家记住这一点哦

二.使用指针访问数组

 好,我们现在对数组名有了一点的理解,再结合前面知识的以及数组的特点,我们就可以很方便的使用指针访问数组了

我们先来简单总结一下:

1.数组在内存中是连续存放的

2.数组名就是首元素的地址(除了2个例外),方便找到起始位置

请看下面这个代码,我们来郑重的介绍一下

int main()
{
 int arr[10] = {0};
 int i = 0;
 int sz = sizeof(arr)/sizeof(arr[0]);
 int* p = arr;

 for(i=0; i<sz; i++)
 {
 scanf("%d", p+i);
 //scanf("%d", arr+i);//也可以这样写
 }
 //输出
 for(i=0; i<sz; i++)
 {
 printf("%d ", *(p+i));
 }
 return 0;
}

这里的int* p =arr,arr就是数组的首元素地址,并将其赋给p

在scanf这行的代码,我们原来给数组赋值会写成scanf("%d",&arr[i]);

现在我们可以写成:

scanf("%d", p + i);
或者这样:scanf("%d", arr+i);

因为arr与p是等价的,都是首元素的地址

同理,我们输出的时候可以写成    printf("%d ", *(p + i));

通过分析我们知道了

其实数组名arr和p在这里是等价的。那我们既然可以使用arr[i]可以访问数组的元素,那p[i]是否也可 以访问数组呢

​

	for (i = 0; i < sz; i++)
	{
		printf("%d ", arr[i]);
	}



	for (i = 0; i < sz; i++)
	{
		printf("%d ", p[i]);
	}


	for (i = 0; i < sz; i++)
	{
		printf("%d ",*(arr+i));
	}



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

​

我们发现,上面的代码都是等价的,其实本质就是指针访问

好,之前我们学过了数组,知道了数组是可以传递给函数的,

所以我们接下来讨论一下数组传参的本质。

三.一维数组传参的本质

我们首先从一个问题开始,我们之前都是在函数外部计算数组的元素个数,那我们可以把函数传给一个函数后,函数内部求数组的元素个数吗?

​
​
void test(int arr[])
{
 int sz = sizeof(arr)/sizeof(arr[0]);
 printf("sz = %d\n", sz);
}
int main()
{
 int arr[10] = {1,2,3,4,5,6,7,8,9,10};
 int sz = sizeof(arr)/sizeof(arr[0]);
 printf("sz = %d\n", sz);
 test(arr);
 return 0;
}

​

​

看一下运行结果,理论上两个sz应该相同

f770445bb2854e04869dddc35e5b42cf.png

我们发现在函数内部是没有正确获得数组的元素个数。

这就要学习数组传参的本质了,上个小节我们学习了:

数组名是数组首元素的地址;那么在数组传参的时候,传递的是数组名,也就是说本质上数组传参本质上传递的是数组首元素的地址。即test()的括号里放的不是arr数组而是arr数组的首个元素的地址,()里并没有创建新的形参数组,放的应该是 int* arr

所以在计算test中的sz时, sizeof(arr)算的是1个指针变量的大小,因为在x64的环境下1个指针变量的大小是8,而后面的 sizeof(arr[0])是一个整数的大小是4个字节,所以最后得到的结果才为2

所以函数形参的部分理论上应该使用指针变量来接收首元素的地址。那么在函数内部我们写 sizeof(arr) 计算的是一个地址的大小而不是数组的大小。正是因为函数的参数部分是本质是指针,所以在函数内部是没办法求的数组元素个数的

一个小知识:我们把传过去的不是数组而是数组的首元素地址叫做数组的降级

总结:一维数组传参,形参的部分可以写成数组的形式,也可以写成指针的形式

四.冒泡排序

在学冒泡排序之前,我们在来引导一下:

就是我们学习了这些数组,函数之后,能不能将它们结合起来实现一些功能呢?

答案是可以的,接下来我们做一个练习,写一个函数,对一个整形数组的数组进行排序

其实,排序的方法有很多,

我们的先驱已经给我找到了很多的算法

比如:

1.冒泡排序(也就是今天要讲的,比较好理解的一种排序思想)

2.选择排序

3.插入排序

4.希尔排序

5.快速排序

........

那我们就来正式介绍一下冒泡排序

冒泡排序的核心思想就是:两两相邻的元素进行比较

下面是最基础的冒泡排序,通过双重的内外循环,这里数组的10个元素,我们外循环走9次,内循环从9次到8次...遍历次数随比较范围缩小一次次减少

所以在循环完后,我们就得到了有序的数组(这里是升序排列)

void bubble_sort(int arr[], int sz)//参数接收数组元素个数
{
 int i = 0;
 for(i=0; i<sz-1; i++)
 {
 int j = 0;
 for(j=0; j<sz-i-1; j++)
 {
 if(arr[j] > arr[j+1])
 {
 int tmp = arr[j];
 arr[j] = arr[j+1];
 arr[j+1] = tmp;
 }
 }
 }
}
int main()
{
 int arr[] = {3,1,7,5,8,9,0,2,4,6};
 int sz = sizeof(arr)/sizeof(arr[0]);
 bubble_sort(arr, sz);
for(i=0; i<sz; i++)
 {
 printf("%d ", arr[i]);
 }
 return 0;
}

9df0765df08848a3b3319ec81e993130.png

不过这段代码也有它的不足,在于效率,比如排序这个数组的序列9,0,1,2,3,5,4,6,8,7

用这样的方法效率就很低了,我们可以用count来跟踪一下比较次数

void bubble_sort(int arr[], int sz)
{
	int i = 0;
	int count = 0;
	for (i = 0; i < sz - 1; i++)
	{
		int j = 0;
		
		for (j = 0; j < sz - i - 1; j++)
		{
			count++;
			if (arr[j] > arr[j + 1])
			{
				
				int tmp = arr[j];
				arr[j] = arr[j + 1];
				arr[j + 1] = tmp;
			}
		}
	}
	printf("%d\n", count);
}

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

结果如下:

0218d2f6bf3f476880ba45d20bd2e5c1.png

所以我们必须优化,我们可以使用一个变量flag来中止比较已经有序的数组

void bubble_sort(int arr[], int sz)
{
	int i = 0;
	int count = 0;
	for (i = 0; i < sz - 1; i++)
	{
		int flag = 1;
		
		int j = 0;
		for (j = 0; j < sz - i - 1; j++)
		{
			count++;
			if (arr[j] > arr[j + 1])
			{
				flag = 0;
				int tmp = arr[j];
				arr[j] = arr[j + 1];
				arr[j + 1] = tmp;
			}
		}
		if (flag == 1)
			break;
	}
	printf("%d\n",count);
}
int main()
{
	int arr[] = { 9,0,1,2,3,5,4,6,8,7 };
	int sz = sizeof(arr) / sizeof(arr[0]);
	bubble_sort(arr, sz);
	int i = 0;
	for (i = 0; i < sz; i++)
	{
		printf("%d ", arr[i]);
	}
	return 0;
}

我们来看一下结果

32d493aefa824a9d812d0b0f34dc23b0.png

果然少了不少

五.二级指针

什么是二级指针呢?

我们之前学的其实都是一级指针,比如:

int * pi;

char * pc;

double* pd

.....

我们说指针变量也是变量,是变量就有地址,那指针变量的地址存放在哪里?

这就是二级指针

来看下面的代码

​
int main()
{
	int a = 0;
	int* p = &a;  //p是指针变量,是一级指针!
	int** pp = &p;//pp是二级指针!
	return 0;
}

​

这里该怎么理解呢?

我们知道在一级指针中,*是用来说明p是指针的,而int表示p指向的对象a类型是int

好,知道了这点,我们把它类比到二级指针中,同理,后面的这个*是用来说明pp是指针,而int*表示pp指向的对象p的类型是int*

这样大家就能理解二级指针了吧

看到这,相信大家可能会产生这样的想法:

既然有二级,那是不是就可以有三级,四级,或者更高级的指针呢?

其实有的,比如三级指针我们就可以写成int***ppp=&pp;ppp就是三级指针

不过,虽然语法支持,但事实上,我们三级指针就已经用的非常少了

我们一般用到二级指针就差不多了,没必要再往下套了

好,我们继续分析上面的代码,看一下二级指针到底该怎么用:

我们知道p里面放的是a的地址,如果我们对它解引用,就能得到a,

pp里面放的是p的地址,如果我们对它解引用,就能得到p

所以我们用printf函数 打印*pp就能得到&a,**pp就能得到a

看到这,其实我们发现,二级指针并没有想象的那么神秘,二级指针变量是用来存放一级指针变量的地址

六.指针数组

对于这个定义,希望大家先记住下面两句话,这样大家就应该能对指针数组有一个很好的理解了

指针数组其实是存放指针的数组

指针数组的每个元素是指针类型

那么现在我们希望得到一个数组,数组有5个元素,每个元素是整型指针,那么我们该怎么写呢?

思考一下,应该能写出下面的形式:

int* arr[5];   这个数组的每个元素都是整型指针,所以它是指针数组

我们来看一下下面这张图来辅助理解一下这个概念吧

4ba1cec0fbaf43a9a1fbc62be6573f5f.png

七.指针数组模拟二维数组

学了指针数组我们可以干什么呢?

接下来我们就通过指针数组来模拟一下二维数组吧 

请看下面的代码:

​
​
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[3] = {arr1, arr2, arr3};

 int i = 0;
 int j = 0;
 for(i=0; i<3; i++)
 {
    for(j=0; j<5; j++)
   {
     printf("%d ", parr[i][j]);
   }
   printf("\n");
 }
return 0;
}

​

​

这里的parr[i][j]其实由指针的理解,每一次内循环是

*(arr1+j)

*(arr2+j)

*(arr3+j)

写在一起就是*(*(parr+i)+j )

下面是parr数组的画图演示,来更直观的理解:

0575cc7ed956472c817373bd68ad28ce.png

parr[i]是访问parr数组的元素,parr[i]找到的数组元素指向了整型一维数组,parr[i][j]就是整型一维数组中的元素

需要注意的是:上述的代码模拟出二维数组的效果,实际上并非完全是二维数组

因为每一行并非是连续的

结语:

好了,今天的分享就到这里了

最后,希望大家点个赞或者关注吧(感谢感谢)

让我们在接下来的时间里一起成长,一起进步吧!(也敬请期待下一篇博客哦)

11d7d69969784ba2a62ccf12606261ed.jpg

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

不吃肉的Humble

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值