Skip to content

Latest commit

 

History

History
133 lines (86 loc) · 10.1 KB

2-2.md

File metadata and controls

133 lines (86 loc) · 10.1 KB

融合态 · 复合类型

以瓶子与篮子作为物品分类后使用的容器,是很好的想法。然而,实际情况并非如此轻松:

  • 买了一堆蔬果,短时间内放在篮子中还不成问题,但时间长久便会腐烂。这时候,也许我们该考虑把它们放在冰箱里。
  • 氢氟酸能够与玻璃反应、腐蚀玻璃容器。液体理应放在瓶子中,不过如果碰上这样厉害的液体,也许我们得额外补充一下使用什么样的瓶子——例如陶瓷瓶或钢瓶。
  • 液氮很容易升华,瞬间产生的高压气体很容易把任何日常使用的瓶子炸的粉碎。看来普通的瓶子是不能用了,也许我们得使用带内胆的专用储气钢瓶才能容得下它。

我们时常会遇到各种复杂的需求,而 C++ 自身提供的那点点类型当然是不够用的。这就需要我们视情况构造符合需求的类型。构造新的类型,形象地讲也就是将类型与类型、类型的一些特性拼凑起来,成为更为复杂的类型。

接下来,我们将首先了解最基本的复合类型——指针、引用与数组。


我们知道,计算机最本质的任务是处理数据,而处理数据的最基本的场所便是内存。

我们在程序中声明的变量都将储存在内存中,而每个变量都会在内存中有一条属于自己的地址

0 x 80002017 1600 (int)a

0 x 80002021 2100000009 (int)b

0 x 80002025 6.18e-1 (double)c

0 x 80002029 (与上一单元连用,double类型一次占用 8 字节空间)

0 x 80002033 0 (int)d

......

形象地讲,每个变量都在内存中“租”了点空间供容纳数据,而这些空间的“门牌号”便是变量所在的地址。也许有些变量的类型很“肥”,“租”下来的空间比别人的要大一些(如doublelong long类型);也许有些变量的类型又很“瘦”,“租”下来的空间又比别人的小一些(如charshort类型)。不管怎么个“租”法,没有哪个变量是有权利“占据”其它尚还“健在”变量的空间的,只得一个挨一个地找闲余的空间使用。

为什么要提到变量在内存中的“住宿情况”呢?因为我们接下来要介绍的类型,便是用来帮助我们顺着相当于“门牌号”地内存地址访问对应变量所占据的空间的:

int a = 3;
int *p = &a;					// 声明一个指针 p,指向变量 a
std::cout <<  p << std::endl	// 第一行输出 p 实际的值,即 a 的地址
    	  << *p << std::endl;	// 第二行输出访问 p 所存地址对应的变量 a 的值

仔细观察一下,新的变量p有何特殊之处?

是的,一是p的前头多了个星号,二是右侧用于初始化变量p的变量a前头多了个与(and)的缩写(即&)。

我们把像变量p这样前头有个星号的变量称作指针。这个名字取得其实很形象,指针者,便是指向某个位置的针。它便是我们用于顺着“门牌号”访问变量的工具。

指针自身其实也是变量。再说得直白点,它就是稍微长得特殊点的变量,同样也需要和其它普通变量那样在内存中“租”点空间使用。这“租”下来的空间不干别的,只用于放和它所指示同类型变量的地址。

光有指针、没有地址怎么能行?所以,刚刚的程序便以变量a为例子,将变量a的地址存进了变量p中。在变量a前加一个与缩写(&),其实表达的意思就是“取变量a的地址”。与缩写(&)是一个运算符,它与加减乘除这些符号其实算是一回事(仅仅是计算机方面的,数学中可没有它!),用于对其附近的对象做些什么运算。就拿刚刚的“&”来讲,它的用途便是提取它右侧对象的地址。所以,&a整体的涵义就是“变量a的地址”。

在将变量p安排妥当后,我们便要看一看变量p所能发挥的能力了。执行刚刚的程序段落,我们可以看到类似以下的输出:

1920389632

3

第二行的输出是固定的 3 —— 其实就是变量a的值。而第一行的输出就飘忽不定了,不仅是在不同的机器上,甚至重新运行几遍,得到的数字(很可能)都不一样。

这个看似不断变化的数字到底是怎么来的呢?我们知道,这个数字其实是变量a在内存中的地址,这一点也可以在代码行中的注释看到。而在现代操作系统中,每次程序运行时被分配的内存片段都是不一样的,亦因此每次程序运行时变量a所占用的空间也在不断变动着。

