裘宗燕从问题到程序(2003年修订),第七章,指针1第七章指针本章讨论指针及其在程序中的重要作用,包括变量地址与指针的概念,C程序里指针的基本定义和使用,指针和数组的关系,函数的指针参数,动态存储分配等问题。7.1地址与指针程序执行中所用的数据都存于内存。任何数据对象在能用期间都有存储位置,占据一定数量的存储单元。内存单元按顺序排,每个单元有一个称为内存地址的编号,内存数据都是通过地址访问的。高级语言把存储单元、地址等低级概念用变量等高级概念掩盖起来,使写程序时可以不必过多关心这方面细节。但内存与地址等仍是最基本的重要概念。前面讲的变量存在期概念实际上与存储有密切关系。建立变量就是为它分配所需的(一批)存储单元。给变量赋值是将值存入对应单元;使用变量值时从相应单元中取用。外部变量和静态局部变量的存在期贯穿整个程序执行期,其存储位置在程序开始前确定,并保持到程序结束。局部自动变量则不同,设x是函数fun里定义的自动变量,只有执行进入x的定义所在的复合语句时才为x确定存储。x占据该块存储直至执行离开这个复合语句。如果执行再次进入该复合语句(包括fun再次执行),就会再次为x分配存储,但是其位置与前一次无关。这些情况决定了自动变量的各种特性。变量存在期就是它占据被分配存储位置的期间。虽然不同变量在这方面的性质不同,但它们在存在期里都有一个固定地址*。既然变量都有地址,地址也用二进制编码,那么就有可能将地址作为处理的数据。问题是这样做有什么价值。许多高级语言把程序对象(如变量)的地址作为一种可处理数据,称为地址值或指针值,以地址为值的变量称为指针变量,简称指针(pointer)。我们知道,机器语言层对各种对象的操作都要通过地址。指针变量里保存程序对象的地址,通过它们就可以访问和处理有关对象。高级语言里的指针是访问程序对象的手段,以便能更灵活方便地实施操作。对指针变量的操作包括:(1)将程序对象的地址(如变量地址,还有其他情况。为简单起见,下面以变量为例)存入指针变量,这称为指针赋值。当一个指针变量保存了某个变量的地址时,也说该指针指向了那个变量。(2)通过指针访问被指对象(变量),称为间接访问。指针变量p指向变量x的情况用图7.1示意。由于指针值是数据,指针变量可以赋值,所以一个指针的指向在程序执行中可以改变。指针p在执行中某时刻指向变量x(如图7.1所示),在另一时刻也可以指向变量y(不应对此感到奇怪,就像一个整型变量在某时可能保存着0,另一时刻可能保存着2)。这样,同一个通过p使用被它指向的对象的语句,在前一时刻访问的就是x,后一时刻访问的就是y。这样就带来了新的灵活性,下面我们将看到这种功能的价值。C语言的指针比其他语言的指针更灵活,功能更强,理解和*这种情况的例外是C语言的寄存器变量,它们可能被安排在CPU的寄存器中。这种变量没有可用的存储地址。本章所讨论的问题都把寄存器变量排除在外。指针变量p变量x图7.1 指针与被指的变量裘宗燕从问题到程序(2003年修订),第七章,指针2掌握都有一定难度,也比较容易用错。因此请读者在学习中特别注意思考,以逐步理解其本质,掌握正确的使用方法。下面也特别解释了指针使用中的一些常见错误,请读者留意。指针是C语言的一种重要机制。用好指针机制常常可以使写出的程序更简洁有效。也有些问题必须借助指针才能处理。指针在较大的复杂软件的中使用广泛。可以说,指针使用能力是评价一个人C程序设计水平高低的一个重要方面。此外,指针也是大部分高级语言都提供的重要机制。掌握了C语言的指针机制后也可以触类旁通。7.2指针变量的定义和使用C语言的指针有类型,每个指针只能指向一种特定类型的变量,保存这种类型变量的地址。例如,如果p是指向int变量的指针,那么p就只能指向int型变量,而不能指向其他类型的变量。因此,程序里也认为p所指向的总是int,从p间接访问的东西总作为整型变量看待。指向整型变量的指针也简称为整型指针。人们常说“int指针p1”、“double指针p2”等等。定义指针变量时需要用类型名说明指向类型,在被定义的指针变量名前面加星号,说明定义的是指针变量。多个同类型指针可以一起定义,例如,下面定义了两个指向整型变量的指针变量(int指针)p和q:int*p,*q;指针变量也可以与其他变量一起定义。在下面定义里,不仅定义了三个整型指针,还定义了一个整型数组和另外两个整型变量:int*p,n,a[10],*q,*p1,m;整型指针的类型用(int*)表示,其他指针的类型表示形式类似。对每个类型都可以定义相关的指针类型,C语言把指针类型也看作基本类型。所有指针占用的存储都一样大,通常是一个机器字的大小。7.2.1指针操作取变量地址的操作用一元运算符&;间接访问操作用一元运算符*,也称间接操作。这两个运算符与其他一元运算符的优先级相同,自右向左结合。取地址运算将取地址运算符&放在变量描述(最简单情况就是变量名)前,就求出该变量的地址,这是一个相应类型的指针值,可以赋给类型合适的指针(变量)。有了前面定义,可以写:p=&n;q=p;p1=&a[1];第一个语句将n的地址值赋给指针p。这个赋值合法,因为n的类型与p所需的类型匹配,int指针可以指向任何int变量。赋值后p指向变量n,通过p就可以间接访问n了。第二个语句把p的值赋给指针q,这将使q也指向变量n。可见,两个同类型指针可以指向同一个变量。p和q都指向n的情况如图7.2所示。指针变量可以做相等判断。相等就是值相等,对指针变量而言,值相等意味着两个指针指向同一位置。在图7.2的情况下p和q相等。上面第三个语句的右边表达指针变量p变量n图7.2两个指针指向同一个变量的情况指针变量q裘宗燕从问题到程序(2003年修订),第七章,指针3式取出数组a中下标为1的元素的地址。由于a是整型数组,其元素相当于整型变量,这一地址也是指向整型变量的指针值,可以赋给指针p1。由于数组成员访问运算[]优先级更高,赋值号右边的表达式里不必写括号。间接运算间接运算由指针得到被指变量。这种表达式可以像普通变量一样用:放在表达式里表示取值参加运算;或放在赋值运算符左边给被指变量赋值。下面是一个间接赋值:*p=17;因为当时p指向变量n,写*p就相当于直接写变量n,因此这个操作完成的是给变量n赋值17。下面是另一个赋值语句:m=*p+*q*n;在这个语句里实际访问了变量n三次(两次间接访问、一次直接访问)。由于变量n当时的值是17,变量m被赋的值是由表达式计算出的306。下面是另一些指针使用的例子及其解释(假定接着上面语句继续做):++*p;/*使变量n的值加1,变成18*/(*p)++;/*使变量n的值再加1,变成19。由于结合性的规定,*p++的意义与此不同*/*p+=*q+n;/*变量n被赋以新值57*/q=&a[0];/*指针q指向了数组a的元素*/*q=*p/16;/*a[0]被赋值3*/7.2.2指针作为函数的参数仅从上面讨论还看不到指针的意义。现在讨论一个在C程序里必须借助指针解决的问题:函数的指针参数。利用这种参数能写出可以改变函数调用时环境的函数。所谓函数调用时环境,指的是在函数调用处能访问的所有变量。下面从一个例子谈起。假设程序里常要交换两个整型变量的值,我们想为此写函数swap,希望调用swap能交换两个变量的值。由于操作中需要改变两个变量,显然不能靠返回值(返回值只有一个)。不仔细考虑也可能认为这个问题很简单,有人可能写出下面函数定义:voidswap0(intx,inty){intt=x;x=y;y=t;}写一段程序定义变量并实际调用这个函数,例如写出如下程序段:intm=1,n=2;swap0(m,n);执行后会发现变量m和n的值没有变。上述定义失败的原因在于C语言的参数机制:调用swap0时m和n的值送给形参x和y,虽然函数里面交换了x和y的值,但不会影响调用的实参m和n。调用结束时局部变量x和y被撤消,m和n的值没有变。前面所有函数有一个共同点:它们可以通过参数使用调用环境中变量的值,但不能改变那里的变量值*。在函数f里以局部变量m调用g(m),绝不会改变m。要想让g能改变调用处可用的m,必须在g内部把握住m。利用指针机制可以解决这个问题:在调用时把m的地址(这也是值,地址值)通过指针参数传进函数g,在g里对参数指针间接就能完成对m的各种操作,包括对m赋值。总结一下,利用指针解决问题的方案包括三方面:函数定义时用指针参数;函数里通过指针参数间接访问被指变量;函数调用*以数组作为函数的实际参数是例外。关于数组参数的实际意义,本章也将给出一个明确解释。这种情况下能改变实际参数数组元素的原因也与指针有关。裘宗燕从问题到程序(2003年修订),第七章,指针4时把变量地址传给函数。图7.3是调用的现场情况。这样,函数swap应定义为:voidswap(int*p,int*q){intt=*p;*p=*q;*q=t;}现在swap有两个整型指针参数。假设需要交换值的变量是m和n,调用形式应该是:swap(&m,&n);调用中m和n的地址传递给了函数的指针参数p和q。函数体里通过对p和q的间接访问,就能交换m和n值了。函数调用时形参与实参的关系如图7.4所示。请注意,swap定义的参数类型是(int*),调用时的实参必须是合法的整型变量地址。假设有下面变量定义:inta[10],k;下面两个调用都是合法的:swap(&a[0],&a[5]);swap(&a[1],&k);如果有关整型变量和数组都已有值,这些调用将完成值交换工作。读者应想到标准库函数scanf。前面介绍scanf时我们反复强调,接受输入值的变量前必须写符号&。那就是为了取得变量地址并把这个地址传入函数。scanf采用的就是上面定义swap所用的技术,通过间接访问方式为指定变量赋值。根据什么介绍,不难想清楚scanf如何把它得到的输入值赋给我们指定的变量。例:改造上一章最后的输入整数值并检查数值范围的函数,通过引进一个指针参数,使之能更好地处理整数输入中出现错误的问题。前面讨论了函数的实现方法,但那里要用一个特殊整数值指明输入出错情况,遗留下了一个需要改进的缺陷。指针参数提供了一种解决问题的新方法。我们可以给函数增加一个指针参数,通过它送回实际读入的值。这样就可以使函数返回值空出来,专门用于传递函数的执行状态信息。我们用1表示函数成功读入了一个整数,用0表示输入遇到麻烦而没有正常完成输入。修改后的函数定义是:intgetnumber(charprompt[],intimin,intimax,intrepeat,int*np){inti;*np=0;//为了安全,保证函数里一定给*np赋了值。for(i=0;repeat=0||irepeat;++i){printf(%s,prompt);if(scanf(%d,np)!=1||*npimin||*npimax){printf(Wronginput.Correctrange[%d,%d].\n,imin,imax);while(getchar()!='\n');}elsereturn1;}return0;}前面的调用现在可以重写为:getnumber(Choosearange[0,n].Inputn:,2,32767,5,&m);getnumber(Yourguess:,0,m-1,5,&guess);g的函数体参数p变量m在函数体里用*p可以访问和改变函数外面的变量m.图7.3在函数里通过指针可以访问外面的变量swap的函数体p变量m图7.4函数调用swap(&m,&n)形成的现场变量nq裘宗燕从问题到程序(2003年修订),第七章,指针5调用这一函数的更合适形式应该是,例如:if(getnumber(Yourguess:,0,m-1,5,&guess)==0){/*输入出错处理程序片段*/}注意,虽然这类函数的参数被定义为指针,作为实际参数必须是某个变量的地址。将这一函数代换到原程序中是非常简单的,这一工作请读者自己完成。从这个函数实例中可