学习笔记:C语言的指针的学习——野指针

一、野指针

(1)概念

概念:野指针就是指针指向的位置是不可知的(随机的、不正确的、没有明确限制的)

什么情况会导致野指针?

1) 指针未初始化

随机生成一个地址是很可怕的。

通过p随机找到一块内存空间,改变它的值,这是非法操作。

编译器也会报错:

#include<stdio.h>
int main()
{
	int* p;                       
	*p = 10;  
	
	return 0;
}

此段代码中,局部变量p未初始化,也就意味着p为随机值没有方向,*p就会非法访问内存空间,此时p就为野指针。

有效改善方法:    初始化为空指针。如果定义指针的时候实在不知道赋什么值,可以先将其定义为空指针,即int*p=NULL;,NULL是代表空指针的意思,后面如果用到指针的话再让指针指向具有实际意义的地址,然后通过指针的解引用改变其指向的内容。

#include<stdio.h>
int main()
{
	int* p=NULL;
	
 
	int b = 8;
	p = &b;         
	*p = 100;
    printf("%d",*p);
	return 0;
}

2)指针越界访问

当指针指向的范围超出数组arr的范围时,p就是野指针。

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

运行结果:100

 arr整型数组,有10个元素,初始化元素值为0,arr数组名表示首元素地址传给了指针p,通过对指针解引用改变数组元素值,当i=10时,此时*p访问的内存空间不在数组有效范围内,此时*p就属于非法访问内存空间,p为野指针。

3)指针指向的空间释放

这里放在动态内存开辟的时候讲解,这里可以简单提示一下。

#include<stdio.h>
int* test()
{
	int a = 10;
	return &a;          //&a=0x0012ff40
}
int main()
{
	int* p = test();
    *p  = 20;
	return 0;
}

注:int a=10;(0x0012ff44)a是一个局部变量,在它进入它的作用域时创建,在它离开它的作用域时销毁。也就是说,它的内存空间返回给操作系统

   int* p = test(); (0x0012ff44)地址虽然返回了,p存的那个地址所指向的空间已经还给操作系统了,*p访问的空间已经不是自己的了。访问的是被释放的空间。

程序开始后首先进入主函数,执行第一步,调用test函数将返回值赋给p,test函数的返回值是局部变量a的地址,假设a的地址为0x0012ff40,由于a只在test函数内有效,出了test函数其内存空间就被释放,也就意味着a的地址编号不存在,短时间内如果再次利用这块地址,它的值还未被改变也就是0x0012ff40还存在,p的值为0x0012ff40,但此时p为野指针,因为p里面所存放的地址是无效的。
 

#include <stdio.h>
int* test()
{
	int a = 10;
	return &a;          //&a=0x0012ff40
}
int main()
{
	int* p = test();
	printf("%d\n",*p);
	
	return 0;
}

当我们用编译器运行,是可以得到结果10。

变量a是局部变量,进入test函数创建,出了大括号就会被销毁。

进入test函数的时候,内存开辟了4个字节的空间,当出了test函数,该内存就被还给了操作系统。

当执行return &a的时候,将a的地址返回去了,但此时a的空间被销毁了。

返回的地址,赋值给了p指针。但是该地址不属于当前程序了。

这时候要通过p指针,找到指向的空间,这块空间可能已经分配给别人了。这样贸然访问,是非法操作!
 

同样的问题下列代码也出现一点的问题:

iint* test(){
    int arr[10]={10};
    return arr;
}
int main(){
    int* p=test();
    printf("%d\n",*p);
}

只要是返回临时变量的地址,都是有问题的!

除了这个临时变量没有被销毁。被static修饰就不会被销毁。

(2)规避野指针

1)指针初始化

一定要记得初始化!!!

int main(){
    int a=10;
    int* pa=&a;	//初始化
}

但我们也会遇到不知道怎么初始化的时候。

这时候就可以给它赋值NULL。(空指针)

如下:

int* p=NULL;

这个NULL是什么呢?

我们点击NULL,速览定义#define NULL 0LL

可以看到,NULL就是0:

NULL用来初始化指针的,给指针赋值。

