目录
1、简介
指针是C语言程序设计的灵魂,是C语言程序设计中被广泛使用的一种数据类型,它在C语言使用中有着重要的地位。使用指针能写出精炼而高效的程序代码,极大的丰富了C语言的功能。
说明:本博客中所列举的代码执行环境是基于Visual Studio 2015。
本博客中的内容及一些观点难免会出现一些错误,为了完善此篇博客,欢迎各位指正!!!
2、存储单元及其地址
为了能深刻理解“指针”,我们首先来了解几个相关的基本概念。
- 位(bit):位是计算机中最小的信息单位,二进制的0或1就是一个二进制位。
- 字节(byte):由8个相邻的二进制位组成的存储单位,称为字节。1 Byte=8 bit。字节是目前计算机最基本的存储单位,也是计算机存储设备容量最基本的计量单位。在这里字节的位编号由低位到高位用b0、b1、b2、b3、b4、b5、b6和b7来表示,如图1所示。
图1 1个字节由8个二进制位组成
1、存储单元地址
计算机中的数据(包括指令数据和数值数据)是在存储器中存放的。如图2所示,存储器由若干个存储单元组成。若按字节编址,一个字节就构成一个存储单元。为了能区分和访问(读写)不同的存储单元,给每个存储单元进行编号,编号依次位0000、0001、…、2004、2005、…,这些编号的作用就像房间的门牌号码一样,可以唯一的确定某一个物理空间,我们形象地把这些编号称为存储单元的“地址”。有了存储单元的地址,就可以准确地描述和访问存储单元。如图2所示,在地址为2000的存储单元里,存放了一个二进制数01010101。
图2 存储器组成及其地址
2、多字节数据的存储规则
多字节数据存入存储器时,要一次占用多个存储单元,其数据的存放原则是:低位字节存入低地址单元,高位字节存入高地址单元。例如,整数数据7要占用4个字节(在Visual C++6.0环境中),在存储单元的具体存放情况如图2所示,若从2001地址开始存放,将连续占用4个字单元2004、2003、2002、2001,共32位。
说明:这里将数据7存储在单元地址为2004~2001的存储单元,只是一种假设,编译系统也可能会分配其他地址单元进行存储,并且在这里存放的是7的二进制补码形式。
3、变量的实质
假设有如下的变量定义:
char c = '0'; //字符型变量c
int iSum = 6; //整型变量iSum
这些变量到底在计算机中是怎么实现的呢?实际上编译程序和操作系统给我们定义的变量分配了某个具体的存储单元与其对应。对变量的访问,实质上是对应的存储单元进行访问。好比给地址为3000的存储单元起了个别名叫做c,3001存储单元别名叫做iSum一样,对c的操作,实际上就是对3000单元的操作,对iSum的操作,实际上就是对3001单元的操作。存储对应情况如图3所示。这里还要注意的是,不同的数据类型所占的内存单元数是不等的,如一个整数变量要占用4个字节,所以对iSum的操作,实际上是对3004、3003、3002、3001组成的32位数据操作。
图3 变量的实际存储情况
对存储单元数据的访问包括“读”和“写”两种操作,只要知道了存储单元的地址,就可以对某存储单元进行访问了。例如,可以将数据写入2000存储单元,或把20001存储单元的数据取出来等。但是直接使用地址(如3000等)来对存储单元的访问时不方便的、也不可靠的。C语言中用变量等概念巧妙地回避了具体的地址值,对地址进行了抽象,而变量和存储单元之间的关系协调,完全由编译系统和操作系统处理,这样我们就只需和变量打交道了。
这里我们举例子说明,古都西安的地理位置在东经107°40′和北纬33°39′,我们会说“去西安旅游”,而绝不会说“去东经107°40′和北纬33°39′旅游”。同样的道理,利用C语言提供的变量就可以对存储单元进行访问了,而不需要使用具体的存储单元地址。这给程序设计提供很大的方便,这也就是系统实现变量的实质。如表1所示时地址及其描述的比较。
表1 地址及其描述比较
地址 | 描述 |
东经107°40′和北纬33°39′ | 西安 |
3000 | 变量c |
3001 | 变量iSum |
【例1】本例用来说明变量和内存的关系。
#include <stdio.h> #include <stdlib.h> int main(void) { char c = '0'; //假设是如图3所示的内存分配情况 int iSum = 6; iSum = iSum + c++; //将3000单元的内容累加到3001单元,并将3000单元加1 printf("%c %x ", c, iSum); system("PAUSE"); return 0; }
程序运行结果如图4所示。
图4 例1的运行结果
程序运行结束之后,内存的变化情况如图5所示。
图5 例1程序运行之后的内存变化情况
4、指针和变量
4.1、指针相关概念的引入
1、存储单元的地址和存储单元的内容
根据一个存储单元的地址,就可以找到并访问这个对应的存储单元。在这里我们再次明确两个概念:存储单元的地址和存储单元的内容。如图6所示,假设有语句“int iCount = 49;”,某个存储单元的地址为4000,并假设将此单元分配给了整型变量iCount,则对应的存储单元内容便是49。
图6 存储单元的地址、单元的内容和变量
说明:为了描述简单,在此图示的时候给iCount整型变量分配了一个单元,实际情况是占用4个字节,图示时做了简化处理。
2、指针概念的引入
在这里我们给存储单元的地址一个新的名称——指针,这是对地址的一种形象的称呼。假如知道了地址4000,就能访问4000单元的内容,我们就可以形象地说4000是指向这个单元的。为了形象地描述,我们从地址处画一个箭头来指向对应的单元,如图7所示。
图7 指针的描述
我们引出符合“&”,“&”是取地址运算符,如“&iCount”的含义就是“取变量iCount所在的存储单元的地址”。在本例中,&iCount等于4000,而iCount等于49。
3、指针变量
我们也可以将4000这个存储单元地址存到另外一个变量中,这种用来存储地址的变量称为指针变量。通过指针变量也可以间接访问存储单元。假设有某个指针变量i_pointer,我们分析下面的语句:
i_pointer = &iCount; //将iCount的地址4000赋给指针变量i_pointer
由于i_pointer的值等于iCount的地址4000,这样我们就认为指针变量i_pointer也形象地指向了iCount,如图8所示。
图8 指针变量和变量的关系
说明:指针变量本身也是通过存储单元实现的。在本例中i_pointer变量所在的存储单元地址为5000。
4、指针运算符“&”和“*”
“&”是取地址运算符;“*”是间接访问运算符,取指针变量指向对象的内容。
假设有如下的指令:
i_pointer = &iCount; // 将i_pointer 变量指向 iCount变量
在如图8所示的情况下,iCount等于6。
&iCount就是变量iCount的地址,即等于4000。
*i_pointer就是指针变量i_pointer所指向的存储单元的内容,即等于6。
也就是说iCount和*i_pointer是等价的。
思考:在这里*i_pointer等于6,那么&i_pointer在本例中等于多少呢?(答案:5000,想想为什么?)
“&”和“*”这两个运算符的优先级别相同,若同时出现在表达式中,则按“从右向左”的结合。例如,&*i_pointer应翻译成&(*i_pointer),等价于&iCount,即变量iCount的地址4000;*&i_pointer应翻译成*(&i_pointer),等价于i_pointer,即等于4000。
“++”和“--”的优先级与“&”和“*”的优先级别相同,若同时出现在表达式中,按从右向左的方向结合。例如,*i_pointer++应翻译成*(i_pointer++),即结果为6,i_pointer指针变量加1,为4004(i_pointer为整型变量);(*i_pointer)++应翻译成iCount++,即iCount加1,变为7。
思考:上例中的*i_pointer++,*(i_pointer++),*(i_pointer+1)执行结果是否相同?
5、直接访问和间接访问
- 同过变量名访问变量的方式称为直接访问。
- 通过指针变量访问变量的方式称为间接访问。
例如:
i_pointer = &iCount; //将i_pointer指向iCount变量,i_pointer等于4000
iCount = iCount +1; //变量的直接访问
*i_pointer = *i_pointer; //变量的间接访问
后两条语句是等价的,都实现了让iCount的之加1的操作。
4.2、指针变量的定义申明
指针变量实际上和普通的变量一样,在使用指针变量之前,也必须”先声明,后使用“,目的是指定变量的类型,编译时按类型分配存储单元。
指针变量的类型是指针类型,它用来存放某对象的地址(如整型变量iCount的地址),而这个对象有它自己的数据类型(如iCount的类型为整型),这个类型我们称为基类型。所以在定义指针变量的时候,一般形式为:
基类型 * 指针变量名
【例2】指针定义举例
#include <stdio.h> #include <stdlib.h> int main(void) { int iSum = 0; //定义整型变量 int *i_pointer; //定义整型指针变量,其基类型为int float rSum = 1; //定义实型变量 float *r_pointer; //定义实型指针变量,其基类型为float i_pointer = &iSum; //初始化指针变量 r_pointer = &rSum; printf("%d %f ", iSum, rSum); //直接访问 printf("%d %f ", *i_pointer, *r_pointer); //间接访问 printf("%ld %ld ", &iSum, &rSum); //输出变量的地址 printf("%ld %ld ", i_pointer, r_pointer); printf("%ld %ld ", &i_pointer, &r_pointer); //输出指针变量本身的地址 system("PAUSE"); return 0; }
程序的运行结果如图9所示,分析如图10所示。
图9 例2的运行结果
图10 指针的定义及初始化
说明:
- 指针变量名是i_pointer和r_pointer,不是*i_pointer和*r_pointer。
- 定义声明指针变量时,指针变量名前有“*”,这里的“*”表示该变量是指针类型,而不是指针间接访问运算符。例如
int *i_pointer; //这里的“*”是指针类型定义声明 printf("%d,%f ",*i_pointer,*r_pointer); //这里的“*”是间接访问运算符
- 指针变量定义后,变量值是不确定的,使用前必循先赋值。例如
i_pointer = &iNum; //初始化指针变量 r_pointer = &rNum;
- 基类型说明了这个指针变量必须指向具有和基类型相同类型的对象,例如
i_pointer = &iNum; //正确的 i_pointer = &rNum; //类型转换错误,类型的指针不能指向实型变量
- 对照图10的a、b,分析程序的运行结果。
4.3、指针变量的引用
1、指针的初始化问题
为了方便,我们在以后的讨论中把指针变量简称为指针。指针是一个特殊的变量,用它来存放存储单元的地址。在32位平台里,指针本身占据了4个字节的长度。
当指针只被定义而不被初始化时,它的值是一个不确定的值,因而未指向某个具体的变量(这时称指针是悬空的,也就是指针有可能指向任何一个数据区、程序区或操作系统区),这时再对指针变量进行引用的话,可能会产生不可预料的后果(破坏数据和应用程序,甚至破坏操作系统,引起系统瘫痪)。为了避免此类问题的发生,我们在定义了一个指针之后,必须给这个指针进行初始化,也就是说制作在定义以后要进行一次明确的赋值操作,让指针指向一个确切的对象。
2、指针的运算
(1)赋值运算
指针获取地址的来源,常见的有如下几种方法:
- 变量地址。将变量的地址赋值给指针,这样指针便指向了这个变量。
- 同类型的指针。相同类型的指针变量之间可以相互赋值。
- 函数名。
- 数组名或数组元素的地址。
- 空值(零指针)。也可以给指针变量赋空值,表示该指针不指向任何对象。
【例3】指针赋值举例
#include <stdio.h> #include <stdlib.h> int main(void) { int a = 1, b = 2;//定义并初始化两个整型变量 int *i_pa, *i_pb;//定义了两个指针变量 i_pa = &a;//变量地址给指针变量赋值,即初始化指针 i_pb= &b; printf("%d,%d ", *i_pa, *i_pb); i_pa = i_pb;//用指针给指针赋值,使i_pa 也指向变量b printf("%d,%d ", *i_pb, *i_pb); system("PAUSE"); return 0; }
程序的运行结果为:
图11 例3程序的运行结果
可以根据图12的(a)、(b)和(c)理解指针初始化的概念。
图12 例3指针的赋值运算分析
【例4】空值指针举例。
#include <stdio.h> #include <stdlib.h> int main(void) { int *i_p1, *i_p2, i_p3;//定义了3个指针变量 i_p1 = NULL;//给指针变量赋空值的3种方法 i_p2 = 0; i_p3 = ' '; system("PAUSE"); return 0; }
说明:
- 尽管这里指针的值等于零,但是要注意,它并不是指向地址为0的存储单元,而是具有一个确定的空值,它不指向任何对象。
- 给指针赋空值和不赋值是两个截然不同的概念:赋空值表示它是空值,不指向任何对象;而未对指针赋值(只定义,不初始化)时,指针将是一个无法预料的值,对它的引用是很危险的,可能会产生不可预料的后果。
- NULL在"stdio.h"中被预定义,其值实际为0。
- i_p2 = 0是合法的,而i_p2=2000语句就不合法了,即使某个变量的地址等于2000,也不允许这样使用,而只能通过运算符"&"来获取变量的地址,否则会警告“不同级别的间接寻址”。编译时会出现的错误提示为“'=':'int *' differs in levels of indirection from 'const int'”。
(2)算术运算
一个指针变量加、减一个整数n:指针加、减一个整数n可以实现指针的移动,这在数组、链表等的操作中使用较多。但是需要注意的是,指针的移动量与它的基类型有着密不可分的关系。
【例5】假设有如下的变量声明及初始化,分析指针的变化情况。
#include <stdio.h> #include <stdlib.h> int main(void) { short nNum, *npointer; npointer = &nNum; //指针的初始化 npointer++; //指针加1运算 system("PAUSE"); return 0; }
分析:本例中指针的基类型为短整型,短整型数据在存储单元中占用2个字节,尽管是给指针变量加1,但它的移动量为2,如图13所示。所以搞清楚基类型在内存中所占用的字节数很重要,如图14所示,是在Visual Studio 2015环境下,不同的数据类型占用字节数的情况。
图13 例5指针的移动分析
#include <stdio.h> #include <stdlib.h> int main(void) { printf("char :%d Bytes ", sizeof(char)); printf("signed char :%d Bytes ", sizeof(signed char)); printf("unsigned char :%d Bytes ", sizeof(unsigned char)); printf("short :%d Bytes ", sizeof(short)); printf("signed short :%d Bytes ", sizeof(signed short)); printf("unsigned short :%d Bytes ", sizeof(unsigned short)); printf("int :%d Bytes ", sizeof(int)); printf("signed int :%d Bytes ", sizeof(signed int)); printf("unsigned int :%d Bytes ", sizeof(unsigned int)); printf("long :%d Bytes ", sizeof(long)); printf("signed long :%d Bytes ", sizeof(signed long)); printf("unsigned long :%d Bytes ", sizeof(unsigned long)); printf("float :%d Bytes ", sizeof(float)); printf("double :%d Bytes ", sizeof(double)); printf("long double :%d Bytes ", sizeof(long double)); system("PAUSE"); return 0; }
代码运行情况如图14所示。
图14 不同数据类型占用字节数情况
【例6】分析下面程序的运行结果。
#include <stdio.h> #include <stdlib.h> int main(void) { int iNum = 6, *ipointer; ipointer = &iNum;//指针的初始化 printf("%d,%d,%d ", &ipointer, &(*ipointer), *ipointer); ipointer += 1;//指针加1运算 printf("%d,%d,%d ", &ipointer, &(*ipointer), *ipointer); system("PAUSE"); return 0; }
运行结果如图15所示。
图15 例6的运行结果
注意:在例6中,i_pointer+=1之后,*i_pointer的引用就很危险了,请分析下为什么?
关于指针的算术运算总结如表2所示。
表2 指针的算术运算
运算 | 含义 |
ipointer = ipointer+n | 表示指针ipointer向高地址端移动n个存储单元块 |
ipointer = ipointer-n | 表示指针ipointer向低地址端移动n个存储单元块 |
ipointer++ | 先引用ipointer,再将指针ipointer向高地址端移动1个存储单元块 |
++ipointer | 先将指针ipointer向高地址端移动1个存储单元块,再引用ipointer |
ipointer-- | 先引用ipointer,再将指针ipointer向低地址端移动1个存储单元块 |
--ipointer | 先将指针ipointer向低地址端移动1个存储单元块,再引用ipointer |
注:1个"存储单元快"的字节数 = n * sizeof(指针基类型)。
两个指针相减:如图16所示。如果两个指针i_pointer1和i_pointer2同时指向一个数组的两个不同元素,则i_pointer2 - i_pointer1等于3。
图16 两个指针相减
(3)关系运算
指针变量也可以进行关系运算:<、<=、==、>=、>、!=。关系运算的结果为逻辑量(“真”或“假”)。指针在进行关系运算之前必循已经初始化,并且两个参加关系运算的指针的基类型要相同。如图17所示,ch_pointer2 > ch_pointer1,表达式的结果为“真”,因为ch_pointer1所指的单元在低地址端,ch_pointer2所指的单元在高地址端。
图17 指针的关系运算
关系运算的具体含义如表3所示。
表3 指针的关系运算
运算 | 结果为“真”时的含义 |
pointer1 < pointer2 | 说明pointer1所指的单元在低地址端,pointer2所指的单元在高地址端 |
pointer1 <= pointer2 |
说明pointer1所指的单元在低地址端,pointer2所指的单元在高地址端 或pointer1和pointer2指向同一个单元 |
pointer1 == pointer2 | 说明pointer1和pointer2指向同一个单元 |
pointer1 >= pointer2 |
说明pointer1所指的单元在高地址端,pointer2所指的单元在低地址端 或pointer1和pointer2指向同一个单元 |
pointer1 > pointer2 | 说明pointer1所指的单元在高地址端,pointer2所指的单元在低地址端 |
pointer1 != pointer2 | 说明pointer1和pointer2指向不同的单元 |
注:假设pointer1、pointer2是两个已经定义、初始化好的指针,且基类型相同。
5、指针和数组
5.1、指向一维数组元素的指针
一个数组是由若干个元素组成的,数组元素的实现和变量一样,都是在存储单元中实现存储的,数组元素按其类型给其分配连续的内存单元进行存储。一个数组由连续的一块内存单元组成。
- 数组的指针:所谓数组的指针,是指数组的起始地址。C语言规定,数组名就代表数组的首地址。
- 数组元素的指针:数组元素的地址就是数组元素的指针。
声明一个指向数组元素指针变量的方法,与以前介绍的指针变量声明相同。
【例 7】指向一维数组的指针示例。
int a[10], *p;//定义数组,指针变量 p = &a[0];//初始化指针变量,p指向数组a的第1个数组元素的地址 p = a;//初始化指针变量,等价于p = &a[0],数组名就代表数组的首地址
或在定义时初始化
int a[10], *p = &a[0];//定义时初始化
或
int a[10], *p = a;
图18 指向数组的指针示例
分析,如图18所示,说明指针和数组的关系以及初始化的方法。数组名代表数组在内存中的起始地址(与第1个数组元素的地址相同),所以也可以用数组名给指针变量赋值。
【例 8】分析程序的运行结果。
#include <stdio.h> #include <stdlib.h> int main(void) { int A[4] = {0,22,5,48};//数组定义及初始化 int *i_pointer1, *i_pointer2; i_pointer1 = A;//初始化指针i_pointer1指向A[0] i_pointer2 = &A[3];//初始化指针i_pointer2指向A[3] if (i_pointer2 > i_pointer1) printf("i_pointer2 > i_pointer1 "); else if(i_pointer2 == i_pointer1) printf("i_pointer2 == i_pointer1 "); else printf("i_pointer2 < i_pointer1 "); printf("i_pointer2 - i_pointer1 =%d ", i_pointer2 - i_pointer1);//计算两个指针的差 system("PAUSE"); return 0; }
分析:例8的运行结果及分析如图19图20所示,i_pointer2较i_pointer1指向了数组的高位地址,所以i_pointer2 > i_pointer1,并且差为3。
图19 例8的运行结果
图20 例8的运行结果分析
思考:i_pointer2 - i_pointer1等于两指针之间的数据占用的字节数吗?