C++入门解惑(3)——初探指针(下)

类别:VC语言 点击:0 评论:0 推荐:

2.形形色色的指针

All Kinds of Pointers

 

前一章我们引入了指针及其定义,这一节我们继续研究各种不同的指针及其定义方式(注:由于函数指针较为特殊,本章暂不作讨论,但凡出现“指针”一词,如非特别说明均指数据指针)。

1)指向指针的指针

我们已经知道,指针变量是用于储存特定数据类型地址的变量,假如我们定义

int *pInt;

那么,pInt为一个指向整型变量的指针变量。好,我们把前面这句话的主干提取出来,就是:pInt为变量。既然pInt是变量,在内存中就会有与之对应的存放数据的地址值,那么理论上也就应该有对应的指针来存储,嗯,实际上也如此,我们可以向这样来定义可以指向变量pInt的指针:

int **pIntPtr;

按前一章的方法很好理解这样的定义:**pIntPtr是一个int类型,则去掉一个*,*pIntPtr就是指向int的指针,再去一个*,我们最终得到的pIntPtr就是一个“指向int型指针变量的指针变量”,呵呵,是点拗口,不管怎么说我们现在可以写:

pIntPtr = &pInt;

令其指向pInt变量,而*pIntPtr则可以得回pInt变量。假如pInt指向某个整型变量如a,*pInt可以代表a,因此*(*pIntPtr)此时也可以更间接地得到a,当然我们如果省去括号,写成**pIntPtr也是可以的。

以此类推,我们还可以得到int ***p这样的“指向指向指向int型变量的指针的指针的指针”,或者再复杂:int ****p,“指向指向指向指向……”喔,说起来已经很晕了,不过原理摆在这里,自己类比一下即可。

 

2)指针与常量

C++的常量可以分两种,一种是“文本”常量,比如我们程序中出现的18,3.14,’a’等等;另一种则是用关键字const定义的常量。大多数时候可以把这两种常量视为等同,但还是有一些细微差别,例如,“文本”常量不可直接用&寻找其在内存中对应的地址,但const定义的常量则可以。也就是说,我们不能写&18这样的表达式,但假如我们定义了

const int ClassNumber = 18;

则我们可以通过&ClassNumber表达式得到常量ClassNumber的地址(不是常数18的地址!)。其实在存储特点上常量与变量基本是一样的(有对应的地址,并且在对应地址上存有相应的值),我们可以把常量看作一种“受限”的变量:只可读不可写。既然它们如此相似,而变量有对应的指针,那么常量也应该有其对应的指针。比如,一个指向int型常量的指针pConstInt定义如下:

const int *pConstInt;

它意味着*pConstInt是一个整型常量,因此pConstInt就是一个指向整型常量的指针。我们就可以写

pConstInt = &ClassNumber;

来令pConstInt指向常量ClassNumber. 给你三秒钟,请判断pConstInt是常量还是变量。1,2,3!OK,假如你的回答是变量,那么说明你对常量变量的概念认识得还不错,否则应该翻本C++的书看看const部分的内容。

唔,既然int、float、double甚至我们自己定义的class都可以有对应的常量类型,那么指针应该也有常量才对,现在的问题是,我们应该如何定义一个指针常量呢?我们通常定义常量的作法是在类型名称前面加上const,像const int a等等,但如果在指针定义前面加const,由于*是右结合的,语义上计算机会把const int *p 视为 (const int) (*p)(括号是为了突出其结合形式所用,但不是合法的C++语法),即*p是一个const int型常量,p就为一个指向const int常量的指针。也就是说,我们所加的const并非修饰p,而是修饰*p,换成int const *p又如何呢?噢,这和const int *p没有区别。为了让我们的const能够修饰到p,我们必须越过*号的阻挠将const送到p跟前,假如我们先在前面定义了一个int变量a,则语句

int * const p = &a;

就最终如我们所愿地定义了一个指针常量p,它总是表示a的地址,也就是说,它恒指向变量a.

嗯,小结一下:前面我们讲了两种指针,一种是“指向常量的指针变量”,而之后是“指向变量的指针常量”,它们定义的区别就在于const所修饰的是*p还是p. 同样,还会有“指向常量的指针常量”,显然,必须要有两个const,一个修饰*p,另一个修饰p:

const int * const p = &ClassNumber;

以*为界,我们同样很好理解:*表示我们声明的是指针,它前面的const int表示它指向某个整型常量,后面的const表示它是的个常量指针。

为方便区别,许多文章都介绍了“从右到左”读法,其中把“*”读作“指针”:

const int *p1 = &ClassNumber; // p1是一个指针,它指向int型常量

int * const p2 = &a;          // p2是一个指针常量,它指向int型变量

const int * const p3 = &ClassNumber; // p3是一个指针常量,它指向int型常量

 