int main()
{
	int a = 10;
	int* pa = &a;//让pa指向a 
	int* p = NULL;//不知道p指向谁 
}

(void*)0:把0强制类型转换成了void*这种类型,本质上还是0。

就相当于之前我们创建变量的时候,不知道赋什么值,就给变量赋值0:

int b=0;

2)小心指针越界
这个没有什么好解释的,上面已经说的很明白了。

3)指针指向的空间释放即使置NULL
指针指向的空间,如果我们还给别人了,我们就可以把指针置为空。

本来指针指向了这块空间,现在我不想用这块空间了,这块空间已经还给别人了。

如果还想让这个指针合法的存在,那就先把它设为空指针。

举个例子:

int main()
{
	int a = 10;
	int* pa = &a;//让pa指向a 
	*pa  = 20;
	//假设我们现在已经操作好了,pa指针也不打算用它了,可以把它置成空,
	//这时候pa指针并没有指向a了,并没有指向任何有向空间了 
	pa = NULL;//这时候已经不指向a了,不会在干扰a了。 
}

当你不想让一个指针指向其他地方的时候,或者它指向的空间已经还给操作系统的时候,这时候可以把一个指针置成空指针。

避免它未来可能成为野指针。

2)指针使用之前检查有效性

当我们对指针进行初始化,这个指针就是有效的。

当我们不用这个指针的时候,我们把它置成空指针(置成空指针的时候,就不能访问它指向的空间了)。

即:遇到指针初始化,用完之后赋值为NULL空指针。

就像这样:

①用的时候初始化

int a=10;
int* pa=&a;
*pa=20;

②不用的时候置为空指针

pa=NULL;

所以,

在后边我们要继续使用这个指针的时候,就需要判断一下:这个指针,如果等于空指针就不用;不是空指针就使用。

即:

if(pa==NULL){
    //如果pa是空指针,就不使用
}
if(pa!=NULL){
    //如果pa不是空指针,就可以使用它
}

当pa已经被赋值为空指针,这时候我们强制访问它,并给它赋值为10。

⛔️ 当指针为NULL的时候,不能访问它!

所以后边使用指针的时候,必须要判断一下。

如果指针不是空指针(说明里面放的是有意义的地址),就可以使用;是空指针就不能使用。

如下判断即可:

int main()
{
	int a = 10;
	int* pa = &a;//让pa指向a 
	*pa  = 20;
	//假设我们现在已经操作好了,pa指针也不打算用它了,可以把它置成空,
	//这时候pa指针并没有指向a了,并没有指向任何有向空间了 
	pa = NULL;//这时候已经不指向a了,不会在干扰a了。 
	*pa = 10;
	if(pa! = NULL)
	{
		
	}	 
}

看个小案例,指针被赋值为了空指针,重新给它指向空间(初始化),就可以继续使用了。

如下:

int main()
{
	int* p = NULL;//让pa指向a 
	int a = 10;
	p = &a;
	if(p != NULL)
	{
		*p = 20;	
	}
	printf("%d\n",*p);	
	return 0; 
}

二、指针运算

(1)指针加减整数

1)指针+整数

看一个小案例:

int main() { 
   int arr[10] = { 1,2,3,4,5,6,7,8,9,10 }; 
  //不用下标来访问,用指针来访问 
   int* p = arr; //数组名就是首元素地址,将arr交给p指针
   int i = 0; 
   int sz = sizeof(arr) / sizeof(arr[0]); //数组元素个数
   for (i = 0; i < sz; i++) { 
     printf("%d ", *p); //第一次打印,p里面放的就是首元素地址
     p=p + 1;//向后跳一个整形 
   } 
    return 0; 
} 

看一下输出结果:1 2 3 4 5 6 7 8 9 10

既然p+1,循环10次,可以输出所有元素。

那么p+2也是可以的,输出奇数。

如下:

