目录
指针是什么?
指针和指针类型
指针+-整数
指针的解引用
野指针
野指针成因
如何规避野指针
指针运算
指针+-数组
指针-指针
指针的关系运算
指针和数组
二级指针
指针数组
在计算机科学中,指针(Pointer)是编程语言中的一个对象,利用地址,它的值直接指向(points to)存在电脑存储器中另一个地方的值。由于通过地址能找到所需的变量单元,可以说,地址指向该变量单元。因此,将地址形象化的称为指针。意思是通过它能找到以它为地址的内存单元。
那我们就可以这样理解为:
内存
内存 一个字节 0XFFFFFFFF 一个字节 0XFFFFFFFE 一个字节 ... 一个字节 0X00000002 一个字节 0X00000001 一个字节 0X00000000 指针
指针就是个变量,存放内存单元的地址(编号)
下面我们用代码来解释一下
#include
int main() { int a = 10;//在内存中开辟一块空间 int *p = &a;//这里我们对变量a,取出它的地址,可以使用&操作符 //将a的地址存放在p变量中,p就是一个指针变量,简称指针。 return 0; } 总结:1.指针就是变量,它是一个用来存放地址的变量。
2.指针即地址,地址即指针。
那这里有两个问题:
一个小的单元到底是多大呢?(1个字节)
如何编址?
经过仔细的计算和权衡我们发现一个字节给一个对应的地址是比较合适的。
对于32位的机器,假设有32根地址线,那么假设每根地址线在寻址的是产生一个电信号正电/负电(1或0)
那么32根地址线产生的地址就会是
00000000 00000000 00000000 00000000
00000000 00000000 00000000 00000001
...
11111111 11111111 11111111 11111111
这里就有2的32次方个地址
如果每个内存单元(地址)是1bit大小的话,那么我们就可以给(2^32bit == 2^32/8Byte ==2^32/8/1024KB == 2^32/8/1024/1024MB == 2^32/8/1024/1024/1024 ==0.5GB)的空间进行编制,但是我们仔细想想一个char类型是1个字节,那它就需要8个地址,这样岂不是太浪费了。
经过仔细的计算和权衡我们发现一个字节给一个对应的地址是比较合适的。
每个地址标识一个字节,那我们就可以给(2^32Byte == 2^32/1024KB == 2^32/1024/1024MB ==2^32/1024/1024/1024GB ==4GB) 4G的空闲进行编址
这里我们就明白
在32位的机器上,地址是32个0或者1组成的二进制序列,那地址就得用4个字节的空间来存储,所以一个指针变量的大小就应该是4个字节。
在64位的机器上,地址是64个0或者1组成的二进制序列,那地址就得用8个字节的空间来存储,所以一个指针变量的大小就应该是8个字节。
总结
指针是用来存放地址的,地址是唯一标示一块地址空间的。
指针的大小在32位平台是4个字节,在64位平台是8个字节。
我们都知道,变量有不同的类型,整型,浮点型等等。那指针有没有类型呢?准确的说:有的。
当有这样的代码:
int num = 10; p = #
要将&num(num的地址)保存到p中,我们知道p是一个指针变量,那它的类型是怎样的呢?我们给指针变量相应的类型。
char *pc = NULL; int *pi = NULL; short *ps = NULL; long *pl = NULL; float *pf = NULL; double *pd = NULL;
我们这里可以看到,指针定义的方式是:type + *。其实:char *类型的指针是为了存放char 类型变量的地址。short *类型的指针是为了存放short 类型变量的地址。 int *类型的指针是为了存放int 类型变量的地址。
#include
int main() { int n = 10; char *pc = (char*)&n; int *p = &n; printf("%p\n", &n); printf("%p\n", pc); printf("%p\n", pc+1); printf("%p\n", p); printf("%p\n", p + 1); return 0; } 总结:指针的类型决定了指针向前或者向后走一步有多大(距离)
int *指针+1跳过4个字节
char *指针+1跳过1个字节
#include
int mian() { int n = 0x11223344; char *pc = (char *)&n; int * p = &n; *pc = 0; *p = 0; return 0; } 通过调试我们发现了如下变化
总结:指针的类型决定了对指针解引用的时候有多大的权限(能操作几个字节).比如:char *的指针解引用就只能访问一个字节,而int *的指针解引用就能访问四个字节。
概念:野指针就是指针指向的位置是不可知的(随机的、不正确的、没有明确限制的)
1.指针未初始化
#include
int main() { int *p;//局部变量指针未初始化,默认为随机值 *p = 20; return 0; } 通过p中存的随机值作为地址,找到一个空间,这个空间不属于我们当前的程序,就造成了非法访问,p就是野指针。
2.指针越界访问
#include
int main()
{
int arr[10] = { 0 };
int *p = arr;
int i = 0;
for (i = 0; i <= 10; i++)
{
//当指针指向的范围超出数组arr的范围时,p就是野指针
*(p++) = i;
}
return 0;
}
越界可以,但是不能够越界访问。
你可以指向越界的那个地址,但是你不能够去访问越界的那块内容。
举个例子就比如说:你可以问别人银行这个地方在哪里,甚至是在银行附近瞎转悠。
但你对银行有非分之想,想进银行抢钱这是不行的。
3.指针指向的空间释放
#include
int* test()
{
int a = 10;
return &a;//int*
}
int main()
{
int *p = test();
printf("%d\n", *p);
return 0;
}
a是test函数里面创建的局部变量,我们都知道局部变量的生命周期是:进入作用域生命周期开始,出作用域生命周期结束。当调用完test函数后,a就会被销毁,空间还给操作系统,虽然说会返回一个地址给p,但是p指向的那个地址已经被释放了,已经不属于我们了,当我们通过p去访问这块空间的时候就会造成非法访问,所以这里的p是一个野指针。
当我们运行程序的时候发现:
结果是10
这是为什么呢?
原因是a变量所占的空间被回收后操作系统还未将其销毁,编译器会对其作一次保留。
虽然说执行程序的结果是10,但是p指向的空间已经被释放,能打印10是因为这块空间被释放后里面的值还没有被覆盖。
举个生活中的例子来解释吧
小李今天晚上不想回家了,然后就找了附近的一个宾馆住下了。交了钱只住一个晚上,第二天上午退了房,小李把这个房间号给记住了。过了几个小时,小李又去了这个宾馆找到了这个房间。他发现房间里面还是和他住的时候一模一样,之所以一模一样是因为保洁阿姨还没有对这个房间进行打扫。但是由于小李已经退了房,这个时候他就不能再回这个房间住了。
当我们在上面的代码里面加入一行代码后:
我们发现此时的值不再是10了。这是因为这次调用printf函数,会使得原来分配给a的空间被覆盖掉,p指向空间的值被覆盖了从而打印的结果便不再是10了。
1.指针初始化
int main()
{
int a = 20;
int *p = &a;//明确指向
int *pc = NULL//当不知道一个指针当前应该指向哪里的时候,可以初始化为NULL
return 0;
}
2.小心指针越界
3.指针指向空间释放后立即置为NULL
4.避免返回局部变量的地址
5.指针使用之前检查有效性
int main() { int *p = NULL; *p = 100;//err //空指针是不能直接解引用操作的 return 0; }
那既然NULL不能使用,为什么我们还要把它设置为NULL呢?
这是为了提醒我们,这是一个空指针,是不能使用的。
这就好像你在路边看到一条野狗,可能没有主人没有人牵着绳子身上脏兮兮的,这个时候它可能会去伤害别人。这个时候有好心人将它抓住了之后将它拴在了树上,并立了一块告示牌说:此处有野狗,请大家绕行。这时你只要避开它走,它就不会伤害到你。但是你非要从它旁边走过去并且在它旁边玩耍,这个时候你就有可能会被它给咬伤。
我们都知道日期+(或者-)天数还是日期,日期-日期为天数,但是日期+日期是没有意义的。通过类比我们可以得出:指针加减整数还是指针,指针减去指针为元素个数,但是指针+指针是没有意义的。
#define N_VALUES 5 int main() { float values[N_VALUES]; float *vp; //指针+-数组:指针的关系运算 for (vp = &values[0]; vp < &values[N_VALUES];) { *vp++ = 0; } return 0; }
++的优先级高于*的优先级
所以在循环体内会先执行++再*,但是由于这里是后置++(先使用再++)
所以这里就好像是先解引用再++。
当vp指向数组最后一个元素其后的地址,不满足条件结束循环。
正如我们上面说的:你可以指向越界的那块地址,但是你不能够访问越界的那块内容。这里没有访问越界的内容,所以就不算越界访问。
int main() { int arr[10] = { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 }; int *p = &arr[3]; printf("%p\n", p); printf("%p\n", p - 1); return 0; }
这里我们就可以知道指针+-整数就是指针向后或者向前跳过整数个该类型大小的字节。
int main() { int arr[10] = { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 }; printf("%d\n", &arr[9] - &arr[0]); printf("%d\n", &arr[0] - &arr[9]); return 0; }
你认为运行结果是什么呢?
答案是9 -9
语法规定:指针-指针,得到的是两个地址之间的元素个数(下标相减)
那么要是在不同的数组中运算又会如何呢?
int main() { int arr[5] = { 1, 2, 3, 4, 5 }; char ch[] = { '0', '3', '5' }; printf("%d\n", &arr[4] - &ch[2]); }
这里编译器不会报错,因为语法没有错误。但是所得到的数组即为元素的个数,那该元素是int类型还是char类型呢?所以这些数字根本是没有意义的。
所以指针-指针的前提是:两指针指向同一块区域
指针-指针得到的数字的绝对值是指针和指针之间元素的个数。
那么指针-指针可以应用在哪里呢?
应用:实现strlen函数
#define N_VALUES 5 int main() { float values[N_VALUES]; float *vp; for (vp = &values[N_VALUES]; vp > &values[0];) { *--vp = 0; } return 0; }
我们可以将上面的代码进行简化
#define N_VALUES 5 int main() { float values[N_VALUES]; float *vp; for (vp = &values[N_VALUES-1]; vp >= &values[0];vp--) { *vp = 0; } return 0; }
实际在绝大部分的编译器上是可以顺利完成任务的,然而我们还是应该避免这样些,因为标准并不保证它可行。
标准规定:
允许指向数组的指针与指向数组最后一个元素后面的那个内存位置的指针比较,但是不允许与指向第一个元素之前的那个内存位置的指针进行比较。
指针和数组有什么关系呢?
数组——是一块连续的空间,放的是相同类型的元素,数组大小和元素类型,元素个数有关系。
指针(变量)——是一个变量,是用来放地址的。
指针变量的大小是4(32bit)/8(64bit)个字节
数组是什么?我们来看一个例子:
int main() { int arr[10] = { 1, 2, 3, 4, 5, 6, 7, 8, 9, 0 }; printf("%p\n", arr); printf("%p\n", &arr[0]); return 0; }
运行结果为:
可见数组名和数组首元素地址是一样的。
结论:数组名表示数组首元素的地址。
敲黑板:数组名确实是首元素的地址,但是有两个例外!!!
1.siezof(数组名)--这里的数组名不是首元素的地址,是表示整个数组,这里计算的是整个数组的大小,单位还是字节。
2.&数组名--这里的数组名不是首元素的地址,是表示整个数组的,拿到的是整个数组的地址。
既然可以把数组名当成地址存放到一个指针中,我们使用指针来访问数组中的元素就成为可能
例如:
int main() { int arr[10] = { 1, 2, 3, 4, 5, 6, 7, 8, 9, 0 }; int *p = arr;//指针存放数组首元素的地址 int i = 0; 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[10] = { 1, 2, 3, 4, 5, 6, 7, 8, 9, 0 }; int *p = arr;//指针存放数组首元素的地址 int i = 0; int sz = sizeof(arr) / sizeof(arr[0]); for (i = 0; i < sz; i++) { printf("%d ", *(p + i)); } return 0; }
指针变量也是变量,是变量就有它的地址,那么指针变量的地址存放在哪里呢?
答案是:二级指针。
int main() { int a = 10; int *pa = &a; int* *ppa = &pa; return 0; }
对于二级指针的运算有:
*ppa通过对ppa中的地址进行解引用,这样找到的是pa,*ppa其实访问的就是pa。
既然有一级指针,二级指针,那么有没有三级指针呢?
答案是有的。
红色框后面的 * 表示该变量是一个指针变量。
一级指针p前面的int表示p指向的对象a是int型的。
二级指针pp前面的int*表示pp指向的对象p是int*型的。
三级指针ppp前面的int**表示ppp指向的对象pp是int**型的。
对于上面的代码,我们可以进行如下分析:
对于一级指针p解引用*p,找到a。
对于二级指针pp解引用*pp,找到p,再解引用**pp,找到a。
对于三级指针ppp解引用*ppp,找到pp,再解引用**ppp,找到p,再解引用***ppp,找到a。
指针数组是指针还是数组呢?
答案是:是数组。是存放指针的数组。
数组我们已经知道整型数组,字符数组
int arr1[5]; char arr2[6];
那指针数组是怎样的呢?
int *arr3[5]//是什么?
arr3是一个数组,有5个元素,每个元素是一个整型指针
对于初阶指针的内容就讲到这里啦,如果觉得对你有帮助的话,可以点赞关注一波哦!后续会更新指针进阶的内容。