似乎从古老的C时代起,指针就开始成为群众心目中的难点;在如今的C++中,面向对象、模板等技术的复杂使得过去C中面向过程基础部分的学习难度淡化了,但指针这部分内容依然占据在“难点区”的位置。究其原因,可能是相当部分C/C++都是从Basic这样比较“高级”的语言转移过来的,甚或从零开始学习而之前基本没有太多的编程经验。而指针则属于C/C++中最“低级”的部分之一,又是重头戏,花样比较多,对底层毫不了解的初学者们在过这道槛时可能会晕一晕,这也在所难免。这节我们就尝试从一些基本的相对底层的原理讲起,唔,但愿能对各位看官有所帮助。
0.先谈谈变量
0.Talk about Variable
啊,变量,那倒是个熟悉亲切的概念。在绝大多数的高级语言中,变量是字母和一些允许的符号(数字、下划线等)组成的标识符,它可以为我们记录有用的数据,并且能够参与运算、重新赋值……嗯,真是老朋友了。不过如果你要学C++,对这位老朋友你还要有更深刻的了解。先从变量的声明说起,例如,我们写:
int main()
{
int a;
// ...
return 0;
}
显然,在主程序的首行我们声明并定义了一个整型变量a,系统(确切地说是编译器)看到这一行,会做两件事:1.分配一小块可用于容纳整型数值的内存专门为存储变量a使用;2.暗中记住这块内存的首地址,以后你的程序只要是使用a的地方,系统就会通过那个地址值来找到那块内存,再按你的要求进行相应的操作……噫,要知道,内存分为许许多多的存储单元,就像城市里的各家各户,每个单元(家户)都有一个独一无二的称为地址值的数字(门牌号)与其它单元区别开,要访问某个单元(查户口,呵呵),就要通过地址值(门牌号)来进行索引(走访)。
噫,到目前为止这些操作都是系统在暗地里为我们代劳了,所以我们才得以轻轻松松地撰写出简洁明了的程序。不过如果我们好奇心大发要看个究竟,C++也是允许的。例如,我们要查看a的地址值,则可以使用取值运算符&,只要在变量前加上&,便可以得到它的地址:
#include <iostream>
using namespace std;
int main()
{
int a;
a = 3;
cout << "a = " << a << '\n'; // 第一个与变量相关联的信息:变量的值
cout << "Address of a: " << &a << endl; // 第二个与变量相关联的信息:变量的
// 地址值
return 0;
}
下面是我的机子上刚刚输出的结果:
a = 3
Address of a: 0x241ff5c
对a = 3的输出各位应该没有什么问题,但Address of a,则不同的机器,甚至同一机器不同的时间都会不一样,这很好理解:毕竟内存布局总是时时改变的,系统给你分配一块空间,能用就是啦,不要太挑剔哦。还有,“0x241ff5c”是不是有些怪怪的?这是因为对地址的输出默认采用16进制,所以看起来比较玄奥,其实骨子里不过一整数罢了,你要看不顺眼用十进制输出当然也行,涉及到输出流的格式操纵符,这里就不多说啦(自己查书^_^)。
现在,只要给对任何一个变量(或者对象,本节之后均统称变量),我们都可以通过&运算符了解它具体的存储地址。那么,反过来,假如我们知道一个变量的地址,如何反过来对它进行变量式的操作呢?答案是使用“*”运算符,和“&”运算符一样,它也是加在一个地址值之前,结果我们就获得了该地址所对应的变量,这个过程通常称为解引用(dereference),“*”便是解引用运算符:
#include <iostream>
using namespace std;
int main()
{
int a;
a = 3;
cout << "a = " << a << '\n'; // 和前面一样……
cout << "Address of a: " << &a << '\n'; // 和前面也一样……
cout << "Get back a: ";
cout << *(&a) << '\n'; // 先用&a得到a的地址,再用*将由地址得回变量a
*(&a) = 5; // 同样,用*得回的变量可以和原变量一样进行常规操作
cout << "Now, a = " << a << endl; // 验证a是不是真的改变
return 0;
}
这个搞笑的程序输出结果如下(同样,地址值只是可能的版本):
a = 3
Address of a: 0x241ff5c
Get back a: 3
Now, a = 5
呵呵,之所以说它搞笑,是因为我们先用取址算符对a进行取址,然后又用反引用算符从地址值得回a,实际开发中不大可能出现如此直接的曲线救国。不过这个搞笑程序倒是挺真切地让我们明白取值运算符&与反引用运算符*是一对截然相反的兄弟,前者由变量得到其地址,而后者则由地址得到其对应变量。如果两者同时施用,即程序中的*(&a),则我们兜了一个圈最后还是得回了a本身。
1.指 针
1.Pointer
现在我们又知道了C++中的新一种数值类型:地址值,它与整型、浮点型、双精度型、字符型等一样是程序组成运作的重要类型,只是大多数时候都被隐藏在程序语言背后而已。我们还知道,整型、浮点型等数值都对应有整型变量/常量、浮点型变量/常量来对它们进行存储,那么地址值是否可以用相应变量/常量进行存储呢?答案是肯定的。C/C++中用于存储地址值的变量称为指针变量/常量,简称为指针。这是一个颇为形象的说法:指针存储的地址值通常是“指”向某个特定的存储空间,有了指针的引导,我们就可以访问相应的量;而且指针变量中存储的地址是可以通过变量赋值改变的,这就使得指针具有相当大的灵活性;此外,动态内存分配也离不开指针……这些都是以后的内容,嗯,慢慢来,我们还是先研究一下如何声明指针变量。
对于整形变量,我们声明时使用关键字int进行修饰,如
int a;
声明了一个名为a的整型变量,由于a是由int修饰的,所以a就有int型变量所具有的属性:比如它支持加法、减法、乘法、整除(而不是除法)、赋值等操作。那么假如我加上几个字符,变成:
int a, *p;
那又是什么意思呢?按照上面的思路,给你五秒钟的时间思考:1,2,3,4,5!OK,你大可以照着说,我们声明了两个整型的东东:a和*p,它们都支持加法、减法、乘法、整除(而不是除法)、赋值等操作……*p与a应当是等同的,它们都理应可以代表一个整型变量。噫,很好,这是我们完全基于自己的理解而推断出来的。现在我们再来研究一下:既然*p可以代表一个整型变量,那么去掉*号,p应该意味着什么?
还记得前面所说的&与*的神奇关系么?它们起着截然相反的功能:对变量加&得到其地址,对地址加*得到其对应变量。现在把前面这句话反向表述,就变成:对地址去掉前面的&则得回其对应变量(如&a变回a),对变量去掉*则得回其地址(如前面所说的*(&a)完全相当于变量a,去掉*变为&a则得到a的地址)。
好,我们知道*p可以代表整型变量,因此去掉*的p,就代表了*p的地址,也就是说,p表示一个地址!
哈,现在我们总算可以理解为什么声明一个指针用的是*号而不是&号或者其它什么东东了。现在有一个问题:声明(定义)变量会出现相应的内存分配,例如系统看到int a;会为a分配地块内存,如此看来,对于语句int a, *p;似乎同时会为a,*p各分配一块存储整数的内存。
噢,比较不幸,这回我们的逻辑没有发挥理所当然的作用,对于C/C++,int a, *p将使系统为a分配一块整型值内存(毫无疑问),但对于*p,由于*是一个运算符,仅起标示作用,所以系统关注的仅仅是标识符p,它将为p分配内存,而不是*p。嗯,再重复一遍吧:*只是帮助系统了解p的地位而出现的,任何声明(定义),系统最终为其分配内存的只是标识符本身,所以最终p会得到属于它的内存。
p得到的是一块怎样的内存呢?既然*p可以代表int型变量,因而刚刚我们分析出p就代表了标示某个int型变量的地址值,把它们连起来说,就是:p得到的内存就用于存储标示某个int型变量的地址值——p是一个存储整型地址的变量——噢,p正是我们前面所设想的指针变量!呵呵,费尽心力,终于得到你了!
嗯,一般的C++教程介绍指针的时候都是直接介绍其声明,而我们基本上是通过逻辑推理,结合第0节的基础一步一步导出声明方法的,这样的学习我想或许更有助于我们在比较细致的层次上把握语言实质。本章表述上较详细,文字较多(本人功夫有限,呵呵),但思路应该还不算太复杂,只有几个推导链,希望初涉此道的学友们有空多读几遍,把它读透读薄吧。然后……放松一下,喝喝茶,听听音乐什么的(其实我也累了^_^ ),下回我们再继续讲指针的声明原理和一些应用。在下才疏,与各位一样都只是C++的初学者与爱好者,虽然无知,但喜欢思考一些杂散的问题,如果有什么错误之处还望众朋友指出,在下感激不尽。如果对本系列有什么建议与意见,也非常欢迎提出。总之,希望我们能够共同进步。