好了,我们前面定义指针常量时,受到了*号右结合的困扰,使得前置的const修饰不到p,假如*号能与int结合起来(就像前一章所说的“前置派”的理解),成为一种“指向整型指针的类型”,如

const (int*) p;

const就可以修饰到p了。但C++的括号只能用于改变表达式的优先级而不能改变声明语句的结合次序,能不能想出另一种方法来实现括号的功能呢?

答案是肯定的:使用关键字typedef.

typedef的一个主要作用是将多个变量/常量修饰符捆梆起来作为一种混合性的新修饰符,例如要定义一个无符号的整型常量,我们要写

const unsigned int ClassNumber = 18;

但我们也可以先用typedef将“无符号整型常量”定义成一个特定类型:

typedef const unsigned int ConstUInt;

这样我们只须写

ConstUInt ClassNumber = 18;

就可以达到与前面等价的效果。

咋看似乎与我们关注的内容没有关系,其实typedef的“捆梆”就相当于加了括号,假如,我们定义:

typedef int * IntPtr;

这意味着什么?这意味着IntPtr是一个“整型指针变量”类型,这可是前面所没有出现过的新复合类型,实际上这才是上章“前置派”所理解的“int*”类型:我们当初即使写

int* p1, p2;

虽然有了空格作为我们视觉上的区分,但不幸的是编译器不吃这一套,仍会把*与p1结合,变成

int (*p1), p2;

所以可怜的p2无依无靠只得成为一个整型变量。但现在我们写

IntPtr p1, p2;

结论就不一样了:有了typedef的捆梆,IntPtr已经成为了名符其实的整型指针类型,所以p1,p2统统成为了货真介实的指针。那么我们写

const IntPtr p;

噢,不好意思,编译出错了:没有初始化常量p……咦,看见了没有?在const IntPtr的修饰下p已经成为指针常量了(而不是const int *p这样的指向常量的指针),哦,明白了,由于typedef的捆梆,const与IntPtr都同心协力地修饰p,即理解为:

(const)  (int *) p;

而不是前面的

(const int)  (*p);

所以,不要小瞧了typedef,不要随意将它看作是一个简单的宏替换。事实上《C++ Primer》就曾经出了这样的类似考题,大约也是考你:const IntPtr p中的p是指向const int的指针呢还是指向int的指针常量。我知道现在你可以毫不犹豫地正确地回答这个问题了。

BTW:当初第一次看到的时候,我也是毫不犹豫,可惜答错了^_^

 

3.指针、动态内存、数组

 

我们上一章谈到变量时已经知道,变量实际上就是编译系统为我们程序分配的一块内存,编译器会将变量名称与这块内存正确地联系起来以供我们方面地读写。设想一下,假如一块这样的存储单元没有“变量名”,我们应该如何访问它呢?噢,如果有这个单元的地址,我们通过*运算符也可以得回该对应的变量。

变量定义可以看作两个功能的实现:1.分配内存;2.将内存与变量名联系起来。

按前面所说,如果知道地址,也可以不需要变量名,所以上两个功能如果变成:1.分配内存;2.将分配所得的内存的地址保存起来;

理论上也可以实现上面的功能。在C++中,我们使用new运算符就可以实现第二种方法。new表达式会为我们分配一适当的内存,并且返会该内存的首地址(确切说应该是一个指针)。在表达式中,关键字new后面通常紧跟着数据类型,以指示分配内存的大小及返回的指针类型,例如new int表达式会为我们分配一块整型变量所需的内存(32位机上通常为4字节),然后这个表达式的值就是一个指向该内存的整型指针值。因此我们可以写:

int *p;

p = new int;    // 分配一块用于存储一个整型变量的内存,并将地址赋给指针p

这样我们就可以通过*p来对这块“没有变量名”的内存进行相同的操作。

前面我们仅仅在内存中分配了一个整型存储单元,我们还可以分配一块能存储多个整型值的内存,方法是在int后面加上用“[ ]”括起来的数字,这个数字就是你想分配的单元数目。如:

int *p;

p = new int[18];  // 分配一块用于存储18个整型变量的内存,并将首地址赋给指针p

但这时候我们用*p只能对18个整型单元的第一个进行存取,如何访问其它17个单元呢?由于这些单元都是连续存放的,所以我们只要知道首地址的值以及每个整型变量所占用的空间,就可以计算出其它17个单元的起始地址值。在C++中,我们甚至不必为“每个整形变量所占空间”这样的问题所累,因为C++可以“自动地”为我们实现这一点,我们只需要告诉它我们打算访问的是相对当前指针值的第几个单元就可以了。这一点通过指针运算可以实现,例如,按前面的声明,现在p已经指向18块存储单元的第一块,如果我想访问第二块,也就是p当前所指的下一块内存呢?很简单,只要写p+1,这个表达式的结果就会神奇地得出第二块内存单元的地址,如果你的机器是32位,那么你感兴趣的话可以打印一下p的地址值与p+1的地址值,你会发现它们之间相差的是4个字节,而不是1个,编译器已经自动为我们做好了转换的工作:它会自动将1乘上指针所指的一个变量(整型变量)所占的内存(4字节)。于是我们如果想要给第二内存单元赋值为3 ,则只须写:

