你好,C++(13)四两拨千斤------3.9 指向内存位置的指针
3.9 指向内存位置的指针
一天,两个变量在街上遇到了:
"老兄,你家住哪儿啊?改天找你玩儿去。"
"哦,我家在静态存储区的0x0049A024号,你家呢?"
"我家在动态存储区的0x0022FF0C号。有空来玩儿啊。"
在前面的章节中,我们学会了用int等数值数据类型表达各种数字数据,用char等字符数据类型表达文字数据,我们甚至还可以 用结构体将多个基本数据类型组合形成新的数据类型,用以表达更加复杂的事物。除了这些现实世界中常见的数据之外,在程序设计当中,我们还有另外一种数据经 常需要处理,那就是变量或者函数在内存中的地址数据。比如,上面对话中的0x0049A024和0x0022FF0C就是两个变量在内存中的地址。而就像 对话中所说的那样,我们可以通过变量在内存中的地址便捷地对其进行读写访问,因而内存地址数据在程序中经常被用到。在C++中,表示内存地址数据的变量被 称为指针变量,简称指针。
指针,是C++从C语言中继承过来的,它提供了一种简便、高效地直接访问内存的方式。特别是当要访问的数据量比较大时,比如某 个体积比较大的结构体变量,通过指针直接访问变量所在的内存,要比移动复制变量来对其进行访问要快得多,可以起到四两拨千斤的效果。正确地使用指针,可以 写出更加紧凑、高效的代码。但是,如果指针使用不当,就很容易产生严重的错误,并且这些错误还具有一定的隐蔽性,极难发现与修正,因而它也成为千千万万程 序员痛苦的根源。爱恨交织,是程序员们对指针的最大感受,而学好指针,用好指针,也成为每个C++程序员的必修课。
3.9.1 内存空间的访问形式
指针是专门用来表示内存地址的,它的使用跟内存访问密切相关。为了更好地理解指针,我们先来看看C++中内存空间的访问形式。
在C++程序中,有两种途径可以对内存进行访问。一种是通过变量名间接访问。为了保存数据,通常会先定义保存数据的变量。定义 变量也就意味着系统分配一定的内存空间用于存储某个数据,而变量名就成了这块内存区域的标识。通过变量名,我们可以间接地访问到这块内存区域,在其中进行 数据的读取或者写入。
另外一种方式就是直接通过这些数据所在内存的地址,也就是通过指针来访问这个地址上的数据。
这两种都是C++中访问内存的方式,只是一个间接一个直接。打个不太恰当的比喻,比如我们要送一个包裹(数据)到某个地方(内 存中的某块区域)去。按照第一种方式,我们说:送到亚美大厦(变量名)。而按照第二种方式,我们会说:送到科技路83号(内存地址)。虽然这两种方式表达 形式不同,但实际上说的是同一件事。
在典型的32位计算机平台上,可以把内存空间看成是由很多个连续的小房间构成的,每个房间就是一个小存储单元,大小是一个字节 (byte),而数据们就住在这些房间当中。有的数据比较小,比如一个char类型的字符,它只需要一个房间就够了。而有的数据比较大,就需要占用好几个 房间。比如一个int类型的整数,其大小是4个字节,就需要4个房间才可以安置。为了方便地找到住在这些房间中的数据,房间都被按照某种规则进行了编号, 这个编号,就是通常所说的内存地址。这些编号通常用一个32位的十六进制数来表示,比如上面例子中的0x0049A024、0x0022FF0C等如图 3-6所示。
图3-6 住在内存中的数据
一旦知道某个数据所在的房间编号,就可以直接通过这个编号来对相应房间中的数据进行读写访问。就像上面的例子中把包裹直接送到科技路83号一样,我们也可以把数据直接保存到0x0022FF0C。
3.9.2 指针变量的定义
指针,作为一种表示内存地址的特殊变量,其定义的形式也有一定的特殊性:
数据类型* 变量名;
其中,我们用指针所表示地址上的数据类型来作为定义指针变量时用的数据类型。比如,我们要定义一个指针来表示某个int类型数 据的地址,那么指针定义中的数据类型就是int。这个数据类型是由指针所指向的数据来决定的,可以是int、string和double等基本数据类型, 也可以是自定义的结构体等复杂数据类型。简而言之,指针指向的数据是什么类型,就用这种类型作为指针变量定义时的数据类型。数据类型之后的"*"符号表示 定义的是一个指针变量。"变量名"就是给这个指针指定的名字。例如:
// 定义指针变量p,它可以记录某个int类型数据的地址
int* p;
// 定义指针变量pEmp,它可以记录某个Employee类型数据的地址
Employee* pEmp
最佳实践:选择合适的定义指针变量的方式
实际上,下面两种定义指针变量的形式都是合乎C++语法的:
int* p;
int *p;
这两种形式都可以编译通过,并表示相同的语法含义。但是,这两种形式所反映的编程风格和对代码阅读者所强调的意义不同。
"int* p"强调的是"p为一个指向int类型整数的指针",这里,可以把int*看成为一种特殊的数据类型,而整个语句强调的是p为这种数据类型(int*)的一个变量。
"int *p"则是把*p当成一个整体,强调的是"这个指针指向的是一个int类型的整数",而p就是指向这个整数的指针。
这两种形式没有对与错的区别,只有个人喜好的区别。本书推荐第一种形式,它把指针也当成是一种数据类型,定义指针变量的语句更加清晰明了,可读性更强。
特别地,当在一条语句中定义多个指针变量时,可能会让人混淆,例如:
// p是一个int类型的指针变量,而q实际上是一个int类型的变量
// 可能会让人误认为p和q都是int类型指针
int* p, q;
// 清楚一些:*p是一个整数,p是指向这个整数的指针,q也是一个整数
int *p, q;
// 定义两个指向int类型数据的指针p和q
int *p, *q;
在开发实践中,有这样一条编码规范:"一条语句只完成一件事情"。按照这条规范,只要我们分开定义p和q,就可以很好地避免上述问题。
如果我们确实需要定义多个相同类型的指针变量,我们也可以用typedef关键字将指针类型定义成新的数据类型,然后用这个新的数据类型定义多个指针变量:
// 将Employee指针类型定义成新的数据类型EMPointer
typedef Employee* EMPointer;
// 用EMPointer类型定义多个指针变量,这些变量都是"Employee*"类型
EMPointer pCAO,pCBO,pCCO,pCDO;
3.9.3 指针的赋值和使用
在定义得到一个指针变量之后,指针变量的值还是一个随机值。它所指向的可能是某个无关紧要的数据,但也可能是重要的数据或者程 序代码,如果直接使用其后果是不可预期的。也许啥事儿没有,也许因此而引起地球毁灭。所以在使用指针之前,必须对其赋值进行初始化,将其指向某个有意义的 合法内存位置。对指针变量进行赋值的语法格式如下:
指针变量 = 内存地址;
可以看到,对指针变量的赋值,实际上就是将这个指针指向某一内存地址,而这个内存地址上存放的就是这个指针想要指向的数据。我 们知道,数据是用变量来表示的,获得变量的内存地址也就相当于获得这个数据所在的内存地址,进而也就可以用它对指针变量赋值了。在C++中,我们可以利用 "&"取地址运算符,将它放在某个变量的前面,就可以获得这个变量所在的内存地址。例如:
// 定义一个整型变量,用以表示整型数据1003
int N = 1003;
// 定义整型指针变量pN,用"&"符号取得整型变量N的地址,
// 并将其赋值给整型指针变量pN
int* pN = &N;
这里,我们用"&"符号取得整型变量N的内存地址,这也就是1003这个整型数据所在的内存地址,然后将其赋值给整型指针变量pN,也就是将指针pN指向了1003这个数据。如图3-7所示。
图3-7 指针和指针所指向的数据
指针的初始化赋值最好是在定义指针的时候同时进行,比如上面的例子中,在定义指针pN的同时即取得变量N的内存地址赋值给它, 从而使得指针在一开始就有一个合理的初始值,避免未初始化的指针被错误地使用。如果在定义指针时,确实没有一个合理的初始值,我们可以将其赋值为 nullptr关键字,它表示这个指针没有指向任何内存地址,是一个空指针(null pointer),还不能使用。例如:
// 定义一个指针变量pN,赋值为nullptr表示它没有指向任何内存位置
// 这里只是定义变量,后面才会用到
int* pN = nullptr;
// ...
// 判断pN是否指向了某个数据
// 如果pN的值不是nullptr初始值,就表示它被重新赋值指向了某个数据
if(nullptr != pN)
{
// 使用pN指针访问它所指向的数据
}
可以用"&"获得一个数据的内存地址,反过来,我们也可以用"*"获得一个内存地址上的数据。"*"称为指针运算符,也称为解析运算符。它 所执行的是跟"&"运算符完全相反的操作。如果把它放在一个指针变量的前面,就可以取得这个指针所指向内存地址上的数据。例如:
// 输出pN指向的内存地址0x0016FA38
cout<<pN<<endl;
// 通过"*"符号获取pN所指向内存地址上的数据"1003"并输出
// 等同于cout<<N<<endl;
cout<<*pN<<endl;
// 通过指针修改它所指向的数据
// 等同于N = 1982;
*pN = 1982;
// 输出修改后的数据"1982"
cout<<*pN<<endl;
通过"*"运算符可以取得pN这个指针所指向的数据变量N,虽然"N"和"*pN"的形式不同,但是它们都代表内存中的同一份数据,都可以对这个数据进行读/写操作,并且是等效的。
特别地,如果一个指针指向的是一个结构体类型的变量,与结构体变量使用"."符号引出成员变量不同的是,如果是指向结构体的指针,则应该用"->"符号引出其成员变量。这个符号,多像一个指针。例如:
// 定义一个结构体变量
Employee Zengmei;
// 定义一个指针,并将其指向这个结构体变量
Employee* pZengmei = &Zengmei;
// 用"->"运算符,引用这个结构体指针变量的成员变量
pZengmei->m_strName = "Zengmei";
pZengmei->m_nAge = 28;
最佳实践:尽量避免把两个指针指向同一变量
当指针变量被正确赋值指向某个变量后,它也就成为了一个有效的内存地址,也可以用它对另外一个指针赋值。这样,两个指针拥有相同的内存地址,指向同一内容。例如:
// 定义一个整型变量
int a = 1982;
// 得到变量a的内存地址并赋值给指针pa
int* pa = &a;
// 使用pa对另外一个指针pb赋值
int* pb = pa;
在这里,我们用已经指向变量a的指针pa对指针pb赋值,这样,pa和pb的值是相同的,都是变量a的地址,也就是说,两个指针指向了同一个变量。
值得特别指出的是,虽然两个指针指向同一变量在语法上是合法的,可是在实际的开发中,却是应当尽量避免的。稍不留意,这样的代码就会给人带来困扰。继续上面的例子:
// 输出pa指向的数据,为1982
cout<<*pa<<endl;
// 通过pb修改它所指向的数据为1003
*pb = 1003;
// 再次输出pa指向的数据,变为了1003
cout<<*pa<<endl;
如果我们仅仅看这段程序的输出,一定会感到奇怪:为什么没有通过pa进行任何修改,而前后两次输出的内容却不同?如果我们结合前面的代码,就会明 白,pa和pb指向的是同一个变量a,当我们通过指针pb修改变量a后,再通过pa来获得变量a的数据,自然就是更新过后的了。表面上看起来没有通过pa 对变量a作修改,而pb却早已暗渡陈仓,偷偷地将变量a的数据做了修改。在程序当中,是最忌讳这种偷偷摸摸的行为的,因为一旦这种行为导致了程序运行错 误,将很难被发现。所以,应尽量避免两个指针指向同一变量,就如同一个人最好不要取两个名字一样。