文章目录
- 1.野指针
- 1.1 什么是野指针
- 1.2 造成野指针的原因有哪些呢
- 1.2.1造成野指针具体代码实例:
- 1.3 如何避免野指针呢?
- 1.3.1如何对指针进行初始化?
- 1.3.2如何才能小心指针越界?
- 1.3.3 指针变量不再使用时,如何及时置NULL,在指针使用之前检查有效性?
- 2.assert断言
- 2.1 什么是assert断言
- 2.2 如何使用assert断言呢?
- 2.3 使用assert有什么好处呢?
- 3.指针的使用和传址调用
- 3.1 学习指针的目的是什么?
- 3.2 什么是传址调用?
- 3.3 怎么进行传址调用?
- 4.数组名的理解
- 4.1 arr和&arr的区别
- 5.二级指针
- 5.1 什么是二级指针
- 5.2 指针变量的地址存放在哪里呢?
- 5.3 对于二级指针的运算是怎么样的呢?
- 6.指针数组
- 6.1 什么是指针数组呢?
- 7.指针数组模拟二维数组
通过前面的介绍
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 = #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]就是整型一维数组中的元素。
需要注意的是,上面的代码模拟出的二维数组的效果,实际上并未完全是二维数组,以为每一行并非是连续的。
** 好啦!今天博主就分享到这里**
** 如果觉得博主讲得不错的话。欢迎大家一键三连支持一下**