C语言-指针讲解(2)



通过前面的介绍
C语言指针详解(一)超详细~

相信大家对指针的基本概念及用法有了初步的了解。

我们来回顾一下上次那个博客讲了什么吧~
1.指针就是变量,用于存放地址的,地址唯一标识的一块内存空间。
2.指针的大小分别是4/8个字节(32位平台/64位平台)
3.指针是有类型的,指针的类型决定了指针±整数的步长,以及指针解引用的权限有多大。
4.指针的运算。
那么这次博主给大家继续深入理解指针的其他高级用法吧
这是本次我们要讲解的知识点:


1.野指针

1.1 什么是野指针

野指针,顾名思义,就是指针指向的位置是不可知的。就好比如没有主人的流浪狗一样。

1.2 造成野指针的原因有哪些呢

1.指针未被初始化
2.指针越界访问
3.指针指向的空间释放
前面两个造成野指针原因都比较容易理解,所以我们一会重点讲一下第三个

1.2.1造成野指针具体代码实例:

1.指针未被初始化

#include <stdio.h>
int main()
{
    
    
	int *p;//局部变量指针未初始化,默认为随机值
	*p = 20;
	return 0;
}

2.指针越界访问

#include <stdio.h>
int main()
{
    
    
	int arr[10] = {
    
    0};
	int *p = &arr[0];
	int i = 0;
	for(i=0; i<=11; i++)
	{
    
    
		//当指针指向的范围超出数组arr的范围时,p就是野指针
		*(p++) = i;
	}
	return 0;
}

3.指针指向的空间释放

int* test()//由于返回的是n的地址,因此函数返回的是int*类型
{
    
    
	int n = 100;//在test函数中创建了局部变量n,
	return &n;//当我们在中间的函数做了一些事情后,我们就返回n,把n的地址返回到指针变量p来接收
	
}
int main()
{
    
    
	int* p = test();//由于返回的是地址,所以我们拿指针变量p来接收
	printf("%d\n", *p);
	return 0;
}

从上面这个代码中,当test()函数中变量n申请了一块空间,而出这个test()函数的时候,这个n的地址就会被销毁,并还给操作系统。然后回到main函数,但是指针变量p仍然记住n的地址,如果到时通过对p进行解引用操作,来改变它所指向对象的值。这就属于是非法访问了,足矣说明p是个野指针。

1.3 如何避免野指针呢?

1.指针初始化
2.小心指针越界
3.指针变量不再使用时,及时置NULL,指针使用之前检查有效性。

1.3.1如何对指针进行初始化?

如果不知道指针指向哪里,可以先给指针复制NULL,NULL是C语言中定义的一个标识符常量,值是0,但是这个地址是无法直接使用的。

初始化如下:

include <stdio.h>
int main()
{
    
    
	int num = 10;
	int*p1 = &num;
	int*p2 = NULL;
	return 0;
}

1.3.2如何才能小心指针越界?

通常来说,一个程序向内存申请了哪些空间,通过指针也只能访问哪些空间,不能超出范围访问,超出了就是越界访问。

1.3.3 指针变量不再使用时,如何及时置NULL,在指针使用之前检查有效性?

  • 当指针变量指向一块区域的时候,我们可以通过指针访问该区域,如果我们后期不再使用这个指针访问空间的时候,我们可以先把该指针置为NULL。
  • 然后到下次使用该指针变量之前,我们要先判断它是否为NULL,如果是就不能指直接使用,不是的话我们才能使用。

2.assert断言

2.1 什么是assert断言

assert.h头文件定义了assert(),它是用于在运行时确保程序符合指定条件,如果不符合,就报错终止运行,这个宏尝尝被称为“断言”。

2.2 如何使用assert断言呢?

在这里插入图片描述

assert(p != NULL);

当上面代码在程序运行到这一行程序时,验证变量p是否等于NULL。如果确实不等于NULL,程序会继续运行,否则就会终止运行,并且给出报错信息提示。

2.3 使用assert有什么好处呢?

  • 它不仅能自动标识文件和出问题的行号,还有一种无需更改代码就能开启或关闭assert()的机制。
  • 如果已经确认程序没有问题,不需要再做断言,就在#include<assert.h>语句的前面,定义一个NDEBUG。
    具体代码如下:
#define NDEBUG
#include <assert.h>

需要注意的是,assert()也是有缺点的。由于引入了额外的检查,会增加程序的运行时间。


3.指针的使用和传址调用

3.1 学习指针的目的是什么?

学习指针的目的是使用指针解决问题,那什么问题,非指针不可呢?

比方说,我们要写一个函数,来交换两个整数变量的值。

经过一番思考后,我们可能会写出这个代码出来~

#include <stdio.h>
void Swap1(int x, int y)
{
    
    
	int tmp = x;
	x = y;
	y = tmp;
}

int main()
{
    
    
	int a = 0;
	int b = 0;
	scanf("%d %d", &a, &b);
	printf("交换前:a=%d b=%d\n", a, b);
	Swap1(a, b);
	printf("交换后:a=%d b=%d\n", a, b);
	return 0;
}

当我们运行此代码,结果如下:

我们会发现这两个变量没有产生交换的效果,这是为什么呢?我们不妨调试一下~

在这里插入图片描述

