AutoCAD 3DMAX C语言 Pro/E UG JAVA编程 PHP编程 Maya动画 Matlab应用 Android
Photoshop Word Excel flash VB编程 VC编程 Coreldraw SolidWorks A Designer Unity3D
 首页 > C++

C++程序设计从零开始(五)

51自学网 http://www.wanshiok.com

 

  四、地址

  前面说过“地址就是一个数字,用以唯一标识某一特定内存单元”,而后又说“而地址就和长整型、单精度浮点数这类一样,是数字的一种类型”,那地址既是数字又是数字的类型?不是有点矛盾吗?如下:

  浮点数是一种数——小数——又是一种数字类型。即前面的前者是地址实际中的运用,而后者是由于电脑只认识状态,但是给出的状态要如何处理就必须通过类型来说明,所以地址这种类型就是用来告诉编译器以内存单元的标识来处理对应的状态。


  五、指针

  已经了解到动态分配内存和静态分配内存的不同,现在要记录用户输入的定单数据,用户一次输入的定单数量不定,故选择在堆上分配内存。假设现在根据用户的输入,需申请1M的内存以对用户输入的数据进行临时记录,则为了操作这1M的连续内存,需记录其首地址,但又由于此内存是动态分配的,即其不是由编译器分配(而是程序的代码动态分配的),故未能建立一变量来映射此首地址,因此必须自己来记录此首地址。

  因为任何一个地址都是4个字节长的二进制数(对32位操作系统),故静态分配一块4字节内存来记录此首地址。检查前面,可以将首地址这个数据存在unsigned long类型的变量a中,然后为了读取此1M内存中的第4个字节处的4字节长内存的内容,通过将a的值加上4即可获得相应的地址,然后取出其后连续的4个字节内存的内容。但是如何编写取某地址对应内存的内容的代码呢?前面说了,只要返回地址类型的数字,由于是地址类型,则其会自动取相应内容的。但如果直接写:a + 4,由于a是unsigned long,则a + 4返回的是unsigned long类型,不是地址类型,怎么办?

  C++对此提出了一个操作符——“*”,叫做取内容操作符(实际这个叫法并不准确)。其和乘号操作符一样,但是它只在右侧接数字,即*( a + 4 )。此表达式返回的就是把a的值加上4后的unsigned long数字转成地址类型的数字。但是有个问题:a + 4所表示的内存的内容如何解释?即取1个字节还是2个字节?以什么格式来解释取出的内容?如果自己编写汇编代码,这就不是问题了,但现在是编译器代我们编写汇编代码,因此必须通过一种手段告诉编译器如何解释给定的地址所对内存的内容。

  C++对此提出了指针,其和上面的数组一样,是一种类型修饰符。在定义变量时,在变量名的前面加上“*”即表示相应变量是指针类型(就如在变量名后接“[]”表示相应变量是数组类型一样),其大小固定为4字节。如:

unsigned long *pA;

  上面pA就是一个指针变量,其大小因为是为32位操作系统编写代码故为4字节,当*pA;时,先计算pA的值,就是返回从pA所对应地址的内存开始,取连续4个字节的内容,然后计算“*”,将刚取到的内容转成unsigned long的地址类型的数字,接着计算此地址类型的数字,返回以原码格式解释其内容而得到一个unsigned long的数字,最后计算这个unsigned long的数字而返回以原码格式解释它而得的二进制数。

  也就是说,某个地址的类型为指针时,表示此地址对应的内存中的内容,应该被编译器解释成一个地址。

  因为变量就是地址的映射,每个变量都有个对应的地址,为此C++又提供了一个操作符来取某个变量的地址——“&”,称作取地址操作符。其与“数字与”操作符一样,不过它总是在右侧接数字(而不是两侧接数字)。

  “&”的右侧只能接地址类型的数字,它的计算(Evaluate)就是将右侧的地址类型的数字简单的类型转换成指针类型并进而返回一个指针类型的数字,正好和取内容操作符“*”相反。
