目录
1、指针是什么
2、指针和指针类型
3、野指针
4、指针运算
5、指针和数组
6、二级指针
7、指针数组
前言
在初学c语言的时候,就听说c语言的指针很难很难。于是我在大一那会学到指针总是不自信,很多概念都比较模糊。现在我也是大四老狗了,重新回来学一遍指针,发现其实并没有那么困难,只是我们总是下意识把它想难了。
如果在一开始就没有信心弄懂,那么最后你大概率真的弄不懂。我说这些就是想告诉你,在读这篇文章之前,我觉得我们应该放轻松,要给自己信心,带着自信去学习。
这篇文章是我自己边学边写的,很感谢鹏哥的视频,讲得很详细,有兴趣了解c语言的朋友真心推荐鹏哥视频。这篇文章算是我近期,或者说是csdn创建以来最花功夫的一篇文章了。希望能让你对指针有清晰的认识。
1、指针是什么
指针就是地址,指针变量就是用来存放地址的变量。我们也习惯叫指针变量为指针,但实际上指针就是地址,被习惯叫做指针的其实是指针变量。(有点绕,稍微理解一下)
指针有什么用?
用于管理内存空间(一个内存空间为一个字节,8比特)。每台电脑都有内存,用于存放数据,有的电脑是8G,有的16G。如此庞大的内存空间如何管理呢,将数据存入内存后这么找到呢?基于此问题,计算机就对每一个内存进行编号,一个号码对应一个内存单元。这里,对每个内存单元的编号就是指针,指针就是地址,就是对内存的编号。
指针变量就是用来存放指针的变量,我们编程的时候,如果需要用到某一个内存的地址的时候,不可能每次都要精准无误地去把他的编号打出来,那太麻烦了,于是我们可以将这一串地址编码放到一个变量当中,这样,下次我们想用这个地址的时候,只需要将这个变量拿出来就好了。
以一个代码来举例:
int main()
{
int a = 0;
printf("%0Xd", &a);
return 0;
}
这个代码我设置了一个变量a,计算机会将它随机存放到内存的一个地址,然后我用&取出a的地址并打印出来,如下:
地址为:6FFBBCd
(我们要注意,a是一个整型,它要占用内存4个字节的空间,因此指向a的地址其实有4个,但是现在只输出一个,这一个是a变量所占内存空间的首地址。)
如何定义指针变量? 很简单,加上‘ * ’号就可以了: int* pa; 这样就定义了一个指针pa。
现在我用一个指针变量 pa 去存放 a 的地址,再将它打印出来:
int main()
{
int a = 0;
int* pa = &a;
printf("%0Xd", pa);
return 0;
}
pa的输出结构是35FB74d
我们发现和上面不一样,那是因为在我们每次重新执行程序的时候,计算机都会重新将a放到新的地址,因此我们可以这样写:
int main()
{
int a = 0;
int* pa = &a;
printf("%0Xd\n", pa);
printf("%0Xd\n", &a);
return 0;
}
发现,两个输出确实是一样的。此时,pa就是一个指针变量,它存放内容的就是a的地址。以后需要用到a的地址的时候,就可以将pa拿出来使用了。
可能有人会问,为什么不直接用&a来表示a的地址,而要用pa去存呢?
因为pa是“变量”,而&a取出来的是常量,很多操作需要用到变量的。因为变量可变的属性,我们可以对pa进行“pa++”这样的操作,即让pa去指向它的下一个地址。但是我们不能写“&a++”,因为&a是常量,是无法修改的。就好比我们不能修改常数“1”一样,1就是1,不能说1=1+1,只有我们将1赋予一个变量x后,我们对x进行修改,x=x+1,这样才合理。很多操作必须用变量才能完成而常量,因此在地址的处理方面我们才有了指针变量。
总结,指针变量,是用来存放地址的变量。
指针变量的大小
对于不同的计算机,指针变量的大小可能不同。如上图,在编译器中,我们可以选择x86和x64计算机进行编译,如果是x86的话,指针大小就为4个字节,因为其为32位机(每一位为1比特,8个比特为1字节)。而如果选择x64计算机编译的话,指针大小就为8。
总结,指针大小在32位平台是4个字节,在64位平台是8个字节。
2、指针和指针类型
指针变量类型和普通变量类型种类相同,包括:int* char* short* double*等等。指针指向的内容是什么类型,指针变量就要定义为什么类型。
它们各自的大小:
int main()
{
char* pa = NULL;
short* ps = NULL;
int* pi = NULL;
double* pd = NULL;
printf("%zu\n", sizeof(pa));
printf("%zu\n", sizeof(ps));
printf("%zu\n", sizeof(pi));
printf("%zu\n", sizeof(pd));
return 0;
}
最后发现都是4,那是因为指针大小与其类型无关,只和编译的环境有关,目前是x86的环境,因此指针大小无论哪个类型都是4;若是x64,则都为8。
那么既然类型不决定指针变量的大小,那不同类型的指针区别在哪呢?能不能用一个统一的类型定义指针呢?
答案是不能,指针变量的类型是有意义的,只不过不是体现在指针自己的内存大小上,而是体现在指针变量所指向的内容上。下面具体分析。
此处我将a赋值为16进制的12345678,在内存中(右边)找到a的地址,可以看到,a地址内存存放的是78 56 34 12。,这个原因我在后面的文章,数据存储(c语言进阶)(一)有讲到为什么,其实就是一个小段存放。总而言之这个位置存放的就是a的值。接下来我用p这个指针(注意,现在定义p的类型是int*)指向a的地址,然后*p(解引用p)也就是取p的内容,把它赋值为0。看看效果:
可以看到,a的值被赋值为00 00 00 00。也就是0。
那如果我将p的类型改为char*会怎么样呢:
可以看到,a的值被赋值为00 56 34 12。没错,由于p指针变量被定义为char类型,所以默认p指向的是一个字符,于是当指向*p=0;时,计算机根据char类型长度(1字节)进行改动,所以只改了a中前一个字节的内容。因此,我们得到,指针类型时有意义的,不能随意定义。
结论,指针类型决定了指针在解引用时访问几个字节,如果是int*的指针,解引用访问4个字节。如果是char*就解引用访问1个字节,以此类推其他类型的指针。
以上是不同指针类型的第一个区别,下面再分析另一个不同。
我定义了两个指针,一个是int*类型的指针pi,另一个是char*类型的指针pc。让它们同时指向a这个变量的地址,然后都加1观察。发现,int*类型的pi指针加1后指向的是4个字节之后的地址;而char*类型的pc指针加1后指向的是后1个字节的地址。
结论,指针的类型决定了指针的步长(指针加一减一要跳过的字节数)。
3、野指针
概念:野指针就是指针指向的位置是不可知的(随机的、不正确的、没有明确限制的)
3.1 野指针形成的原因
1、指针未初始化
int main()
{
int* p;
*p = 10;
return 0;
}
这里p指针就没有初始化,当一个变量未初始化的时候,它所存放的内容是不确定的。它就是一个野指针。
2、指针越界访问
int main()
{
int arr[10] = { 0 };
int* p = arr;
int i = 0;
for (i = 0;i <= 10;i++)
{
*p = i;
p++;
}
return 0;
}
这里注意for循环循环了11次,p在最后超出了arr这个数组的地址的内存范围,当指针超出arr的范围时,p就越界了,p就成了野指针。
3、指针指向的空间释放
指针原本指向的空间被释放了,指针就变成了野指针。
int* test()
{
int a = 10;
return &a;
}
int main()
{
int* p = test();
return 0;
}
这里当test()函数结束后,a被销毁,p就成了野指针。
野指针是指指向已经释放或者未分配的内存地址的指针。使用野指针会导致程序崩溃、内存泄漏等严重问题,因此需要杜绝野指针的使用。
3.2 如何避免野指针的出现
1、指针初始化
2、小心指针越界
3、指针指向空间释放时使指针为NULL
4、避免返回局部变量的地址
5、指针使用前检查有效性
在建立一个新的指针时就对其进行初始化,如果不知道初始化为什么的话就初始化为NULL。但是初始化为NULL之后不能直接对指针内容进行操作,因为计算机不允许0地址的操作。
int main()
{
int* p2 = NULL;
*p2 = 100;
return 0;
}
上面这个代码就会报错,因为p2指向的是NULL,而这个地址不允许被更改,对它赋值100就不行。
所以在用之前再判断这个指针是否为NULL,不为NULL再去操作其内容。(因为中间可能找到了指针要赋值的地址所以指针就不再是NULL了,这里前面说初始化的时候当时没想到要赋值什么的时候就先给NULL,后续找到了想要指向的位置就再指向它,这样避免了因为没有初始化而产生野指针)。
int main()
{
int* p3 = NULL;
//
//.........中间过了一堆的程序
//
if (p3 != NULL)
{
*p3 = 100;
}
return 0;
}
如此就可以一定程度上避免因初始化而导致的野指针的产生。
4、指针运算
4.1 指针+-整数
上面这个代码,通过指针p对a数组进行输出,我们可以看到,输出时用了*p++这个操作,其实就是对指针进行运算。p本来指向了a[0]的地址,*p++时先解引用p也就是访问p,得到p地址的内容是a[0],将其输出后,再执行p++(p=p+1)。p=p+1就是指针指向下一个“相邻”地址,这里就是a[1]的地址,这里步长就和类型相关。然后下一次循环再次执行,先解引用p输出a[1]的值,再p=p+1指向下一个“相邻”地址。最后效果就是输出a数组的所有元素。
同理,p--就是使p指向其前一个“相邻”的地址。
4.2 指针 - 指针
看一个程序 :
这样的代码,表示数组a[9]的地址减去a[0]的地址,也就是指针减指针的操作。最后输出的是9,而不是36。指针-指针得到是指针之间元素的个数,而不是两个地址之间的的差值。用这个特性可以求数组的元素个数。
不是所有的指针相减都有意义,指向同一块空间的指针相减才有意义。
指针 + 指针不讨论,没有意义。两个指针相加后的值是不确定的,就像日期只有相减才有意义,相加无意义。
4.3 指针的关系运算
其实就是比较大小,看一个程序:
这个程序,再while的执行判断中,比较了pa指针和pb指针的大小,其中pa是指向的arr数组的第一个元素的地址,pb指向的是arr第5个元素地址。比较两个指针,如果pa小于pb,则输出pa对应地址的内容,直到pa>pb,最后的效果就是输出arr数组的所有元素。
这里要注意,c语言标准规定:允许指向元素的地址与元素数组最后一个元素后面那个内存地址进行比较,但不允许与指向第一个元素之前的那个内存地址进行比较。这个稍微理解即可,意思就是,在从前往后遍历一个数组的时候,可以标记一个数组最后一个元素在往后一个位置的地址为结束判断地址。但是,从后往前遍历数组的时候,不能将数组第一个元素前面的那个地址作为结束判断地址。
5、指针和数组
数组:一组相同类型的元素集合
指针变量:是一个变量,存放的是地址
看上去没关系,其实也有关系。数组名就是数组首元素的地址,可以通过指针来访问数组,如:
这里通过p=arr将arr数组首元素地址赋给p指针变量,然后p指针变量就可操作数组arr中的数了。
此外,直接将数组名传参给函数,接收处形参可以用指针接受,如:
上面函数同样实现了数组各元素打印,传参是数组名arr,接收用了p指针。
6、二级指针
二级指针就是指针的指针,需要解引用两次才能访问地址内容,解引用一次得到的是一级指针变量。二级指针的类型是根据其指向的指针的类型确定。
7、指针数组
存放指针的数组就是指针数组。
parr就是指针数组,它的元素类型都是指针。
我们还可以用指针数组去模拟二维数组:
这里的parr[3]其实就相当于arr[3][4]={{1,2,3,4},{2,3,4,5},{3,4,5,6}};这个数组。
以上就是c语言指针的全部内容了,能看到这里十分感谢,能对你理解指针有帮助我会非常开心!