从图中,我们可以看出在main函数中,我们调用了swap函数。并把变量a和b作为传过去,形参用x和y来接收。但我们发现,这里的变量a和变量x的地址不相同,变量b和变量y的地址也不相同。这也说明形参x和y是一个独立的空间。当swap函数调用结束后返回main函数,a和b的变量依然无法交换,swap在使用的时候,本质上就是把变量本身传递给函数,这也就是我们常说的传值调用

因此我们得出以下结论:

实参传递给行参的时候,形参会单独创建一份临时空间来接收实参,对形参的修改不影响实参,所以swap是失败的。

那我们怎么解决呢?
在这里插入图片描述
我们得借助函数间传址调用来解决。

3.2 什么是传址调用?

传址调用,顾名思义就是将main函数中的变量地址传到所调用的函数中,然后在被调函数中,通过地址间的操作即可实现两个数的交换。

3.3 怎么进行传址调用?

那回到刚刚那种情景,我们只需把变量a和b的地址分别传给swap函数,然后swap函数内部中,通过地址间的操作即可实现main函数中a和b两个数的交换。

代码实现如下:

#include <stdio.h>
void Swap2(int*px, int*py)
{
    
    
	int tmp = 0;
	tmp = *px;
	*px = *py;
	*py = tmp;
}
int main()
{
    
    
	int a = 0;
	int b = 0;
	scanf("%d %d", &a, &b);
	printf("交换前:a=%d b=%d\n", a, b);
	Swap1(&a, &b);
	printf("交换后:a=%d b=%d\n", a, b);
	return 0;
}

运行结果如下:
在这里插入图片描述

4.数组名的理解

在上一次博客C语言指针详解(一)超详细~
我们曾写过两行代码:

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

这里我们是使用&arr[0]的方式拿到了数组第一个元素的地址。但是数组名本来就是地址,不信我们可以拿VS编译器来测试一下。
在这里插入图片描述

从上图,我们发现数组名和数组首元素的地址打印出的结果是一模一样的。

因此我们可以得出这个结论:数组名是数组首元素(第一个元素)的地址
但是呢,有同学会有疑问,如果数组名是数组首元素的地址,那这个代码该怎么理解?

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

从下图,我们发现输出结果是40。
在这里插入图片描述
为什么不是4/8呢?如果数组是首元素的地址,按理说输出的应该是4/8才对。

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

  • sizeof(数组名),sizeof中单独放数组名,这里的数组名表示整个数组,计算的是整个数组的大小。
  • &数组名表示整个数组,取出的是整个数组的地址,(整个数组的地址和数组的首元素的地址是有区别的)。

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


这时,或许还会同学不理解,他们也许会再测试一下这个代码:

#include <stdio.h>
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;
}

发现这三个打印的结果都一样,会再次出现疑惑?
在这里插入图片描述
那接下来我来介绍他们之间的区别。

4.1 arr和&arr的区别

我们直接上代码分析~

#include <stdio.h>
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就是跳过整个数组。

相信到这里大家应该搞清楚数组名的意义了吧。
除了有两个例外,其他的数组名都是数组首元素的地址。


5.二级指针

5.1 什么是二级指针

二级指针指向的是一级指针的指针,也就是说一个指针指向的是另外的指针,同时二级指针也是存放一级指针的地址,则称之为二级指针。

5.2 指针变量的地址存放在哪里呢?

这个我们可以先画个图来分析一下~

在这里插入图片描述

比方说,我们从上图可以得知,我们可以得知指针变量pa存放的是a的地址,而指针变量ppa存放的是指针变量pa的地址,你们由这个规律,我们就能推导出指针变量pppa存放的是指针变量ppa的地址。

另外,这里有个小细节需要大家注意的是,由于pa中的p左边的*代表pa是个指针变量,而前面的int代表pa是个int类型的指针变量。那同理,ppa中的p左边的 *代表ppa是个指针变量,而旁边还有一个 *。代表的是ppa是一个int *类型的指针变量。

5.3 对于二级指针的运算是怎么样的呢?

我们先来看下面代码,然后再逐一进行分析。

#include <stdio.h>
int main() {
    
    

	int a = 10;
	int* p = &a;//p是一级指针

	int** pp = &p;//pp是二级指针
	printf("%d\n", **pp);


	return 0;
}

从上图可以得知,首先,** pp先通过*pp找到p,然后我们再对p进行解引用操作: *p,那找到的就是a,那么最终输出的结果就是10。

VS运行结果如下所示:
在这里插入图片描述



6.指针数组

6.1 什么是指针数组呢?

俗话说,存放整型的数组是整形数组。
存放字符的数组是字符数组。
在这里插入图片描述
那么同理,存放指针的数组则是指针数组。

并且指针数组的每个元素都是用来存放地址(指针)的。
如下图所示:
在这里插入图片描述

我们会发现指针数组每个元素都是存放地址的,又可以指向一块区域。



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

#include <stdio.h>
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数组中
	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]);
			//parr[i][j]==*(*(parr+i)+j))
		}
		printf("\n");
	}
	return 0;
}

在这里插入图片描述

从图中,我们可以看出parr[i]是访问parr数组的元素,parr[i]找到的数组元素指向了整型一维数组,parr[i][j]就是整型一维数组中的元素。
需要注意的是,上面的代码模拟出的二维数组的效果,实际上并未完全是二维数组,以为每一行并非是连续的。


** 好啦!今天博主就分享到这里**
在这里插入图片描述
** 如果觉得博主讲得不错的话。欢迎大家一键三连支持一下**

猜你喜欢

转载自blog.csdn.net/m0_63564767/article/details/134111043