*(p + 1) = 3;   // 注意:*号优先级比+号要高,所以要加上括号

要打印的时候就写:

cout << *(p+1);  // 输出3

总之这些和一般的变量一样使用没有什么两样了。我们当然也可以将它的地址值赋给另外的指针变量:

int *myPtr;

myPtr = p + 1;       // OK,现在myPtr就指向第二内存单元的地址

也可以进行自加操作:

myPtr++;       // 按上面的初值,自加后myPtr已经指向第三内存单元的地址

*myPtr = 18;    // 现在将第三个内存单元赋予整型值18,也就相当于*(p + 2) = 18

到目前为止一切都很好,但*(p +1)这样的写法太麻烦,C++为此引入了简记的方法,就是“[ ]”运算符(当初定义的时候也用过它哦):要访问第二单元内存,我们只需要写p[1]就可以,它实际上相当于*(p + 1):

p[1] = 3;      // *(p + 1) = 3;

cout << p[15];   // cout << *(p + 15);

p[0] = 6;      // *(p + 0) = 6;  也就是 *p = 6;

为了说明“[ ]”与*(… + …)的等效性,下面再看一组奇怪的例子:

1[p] = 3;      // *(1 + p) = 3;

cout << 15[p];   // cout << *(15 + p);

0[p] = 6;       // *(0 + p) = 6; 也就是 *p = 6;

看起来是不是很怪异?其实这一组只不过交换了一下加数位置而已,功能与上一组是完全一样的。

前面我们介绍了一种分配内存的新方法:利用new运算符。new运算符分配的内存除了没有变量分配时附带有的变量名外,它与变量分配还有一个重要的区别:new运算符是在堆(heap)中分配空间,而通常的变量定义是在栈(stack)上分配内存。堆和栈是程序内存的两大部分,初学可以不必细究其异同,有一点需要明白的是,在栈上分配的内存系统会自动地为其释放,例如在函数结束时,局部变量将不复存在,就是系统自动清除栈内存的结果。但堆中分配的内存则不然:一切由你负责,即使你退出了new表达式的所处的函数或者作用域,那块内存还处于被使用状态而不能再利用。好处就是如果你想在不同模块中共享内存,那么这一点正合你意,坏处是如果你不打算再利用这块内存又忘了把它释放掉,那么它就会霸占你宝贵的内存资源直到你的程序退出为止。如何释放掉new分配的堆内存?答案是使用delete算符。delete的大概是C++中最简单的部分之一(但也很容易粗心犯错!),你只要分清楚你要释放的是单个单元的内存,还是多个单元的内存,假如:

int *p = new int;        // 这里把分配语句与初始化放在一起,效果和前面是一样的

…  // 使用*p

delete p;     // 释放p所指的内存,即用new分配的内存

如果是多个单元的,则应该是这样:

int *p = new int[18];

… // 使用

delete[] p;    // 注意,由于p指向的是一块内存,所以delete后要加“[]”

// 以确保整块内存都被释放,没有“[]”只会释放p指的第一块内存

刚才我们是在堆中分配连续内存,同样,在栈上也可以分配边续内存,例如我们同样要分配18个单元的整型内存空间,并将首地址赋予指针a,则定义如下:

int a[18];

类似于前面用new的版本,系统会在栈上分配18个整型内存单元,并将首地址赋予指针a,我们同样可以通过“[ ]”操作符或者古老的“*(… + …)”来实现对它的访问。需要注意的是a是一个指向整型的指针常量类型,不可以再对a赋值使其指向其它变量。同样,由于是在栈中分配内存,释放工作也不必由我们操心。由于a“看起来”包含了许多个相同类型的变量,因此C++将其称为数组。

由上面看来,栈分配的数组似乎比堆分配要简单好用,但栈分配有一个缺点,就是必须在编译时刻确定内存的大小,也就是说,假如我要写一个排序程序,每次参加排序的元素个数都不一样,但我不能写

int number;

cin >> number;

int a[number];   // 错误,number是变量,而作为栈上分数空间的数组a的大小必须在

            // 编译时就决定

但我可以写

int number;

cin >> number;

int *a = new int[number];       // 没有问题,堆空间分配可以在程序运行时才确定

当然最后别忘了释放就成了:

delete[] a;

由于堆内存的分配比栈内存具有更大的灵活性,可以在程序执行期动态决定分配空间的大小,所以又称为动态内存。

本文地址:http://com.8s8s.com/it/it1538.htm