说点题外话,每次分配给正在执行的程序的内存片段都不一样,是出于安全上的考虑——在计算机技术的早期发展阶段,操作系统为程序分配内存的方式仅仅是简单地按顺序分配空闲的内存,而这一漏洞是黑客们得以实现缓冲区溢出攻击的重要前提条件。感兴趣的读者可自行查阅相关资料。

于是第一行输出的奇怪数字便解释得通了,它不过是每次运行时变量a所存放的内存空间位置不断在变化罢了。

虽然第二行输出的是变量a的值,但cout接收到的内容并不是变量a本身,而是带了个星号的变量p,即*p。当*出现在某个变量或表达式(现在可暂且理解为算式的一个别称)之前时,它也就相当于与刚刚的&一个类型的符号,用于对其右侧对象进行解引用。更直白地讲,它用于读取其右侧对象所存储的“门牌号”地址,再根据这个地址找到对应的内存空间“查水表”——将该部分空间的数据以指针声明时所用的类型提取出来。它此时便代表了那个被引用的变量本身,我们可以对其进行读写。就刚刚的例子来看,*p其实就是先顺着变量p给的地址找到变量a所占据的内存空间,然后以变量p声明时所用的int类型将这部分空间的数据提取出来,最后就成了我们再屏幕上看到的数字 3 —— 即变量a所储存的值。


指针的类型是可以跟随你想指向的变量类型的。不过,很多时候我们其实无从得知将需要指向什么类型的变量。

既然是指针,让其指向的变量类型也便是由我们自由定义的。我们能否做出这样一种指针,这种指针可以随心所欲地指向任意类型地变量呢?

答案是肯定的,确实有这样的通用类型指针,允许我们使用它指向任意类型的变量:

int a = 1;
double b = 2;
void *pa = &a, *pb = &b;	// void * 类型指针可接受任意类型的指针

而要想将这种指针变回具体的数据却是有些复杂的操作。我们将在后续学习类型转换时具体了解有关于此的数据类型转换方法。


指针可以指向任意类型的变量,其中当然也包括指针类型:

int a = 1;
int *p = &a;	// 第一层指向 a
int **pp = &p;	// 第二层指向 p;注意标识符 pp 前连续使用了两个星号

是的,它们形成了一个“链条”,即pp指向pp又指向a

而要想通过第二层的指针pp访问到变量a的值,我们需要连续使用两次解引用:

std::cout << **pp << std::endl		// 输出变量 a 的值,即 1
    	  <<  *p  << std::endl		// 输出指针 p 的值,即变量 a 的地址
    	  <<   p  << std::endl;		// 输出指针 pp 的值,即指针 p 的地址

要正确理解**pp的工作机制,我们需要将这个式子从右往左读。从右侧开始,*pp算作一个整体,得到的是指针p;紧接着,第二个*再对刚刚计算得到的p(即原来的*pp)再作一次解引用运算,相当于执行的是*p,得到的便是变量a的值了。

像这样的链式调用并不是全部。在实际场景种,你也许还能见到如同网状或树状的指针引用关系,不过这已经是后话了。


有时候,我们虽然定义了指针,但一时半会还找不到需要它需要指向的变量。或者,当我们我们原本让指针指向的变量不再需要被我们继续使用,亦一时半会儿找不到顶替的新变量给指针指向。

此时,我们可以将其设置为一个空值:

int a = 1;
int *pa = &a;		// 先将指针 pa 指向 a
pa = nullptr;		// 再利用赋值将 pa 设置为一个空值,使其不指向任何对象
int *pn = nullptr;	// 指针 pn 刚定义时便被设置为空值

其中的nullptr便是所谓的空值。它是 C++ 的关键字之一,实质上就是数字 0 而已。它是专为指针定制的空值,相比于 0 来讲,它能够更直观地传达给程序员这样一层意思——我们即将对一个指针作出操作。

另外,我们在实际编程时,一般会初始化尚还不确定指向谁的指针,而恰当的做法便是赋予这样的指针的值为 nullptr


总结一下,我们学习了这样一些内容:

  • 指针(pointer)是储存了具体某个内存地址的一类特殊变量,它可帮助开发者间接地访问内存中的数据,进而实现一些特别的用途。要定义指针类型,仅需在普通类型后加*即可。
  • 取指针运算符(&)用于获取某个变量所在的内存地址。使用时需要将其写在被取指针的变量之前。
  • 解指针运算符(*)用于将某个指针转换回其所指向的数据,开发者可以像使用普通变量一样使用被解的指针。转换回的数据类型将与原本指针所指向的数据类型一致。

指针是自 C 语言以来就有的语言特性。直接使用指针很容易写出有内存溢出风险的程序,即使是有丰富经验的老手也不能保证万无一失,故而请仅在必要时使用指针!在未来的学习中,我们还将深入探索指针,并探寻规避指针缺陷的途径。