一、野指针
(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