是时候深入一些,讲讲几个新的指针主题了!如果你对指针还不熟悉,可以先看看本指南中关于这个问题的第一节。
原来指针也可以做数学运算,尤其是加法和减法。
但这样做到底是什么意思呢?
简而言之,如果你有一个指向某种类型的指针,给这个指针加一,就会移动到内存中紧跟在它后面的下一个该类型的元素。
重要的是要记住,当我们四处移动指针、查看内存中不同的位置时,必须确保在解引用之前,指针始终指向内存中的有效位置。如果我们跑偏到了荒郊野外,还想看看那里有什么,行为就是未定义的,常见的结果就是崩溃。
这和下文数组与指针的等价性有点先有鸡还是先有蛋的味道,不过我们无论如何都要试一试。
首先,让我们取一个数字数组。
int a[5] = {11, 22, 33, 44, 55};然后取得一个指向该数组第一个元素的指针:
int a[5] = {11, 22, 33, 44, 55};
int *p = &a[0]; // 或者写成 "int *p = a;" 也一样然后解引用这个指针,把那里的值打印出来:
printf("%d\n", *p); // 打印 11现在我们用指针算术来打印数组中的下一个元素,也就是索引为 1 的那个:
printf("%d\n", *(p + 1)); // 打印 22!!那里发生了什么?C 知道 p 是指向 int 的指针。所以它知道 sizeof 一个 int83 的结果,也知道要跳过那么多字节,才能到达紧随第一个 int 之后的元素!
事实上,前面的例子可以写成下面这两种等价的形式:
printf("%d\n", *p); // 打印 11
printf("%d\n", *(p + 0)); // 打印 11因为给指针加上 0 得到的还是同一个指针。
让我们想想这里的要点。我们可以用这种方式遍历数组的元素,而不使用数组:
int a[5] = {11, 22, 33, 44, 55};
int *p = &a[0]; // 或者写成 "int *p = a;" 也一样
for (int i = 0; i < 5; i++) {
printf("%d\n", *(p + i)); // 和 p[i] 一样!
}这和用数组写法效果完全相同!哇哦!离数组与指针等价这件事越来越近了!本章后面还会详细讲。
但这里实际发生的是什么?它是怎么工作的?
还记得前面说过,内存就像一个大数组,每个数组索引处存放着一个字节吗?
而内存中的数组索引有几个名字:
所以指针就是内存中某个位置的索引。
随便举个例子,假设数字 3490 存储在地址(“索引”)23,237,489,202 处。如果我们有一个指向那个 3490 的 int 指针,那么这个指针的值就是 23,237,489,202……因为指针就是内存地址。同一件事的不同说法而已。
现在假设我们有另一个数字 4096,紧跟在 3490 之后,存储在地址 23,237,489,210 处(比 3490 的地址高 8,因为在这个例子里每个 int 长 8 字节)。
如果我们给那个指针加上 1,它实际上会向前跳 sizeof(int) 个字节,到达下一个 int。它知道要跳这么远,因为它是一个 int 指针。如果它是个 float 指针,就会向前跳 sizeof(float) 个字节,以到达下一个 float!
所以,给指针加 1 就能看到下一个 int,加 2 就能看到再下一个,依此类推。
上一节我们看到了如何给指针加上一个整数。这一次,让我们修改指针本身。
你可以直接给任何指针加上(或减去)整数值!
我们再做一次那个例子,不过要改几处。首先,我要在我们的数字末尾加一个 999,充当哨兵值。这样我们就能知道数据在哪里结束。
int a[] = {11, 22, 33, 44, 55, 999}; // 在这里加上 999 作为哨兵值
int *p = &a[0]; // p 指向那个 11另外,和之前一样,我们让 p 指向 a 中索引为 0 的元素,也就是 11。
现在——让我们开始递增 p,让它指向数组中的后续元素。我们会一直这样做,直到 p 指向那个 999;也就是说,直到 *p == 999 为止:
while (*p != 999) { // 只要 p 指向的东西不是 999
printf("%d\n", *p); // 打印它
p++; // 把 p 移到指向下一个 int 的位置!
}挺疯狂的吧?
运行起来时,p 首先指向 11。然后我们递增 p,它就指向 22,再递增一次,又指向 33。就这样继续,直到它指向 999,我们就停止。
你也可以从指针中减去一个值,以到达更早的地址,就像之前给它们做加法一样。
但我们也可以把两个指针相减,求出它们之间的差值,例如计算两个 int* 之间有多少个 int。问题在于,这只在同一个数组内84才有效——如果指针指向别的任何东西,就会得到未定义行为。
还记得在 C 中字符串就是 char* 吗?我们来看看能不能利用这一点,写出另一个版本的 strlen(),用指针相减来计算字符串的长度。
思路是:如果我们有一个指向字符串开头的指针,就可以通过向前扫描查找 NUL 字符,找到一个指向字符串末尾的指针。
如果我们有一个指向字符串开头的指针,又算出了指向字符串末尾的指针,那么只要把这两个指针相减,就能得到长度!
#include <stdio.h>
int my_strlen(char *s)
{
// 从字符串的开头开始扫描
char *p = s;
// 一直扫描到 NUL 字符为止
while (*p != '\0')
p++;
// 返回指针之差
return p - s;
}
int main(void)
{
printf("%d\n", my_strlen("Hello, world!")); // 打印 "13"
}记住,只有在两个指针都指向同一个数组时,才能对它们使用指针相减!
我们终于可以谈谈这个了!我们已经见过很多把数组写法混用进来的例子,现在让我们给出数组与指针等价性的基本公式:
a[b] == *(a + b)好好研究一下!它们是等价的,可以互换使用!
我稍微简化了一些,因为在上面的例子里,a 和 b 都可以是表达式;如果表达式很复杂,我们可能还想多加几个括号来强制规定运算顺序。
规范一如既往地明确,它声明(见 C11 §6.5.2.1¶2):
E1[E2]等同于(*((E1)+(E2)))
但这稍微难吃透一些。只要确保在表达式复杂时加上括号,让你所有的运算都按正确的顺序进行。
这意味着,对于任何数组或指针(假设它指向数组的某个元素),我们都可以自行决定使用数组写法还是指针写法。
让我们对一个数组和一个指针分别使用数组写法和指针写法:
#include <stdio.h>
int main(void)
{
int a[] = {11, 22, 33, 44, 55};
int *p = a; // p 指向 a 的第一个元素,即 11
// 用多种方式打印数组的所有元素:
for (int i = 0; i < 5; i++)
printf("%d\n", a[i]); // 对 a 使用数组写法
for (int i = 0; i < 5; i++)
printf("%d\n", p[i]); // 对 p 使用数组写法
for (int i = 0; i < 5; i++)
printf("%d\n", *(a + i)); // 对 a 使用指针写法
for (int i = 0; i < 5; i++)
printf("%d\n", *(p + i)); // 对 p 使用指针写法
for (int i = 0; i < 5; i++)
printf("%d\n", *(p++)); // 移动指针 p
//printf("%d\n", *(a++)); // 移动数组变量 a——错误!
}所以你可以看到,一般来说,如果你有一个数组变量,你可以用指针写法或数组写法来访问元素。指针变量也一样。
最大的区别在于,你可以修改指针,让它指向不同的地址,但对数组变量不能这样做。 换句话说,你根本不能给数组变量赋值——只能给该数组的各个元素赋值。
如果你真的想把一个数组复制到另一个数组,就必须使用像 memcpy() 这样的函数(或者循环)来实现。
你肯定最常在这里遇到这个概念。
如果你有一个接受指针实参的函数,例如:
int my_strlen(char *s)这意味着你可以把数组或指针传给这个函数,它都能正常工作!
char s[] = "Antelopes";
char *t = "Wombats";
printf("%d\n", my_strlen(s)); // 可以!
printf("%d\n", my_strlen(t)); // 也可以!这也是为什么下面两个函数签名是等价的:
int my_strlen(char *s) // 可以!
int my_strlen(char s[]) // 也可以!void 指针你已经见过 void 关键字用在函数上,表示没有形参或没有返回值,但这里是完全另一回事,与那种用法无关。
void* 绝对是一个指向某个存在的东西的指针。但其中的 void 部分表示我们不知道这个东西的类型。有时候,信不信由你,这真的很有用。它让我们能写出稍微更不依赖类型的代码,这在 C 这样的类型化语言里是一种很好的灵活性。
这基本上有两种用例——我们来看看,看能不能把它讲得明白一些。
有一种情况是,函数要逐字节地处理某个东西。例如,memcpy() 把内存中的字节从一个指针复制到另一个指针,而那些指针可以指向任何类型。memcpy() 利用了一个事实:如果你通过 char* 来遍历,那么无论对象是什么类型,你都是在遍历该对象的字节。更多内容见多字节值小节。
另一种情况是:某个函数调用你传给它的函数(回调),并把数据传给你。你知道数据的类型,但调用你的那个函数不知道。所以它传给你 void*——因为它不知道类型——你再把这些转换成需要的类型。内置的 qsort()85 和 bsearch()86 就使用了这种技术。
让我们看一个例子,即内置的 memcpy() 函数:
void *memcpy(void *s1, void *s2, size_t n);这个函数从地址 s2 开始,复制 n 个字节的内存,放到从地址 s1 开始的内存中。
但你看!s1 和 s2 都是 void*!为什么?这意味着什么?我们多跑几个例子看看。
例如,我们可以用 memcpy() 复制一个字符串(不过对字符串来说 strcpy() 更合适):
#include <stdio.h>
#include <string.h>
int main(void)
{
char s[] = "Goats!";
char t[100];
memcpy(t, s, 7); // 复制 7 个字节——包括 NUL 终止符!
printf("%s\n", t); // 结果就是 "Goats!"
}或者我们可以复制一些 int:
#include <stdio.h>
#include <string.h>
int main(void)
{
int a[] = {11, 22, 33};
int b[3];
memcpy(b, a, 3 * sizeof(int)); // 复制 3 个 int 的数据
printf("%d\n", b[1]); // 22
}那个例子有点狂野——你看到我们用 memcpy() 做了什么吗?我们把数据从 a 复制到了 b,但必须指定要复制多少字节,而一个 int 不止一个字节。
那么——一个 int 占多少字节?答案是:取决于系统。不过我们可以用 sizeof 运算符得知任何类型占多少字节。
答案就在这里:一个 int 需要 sizeof(int) 个字节的内存来存储。
如果我们的数组里有 3 个 int,就像那个例子里一样,那么这 3 个元素占用的整个空间就是 3 * sizeof(int)。
(在之前的字符串例子里,从技术上来说,复制 7 * sizeof(char) 个字节会更准确。但按照定义,char 总是只占一个字节,所以那也就退化成 7 * 1 了。)
我们甚至可以用 memcpy() 复制一个 float 或一个 struct!(不过这样做有点滥用——那种情况我们直接使用 = 就好):
struct antelope my_antelope;
struct antelope my_clone_antelope;
// ...
memcpy(&my_clone_antelope, &my_antelope, sizeof my_antelope);看看 memcpy() 有多通用!只要你有一个指向源的指针、一个指向目标的指针,并且知道要复制的字节数,你就能复制任何类型的数据。
想象一下如果没有 void*。我们就得为每种类型各写一个专用的 memcpy() 函数:
memcpy_int(int *a, int *b, int count);
memcpy_float(float *a, float *b, int count);
memcpy_double(double *a, double *b, int count);
memcpy_char(char *a, char *b, int count);
memcpy_unsigned_char(unsigned char *a, unsigned char *b, int count);
// 等等……真恶心!更好的做法是直接用 void*,让一个函数包办所有情况。
这就是 void* 的威力。你可以写出一个不关心变量类型的函数,却仍然能拿它做各种操作。
但能力越大,责任越大。在这个例子里也许没那么大,但确实存在一些限制。
void* 做指针运算。void*。void* 使用箭头运算符,因为那也是一种解引用。void* 使用数组写法,因为那同样也是一种解引用87。仔细想想,这些规则是合理的。上面这些操作全都依赖于知道所指向数据的类型的 sizeof,而对于 void*,我们并不知道所指向数据的大小——它可以是任何东西!
但等等——如果连 void* 都不能解引用,那它到底能给你带来什么好处?
就像 memcpy() 那样,它能帮你写出可以处理多种数据类型的通用函数。但秘诀在于,在底层,你要先把 void* 转换成另一种类型,然后再使用它!
而转换很简单:只要把它赋值给一个所需类型的变量就行88。
char a = 'X'; // 单个 char
void *p = &a; // p 指向那个 'X'
char *q = p; // q 也指向那个 'X'
printf("%c\n", *p); // 错误——不能解引用 void*!
printf("%c\n", *q); // 打印 "X" 我们自己写一个 memcpy() 来试试。我们可以按字节(char)复制,而字节数是作为参数传进来的,所以我们是知道的。
void *my_memcpy(void *dest, void *src, int byte_count)
{
// 把 void* 转换成 char*
char *s = src, *d = dest;
// 现在有了 char*,我们就能解引用并复制它们了
while (byte_count--) {
*d++ = *s++;
}
// 这些函数大多会返回目标地址,以备
// 调用方不时之需。
return dest;
}就在开头,我们把 void* 复制进 char*,这样就能把它们当作 char* 来用了。就这么简单。
然后是在 while 循环里的一点花样:我们不断递减 byte_count,直到它变为假(0)。记住,使用后置自减时,先算出表达式的值(供 while 使用),然后才递减变量。
复制过程中也有一点花样:我们用 *d = *s 来复制这个字节,但用的是后置自增,这样在赋值完成之后,d 和 s 都会移动到下一个字节。
最后,大多数内存函数和字符串函数都会返回一个指向目标的指针副本,以防调用方想用它。
做到这里,我只想快速指出:我们可以用这个技巧遍历 C 中任何对象的字节,float 也好,struct 也好,什么都可以!
我们再用内置的 qsort() 跑一个真实世界的例子;多亏了 void* 的魔力,它可以对任何东西排序。
(在下面的示例中,你可以忽略 const 这个词,我们还没讲到它。)
#include <stdio.h>
#include <stdlib.h>
// 我们要排序的结构体类型
struct animal {
char *name;
int leg_count;
};
// 这是 qsort() 调用的比较函数,帮它决定
// 到底按什么排序。我们用它把一个 struct
// animal 数组按 leg_count 排序。
int compar(const void *elem1, const void *elem2)
{
// 我们知道要排序的是 struct animal,所以把两个
// 参数都做成指向 struct animal 的指针
const struct animal *animal1 = elem1;
const struct animal *animal2 = elem2;
// 根据我们想按什么排序,返回 <0、=0 或 >0。
// 我们按 leg_count 升序排序,所以返回两者
// leg_count 之差
if (animal1->leg_count > animal2->leg_count)
return 1;
if (animal1->leg_count < animal2->leg_count)
return -1;
return 0;
}
int main(void)
{
// 我们来构造一个含 4 个 struct animal 的数组,它们各有不同的
// 特征。这个数组按 leg_count 来说是乱序的,但
// 我们马上就会给它排序。
struct animal a[4] = {
{.name="Dog", .leg_count=4},
{.name="Monkey", .leg_count=2},
{.name="Antelope", .leg_count=4},
{.name="Snake", .leg_count=0}
};
// 调用 qsort() 给数组排序。必须确切地告诉 qsort()
// 按什么来排序这些数据,而这件事我们在 compar()
// 函数里完成。
//
// 这次调用是在说:对数组 a 调用 qsort,它有 4 个元素,
// 每个元素的大小是 sizeof(struct animal) 字节,而这就是
// 用来比较任意两个元素的函数。
qsort(a, 4, sizeof(struct animal), compar);
// 把它们全部打印出来
for (int i = 0; i < 4; i++) {
printf("%d: %s\n", a[i].leg_count, a[i].name);
}
}只要你给 qsort() 一个函数,能比较待排序数组中你有的任意两个元素,它就能对任何东西排序。而且它这么做时,完全不需要在代码里把元素的类型硬编码进去。qsort() 只是根据你传入的 compar() 函数的结果,重新排列字节块。