再上一篇:7.3 共同体的定义及应用
上一篇:7.4 枚举类型
主页
下一篇:8.2 指针与数组
再下一篇:8.3 指针数组与指向指针的指针变量
文章列表

第八章 指针和引用

《VC++程序设计基础》,讲述C++语言的语法和标准库,以及Visual C++ 函数库和MFC类库的使用,并附相关代码示例。

本章是本书的重点之一,指针的使用是比较复杂的,但又是十分重要的。正确灵活地应用指针,可有效地使用各种复杂的数据结构;能动态地分配内存空间;能更有效更方便地使用数组和字符串;能编写通用的程序等。正确熟练地掌握指针的应用,设计的程序可以简洁、高效。但是,若程序中不正确地使用指针,则容易导致程序运行时的错误,或导致系统的崩溃。在学习本章时,一定要理解指针的本质,并能正确应用指针。对于初学者,指针的概念和用法不容易掌握,学习时要认真领会其特点和本质。

8.1指针及指针变量

8.1.1 指针的概念

当我们说明一个变量时,编译程序要为这个变量分配一个连续的内存(主存)单元。为了区分不同的内存单元,必须给每一个内存单元指定一个唯一的编号,这个编号称为内存单元的地址。目前,多数计算机是以一个字节(八个二进位)作为一个最小的内存单元。内存单元的地址类同于一个旅馆中的房间号码,存放在内存单元中的数据类同于住在某一房间号码中的旅客。当知道一个旅客住在一个旅馆时,只要知道其房间号码,就能找到这个旅客。同样地,只要知道了为变量分配的内存单元的地址,就可以使用或改变这个变量的值。当然,每一个房间的“房间号码”(内存单元的地址)是唯一的,是不可改变的;而在房间内的“旅客”(变量的值)是可以经常改变的。

设有说明语句:

char c1=’a’;

float x=50.5;

编译程序在编译时,或在程序的执行期间要为变量c1和x分配内存单元,设分配的内存单元分别为20001和20004,如图8.1所示。

20001: a 20004: 50.5

图8.1 变量的地址与变量的值

图中的20001和20004为地址,为变量c1分配了一个字节的内存单元,而为变量x分配四个字节的内存单元。变量c1的值a存放在20001的内存单元中,而变量x的值50.5存放在以20004开始的四个连续的字节(内存单元)中。在变量c1和x的生存期内,为其分配的内存单元地址是不变的,当改变变量的值时,存放在这二个地址中的值随之变化。

注意,应该区分变量的地址和变量的值的概念,在编译或执行期间为每一个变量分配的内存单元的编号(地址)称为变量的地址;而该内存单元中的内容称为变量的值。变量的地址称为变量的指针,简称为指针,即指针是一个内存单元的地址。当定义(说明)一个变量时,其值总是用来存放一个内存单元的地址(指针)时,称这种变量为指针变量。换言之,一个指针变量的值一定是另一个变量的地址。引入指针变量的目的是提供一种对变量的值进行间接访问的手段;根据指针值,可以使用或修改该指针所指向的内存单元中的值。

8.1.2 指针变量的说明

指针变量与其它类型的变量一样,必须先说明后使用,说明指针变量的一般格式为:

《存储类型》 <类型> *<变量名1> 《,*<变量名2>,......》;其中存储类型是可任选的;变量名前的星号*指明所说明的变量为指针变量;而类型则指出指针变量所指向的数据类型,即指针所指向的内存单元中存放的数据值的类型。例如:

int *p1, *p2 ,i, j;

float *p3, *p4 , x ,y ;

char *pc, c;

说明了五个指针变量p1, p2,p3, p4和pc,它们分别为整型、实型和字符型指针变量。

有关指针变量,说明以下几点:

1)在变量说明语句中,变量名前的星号*具有特定的意义,它表示这种变量是指针型变量。如例中的 p1,p2 为指向整型数据的指针型变量;p3, p4 为指向实型数据的指针型变量。

