屁股敲出来的东西肯定不可能多深啦,只是简单讲一下基础知识。
指针是什么?
指针和地址在本质上其实是一个东西,都是内存单元中最小单元的编号。
在生活中,外卖和快递的派送人员通过收货人留下的地址来确定目的地,如地址“XX市XX小区X号楼XX户”,这是一个确定的、具有唯一性的标识。
在编程中,每一个内存单元其实也有一个独属于自己的地址(编号),方便我们访问和使用他们。
而一个内存单元的大小刚好是一个字节,相近的内存单元的地址也是一一对应,有序可循的,相距一字节的地址差是1,形式是二进制如:
内存 | 地址(指针) |
字节 | 0XFFFFFFFF |
字节 | 0XFFFFFFFE |
… | …… |
字节 | 0X00000001 |
1.1指针变量
平时口语中说的指针,通常指的是指针变量,是用来存放内存地址的变量。
我们可以通过&(取地址操作符)取出变量的内存起始地址,把地址可以存放到一个变量中,这个变量就是指针变量。
创建一个指针变量的格式为:
数据类型* 变量名 = 初始化;
通常”数据类型“为要存储地址变量的数据类型。
#include <stdio.h>
int main()
{
int a = 1;//在内存中开辟一块空间
int *p = &a;//这里我们对变量a,取出它的地址,可以使用&操作符。
//a变量占用4个字节的空间,这里是将a的4个字节的第一个字节的地址存放在p变量
中,p就是一个之指针变量。
return 0;
}
以上例子是对于32位(X86)的机器而言的,一个地址是32byte的话,如果对每个字节进行编址,那么最多可以为2的32次方个字节编址。
注:32位机器有32根地址线,每根地址线在寻址的时候产生高电平(高电压)和低电平(低电压)就是(1或者0),组成32位二进制数。
同样的方法,64位机器,就能为2的62次方个内存单元编址,一下富裕了老多倍。
可此时也有一个问题,和32位机器不同,64位机器上地址是64byte,相对的,指针变量存储一个地址所需要的空间也翻了一番,实现了从4到8的飞跃。
最后总结一下:
指针变量是用来存放地址的,地址是唯一标示一个内存单元的。
指针的大小在32位平台是4个字节,在64位平台是8个字节
1.2指针变量的访问(解引用)
指针变量存储一个地址可不是用来看的,就像现在国家的战略储备一样,关键时刻是要拿来用的。
那地址咋用嘞?
废话,当然是用来找东西了。
通过“*”解引用操作符可以访问指针变量存储的地址的所在内存空间,对其进行访问。
int a = 1;
int* b = &a;
printf("%d\n",*a);
"*a"可以简单理解为“1”,输出结果也正是1.
int a = 1;
int* b = &a;
*p = 2;
printf("%d\n",*a);
输出结果为2,*通过指针变量p存储的地址访问到了变量a。
2. 指针和指针类型
创建指针变量的格式是“数值类型*”,不同的数据类型也说明指针的类型是怎样的,像是前面提到的”int*“就是整形类指针变量。
char *pc = NULL;
int *pi = NULL;
short *ps = NULL;
long *pl = NULL;
float *pf = NULL;
double *pd = NULL;
指针变量类型很多,和每个数据类型对应的都有,但他所被分配的内存空间永远不会改变,都是4(8)个字节,因为他存储的都是元素所在内存起始字节的地址,地址的大小是固定的,由机器决定的。
既然所有指针的大小是固定的,不像一般数据类型一样,不同类型划分不同的空间,那么指针类型的区分还有个锤子意义?
最初的设计者肯定不是我这种信球。
我们可以通过下述示例观察到不同指针类型的“步幅”。
#include <stdio.h>
int main()
{
int n = 10;
char *pc = (char*)&n;
int *pi = &n;
printf("%p\n", &n);
printf("%p\n", pc);
printf("%p\n", pc+1);
printf("%p\n", pi);
printf("%p\n", pi+1);
return 0;
}
我们观察到,char*的步幅是1字节,int*的步幅是4字节(相邻字节地址相差1)。
又有一个例子:
#include <stdio.h>
int main()
{
int n = 0x11223344;
char *pc = (char *)&n;
int *pi = &n;
*pc = 0; //重点在调试的过程中观察内存的变化。
*pi = 0; //重点在调试的过程中观察内存的变化。
return 0;
}
原本&n内:
*pc = 0;执行后:
*pi = 0; 执行后:
char*类型的指针仅对单个字节进行了更改,int*类型则改变了四个字节的内容
总结:①指针的类型决定了,对指针解引用的时候有多大的权限(能操作几个字节)。
②指针的类型决定了指针向前或者向后走一步有多大(距离)。
3.野指针
概念:野指针就是指针指向的位置是不可知的(随机的、不正确的、没有明确限制的)
野指针,也是指针界的疯狗,没有主人管着,指不定啥时候就咬你一口,非常地危险。
3.1 野指针成因
1. 指针未初始化
#include <stdio.h>
int main()
{
int *p;//局部变量指针未初始化,默认为随机值
*p = 20;
return 0;
}
*p就是个野指针,未初始化的它存储的是随机值,对它解引用无疑和对狗说人语一样操蛋。
2. 指针越界访问
#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;
}
*p这个指针飘了,超出数组范围就相当于,狗子挣脱了缰绳,野了。
3. 指针指向的空间释放
不会呢:>
3.2 如何规避野指针
1. 指针初始化
2. 小心指针越界
3. 指针指向空间释放,及时置NULL(空指针,int*p = NULL)
4. 避免返回局部变量的地址(如:在自定义函数中创建的局部变量,结束时将其地址返回,再对这个地址进行解引用是不行的,此时局部变量的生命周期已经结束,地址内就是TM的乱码)
5. 指针使用之前检查有效性
#include <stdio.h>
int main()
{
int *p = NULL;//初始化+NULL
//....
int a = 10;
p = &a;
if(p != NULL)//检查有效性
{
*p = 20;
}
return 0;
}
指针未使用前一定要初始化,没东西初始化咱可以NULL一下嘛。
注:对NULL解引用会导致程序崩溃,它是空指针,对它解引用就是老太监娶妻,无只因之谈。
4.指针运算
指针 = 地址
对地址进行加减访问不同的内存单元,这点指针同样可以做到,只不过指针是有“步幅”的。
那么,除了指针的加法运算还能怎么运算呢?
还有:
① 指针+- 整数
② 指针-指针
③ 指针的关系运算
4.1指针+-整数
举一个-整数的例子:
int a[10] = {1,2,3,4,5};
int* b = &a[3];
printf("%d\n",*(b-1))
输出结果不出所料的是“3”(a[2]的值)。
如果说+1能让int*指针地址“前移”4字节,-1则能让int*指针“倒退”4字节。
4.2指针-指针
同样把指针当成地址来理解,地址-地址会发生什么?
像是char数组a内的第三个变量地址与第十个变量地址之差,数组地址是连续的,那相差我们就可以得出a[2]到a[9]之间的"距离"。
int s =&a[2] - &a[9];
//&a[2]="00000011";&a[9] = "00001010";瞎编的!
s = 7;
在char类数组中,我们又可以说这个“距离”就是相距元素个数,当然对于其他类型数组可不敢这么说了。
但指针不亏是指针,还是和纯地址不同的,人家可是有“步幅”,char*指针一样,其他指针如int*类:
int a[10] = {1,2,3,4,5,6,7,8,9,10};
int* pa1 = &a;
int* pa5 = &a[4];
int s = pa5 - pa1;
//s == 4;
通过指针相减得出“距离”的特性可以被我们用来模拟strlen操作符。
int my_strlen(int *s)//s是数组名,ok?
{
int *p = s;
while(*p != '\0' )
p++;
return p-s;
}
4.3 指针的关系运算
指针->地址->二进制数->可比较
for(vp = &values[N_VALUES]; vp > &values[0];)
{
*--vp = 0;
}
重点是标准规定,允许指向数组元素的指针与指向数组最后一个元素后面的那个内存位置的指针比较,但是不允许与指向第一个元素之前的那个内存位置的指针进行比较。
for(vp = &values[N_VALUES-1]; vp >= &values[0];vp--)
{
*vp = 0;
}
这样就不老行了。
5. 指针和数组
数组名表示的是数组首元素的地址
int arr[10] = {1,2,3,4,5,6,7,8,9,0};
int *p = arr;//p存放的是数组首元素的地址
这样写是么有问题的,而既然可以把数组名当成地址存放到一个指针中,那我们使用指针来访问一个就成为可能。
#include <stdio.h>
int main()
{
int arr[] = {1,2,3,4,5,6,7,8,9,0};
int *p = arr; //指针存放数组首元素的地址
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的地址
那我们就可以直接通过指针来访问数组。
int main()
{
int arr[] = { 1, 2, 3, 4, 5, 6, 7, 8, 9, 0 };
int *p = arr; //指针存放数组首元素的地址
int sz = sizeof(arr) / sizeof(arr[0]);
int i = 0;
for (i = 0; i<sz; i++)
{
printf("%d ", *(p + i));
}
return 0;
}
这还不是很骚。
我们知道,操作符本质只是执行了一个操作,对于a+b而言,不论是a+b还是b+a都是一个结果它需要的只是两个操作数。
[ ]下标引用操作符也是一样的,姿势可以自行开发:
a[i+3];
*(pa+3)[i];
i[*(pa+3)];
这三个都是一个玩意。
再骚点,可以对二维数组下手,如:
int a[3][4] = {0};
int* pa = a;
//a[1][2] == *(pa + 1)[0][2] == *(*(pa + 1)+2);
6.多级指针
指针变量也是变量,是变量就有地址,指针变量的地址存放在存放在更高级的指针变量中。
一级指针最常见,如:
int a = 1;
int* pa = &a;
指针pa存储的是a的地址;
二级指针,如:
int a = 1;
int* pa = &a;
int* * ppa = &pa;
二级指针ppa存储了pa的地址,pa指向a,ppa指向pa。
三级指针,如:
int a = 1;
int* pa = &a;
int* * ppa = &pa;
int* * * pppa = &ppa;
pppa存储了ppa的地址,以下无限套娃,套几层便是几级指针。
对不同级的指针变量解引用原理基本相同,*ppa 通过对ppa中的地址进行解引用,这样找到的是 pa , *ppa 其实访问的就是 pa ,**ppa 先通过 *ppa 找到 pa ,然后对 pa 进行解引用操作: *pa ,那找到的是 a 。
int b = 20;
*ppa = &b;//等价于 pa = &b;
**ppa = 30;
//等价于*pa = 30;
//等价于b = 30;
7.指针数组
int arr1[5];
char arr2[6];
类似的:
int* arr3[7];
char* arr4[8];
最后,作者水平有限,是个新手,如有不当之处还望指正。
这里,我携鸽鸽祝大家头发永远可以梳中分:>