int main() { 
   int arr[10] = { 1,2,3,4,5,6,7,8,9,10 }; 
  //不用下标来访问,用指针来访问 
   int* p = arr; //数组名就是首元素地址,将arr交给p指针
   int i = 0; 
   int sz = sizeof(arr) / sizeof(arr[0]); //数组元素个数
  // for (i = 0; i < sz; i++) { 
  //   printf("%d ", *p); //第一次打印,p里面放的就是首元素地址
  //   p=p + 1;//向后跳一个整形 
  // }
  for(i = 0;i < 5;i++)
  {
  	printf("%d",*p);
  	p += 2;	
   } 
    return 0; 
} 

输出结果:1 3 5 7 9

2)指针-整数

既然指针+整数可行,那么指针-整数也是可行的。

这里我们要稍微改动一下代码。

先将第十个元素的地址赋值给指针p,即int* p=&arr[9]

int main() { 
   int arr[10] = { 1,2,3,4,5,6,7,8,9,10 }; 
  //不用下标来访问,用指针来访问 
   int* p = &arr[9]; //将第十个元素的地址交给p指针
   int i = 0; 
   int sz = sizeof(arr) / sizeof(arr[0]); //数组元素个数
   for (i = 0; i < sz/2; i++) { 
     printf("%d ", *p); //第一次打印,p里面放的就是首元素地址
     p-=2;//指针向前指
   } 
    return 0; 
} 

输出结果:10 8 6 4 2 

3)案例

再举个例子:

#define N_VALUES 5
int main(){
    float values[N_VALUES];	//定义一个数组,数组里面5个元素
    float* vp;	//定义一个指针
    for(vp=&values[0];vp<&values[N_VALUES];){
        *vp++ =0;
    }
    return 0;
}

这几行代码的意思,就是最终让数组里面的元素,都变成0:

(2)指针减指针

1)案例一

指针-指针,就是地址-地址

比如:

int main(){
    int arr[10]={1,2,3,4,5,6,7,8,9,10};
    printf("%d\n",&arr[9]-&arr[0]);	//让第10个元素的地址减去第1个元素的地址
}

输出看一下:9

🍰 指针减去指针:得到的是中间的元素个数。

中间元素有:1,2,3,4,5,6,7,8,9(一共9个)

如果是小地址减去大地址,得到的就是负数(反过来了)。

int main(){
    int arr[10]={1,2,3,4,5,6,7,8,9,10};
    printf("%d\n",&arr[0]-&arr[9]);	//让第10个元素的地址减去第1个元素的地址
}

输出看一下:-9

所以,

想得到元素个数,一定是大地址减去小地址,而小地址减去大地址的绝对值是最终结果。

2)错误案例

①不同类型

再来看一个错误:

int main(){
    int arr[10]={1,2,3,4,5,6,7,8,9,10};//整型数组
    char ch[5]={0};//字符数组
    printf("%d\n",&arr[9]-&ch[0]);	
}

以上这种情况,是按照整型讨论还是字符?乱套了!!!这种写法最终结果是不可预知的。

当一个指针减去一个指针的时候,那这两个指针一定是指向同一块空间的。

这是错误写法!两个不同类型的指针相减,是没有任何意义的。

②指针相加

⛔️ 注意:两个指针相加也没有意义!

3)案例二

再举个例子:求字符串长度

现在我们拥有一个数组arr,将数组首元素地址传给了my_strlen函数。

 int my_strlen(char* str) { 
 	
 } 
 int main() { 
     //strlen-求字符串长度 
     //讲“递归”的时候,我们模拟实现了strlen,1、递归的方式 2、计数器的方式 
     char arr[] = "bit"; 
     int len=my_strlen(arr);//把数组的首元素地址放进去了 
     printf("%d\n", len); 
     return 0; 
  } 

🌵 分析:

如果现在有一个指针(start)指向数组第一个元素,还有一个指针(end)指向最后一个元素。

那么,就可以用end指针减去start指针,就可以得到字符串长度了。

那么这两个指针如何表示?

start指针很简单,我们传上去的就是数组首元素地址,直接赋值给start指针即可。

char* start=str;

end指针可以利用循环,找到最后的元素\0,停止循环,即可找到最后元素的地址。

char* end=str;
while(*end !='\0'){
    end++;
}

最后返回元素个数(end-start):

