1. 结构体与联合体
// 包含头文件
// #include<stdio.h> 就是一条预处理命令,它的作用是通知C语言编译系统在对C程序进行正式编译之前需做一些预处理工作
#include<stdio.h>
// 函数就是实现代码逻辑的一个小的单元
// 主函数 在最新的C标准中,main函数前的类型为int而不是void
int main()
{
/*定义结构体*/
struct _s{
char a;
int b;
long c;
void* d;
int e;
char* f;
} s;
/*
成员变量赋值
*/
s.a = 'a'; //地址 0x7fffffffe310
s.b = 1; //地址 0x7fffffffe314
s.c = 2; //地址 0x7fffffffe318
s.d = NULL; //地址 0x7fffffffe320
s.e = 3; //地址 0x7fffffffe328
s.f = &s.a; //地址 0x7fffffffe330
printf("size: %d", sizeof(s)); // size: 40
return 0;
}
结构体是进行内存对齐的, 64位一般是按照8个字节对齐, 每个成员根据自身占用空间, a占用1个字节后会空出3个字节然后b占用4个字节, c和d和f正好占用8个字节, e占用4个字节, 则该结构体共占用40个字节的大小
#include<stdio.h>
// 一个C程序有且只有一个主函数,即main函数
// C程序就是执行主函数里的代码,也可以说这个主函数就是C语言中的唯一入口
int main()
{
/*定义联合体*/
union _u{
char a;
int b;
long c;
void* d;
int e;
char* f;
} u;
/*成员变量赋值*/
u.a = 'a'; //地址 0x7fffffffe330
u.b = 1; //地址 0x7fffffffe330
u.c = 2; //地址 0x7fffffffe330
u.d = NULL; //地址 0x7fffffffe330
u.e = 3; //地址 0x7fffffffe330
u.f = &u.a; //地址 0x7fffffffe330
printf("size: %d", sizeof(u)); // size: 8
return 0;
}
联合体则是共用内存, 以成员占用的最大内存大小对齐, 成员a占用1个字节, 然后b占用4个字节会复用a的内存, c占用8个字节也会复用b的空间, 使用的是同一块内存空间, 即后边的值会覆盖掉前面的值
2. 宏定义
宏 即 “替换”
define ZEND_ENDIAN_LOHI_4(a,b,c,d) d;c;b;a;
// 以下4个变量
ZEND_ENDIAN_LOHI_4(
zend_uchar flags,
zend_uchar nApplyCount,
zend_uchar nIteratorsCount,
zend_uchar consitency
);
// 使用宏之后, 即4个变量会倒过来
consitency,nIteratorsCount,nApplyCount,flags
3. 大小端
PHP要在不同的机器上运行, 而有的机器是大端机器 ( 高位放在低地址, 低位放在高地址 ), 有的是小端机器 ( 低位放在低地址, 高位放在高地址 ), 所以取值要区分大小端
#include<stdio.h>
// 定义一个宏
#define MY_MACRO " a %s"
int main()
{
char *m = "macro";
printf("this is" MY_MACRO, m);
}
#include<stdio.h>
void func1()
{
// 定义16进制 12345678
int i = 0x12345678; // 4个字节
// 0x78 0x56 0x34 0x12
if(*((char*)&i) == 0x12)
{
printf("func1 big endian");
}else{
printf("func1 little endian");
}
}
void func2()
{
// 定义一个联合体
union _u{
int x; // 4个字节
char y; // 1个字节
} u;
u.x = 1; // 4个字节
// 0000 0000 0000 0000 0000 0000 0000 0001
if(u.y == 1)
{
printf("func1 little endian");
}else{
printf("func1 big endian");
}
}
4. 小而巧的zval
php-7.413源码包, 找到Zend文件夹zend_types.h
typedef struct _zval_struct zval;
typedef struct _zend_string zend_string;
typedef struct _zend_array zend_array;
struct _zval_struct {
zend_value value; /* value */
union {
struct {
ZEND_ENDIAN_LOHI_3(
zend_uchar type, /* active type */
zend_uchar type_flags,
union {
uint16_t extra; /* not further specified */
} u)
} v;
uint32_t type_info;
} u1;
union {
uint32_t next; /* hash collision chain */
uint32_t cache_slot; /* cache slot (for RECV_INIT) */
uint32_t opline_num; /* opline number (for FAST_CALL) */
uint32_t lineno; /* line number (for ast nodes) */
uint32_t num_args; /* arguments number for EX(This) */
uint32_t fe_pos; /* foreach position */
uint32_t fe_iter_idx; /* foreach iterator index */
uint32_t access_flags; /* class constant access flags */
uint32_t property_guard; /* single property guard */
uint32_t constant_flags; /* constant flags */
uint32_t extra; /* not further specified */
} u2;
};
typedef union _zend_value {
zend_long lval; /* long value */
double dval; /* double value */
zend_refcounted *counted;
zend_string *str;
zend_array *arr;
zend_object *obj;
zend_resource *res;
zend_reference *ref;
zend_ast_ref *ast;
zval *zv;
void *ptr;
zend_class_entry *ce;
zend_function *func;
struct {
uint32_t w1;
uint32_t w2;
} ww;
} zend_value;
zval为结构体, 包含3个成员变量, 联合体zend_value 别名 value, 联合体 别名 u1, 联合体 别名 u2, value最大的内存为指针类型8字节, u1由两个成员变量构成, 联合体共用内存, 最长4个字节, 结构体v占4个字节(2个uchar, uint16), 另外一个变量也占4个字节(uint32), u2也为联合体, 最长4个字节 (uint32)
zval结构体以8字节对齐, 最长16个字节, 占用空间很小, PHP虽然为弱类型语言, 但是底层也是会标记类型, 通过zval来实现, zval的成员变量value, 定义了多种类型, 然后通过zval的成员变量u1来区分变量的不同类型, u1有一个zend_uchar类型( 1个字节 )的type变量, 通过不同的值来区分不同的类型
若type=4, zval取的就是zend_long类型的lval, 若type=6, zval取的就是zend_string类型的*str( 地址指针 ), 根据地址获取内容, 进而再根据字符串结构体的len和val取字符串
字符串类型zend_string结构体, 成员变量gc也是一个结构体 ( 引用计数 ), 占8个字节, h占8个字节, len占8个字节 ( 字符串长度 ), val[1]占1个字节( val为真正存储字符串的柔性数组, val[1]占位, 后续则存储字符串内容 ), 由于字节对齐所以也占8个字节, 总共32个字节, zval根据该结构体的len和val来取字符串内容
对于简单类型 ( int, double ), 则是直接复制, 先将10赋值给a, 把a赋值给b时, 则是直接将10拷贝一份给b, 因为只用了16个字节的空间存储, 浪费较少
对于复制类型, 则是写时复制
将字符串复制给变量a时, 底层通过zval实现, zval结构体的成员变量u1的type为6, value.str存储的则是字符串真正的内存地址, 即指向一个zend_string结构体, 其引用计数为1
当把变量a赋值给b时, 则b的zval中的value.str也指向a的内存地址, 只不过该字符串zend_string结构体中的引用计数加1
当给变量b重新赋新值时, 则b的zval中的value.str指向另一个字符串存储的内存地址, 即一个新的zend_string结构体, 且变量a指向的zend_string的引用计数减1
数组 ( Hash Table ) 类型
// 定义了一个_zend_array结构体, 别名 HashTable
typedef struct _zend_array HashTable;
typedef struct _Bucket {
zval val;
zend_ulong h; /* hash value (or numeric index) */
zend_string *key; /* string key or NULL for numerics */
} Bucket;
typedef struct _zend_refcounted_h {
uint32_t refcount; /* reference counter 32-bit */
union {
uint32_t type_info;
} u;
} zend_refcounted_h;
struct _zend_array {
zend_refcounted_h gc;
union {
struct {
ZEND_ENDIAN_LOHI_4(
zend_uchar flags,
zend_uchar _unused,
zend_uchar nIteratorsCount,
zend_uchar _unused2)
} v;
uint32_t flags;
} u;
uint32_t nTableMask;
Bucket *arData;
uint32_t nNumUsed;
uint32_t nNumOfElements;
uint32_t nTableSize;
uint32_t nInternalPointer;
zend_long nNextFreeElement;
dtor_func_t pDestructor;
};
_zend_array结构体, 成员变量gc为8个字节的结构体, 引用计数, 与垃圾回收相关, 成员变量u为4个字节的联合体, uint32_t类型的nTableMask, 用来做散列, 计算落在哪个桶里面, 通过键值对的key算出一个散列值, 然后与nTableMask做或运算, 保证元素散列到指定大小的Bucket里, arData中前面存储的是一个个的int32, 记录每个key对应的位置, 后边存储的是一个个Bucket结构体, 每个Bucket里面存储是真正的键值对, ( 成员变量val对应key=>value键值对中的value, 为zval结构体类型, h为通过哈希计算出来的哈希值, 字符串类型的key对应键值对中的key ), 首先通过散列能知道对应的键值对在哪个Bucket上, 然后若出现冲突再通过前面int32记录的位置建立一个逻辑上的列表来解决冲突 , nTableSize为Bucket数组的个数, 初始化时为8, 后续会以2倍值变化, 即16
数组a中先存入索引为foo的值1, 首先foo通过哈希运算得到一个哈希值h, h与nTableMask ( 初始化时, Packed Array的值为-2, 即两个位置, Hash Array的值为-8, 后续会根据插入值数量以2倍值变化, 即-16 )做或运算, 假如得到nIndex=-7, 为了保证数组插入时的顺序, 保证数组foreach时可以根据插入顺序遍历出来, 所以会存在第0个位置, 前面会在索引数组-7的位置上写入0, 后边在位置0的Bucket中存储键值对 ( 其中key实为内存地址, 指向真正的zend_string, val为具体值), nNumUsed和nNumOfElements都加1
若是要查询数组a中foo的值, 先将foo进行哈希运算然后与nTableMask或运算, 得到-7, 然后获取-7位置上的值, 根据该值去对应位置上的Bucket, 最后在判断两个key是否匹配, 匹配则返回val的值
数组a中插入没有key的value为2的值, 没有key时需要用到nNextFreeElement ( 初始化值为0 ), 则h为0, 与nTableMask进行或运算, 得到nIndex为-8, 把该数组元素放到第1个位置的Bucket上, 然后前面索引数组-8位置的值写入1, 同样nNextFreeElement , nNumUsed和nNumOfElements都加1
取值时同样与nTableMask或运算, 然后在索引数组上得到位置, 根据这个位置去对应的Bucket得到值
数组a中插入key为s的值3, 假如s经过哈希运算后与nTableMask进行或运算, 得到nIndex也是-7, 与key为foo的元素产生冲突, 此时仍会将该元素存储到位置为2的Bucket上, 然后在索引数组-7的位置写上2, 同时Bucket成员变量val为zval类型, 其成员变量u2中有一个变量next, 会记录上一个被覆盖的索引数组的位置值, 类似逻辑链表, 同样nNumUsed和nNumOfElements加1
当获取foo的值时, 先找到索引数组-7位置上的值为2, 然后找到对应位置的Bucket, 发现key不匹配, 然后继续找到val.u2.next的值0, 再去0位置的Bucket, key匹配然后获取val值
数组a中再插入key为x的值4, 假如x哈希计算后与nTableMask或运算, 得到nIndex也为-7, 将该元素存储到位置为3的Bucket上, 然后索引数组-7位置上写入3, 同时zval类型的val, 成员变量u2的next, 记录上一个被覆盖的索引数组的位置值2
当获取foo的值时, 先找到索引数组-7位置上的值为3, 然后找到对应位置的Bucket, 发现key不匹配, 然后继续找到val.u2.next的值2, 再去2位置的Bucket, key仍不匹配, 然后继续找到val.u2.next的值0, 再去0位置上的Bucket, key匹配, 获取val值
Packed Array, 前面是索引数组, 后边是Bucket, 其元素的key是从0递增的整数, 所以不需要再计算哈希值, 前面的索引数组用不到, 只保留了2个位置, 取值时直接根据key来找对应位置的Bucket
Hash Array, 前面是索引数组, 后边是Bucket, 其元素的key没有规律, 任意字符串或者数字, 需要对key进行哈希计算, 并使用前面的索引数组维护后边Bucket位置
假如同时向以上两种数组中插入10万个元素, 最后占用的内存大小, Hash Array要大于Packed Array, 因为Hash Array需要前面的索引数组维护位置 ( 10万个元素需要10万位置, 每个位置是一个int ), 则会占用更多的内存空间
所以实际工作中, 同样可以解决问题的, 尽量使用 Packed Array
<?php
// 初始化
$arr = []; // 初始化默认为Packed Array, 其nTableMask为-2, 索引数组保留2个位置
// 无key赋值
$arr[] = 'foo'; // 写入在第0个位置的Bucket上, 无key, h为0, 取值时直接根据key来获取
// 数字key赋值
$arr[2] = 'abc'; // 写入在第2个位置的Bucket上, 无key, h为2, 取值时直接根据key来获取, 第1个位置空置
// 字符串key赋值
$arr['a'] = 'bar'; // 由Packed Array变为Hash Array, 其nTableMask为-8, 索引数组暂时保留8个位置, 元素位置变动, 把原先第2个位置Bucket的元素放到了第1个位置的Bucket, 该元素放到现在第2个位置的Bucket, 且key为a, 并根据a哈希计算后与nTableMask或运算得出的值, 在前面索引数组对应位置上写入Bucket的位置值2, h为a哈希后的值, 取a的值时, 先哈希再或运算然后到对应的Bucket获取
// 无key赋值
$arr[] = 'xyz'; // 写入在第3个位置的Bucket上, 无key, h为3, 取值时直接根据key来获取
// 已存在的key赋值
$arr['a'] = 'foo'; // 对key哈希计算与nTableMask或运算, 找到索引数组对应位置的值, 然后替换到对应位置Bucket的元素
// 根据key查找
$arr['a'];
// 删除key
unset($arr['a']); // 找到对应位置的Bucket, Bucket中的h没变, key没变, 然后把zval类型的val中, 成员变量u1的type值, 不再是6 (IS_STRING )而是变为0 ( IS_UNDEF ), 即没有意思, 所以unset并不是将该元素去掉, 后边可以再进行覆盖
>