开篇语
关于指针,绝对是C语言和C++的灵魂所在,我们常说C语言和C++是非常灵活的,其原因根本就在于指针是非常灵活的,我们都知道其实指针呢,本质就是地址,不管什么数据,在计算机中存储那么就一定有它的地址,我们可以通过指针来直接访问到,可见,这项功能是多么的强大,到后面我们学习数据结构的话,数据结构中的队列、栈、链表、树等,无论如何复杂,数据结构也总是位于计算机的内存中,因此必有地址。利用指针就可以使用地址作为一个完整值的速记符号,因为一个内存地址在内部表示为一个整数。当数据结构本身很大时,这种策略能节约大量内存空间。当然了,这里只是举一个简单的例子,比较容易理解到指针的强大功能,关于指针的好处呢,其实还有很多:
提高程序的编译效率和执行速度, 使程序更加简洁,
可以实现动态内存分配, 用于表示和实现各种复杂的数据结构,
可以直接操纵内存地址,从而可以完成和汇编语言类似的工作,
…
说了这么多呢,其实想要传达的意思就一个,指针很重要,指针很重要,指针很重要!!!
听到这里,你是否迫不及待想要揭开指针神秘的面纱了呢,别着急,指针虽好,当然指针其实操作上限很高,使用不当可是极其容易出问题的,我们今天这篇只是初阶,相对简单的去学习一遍,关于拔高的进阶部分呢,放到之后我们再来,毕竟罗马不是一天建成的。
指针是什么
在理解指针是什么之前,我们要先知道内存是什么,看下图:
像图中所示的一个个内存单元,每一个都有其对应的编号,这个编号就是地址,在C语言中也叫做指针,就像一栋宿舍楼一样,每一个房间都有门牌号一样。那么关于指针我们应该如何去理解呢?
1. 指针是内存中一个最小单元的编号,也就是地址。
- 平时口语中说的指针,通常指的是指针变量,是用来存放内存地址的变量
- 总结来说:指针就是地址,口语中说的指针通常指的是指针变量
关于指针变量:
我们可以通过&(取地址操作符)取出变量的内存其实地址,把地址可以存放到一个变量中,这个
变量就是指针变量。
我们来看一段简单的代码:
#include<stdio.h>
int main()
{
int a = 10;//创建一个变量,一定会开辟一块空间;
int* pa = &a;//将a的地址取出来放到pa里面去,pa就是一个指针变量;
printf("%p\n", &a);//将a的地址和pa打印出来看一下;
printf("%p\n", pa);
*pa = 20;//*解引用操作符,*pa得到的其实就是a,
//这里就是直接通过指针找到了a,从而直接对a进行操作
printf("a=%d\n", a);
return 0;
}
可以看到,a的地址和pa是完全相同的。并且通过解引用操作可以通过地址直接访问到a,然后对其进行操作。
总结一下:指针变量,用来存放地址的变量。(存放在指针中的值都被当成地址处理)
内存中如何编址
我们这里以32位平台为例,对于32位的机器,假设有32根地址线,那么假设每根地址线在寻址的时候产生高电平(高电压)和低电
平(低电压)就是(1或者0)
我们已经知道一个内存单元大小是1个字节,那么2^32个字节换算成我们平时说的GB大小是多少呢?
我们知道内存大小有bit(比特)、byte(字节)、KB、MB、GB、TB…除了特殊的一字节等于8个比特位,再向后走换算大小都是1024,我们可以写出来二进制序列来换算一下,
最后得到的结果是4GB,所以这就是早期的32位电脑内存的来由,现在大都是64位了,也就是有64根地址线,具体可以自己去算。
总结:
指针变量是用来存放地址的,地址是唯一标示一个内存单元的。
指针的大小在32位平台是4个字节,在64位平台是8个字节
指针和指针类型
可能到这里你会有疑问了,我们说指针大小只和所在平台有关,大小都是4/8个字节,那么指针类型有什么作用呢?我们来继续看:
当我调试的时候,如果再执行(*pa=0)句代码,我们来看一下结果:
可以看到4个字节的内容全部被改掉了,但如果我把a放到(char *)类型的指针变量中去呢,相同的步骤,我们再来看一下:
可以发现,实际改掉的只有一个字节大小的内容,从这里我们就可以总结一下了:
指针类型其实是有很大意义的,指针类型决定了我们访问不同大小空间的内容,当我们想访问几个字节大小的时候,就需要选用相应的指针类型。看到这里,我想你就应该有一点感知到我们开头说的指针的灵活性了。
指针±整数
我们再来看一段代码:
这也是一个能够说明指针类型意义的知识点,当指针+整数时,不同类型的指针变量变化的大小也是不同的,我们也可以说:
指针类型还决定了指针变化的步长
千万不要以为int类型的数据只能用int * 的指针。指针类型和数据类型不是一一对应的关系。只有+1跳过的步长和数据类型的有关。
重点要理解的内容就是指针类型的意义:
指针类型决定了我们访问不同大小空间的内容,当我们想访问几个字节大小的时候,就需要选用相应的指针类型。
指针类型还决定了指针变化的步长 ,
总结就是指针类型为我们提供了不同的视角去观察和访问我们的存储的数据
野指针
野指针概念: 野指针就是指针指向的位置是不可知的(随机的、不正确的、没有明确限制的)
野指针成因
1 .指针未初始化
例如:
#include<stdio.h>
int main()
{
int* p;//局部变量指针未初始化,默认为随机值
*p = 20;
return 0;
}
这种就是极其危险的行为,这里的p是一个地址,而没有初始化的指针指向的地址你是完全不清楚是哪里的,这样的行为你觉得危险不危险。肯定要出大问题的,内存中的数据你连什么都不清楚就直接进行访问,当然要出大乱子的。
2 .越界访问
#include<stdio.h>
int main()
{
int arr[5] = { 1,2,3,4,5 };
int sz = sizeof(arr) / sizeof(arr[0]);
int i = 0;
int* p = arr;
for (i = 0; i <= sz; i++)
{
*(p + i) = 0;
}
return 0;
}
当我们拿这段代码去编译器运行的时候你会看到下面这个错误:
诶,是不是看着很熟悉,似乎在前面学习数组部分专门有强调过,这个明显就是数组越界问题,其实对于指针也是一样,越界访问的时候,你是完全不知道你越界到的地方是有什么的东西的,但是你非要去访问,这种行为当然也是不可行的。
3 .指针指向的空间释放
这个是什么意思呢,我们来看下面代码:
#include<stdio.h>
int* test()
{
int a = 10;
return &a;
}
int main()
{
int* p=test();
printf("%p\n", p);
printf("%d\n",*p);
return 0;
}
看完后是不是感觉合情合理,但是如果你觉得它正确的话,那你就大错特错了,这里面被忽略了一个重要的问题就是,a是test函数栈里面的一个临时变量,当出test函数生命周期到了要被销毁,这时候p里面存放的是a的地址吗?当你拿这段代码在编译器上试一下,你会发现,结果是对的,只是报出一个警告,但是,结果是对的不代表你的代码是对的。这点一定要注意,这种野指针我们是一定要去避免的。
如何避免野指针
1. 指针初始化
- 小心指针越界
- 指针指向空间释放,及时置NULL
- 避免返回局部变量的地址
- 指针使用之前检查有效性
这里只是列举出一些方法,留下一个印象,真正想要避免这种问题还是得以后多多练习,积累经验。
值得强调的一点就是指针的初始化,一种就是直接有明确的指向,我就很清楚这个指针里面要存哪个地址,但是当你真的不清楚要指向哪里的时候,我们提供了一个空指针NULL,但是这个空指针其实是使用不了的,什么意思呢?我们来看:
当你用空指针初始化后,再去使用p,你会发现这个程序直接挂掉了,其实计算机本身有一块空间是专门属于操作系统的,叫做内核区,NULL的本质其实就是个0,这个地址当你去想要访问时,计算机是不会给你用的,这块空间不属于用户区。所以在初始化空指针后我们通常要判断有效性,搭配判断来使用,代码便于理解我们来看:
#include<stdio.h>
int main()
{
int* p = NULL;
if (p != NULL)
{
}
return 0;
}
指针运算
1.指针± 整数
2.指针-指针
3.指针的关系运算
1.关于指针±整数其实上面已经解释了,就是指针变量可以通过加减然后访问不同位置的数据。
2.指针-指针,这个可能我们没有接触过,我们来重点看一下:
首先,两个指针相减前提条件是指向同一块空间。否则相减是没有意义的。
#include<stdio.h>
int main()
{
int arr[10] = { 0 };
int p = &arr[9] - &arr[0];
printf("%d\n", p);
return 0;
}
看这个代码的执行结果,我们就可以分析出,它的原理其实是这样的,
这就是两指针相减的计算方法,用途呢,很明显,计算两个地址之间的元素个数嘛。
3 .指针的关系运算
指针的关系运算与比大小类似,根据高低地址来进行比较即可,但是有一个特别的规定:
标准规定:允许指向数组元素的指针与指向数组最后一个元素后面的那个内存位置的指针比较,但是不允许与
指向第一个元素之前的那个内存位置的指针进行比较。
什么意思呢?我们画个图来解释这个代码的问题:
指针和数组
指针和数组其实并没有太直接的关系,但是似乎有些相似性容易让人搞混,我们来分析一下:
指针是用来存放地址的变量,大小4/8个字节;
数组是一组相同类型元素的集合,大小取决于元素个数和元素类型;
联系:数组名是首元素的地址,可以放到指针变量里面,通过指针来访问数组里的元素
看一段代码来感受一下:
#include<stdio.h>
int main()
{
//将这个数组初始化为1~10;
int arr[10] = { 0 };
int* p = arr;
int i = 0;
int sz = sizeof(arr) / sizeof(arr[0]);
for (i = 0; i < sz; i++)
{
*(p + i) = i + 1;
}
for (i = 0; i < sz; i++)
{
printf("%d ", *(p + i));
}
return 0;
}
学到这里对指针有一定的理解了,只要我拿到第一个元素的地址,后面的元素我顺藤摸瓜一定都可以找到。
在指针和数组这块呢,其实还有一个小彩蛋:
记住,这些特殊的形式没错,但是一般不这么写,这样思考一下加深一下理解挺好,但是千万不要这样去写。
二级指针
我们都知道一个变量可以存放到指针变量里面,但是你有没有想过:指针变量也是变量,是变量就有地址,那指针变量的地址存放在哪里呢?
这时候就出现了二级指针,我们先来看一段代码:
int main()
{
int a = 10;
int* pa = &a;//一级指针变量
int** ppa = &pa;//二级指针变量
int** *pppa=&ppa;//三级指针变量
return 0;
}
这里呢初看可能就懵逼了,又来套娃,其实这样想也没错,因为按道理来说确实是可以无限套娃下去的,但是事实上即使是三级指针都很少见,所以也不用担心,熟练掌握二级指针后即可,到那时多级指针其实也不算得上障碍,逻辑基本都一样。但是今天呢,我们只是指针初阶篇,不做过多的深入学习,只是了解二级指针,知道是个什么东西,会最基本的使用即可,更加深入的放到指针进阶篇来讲。
上面这个代码其实还是得认真对待,我们来解释一下:
那么二级指针如何使用呢?
int main()
{
int a = 10;
int * pa = &a;
int * * ppa = &pa;
**ppa = 50;
printf("%d\n", a);
return 0;
}
我这里两个*的原理其实是 *(ppa)得到的是pa,而再来 *一下就相当于 *pa,这样找到的就是a,然后将它修改,这样是没有任何问题的,你也可以简记成二级指针就用两个 **。
学到这里,我们就应该清楚二级指针是用来干什么的,就是用来存放一级指针的地址。
指针数组
指针数组,顾名思义就是一个数组里面全部放置的是指针变量,例如这样:
int* arr[4] = { &a, &b,&c,&d };
那么指针数组怎么使用呢?看一段代码:
#include<stdio.h>
int main()
{
int a = 10;
int b = 20;
int c = 30;
int d = 40;
int* arr[4] = { &a, &b,&c,&d };
int i = 0;
for (i = 0; i < 4; i++)
{
printf("%d ", *(arr[i]));
}
return 0;
}
虽然这个例子有点牵强,但是仅仅是为了展示一下指针数组,我们平时肯定不会这样一个变量一个变量放的。
我们接下来举一个比较正常的例子来展示一下指针数组的用途,假设我们要用一维数组来模拟二维数组。
#include<stdio.h>
int main()
{ //创建一维数组
int arr1[4] = { 1,2,3,4 };
int arr2[4] = {5, 6, 7, 8};
int arr3[4] = { 9,10,11,12 };
//指针数组,数组名表示首元素地址
int* arr[3] = { arr1,arr2,arr3 };
int i = 0;
for (i = 0; i < 3; i++)
{
int j = 0;
for (j = 0; j < 4; j++)
{
printf("%d ", arr[i][j]);
}
printf("\n");
}
return 0;
}
好了,到这里我们的指针初阶篇就结束了,关于指针的初阶篇并不难,要对指针有初步的理解和掌握基本的使用方法,关于指针的进阶我们到后期还会继续深入学习。最后还是我们的鸡汤时间:
所谓梦想,是一想起来就可以让人热血沸腾,睡不着觉,它融入了你的血脉,成为了你精神源泉。
禁止摆烂!,禁止摆烂!,禁止摆烂!,我不断得告诉自己,我才大一,这是一句非常危险的话,因为这句话潜意识里就告诉自己不着急时间还多,但是从我入学以来,已经过完一学期了,我甚至没有一点感觉,就是前几天回过头看我才意识到时间是过的真的很快很快,但是要学的东西还有好多好多,希望看到这里的你能够和我一起勉励进步,别的就不多说了,加油!加油!加油!