2)指针型变量的值只能是某一个变量地址(起始地址),在说明指针变量时,通常其值是不确定的(静态存储类型、文件作用域类型的变量除外)。只有对指针变量赋值后,才能使用它。

3)编译程序也要为指针变量分配内存单元,因为指针变量的值是一个地址,其取值范围是不变的,通常用四个字节来表示地址值;所以,为不同类型的指针变量所分配的内存单元的大小是相同的。对于上例中的变量说明,为指针变量p1, p2,p3, p4和pc各分配四个字节大小的内存空间。

4)定义指针类型变量时,其类型定义了指针变量所指向的数据类型,这类型确定了数据占用的存储空间的大小。 如,p1,p2 所指向的数据为整型,故占用四个字节的空间,而pc 所指向的数据为字符型,它占用一个字节的存储空间。

5)在说明指针变量的同时也可以对它进行初始化。通常是用与指针变量同类型变量的指针来进行初始化。如:

int j , *p=&j;

说明了整型变量j和整型指针变量p,同时将变量p的初值置为变量j 的起始地址。运算符“&”称为地址运算符,这是一个一元运算符,它的运算结果是取其操作数的地址值。该运算符的操作数只能是一个变量或对象。在程序中,要得到一个变量的地址时,可用这运算符来获得。

在C++中,允许将一个整型常数经强制类型转换后,来初始化指针变量。如:

int *pp= (int *)0x5600;

则将指针变量pp的初值置为0x5600,即使pp指向地址为0x5600的内存单元。这种初始化方法,只有在设计系统程序或对计算机硬件方面非常清楚内存单元的作用时,才是有意义的。否则,这种初始化的物理意义不仅不明确,而且还可能产生极为严重的后果。因此,对于初学者来说,不能使用这种方法来对指针变量初始化。

8.1.3 指针可执行的运算

指针的运算(操作)只有赋值运算、关系运算和算术运算三种。

8.1.3.1 指针的赋值运算

指针的赋值运算是将一个地址值赋给一个指针变量。这种赋值有三种情况:

1、可以将与指针变量同类型的任一变量的地址赋给指针变量。

例8.1 指针的赋值运算和算术运算。

#include

void main(void )

