<audio id="audio" title="07 | 指针系列（一）：记住，指针变量也是变量" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/70/9f/70a156ff4af3f7418b5ff24bab9be59f.mp3"></audio>

你好，我是胡光，上节课中，我们对两个概念做了区分，就是“值”和“变量”。你也看到了，当我们将 printf 函数中的第一个参数，抽象成变量以后，整个程序的功能会变得异常的灵活。

今天我们将要学习的 “指针”呢，也是一种变量，这是一种存储地址的变量。这种变量，可谓是所有变量的终极形态，掌握了指针，也就掌握了程序设计中“变量”的全部知识。今天，我们只会围绕着一句话进行学习，一定要记住，那就是 “指针变量也是变量”。

## 任务介绍

这次的任务，是需要我们结合两次学习（本节内容和下一节内容）才能完成，到底是什么呢？你不要有畏惧心理，其实这个任务很简单，假设有如下结构体数组，请看如下代码：

```
struct Data {
    int x, y;
} a[2];

```

请用尽可能多的形式，替换下面代码中 &amp;a[1].x 的部分，使得代码效果不变：

```
struct Data *p = a;
printf(&quot;%p&quot;, &amp;a[1].x);

```

你会看到，如上代码中，其实就是输出 a[1].x 的地址值。

到了这里，你可能对结构体还不熟悉，并且，你可能对于这个任务应该如何完成还是一头雾水，没关系，暂时忘了这个任务，我们先来讲讲可以解决任务的一些基础知识，再回来看这个任务。

进行下面的学习之前，我还是要强调一下那句话，这句话是我们这两次学习的重点，也是帮助你学习指针的利器，叫做“**指针变量也是变量**”。

## 必知必会，查缺补漏

#### 1. 初识：结构体

为了完成今天的任务，你先要学习一些关于结构体的知识。先来想一个这样的问题：想要在程序中输入 n 个整数的话，我们知道可以用整型数组来进行存储，可是如果想要是输入 n 个点的坐标信息呢？用什么类型的数组来存储呢？是使用坐标类型的数组来存储么？没错！

你可能会疑问了，坐标类型怎么表示呢？其实这个坐标类型，可不像整型一样，整型是程序语言中给我准备好的现成的类型，而这个所谓的坐标类型，虽然程序语言中没有，但我们**可以通过C语言里面的工具来描述这种类型的特点，这个可以用来描述和定义新类型的工具，就叫做：结构体**。

下面我们看看如何用结构体定义一个新的数据类型，名字就叫做 point 类型吧：

```
struct point {
    // 描述这个类型的组成部分
};

```

上面在这行代码中，我们定义了一个新类型，是 struct point，也就是结构体点类。我这里强调一下，这个新类型不是 point，在 C 语言中，这个新类型是 struct point。struct 是关键字，代表结构体，point 是为了与其它结构体定义的类型相区分，后面的大括号内部是用来描述这个新类型的组成部分的。

有了这个类型以后，你就可以写如下的代码，来定义点类型的变量了：

```
struct point p1, p2;

```

正如你看到的，我们定义了两个点类型的变量，p1 和 p2，可由于上面我们没有具体描述点类型的组成部分，所以这个 p1 和 p2 变量只是名义上的点类型变量，却没有什么实质性的作用。

什么叫做“具体描述点类型的组成部分”呢？来让我们想想，我们如何表示一个坐标点，在数学中，一般情况是用一个二元组 (x, y) 表示一个点坐标。假设，在我们的问题场景中，点坐标都是整型的话，那么程序中的点类，就应该是由一对基础的整型变量组成的，具体写成代码如下所示：

```
struct point {
    int x, y; 
};

```

正如你所看到的，我们在原本的结构体点类的大括号中，加入了两个整型字段，具体的语义含义是，一个点类型数据其实可以具体的表示成为两个整型数据。

在这个过程中，有没有一种盖房子的感觉？先有地基，再盖一楼，然后是二楼。也就是在程序中，先有基础数据类型，然后是基于这些基础数据类型，定义出新的数据类型。

你也可以想象，我们其实可以用我们定义出来的新类型，去定义另一个更新的类型。而所谓 C 语言中的基础数据类型，就是程序语言给我们准备好了的地基，而所谓程序的功能模块，就是别人盖好的房子，我们直接拿过来使用。就像之前我们了解的 printf 函数和 scanf 函数一样，都是C 语言给我们准备好了的基础功能模块。

有了基础功能，我们可以开发更高级的功能，有了基础类型呢，我们也可以开发更复杂的类型。这个过程，将来你可以自己逐渐的加深体会，在这里，我就不过多的展开来说了。