上面正常情况下应该会让你很晕,下面释疑。

unsigned long a = 10, b, *pA; pA = &a; b = *pA; ( *pA )++;

  上面的第一句通过“*pA”定义了一个指针类型的变量pA,即编译器帮我们在栈上分配了一块4字节的内存,并将首地址和pA绑定(即形成映射)。然后“&a”由于a是一个变量,等同于地址,所以“&a”进行计算,返回一个类型为unsigned long*(即unsigned long的指针)的数字。
应该注意上面返回的数字虽然是指针类型,但是其值和a对应的地址相同,但为什么不直接说是unsigned long的地址的数字,而又多一个指针类型在其中搅和?因为指针类型的数字是直接返回其二进制数值,而地址类型的数字是返回其二进制数值对应的内存的内容。因此假设上面的变量a所对应的地址为2000,则a;将返回10,而&a;将返回2000。

  看下指针类型的返回值是什么。当书写pA;时,返回pA对应的地址(按照上面的假设就应该是2008),计算此地址的值,返回数字2000(因为已经pA = &a;),其类型是unsigned long*,然后对这个unsigned long*的数字进行计算,直接返回2000所对应的二进制数(注意前面红字的内容)。

  再来看取内容操作符“*”,其右接的数字类型是指针类型或数组类型,它的计算就是将此指针类型的数字直接转换成地址类型的数字而已(因为指针类型的数字和地址类型的数字在数值上是相同的,仅仅计算规则不同)。所以:

b = *pA;

  返回pA对应的地址,计算此地址的值,返回类型为unsigned long*的数字2000,然后“*pA”返回类型unsigned long的地址类型的数字2000,然后计算此地址类型的数字的值,返回10,然后就只是简单地赋值操作了。同理,对于++( *pA )(由于“*”的优先级低于前缀++,所以加“()”),先计算“*pA”而返回unsigned long的地址类型的数字2000,然后计算前缀++,最后返回unsigned long的地址类型的数字2000。

  如果你还是未能理解地址类型和指针类型的区别,希望下面这句能够有用:地址类型的数字是在编译时期给编译器用的,指针类型的数字是在运行时期给代码用的。如果还是不甚理解,在看过后面的类型修饰符一节后希望能有所帮助。


  六、在堆上分配内存

  前面已经说过,所谓的在堆上分配就是运行时期向操作系统申请内存,而要向操作系统申请内存,不同的操作系统提供了不同的接口,具有不同的申请内存的方式,而这主要通过需调用的函数原型不同来表现(关于函数原型,可参考《C++从零开始(七)》)。由于C++是一门语言,不应该是操作系统相关的,所以C++提供了一个统一的申请内存的接口,即new操作符。如下:

unsigned long *pA = new unsigned long; *pA = 10;
unsigned long *pB = new unsigned long[ *pA ];

  上面就申请了两块内存,pA所指的内存(即pA的值所对应的内存)是4字节大小,而pB所指的内存是4*10=40字节大小。应该注意,由于new是一个操作符,其结构为new <类型名>[<整型数字>]。它返回指针类型的数字,其中的<类型名>指明了什么样的指针类型,而后面方括号的作用和定义数组时一样,用于指明元素的个数,但其返回的并不是数组类型,而是指针类型。

  应该注意上面的new操作符是向操作系统申请内存,并不是分配内存,即其是有可能失败的。当内存不足或其他原因时,new有可能返回数值为0的指针类型的数字以表示内存分配失败。即可如下检测内存是否分配成功。

unsigned long *pA = new unsigned long[10000];
if( !pA )
// 内存失败!做相应的工作

  上面的if是判断语句,下篇将介绍。如果pA为0,则!pA的逻辑取反就是非零,故为逻辑真,进而执行相应的工作。

  只要分配了内存就需要释放内存,这虽然不是必须的,但是作为程序员,它是一个良好习惯(资源是有限的)。为了释放内存,使用delete操作符,如下:

delete pA; delete[] pB;

  注意delete操作符并不返回任何数字,但是其仍被称作操作符,看起来它应该被叫做语句更加合适,但为了满足其依旧是操作符的特性,C++提供了一种很特殊的数字类型——void。其表示无,即什么都不是,这在《C++从零开始(七)》中将详细说明。因此delete其实是要返回数字的,只不过返回的数字类型为void罢了。

  注意上面对pA和pB的释放不同,因为pA按照最开始的书写,是new unsigned long返回的,而pB是new unsigned long[ *pA ]返回的。所以需要在释放pB时在delete的后面加上“[]”以表示释放的是数组,不过在VC中,不管前者还是后者,都能正确释放内存,无需“[]”的介入以帮助编译器来正确释放内存,因为以Windows为平台而开发程序的VC是按照Windows操作系统的方式来进行内存分配的,而Windows操作系统在释放内存时,无需知道欲释放的内存块的长度,因为其已经在内部记录下来(这种说法并不准确,实际应是C运行时期库干了这些事,但其又是依赖于操作系统来干的,即其实是有两层对内存管理的包装,在此不表)。


  七、类型修饰符(type-specifier)

  类型修饰符,即对类型起修饰作用的符号,在定义变量时用于进一步指明如何操作变量对应的内存。因为一些通用操作方式,即这种操作方式对每种类型都适用,故将它们单独分离出来以方便代码的编写,就好像水果。吃苹果的果肉、吃梨的果肉,不吃苹果的皮、不吃梨的皮。这里苹果和梨都是水果的种类,相当于类型,而“XXX的果肉”、“XXX的皮”就是用于修饰苹果或梨这种类型用的,以生成一种新的类型——苹果的果肉、梨的皮,其就相当于类型修饰符。

  本文所介绍的数组和指针都是类型修饰符,之前提过的引用变量的“&”也是类型修饰符,在《C++从零开始(七)》中将再提出几种类型修饰符,到时也将一同说明声明和定义这两个重要概念,并提出声明修饰符(decl-specifier)。

  类型修饰符只在定义变量时起作用,如前面的unsigned long a, b[10], *pA = &a, &rA = a;。这里就使用了上面的三个类型修饰符——“[]”、“*”和“&”。上面的unsigned long暂且叫作原类型,表示未被类型修饰符修饰以前的类型。下面分别说明这三个类型修饰符的作用。

  数组修饰符“[]”——其总是接在变量名的后面,方括号中间放一整型数c以指明数组元素的个数,以表示当前类型为原类型c个元素连续存放,长度为原类型的长度乘以c。因此long a[10];就表示a的类型是10个long类型元素连续存放,长度为10*4=40字节。而long a[10][4];就表示a是10个long[4]类型的元素连续存放,其长度为10*(4*4)=160字节。

  相信已经发现,由于可以接多个“[]”,因此就有了计算顺序的关系,为什么不是4个long[10]类型的元素连续存放而是倒过来?类型修饰符的修饰顺序是从左向右进行计算的,但当出现重复的类型修饰符时,同类修饰符之间是从右向左计算以符合人们的习惯。故short *a[10];表示的是10个类型为short*的元素连续存放,长度为10*4=40字节,而short *b[4][10]; 表示4个类型为short*[10]的元素连续存放,长度为4*40=160字节。

   指针修饰符“*”——其总是接在变量名的前面,表示当前类型为原类型的指针。故:
short a = 10, *pA = &a, **ppA = &pA;

  注意这里的ppA被称作多级指针,即其类型为short的指针的指针,也就是short**。而short **ppA = &pA;的意思就是计算pA的地址的值,得一类型为short*的地址类型的数字,然后“&”操作符将此数字转成short*的指针类型的数字,最后赋值给变量ppA。

  如果上面很昏,不用去细想,只要注意类型匹配就可以了,下面简要说明一下:假设a的地址为2000,则pA的地址为2002,ppA的地址为2006。

  对于pA = &a;。先计算“&a”的值,因为a等同于地址,则“&”发挥作用,直接将a的地址这个数字转成short*类型并返回,然后赋值给pA,则pA的值为2000。

  对于ppA = &pA;。先计算“&pA”的值,因为pA等同于地址,则“&”发挥作用,直接将pA的地址这个数字转成short**类型(因为pA已经是short*的类型了)并返回,然后赋值给ppA,则ppA的值为2002。

  引用修饰符“&”——其总是接在变量名的前面,表示此变量不用分配内存以和其绑定,而在说明类型时,则不能有它,下面说明。由于表示相应变量不用分配内存以生成映射,故其不像上述两种类型修饰符,可以多次重复书写,因为没有意义。且其一定在“*”修饰符的右边,即可以short **&b = ppA;但不能short *&*b;或short &**b;因为按照从左到右的修饰符计算顺序,short*&*表示short的指针的引用的指针,引用只是告知编译器不要为变量在栈上分配内存,实际与类型无关,故引用的指针是无意义的。而short&**则表示short的引用的指针的指针,同上,依旧无意义。同样long &a[40];也是错误的,因为其表示分配一块可连续存放类型为long的引用的40个元素的内存,引用只是告知编译器一些类型无关信息的一种手段,无法作为类型的一种而被实例化(关于实例化,请参看《C++从零开始(十)》)。

  应该注意引用并不是类型(但出于方便,经常都将long的引用称作一种类型),而long **&rppA = &pA;将是错误的,因为上句表示的是不要给变量rppA分配内存,直接使用“=”后面的地址作为其对应的地址,而&pA返回的并不是地址类型的数字,而是指针类型,故编译器将报类型不匹配的错误。但是即使long **&rppA = pA;也同样失败,因为long*和long**是不同的,不过由于类型的匹配,下面是可以的(其中的rpA2很令人疑惑,将在《C++从零开始(七)》中说明):
long a = 10, *pA = &a, **ppA = &pA, *&rpA1 = *ppA, *&rpA2 = *( ppA + 1 );

  类型修饰符和原类型组合在一起以形成新的类型,如long*&、short *[34]等,都是新的类型,应注意前面new操作符中的<类型名>要求写入类型名称,则也可以写上前面的long*等,即:
long **ppA = new long*[45];

  即动态分配一块4*45=180字节的连续内存空间,并将首地址返回给ppA。同样也就可以:
long ***pppA = new long**[2];
而long *(*pA)[10] = new long*[20][10];

  也许看起来很奇怪,其中的pA的类型为long *(*)[10],表示是一个有10个long*元素的数组的指针,而分配的内存的长度为(4*10)*20=800字节。因为数组修饰符“[]”只能放在变量名后面,而类型修饰符又总是从左朝右计算,则想说明是一个10个long元素的数组的指针就不行,因为放在左侧的“*”总是较右侧的“[]”先进行类型修饰。故C++提出上面的语法,即将变量名用括号括起来,表示里面的类型最后修饰,故:long *(a)[10];等同于long *a[10];,而long *(&aa)[10] = a;也才能够正确,否则按照前面的规则,使用long *&aa[10] = a;将报错(前面已说明原因)。而long *(*pA)[10] = &a;也就能很正常地表示我们需要的类型了。因此还可以long *(*&rpA)[10] = pA;以及long *(**ppA)[10] = &pA;。

  限于篇幅,还有部分关于指针的讨论将放到《C++从零开始(七)》中说明,如果本文看得很晕,后面在举例时将会尽量说明指针的用途及用法,希望能有所帮助。

 
 

上一篇:C++程序设计从零开始(六)  下一篇:C++程序设计从零开始(四)