基本知识

为了更好地理解 PHP 7 中 zval 的实现,本节先介绍一下相关的基本知识,包括什么是数据类型,PHP 7 中都有哪些变量类型,另外会介绍与变量存储相关的堆和栈的基本知识。

数据类型

数据类型是一种为了对数据(或数据值)进行分类而由用户定义的类型,在数据结构中的定义是一个值集合以及定义在这个值集合上的一组操作。

基本的数据类型有 intdoublelongchar 及各种指针类型。在 C 语言中,使用变量时,提前定义变量并指定变量类型,而在 PHP 中变量不需要指定类型,3.2 节将会讲解弱类型的实现。

C 语言的数据类型在不同的操作系统中长度不同,举个例子,x86-64 系统架构下,一个 char 类型的数据占 1 个字节,一个 int 类型的数据占 4 个字节,一个指针类型的数据占 8 个字节,一个 long 类型的数据占 8 个字节,可以在 gdb 下使用 sizeof 打印验证:

(gdb) p sizeof(char)
$1 = 1
(gdb) p sizeof(int)
$2 = 4
(gdb) p sizeof(long)
$3 = 8
(gdb) p sizeof(char*)
$4 = 8
(gdb) p sizeof(void*)
$5 = 8
  • 32 位指针最多能表示 232 = 4,294,967,296(4 GB)的内存地址空间。

  • 64 位指针最多能表示约 264= 18,446,744,073,709,551,616(16 EB)(艾字节)的内存地址空间。

类型所占空间大小与系统架构有关,可以使用 show architecture 来查看:

(gdb) show architecture
The target architecture is set automatically (currently i386:x86-64)

也可以使用 set architecture i386:x86-64 来设置:

(gdb) set architecture i386:x86-64
The target architecture is assumed to be i386:x86-64

结构体与联合体

结构体是使用 struct 定义的结构,比如定义一个如下的结构体:

struct test{
    char a; //1
    int b;  //4
    long c; //8
    void* d; //8
    int e;  //4
    char* f; //8
};

在代码中标记了每个成员的大小,那么结构体的总大小是 1+4+8+8+4+8=33 吗?

下面来打印一下:

(gdb) p sizeof(struct test)
$1 = 40

为什么是 40 而不是 33 呢?这里面涉及结构体对齐问题,在笔者的机器上,结构体是按照 8 字节对齐的,那么可以画出对齐后占用的字节数,如图3-1所示。

image 2024 06 07 00 23 26 536
Figure 1. 图3-1 结构体

从图3-1中可看出,虽然 char a 只占了 1 字节,int b 只占了 4 字节,但是 long c 并不是紧跟着 b,而是根据 8 字节对齐后,cb 之间空了 3 字节。同样,char* fint e 之间也空了 4 字节,因此总大小为 40 字节。虽然浪费了 7 字节,但得益于内存对齐,存取速度会更快。这是结构体对齐的基础。

接下来讨论一下联合体(union),联合体的定义跟结构体类似,定义一个如下的联合体:

union test{
    char a; //1
    int b;  //4
    long c; //8
};

同样打印一下这个联合体的大小:

(gdb) p sizeof(union test)
$1 = 8

那么联合体是怎样的一种格式呢?它复用了同一块内存,如图3-2所示。

image 2024 06 07 00 25 39 041
Figure 2. 图3-2 联合体

从图中可以看出,abc 共用同一块内存,修改 a,也会影响 bc 的值,同时可以知道联合体的大小为其最大成员的大小,比如图3-2中联合体 test 的大小为其最大的成员 long c 的大小,也就是 8

这里简单学习了结构体和联合体的区别,以及结构体的对齐,接下来再回顾一下程序执行时的堆和栈相关的基本知识。

联合体是成员变量共享一块内存,可以根据使用确定含义;而结构体是不共享的,成员变量不共享一块内存。另外,结构体存在对齐问题。

堆和栈的基本知识

image 2024 06 07 00 27 27 303
Figure 3. 图3-3 程序的堆和栈

程序执行时的内存布局主要如下:

  1. 栈区(stack)——存储参数值、局部变量,维护函数调用关系等。

  2. 堆区(heap)——动态内存区域,随时申请和释放,程序自身要对内存泄漏负责。

  3. 全局区(静态区)——存储全局和静态变量。

  4. 字面量区——常量字符串存储区。

  5. 程序代码区——存储二进制代码。

程序的堆和栈如图3-3所示。

接下来写一段 C 代码来理解一下各变量分别存在哪个段区,代码如下:

int a=0;     //全局初始化区
char *p1;    //全局未初始化区
main()
{
    static int b=0;    //全局(静态)初始化区
    int c; //栈
    char d[] = "abc";    //栈
    char *p2;          //栈
    char *p3 = "hello";    //hello\0在常量区,p3在栈上
    p1 = (char*)malloc(10);
    p2 = (char*)malloc(20); //分配得来的10和20字节的区域就在堆区
    strcpy(p1, "hello");    //hello\0放在常量区,编译器可能会将它与 p3 所指向的 "hello" 优化成一个地方
}

总体来讲,栈上的变量是局部的,随着局部空间的销毁而销毁,由系统负责。

堆上的变量可以提供全局访问,需要自行处理其生命周期。

这一节学习了数据类型、结构体、联合体,以及程序运行时的堆和栈信息等基本知识,接下来讨论一下 PHP 中的变量。