描述了结构体点类型的具体组成部分以后，之前的 p1 和 p2 变量就具备了实际的功能了，下面，我们让 p1 代表点(2, 3)，让 p2 代表点 (7, 9)，代码如下：

```
p1.x = 2;
p1.y = 3;
p2.x = 7;
p2.y = 9;

```

可以看到，我们可以给 p1 和 p2 变量中的 x，y 字段分别赋值。这里出现了一个新的运算符，就是点“.”运算符，这个也叫做“直接引用”运算符，p1.x，意思是 p1 变量里面的 x 字段。后面讲解完指针内容以后，我们还会介绍间接引用运算符“-&gt;”，由一个减号和一个大于号组成，这个我们后面再说。

#### 2. 结构体变量的大小

就像我们之前所说的，变量是存储值的地方，只要是变量，就一定占用若干存储单元，也就是占用若干字节的空间。结构体变量既然也是变量的话，那么一个结构体变量又占用多少个字节呢？

以我们刚才设置的结构体变量为例，这个包含两个整型字段的结构体类型变量，占多少个字节的存储空间呢？你可能会想，那还不简单，最起码要拥有足够放下两个32位整型数据的存储空间吧，因为其中包括了两个整型字段，所以一个 struct point 类型变量最起码应该占 8 个字节。如何验证你的想法呢？还记得之前讲过的 sizeof 方法吧？

```
struct point p;
sizeof(p);
sizeof(struct point);

```

这两种使用 sizeof 方法的代码均能正确的告诉你一个 struct point 类型的变量占用的存储空间大小。至此，你可能感觉自己已经掌握了计算结构体变量大小的诀窍。

先不要高兴太早，看下面这两个结构体的情况：<br>
<img src="https://static001.geekbang.org/resource/image/86/21/86eb572fca7bff9391303e90ddd1fa21.jpg" alt="" title="图1：结构体占用空间对比">

可以看到， Data1 和 Data2 两个结构体，都是由两个字符型字段和一个整型字段组成的。但这个对比中，存在两个你无法忽视的问题：

- Data1 结构体，只包含一个整型和两个字符型字段，所占用的空间大小应该是 4+1+1=6 个字节啊，怎么变成了 8 个字节？
- Data2 结构体，和 Data1 结构体包含字段种类都是一样的，那既然 Data1 是 8 个字节，为什么 Data2 是 12 个字节呢？

下面我们就来对这两个问题，一一作答，学会了这两个问题，你才是真正抓住了计算结构体变量大小的诀窍。

先来看第一个问题，为什么 Data1 类型的变量占用的是 8 个字节，而不是 6 个字节呢？这里就要说到结构体变量申请存储空间的规则了。正如你知道的，像整型这种 C 语言原有的内建类型，都是占用若干个字节，整型变量的存储，就是以字节为单位的。而今天我们学到的结构体变量，需要占用若干个存储单元，结构体变量的存储，就是以存储单元为单位的，那么一个存储单元占用多少个字节呢？

记住，下面这个就是重点了：**对于某个结构体类型而言，其存储单元大小，等于它当中占用空间最大的基础类型所占用的字节数量。**

说白了，对于 Data1 结构体类型来说，整型是其当中占用空间最大的基础类型，它的一个存储单元的大小，就是 4 个字节，等于它当中整型字段所占用的字节数量。也就是说，Data1 这个结构体类型，要不就占用 1 个存储单元，即 4 个字节的空间，要不然就占用 2 个存储单元，即8个字节的存储空间，不会出现 6 个字节的情况。

那么究竟占多少呢？按照最小存得下原则，Data1 最少应该占用 2 个存储单元，才能放下一个整型和两个字符型，这就是为什么 Data1 类型占用 8 个字节的原因。

你会问了，按照这个解释，那 Data2 为什么占用 12 个字节呢？Data2 中不也是一个整型和两个字符型么？先别着急，这就进入我要讲的第二个重点了：**结构体的字段在内存中存储的顺序，是按照结构体定义时的顺序排布的，而且当本存储单元不够安放的时候，就从下个存储单元的头部开始安放。**

这是什么意思呢？下面是我给你准备的一张 Data1 和 Data2 两个结构体类型的内存占用情况图：<br>
<img src="https://static001.geekbang.org/resource/image/30/b9/306b94ab84e3c096f90716d7153430b9.jpg" alt="" title="图2：结构体内存结构示意图">

你可以看到，在 Data1 中，首先是 int 类型的 a 变量，占用了第一个存储单元，然后 b 和 c 占用了第二个存储单元的前两个字节。

