要想读懂 Redis 里面关键的代码实现,还是需要先有一点点 C 语言基础的,因此,本文我们就来做一个简单的 C 语言入门讲解,主要是介绍一下 C 字符串结构体指针以及数组这四方面的内容。这四方面内容会和 Java 对比着说,尽可能让熟悉 Java 的小伙伴做到零门槛。

当然,如果你非常熟悉 C 语言的话,可以直接跳过这一篇文章。

C 字符串

首先来看 C 语言中的字符串。在 Java 里面,我们是如何创建一个字符串呢?直接 new 一个 String 对象,就行了。比如,下面就定义一个 s 字符串变量:

1
String s = new String("Hello World");

new String() 是在堆里面分配一个 String 对象,s 这个变量存的就是这个 String 对象的地址,就和下面这张图一样:

image.png

如果你看过 Java String 的源码,会知道 String 底层是拿 char 数组(字符数组)实现的。我这里用的还是 JDK 8 的源码,JDK 9 的话,就已经变成 byte[] 数组(字节数组)了。

image.png

C 语言里面的字符串也是这么实现的:C 语言里面没有定义 String 这种类,直接就是拿 char[] 数组来表示字符串的

演示一下,先在 CLoin 里面创建一个 demo_c 的 C 语言项目,接着在 main.c 里面创建一个字符串,然后输出一下。这个 printf() 和 Java 里面的 System.out.println() 一样,就是在控制台中输出指定的内容。

1
2
char c[] = "Hello World!";
printf("%s", c);

除了我们能看到的 “Hello World!” 这些字符之外,C 语言的编译器会帮我们加一个 \0 的结束符,表示这个字符串结尾,如下图所示:

image.png

在 Java 里面,我们要获取一个字符串长度的时候,直接调它的 length() 方法就可以了。但是,在 C 语言里面,数组是没有 length 这种方法的。然后,我们就需要遍历整个 char 数组,从第一个字符开始遍历,一直遍历到 \0 才算这个字符串结束,是不是有点麻烦?但是木有办法。

还有一个地方需要说一下,Java 里面,一个 char 占两个字节,也就是两个 byte;在 C 语言里面,一个 char 占一个字节,而且 C 语言里面是没有 byte 的概念,这个注意一下就好了。

结构体

接着我们再来看一下 C 语言中的结构体,也就是 struct 这个关键字。

C 语言里面的结构体和 Java 里面的基本上可以对标的,但是有一个不太一样的点,那就是 C 语言的结构体里面不能定义方法,而 Java 的类里面是可以定义方法的,这是两者最主要的区别。

例如,有一个 Student 类,在 Java 里面的话,我们可以这么写,里面有 name 和 age 两个字段,相应的实现代码如下:

1
2
3
4
public class Student {
private String name;
private int age;
}

对标到 C 语言里面,我们定义一个 Student 类型的话,需要定义一个 student 结构体,首字母一般是小写,里面放一个 char 数组来存学生的名字,还有一个 int 类型的 age 来存学生年龄,具体定义如下:

1
2
3
4
struct student {
char name[13];
int age;
};

然后在 C 语言里面使用这个 student 结构体的时候,需要先创建一个 student 实例,如下面这段代码所示。给 name 和 age 赋值,然后打印出来。这里的 strcpy() 函数的功能呢,就是把 Kouzhao 这个字符串的内容,拷贝到 name 这个数组里面去。C 语言的字符串赋值没法像 Java 那样,直接一个等号完事。

1
2
3
4
5
6
7
int main() {
struct student s; // 创建一个s实例
strcpy(s.name, "Kouzhao"); // 给name赋值
s.age = 25; // 给age赋值
printf("%s %d", s.name, s.age); // 打印s里面的name和age字段
return 0;
}

在后面介绍 Redis 源码的时候,会看到使用 typedef struct 来定义结构体的方式,所以这里我们也来演示一下,具体代码如下所示:

1
2
3
4
5
6
7
8
9
10
11
// 第一种定义方式
typedef struct student{ // struct前面添加typedef关键字
char name[13];
int age;
} student; // 这里添加student

// 第二种方式
typedef struct { // struct关键字后的student可以省略
char name[13];
int age;
} student;

上面的两种 typedef struct 定义方式与前文介绍的 struct 定义方式是等价的,唯一的区别就是,使用 typedef struct 定义方式之后,在创建 student 实例的时候,不用再加 struct 关键字了,相应的 main 方法代码如下:

1
2
3
4
5
6
7
int main() {
student s; // 创建一个s实例,前面不用再加struct关键字了
strcpy(s.name, "Kouzhao"); // 给name赋值
s.age = 25; // 给age赋值
printf("%s %d", s.name, s.age); // 打印s里面的name和age字段
return 0;
}

指针

最后一个要聊的话题是 C 语言中的指针。这里还是拿 Student 这个类来举例子,在 Java 里面,如果要创建一个对象的话,直接用 new Student 就可以了:

1
Studnet s = new Student();

这个 Student 对象在内存里面的状态是这样的:在 Java 的栈上面有一个 s 的变量,然后这个变量里面存的内容,就是 JVM 堆里 Student 对象的地址;在程序代码使用这个 s 变量的时候,实际上都是操作 JVM 堆里面的 Student 对象。就如下图所示:

image.png