{

int a1 = 1 , a2 = 2;

int *p1,*p2, *p3 ;

float *fp1, *fp2;

float b1 = 23.5 , b2 = 55.6;

p1 = &a1; //A 将变量a的地址赋给p1

p2 = p1 ; //B 同类型的指针变量之间的赋值

p3 = &a2;

fp1 = &b1; //C

fp2 = &b2;

cout << "*p1="<<*p1<<" *p2=" <<*p2<<" *p3="<<*p3<<"\n"; //F

cout <<"p1 = " << p1 << " p3 = " << p3<<'\n';

cout << "*fp1 = " << *fp1 <<" *fp2 = " << *fp2 << '\n'; //G}

执行程序后的输出为:

*p1=1 *p2=1 *p3=2

p1 = 0x0065FDDC p3 = 0x0065FDD8

*fp1 = 23.5 *fp2 = 55.6

程序中的A行是将整型变量a的地址赋给指针变量p1,经这种赋值后,称指针变量p1指向变量a1。

当定义了一个指针变量,并对其赋初值后,就可以在程序中访问该指针变量。对指针变量的访问一般有二种形式:一是访问指针变量的值;二是访问指针变量所指向的内存单元中的数据。第二种访问形式称为访问指针的内容。对指针值的访问通常是将一个指针变量的值赋给另一个指针变量或者是进行指针的运算。通常,程序设计者并不并心指针变量的具体值,而只关心它指向哪一个变量。上例中的B行是将指针变量p1的值赋给指针变量p2,这是同类型的指针变量之间的赋值。这种赋值,使得p1和p2都指向变量a,在图8.2中给出了指针的指向关系。

a:

p1:

1

p2:

图8.2 二个指针变量指向同一个变量

要访问指针变量所指向的内容时,要用到运算符“*”,该运算符称为取内容运算符,它是一个一元运算符,要求一个指针作为它的操作数。该运算符的运算结果为取其操作数所指向的内存单元的值(变量值)。如上例中的F行,*p1表示取出p1所指向的内容,即值为1。又如:

int *ip1,*ip2 ,i=100,j ;

ip1=&i; ip2=&j;

*ip2 = *ip1 +200; //将300赋给ip2所指向的内存单元

当指针运算符出现在赋值运算符的左边时,表示将计算结果赋给指针变量所指向的变量。因ip2指向变量j,经以上的赋值运算后,j的值为300。

2、在C++语言中可以将0赋给任一指针变量,其含义是初始化指针变量,使其值为“空”。实际上是告诉系统,指针值为0的指针变量不指向任一内存单元,即不指向任一变量。如程序:

#include

void main(void )

{

int *pt;

*pt=0;

*pt=100; //A

cout<<*pt<<’\n’;

}

这程序能正确编译和连接,在执行程序时,当执行到A行时,系统提示“该程序执行了非法的操作”,并终止程序的执行。因pt不指向任一内存单元,当然就不允许向pt所指向的内存单元赋值。

类同于其它类型的变量,当被说明为静态存储类型的指针变量或全局的指针变量时,其缺省的初值为0。

C++中允许将一个整型常数经强制类型转换后赋给一个指针变量。如:

float *fpp;

fpp= (float *) 5000;

表示将5000作为一个地址值赋给指针变量fpp。因为一般的程序设计者并不知道每一个存储单元的用途,这种赋值不但通常没有意义,而且是非常危险的,程序执行时,有可能破坏系统,造成系统不能正常运行。

应当强调指出的是,向一个未初始化的指针变量所指向的内容赋值是极其危险的,并且是不允许的。如以下的程序:

#include

void main(void )

{

int *pp;

cout<<"输入一个整数:";

cin>>*pp;

cout<<*pp<<'\n';

}

该程序可以被编译和连接,当执行时输入100,则输出的值也是100。似乎程序是正确的,实际上程序中存在着潜伏的危险。因指针变量pp是一个局部变量,编译程序为pp分配了一个存储空间,并不对其作任何初始化工作,pp的值是一个随机值。该值所表示的内存单元可能是没有用到的空闲空间;但是,若该值所表示的内存单元是一个系统正在使用的关键内存单元,把数据写入这内存单元,轻者导致程序不能正确执行,重者导致系统出错或系统崩溃。对于初学者尤其要注意,在程序中绝对不允许出现类似情况。

3、同类型的指针变量之间可以相互赋值,而不同类型的指针变量之间的赋值经强制类型转换后尽管是允许的, 但通常是没有意义的。如程序:

#include

void main(void )

{

int i= 100 , *p1 ;

float x = 2.5, *p3;

p1 = &i;

cout << "*p1 = " << *p1 <<"\n";

p3 = &x;

cout << "*p3 = " << *p3 << "\n"; //A

p1 = (int * )p3; //B

cout << "*p1 = " << *p1 << "\n"; //C

}

执行程序后的输出结果为:

*p1 = 100

*p3 = 2.5

*p1 = 1075838976

显然最后一行的输出结果是错误的。A行中取p3所指向的数据时,是按实数来取的,能输出正确的结果2.5;经B行赋值后,尽管p1和p3的值相同,但C行中取p1所指向的数据时,是按整数来取的,所以输出的值不是2.5。

注意,由于地址的取值范围是相同的,所以,当说明不同类型的指针变量时,编译程序为指针变量所分配的存储空间的大小是相同的,目前多数计算机上大小均为四个字节。如:

char *pc1;

int *pi1;

double *pd1;

系统分别为指针变量pc1,pi1和pd1各分配四个字节的内存空间。

8.1.3.2 指针的算术运算

可以对指针进行算术运算,但在实际使用中主要对指针进行加或减的运算。加减运算又可以分为二种:一是对指针变量的++或--操作,二是指针变量值加或减一个整型常数。

1、指针型变量可以执行++或--操作,其含义并不是指针变量的值进行加1或减1的操作;而是使指针变量指向下一个或上一个元素。如:

<指针变量>++;

计算机内部是按下式计算的:

<指针变量> = <指针变量> + sizeof(<指针变量类型>)

又如:

<指针变量>--

计算机内部是按下式计算的:

<指针变量> = <指针变量> - sizeof(<指针变量类型>)

例8.2 指针变量的++和—运算。

#include

void main(void )

{

int a[10]={100,200,300,400,500};

int i= 10 , j=20,*p1=&j,*p2=&a[0] ;

char c1[5]="abcd",*pc1=&c1[0];

cout<<"*p1="<<*p1<<'\t'<<"p1="<<(int)p1<<'\n';

p1++;

cout<<"*p1="<<*p1<<'\t'<<"p1="<<(int)p1<<'\n';

cout<<"*p2="<<*p2<<'\t'<<"p2="<<(int)p2<<'\n';

p2++;

cout<<"*p2="<<*p2<<'\t'<<"p2="<<(int)p2<<'\n';

p2--;

cout<<"*p2="<<*p2<<'\t'<<"p2="<<(int)p2<<'\n';

cout<<"*pc1="<<*pc1<<'\t';

cout<<"pc1="<<(int)pc1<<'\n';

pc1++;

cout<<"*pc1="<<*pc1<<'\t'<<"pc1="<<(int)pc1<<'\n';

}

执行程序后的输出为:

*p1=20 p1=6618552

*p1=10 p1=6618556

*p2=100 p2=6618576

*p2=200 p2=6618580

*p2=100 p2=6618576

*pc1=a pc1=6618568

*pc1=b pc1=6618569

对照源程序与程序的输出,可以看出,整型指针变量的值加1,实际运算是加4,因一个整数占用四个字节;字符型指针变量的值加1,实际运算也加1,因一个字符占用一个字节。指针变量p2开始时指向数组a的第0个元素,输出的*p2值为100;经加1后,它指向数组a的第一个元素,输出的*p2值为200。

另外,根据程序的输出也可以看到:p1开始指向变量j,输出*p1的值为20;把p1的值加1后,它指向变量i,输出*p1的值为10。在C++中,同一个说明语句中所说明的变量,均分配在同一个连续的内存空间,但分配的顺序随不同的编译器而异,有的是按说明变量从左到右的顺序来分配内存空间的,而有的则是按从右到左的顺序来分配的。VC++编译器是按后一种方式分配的。因此,上面的程序在不同的编译程序环境下编译连接后,执行时输出的结果可能是不同的。

2、指针变量的值加或减一个整型常数。指针变量值加一个常数 n,类同于++运算,并不是简单地将指针值加n,而是使指针指向其后的第n个元素。如程序中:

<指针变量>=<指针变量> + n;

计算机的实际运算为:

<指针变量>=<指针变量> + sizeof(<该指针变量的类型>)*n;

例8.3 指针变量加一常数。

#include

void main(void )

{

int a[10]={100,200,300,400,500};

int *p2=&a[0] ;

cout<<"*p2="<<*p2<<'\t'<<"p2="<<(int)p2<<'\n';

p2=p2+4; //A

cout<<"*p2="<<*p2<<'\t'<<"p2="<<(int)p2<<'\n';

p2=p2-2; //B

cout<<"*p2="<<*p2<<'\t'<<"p2="<<(int)p2<<'\n';

}

执行程序后的输出为:

*p2=100 p2=6618576

*p2=500 p2=6618592

*p2=300 p2=6618584

指针变量p2开始指向数组a的第0个元素,A行使p2的值加4,则使p2指向数组a的第4个元素;B行使p2的值减2,则使p2指向数组a的第2个元素。8.1.3.3 指针的关系运算

指针变量可以进行关系运算,二个指针变量的关系运算是根据二个指针变量值的大小(作为无符号整数)来进行比较的。通常只有同类型的指针变量进行比较才有意义,相等比较的含义是判二个指针变量是否指向相同的内存单元,即二个指针值是否相同;而不等比较的含义是判二个指针变量是否指向不同的内存单元。当指针变量与0比较时,表示要判指针变量的值是否为空。

例8.4 指针变量的关系运算。

#include

void main(void )

{

int a[5]={100,200,300,400,500};

int *p2,*p1;

for( p2=&a[0];p2<=&a[4];p2++) //A

cout<<*p2<<'\t';

cout<<'\n';

p1=&a[0]+5; //B

p2=&a[0]; //C

int sum=0;

while(p2 != p1) //D

sum+=*p2++; //E

cout<<"元素之和为:"<

}

执行程序后的输出为:

100 200 300 400 500

元素之和为:1500

程序输出数组a的各个元素值和数组的五个元素之和。程序中的B行使指针p1指向数组a的最后一个元素的后面;A行的循环语句执行完后,p2已指向数组a最后一个元素的后面,C行使p2重新指向数组的第0个元素;D行中的条件成立时,表示还没有历遍a数组中的所有元素。对于E行中的*p2++,由于运算符++和*的优先级相同,并按从右到左的顺序计算,先进行p2++的运算,即取出p2的值参加接着的运算,然后使p2的值加1。因此,E行的一个语句等同于以下二个语句:

sum += *p2; p2++;

关系运算符<,<=,>,>=主要用于对数组元素的判断。上例说明了关系运算符“<=”的用法,其它关系运算符的用法完全类同。

从上面的几个简单的例子可以了解到,C++语言中指针变量的使用是比较复杂的。在编译源程序时, C++编译器只对指针的运算作语法上的检查,而不管指针的使用是否正确。指针的正确使用,必须由程序设计者自已来保证。一旦错误地使用了指针,在执行程序时,可能导致程序的运行错误或系统崩溃。

在 C++语言中,同一个符号可能表示不同的运算符。如“*”既表示乘法运算符,又表示指针运算符,编译器是根据运算符的优先级、操作数的类型及个数来区分的。作为乘法运算符时,它必须有二个操作数;而作为指针运算符时,它只有一个操作数。并且指针运算符的优先级要比乘法运算符的优先级高。同样地,“&”符号可表示“按位与”运算符,又可表示取地址运算符。在 C++中,对所有的运算符都规定了优先级, 我们要特别注意以下几种运算符的混合运算及其优先级:

1、指针运算符“*”和取地址运算符“&”的优先级相同,按自右向左的方向结合。设有说明语句:

int a[ 5]={100,200,300,400,500}, *p1=&a[0],b;

对于表达式“&*p1”,其求值顺序是先进行*运算,再进行&运算,即先求出p1所指向的变量,再求出该变量的地址。因此,该表达式的值为变量 a[0]的地址,它等同于& a[0]或p1的值。对于表达式 “*&a[0]”,则是先进行&运算,得到变量a[0]的地址,再进行*运算,求出该指针所指向的变量,即等同于a[0]。

2、++,-- 、指针运算符*和取地址运算符&的优先级相同,按自右向左的方向结合。如,以下表达式:

p1=&a[0],b=*p1++

对于*p1++,先进行++运算,再进行*运算,因++是后置运算符,它等同于取*p1的值参加运算,再使指针p1的值加1。执行的结果是:b的值为100,p1指向数组a的第一个元素。表达式:

p1=&a[0],b=*++p1

而对于*++p1,则是先进行++运算,再进行*运算,因++是前置运算符,先使指针p1的值加1,然后再取*p1的值参加运算。执行的结果是:b的值为200,p1指向数组a的第一个元素。表达式:

p1=&a[0],b= (*p1)++

对于(*p1)++,因括号内的运算优先,故先进行*运算,再进行++运算,即取出*p1的值参加接着的运算,再完成*p1的值加1的运算。执行的结果是:b的值为100,p1仍指向数组a的第零个元素,并把a[0]的值修改为101。表达式:

*(p1++)

等同于表达式*p1++。表达式:

p1=&a[1],b=++*p1

其中表达式++*p1,先进行*运算,再进行++运算,即取出p1所指向的内容,使其加1后参加接着的运算。执行的结果是:b的值为201,a[1]的值修改为201,p1仍指向数组a的第一个元素。