再看 Data2，由于 Data2 不同于 Data1 的字段顺序，b 占用了第一个存储单元的第一个字节，剩余的 3 个字节不够存放一个 int 类型变量的，所以按照上面我们讲的规则“当本存储单元不够安放的时候，就从下个存储单元的头部开始安放”， a 变量就单独占用了第二个存储单元，c 自己占用第三个存储单元的第一个字节。

所以，虽然在数据表示上，Data1 和 Data2 是等价的，可 Data2 却占用了更多的存储空间，相比于 Data1 造成了 50% 的空间浪费。由此可见，**在设计结构体的时候，不仅要设计新的结构体类型中所包含的数据字段，还需要关注各个字段之间的顺序排布**。

#### 3.指针变量也是变量

看完了结构体相关的知识以后，下面来让我们进入一个被很多初学者称为 C 语言中最难理解的的部分，指针相关知识的学习。面对这部分内容，我只希望你记住一句话：指针变量也是变量。

想想之前我们学习的“变量”和“值”的概念，我们说，什么类型的值，就用什么类型的变量进行存储，整型变量，是存储整型值的东西，浮点型变量是存储浮点型的东西。

当你听到“指针变量也是变量”这句话的时候，我希望你能提出如下问题：既然指针变量也是变量，那指针变量是存储什么类型的值的呢？还记得我们之前讲的地址的概念吧，你会发现，所谓变量的地址，就像整数和字符串一样，其实是一个明确的值啊。

那对于地址，我们使用什么变量来进行存储呢？没错，**指针是变量，指针是一种用来存储地址的变量！**在这里我再强调一遍“指针变量也是变量”，这意味着，你之前对于“变量”这个概念的认识，都可以放到指针变量的理解上。

让我们先来看一下如何定义一个指针变量：

```
int a = 123, *p = &amp;a;
printf(&quot;%d %p %d\n&quot;, a, p, *p);

```

在上面这段代码中，a 是一个整型变量，p 变量前面多了一个*，这个*就是用来说明 p 是一个指针变量，是一个存储整型变量地址的指针变量，在代码中，你也可以看到，我们将 a 的地址赋值给了 p 变量。

代码的第2行，共输出三项信息：第一项输出 a 中存储的整型值（第一个%d对应的是a)，第二项是输出 p 中存储的地址值（%p对应的是p)，第三项输出的是 *p 的值（第二个%d对应的是 *p)，p 里面存储的是地址，*p 代表了 p 所指向的存储区内部的值。

为了更清楚的解释 *p，给你准备了下面的图，以便你理解 a 和 p 的关系：<br>
<img src="https://static001.geekbang.org/resource/image/ac/ff/ac2972988abc713c2db1960062f3a4ff.jpg" alt="" title="图3：a 变量与 p 变量">

从图中你可以看到，p 变量中存储的就是 a 变量的首地址，也就是说，我们可以通过 p 变量中所存储的信息，按图索骥，就能找到 a 变量所代表的存储区，进而操作那片存储区中的内容。 p 变量对于 a 变量的作用，是不是很像一个指路牌呢？指针的名称，也就由此而来。

我们再来看，如果 p 本身代表了 a 变量的地址，那么如何取到这个地址所对应的存储空间中的内容呢？这个就是 * 运算符，放到变量名前面，我们叫做“取值”运算符，对于 *p 的理解就是取值 p 所指向存储区的内容，也就是原有 a 变量中所存储的值。

一种更简单的理解方法是，在写程序的时候 *p 就是等价于 a，也就是说当你写如下代码的时候：

```
*p = 45; 

```

实际上等价于写了一行代码 a = 45。也就是说，实际上是把 a 变量中存储的值给改变了。

## 课程小结

在最后的这个例子中呢，聪明的你有没有注意到这样一个问题：a 变量实际上有 4 个地址，p 中存储的只不过是 a 变量的首地址，也就是说，p 中所存储的地址，只指向了一个字节的存储空间，那为什么当我们使用 *p 的时候，程序可以正确的对应到 4 个字节中的数据内容呢？

上面这个问题，就要涉及到指针的类型的作用了，下一篇文章我们再详细聊一下这个事情。今天要说有什么重点需要你记住的，那就是希望你记住如下两点：

1. 结构体是用来创造新类型的利器，而结构体类型所占存储空间大小，与其内部字段的组成和各个字段的顺序排布均有关。
1. 指针变量也是变量，这是一种存储地址信息的变量。

好了，我是胡光，我们下次见。
