指针专题(2)

前言

上一节我们学习了指针的相关内容,本节我们继续学习指针专题,更加深入的了解指针,那么废话不多说,我们正式进入今天的学习

1.对数组名的深入理解

在上一节的内容中,我们提到了用指针来访问数组的操作,我们通过使用 &arr[0] 来拿到数组首元素的地址。因为数组的内容在内存空间里面是连续排放的,所以我们只要知道了首元素的地址和数组中元素的个数就可以访问到每个元素的地址。

在前面的学习中我们知道:数组名是首元素的地址,我们来验证一下:

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

通过以上代码,因为两者打印出来的结果是一样的,所以我们就可以确认数组名是首元素地址

(代码运行的环境是X32)

现在我们再来联想一下之前学过的sizeof函数,如果说数组名是首元素的地址的话,那么我们打印sizeof(arr)的结果只有可能是8或者4,其取值只和64位或者32位有关;

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

我们来运行一下这段代码,但却得出了我们意料之外的取值:

所以我们可以知道:数组名是首元素的地址这个说法存在一定的疏漏

数组名是数组首个元素的地址基本上是正确的,但是存在两个特殊情况

1.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;
}

我们发现这三种情况打印出来的结果是一模一样的,都是从第一个元素地址开始的,那么&+数组名和数组名又有什么区别呢?

我们写一串代码来表示它们之间的区别吧:

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;
}

这⾥我们发现

&arr[0] 和 &arr[0]+1相差4个字节;

arr 和 arr+1相差4个字节;

是因为&arr[0]和arr都是首元素的地址,说明+1是跳过⼀个元素

 但是 &arr 和 &arr+1 相差40个字节,这就是因为&arr是数组的地址,说明+1操作是跳过整个数组

这样我们就能很清楚的知道&+数组名和数组名的区别了

因为&arr+1跳过的不是4个字节而是40个字节,所以说它的类型就不是int*,其具体类型请看后文

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 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;
}

这两串代码都可以完成通过指针来访问数组的功能。通过比较,我们可以发现:我们将*(p+i)换成p[i] 也是能够正常打印的,所以本质上 p[i] 是等价于 *(p+i)

同理 arr[i] 也等价于*(arr+i),数组元素的访问在编译器处理的时候,也是转换成首元素的地址+偏移量求出元素的地址,然后解引⽤来访问的

3.一维数组传参的本质

我们知道:数组是可以传递给函数的,那么我们讨论⼀下数组传参的本质

我们来看一下下面代码:

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

之前我们都是在函数外部计算元素的个数的,但是我们以如上的形式在函数内部计算元素的个数的时候却出现了问题:我们发现我们往函数内部传入数组的时候,在函数内部没有正确获得数组的元素个数

数组传参的时候,传递的是数组名。所以本质上数组传参本质上传递的是数组首元素的地址,正是因为函数的参数部分是本质是指针,所以在函数内部是没办法求的数组元素个数的;

所以函数形参的部分理论上应该使用指针变量来接收首元素的地址,但是我们写作数组的形式也没有问题,例如以下代码:

写成指针形式更规范,写成数组的形式更容易理解

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;
}

通过上面的代码我们知道了:⼀维数组传参,在函数内形参接收时可以写成数组的形式,也可以写成指针的形式,但是本质上还是指针

总结:

1.一维数组传参的时候,传过去的是数组首元素地址

2.形参的部分可以写成指针的形式,也可以写成数组的形式,但是其本质上还是指针,写成数组的形式只是为了方便理解

4.冒泡排序

如果我们在数组中有一组数据,我们能否通过指针来实现对数组元素的排序呢?答案是可行的,我们此时就需要用到排序

排序的算法有很多,举几个常见的例子:

1.冒泡排序

2.选择排序

3.插入排序

4.快速排序

5.堆排序

6.希尔排序

那么今天我们就来学习一下冒泡排序;

假设数组里面有以下元素:

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

我们需要把数组里面的内容排为升序要该怎么做呢?

首先我们需要知道冒泡排序的原理和思想:

我们首先需要对两两相邻的元素的大小进行比较,如果不满足我们所求的顺序(降序或者升序)就把两个元素进行交换,如果满足顺序就找下一对

9876543210
8976543210
8796543210

到最后:

8765432109

这样就执行了一趟冒泡排序,我们解决了最高位的9

我们再重复将一趟冒泡排序执行八次(一共执行了九次,是最坏的情况):

0123456789

所以我们可以得知:一趟冒泡排序能解决数组中一位元素的顺序,在最坏的情况下(升序改为降序或者降序改为升序)会执行(数组里的元素个数-1)次冒泡排序

注意:我们第一趟冒泡排序需要进行9次比较。而第二趟冒泡排序因为数组里面的最高为元素已经被确定,所以我们此时只需要进行8此比较;进行第三趟冒泡排序的时候最高的两位元素已经被确定,所以我们此时只需要进行7次比较;

所以我们可以知道:我们每进行一趟冒泡排序,都可以确定一位元素的位置,那么下一趟冒泡排序的比较数据的次数就要减少一次