return end-start;

看一下编译器输出结果:

#include<stdio.h>
 int my_strlen(char* str) { 
 	 char* start = str; 
	 char* end = str; 
     while (*end != '\0') { 
     	end++; 
     } 
     return end - start;//字符个数 
 } 
 int main() { 
     //strlen-求字符串长度 
     //讲“递归”的时候,我们模拟实现了strlen,1、递归的方式 2、计数器的方式 
     char arr[] = "bit"; 
     int len=my_strlen(arr);//把数组的首元素地址放进去了 
     printf("%d\n", len); 
     return 0; 
  } 

编译结果为:3

(3)指针的关系运算

指针的关系运算即指针比较大小

#define N_VALUES 5 
int main() { 
       float values[N_VALUES]; //创建一个数组,里面5个元素
       float* vp; 
       for (vp = &values[N_VALUES]; vp > &values[0];) { 
         *--vp = 0; 
       } 
       return 0; 
 } 

三、指针和数组

(1)回顾数组

先回顾一下:

数组名是什么?

数组是具有相同类型的集合,数组的大小(即所占字节数)由元素个数乘以单个元素的大小。
数组只能够整体初始化,不能被整体赋值。只能使用循环从第一个逐个遍历赋值。
初始化时,数组的维度或元素个数可忽略 ,编译器会根据花括号中元素个数初始化数组元素的个数。
当花括号中用于初始化值的个数不足数组元素大小时,数组剩下的元素依次用0初始化。
字符型数组在计算机内部用的时对应的ascii码值进行存储的。
一般用”“引起的字符串,不用数组保存时,一般都被直接编译到字符常量区,并且不可被修改。

数组名是首元素地址(两个特例)。

既然数组名是首元素地址,那么我们打印arr&arr[0]结果应该是一样的。

如下图:

#include<stdio.h>
int main()
{
	int arr[10] = {0};
	printf("%p\n",arr);
	printf("%p\n",&arr[0]);
	return 0;
}

arr与**arr[0]**地址相同。所以都是首元素地址,这是无疑的。

绝大多数情况下,数组名都是首元素地址。

有两个例外:

①&arr

整个数组的地址。

②sizeof(arr)

整个数组的大小,单位为字节。

#include<stdio.h>
int main()
{
	int arr[10] = {0};
	printf("%p\n",arr);
	printf("%p\n",&arr[0]);
	printf("%p\n",&arr);
	return 0;
}

🍰 总结

arr&arr[0]得到的是首元素地址,也仅仅只有首元素地址。

&arr得到的地址,后面包括一整个数组。

可能上面说的比较含糊,如果将它们分别加一,观察分别输出的地址:

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

的小伙伴可能不太会地址的计算,之前博客有写过,这里再说一下吧。

内存中地址是十六进制存储的,1~9,a~f(10~15)。

地址计算:(案例)

00EFF8E0 — > 00EFF908

将后面三项拿出来:8E0 — > 908

怎么计算相差多少呢?如下图:00EFF908-00EFF8E0=28

(2)指针和数组

既然数组名表示首元素地址,那么就可以直接将数组名存入指针里面,如下:

int arr[10]={1,2,3,4,5,6,7,8,9,0};
int* p=arr;	//p里面存放的是数组首元素地址

既然可以把数组名当成地址存放到一个指针中,我们就可以使用指针来访问数组。

即,数组可以通过指针进行访问。

我们不妨通过两种方法,分别输出每一个元素的地址。

第一种,可以这样输出数组元素的地址:&arr[i];第二种:p+i

具体代码如下:

 int main() { 
   int i = 0; 
   int arr[] = { 1,2,3,4,5,6,7,8,9,0 }; 
   int* p = arr; //p里面存放的是数组首元素地址
   int sz = sizeof(arr) / sizeof(arr[0]); 
   for (i = 0; i < sz; i++) { 
     printf("&arr[%d]=%p<===>p+%d=%p\n", i, &arr[i], i, p + i); 
   } 
   return 0; 
} 

输出结果:

上面我们可以发现,p+i&arr[i]结果一模一样。

