前言
本篇博客适合指针小白和对指针理解总是模棱两可的朋友。是关于指针最基本和本质的内容。分为上下两卷,该为上卷。
指针(pointer),是C语言中的一个重要概念,是C语言学习过程中比较困难的部分。
对于那些未学过指针的朋友而言,看到这里,恭喜你接触到指针,以下内容全是满满的干货,无微不至。对于已学习过指针的朋友,不妨再一起回顾一下指针理解里重要的这些内容。
指针简介
在 C/C++语言中,指针一般被认为是指针变量,意思是当我们简单地说“指针”这两个字时,我们真正在讨论的东西其实是指针变量。本质上,指针是一个值为内存地址的变量。就像char类型的变量,值是字符,int类型的变量,值是整数。
在理解指针之前,我们必须先理解什么是地址。
内存和地址
只要对电脑稍微有一点了解我们就知道电脑是有内存大小的,无论是几GB,数据就是放到这些内存中去的。而CPU(中央处理器),就负责处理这些数据,也就是从内存中读取、处理、再放回内存中,这样一个过程。
举个例子,内存中存放数据,就像一个酒店里面住着客人,而为了更好对酒店进行管理,每个房间会有房间号,同样的道理,内存也划分成为一个个内存单元,每个内存单元多大呢?1个字节,也就是8位,也就是说每个字节都有唯一的地址。如果内存中有n个字节,那么可以把地址看做0~n-1的数。
所以当我们得到了房间号就能找到这个房间,我们得到了地址就能找到这个地址指向的内存单元,也就是这个字节。但是,程序中的每个变量往往并非只有一个字节这么大,而是占有一个或多个字节内存。所以C语言规定,第一个字节的地址就是这个变量的地址。这一点非常重要,一定要时刻记住!
int a = 10;//创建一个变量a,存放的值为整数10
//我们知道int类型占四个字节,假设这四个字节的地址分别是2000~2003,那么变量a的地址其实就是2000
说得更深入点,当我们创建变量a时,本质是在在内存中申请了一部分空间,这部分空间的大小是4个字节,用来存放10这个数。变量名其实只是给程序员看的,编译器是通过地址而非名称来寻找这块内存的。
那么这和指针又有什么关系呢?
在听完以上这些你知道地址本质是也是一个数字,在大多数系统内部,该地址由一个无符号整数表示,但是注意不要把指针认为是整数类型,一些处理整数的操作不能用来处理指针,反之亦然。从另一角度来说,地址的取值范围可能不同于整数的范围,所以我们用普通的整型变量存储地址并不是很好的办法。
于是就有了一种特殊的变量,指针变量,专门用来存储地址。假如一个指针变量p存储变量a的地址,我们常常说p“指向”a。其实,指针就是地址,而指针变量就是存储地址的变量。这才是“指针”两个字真正的含义。告诉你理解的诀窍,“指针”到底指的是地址还是指针变量,主要看上下文语境哦。
指针的使用
指向
假设一个指针变量名为ptr,请看以下语句:
ptr=&a;//把a的地址赋给ptr
我们说ptr“指向”a。a和&a的区别在于,a是变量,而&a却是一个常量。如果你知道什么是左值和右值,更进一步说,ptr是可修改的左值,而&a是右值。这其实意味着,我们可以改变ptr的指向,让它指向别处:
ptr=&b;//把ptr从指向a改为指向b
此时ptr的值变为了变量b的地址。
取地址运算符:&
你应该可以自行感受到,&其实就是取地址的意思、&是一种运算符,叫做取地址运算符。如果x为一个变量,&x就是x在内存中的地址,本质是一个常量。
要注意的是,根据我们上面提到过的,&x拿到的其实是x变量代表的内存空间的第一个字节的地址,即首地址。
指针变量的声明
对指针变量的声明表面上与对普通变量的声明其实还是很相似的,看起来唯一不同的就是必须在指针变量名字前放置星号:
int *p;//声明了一个指针变量p
这句语句说明p是指向int类型对象的指针变量。
所以为什么说只是看起来相似呢?因为我们不能写出下面这样的指针声明:
int i;//i是变量名,int是这个变量存放的值的类型
pointer p;//如果我们照猫画虎,就会写出这样的代码
为什么不能这么写呢?因为在声明指针变量时,必须指定指针所指向变量的类型,而pointer p;很明显是没有体现出这一点的。
为什么要指定指针所指向变量的类型呢?因为指向的变量类型不同,我们其实指向的存储空间大小就不同,指向int类型时本质上意味着我们指向了一个4个字节的内存空间,而指向char类型时我们指向的是一个1个字节的内存空间。这在一些指针操作中是非常重要的。更进一步说,即使是可能占用相同存储空间的long和float,它们存储数字的方式却并不相同,所以知道指向的变量是什么类型非常重要。
但是,指针变量本身也是有类型的,上面这个代码中,int *就是p的类型,用文字描述就是“指向int类型的指针”。就像int i;中int是i的类型。去掉标识符剩下的就是类型,从这一点上说是一样的。
声明中*的意义
注意我强调了“声明中”,因为只有在声明中,星号(*)的作用是表明声明的变量是一个指针。*和指针名字之间的空格可写可不写。
所以我们在指针变量的声明中,以int *p;为例,应该将int和*拆开理解,int代表p指向的是int类型的对象,*代表p是个指针。
注意,每个指针变量可以改变指向(上面提到),但只能指向一种特定类型的对象:
int *pi;//pi只能指向int类型对象
double *pd;//pd只能指向double类型对象
char *pc;//pc只能指向char类型对象
只能指向一种特定类型的道理和为什么要指定指针所指向的变量的类型是一样的。
指针的初始化
声明指针变量本质上只是为指针留出了内存空间 ,但还没有将它指向对象。
而在指针使用前初始化是非常重要的,这和在使用普通类型变量前要初始化是一样的道理。
int a = 10;
int *p;
p = &a;//注意这里就不用在p前面加星号了,因为声明中的*只是起到表明p是个指针的作用。
这样我们就完成了一个指针变量p的声明和初始化。其实也可以简写成这样:
int a = 10;
int *p = &a;
本质是在声明的同时初始化,就和a一样。
解引用运算符:*
解引用运算符*(indirection operator)也可以叫间接运算符、间接寻址运算符。
正如最开始我们举的那个例子,通过房间号就能找到对应的房间,指针变量在指向对象后我们就可以使用*运算符去访问存储在对象中的内容。
假设p指向i:
printf("%d",*p);
打印出来的结果我们可以看到正是变量1中的值,而不是i的地址。可以把*p想成i的别名。而且重要的是,当我们给*p赋一个新的值,打印i的值会发现就变成了我们新赋的值(*p是左值,可以给它赋值)。
注意:对未初始化的指针变量p进行解引用是非常危险的,为什么这么说呢?因为p在初始化前可能恰好放有有效的内存地址,此时*p改变了存储在该有效地址的数据,而这绝不是我们希望的。
总之,使用*p来访问i,体现了“间接”访问,这也就是为什么解引用运算符也叫间接运算符。
&和*的关系
*有点像&的逆运算,对变量使用&运算符得到的结果是指向变量的指针(地址),而对指针(地址)使用*运算则可以得回原始的变量。
int a = 10;
int *p;
p = &a;
printf("%d",*p);//此时打印出来的就是a的值,即10
它们像是一对,实际上它们还有互相抵消的效果:
*&a==a;//该表达式为真
指针赋值
这是什么意思呢?两个指针如果具有相同的类型,就可以进行如下的操作:
int i,j;
int *p1,*p2;
p1 = &i;//将i的地址赋值给p1
p2 = p1;//将i的地址拷贝了一份给p2,现在p2存放的也是i的地址,也就是p2也指向了i
可以想象成p1有一个秘密基地,一开始只有它知道地址,只有它能够访问这里。当它把地址透露p2后,这里也变成了p2的秘密基地,p1和p2现在都有能力独立访问这个秘密基地。
那么,上卷内容就到此结束了,相信真正读完的读者一定会有或多或少的启发!希望大家看到错误可以及时反馈给我,敬请期待下卷。