通过以上我们归纳的注意事项,我们就可以实现冒泡排序了:

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 - 1 - i; j++)
		{
			if (arr[j] > arr[j + 1])
			{
				int tmp = arr[j];
				arr[j] = arr[j + 1];
				arr[j + 1] = tmp;
			}
		}
	}
}

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

int main(void)
{
	int arr[] = { 9,8,7,6,5,4,3,2,1,0 };
	//排序为升序
	int sz = sizeof(arr) / sizeof(arr[0]);
	bubble_sort(arr,sz);
	print(arr, sz);
}

优化

虽然刚才的代码能够完成冒泡排序的功能,但是代码存在冗杂的情况,我们还可以对其进行优化

我们举一个极端的例子,如果我们数组里面的数据如下:

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

它已经存在着顺序了,我们按常理来说并不需要进行任何的操作,但是我们按照之前代码的做法还是会一直比较,这样就会拖垮程序的运行时间,影响程序效率

我们知道:如果一趟冒泡排序过后,如果没有一对元素进行了交换,说明此时数组里面已经有顺序了,此时就不需要再往后执行冒泡排序了

因为,我们不知道具体要进行几趟冒泡排序以后数组里面的元素才会有顺序,所以此时我们先假设是有序的,我们定义一个变量并且命名为flag,我们设flag的初始值为1,表示默认数组里面所有元素都是有序的一旦数组里面元素有交换,我们就把flag赋值为0,每次进行完一趟冒泡排序以后我们都对flag的取值进行一次判断,如果flag的取值仍然为1,说明此时数组内的元素就已经有顺序了,我们执行break跳出循环

​
int count = 0;
void bubble_sort(int arr[], int sz)
{
	int i = 0;
	for (i = 0; i < sz - 1; i++)
	{
		//一趟冒泡排序
		int flag = 1;//假设是有序的
		int j = 0;
		for (j = 0; j < sz - 1 - i; j++)
		{
			count++;
			if (arr[j] > arr[j + 1])
			{
				int tmp = arr[j];
				arr[j] = arr[j + 1];
				arr[j + 1] = tmp;
				flag = 0;//不是有序的
			}
		}
		if (flag == 1)
		{
			break;
		}
	}
}

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

int main(void)
{
	int arr[] = { 0,1,2,3,4,5,6,7,8,9 };
	//排序为升序
	int sz = sizeof(arr) / sizeof(arr[0]);
	bubble_sort(arr,sz);
	print(arr, sz);
	printf("冒泡排序的次数为 %d\n", count);
}

​

此时冒泡排序只需要执行9次,相比之前的45得到了优化(这里的次数不是趟数,次数是判断两个相邻元素的大小的次数)

5.二级指针

我们通过之前的学习知道了指针变量也是一个变量,那么指针变量也会有自己的地址,那么我们可能会产生疑问 :是不是指针变量的地址也可以用一个变量来存起来呢?

这种存储指针变量的地址的变量就叫做二级指针

	int a = 0;
	int* p = &a;

如上之前我们所学过的指针变量p其实叫做一级指针变量

因为p指针变量也有自己的地址,所以我们可以用&p来取出p的地址,此时我们需要定义一个二级指针变量pp来存入p的地址

所以我们可以知道二级指针变量是用来存放一级指针变量的地址的

int main(void)
{
	int a = 0;
	int* p = &a;
	int** pp = &p;
	return 0;
}

二级指针的运算

int main(void)
{
	int a = 0;
	int* p = &a;
	int** pp = &p;
	printf("   a = %d\n", a);
	printf("   p = %p\n", p);
	printf("  &a = %p\n", &a);
	printf(" *pp = %p\n",*pp);
	printf("**pp = %d\n", **pp);
	return 0;
}

1.*ppa 就是对ppa中的地址进行解引用,这样找到的是 pa , *ppa 其实访问的就是 pa 

2.**ppa 先通过 *ppa 找到 pa ,然后对pa 进⾏解引用操作等同于*pa 找到的是 a

6.指针数组

我们初次看到指针数组这个概念的时候可能会产生疑问?指针数组里面既有指针又有数组,那么指针数组的本质是什么呢?

我们可以尝试类比一下:

之前我们学习过:

整型数组:例如int arr[10],整型数组是存放整型的数组

字符数组:例如char arr[10],字符数组是存放字符的数组

通过这些例子,我们可以推断:指针数组是存放指针的数组

所以我们就可以知道数组指针的表达形式为:Type* arr[n]

指针数组中的每个元素都是用来存放地址的,如下图所示:

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

我们先来思考一下:我们有没有什么办法能不使用二维数组来实现二维数组的功能呢?

因为数组名表示的是首元素的地址,我们此时就可以使用指针数组来模拟实现二维数组

//使用指针数组模拟实现二维数组
int main(void)
{
	int arr1[] = { 1,2,3,4,5 };
	int arr2[] = { 2,3,4,5,6 };
	int arr3[] = { 3,4,5,6,7 };

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

结尾

本节我们继续学习了数组有关的内容,下一节我们学习的内容还是指针,谢谢大家的浏览!!!

  • 41
    点赞
  • 26
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值