前言:
对于许多正在学习C语言的小伙伴来说,指针可能会让你非常的头疼,很多人不知道如何控制指针变量,甚至都不敢用指针来写代码。但是在实际的开发中还是经常会和指针打交道的,今天我们开启C语言指针系列的章节学习~
指针是什么?这个我们先不着急,我们先来模拟一个场景:
[事件1] 现在,假设你今天要去西藏旅游,到了地方总得有住的地方,于是你去希望旅馆定了一个房间号为302的房间。你是第一次来这个旅馆并不熟悉房间的排列,所以你得一个一个的去找到你的房间。你按着顺序找到了302的房间,并将行李放了进去… [结束]
现在就可以回答前面的问题了,指针其实就是地址,事件1的房间号就可看作地址,有了房间号,我们就可以找到对应的房间,将行李都放进房间了。指针也是如此:指针通过地址进而找到对应的内存空间从而进行访问。
对于指针:
指针是内存中一个最小单元的编号,也就是地址。
注意: 我们平常所说的指针,其实是指 指针变量,是用来存放地址的 变量。
我们也可以这样来理解指针:
通过指针的地址来找到对应的内存空间。
我们已经了解了指针就是地址,那么地址又是什么?实际上:
在计算机运行时,数据会存放在内存中,内存会以 字节 为单位划分为多个存储空间,并且为每个字节默认设置一个对应的编号,这个编号就是地址。
可能你还会有疑问:“为什么内存会以字节为单位划分呢?”
其实经过前人的计算与考量,发现一个字节给一个对应的地址是比较合适的。在32的机器上,假设有32根地址线,每根地址线在寻址的时候产生的高低电平就是0和1。
那么32根地址线产生的地址就会是:
00000000 00000000 00000000 00000000
00000000 00000000 00000000 00000001
00000000 00000000 00000000 00000002
…
111111111 111111111 111111111 111111111
这里就会产生2^32 次方个地址。如果每个地址来标识一个字节,那么我们就能给大约4GB(2^32 Byte = = 2^32 /1024KB = = 2^32/1024/1024MB= =2^32/1024/1024/1024GB = = 4GB)的的空间进行编址了。同样的方式,64位机器能编址多大空间?可以自己算算。
这里我们就知道了:
1、在32位机器上,地址是32个0或1组成的二进制序列,地址有 4个字节 的空间的大小来存储,所以在32位机器下,一个指针变量的大小就是4字节。
2、在64位的机器上,有64个地址线,一个指针变量的大小就是8个字节了。
还记不记得我们在学C语言中用到的 ‘&’ 符号?是不是很眼熟?没错,我们经常在scanf函数里面用到这个符号,其实这个符号叫做: 取地址操作符 。顾名思义,就是用来提取变量的地址。
我们可以通过& 来取出变量的内存地址,把地址可以存放到一个变量中,这个变量就是指针变量。
#include
int main()
{
int a = 10;//整形大小为4个字节
int *p = &a;//取变量a的地址赋给指针变量p,虽然整形大小为4个字节
//但是指针存储的仅仅是四个字节中的第一个起始字节
return 0;
}
总结:
1、 指针变量是用来存放地址的
2、 在32位平台下,指针大小为4字节,在64位平台下,指针大小为8字节
看到标题你可能会有些疑问:既然我们的指针只能保存一个字节的内容,我们为什么还要给指针分为不同的类型呢?
实际上,我们规定指针这样定义:
int a = 0;
int *p = &a;
float b = 0;
float *pb = &b;
double c = 0;
doublr *pc = &c;
//...
我们可以看到,指针变量的 定义方式为:类型 + * 而指针前面的类型表示指针的类型,我们可以看到,指针的类型有,int,double,float…我们常用的类型都有对应的指针类型。
那这些指针类型究竟有什么用呢?代表什么意思呢?
我们来看这样一段代码:
#include
int main()
{
int a = 1;
char *pa1 = (char *)a;//既然指针只保存一个字节的值,那我们不妨直接把int强转成char
//只取int的首个字节的地址进行操作看看会发生什么?
int *pa2 = &a;//将未强转的类型也用指针保存,用来做对照
printf("%p\n", &a);//地址打印用%p
printf("%p\n", pa1);
printf("%p\n", pa1 + 1);
printf("%p\n", pa2);
printf("%p\n", pa2 + 1);
return 0;
}
那么结果会是多少呢?
我们发现第一个与第二个和第四个的打印结果是相同的,也就是说他们的起始地址是相同的,第一个和第四个就不用多说,两个是同一个变量取地址打印。
第二个结果也刚好能验证我们指针取的地址是元素的首个字节的地址。
可以看到,pa1 + 1的地址要比pa1的地址大了1(16进制),也就是说pa1向后加一就是往后走一个字节的距离。
再来看pa2与pa2 + 1,这里的差值却为4(十六进制10 - 0C),也就是说pa2加一是跳过了4个字节。我们发现,他们跳过的字节数刚好和指针对应的类型大小相同!这里我们就可以得出结论:
指针类型决定了指针向前或向后走一步的步长(距离)
我们已经知道了指针如何在内存中工作的,那么我们该如何将指针给用起来呢?其实啊,我们有了变量的地址,保存在指针变量里,接下来就是放行李的过程,也就是对内存空间进行访问。
[事件2] 你打开了302房间的房门,刚走进去,不禁皱起了眉头,里面的杯子还是乱的,垃圾桶还没清理,甚至地下还有垃圾,你直接去找了酒店前台,前台十分抱歉,于是叫来了保洁阿姨,很快的,你的房间就焕然一新了,上个房客剩下的东西统统清理干净,随后将你的东西放进角落…[结束]
指针的意义就是为了来管理我们的内存,在C语言中用指针来访问内存有一个专门的运算符:*(解引用运算符) ,这里的解引用,就可以对指针指向的内存空间随意访问啦。
用法为:
int a = 0;
int *p = &a;//正常取变量地址
*a = 1;//这就是对指针所指向的内存空间进行访问
//也就是说,指针可以通过解引用来更改变量a的内容
这里将原来的0通过指针解引用改变为了1,酒店里你发现订的房间居然很乱?现在是你要住进来,你可不管之前住的是谁。这就是通过对指针解引用,来访问内存,可以对于原来的值进行修改。
注意:初学者总是会搞错指针类型的大小与指针所指向变量的大小关系,指针的 大小永远为4/8个字节。我们来看下面例子:
#include
int main()
{
printf("%d\n",sizeof(int *));//sizeof对不同的指针类型求大小
printf("%d\n",sizeof(char *));
printf("%d\n",sizeof(short *));
printf("%d\n",sizeof(long *));
printf("%d\n",sizeof(float *));
printf("%d",sizeof(double *));
//...
return 0;
}
得出的结果为:
其实说白了指针就是地址,指针可不管你是int、还是double还是什么类型,到我这里都是地址,指针的类型大小是跟指针所指向的类型无关,我的机器为64位机器,所以我的指针大小就一定是8个字节。
我相信你还有一些疑问:“还是那个问题,既然指针只需要一个字节的地址,那为什么还要分什么类型,我全都是char *不就完了吗?”。
其实上面我已经解释了为什么指针需要类型,这里在从解引用的角度来分析一下,我们来看下面的例子:
#include
int main()
{
int a = 0x44332211;//这里不是地址,而是16进制的数字进行赋值
int *p = &a;//整形指针取地址
printf("%x\n",*p);//以十六进制形式打印
char *pa = (char *)&a;//字符指针取地址
printf("%x\n", *pa);//十六进制打印
printf("%x\n", *(pa+1));//指针向下一个位置访问
return 0;
}
由打印结果我们可以看到:不同类型的指针使用解引用而访问到不同的字节数,这里char*指针只访问了变量a的一个字节,而int*指针访问了变量a的4个字节。
总结:
指针的类型 决定了指针解引用时候有多大权限(访问几个字节数)
哎呀,指针真好用!我有了谁的地址,我就可以随便来玩了,有一天,你写了这样一段代码:
#include
int main()
{
int *p;
*p = 20;
printf("%d\n", *p);
return 0;
}
哎呀,这里程序怎么挂了?其实,这里没有对指针p进行初始化,他没有保存任何变量的地址。这个指针也是一个局部变量,当局部变量不初始化的时候,内容是随机值。
既然是随机值,也就是说这里的指针是随机的地址,你说万一这地址里面存的是什么重要数据,你在这里把他改了?是不是就太危险了?!
这种有越界访问的指针我们统称:野指针
野指针就是指针指向的位置是不可知的(随机的、不正确的、没有明确限制的)
除此之外,还有其他导致野指针的原因,我们来看下面代码:
#include
int main()
{
int arr[10] = {0};
int *p = arr;//相当于int *p = &a[0];
int i = 0;
for(i=0; i<=11; i++)
{
//当指针指向的范围超出数组arr的范围时,p就是野指针
*(p++) = i;//先解引用赋值,再后置++使指针指向下一个位置
}
return 0;
}
在C语言中,数组名表示数组首元素地址,我们将数组的首元素地址给了指针p,我们通过for循环用指针对数组进行访问,这里我们只有10个元素,我们却要访问12次,那么就会发生越界访问问题。
这里也就会造成指针越界访问的问题,同样,当出了数组之后,指针也会变成随机值,造成越界访问。
[事件3] 这几天在西藏你玩的很爽,玩够了,也该回家族打理产业了,然后你就想在过了今天晚上,明天就回去,可是当你走到302的房门前,发现你的行李整齐地摆放在地下,这个时候你才想到,原来房间今天早上就到期了…身上的钱也只够回家了,看来今天只能露宿街头了…[结束]
除了上面的情况之外,我们还有一种常见的导致野指针的问题:
#include
int *Test()
{
int a = 1;
int *p = &a;
return p;
}
int main()
{
int *ret = Test();
*ret = 2;
printf("%d\n", *ret);
return 0;
}
我们来仔细分析一下:从main函数开始,第一个语句直接进入到Test函数里,那么Test函数会在函数栈帧上开辟一块空间,变量a也开了一块空间,指针变变量p也开辟一块空间用来记录a的地址。
在函数调用结束的时候,会创建一个临时变量记录返回值,函数栈帧销毁,变量a和指针变量p都销毁了,临时变量被返回值传到main函数的ret。
那么ret就记录下了这个地址,我们对ret解引用赋值,但是这个时候Test函数已经销毁了,里面的变量的值已经回收了,这个时候再去访问这个已经回收的地址,那么肯定会发生越界访问的。也就是说,你的房间已经被退房了,这个时候你还想去302,就是非法的了。
总结:
1、野指针会造成越界访问的问题,因此对于指针控制范围非常重要。
2、已经回收资源的地址,再次访问这个地址就是非法访问。
由上面的学习我们知道指针玩不好代价是很大的,那么有没有什么办法防止指针越界等问题呢?要想玩好指针,你必须要记住这五个点:
1、指针一定要初始化
2、小心指针越界
3、指针指向的空间释放,及时将这个指针置为NULL
4、避免返回局部变量的地址
5、指针使用之前要检查有效性
指针在使用之前一定要初始化,如果没有需要引用的对象,就将指针置为NULL,如下:
#include
int main()
{
int a = 10;
int *p = &a;
int *ptr = NULL;
return 0;
}
这里的NULL我们可以转到定义来看一下:
我们可以看到,在c++中的NULL就是0,在C语言中NULL的类型就是(void *)空指针类型,严格来说C语言的NULL是更加正确的。
牢记这5点,妈妈就再也不用担心我的指针老是出错了。
我们已经理解指针的基本功能了,除了上面的基本功能,指针还有一个很重要的东西———指针运算。
我们前面已经学过,指针加上整数就是跳过整数倍指针类型个字节,就像:
#include
int main()
{
int a = 0x44332211;
char *p = (char *)&a;
printf("%x\n", *p);
printf("%x\n", *(p + 1));
printf("%x\n", *(p + 2));
printf("%x", *(p + 3));
return 0;
}
指针加上整数除了可以进行读取数据以外,还可以连续的存储数据,我们看下面代码:
#include
#define N 5//数组元素个数
int main()
{
int arr[N] = { 0 };
int *parr = NULL;//指针用来保存数组首元素地址
for(parr = &arr[0] ; parr < &arr[N] ; )//将数组首元素地址赋值、小于判断条件就一直执行
{
*parr++ = 1;//指针解引用对内存空间进行赋值,随后+1指向下一个位置
}
int i = 0;
for(i = 0 ; i < N ; i++)//打印出来数组里的值看看是否改变
{
printf("%d ", arr[i]);
}
return 0;
}
我们用指针解引用访问对应的内存空间从而完成了赋值操作。这种是指针parr的位置一直在变化,如果不想要指针的位置,我们可以这样写:
#include
#define N 5
int main()
{
int arr[N];
int *parr = &arr[0];
int sz = sizeof(arr) / sizeof(arr[0]);//sizeof(数组名)求出整个数组的字节大小
//然后再除上一个元素的大小,就是数组元素的个数。sizeof(arr) == N
int i = 0;
for(i = 0 ; i < sz ; i++)
{
*(parr + i) = 1;
}
for(i = 0 ; i < N ; i++)
{
printf("%d ", arr[i]);
}
return 0;
}
这样就能控制指针的地址不变,而完成数组元素的赋值了。
总结:
指针加减整数的意义就是指针跳过了 指针类型大小*整数 个字节,进而访问对应的内存空间。
我们来看下面的代码:
#include
int main()
{
int arr[10] = { 0 };
printf("%d\n", &arr[9] - &arr[0]);
return 0;
}
这里两个指针相减,你来思考一下,得出的结果是多少?相信聪明的你能很快的得出正确的答案。我们直接来看结果:
答案是9,不知道你想对了没有,我们取了数组元素的第10个元素地址,与第一个元素地址作差,得出来的结果是9,正好就是两个数组元素的距离。
但是如果是这两种情况:
#include
int main()
{
int arr[10];
double a = 1;
int *ptr1 = &arr[0];
double *ptr2 = &a;
printf("%d", ptr1 - ptr2);
}
如果采用了不用类型的参数进行相减,就会报错,而且最好是像数组这种连续的内存空间使用指针相减,否则相减出来的值是几乎没有什么意义的。
总结:
1、指针-指针得到的就是指针和指针之间元素的个数。
2、两个指针相减的前提是他们的类型必须相同。
3、指针相减的到时元素的个数,所以在连续的内存空间下相减是比较有意义的,不推荐两个毫不相关的指针相减,因为几乎没什么意义。
我们的地址是有大小的,有高低地址之分,而指针的关系运算就是比较指针的大小。我们来看下面的代码:
#include
#define N 5
int main()
{
int arr[N] = { 1 };
int *parr = NULL;
for(parr = &arr[5] ; parr > &arr[0] ; )
{
*--parr = 0;
}
int i = 0;
for(i = 0 ; i < N ; i++)
{
printf("%d ", arr[i]);
}
return 0;
}
数组一共有5个元素,将数组元素全部初始化为1,随后我们将数组的最后一个元素的地址放进指针变量parr里面,我们准备使数组中的元素从后往前进行赋值,将数组中的元素全部赋值为0。
for(parr = &arr[5] ; parr >= &arr[0] ; parr--)//这两种方式都是相同的
{
*parr = 0;
}
得到的结果同样是:
指针的比较还有一个要点:就是只能向后比较,但是不能向前比较,如下图:
C语言规定了:允许指向数组元素的指针与指向数组最后一个元素后面的那个内存位置的指针比较,但是不允许与指向第一个元素之前的那个内存位置的指针进行比较。
指针和数组是什么关系呢?我们前面也使用了数组名作为首元素地址,那么数组与指针究竟有着什么样的渊源呢?大型纪录片之《指针与数组的故事》持续为您播出…
指针变量就是指针变量,不是数组,指针变量的大小为4/8个字节,专门用来存放地址的。数组也就是数组,不是指针,数组有一块连续的内存空间,可以存放1个或多个类型相同的数据。
我们来看下面的代码:
#include
int main()
{
int arr[10] = { 0, 1, 2, 3, 4, 5, 6, 7, 8, 9 };
int len = sizeof(arr)/sizeof(arr[0]);
int *p = arr;//指向数组首元素地址
int i = 0;
for(i = 0 ; i < len ; i++)
{
printf("%p == %p\n",p + i, &arr[i]);
}
return 0;
}
我们可以看到,数组与指针的地址全部都是对应的,一模一样,所以说,数组名就是首元素地址,那么以后你就可以不使用[]来访问数组的内容了,可以使用指针 + 偏移量 的方式来访问数组元素:
int a[1000];
int *p = arr;
for(int i = 0 ; i < sizeof(arr)/sizeof(arr[0]) ; i++)
{
*(p + i) = 1;//i就为偏移量
}
数组与指针的联系:
前面我们也用到了,数组名就是首元素地址,数组名 == 地址 == 指针 ,当我们知道数组首元素的地址的时候,又因为数组是连续存放的,所以通过指针就可以遍历访问数组,前面也演示过了,数组可以通过指针来访问。
数组名就是数组的首元素地址,但是在这两个情况下是例外的:
//sizeof(数组名) sizeof数组名是直接得到整个数组的字节大小
//& 数组名 &数组名 如果进行+1操作是直接跳过一整个数组
其余的情况数组名就是首元素地址。
指针的基本用法我们大概了解了,但是我们了解的是 “一级指针” 的用法,其实还存在着二级指针、三级指针…多级指针,因为二级指针用的最多,所以我们在这里主要阐述二级指针,其他指针的情况类比就行了。
那么究竟什么是二级指针呢?我们先来看我们日常所说的一级指针:
int a = 0;
int *pa = &a;//这里的指针pa就是一级指针
我们再来看看二级指针:
int a = 0;
int *pa = &a;//这里的指针pa就是一级指针
int **ppa = &pa;//这里为二级指针
我们要理解一个东西,指针变量也是变量 啊,既然是变量,那么就一定有内存空间来存储指针变量,而二级指针就是取一级指针变量的地址 的指针。如下图:
这里二级指针的两个*可以这么来理解:int **是种类型,而我们可以把int ** 看成int* * 前面的int*是指向变量的类型,也就是一级指针(一级指针的类型为int *),而你本身是二级指针,指针的类型必须是int *,加上一级指针的类型int* 就是int ** 。
同样,三级整形指针的返回类型就为int ***,多级指针以此类推…我们来看下面代码:
int a = 0;
int *p = &a;//一级指针
int **pp = &p;//二级指针
现在我们想通过二级指针来修改变量a的值,我们该如何做?我们是如何由一级指针访问变量内存的?使用解引用来访问:
*p = 1;//以上面代码为续接
那我们二级指针解引用就找到了一级指针的地址,然后我们在解引用一次,不就可以访问变量a了吗?
* *pp = 100;//两次解引用,第一次解引用找一级指针内存,第二次解引用就访问到变量a了
以上就是二级指针的具体用法了,多级指针以此类推。
在我们C语言中存在着这样一个东西————指针数组 ,那么请你思考一下,指针数组究竟是指针呢还是数组呢?
答:是一个数组,用来存放指针的数组。
我们知道,数组有不同的类型,有int型数组,double型数组、char型数组…
那么我们的指针数组呢?刚才我们也回答了,指针数组里面存放的都是指针变量,那这里的数组名其实就是二级指针了。
我们来看下面代码:
#include
int main()
{
char arr1[] = "abcdef";
char arr2[] = "talk is cheap";
char arr3[] = "show me code";
char *parr[] = { arr1, arr2, arr3 };
char **p = parr;
return 0;
}
我们可以通过指针p来访问数组中的元素的指向。
字符指针数组的每个值的类型都是char*,而数组名就为数组的首元素地址,首元素为指针,所以数组名就是二级指针。
; 这里我们用二维数组的方式打印出各个数组里的字符串,我们只需要:
#include
#include
#include
int main()
{
char arr1[] = "abcdef ";
char arr2[] = "talk is cheap";
char arr3[] = "show me code ";
char *parr[] = { arr1, arr2, arr3 };
char **p = parr;
int len = strlen(arr1);
int i = 0;
for(i = 0 ; i < 3 ; i++)
{
int j = 0;
for(j = 0 ; j < len ; j++)
{
printf("%c",parr[i][j]);//通过[]来访问下标
}
printf("\n");
}
return 0;
}
我们可以看到,完全可以用二级指针来模拟二维数组。其实在第二层的for循环里面我们可以这样改:
for(j = 0 ; j < len ; j++)
{
printf("%c",*(parr[i] + j));
}
printf("\n");
先由数组名可以访问每个元素,而每个元素的类型都是char*所以我们可以使用偏移量来对每个指针变量所指向的数组进行访问。当然,这里还有其他的写法可以支持访问,大家可以自由的探索。