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 = ;
// 定义整型指针变量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”
cout<<*pN<<endl;

通过“*”运算符可以取得pN这个指针所指向的数据变量N,虽然“N”和“*pN”的形式不同,但是它们都代表内存中的同一份数据,都可以对这个数据进行读/写操作,并且是等效的。

特别地,如果一个指针指向的是一个结构体类型的变量,与结构体变量使用“.”符号引出成员变量不同的是,如果是指向结构体的指针,则应该用“->”符号引出其成员变量。这个符号,多像一个指针。例如:

// 定义一个结构体变量
Employee Zengmei;
// 定义一个指针,并将其指向这个结构体变量
Employee* pZengmei = &Zengmei;
// 用“->”运算符,引用这个结构体指针变量的成员变量
pZengmei->m_strName = "Zengmei";
pZengmei->m_nAge = ;

最佳实践:尽量避免把两个指针指向同一变量

当指针变量被正确赋值指向某个变量后,它也就成为了一个有效的内存地址,也可以用它对另外一个指针赋值。这样,两个指针拥有相同的内存地址,指向同一内容。例如:

// 定义一个整型变量
int a = ;
// 得到变量a的内存地址并赋值给指针pa
int* pa = &a;
// 使用pa对另外一个指针pb赋值
int* pb = pa;

在这里,我们用已经指向变量a的指针pa对指针pb赋值,这样,pa和pb的值是相同的,都是变量a的地址,也就是说,两个指针指向了同一个变量。

值得特别指出的是,虽然两个指针指向同一变量在语法上是合法的,可是在实际的开发中,却是应当尽量避免的。稍不留意,这样的代码就会给人带来困扰。继续上面的例子:

// 输出pa指向的数据,为1982
cout<<*pa<<endl;
// 通过pb修改它所指向的数据为1003
*pb = ;
// 再次输出pa指向的数据,变为了1003
cout<<*pa<<endl;

如果我们仅仅看这段程序的输出,一定会感到奇怪:为什么没有通过pa进行任何修改,而前后两次输出的内容却不同?如果我们结合前面的代码,就会明白,pa和pb指向的是同一个变量a,当我们通过指针pb修改变量a后,再通过pa来获得变量a的数据,自然就是更新过后的了。表面上看起来没有通过pa对变量a作修改,而pb却早已暗渡陈仓,偷偷地将变量a的数据做了修改。在程序当中,是最忌讳这种偷偷摸摸的行为的,因为一旦这种行为导致了程序运行错误,将很难被发现。所以,应尽量避免两个指针指向同一变量,就如同一个人最好不要取两个名字一样。

