指针及其语义和运用
本篇是《C++从零开始》系列的附篇。因友人一再认为《C++从零开始》系列中对指针的阐述太过简略,而提出的各个概念又杂七混八,且关于指针这一C++中的重要概念的运用少之又少,故本篇重点说明在《C++从零开始》系列中提出的数字、地址、指针等基础概念,并给出指针的语义,说明指针和数组的关系,阐述多级指针、多维数组、函数指针、数组指针、成员指针的语义及各自的运用。
数字、操作符、类型、类型修饰符
在《C++从零开始(三)》中已经说明,其实CPU连二进制数都不认识,其只能处理状态,而它能处理的状态恰好能用二进制数表示,故称CPU只认识二进制数。应注意由于CPU认识二进制数是认识其所表示的状态,并不是数学意义上的二进制数,因此CPU并不认识十进制数20。不过将20按数学规则转成二进制数10100后,运气极好地CPU的设计人员将加法指令定义成状态10100和状态10100相加将得到状态101000,而这个二进制数按数学规则转成十进制数正好是40,即CPU连加减乘除都不会,只会以不同的方式改变状态,而CPU的设计人员专门将那些状态的改变方式定义成和数学上的加减乘除一样进而使CPU表现得好像会加减乘除。
所以,为了让CPU执行一条指令,则那条指令所涉及的东西只能是二进制数,就必须有规则将给出的数学意义上的数转换成二进制数。如前面的十进制转二进制的规则,在《C++从零开始(二)》中提到的原码、补码、IEEE real*4等。而要编写C++代码,则一定要在代码中能体现上述的转换规则,并且要能在代码上体现欲被转换的数学意义上的数,这样编译器才能根据我们书写的代码来决定CPU要操作的东西的二进制表示。对此,C++中用类型表现前者,用数字体现后者,用操作符表示CPU的指令,即CPU状态的变换方式。
因此,为了让CPU执行加法指令,代码上书写加法指令对应的操作符——“+”,“+”的两侧需要接两个数字,而数字的类型决定了如何将数字所表示的数学上的数转换成二进制数。应注意数字是编译级的概念,不是代码级的概念,即无法在代码上表现数字,而只能通过操作符的计算的返回来获得数字。因为任何操作符都要返回数字(不返回数字的操作符也可以通过返回类型为void的数字来表示以满足这一说法),而最常见的一种得到数字的操作符就是通常被称作常数的东西,如6.3、5.2f、0772等。我在《C++从零开始(二)》中将其称作数字的确引起概念混淆,在此特澄清。
应注意只要是返回数字的东西就是操作符,故前面的常量也是一种操作符。对于变量、成员变量及函数,在《C++从零开始》系列中已多次强调它们都是映射元素,直接书写变量名、成员变量名和函数名将分别返回各自所映射的数字,即变量名函数名等也都是操作符。
数字是具有类型的,C++提供了自定义类型struct、class等来自定义复杂的类型,但不仅如此,C++还提供了更值得称赞的东西——类型修饰符。在《C++从零开始(五)》中已经说明,类型修饰符就是修饰类型用的,即按某种规则改变被修饰类型(称作原类型)所表征的数字转换规则。如猪血羊血和猪肉羊肉,这里的“血”和“肉”都是类型修饰符,改变其各自的原类型——“猪”和“羊”。上面感觉更像后者修饰前者而非前者修饰后者,如猪血中的“血”是主语而“猪”是定语。即类型修饰符其实是以原类型的信息来修改其自身所表征的那个数字转换规则。这就如称“血”、“肉”是一种东西一样,也说某类型是指针类型、引用类型、数组类型、函数类型等。
在《C++从零开始》系列中共提出下面几种类型修饰符——引用“&”、指针“*”、数组“[]”、函数“()”、函数调用规则“__stdcall”、偏移“<自定义类型名>::”、常量“const”和地址类型修饰符。其中的地址类型修饰符是最混乱的。
在《C++从零开始(三)》中已经说明地址在32位操作系统中就是一个数,这个数经常以32位长的二进制数表示,以唯一标识一特定内存单元。而一个数字的类型是地址类型时(因为有地址类型修饰符,就好像一个数字是数组类型时),就将这个数字所代表的数学意义上的数用二进制表示,以标识出一个内存单元,然后按照原类型的规则来解释那块内存单元及其后续单元的内容(类型的长度可能不止一个字节,而地址类型是类型修饰符,故一定有原类型)。由于变量映射的数实际是地址,故变量所映射的数字就是地址类型的。如long a;,假设a映射的是3006,当书写a = 3;时,由于a是变量名,故返回a所映射的数字3006,类型是long类型的地址类型。由于是地址类型,“=”操作符的语法检查成功(这是类型的另一个用处——语法检查,就好像动名形容词一样),执行“=”操作符的计算。
应注意C++并未提出地址类型修饰符这个概念,只是我出于语法上的完备而提出的,否则要涉及更多的无谓概念和规则,如*( p + 1 ) = 20; a[2] = 3;等的解释将复杂化,故在《C++从零开始》系列中提出地址类型的数字这个概念,旨在以尽量少的概念解释尽量多的语法。
最常用的类型修饰符——指针和数组
在《C++从零开始(五)》中已说明指针只是一种类型修饰符。一个数字是指针类型时,将这个数字所代表的数学意义上的数用二进制表示并返回。前面已说过数字的用处就是转换其代表的数为二进制数,其类型仅说明如何转换,而指针类型所代表的规则就是按数学规则变成二进制数,而不管其原类型是何种类型。由于不受原类型的影响,故指针类型总是固定长度(对成员指针则不一定),在32位操作系统上是四个字节。
如long a; long *p = &a;。假设a映射的是3006,p映射的是3010。对于*p = 3;,p这个操作符返回类型为long的指针类型的地址类型的数字3010,即这个数字的类型被两个类型修饰符两次修饰,由于最后是被地址修饰,故3010是地址类型的数字,而其原类型是long的指针类型。故*p返回类型为long类型的地址类型的数字3006,然后执行“=”操作符的计算。
这里请注意两个操作符——取内容操作符“*”和取地址操作符“&”。在《C++从零开始(五)》中也强调过,它们其实名不副实,应该叫类型转换操作符才对。即前者后接指针类型的数字,返回的数字原封不动,仅将其类型变为地址类型;后者后接地址类型的数字,返回的数字原封不动,仅将其类型变为指针类型。这有点耍小聪明的感觉,但请注意:long *p1 = 0; long *p2 = &*p1;
如果“*”的操作是取内容,则&*p1将先取地址为0的内存单元的内容,这将引起内存访问违规,但实际并不会,因为“*”仅转换类型而非取内容(取内容是由地址类型的数字的计算来实现的)。
前面已说明,在指针类型的数字返回二进制数时,并不需要原类型的参与,即类型为long*的数字3006和类型为char*的数字3006返回的二进制数都一样,都是3006对应的二进制数。那么为什么要让指针是类型修饰符以带个不用的原类型?根据前面即可看出指针类型的原类型是给取内容操作符“*”用的。但它还有个用处,因为数组类型修饰符的加入,使得指针多了一个所谓的运算功能,在此先看看数组类型。
在《C++从零开始(五)》中已详细说明数组的修饰功能是将原类型的元素重复多个连续存放以此形成一个新的类型。如long a[10];,a的类型就是long[10],长度为10*sizeof(long)=40个字节,而char[7]类型所对应的长度就是7*sizeof(char)=7个字节。一个数字的类型是数组类型时,因这个数字的长度可一个字节,可一万个字节,故这个数字一定被存放在某块内存中,而数组类型的数字返回的二进制数就是其被存放的内存的首地址。所以前面提到的常数就不能返回一个数组类型的数字,因其没有给出一块内存来存放数组类型的数字。
这里有点混乱,注意数字不一定非要被内存所存储。对于long a[3] = { 45, 45, 45 };,假设a映射的数字是3000,则表示以long[3]的规则解释内存单元3000所记录的数字,这个数字的长度是3*sizeof(long)=12个字节,它的值由于数组类型是长度可变的而决定使用3000(记录它的内存的地址)来代表它,实际是3个值为45的数。所以a;将先返回long[3]类型的地址类型的数字3000,然后计算此地址类型的数字而返回其原类型的数字,由于原类型是long[3],而这个数字存放在3000所标识的内存处,故最后返回3000所对应的二进制数。
容易发现指针返回的是一个地址,数组也是一个地址,当它们原类型相同时,后者可以隐式类型转换为前者,但反之不行,因为数组还具备元素个数这个信息,即long[2]和long[3]的原类型相同,但类型不同。因此有:long a[3]; long *p = a;。这里没任何问题,假设a映射的是3000,则p的值就是3000。因此*p = 3;就是将3放到3000处存放的数组类型的数字的第0个元素中。为了放到第1个和第2个元素中,C++提供了一个所谓的指针运算功能,如下:*( p + 1 ) = 4; *( p + 2 ) = 5;。这里就把4放到第1个元素,5放到第2个元素中。对*( p + 1 ) = 4;,p返回一个long*的数字3000,而p + 1返回long*的数字3004,然后继续后续计算。同理,p + 2返回类型为long*的数字3000+2*sizeof(long)=3008。即指针只能进行整数加减,如:
char *p1 = 0; p1++; p1 = p1 + 5 * 8 - 1; short *p2 = 0; p2 += 11; p2--;
上面p1的值为40,p2的值也为40,因为p1的原类型是char而p2的是short。
因此为了获得数组的第2个元素的值,需*( p + 2 );,这很明显地不便于阅读,为此C++专门提供了一个下标操作符“[]”,其前面接指针类型的数字,方括号中放一整型数字,将指针类型的数字换成地址类型,再将值按前面提到的指针运算规则变换,返回。如long a[4]; long *p = a;,假设a映射的是3000。则a[2] = 1;等效于*( p + 2 ) = 1;,a[2]前面接的是long*类型的数字3000(隐式类型转换,从long[4]转成long*),加2*sizeof(long),返回3008,类型则简单地变成long类型的地址类型。由于“[]”仅仅只是前面说的指针运算的简化易读版本,故也可a[-1] = 3;,其等效于*( p - 1 ) = 3;。由于“[]”前接指针,故也可p[-1] = 3;,等效于a[-1] = 3;。
类型修饰符的重复修饰——多级指针和多维数组
前面提到的类型修饰符中,只有指针、数组和偏移三个类型修饰符是可以连续重复修饰类型的。偏移重复修饰以表示类型嵌套,指针重复修饰被称作多级指针,数组重复则称作多维数组。这看起来好像变复杂了,实际完全没有,只用记住一点:类型修饰符修饰某一原类型后形成一种新的可被认为是原类型的类型。
long **p;。p映射的数字是一long**的地址类型的数字,则p;返回的数字的类型就是long**。对于*p;就可将p返回的数字的类型看作是long*的指针类型,原类型为long*,故*p返回的数字的类型是long*的地址类型的数字,而**p;则返回long的地址类型的数字。
对于long a[2][3];也是同样。由于其原类型是long[3],故可以long ( *p )[3] = a;,进而a[1]返回的是long[3]的地址类型的数字,而a[1][2]返回的就是long的地址类型的数字。
注意为什么a的原类型不是long[2]而是long[3]。在《C++从零开始(五)》中已经说明,当多个类型修饰符同时修饰时,修饰顺序是从左到右,而相同连续修饰符的修饰则从右到左以符合人的习惯,而“()”则降低修饰优先度。如long*[2][3]是long的指针类型的数组[3]类型的数组[2]类型,即它是一个数组类型,有两个元素,原类型是long*[3]。也就是“[2]”最后修饰,优先级最低(从其所修饰的数字的类型方面来看,也可认为其优先级最高)。而long*[3]*[2][3]却是一个错误的类型,虽然按照前面所说的从左到右进行修饰没有任何问题,但C++规定变量定义时,指针类型修饰符必须在变量名的左侧,数组类型修饰符必须在右侧以符合人的阅读习惯,也因此才会在类型中出现“()”以降低修饰优先度。所以long*[3]*[2][3]应写成long *( *[2][3] )[3],欲定义变量则long *( *p[2][3] )[3];,其中最后修饰的是“[2]”(不是“[3]”,因相同连续修饰符修饰时是从右到左而非从左到右)。故p是一个有两个元素的数组,原类型为long *( *[3] )[3],而long *( *( *pp )[2][3] )[3] = &p;没有任何问题,因为最后修饰的是“*”,而原类型为long *( *[2][3] )[3]。
除此以外还应注意一件事——不管什么多级指针,其长度都为4字节(这是对于32位操作系统,而成员指针可能不止4个字节),但数组的维数越多,类型的长度就越长(当然,如果元素个数为1则长度没有变化)。如long ***p;,此时只分配4个字节内存空间,而long a[2][3][5];则分配2*3*5*sizeof(long)=120个字节的内存空间。如下:
long a = 0; long *p = &a, **pp = &p; long b[2][3][4];
假设上面a映射的是3000,则p就映射3004,pp就映射3008,而b映射3012。如上赋值后,a的值为0,类型为long;p的值为3000,类型为long*;pp的值为3004,类型为long**;b的值为3012,类型为long[2][3][4]。
对于*( *( pp + 1 ) ) = 5;,pp返回类型为long**的数字3004,而原类型long*的长度是4个字节,故pp + 1返回类型为long**的数字3008,而*( pp + 1 )仅转换类型,返回类型为long*的地址类型的数字3008,返回类型为long*的数字3004,故*( *( pp + 1 ) )返回类型为long的地址类型的数字3004,而*( *( pp + 1 ) ) = 5;则将5按照long的存放规则放到3004所标识的内存中,结果p的值变为5而不再是3000(运气极好地5是正数,此时long类型的数字转换规则和long*一样),进而如果再继续*p = 1;将错误(应注意上面是假设编译器顺序安放a、p和pp,进而使pp的地址较p多4。不同的编译设置和编译器将不一定如上顺序安放局部变量,则*( *( pp + 1 ) ) = 5;将有可能失败)。
对于*( *( *( b + 1 ) + 1 ) + 1 ) = 5;,b返回类型为long[2][3][4]的数字3012,原类型为long[3][4],则b + 1将先进行隐式类型转换以将3012转换为long(*)[3][4],而sizeof(long[3][4])=48字节,则b + 1将返回类型为long(*)[3][4]的数字3012+48=3060,而*( b + 1 )返回类型为long[3][4]的地址类型的数字3060,再返回类型为long[3][4]的数字3060。则*( b + 1 ) + 1返回类型为long(*)[4]的数字3060+sizeof(long[4])=3076,同理,*( *( *( b + 1 ) + 1 ) + 1 )返回long类型的地址类型的数字3076+sizeof(long)=3080,将5放在3080所标识的内存中。由前面对“[]”的说明可知*( *( *( b + 1 ) + 1 ) + 1 ) = 5;等效于b[1][1][1] = 5;,可如上自行推验。应注意虽然b是多维数组,但它仍是一块连续的内存空间。
为什么要有多级指针和多维数组?long a[3][4];和long a[12];都是分配一块连续的48字节内存空间,它们有什么区别?何时用前者何时用后者?在《C++从零开始》系列中强调要按语义编写程序,因此只要明确指针和数组的语义就能有条理地使用它们了。
数组的语义及运用——矢量和容器
一种类型,由几个相同类型的元素共同构成,这就是数组类型修饰符的语义。这正好可以用来映射线性代数中的矢量,如二维平面上的点坐标就是二维矢量。假设用double来记录点坐标的分量,则double a[2];就可以认为定义了一个二维点坐标,如果想要更具可读性,可typedef double POINT_2D[2]; POINT_2D a;。
实际中,很容易就发现数组其实实现了一个集合,即可用来作为容器以记录多个同一类型的数字。此时数组在类型上表现的语义——矢量——已被忽略,重点是定义出来的数组变量,即重点不再是POINT_2D,而是a。如:double container[300];就通过变量名而不再是类型来体现语义了。作为容器光写个double container[300];是不够的,因为无法知道哪些元素有效哪些无效,因此还需其它的变量介入以共同完成一个容器的基本功能。对此,实际中常编写一个类来隐藏上面实现容器的基本功能的细节,这个类映射的语义是容器,这种类一般被称作容器类或集合类,如STL中的容器类vector。这种类也被称作是对数组container的封装类。
那多维数组的语义是什么?有何意义?这其实很简单,因为数组是类型修饰符,不用去管它一维还是多维,只用知道它是多个原类型的元素构成的一个类型。比如long a[2][3];就可以映射为一个二维矢量,只不过每个分量又是一个三维矢量罢了,这正好可映射线性代数中的矩阵。因此typedef MATRIX_23[2][3]; MATRIX_23 a;。同理可只注重定义的数组变量而忽略其类型所表征的语义,如double a[300][300];。这里的a和double b[90000];定义的b有什么区别?最终所操作的内存没有区别,仅仅语义上的差别——前者所代表的容器需要两个关键字(key)才能定位一个元素(value),而后者只用一个关键字就可以定位元素(a[2][3] = 3;等同于b[603] = 3;,但前者效率更低)。前者如电子表格中的表格,给出横纵两个坐标才能定位欲写入数据的位置;后者则是一般的连续容器。还可以既注重数组的类型语义——矢量,又注重数组的实例语义——容器,如:POINT_2D PContainer[300];,PContainer的类型实际是double[300][2]。
引用和间接
欲说明指针的语义及其运用,最好是先了解一下引用和间接。这里的引用不是指C++中的引用类型修饰符“&”,而是一个语言无关的概念,在《C++从零开始(八)》中已详细阐明,在此再说明一下:引用表示了一个连接关系,以至于原来需要某种手段才能达到的目的,现在另一种手段也能达到,这种手段往往比原来的手段在某方面有优势。如某人的手机号码就是和某人谈话的引用;图书馆里藏书的编号就是书的引用;某人的名字就是某人的引用。引用有什么用?其本身是没用的,一定有一些手段来实现其“相当于”的目的。如某人的手机没带或没电,则“手机号码”无法完成相当于“和某人谈话”的功能;没有正确的排放书籍,书的编号毫无意义;没有完备的搜查系统,给出人的名字也无法找到人。这是因为引用是“间接”的一部分,在《C++从零开始(十一)下篇》中说明了何谓间接,指出其三大优点——简化操作、提高效率(从某方面)和增加灵活性,下面说明。
用手机和某人谈话是通过手机间接和那人谈话;看风景照是通过照片间接观看风景;商品卖出是通过销售员间接卖出去的(不是老板亲自卖的)。即有一个“原始手段”可以达到目的,有一个“高级手段”能操控那个“原始手段”,则通过使用“高级手段”来达到目的称作间接,而“高级手段”的配置信息(说明如何使用“高级手段”)就是引用。所谓的手段就是某种方法或功能,而方法的配置信息用于完善方法的运用。如讲话是个方法,而讲话的人就是引用,即人引用了讲话,表示只要拥有个人,就可以实现讲话。“声音传到人的耳朵里”是“原始手段”,“手机能传递声音并使其传到人的耳朵里”是“高级手段”,“手机号码”决定手机如何传递声音(传给哪个手机);“风景反射的光线进入眼睛”是“原始手段”,“照片反射的光线进入眼睛”是“高级手段”,“照片”决定如何反射光线;“销售员能把商品卖出”是“原始手段”,“销售员能被命令卖出商品”是“高级手段”,“销售员”决定了如何命令销售员卖商品(命令哪个)。“手机号码”引用了“和某人谈话”;“照片”引用了“风景”;“销售员”引用了“卖出商品”。
应注意引用自身也是一种资源,其可以被操作,当它被修改时,对于同一个“高级手段”的执行,将得到不同的结果,因此间接能提高灵活性。即之所以能提高灵活性,是因为“高级手段”的可配置,如果不可配置则间接无法提高灵活性。
还应注意“原始手段”可能是另一个间接中的“高级手段”,如A向B要钱,B命令C卖商品以得到钱,即间接的间接。如果两级间接的“高级手段”都可配置,则此二级间接的灵活性将可能高于一级的间接(只是可能,要视“可配置”这个功能是否被运用)。
间接一定降低效率。因为原来一个“原始手段”即可达到目的,现在却要施行“高级手段”和“原始手段”两个手段才能达到目的。那要间接有何意义?必定因为“高级手段”在某方面优于“原始手段”。如施行方面以达到简化操作,如MFC中的窗口包装类CWnd;使用方面通过“高级手段”的可配置以提高灵活性;无法施行“原始手段”而需借助“高级手段”,如A没时间而派B去跟客户洽谈;管理方面为“高级手段”赋于一定的意义以简化管理,等同于归类分层,如营销部卖商品而不是销售员卖商品。
指针的语义——引用
上面说那么多,到底关指针什么事?指针的语义就是引用。在《C++从零开始》系列中已说明,程序即方法的描述,方法即说明如何操作资源,在C++中能被操作的资源只有数字,数字以其类型所决定的规则存放在一块内存中,内存块是通过其首地址来标识的。而前面已说地址类型的数字的计算将进行取内容操作,指针类型的数字的计算则不取内容,直接返回地址,即指针类型的数字就是一个地址,而地址是C++中唯一能操作的资源——数字——被存放的位置,即其是某块内存块的引用。
指针既然被称作引用也就带入了间接。数字的计算(即按数字的类型定义的规则来返回相应的二进制数)是“原始手段”,指针是引用,而“高级手段”则是取内容(通过取内容操作符“*”将指针类型换成地址类型以实现)。应注意引用也可以不使用取内容这个“高级手段”,其他的也可以,但需要保证此“高级手段”能控制(或执行)“原始手段”——数字的计算(即C++语法中所有可以放表达式的地方,指针类型转地址类型正好是通过操作符的形式实现,即可放于表达式中而达到执行“原始手段”的目的),如类型转换。现已了解指针所引入的间接,下面就来看如何运用这个间接。
“高级手段”取内容有什么方面比“原始手段”计算数字优越呢?取内容具有一个唯一的可配置信息——取内容的位置,即地址。前面已说,“高级手段”的可配置带来灵活性,如下:
long a1 = 1, a2 = 2, a3 = 3, *p = 0; /* 一些操作以决定p的值 */ *p += 5;
上面的灵活性体现在多次执行*p += 5;这条语句,可能会将不同内存中的值增5。但如果a2 += 5;,则无论执行多少遍都是将a2对应的地址所标识的内存中的值增5。之所以能这样是因为指针是地址类型的数字的引用。
应注意地址类型也带入一个间接,其是数字的引用。数字的计算是“原始计算”,取内容是“高级手段”(感觉和指针一样,但后者需要取内容操作符的转换,而前者不用),地址类型的数字就是引用。这也就导致a3 = a1 + a1;也具有灵活性,因为不是a3 = 1 + 1;,而是由地址间接获得long类型的数字。对于a3 = *p + *p;则先间接获得地址,再间接获得放在内存中的数字,这是一个二级间接,其灵活性要高于a3 = a1 + a1;,因此指针也可以说是一个数字的引用。
前面提到的间接的简化操作等功用,由于需要“高级手段”的支持,而这里一般性的只有取内容操作符,故指针主要用于增加灵活性。
注意引用一般是一个较小的资源,易于记录才会被视为引用。如只用将B的手机号码给A而不用将B带到A面前就可以实现A和B谈话。当引用的是对某种资源的操作时,给出较小的引用的代价要小于给出那种资源的一个实例,此时传递引用就比传递那种资源的实例更有效率,此即前面所谓的提高效率。这在C++中非常流行,这完全依赖于指针的二级间接以实现对数字的引用。即一个数字很大,如长度300字节(如类型为char[300]),而其引用——地址——只有4个字节,故欲将其传递给函数时应传递地址,而指针类型的数字就是地址,故函数的参数类型应是指针(char(*)[300])而不是原类型(char[300]),后者的传递费用更高。
指针的运用——间接
至此,根据上面已经可以得出指针的四个运用目的了——增加灵活性、引用内存块、引用数字、语义需要,分述如下:
增加灵活性 编写一段代码时,尤其是循环,循环体中是经常使用指针的地方,因为之所以能体现灵活性,就是相同的做法得到不同的结果,也因此多次循环执行相同的循环体却得到不同的结果以达到简化程序编写的目的(否则需对每种结果书写一段代码)。如需增加100个分散的等间隔20字节的内存块中的值,则可将第一个地址放在一个指针中,如下:
long *p; /* 初始化p */ for( long i = 0; i < 100; i++, p += 5 ) *p++;
因此对于不想或不能改变代码的地方,为了增大其适用范围,常利用指针来增加其灵活性。如函数体内、库文件内(如动态连接库)、模块内(指程序结构上,如游戏逻辑计算模块和绘图模块)等。
注意函数指针的存在。函数的一个原型代表了一种调用规则,即《C++从零开始(十八)》中说的接口,则函数指针是一个接口的引用,则两个模块(或称组件)之间衔接时,如逻辑计算模块调用void Draw();来绘图,但绘图模块中(即函数Draw内部)可能根据当前绘图数据的情况(如场景复杂与否等)而决定不同的绘图方法,则可使用一全局函数指针void ( *g_pDraw )();,每次绘图模块会根据绘图情况设置此变量,而逻辑计算模块就变成( *g_pDraw )();而不再Draw();(此法不推荐,在此仅作为例子提出)。
对于成员指针,在《C++从零开始(九)》中已指出非静态成员变量映射的是偏移而不是地址,即成员变量指针不是内存块的引用,仅仅是偏移类型的数字的引用,而偏移类型的数字4个字节长,其指针反而可能8字节长,因此它不用于前面的传递的优化,也不能引用内存块,仅只有增加灵活性和语义需要。如一个结构是学生成绩,里面有40门学科的成绩,则欲计算平均分,如下:
lang=EN-US struct Student
{
float Course1, Course2, …, Course40;
static float Student::*pCourse[40];
};
float Student::*Student::pCourse[40] = { &Student::Course1, …, &Student::Course40 };
则可书写一循环来简化计算,如:
Student a; float avg = 0.0f;
for( long i = 0; i < 40; i++ ) avg += a.*Student::pCourse[ i ];
avg /= 40.0f;
对于成员函数指针也是一样的,只用按照前述的规则和语义就可了解各类指针的运用。
引用内存块 这是指针的原始语义,当欲传递内存块时,就只能靠传递指针来实现(内存块是不能传递的),如:
bool GetPersonInfo( PersonInfo *pInfo );
PersonInfo temp; GetPersonInfo( &temp );
// 使用返回的放在temp中的个人信息
上面的参数pInfo就是传递一个内存块给函数GetPersonInfo以让它将个人信息填在给定的内存块中以进行传递。如果PersonInfo* GetPersonInfo();,那么返回的指针所引用的那块内存块需要被释放,但使用GetPersonInfo的代码无法知道那内存块是如何分配的进而无法释放,这属于内存管理方面,在此不深入讨论。再如:
bool GetMaxAndMin( long[10], long *max, long *min );
long a[10] = { 5, 3, 10 }, max, min; GetMaxAndMin( a, &max, &min );
引用数字 这是借助地址类型的数字所带入的间接而形成的二级间接的运用,目的是被引用的数字的大小大于指针的大小时传递指针以减少传递成本。如上面的long[10]是40个字节,如果传递它的指针则只有4个字节,大大降低成本,如下:
bool GetMaxAndMin( long(*)[10], long*, long* );
long a[10] = { 5, 3, 10 }, max, min; GetMaxAndMin( &a, &max, &min );
这里数组a的传递就比之前有效率得多。再如之前的PersonInfo* GetPersonInfo();而不PersonInfo GetPersonInfo();也是出于sizeof(PersonInfo*)<sizeof(PersonInfo)。也因此这样是愚蠢的:long Max( char*, char*, char* );。这里欲返回最大值,但sizeof(char*)>sizeof(char),不仅没提高反而降低了效率(对于32位编译器,这里将不是由于传递降低效率,而是间接引用数字降低效率)。
应注意指针能引用数字是由于二级间接,这导致多了一级无谓的引用——对内存块的引用,本只想传递数字,结果连装数字的内存块也传递了,将引起问题。如前面的GetMaxAndMin( &a, &max, &min );,执行后,数组a的内容可能被改变,因为记录a的内存块也被传过去了,函数GetMaxAndMin可任意修改传进去的内存块的内容,也就是数组a的内容。当函数GetMaxAndMin和调用它的代码不是同一个人编的时候,则前者就可以肆意破坏a的内容而导致后者发生问题,这被称作漏洞。所以当设计对外的接口时,应小心确定参数类型是否应为指针类型(注意对于对外的接口,即使写成bool GetMaxAndMin( const long(*)[10], long*, long* );也是毫无意义的)。
语义需要 指针的语义是引用,当逻辑上需要引用时就使用指针来映射,如链表的结点中有个对下一个结点的引用,因此设计链表的结点时其就应该有一个指针类型的成员。
对于函数指针,函数就是一段程序,程序即方法的描述,即函数指针是方法的引用。如一个容器类具有一个排序方法的引用,这样使用它时给出不同的排序方法就能实现不同的排序(这是“间接”的增加灵活性的表现),因此那个容器类应有一个成员变量是函数指针类型。
再有,函数指针可实现回调。如搜索数据时,通过调用函数Search,其有一个参数以指明当搜索时间超时时应怎么办,所以Search需要一个函数的引用(代码不能传递),也就是函数指针,如下:
bool bContinue( unsigned long overSeconds )
{
if( /* 判断是否继续 */ )
{ /* 必要的处理 */ return true; }
return false;
}
bool Search( bool(*)( unsigned long ), /* 其它参数 */ );
Search( &bContinue, /* 其它参数 */ );
上面的Search中就可以每隔一定时间就调用bContinue以询问是否还要继续搜寻以给出一种手段来终止过长时间的搜索。
对于成员指针,同样,是对成员的引用。在《C++从零开始(十二)》中已说明,成员变量就是相应资源的属性和状态,成员函数就是相应资源的功能,则成员指针就是资源的属性和状态的引用以及资源功能的引用。如狗具有走钢丝、跳火圈、倒立三个功能,有一个方法是马戏表演。为了增加灵活性,马戏表演这个函数中就应该保持一个对狗的功能的引用,以不同的场次表演不同的马戏。
从上可以看出,所谓语义需要,应是根据前面所说的间接的好处来设计算法,再从语义上表现出对引用的需要。但间接也有坏处,即它可能失败,而直接就总是能成功。“原始手段”一定成功(因为是直接达到目的,如果是在多级间接中则要考虑其有效性),失败是因为“高级手段”控制失败。而“高级手段”之所以控制失败是因为其配置信息无效,也就是引用是无效的。对于指针,则需要保证其值是有效的,一旦无效,将可能引起内存访问违规。如果没有适当处理措施,则程序可能崩溃。因此大多在使用指针前先检测其有效性,检测的方法就是判断其所引用的地址所标识的内存当前是否可访问(操作系统一般都提供了这样的接口),但借助操作系统效率较低,一般认为只要不为零就是有效的,因此一般如下:
if( !p ) { /* 使用指针p */ }
所以一般都将指针初始化为0,如long *p = 0;。对此一般都会建立一个宏或常量来表现这个语义,在MFC中,就定义了NULL这个宏以代表0,则就可以:long *p = NULL;,NULL表示空。
至此已说明了指针及其运用,指针的运用是建立在间接的基础上的,明确了间接的优点和缺点将有助于更好地运用指针。