本篇是上一篇的续篇,接着为大家介绍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.其表示无,即什么都不是,因此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的皮”就是用于修饰苹果或梨这种类型用的,以生成一种新的类型——苹果的果肉、梨的皮,其就相当于类型修饰符。
本文所介绍的数组和指针都是类型修饰符,之前提过的引用变量的“&”也是类型修饰符。
类型修饰符只在定义变量时起作用,如前面的
- 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是4个long[10]类型的元素连续存放,其长度为4*40=160字节。
相信已经发现,由于可以接多个“[]”,因此就有了计算顺序的关系,为什么不是10个long[4]类型的元素连续存放而是倒过来?类型修饰符的修饰顺序是从左向右进行计算的。故short *a[10];表示的是10个类型为short*的元素连续存放,长度为10*4=40字节。
指针修饰符“*”——其总是接在变量名的前面,表示当前类型为原类型的指针。故:
- 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的地址这个数字转成long*类型并返回,然后赋值给pA,则pA的值为2000.
对于
- ppA = &pA;
先计算“&pA”的值,因为pA等同于地址,则“&”发挥作用,直接将pA的地址这个数字转成long**类型(因为pA已经是long*的类型了)并返回,然后赋值给ppA,则ppA的值为2002.
引用修饰符“&”——其总是接在变量名的前面,表示此变量不用分配内存以和其绑定,而在说明类型时,则不能有它,下面说明。由于表示相应变量不用分配内存以生成映射,故其不像上述两种类型修饰符,可以多次重复书写,因为没有意义。且其一定在“*”修饰符的右边,即可以
- long **&a = ppA;
但不能
- long *&*a;
- 或
- long &**a;
因为按照从左到右的修饰符计算顺序,long*&*表示long的指针的引用的指针,引用只是告知编译器不要为变量在栈上分配内存,实际与类型无关,故引用的指针是无意义的。
而long&**则表示long的引用的指针的指针,同上,依旧无意义。同样
- long &a[40];//错误的
因为其表示分配一块可连续存放类型为long的引用的40个元素的内存,引用只是告知编译器一些类型无关信息的一种手段,无法作为类型的一种而被实例化。
应该注意引用并不是类型(但出于方便,经常都将long的引用称作一种类型),而
- long **&rppA = &pA;是错误的
因为上句表示的是不要给变量rppA分配内存,直接使用“=”后面的地址作为其对应的地址,而&pA返回的并不是地址类型的数字,而是指针类型,故编译器将报类型不匹配的错误。
但是即使
- long **&rppA = pA;
也同样失败,因为long*和long**是不同的,不过由于类型的匹配,下面是可以的:
- 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++中的指针概念以及用法。