你好,C++(15)四两拨千斤——3.9 指向内存位置的指针的更多相关文章

  1. 四两拨千斤式的攻击!如何应对Memcache服务器漏洞所带来的DDoS攻击?

    本文由  网易云发布. 近日,媒体曝光Memcache服务器一个漏洞,犯罪分子可利用Memcache服务器通过非常少的计算资源发动超大规模的DDoS攻击.该漏洞是Memcache开发人员对UDP协议支 ...

  2. 转载:四两拨千斤:借助Spark GraphX将QQ千亿关系链计算提速20倍

    四两拨千斤:借助Spark GraphX将QQ千亿关系链计算提速20倍 时间 2016-07-22 16:57:00 炼数成金 相似文章 (5) 原文  http://www.dataguru.cn/ ...

  3. 四两拨千斤,ARM是如何运作、靠什么赚钱的

    在智能手机.平板大行其道的今天,ARM这个名字我们几乎每天都要见到或者听到几次,作为编辑的我更是如此,每天涉及到的新闻总是或多或少跟ARM扯上关系,它还与Intel.AMD.NVIDA等公司有说不清道 ...

  4. 四两拨千斤——你不知道的VScode编码TypeScript的技巧

    转载请注明出处:葡萄城官网,葡萄城为开发者提供专业的开发工具.解决方案和服务,赋能开发者. 原文参考:https://blog.bitsrc 如果你体验过JAVA这种强类型语言带来的便利,包括其丰富的 ...

  5. Java压测之四两拨千斤

    压测之四两拨千斤核心观念: 1.传统的http请求肯定不能用于压测,原因是请求一次,响应一次,而响应数据同时占用了客户端的带宽,故此,客户端请求后,不需要接受响应,让服务器单相思去. 2.寻找可以令服 ...

  6. [XSS防御]HttpOnly之四两拨千斤

    今天看了<白帽子讲web安全>一书,顺便记录一下,HttpOnly的设置 httponly的设置值为 TRUE 时,使得Javascript无法获取到该值,有效地防御了XSS打管理员的 c ...

  7. {Python之线程} 一 背景知识 二 线程与进程的关系 三 线程的特点 四 线程的实际应用场景 五 内存中的线程 六 用户级线程和内核级线程(了解) 七 python与线程 八 Threading模块 九 锁 十 信号量 十一 事件Event 十二 条件Condition(了解) 十三 定时器

    Python之线程 线程 本节目录 一 背景知识 二 线程与进程的关系 三 线程的特点 四 线程的实际应用场景 五 内存中的线程 六 用户级线程和内核级线程(了解) 七 python与线程 八 Thr ...

  8. C++对象模型——指向Member Function的指针 (Pointer-to-Member Functions)(第四章)

    4.4 指向Member Function的指针 (Pointer-to-Member Functions) 取一个nonstatic data member的地址,得到的结果是该member在 cl ...

  9. C语言提高 (4) 第四天 数组与数组作为参数时的数组指针

    1昨日回顾 const int 和 int const是一样的 const char *p;值不变 char * const p; 指针不能变 编译器对参数的退化: 第三种模型: 三级指针 三级指针局 ...

随机推荐

  1. 杂谈之SolrCloud这个坑货

    杂谈之SolrCloud这个坑货 看<Solr In Action>时候看到对Solr不足的介绍有这么一段话:“One final limitation of Solr worth men ...

  2. mongodb 排序 Unable to determine the serialization information for the expression 异常

    好久没用mongodb了...最近又开始用起来了. 遇到情景:   2句话分开写.是正常的,因为我是先取再排序的   然而.我想直接排序出来. 就写在了一起.最后.ToList()   然后报 Una ...

  3. db file sequential read等待事件的一点研究

    db file sequential read等待事件有3个参数:file#,first block#,和block数量. 这个等待事件有3个参数P1,P2,P3, 其中P1代表Oracle要读取的文 ...

  4. 【HDOJ】5063 Operation the Sequence

    #include <cstdio> #include <cstring> #include <cstdlib> #define MAXN 100005 #defin ...

  5. HDOJ 1287 破译密码(异或运算)

    Problem Description 有个叫"猪头帮"的国家,采用一种简单的文法加密,他们所用的语言里面只有大写字母,没有其他任何字符:现在还知道他们加密的方法是:只用一个大写字 ...

  6. linux有用网址

    正则表达式在线测试 http://tool.oschina.net/regex

  7. 四种方法解析JSON数据

    (1)使用TouchJSon解析方法:(需导入包:#import "TouchJson/JSON/CJSONDeserializer.h") //使用TouchJson来解析北京的 ...

  8. python_安装工具easy_install和pip

    前言 用python就必须知道easy_install和pip这两个东西啦 easy_insall提供了在线一键安装模块或包的方便方式,而pip是easy_install的改进版,提供更好的提示信息, ...

  9. EMV/PBOC 解析(二) 卡片数据读取

    上一篇简单的了解了IC智能卡的文件结构和APDU报文,这篇我们直接来读取卡内的数据.下面我们主要参照<中国金融集成电路(IC)卡规范>. 好了废话不多说,下面贴指令: (1)卡片接收一个来 ...

  10. JVM调优之jstack找出最耗cpu的线程并定位代码

    jstack可以定位到线程堆栈,根据堆栈信息我们可以定位到具体代码,所以它在JVM性能调优中使用得非常多.下面我们来一个实例找出某个Java进程中最耗费CPU的Java线程并定位堆栈信息,用到的命令有 ...