C 语言里面的指针,和 Java 的变量其实是非常像的。例如,我们下面就创建一个 student 指针,指向一个 student 实例对象,具体代码如下。这里还是先创建一个 student 实例,然后定义一个 student 指针 p,等号后面的 & 符号,表示的是取首地址的意思,也就是说,p 指针里面存的是 s 这个实例的内存首地址。也就是我们常说的,p 指针指向了 s 实例。之后要在程序中使用 s 这个 student 实例中的 name 字段时,我们就可以用 “p->name” 这种写法拿到 name 字段的值,使用其他字段的方式也是类似的。

1
2
3
4
5
6
7
8
int main() {
student s; // 创建一个student实例
strcpy(s.name, "Kouzhao"); // name字段赋值
s.age = 25; // age字段赋值
student *p = &s; // 定义一个student指针p,指向s这个实例
printf("%s %d", p->name, p->age); // 通过指针使用s这个实例
return 0;
}

p 指针以及 s 实例在内存中的状态,就是下图展示的样子:p 这个指针变量里面存的是一个内存地址,这个内存地址实际上就是 s 实例的内存首地址

image.png

说完指针的定义之后,再来看另一个概念——解引用。解引用的意思就是获取这个指针指向的这个空间里面存的值。例如下面这段代码,定义了一个 int 的指针 p,指向了 i 这个 int 的值,i 里面存的是 100,那 *p 这个表达式就是 100 这个值。

1
2
3
4
5
6
int main() {
int i =100; // 定义变量i,并赋值
int* p = &i; // p里面存的就是i这个变量的地址
printf("%d %d", *p, i); // *p中的*就是解引用,获取i的值
return 0;
}

p 指针以及 i 变量在内存中的状态,如下图所示:

image.png

既然 C 语言中的指针可以存储一个内存地址,那我们是不是可以把一个指针的地址放到另一个指针里面呢?有的小伙伴可能会惊呼:“你搁这套娃呢?”没错,就有点类似套娃的意思。这种指向指针的指针,就是我们所说的二级指针。下图就展示了一个二级指针的内存状态:

image.png

在上图中,首先在堆上创建了一个 student 实例,它的起始地址是 0x123456,然后创建了一个 s1 指针,指向了这个 student 实例,此时 s1 指针的里面存的是 student 的首地址,就是上图中的 0x123456 这个地址。然后,我们又创建了一个 s2 指针,并且把 s2 指向 s1,那 s2 里面存的就是 s1 的地址,也就是上图中的 0x654321。那定义 s2 的时候,就需要加两个 * 号,表示 s2 是一个指向指针的指针。

要通过 s2 拿到 student 实例的话,我们就要解两次引用:第一次解引用,拿到的是 s1 指针;第二次解引用,才能真正拿到 student 实例。

数组

我们前面介绍 C 字符串的时候提到,C 字符串实际上是一个 char 类型的数组,你也可以认为 C 语言里面的数组其实就是一个指针。当然,这只是方便你理解,指针和数组本身还是有本质区别的,只是在实际使用的时候,感觉不到那么大的区别。

指针和数组都是指向数组第一个元素的地址,那我们也可以用 char 指针来创建字符串,就像下面这张图,s 和 c 存的都是char 数组的第一个元素的地址。

image.png

相应的示例代码如下所示:

1
2
3
4
5
6
7
8
int main() {
char* s = "Hello World!"; // 定义一个char指针
char c[]= "Hello World!"; // 定义一个char数组
printf("%s %s", s, c);
s = c; // 将s指针指向数组的第一个元素的首地址
printf("%s %s", s, c);
return 0;
}

在本文的最后,我们也来简单看一下指针数组 。指针数组本身是个数组,数组中的每个元素都是一个指针。如下图所示,这里有一个指针数组 p,其中每一个元素都是元素 int 指针,每个元素都可以执行一个 int 变量。

image.png

除了 int *p[] 这种一级指针数组之外,我们还可以定义二级指针的数组,例如 int **q[]。这里告诉你一个分析多级指针以及指针数组定义的技巧:先找到变量,然后用英语的方式读右边,再读左边

什么意思呢?我们以 int **q[] 这个定义为例,如下图所示,我们一眼看去,就知道 q 是我们的变量,q 的右边是 “[]”,我们记录“an array of”,再往右没有其他符号了;然后从 q 这个变量名向左读,第一个 “*” 记录 “a pointer of”,第二个 “*” 记录 “a pointer of”;继续向左是 int,那记录 “int”。

image.png

将整个定义的分析结果连接起来就是:q is an array of a pointer of a pointer of int。翻译过来,就是 q 是一个数组,数组里面存的什么呢?存的指针(第一个 a pointer)。指针里面存的什么呢?还是存的指针(第二个 a pointer),很明显,指针的指针就是二级指针了。那二级指针里面存的什么呢?int 变量(最后的 int)。

你也可以自己找一些比较复杂的变量的定义,用这个方法读一下,例如:int (*p)[5],这里有个小提示括号的优先级是最高的,先在括号里面完成左右读取,然后再在括号外进行左右读取

总结

本文我们简单介绍了一下阅读 Redis 源码需要的 C 语言基础,主要包括 C 字符串、结构体、指针以及数组四部分内容。

其中,我们将 C 字符串与 Java 中字符串的底层实现进行了对比说明;将 C 语言中的结构体与 Java 中的类进行了对比介绍,如果你有 Java 基础或者其他语言的基础,相信写写 Demo,就可以快速上手了。

最后,我们讲解了 C 语言中的一级指针、二级指针以及指针与数组的组合类型,还提供了一种分析复杂类型定义的技巧,并带你结合示例做了个简单的练习。

在后续分析 Redis 核心代码实现的时候,这些都是非常重要的基础知识点,你一定要彻底掌握呀。