我们就可以直接使用指针p来访问数组了

我们可以做个小案例:

#include<stdio.h>
int main(){
 	int i = 0; 
 	int arr[10] = { 0 }; 
 	int* p = arr; 
 	int sz = sizeof(arr) / sizeof(arr[0]); 
 	for (i = 0; i < sz; i++) { 
  		 *(p + i) = i;//把arr数组元素改成0,1,2,3... 
 	} 
	for (i = 0; i < sz; i++) { 
  		 printf("%d ", arr[i]); 	//用数组形式,输出数组里面元素
	} 
	printf("\n"); 
	for (i = 0; i < sz; i++) { 
  	 	printf("%d ", *(p + i)); 	//用指针访问数组的形式,输出数组里面元素
	} 
    return 0;
} 

输出结果:0 1 2 3 4 5 6 7 8 9

🍰 总结

我们可以看到:数组可以通过指针来进行访问。

但数组和指针不是一回事,

数组可以存放一组相同类型的数据,而指针只是可以存放一个地址或是一个数组起始位置/任意位置的地址

四、二级指针

(1)介绍

平时我们写的指针都是一级指针。

如下:

int main(){
    int a=10;
    int* pa=&a;	//pa就是一级指针变量,int*就是一级指针类型
}

a是一个变量,将a的地址取出来,放进pa里面,pa的类型是int*。pa是一级指针变量。

再来想一下,pa是指针变量,变量创建要在内存中开辟空间。

如果现在这样写:&pa,就拿到了pa空间的地址。这块地址也想存起来,怎么办呢?

比如将pa的地址,存放进ppa变量里面,这时候ppa的类型就应该这样写:int**。

如下:

int** ppa=&pa;

ppa就是二级指针(存放一级指针的地址)。

同样,如果想要取出ppa的地址,存放进pppa变量里面。pppa变量的类型应该是int***

如下:

int*** pppa=&ppa;

二级指针有什么用呢?

比如现在我们想通过ppa拿出a的值,先解引用*ppa,找到pa,然后再解引用**ppa,找到a,输出即可。

如下:

printf("%d\n",**ppa);

(2)理解

int* pa=&a*表示pa是指针类型的,int表示它指向的对象是int类型。

int* * ppa=&pa;:第二个*表示ppa是指针类型的,int*表示它指向的对象是int**类型。

还可以通过ppa改变a的值。

 #define _CRT_SECURE_NO_WARNINGS 
 #include<stdio.h> 
 int main() { 
   int a = 10; 
   int* pa = &a;//pa是一级指针变量,int*是一级指针类型 
   int* * ppa=&pa;//ppa就是二级指针变量 
   **ppa = 20; 
   printf("%d\n", **ppa); 
   printf("%d\n", a); 
    //int** * pppa = &ppa;//pppa就是三级指针变量 
    return 0; 
  } 

输出结果:20  20

五、指针数组

 问:指针数组是指针还是数组?

是数组。是存放指针的数组。

看一个例子:

int main(){
    int a=10;
    int b=20;
    int c=30;
    //分别将a,b,c的地址存入指针变量pa,pb,pc中
    int* pa=&a;
    int* pb=&b;
    int* pc=&c;
}

如果我想把a,b,c的地址存起来,就需要三个指针变量pa,pb,pc。

那能不能写一个数组,把他们三个地址都存放起来?数组里面放的都是整型变量的地址。

如何写一个指针数组?

之前我们写整型数组:int arr[3],那么写指针数组就可以这样写:int* arr[3]。

然后初始化:

int* arr[3]={&a,&b,&c};

拿到指针数组里面的指针也很简单:

*(arr[i]);	//i=0,1,2

输出所以代码:

int main(){
    int a=10;
    int b=20;
    int c=30;
    //分别将a,b,c的地址存入指针变量pa,pb,pc中
    int* pa=&a;
    int* pb=&b;
    int* pc=&c;
    int* arr[3]={&a,&b,&c};
    int i=0;
    for(i=0;i<3;i++){
        printf("%d ",*(arr[i]));
    }
    return 0;
}

输出结果:10  20  30

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值