前言
指针相信大家都或多或少了解过一点,大多数朋友对指针都是望而却步,原因是因为指针总是很绕人,什么地址,什么解引用,什么指向的概念,又或者传参时候传指针可以改变外面的实参,多级指针啥啥的…。确实,指针是一个庞大的系统知识,晦涩难懂,不过,今天我要挑战这个回溯难懂的东西,把我对指针的理解分享出来。
我打算分四篇文章讲解指针:
- 第一篇:讲讲指针基本的内容,即这篇
- 第二篇:讲讲指针和数组那些故事,数组指针,指针数组,还有字符指针等
- 第三篇:讲讲指针和函数的那些是,函数指针,指针函数,指向函数指针的数组等
- 第四篇:是一些指针笔试面试题,等;
大概是这样啦
提示:本指针都是在32位的环境测试下来的。
简易的内存模样
要了解指针,不得不先了解内存这个东西,形象的感官理解内存;
从实体来看就是一个计算机的硬件设备,
而虚拟的来看就是一个由多个格子组成的长方形。
每个格子都有自己的名字,我们把每个格子的名字叫内存单元;而格子的标识就是内存编号
❓问:那内存编号怎么来的呢?
这是由计算机硬件设计所的来的,在我们32位计算机中,计算机的设计者设计了32根地址线,连着内存,而32根线中,每一根线都有两种状态,有点和没电,在计算机里面用,1表示有电,0表示没电;那么在有32根线,,每一个线有两种状态,那么就可以这么想:
1根线, 可以表示 1 或者 0,就有2个状态;就是 21
2根线,可以表示00 01 10 11 就有4个状态;就是22
3根线,可以表示 000 001 010 011 100 101 110 111 就有8个状态;就是23
。。。。。。。。。。。。。。。顺着思路下来。。。。。。。。。。。。。。。
32根线,就是 232个状态呀;
看图:这就是每个格子的内存编号;
那我们直到每个格子都用二进制去表示,显得太长了;所以一般我们内存编号都用16进制来表示,就是把2进制转化位16进制;如图
这就简单了好多
❓问:那内存的表示范围是多少?
在32位就是 0x00 00 00 00 到 0xff ff ff ff;也就是寻址范围:0 — 232 ,用平时得理解就是4G内存大小
❓问:内存中每个格子是什么?格子的大小有是多少?
内存中每个格子我们叫做内存单元;每个格子是1个字节;
那这就引出来我们把内存单元对应的内存编号叫做内存单元的编号,那内存单元的编号叫做地址;
❓问:内存单元里面是什么?
内存内存里面就是你要存放的数据;
❓问:内存单元编号有啥作用?
通过内存单元编号,我们可以找到每个一内存单元的地址,那就可以知道地址对应的内存单元里面的数据;
❓问:如何形象的理解这种关系呢?
可以把内存类比酒店, 每个格子类比于每个房间,每个内存编号类比于每个房间的门牌号;
这就说明一个问题,你要找到房间里的人,可以通过门牌号找到,类比于,你要找到内存的数据,可以通过内存编号找到;
说了那么多,其实最主要的就是 :
- 格子 = 内存单元,其大小为1byte ;
- 内存编号 = 地址,在32位的字节大小位 4byte,并且我们能通过地址找到内存单元里面的数据。
在《C和指针》92页中说到,对于内存,我们暂时感兴趣的:
- 内存中的每一个位置由独一无二的地址标识。
- 内存中的每个位置都包含一个值。
指针
我们大家都知道一句话指针就是地址,地址就是指针;那这句话怎么由来的呢?其实就是很形象的理解,因为,我们可以通过内存编号,也就是地址,也就是内存每一我位置独一无二的地址标识,找到内存单元,可以说,地址指向了该内存单元,所以我们形象的说地址就是指针,指针就是地址。
int a =10;
int *p = &a;
*p = 20;
解释代码:
这里 a
就是一个变量,由于int
类型,所以占用4个byte;
int *p = &a
理解:*
标识p
是一个指针,int
表示指针p
指向的类型;
指针p
也是一个变量,里面存放的是变量a
的地址;变量都有自己的地址所以说,p也有自己的地址;要区分指针的p
的地址和指针p
执行a
的地址不是同一个东西。
*p = 20
;表示间接访问变量a
,把a
修改为 20;所以说 *p
就表示 a
;我们通常说解引用p
得到变量a
由于p
存放的是地址,而地址又是32位的,所以指针p
的大小是4个字节;
这里我想说明的是:
指针存放的永远是地址;
并且我还相说明一个问题:
在定义指针时候,无论你定义多少级的指针,只有一个星号*表示这个变量是一个指针,剩下的都是指针指向的类型; 请你记住这句话,下面可以帮你你理解更加复杂的指针声明,和看懂指针。
指针的大小和指针类型
我们说指针的大小是4个字节(在64位是8个字节)如何验证呢?
# include<stdio.h>
int main()
{
printf("%d\n", sizeof(int*));
printf("%d\n", sizeof(char*));
printf("%d\n", sizeof(double*));
printf("%d\n", sizeof(float*));
return 0;
}
//这里的指针类型都是不一样的,假如我得到的结果都是一样的,就说明指针的大小都是固定的。
结果都是4;说明指针大小都是4个字节。
❓问:既然来说指针大小是4个byte,那么指针类型到底有什么意义呢?
指针类型的意义:
决定了指针可以访问的字节数,也说明就说明指针在解引用时所能访问的空间大小;如:
char*
表示指针每次访问是以1byte 去访问内存字节数;
int*
表示指针每次访问是以4byte 去访问字节数;
# include<stdio.h>
//指针类型的意义:说明就说明指针在解引用时所能访问的空间大小
int main()
{
int a = 10;
char* p = &a;
*p = 20;
return 0;
}
这用一个char*
指针指向了int
类型的变量,通过*p
试图改变 变量a
,请问能够改变呢?也就是说a的值,是否是20呢?
答案:是可以的,但是访问的却是一个字节的内容,假如数字更大超过一个字节的大小,这样改变就不会得到预期的结果了,这种方式是不可取的。
如下图:没改变前 a的值
改变后 a
变红的地方只有一个字节,说明char*
指针类型访问改变了一个字节的内容;
但是这种行为我们是认为不可取的,你定义了什么类型的指针,就应该执行什么类型的地址,比如,你定义了 int a = 20; 你想定义一个指针 p,你的指针p的类型就应该设置为 int * ,不因该设置为其他类型的;
但是,这种行为也恰恰说明了指针的强大,可以精确的访问到内存的字节单元,内存的 每一个单元的内容,指针都可以窥探
指针加减整数
既然指针的类型意义是决定了指针能访问的字节数,指针解引用所能访问空间的大小;那指针加减整数时候就可以表示指针向后向前移动了多少步长
比如:
- int* p ; p+1 表示指针向前移动了4 个字节;
- char* p ; p+1 表示指针向前移动了 1 个字节;
- double* p ; p+1 表示指针向前移动了 8 个字节;
- float* p ; p+1 表示指针向前移动了 4 个字节;
# include<stdio.h>
//测试指针加减整数的意义
int main()
{
int a = 10;
int *p = &a;
//%p的形式是打印指针的地址,以十六进制未数显示
printf("%p\n", p);
printf("%p\n", p + 1);
char ch = 'a';
char *p1 = &ch;
printf("%p\n", p1);
printf("%p\n", p1 + 1);
return 0;
}
//注意:指针不单单是只能加1,也是可以加2,加3,减1,减2,等整数滴。主要是看你的程序需求而定。
如下图:
上面的例子也可以侧面验证:指针类型决定了访问字节数是多少,指针+1,由于是 int*
类型的指针,访问的字节就为4
个,由于是char*
类型的指针,访问的字节就是1
个。
既然指针+整数会使得地址向后移动,那么指针-整数就是向前移动,朋友们可以类比以下就好。
指针和指针相减
(前提:指针和指针的类型相同)指针和指针相减是得到指针之间有多少个元素个数,因为指针相减会得到地址的差值,这个差值就可以表示元素的个数啦。指针是不可以相加的,因为指针和指针相加时没有啥意义呀。得到的数就是一个更大的地址,是没有什么用的,而且语法也不允许;
int my_strlen(char *s)
{
char *p = s;
while(*p != '\0' )
p++;
//退出循环后,指针p指向‘\0’;而s指针还是指向字符串的首元素
return p-s;
}
int main()
{
char* s = "abcdef";//指针s指向字符串的首地址,
int ret = my_strlen(s);
return 0;
}
在监视中,我们可以看到,指针 - 指针的值,就是 abcdef 元素的个数 6 ;
如图:
具体,指针减指针有什么用,到你以后深入时候,有需求时候,自然会用上。
一些指针++和指针- -的运算操作解释
比如有时候我们看别人的代码有这个语句:
*p++;
*p--;
*++p;
*--p;
这怎么理解呢?
首先
*p++:是先使用解引用p,然后指针p加1;
*p--:是先使用解引用p,然后指针p加1;
*++p:先指针p加1,然后解引用指针p;
*--p:先指针p减1,然后解引用指针p;
++*p:
指针和数组的关系
我们知道数组名就是数组首元素的地址;如何理解呢?
看看一段代码:
代码就是打印一维数组数组:
int arr[10] ={0};//初始化十个元素都为0
for (int i = 0;i<10;i++)
{
printf("%d ",arr[i]);
}
我们都会这种打印数组的方式,这是通过下标操作符 [ ],去访问数组中的每一个元素;
但是,接下来这种方式也可以打印数组:
//第一种
int arr[10] ={0};//初始化十个元素都为0
for (int i = 0;i<10;i++)
{
printf("%d",*(arr + i));
}
//第二种
int arr[10] ={0};//初始化十个元素都为0
int * p = arr;
for (int i = 0;i<10;i++)
{
printf("%d",*(p + i));
}
这种通过数组名的方式也可以访问数组的每一个元素,原理是什么呢?
本质就是数组名就是首元素的地址;地址是什么?地址就是指针,指针可以干嘛?指针可以加整数,加整数有什么用?加整数可以跳过一定的步长到达下一个位置,到达下一个位置了然后呢?就可以解引用该位置的指针去访问指针指向的变量了,这里的变量就是数组中的元素。
所以说,对于数组来说,数组名表示首元素的地址,那么就有下面的等价关系;
arr == &arr[0];
arr+1 == &arr[1];
arr+i == &arr[i];
那么,当我解引用时候得到数组下标对于的值:
*arr = arr[0];
*(arr+1) == arr[1]; 注意:这里*解引用的(arr+1)
*(arr + i) == arr[i];
也就是说
*arr + 1 == arr[0] +1; //注意:这里*解引用的arr
如下图:
看看另一代码:
int main()
{
int arr[10] = { 0 };
printf("arr的地址 %p\n", arr);
printf("arr+1的地址 %p\n", arr+1);
printf("arr[0]的地址 %p\n", &arr[0]);
printf("arr[0]+1的地址 %p\n", &arr[0] + 1);
printf("&arr的地址 %p\n", &arr);//注意是取&arr
printf("&arr+1的地址 %p\n", &arr + 1);
return 0;
}
这说明
arr = &arr[0]; arr != &arr ; &arr+1表示加整个数组大小的步长;而arr+1表示加 一个数组元素的步长;
注意:当我们取地址数组名,即**&arr时候,得到的是整个数组的地址,不是首元素的地址**;这个在后面深入文章会谈到这个问题。现在有个理解即可
野指针
野指针就是指针指向的位置是不可知的(随机的、不正确的、没有明确限制的)。这是不好的行为,我们要避免它;
看一段代码
int main()
{
int *p;//没有初始化的指针变量p,里面存放的是随机值
*p = 20;//通过随机值地址找到这份空间,在这段空间存放20,
//这就是一种非法访问,因为你找到随机值地址的空间,并不是这个程序所开辟的。
return 0;
}
//这里的p指向的位置是不可知道的,所以p是个野指针。
这段代码有很大的问题,首先指针p是一个变量,变量就会开辟内存空间,指针p里面存放的是什么呢?是指针p所指向的内存空间,但是指针p并没有指向任何空间,并且指针p又是有内存空间,这就说明有值的,只不过这个值是随机值,随机值那就说明这个指针p指向的空间就是这个随机值所代表的地址,那么你对一个随机值地址进行解引用就会发生错误,因为你并不知道这个指针p存放的地址到底是哪里,假如这个指针p存放的是你重要的信息的变量的地址呢?
那你还解引用得到这个变量的地址去修改它的值,那就会出大问题;
看看另一段代码:
int arr[10] = {0};
int *p = arr;
for (int i = 0;i<=10;i++)
{
printf("%d",*(p + i));
}
乍一看上面的代码好像没什么问题;可是你发现没有,这个循环变量条件i<=10
,当i = 10时候,在进去循环体,这个时候*(p + i)
就等于*(p + 10)
;很明显,这是越界数组访问的情况,越界访问数组,那段空间又不是你程序所开辟的空间,这也是会导致野指针的出现;所以这个 p+10 就是野指针,并且解引用*(p + 10)得到的值也是随机值,会发生错误;如下图:
看看另一段代码:
#include <stdio.h>
int* test()
{
int a = 10;
return &a;
}
int main()
{
int *p = test();
printf("%d",*p);
return 0;
}
解释:
这段代码本意是用指针p
接收test
函数返回的地址,则指针p
就指向test
函数内部的局部变量a
的地址,然后解引用*p
,间接打印变量a的值,但这是错误的用法,会导致指针p
变为野指针;
分析指针p成为野指针的原因:
我们知道局部变量的声明周期是函数结束的时候,也会自己挂掉,尽管test函数中局部变量a
在挂掉之前把变量a的地址交给了指针p
,一旦test
函数结束后,变量a就没了,那就说明变量a
的空间内容被系统回收了,就说明变量a的内容不在是以前的10了,也就是说,虽然你指针p
有a
的地址,但是没有a
的内容了,那么你通过指针解引用访问a
的内容,就是得到垃圾值,并且这个地址已经不是你的了,被系统回收了,就会导致指针p
成为野指针。
所以结论:
就是我们尽量不要返回局部变量的的地址,就像上面的例子一样,会得到垃圾值。
二级指针
二级指针也是指针,只不过存放的是一级指针的地址;一个*
表示 一级指针,两个**
表示二级指针;
比如:
int a = 10;
int *p = &a;
int **pp = &p;
在后面文章深入时候,我们会探讨,指针作为参数到底怎么思考传参的问题,如,什么时候接设计为一级指针二级指针,他们可以接收什么参数,从设计函数的角度思考,和从调用者的角度思考这个问题。
一般二级指针用在二维数组,还有一些需要修改一级指针指向时候会用到,比如链表的指向,二叉树的结点等。。。这些以后你会慢慢接触到的。
这里透露一点书写指针的技巧,到时候,设计函数指针参数时候,也会用到这种思想方式。
指针的书写,其实是有技巧的
这都是本人总结出来的技巧,越到后面越复杂的指针类型声明,书写,阅读时候,你就会发现这些技巧越有用。
你需要有的认知:
- 等号=两边的类型是必须相同的,(除非有隐式转换,或者你可以显示转换)。
- 要得到一个变量的类型,去掉变量名字,等号左边剩下的就是变量类型
- 要得到一个指针的类型,也是去掉指针名字,等号左边剩下的就是指针的类型
- 要得到一个指针指向变量的类型,去掉指针名字和一个*星号,等号左边剩下的就是指针指向变量类型
- 要得到一个数组的类型,去掉数组的名字,等号左边剩下的就是数组的类型
- 要得到数组里元素的类型,去掉数组名字和[ ] 中括号,等号左边剩下的就是数组里元素的类型
- 定义指针变量时候,一个*星号就可以代表该变量为指针变量了
有了上面的认知,你就可以看下去,虽然很多不理解什么意思,但,等到后面复杂的指针时候,我会解释的。
比如 int a = 10; 我想写一个指针指向a,如何书写呢?
大多数人都可以不用思考的写出来:
int *p = &a;
可以这么解读:p是一个指针变量,指向一个类型为int的变量a;
这是一个比较简单的指针,确实可以不用思考写出来,但是假如我们碰到一个比较复杂的例子:
如: int* arr[10] = {0};我想你书写一个指针,能够指向该数组的指针,那你是如何书写呢?
我们就讲解一下书写技巧吧,就上面最简单的 int a = 10;
1.首先我们要有人认知:要得到一个变量的类型,去掉变量名字,等号左边剩下的就是变量类型
所以对于 int a = 10; 去掉 a后,等号左边剩下的就是 int ,这就是变量 a 的类型;
2.其次:在书写指针,时候,必须有 * 星号,这是指针变量的标识,
所以我们先会 毫不犹豫在等号左边先书写 *pa =
3.这时候,来到了重点,我们知道a的变量类型是 int ,所以 我们在 *pa = 这个表达式中,*星号的左边填上 int;那么表达式为 int *pa = ;
4.最后,这时候等号=右边直填上 &a就可以啦!
所以最终的结果为 int *pa = &a;
也许看着步骤有点啰嗦,可是到后面复杂的指针书写时候,你就发现,真是很简单的事情。
到时候还会有阅读指针的方法,带你阅读复杂类型声明。
更多级别的指针也可以这么书写;很实用。