本节我们将介绍C语言的指针部分,我们知道C语言不同于其他常用高级语言(Java、C#、Python、javascript......)就是因为他既可以拥有高级编程语言的特
性,有可以操作计算机硬件(仅能操作内存)。而操作计算机内存所用到的就是指针了。那么指针之于C语言又有着什么样地作用与地位呢:很多人都认为,c语
言之所以强大,以及其自由性,很大部分体现在其灵活的指针运用上。因此,说指针是c语言的灵魂,一点都不为过(PS:其实并非仅有C语言才支持指针操作,pascal
语言本身也是支持指针的。从最初的pascal发展至今的object pascal,可以说在指针运用上,丝毫不会逊色于c语言的指针)。
那么C语言的指针都包括哪些类型呢?在这里我画了一张表来展示C语言的指针:
以上表所展示的指针基本包括了大多数C语言的使用场景在下面我会带着大家一起一点一点分析并应用C语言的指针。
好了,我们知道了C语言的指针是用来操作计算机内存的,那么计算机的内存是什么?所以我们如果想深入的了解C语言的指针我们就不得不先好好学习关于计算机的
内存的问题,好了,既然如此那么下面就让我们一起来简单了解一下计算机的内存吧:
如果你有计算机硬件基础的话都应该知道内存之所以能够存储数据是因为其内部拥有内存颗粒,我们可以简单的认为一个内存颗粒就可以保存一位数据(0/1)而我们
的计算机如果想要读取内存中的数据就要告诉我们他要读取哪一位数据(要读取的数据的地址),而我们为了给每一个内存颗粒一个地址就需要为每个颗粒一个数字
来标记位置,那我们最多可以有多少个地址呢?那就要看我们的操作系统了(32位的操作系统有32位0/1值来标记数据而64位系统则可以使用64位0/1值)。
下面我们来看一下内存的物理结构图:
下面我们来看一下内存的逻辑结构图:
好既然我们已经了解了物理内存的基本原理,那么接下来我们就要正式进入C语言的指针的学习来了。
计算机中的内存都是编址的,就像你家的地址一样。在程序编译或者运行的时候,系统(可以不关心具体是什么,可能是编译器,也可能是操作系统)开辟了一张表。
每遇到一次声明语句(包括函数的传入参数的声明)都会开辟一个内存空间,并在表中增加一行纪录。记载着一些对应关系。下面就是在函数使用中的代码示例图:
指针,是一个无符号整数(unsigned int),它是一个以当前系统寻址范围为取值范围的整数。32位系统下寻址能力(地址空间)是4G Bytes(0~2^321)二进制表示长
度为32bits(也就是4Bytes), unsigned int类型也正好如此取值。而64位系统下寻址能力(地址空间)是0~2^64二进制值。
下面我们首先我们介绍有关于指针运算的两个运算符*和&
(*p)操作是这样一种运算,返回p 的值作为地址的那个空间的取值。(&p)则是这样一种运算,返回当时声明p时开辟的地址。显然可以用赋值语句对内存地址赋值。
以上我们所讲的都是单级指针,也就是说指针指向的地址所保存的内容就是一个普通的变量。其实我们都知道指针也是一种变量,叫做指针变量,只不过他保存的内容
是一个地址而已。那么既然如此我们是不是可以让一个指针保存另一个指针所在的地址?当然可以!那么我们就称这种保存另一个指针地址的指针叫做二级指针:在声
明时,我们用2 个*号,声明指向指针的指针。它的意思是“它是一个整数,这个整数指向某个内存地址,一次访问sizeof(int长度,其值是一个整数,那个整数值指向
某个内存地址,一次访问sizeof(BTree)长度。”。那么我们想既然我们可以使用指针指向一个指向常量的指针,那么我们可不可以使用一个指针指向一个指向指针的指针
呢?当然是可以的,如他指向的那个指针指向的地址保存的内存是一个普通变量则称为三级指针,如果他那段内存保存的还是一个指针那么我们就依次类推直到指向的
内存不再是地址为止有几层我们就称之为几级指针(不过这时我们一般通称为多级指针)。下面我们讲解指针的初始化:
对指针进行初始化或赋值只能使用以下四种类型的值[3] :
1. 0 值常量表达式,例如,在编译时可获得 0 值的整型 const对象或字面值常量 0。
2. 类型匹配的对象的地址。
3. 另一对象末的下一地址。
4. 同类型的另一个有效指针。
好下面我们通过一段代码来演示一下各种指针的基本使用:
int ival;
int zero = 0;
const int c_ival = 0;
int *pi = ival; // 错误: pi指针不能使用int类型值赋值
pi = zero; // 错误: pi不能使用非常量的0值初始化
pi = c_ival; // 正确:pi使用非常量的0值初始化
pi = 0; // 正确: pi使用非常量的0值初始化
数组与指针的关系
指针数组:就是一个由指针组成的数组,那个数组的各个元素都是指针,指向某个内存地址。 char *p[10];//p是一个指针数组
数组指针:数组名本身就是一个指针,指向数组的首地址。注意这是一个常数。
-------------------------------------------------------------------------------------------------------
好讲了那么多,不知各位是否还记得我们在本篇刚刚开始出展示的那个图表,他表明了我们最常使用的关于普通变量、数组、指针、多级指针、函数指针的形式与类型
而下面我们将一步一步慢慢探索他们每一种的基本用法:
一、普通变量:
我想可能有读者就会问我们学习C语言到这个地方,早就能够熟悉的使用C语言的变量了,对他的了解也是很熟悉的,你还有必要再讲解吗?还请各位看官
莫急,且听我细细道来。首先我想问的是我们定义的变量是如何保存的?我们定义变量为什么一定要声明变量类型(本文的观点仅限于C语言的概念范围内)?我们是如
和引用于修改变量的?好,我们就以上问题来给各位一一解答:第一个问题,我们的所有变量都是通过0/1编码来保存的,而这些0/1编码就是保存在内存颗粒中。第二个
问题,首先我们知道我们是通过0/1编码来保存数据的,例如我们char类型仅有255个数据所以通过最少8个0/1编码就完全可以表达清楚,但是我们的long类型显然是不行
的,所以我们使用类型来保存数据主要是通过类型来标记他在内存所占的位数,这时我们只需知道某一变量的起始地址以及他的长度就可以读取他了.
下面我们我们来看一个最基本的指针的使用:
我们将定义一个int类型的指针(首先我们说过一个指针只是保存着他要指向的内容的地址,无论他指向的那个数据是什么类型占多少位内存,地址的长度都是固定的32位
系统的地址占32位,64位系统内存地址占64位),也就是说如果没有类型的话我们通过指针只能知道一个数据的首地址而由于不知道长度所有不能真的去取数据。下面一
张图展示的就是我们的指针和指针指向的数据的关系:
二、好我们继续来讲解第一种指针int *d = &a;int *d, *e;
我们看到指针的定义是在类型和变量名之间添加一个'*'来声明的,关于指针的赋值由于上面已经讲过所以在这里我们不再赘述,好了不知道大家有没有看到我们在后面
的另一个指针的声明,这个声明的不同之处在于,他一次声明了多个指针变量,那么对于多个指针变量的声明我们应该如果处理呢?我们看到我们在在每个变量名的前
面都添加了一个'*',其实如果某一个比变量名不使用'*'那么整行的声明语法也是正确的(我们并不推荐使用这种容易产生混乱的地方),只不过那个没有使用'*'号的
变量不再是指针而只是一个普通变量。
下面我们看代码示例
三、下面我们讲解关于数组的使用及其本质:int[x]
我们知道数组在C语言中是一个很好用的工具,但是他也有一个很不好的问题那就是声明时必须指定长度或者初始化,而且长度不可改变,我们发现有时我们将一个数组
赋值给同类型的指针也是可以的。那么数组究竟是一个什么东西?其实数组实际上就是一个指针,不过这个指针是一个const指针(常量指针),也就是说我们是不能直
接将另一个数组赋值给一个数组或者将另一个指针赋值给本数组的。下面我们将通过一段代码来证明我们的以上结论。
看代码:
四、我们来讲解指针数组与数组指针:int *p[x] 和 int(*p)[x]
有关于指针数组与数组指针估计让很多读者头疼不已,他们只不过是在用词顺序上有所不同而已,但在代码中的函数的意义却是大不相同。简单来讲就是:指针数组就是
x个指针组成一个数组,而数组指针就是一个指针他指向的内容是一个数组。
int *p[x]的定义式是指我们要定义指针数组p,他是由x个指向整型数据的指针元素构成的。
int (*p)[x]的定义是指我们要定义一个指针(数组指针),他就一个指针,指向的内容是一个数组。
下面我们来看代码:
五、函数指针。
不知各位有没有汇编的经验,如果你写过汇编(无论是x86汇编还是ARM汇编)或者读懂过汇编的话那么你就很有可能对一个叫做“入口地址”的概念是比较熟悉的。我们要
进入一个函数其实就是跳转到代码区的某一个地址,而函数的返回也是要返回到调用本函数的地址,那么既然明白了这个那么就应该对函数指针的概念有个直观的理解了
。那么函数指针究竟是什么?:如果我们在程序中定义了一个函数,在编译时,编译系统为代码分配一段存储空间,这段存储空间的起始地址(又称为入口地址)称为这
个函数的指针。
我们其实可以定义一个指向函数的指针变量,用来存放某一函数的起始地址,这也就意味着此指针变量指向此函数:
int (*p)(int,int);
那么我们究竟具体上来讲如何定义一个指向函数的指针呢?看下面:
定义指向函数的指针变量的基本形式:类型名 (*指针变量名)(函数的参数列表);如“int (*p)(int,int);”,这里的类型名就是指函数返回值的类型。
请读者熟悉指向函数的指针变量的定义形式,怎样判断一个指针变量是不是指向一个函数呢?首先看变量名的前面有没有'*'号,如*p。如果有那么此变量一定是一
个指针变量而不是普通变量。其次再来看变量名的后面是否有圆括号,内有形参的类型。如果有,就是指向函数的指针变量,这对圆括号就是函数指针的标志。要注意的
是:由于优先级的关系,“*指针变量名”要用圆括号括起来。
下面我们来看代码示例:
关于函数指针的一些说明:
(1)定义指向函数的指针变量,并不意味着这个指针变量就可以指向任何函数,他只能指向在定义函数指针时指定类型的函数(具有同样的返回值和参数列表).
(2)如果要使用指针调用某个函数,就必须先使用指针指向这个函数。
(3)在个函数指针变量赋值时,只需给出函数名,而不必给出参数。
(4)使用指针变量调用函数时,只需将(*p)代替函数名即可(p为指针变量名),在(*p)之后的括号中给出函数的参数。
(5)对指向函数的指针变量不能进行算术运算,如p+n/p++/p--等运算是无意义的,请不要使用。
(6)用函数名调用函数,只能调用所指定的函数,而通过指针变量调用函数就是比较灵活的了,可以根据不同的情况先后调用不同的函数。
六、返回指针值的函数:
一个函数可以返回一个整形值、字符型值、实型值,也可以返回指针型的数据,即地址。其概念与以前类似,只是返回值的类型是指针类型而已。
我们先看代码示例:
例如“int *a(int x, int y);” a是函数名,调用他以后可以获得一个int*型的返回指针。x,y是函数a的形参,为整型。
定义返回类型为指针的函数的一般形式为:
类型名 *函数名(参数列表);
对应C语言的初学者而言,这种定义形式可能不太容易理解,容易弄错,使用时要十分小心。
七、二级指针。
我们上面讲了指针数组,那么在此基础上我们再学习指向指针数据的指针变量(简称:指向指针的指针),就比较容易了。
我们可以看到下图:name是一个指针数组,他的每一个元素都是一个指针类型的变量,其值为地址。name既然是一个数组,他的每一个值都应该是有地址的。数组名代表
数组的首地址。name+i代表的就是name[i]的地址。name+i就是指向指针型数据的指针。
下面我们看一段代码示例:
以上代码示例的结果也佐证了我们的观点。
OK,今天我们关于C语言指针的内容大致讲解了一遍,有可能对于绝大多数C的初学者来讲,指针的概念比较繁杂难懂,不过相信通过一段时间的学习后,每个人都能够很好的掌握C的指针。拜。