简单说,是为了必要的,且很基础的表达能力 (描述能力)。
0. 数据四要素:名、值、址、型
指针、引用的基础,就是在描述一个数据时,除了这个数据的“值”以外,引入了这个数据的“地址”(以下也会简称“址”),即数据在哪个位置上。
进一步理解,数据数据的“地址”,表达的是这个数据的“实体”,数据的“值”,表达的是数据的“表象”。
就好比说,你老婆的漂亮值是 100,我老婆的也是,但这两个100并不是在描述同一个实体。
相同的值,可能在表达不同的类型的数据,所以,在值、址之外,数据还需要一个“类型”(以下也会简称“型”)。
最后,除了“值”、“址”、“型”为了方便我们在代码中指定某个数据,代码中的多数据,还有一个属性“名字”比如变量或常量的名字(以下也会简称“名”)。有名数据通常就是变量、常量,无名的数据就是“字面量”。
编译型的代码,在编译之后,“名”这个属性通常没有一直存在的必要性(哪怕有带反射),因为程序绝大多数情况下,可以通过地址来精确指代一个数据。
类似的,“型”在代码表达中极其重要,但在在编译之后的程序里,必要性也不高。
1. 猥琐语言的进化史
好,同学们,下面我来举一个语言的例子,让大家肉眼观察这门语言是如何进化出“指针、引用”的。首先我们需要从基础的,数据据的名、值、址的理解开始。
我们即时发明的一种程序语言,名为 “猥琐”计算机语言,有时也简称WS。
1.1 从字面数据开始
让我们WS语言开始写代码:
18
18
由于网上竟然还没有支持猥琐语言的高亮脚本,所以以下只好委屈 C、C++、Go 等语言了,我们的代码标记为它们以获得高亮。
代码中出现两个18!问题马上来了:这两个18表达的是同一个意思吗?如果是同一个意思?那为什么要有两个18?
1.2 上帝说:要有类型……
也就是说,现在,两个数据都有“值”,但是,它们在表达什么?它们没有类型吗?让我们查询一下WS语言手册有什么内置类型……
哇!好棒,内置类型竟然有 年龄、长度、胸围……
我们选长度:
18 : 长度 // WS语言语法: 值 : 类型
18 : 长度
有“值”有“型”,但还不够——
- 它们分别是什么东西的长度?
- 假设代码中有很多地方需要使用到这个长度,难道一直写 18 ,万一哪天要改成 17呢?
- 如果就是写 18,那为什么要定义两个18?只有一个就够了呀?
好吧,是时候让“名”登场了:
JJ长 = 18 : 长度 // WS是一门国际化语言,支持数据使用汉字作名字
JJ长 = 18 : 长度
1.3 不要重名,不要重名!
等等!!!一前一后两个数据的“名”竟然完全同名!WS语言制定者,你们太不专业了!
- WS语言制定者:名字相同有什么不合理吗?现实生活中,你老婆叫王大锤,我老婆也叫王大锤啊!
- WS语言用户:那我们怎么区分哪个是哪个啊!!!!
- WS语言制定者:当然是看数据的地址,也就实体啊!地址不同,实体就不同;在你家床上睡的王大锤是你老婆,在我家床上睡的是我老婆啊?有什么区分困难吗?
- WS语言使用者:可是上面代码中,没有体现数据的地址属性啊。
哦!也对。计算机语言中的数据,是如何体现它们(存放在内存中的)地址不同呢? 让我们借鉴一下其它语言吧,包含但不限于:C、C++、C#、Java、Go……
借鉴完毕,原来这么简单呀:就是不直接在代码使用地址表达数据,但是在有需要时,可以通过数据“名字”来映射到数据的“地址”。
"有需要时" 是重点。说明见后。
这么说,数据的名字,还真是不要允许同名的好,万一要同名的话,两个数据就必须处于不同的上下文中……比如复合语句作用域,比如名字空间……
算了算了,猥琐语言主打简洁而猥琐,就让我们简(猥)单(琐)地规定所有变量不允许同名吧!
我的JJ长 = 18 : 长度
你的JJ长 = 18 : 长度
好,现在我们拥有两个数据:我的JJ长,和你的JJ长。核心问题来了:这两个数据除了名字不一样以外,它们“型”相同、“值”相同,所以,它们是同一个数据吗?
1.4 地址不同,才是真的不同
当然不是!我的JJ在我这里,你的JJ在你这里。
同学: 老师,偶尔也会在王大锤(们)那里。
老师:你们在说什么?
也就是说,“我的JJ长”和“你的JJ长”,值相同,型相同,名不同,址不同。
“址”肯定代表一个数据实体。而“名”,如前所述,主要是给程序员使用的,它有时代表“值”,有时代表“址”。
[重点]
- 有的计算机语言,在某些上下文可以自行决定这个名字表达的是“值”,还是“址”,即左值右值之分,在无法自动区分的情况下,当需要从名到址转换时,采用一个特殊的操作来表达,比如C、Go、C++中用特定的“取址操作符”(通常是 &) ;
- 而有的语言,几乎都可以纯粹通过上下文来判断,比如 Java。这类语言通常就是把“型”和“名”的作用做了硬性绑定:某些“型”的数据,它的“名”只表达值,某些“型”的数据,它的“名”可以表达“址”。
1.5 当数据需要传递……
猥琐语言少不了有函数。现在我们就需要一个叫“增大术”的函数,长这样:
func 增大术 ( jjl : 长度 ) -> boolean {
if jjl <= 5 :
输出 "根基太小了,恕本医师无能为力"
return false
if jjl >= 30 :
输出 "请不要过份追求肉体上的尺寸!内心的强大才是真正的强大!"
return false
jjl++
return true
}
可以把“增长术”这个函数,理解为一场“手术”,所以它返回手术是否成功,而不是返回新长度;同时,它动刀的是原有长度,同样不是返回新长度。
由此,我们预期:
你的JJ长 = 18 : 长度
增大术 (你的JJ长)
输出 你的JJ长 // 应该得到 19
但是,问题来了, 代码中第二行的, 传入 增大术函数的 你的JJ长 ,现在这个“名”,它表达的是对应数据的“值(表象)”?还是“址”(实体)呢?
显然得是后者!显然没有哪一位大神医师,能通过计算一个18+1得到19,就让你身上的某个东西从18变成19,你必须掏出实体并且放到手术台上啊!
猥琐语言使用者(医生角色):“来,这位患者,请把“你的JJ长”的实体,传送过来,摆在这里,我要动刀(修改)了”
猥琐语言制定者:“等一下,让我想想……”
没错,是时候想一想WS语言的下一步发展了。如前所述,我们有两个选择:
- 通过增加明确的表达方式(比如一个符号),来区分一个数据的名字是“值”或是“址”(指针);
- 通过将“型”和“名”的作用硬性绑定,让特定的某些类型的数据,名字即可表达数据的“址”(常被称为“引用类型”),而另一些类型的数据,名字就永远只能表达“值”(常被称为“值类型”)
选择1,是C++的路;选择2,是Java的路。
选择在1和2之间来回混的,是 go语言的路。
无论何种选择,反正,指针或引用,就这么引入了。让我们都给个例子:
(一)、 C++风格
func 增大术 ( jjl : *长度 ) -> boolean { // 注意,现在入参的类型是 “ *长度 ”
...
}
你的JJ长 = 18 : 长度
增大术 ( & 你的JJ长) // 注意多了一个取址符 &
输出 你的JJ长 // 得到 19
(二)、Java风格
先规定 长度 是值类型(不能表达址),再特意搞出一种对应类型,叫 “长度的包装箱”,是引用类型:
func 增大术 ( jjl : 长度的包装箱 ) -> boolean { // 注意,现在入参的类型是 “长度的包装箱”
...
}
你的JJ长 = 18 : 长度
放在这个箱子里的你的JJ长 = 装箱操作(你的JJ长) // 注意有个装箱操作
增大术 ( 放在这个箱子里的你的JJ长 ) // 注意实参现在放在箱子里再传过去……
你的JJ长 = 拆箱操作(放在这个箱子里的你的JJ长) // 注意这里拆快递了
输出 你的JJ长 // 得到 19
2 最后,回到问题
指针和引用当然还有别的用处,且有既有紧密关系又有重大区别。但都不是问题“为什么要有指针和引用类型”的重点了,因为本文已经讲了“为什么需要指针或引用”的一个必要条件。