PHP7内核剖析 学习笔记 第七章 面向对象
面向对象编程,简称OOP,是一种程序设计思想。面向对象把对象作为程序的基本单元,一个对象包含了数据和操作数据的函数。面向对象一直是软件开发领域内比较热门的话题,它更符合人类看待事物的一般规律。与Java不同,PHP并非纯面向对象的语言,它同时支持面向过程、面向对象,PHP最初的版本是不支持面向对象的,从PHP5开始全面实现了面向对象。
面向对象的特点:
1.封装:将类中的成员属性和方法结合成一个独立的单位,确保类外的部分不能随意存取类的内部数据。
2.继承:一个类可以继承并拥有另外一个类的成员属性和成员方法。
3.多态:程序能够处理多种类型对象的能力,PHP中可以通过继承和接口两种方式实现多态(多个派生类调用同一基类接口,会有不同效果)。
7.1 类
类是现实世界或思维世界中的实体在计算机中的反映,它将某些具有关联关系的数据以及这些数据上的操作封装在一起,是具有相同属性和服务的一组对象的集合。在面向对象编程中,类是对象的抽象,对象是类的具体实例。
在PHP中类是编译阶段的产物,而对象是运行时产生的,它们归属于不同阶段。一个类可以包含属于自己的常量、变量(称为“属性”)、函数(称为“方法”),PHP中我们这样定义一个类:
class 类名 {// 常量;// 成员属性;// 成员方法;
}
除了在PHP脚本中定义的类,还有一种类是内核、扩展提供的。在内核中,不管是内部类还是用户自定义类,均通过zend_class_entry结构表示,类的常量、成员属性、成员方法均保存在这个结构中,这个结构包含的成员非常多,这里列几个比较重要的:
typedef struct _zend_class_entry zend_class_entry;struct _zend_class_entry {// 类的类型:内部类ZEND_INTERNAL_CLASS(1)、用户自定义类ZEND_USER_CLASS(2)char type;// 类名,PHP类名不区分大小写,统一为小写zend_string *name;// 父类struct _zend_class_entry *parent;int refcount;// 类掩码,如普通类、抽象类、接口,除了这些还有别的含义uint32_t ce_flags;// 普通属性数,包括public、privateint default_properties_count;// 静态属性数int default_static_members_count;// 普通属性值数组zval *default_properties_table;// 静态属性值数组zval *default_static_members_table;zval *static_members_table;// 成员方法符号表HashTable function_table;// 成员属性基本信息哈希表,key为成员名,value为zend_property_infoHashTable properties_info;// 常量符号表HashTable constants_table;// 以下是构造函数、析构函数、魔术方法的指针union _zend_function *constructor;union _zend_function *destructor;union _zend_function *clone;union _zend_function *__get;...// 自定义的钩子函数,通常在定义内部类时使用,可以灵活地进行一些个性化操作,用户自定义类不会用到,可忽略zend_object *(*create_object)(zend_class_entry *class_type);zend_object_iterator *(*get_iterator)(zend_class_entry *ce, zval *object, int by_ref);int (*interface_gets_implemented)(zend_class_entry *iface,zend_class_entry *class_type);union _zend_function *(*get_static_method)(zend_class_entry *ce, zend_string *method);// 序列化调用的接口int (*serialize)(zval *object, unsigned char **buffer, size_t *buf_len,zend_serialize_data *data);int (*unserialize)(zval *object, zend_class_entry *ce, const unsigned char *buf,size_t buf_len, zend_unserialize_data *data);uint32_t num_interfaces; // 实现的接口数uint32_t num_traits;zend_class_entry **interfaces; // 实现的接口// union info: 类所在文件、起始行号、所属模块等信息...
};
PHP脚本中定义的类,在编译阶段将被解析为zend_class_entry结构,例如成员方法会按照函数的编译规则编译为zend_function,然后注册到zend_class_entry->function_table中。类编译完成后,将像函数那样注册到一个全局符号表中:EG(class_table)。使用类时根据类名到符号表EG(class_table)中索引,EG(class_table)保存着全部的类,包括用户自定义类和内部类,如图7-1所示:
7.1.1 常量
PHP中可以把在类中始终保持不变的值定义为常量,在定义和使用常量时不需要使用$
符号,常量的值必须是一个定值,比如布尔型、整型、字符串、数组,不能是变量、数学运算的结果(我使用php 8.0.7测试时,常量可以是数学运算的结果,将常量赋值为1+1一切正常)、函数调用,它是只读的,无法进行赋值。类中的常量与PHP中普通的常量(通过define()或const定义的)含义是一样的,只不过类中的常量只属于固定的类,而普通的常量是全局的。
常量通过const关键字定义,常量名通常采用大写,常量通过class_name::CONST_NAME访问,或在类的内部通过self::CONST_NAME访问,例如:
class my_class {// 定义一个常量const CONST_NAME = const_value;public function __construct() {// 在类内部访问常量self::CONST_NAME;}
}
// 在类外访问类的常量
my_class::CONST_NAME;
常量属于类的维度,对于类的所有实例化对象没有任何差异,这一点与成员属性不同。与普通常量相同,类的常量也通过哈希表存储,类的常量保存在zend_class_entry->constants_table中。通常,访问常量时需要根据常量名索引,但有些情况下会在编译阶段将常量直接替换为常量值使用,比如:
// 示例1
echo my_class::A1;class my_class {const A1 = "hi";
}// 示例2
class my_class {const A1 = "hi";
}echo my_class:A1;
以上两例唯一的不同就是常量的使用时机:示例1是在定义前使用的,示例2是在定义后使用的。PHP中无论是变量、常量、函数,都不需要提前声明。
具体debug一下上面两例会发现:示例2编译生成的主要指令只有一个ZEND_ECHO,即直接输出值了,并没有涉及常量的查找,进一步查看发现它的操作数为CONST变量,值为“hi”,即my_class::A1的值;而示例1首先执行的指令是ZEND_FETCH_CONSTANT,查找常量,接着才是ZEND_ECHO。
这两种情况内核会有两种不同的处理方式:示例1会在运行时根据变量名索引zend_class_entry_constants_table,取到常量值后再执行echo;示例2中,由于PHP代码的编译顺序是顺序的,示例2在编译到echo my_class::A1时,首先会尝试检索是否已经编译了my_class,如果能在CG(class_table)中找到,则进一步从类的constants_table中查找对应的常量,找到的话会复制其value替换常量,也就是将常量的检索提前到了编译时,通过这种“预处理”优化了耗时的常量检索过程,避免多次执行时的重复检索,同时也可以利用opcache避免放到运行时重复检索常量(看上去可以更进一步优化,在编译完的pass_two函数里将常量全部替换成值)。
7.1.2 成员属性
类的变量成员叫属性。属性声明由关键字public、protected、private三者之一开头,然后跟一个普通的变量声明来组成。属性中的变量可以初始化,但初始化的值必须是固定不变的值,不能是变量,初始化值在编译阶段就可以得到其值,而不依赖于运行时信息才能求值(看起来类似C++ constexpr),比如public $time = time()
,这样定义属性会触发语法错误。
public、protected、private用于限制成员属性、方法的访问权限:public为公有的,可以在任何地方被访问;protected为受保护的,可以被自身及其子类、父类中访问,在类之外无法访问;private为私有的,只能被其定义所在的类访问。按权限大小排序:public>protected>private,类属性必须定义为三者之一,如果用var定义,则被视为公有。
成员属性分为两类:普通属性、静态属性。静态属性通过static声明,通过self::$property
或className::$property
访问;普通属性通过$this->property
或$object->property
访问。例如:
class my_class {// 普通属性public $property = "normal property";// 静态属性public static $static_property = "static property";public function __construct() {// 内部访问$this->property; // 普通属性self::$static_property; // 静态属性}
}// 外部访问
$obj = new my_class;
$obj->property; // 普通属性
my_class::$static_property; // 静态属性// 我用PHP 8.0.7 (cli)测试时,静态属性也可以这样访问:
$obj::$static_property;
静态成员属性为各对象共享,与常量类似,而普通成员属性是各对象独享,对象之间对普通成员属性的修改不会相互影响。与常量的存储方式不同,成员属性的初始化值并不直接以属性名作为索引的哈希表存储,而是通过数组保存的,普通属性、静态属性各有一个数组分别存储,即default_properties_table、default_static_members_table。编译后类的成员属性的数组如图7-2所示。
静态属性是共享的,所以运行时各对象操作的就是zend_class_entry->default_static_members_table数组中的值,即静态属性值保存在类中,而不是对象中;但普通成员属性是对象独享的,各对象的普通成员属性的值不会保存在类中,而是存储在对象结构中,即zend_object。类中的default_properties_table数组中的值只是用于初始化对象的,在实例化对象时会拷贝到对象中。
普通成员属性的存储与局部变量的实现类似,它们分布在zend_object上,通过相对zend_object的内存偏移进行访问,各属性的内存偏移值在编译时分配,普通成员属性的存取都是通过这个内存offset完成的。静态属性直接根据数组下标访问。
实际上,default_properties_table、default_static_members_table数组只是用来存储属性值的,并不是保存属性信息的,这里说的属性信息包括属性的访问权限(public、protected、private)、属性名、静态属性值的存储下标、非静态属性的内存offset等,这些信息通过zend_property_info结构存储,该结构通过zend_class_entry->properties_info符号表存储,这是一个哈希表,key就是属性名。
typedef struct _zend_property_info {uint32_t offset; // 普通成员变量的内存偏移值,静态成员变量的数组索引uint32_t flags; // 属性掩码,如public、protected、private及是否为静态属性zend_string *name; // 属性名:并不是原始属性名zend_string *doc_comment;zend_class_entry *ce; // 所属类
} zend_property_info;
关键成员含义:
1.name:属性名,注意这里不是原始属性名,private会在原始属性名前加上类名,protected则会加上*作为前缀。
2.offset:普通成员属性的内存偏移值,与CV变量的操作数相同,普通成员属性分配在zend_object结构上,读取时根据zend_object地址 + offset
获取;对于静态成员属性则是default_static_members_table数组索引。
3.flag:属性标识,有两个含义,一是区分是否为静态,静态、非静态属性的结构都存储在这个符号表中;二是属性权限,即public、private、protected。
// flags标识位
#define ZEND_ACC_PUBLIC 0x100
#define ZEND_ACC_PROTECTED 0x200
#define ZEND_ACC_PRIVATE 0x400#define ZEND_ACC_STATIC 0x01
在编译时,成员属性将根据属性类型按照属性定义的先后顺序分配一个对应的offset,用于运行时索引属性的存储位置。读取成员属性分两步:
1.根据属性名从zend_class_entry.properties_info中索引到属性的zend_property_info结构。
2.根据zend_property_info->offset获取具体的属性值,其中静态成员属性通过zend_class_entry.default_static_members_table[offset]
获取,普通成员属性则通过((char *)zend_object) + offset
获取。
下类作为示例:
class my_class {public $property_1 = "hi,php~";public $property_2 = array();public static $property_3 = 110;
}
上类生成的属性结构如图7-3:
当访问self::$property_3
时,首先根据字符串“property_3”检索properties_info,找到该属性的zend_property_info结构,然后以zend_property_info->offset为下标,在default_static_members_table数组中读取对应数据,即default_static_members_table[0]。
7.1.3 成员方法
每个类可以定义若干个属于本类的函数,称之为成员方法。成员方法与普通的函数相比并没有本质上的差别,只不过成员方法封装在类中,是类专有的函数,不能被别的类调用,所以成员方法保存在类中而不是EG(function_table)
全局函数符号表中。
与成员属性一样,成员方法也有权限控制,也有静态、非静态之分,其中静态成员方法不需要通过对象调用,可以直接根据类名::静态成员方法
调用,或者在类内部通过self::静态成员方法
调用,而普通非静态成员方法则只能通过对象发起调用。另外,成员方法除了static静态之分,还有abstract、final两类,分别表示抽象方法、不可被覆盖方法(即不可被重写方法)。
class my_class {static public function static_func() {// ...}public function func() {// ...}
}// 静态方法的调用
my_class::static_func();
// 非静态方法的调用
$obj = new my_class;
$obj->func();
成员方法的存储结构也是zend_function,其中zend_function.common->scope(对用户自定义类而言就是zend_op_array->scope)为该方法所属类,zend_function.common->fn_flags用于标识成员方法的权限、类型(如abstract、final、static),除了这些,fn_flags还有很多其他标识,比如成员方法指定了返回值类型,则fn_flags将包含ZEND_ACC_HAS_RETURN_TYPE标识。
union _zend_function {zend_uchar type; /* MUST be the first element of this struct! */struct {zend_uchar type; /* never used */zend_uchar arg_flags[3]; /* bitset of arg_info.pass_by_reference */uint32_t fn_flags; // 方法标识:finnal/static/abstrct、private/public/protectedzend_string *function_name;zend_class_entry *scope; // 成员方法所属类union _zend_function *prototype;uint32_t num_args;uint32_t required_num_args;zend_arg_info *arg_info;} common;zend_op_array op_array;zend_internal_function internal_function;
};
编译过程与普通函数相同,但成员方法最后被注册到所属类的function_table符号表中,当调用一个成员方法时,将到zend_class_entry.function_table中进行查找。最终编译生成的成员方法符号表如图7-4所示。
7.1.4 类的编译
以下例来看一下类的编译过程:
// 示例:/tmp/user.php
class User {const TYPE = 110;static $name = "uuu";public $uid = 900;public function getName() {return $this->uid;}
}
类定义的语法规则(这段语法规则是使用Bison(或类似的解析器生成器,如Yacc)的语法编写的):
// 类声明规则
class_declaretion_statement:// 情况1:带有类修饰符(class_modifiers,如abstract、final)的类声明// T_CLASS是关键字class// $<num>$ = CG(zend_lineno)记录了当前行号class_modifiers T_CLASS { $<num>$ = CG(zend_lineno); }// T_STRING是类名// extends_from是继承的父类// implements_list是实现的接口列表// backup_doc_comment用于保存类的文档注释// class_statement_list是类体中的成员列表(属性、方法等)T_STRING extends_from implements_list backup_doc_comment '{' class_statement_list '}'// 动作代码:调用zend_ast_create_decl创建类的AST节点,ZEND_AST_CLASS是节点类型// $1是类修饰符(来自class_modifiers),$<num>3是行号// 剩下的$+数字的参数都是变量,如父类、实现的接口列表等,其中zend_ast_get_str是获取类名字符串// 因此$4应该是类名T_STRING{ $$ = zend_ast_create_decl(ZEND_AST_CLASS, $1, $<num>3, $7, zend_ast_get_str($4),$5, $6, $9, NULL); }// 情况2:无修饰符的类,基本同情况1| T_CLASS { $<num>$ = CG(zend_lineno); }T_STRING extends_from implements_list backup_doc_comment '{' class_statement_list '}'{ $$ = zend_ast_create_decl(ZEND_AST_CLASS, 0, $<num>2, $6, zend_ast_get_str($3),$4, $5, $8, NULL); }
;// 整个类内为list,每个成员属性、成员方法都是一个子节点
class_statement_list:// 递归合并class_statement_list和class_statementclass_statement_list class_statement// 将新成员添加到列表中{ $$ = zend_ast_list_add($1, $2); }| /* empty,空类体 */// 创建一个空的ZEND_AST_STMT_LIST节点{ $$ = zend_ast_create_list(0, ZEND_AST_STMT_LIST); }
;// 类内语法规则:成员属性、成员方法
class_statement:// 成员属性// variable_modifiers是属性修饰符(如public、static),property_list是属性列表(如$a、$b)variable_modifiers property_list ';'// 动作代码:将修饰符$1赋值给属性列表的attr成员{ $$ = $2; $$->attr = $1; }// 常量// T_CONST是关键字const,class_const_list是常量列表(如NAME = value)| T_CONST class_const_list ';'// 重置文档注释{ $$ = $2; RESET_DOC_COMMENT(); }...// 成员方法// method_modifiers是方法修饰符(如public、final),function是关键字// returns_ref是是否返回引用,identifier是方法名,backup_doc_comment是方法的文档注释| method_modifiers function returns_ref identifier backup_doc_comment // parameter_list是参数列表'(' parameter_list ')'// return_type是返回类型(如: void),method_body是方法体return_type method_body// 动作代码:调用zend_ast_create_decl创建ZEND_AST_METHOD类型的节点{ $$ = zend_ast_create_decl(ZEND_AST_METHOD, $3 | $1, $2, $5, zend_ast_get_str($4), $7, NULL, $10, $9); }
;
从语法规则可以看出,类被编译为ZEND_AST_CLASS节点,该节点有3个子节点,分别表示:继承类、实现的接口列表、类的表达式,其中类的表达式是一个list节点,每个常量、成员属性、成员方法对应一个子节点,其节点类型分别为ZEND_AST_CLASS_CONST_DECL、ZEND_AST_PROP_DECL、ZEND_AST_METHOD:
1.ZEND_AST_CLASS_CONST_DECL:类常量声明节点,类型为list,因为可以同时声明多个常量,比如const C1, C2, C3
,list中的每个子节点都是一个常量,类型为ZEND_AST_CONST_ELEM,它有两个子节点,分别用于常量名、常量值,具体规则见语法规则文件中的class_const_list配置。
2.ZEND_AST_PROP_DECL:成员属性声明节点,与常量节点相同,这也是一个list节点,其中可以同时声明多个成员属性,子节点类型为ZEND_AST_PROP_ELEM,它有三个子节点,其中前两个子节点分别表示属性名、属性初始化,第三个子节点用于保存注释。需要注意的是,属性的访问权限、是否为静态的信息没有保存在ZEND_AST_PROP_ELEM节点中,而是通过ZEND_AST_PROP_DECL->attr保存。具体规则见语法规则文件的property_list。
3.ZEND_AST_METHOD:成员方法声明节点,与函数的声明节点ZEND_AST_FUNC_DECL类似。每个ZEND_AST_METHOD节点表示一个成员方法,它有4个子节点,其中第一个子节点是参数列表,第二个子节点没有使用,第三个子节点为函数体,第四个子节点为返回值类型。
示例/tmp/user.php最终生成的抽象语法树如图7-5所示:
ZEND_AST_CLASS节点由zend_compile_class_decl()函数完成编译,类的各个子节点的编译过程相对比较独立,依次进行编译即可。编译步骤如下:
1.分配zend_class_entry结构,如果有继承的父类,则编译一条ZEND_FETCH_CLASS指令。
2.编译一条类声明的ZEND_DECLARE_CLASS指令,该指令的作用是将编译后的类注册到EG(class_table),与函数的ZEND_DECLARE_FUNCTION指令作用相同。同时将类的zend_class_entry注册到CG(class_table)。注意:key并不是类名,而是\0 + 类名 + 文件名 + lex_pos
,这个与函数编译时的处理是一样的,此时注册后的类还不能被使用。
3.将CG(active_class_entry)指向当前类的zend_class_entry结构,然后编译常量、成员属性、成员方法,并把编译结果注册到CG(active_class_entry)对应的符号表中。
// ast参数是类声明的AST节点
void zend_compile_class_decl(zend_ast *ast) {zend_ast_decl *decl = (zend_ast_decl *)ast;zend_ast *extends_ast = decl->child[0]; // 父类zend_ast *implements_ast = decl->child[1]; // 实现的接口节点zend_ast *stmt_ast = decl->child[2]; // 类中声明的常量、属性、方法zend_string *name, *lcname;// 1) 分配zend_class_entryzend_class_entry *ce = zend_arena_alloc(&CG(arena), sizeof(zend_class_entry));zend_op *opline;...lcname = zend_new_interned_string(lcname);ce->type = ZEND_USER_CLASS; // 类型为用户自定义类ce->name = name; // 类名// 初始化类结构zend_initialize_class_data(ce, 1);...if (extends_ast) {...// 有继承的父类则首先生成一条ZEND_FETCH_CLASS的opcodezend_compile_class_ref(&extends_node, extends_ast, 0);}// 2)生成类声明指令// 获取下一条opcodeopline = get_next_op(CG(active_op_array));zend_make_var_result(&declare_node, opline);...// 操作数2为类名opline->op2_type = IS_CONST;// 将类名的小写形式存入op2LITERAL_STR(opline->op2, lcname);// 如果是匿名类if (decl->flags & ZEND_ACC_ANON_CLASS) {...} else {zend_string *key;// 如果有父类if (extends_ast) {// 生成ZEND_DECLARE_INHERITED_CLASS指令opline->opcode = ZEND_DECLARE_INHERITED_CLASS;opline->extended_value = extends_node.u.op.var;} else {// 无继承类则生成ZEND_DECLARE_CLASS指令opline->opcode = ZEND_DECLARE_CLASS;}// 生成类的临时注册key:\0 + 类名 + 文件名 + lex_poskey = zend_build_runtime_definition_key(lcname, decl->lex_pos);opline->op1_type = IS_CONST;// 将这个临时key保存到操作数1中LITERAL_STR(opline->op1, key);// 以临时key为key,将zend_class_entry注册到类符号表CG(class_table)zend_hash_update_ptr(CG(class_table), key, ce);}// 3)编译类的常量、成员属性、成员方法// 设置当前编译的类上下文CG(active_class_entry) = ce;// 递归编译类体内的成员zend_compile_stmt(stmt_ast);...CG(active_class_entry) = original_ce;
}
生成的ZEND_DECLARE_CLASS指令有两个操作数:操作数1记录的是将类注册到CG(class_table)中的那个特殊key(以上代码中的key变量);操作数2记录的是小写的类名。两个操作数的类型都是CONST,如图7-6:
接下来我们分别看下常量、成员属性、成员方法的编译过程。
1.类常量的编译
常量的节点类型为ZEND_AST_CLASS_CONST_DECL,该节点为list节点,如果一个同时声明(一个const声明多个常量的情况,例如const a, b;
)了多个常量,则会有多个子节点。如果通过多个const声明了常量,则会有多个ZEND_AST_CLASS_CONST_DECL节点。编译过程如下:
// ast是类常量声明的语法树节点
void zend_compile_class_const_decl(zend_ast *ast) {// 将语法树节点类型转换为list节点zend_ast_list *list = zend_ast_get_list(ast);// 获取当前编译的类zend_class_entry *ce = CG(active_class_entry);uint32_t i;// 依次编译每个子节点:const C1, C2, C3 = 100;for (i = 0; i < list->children; ++i) {// 单个常量的ast节点zend_ast *const_ast = list->child[i];// 常量名节点zend_ast *name_ast = const_ast->child[0];// 常量值节点zend_ast *value_ast = const_ast->child[1];// 从ast节点中取出常量名字符串zend_string *name = zend_ast_get_str(name_ast);zval value_zv;// 从ast节点中取出常量值zend_const_expr_to_zval(&value_zv, value_ast);name = zend_new_interned_string_safe(name);// 将常量注册到zend_class_entry->constants_table哈希表中if (zend_hash_add(&ce->constants_table, name, &value_zv) == NULL) {...}...}
}
2.成员属性的编译
属性节点类型为ZEND_AST_PROP_DECL,该节点也是list节点,表示同时声明多个属性的情况(如public $a, $b;
),子节点类型为ZEND_AST_PROP_ELEM。ZEND_AST_PROP_DECL节点的attr保存着要声明的属性的访问权限,即public、protected、private。编译过程如下:
void zend_compile_prop_decl(zend_ast *ast) {zend_ast_list *list = zend_ast_get_list(ast);// 属性修饰符:static、public、private、protecteduint32_t flags = list->attr;zend_class_entry *ce = CG(active_class_entry);uint32_t i, children = list->children;for (i = 0; i < children; ++i) {// 子节点类型为ZEND_AST_PROP_ELEMzend_ast *prop_ast = list->child[i];// 属性名节点zend_ast *name_ast = prop_ast->child[0];// 属性值节点zend_ast *value_ast = prop_ast->child[1];// 注释zend_ast *doc_comment_ast = prop_ast->child[2];}...// 检查属性是否重复、赋予属性初始值或默认值
}
接下来会创建属性的zend_property_info结构,并将其注册到zend_class_entry->proterties_info符号表中。然后将属性值保存到属性默认值数组中,这一步会分配属性的offset,静态属性的offset为数组下标0、1、2···依次分配,每编译一个静态属性,就将default_static_members_count的值加1,从而记录静态属性的数量;非静态属性的offset为相对zend_object结构的内存偏移,每编译一个非静态属性,就将default_properties_count的值加1,从而记录非静态属性的数量,分配时也是根据这个值进行编号的,然后将编号乘以sizeof(zval)
得到内存偏移值。
3.成员方法的编译
成员方法和函数的编译过程都是由zend_compile_func_decl函数完成的,该函数在zend_compile_stmt函数中被调用。
但编译成员方法时(通过传入zend_compile_func_decl函数的zend_ast抽象语法树节点中的值来判断是否是方法),会调用zend_begin_method_decl,而非zend_begin_func_decl。zend_begin_method_decl函数会将成员方法的zend_op_array注册到zend_class_entry->function_table,另外,如果成员方法是魔术方法,则会将zend_class_entry结构中对应的魔术方法指向该成员方法。
完成以上类常量、成员属性、成员方法的编译后,最后会将类注册到EG(class_table),注册过程与函数的注册过程完全相同,即在zend_compile_top_stmt函数中,如果函数的抽象语法树节点参数的类型是函数或类编译,则进入相同的处理步骤,该步骤中会调用zend_do_early_binding。
类开始编译时曾生成一条ZEND_DECLARE_CLASS指令,它的作用就是注册类,但该指令不是在运行时执行的,而是在zend_do_early_binding函数中执行,该指令会调用do_bind_class注册类,注册完后会将前面临时注册的类的key从EG(class_table)中删除。
do_bind_class函数首先会根据ZEND_DECLARE_CLASS指令的操作数1获取临时key,然后根据该key从CG(class_table)中得到zend_class_entry,然后再以操作数2记录的实际类名为key,将该zend_class_entry注册到CG(class_table)中。之后会将ZEND_DECLARE_CLASS置为空指令,同时删除它的两个CONST操作数。
7.1.5 内部类
内部类是由内核或扩展直接注册的类,它不需要经历编译的过程。内部类的结构也是zend_class_entry,其注册位置和用户自定义类相同(都是EG(class_table)
)。内部类更简单灵活,可进行一些个性化处理,如定义一个创建对象的钩子函数,在对象实例化时调用它。
定义内部类时的操作(如定义常量、定义方法、注册类到符号表等)与用户自定义类的实现相同,只是定义内部类时直接调用相关API完成这些操作。
7.1.6 类的自动加载
PHP中,我们通常会把类定义在一个文件中,使用时通过include加载进来,有时这导致长长的include列表;有时文件名修改了,我们要把每个引用的地方都改一遍。而PHP通过类自动加载功能,在使用未被定义的类时,自动将类加载进来。
PHP通过__autoload()
、spl_autoload_register()
实现自动加载。
类自动加载实际就是内核提供的一个钩子函数,实例化类时如果EG(class_table)中没找到对应的类,则调用该钩子函数,调用完重新查找一次,该钩子函数通过EG(autoload_func)指定。
1.__autoload()
这种方式只需用户提供一个__autoload()
函数,参数是类名,在函数中include类名对应的文件。
__autoload()
是默认的类加载器,我们也可以自定义类加载器,然后将EG(autoload_func)指向自定义的即可。没有自定义的类加载器时,会将PHP用户代码中定义的__autoload()
作为类加载器。类的查找通过zend_loopup_class_ex()完成,其中会先查找EG(class_table),如果找到了就返回类,否则就查看是否定义了__autoload()
,如果定义了就调用它,之后再次在EG(class_table)中查找类。
2.spl_autoload_register()
spl_autoload_register()允许定义多个加载器,而__autoload()
只能定义一个。实现上,spl(Standard PHP Library,标准PHP库)创建了一个队列保存用户注册的所有加载器,然后定义了一个函数到EG(autoload_func),当找不到类时,内核回调spl_autoload_call,该函数会依次调用用户注册的加载器,每调用一个就到EG(class_table)中重新检查类是否被注册了,直到注册成功为止。
spl_autoload_register函数的参数:
bool spl_autoload_register(callable $autoload_function, bool $throw = true, bool $prepend = false);
参数autoload_function是函数或成员方法;参数throw用于设置注册失败时是否抛异常;参数prepend为true时会将函数添加到队列之首,而非队列之尾。
SPL的实现位于ext/spl目录下。
7.2 对象
对象是类的实例,创建时使用new关键字。PHP内部,对象的结构为zend_object:
成员含义:
1.handle:一次request期间的对象唯一编号,与创建顺序有关,垃圾回收时会用到。
2.ce:对象所属的类。
3.handlers:对象操作的处理函数,如成员属性读写、成员方法获取、对象销毁和克隆等。这些操作有默认的函数,也可通过扩展的自定义处理函数覆盖:
zend_object中的const zend_object_handlers *handlers
成员的const修饰的是handlers指向的对象,因此可以将其指向自定义的handlers,如obj->handlers = xxx
,但不能修改handlers中的const值,如obj->handlers->write_property = xxx
。如果想自定义某个handler,通常复制一份std_object_handlers,然后修改该副本,最后整体替换handlers。
4.properties:创建对象之初此值为NULL,主要用于定义动态属性。
5.properties_table:非静态成员属性数组,用于存储其属性值。非静态成员通过其offset分配在该数组对应位置上,读写非静态成员属性就是操作此数组。该数组的长度上图中是1,它是一个变长数组。它的长度为1而非0的原因为,类中定义了魔术方法__get
、__set
、__unset
、__isset
时,会多分配一个元素,用于防止魔术方法循环调用。
7.2.1 对象的创建
语法:
// 规则名:new_expr
new_expr:// T_NEW对应new关键字// class_name_reference是一个规则,表示类名的引用,可以是类名字符串或变量(如$className)// ctor_arguments是一个规则,表示构造函数的参数列表T_NEW class_name_reference ctor_arguments// $$表示当前规则的结果// zend_ast_create是PHP内部函数,用于创建AST节点// ZEND_AST_NEW是要创建的AST节点类型// $2是子规则2(即class_name_reference)对应的AST// $3是子规则3(即ctor_arguments)对应的AST{ $$ = zend_ast_create(ZEND_AST_NEW, $2, $3) }// |表示或,即另一种可能的语法结构// anonymous_class是一个规则,表示匿名类的完整定义(类体、继承等)| T_NEW anonymous_class{ $$ = $2 }
;
以上语法规则中的匿名类的一个例子为:
// 匿名类对象
$obj = new class($arg) extends ParentClass
{private $prop;public function method() {}
};
创建对象的语句在语法解析阶段被编译为ZEND_AST_NEW节点,之后该节点被zend_compile_new()编译为ZEND_NEW指令,执行时该指令将创建并初始化对象,步骤如下。
1.查找类
现根据类名从EG(class_table)中查找该类,即zend_fetch_class_by_name()操作,如果没有找到会触发类的自动加载机制,如果找到了会返回类的zend_class_entry结构。另外,如果类名是CONST类型(而非$className
变量),则会用到运行时缓存机制,避免下次调用重复查找。
2.创建、初始化对象
ZEND_NEW指令通过object_init_ex函数完成。
该过程分两步:第一步分配zend_object内存,分配时会根据非静态属性的数量zend_class_entry->default_properties_count,将非静态属性的内存一起分配;第二步是初始化非静态成员属性,将非静态成员属性的默认值拷贝至zend_object->properties_table。object_init_ex函数中会调用_object_and_properties_init()
完成这两步。
_object_and_properties_init()
中,如果没有自定义的对象创建handler时的操作:
(1)创建对象
会调用zend_objects_new()分配zend_object,分配时会调用zend_object_properties_size()获取非静态属性占用的内存大小,该大小通过非静态属性的个数乘sizeof(zval)
来确定,如果定义了魔术方法,那么会多分配一个zval的空间,然后还会设置对象处理handlers为默认的handlers(即std_object_handlers)。
(2)初始化非静态成员属性
非静态成员属性的默认值保存在类的default_properties_table数组中,初始化时根据属性的offset将各属性的value复制到对应位置。这里不是深拷贝,两者指向的value还是同一份,之后如果修改属性值,会触发写时复制,从而重新复制一份。但在ZTS(Zend Thread Safety,Zend线程安全)下,数组、普通字符串会发生深拷贝。
_object_and_properties_init()
中,如果自定义了对象创建handler(通过覆盖类的zend_class_entry.create_object接口成员),则需要自己创建对象并初始化非静态成员属性,此时可以自己设置对象处理handlers。
3.调用构造方法
如果类定义了构造方法,则在对象创建完后会调用它。
7.2.2 非静态成员属性的读写
非静态成员属性的读写处理handler分别为zend_object->handlers中的read_property、write_property,默认的处理函数为zend_std_read_proerty、zend_std_write_property。
1.读取
首先根据属性名查找zend_class_entry->properties_info,找到属性的zend_property_info结构,并检查是否有该属性的操作权限,然后根据zend_property_info->offset从zend_object->properties_table数组中获取对应属性。
如果没有找到属性,会检查类是否定义了__get
魔术方法,如果定义了则调用魔术方法进行处理。
如果类定义了__get()
,在实例化对象分配properties_table时会多分配一个zval,类型为HashTable,称其为guard,每次调用__get($var)
时会把$var
存入其中,目的是防止循环引用。
例如,在__get($var)
中又访问了$var
,会再次调用__get($var)
,从而无限递归下去。所以在调用__get
前会先判断要访问的属性是否已在guard哈希表中了,如果存在则不再调用__get
,如果不存在则将其插入哈希表guard。
其他魔术方法也会用到guard,哈希表的值类型为zend_long,不同魔术方法占用不同bit位。这个HashTable只有在zend_class_entry->ce_flags包含ZEND_ACC_USE_GUARDS标识时才会分配,编译类时,如果发现定义了__get
、__set
、__unset
、__isset
时会给ce_flags打上此标识。
2.修改
类似读取,首先查找属性,然后再修改。如果属性没有找到且定义了__set
魔术方法,则会调用它。
7.2.3 对象的复制
PHP普通对象的复制可直接通过赋值完成,如:
$a = array();
$b = $a;
但将对象赋值给另一个变量时,不会发生深拷贝,如果修改其中一个对象,另一个也随之改变。
第4章介绍写时复制机制时,说过zval的type_flag,只有有IS_TYPE_COPYABLE标识的类型的变量在变量赋值时才进行深拷贝,而object类型没有这个标识。
PHP通过clone关键字实现对象的复制:
$copy_of_object = clone $object;
clone时会复制对象的所有属性(不会深拷贝),这样各自的修改就不会互相影响。如果类中定义了__clone
魔术方法,则clone时会调用此函数。clone通过zend_object->clone_obj(默认是zend_objects_clone_obj()
)进行处理。
7.2.4 对象的比较
使用比较运算符==
比较两个对象变量时,如果两个对象的属性和属性值都相等,且两个对象是同一个类的实例,那么这两个对象变量相等。如果使用全等运算符===
,这两个对象变量一定要指向某个类的同一个实例才为true。
对象间的比较运算符通过zend_std_compare_objects()处理,全等运算符通过zend_is_identical()处理。
7.2.5 对象的销毁
销毁对象由zend_objects_store_del()完成,其中主要操作有:清理成员属性、从EG(objects_store).object_buckets中删除、释放zend_object内存。
垃圾回收时因循环引用导致无法正常回收的垃圾类型,一种类型是数组,另一种是对象。减少refcount时如果发现object的引用计数大于0,则将加入垃圾回收器。
7.3 继承
继承是面向对象的三个特性之一,它允许子类继承父类所有公有或受保护的特征和行为。
PHP通过extends关键词继承一个父类,一个类只允许继承一个父类,但可以多级继承:
class parent {}class child extends parent {}
如果有继承的父类,则该子类的zend_class_entry->parent指向父类的结构。编译含继承的类时,在注册前需先找到父类,因此父类要在子类前注册。含继承的类编译生成的类声明指令是ZEND_DECLARE_INHERITED_CLASS,不含继承的类编译生成的类声明指令是ZEND_DECLARE_CLASS。同时,含继承的类的声明指令前会生成一条ZEND_FETCH_CLASS指令。
接下来编译类常量、成员属性、成员方法的过程同普通类。之后zend_do_early_binding()的类注册环节不同,普通类直接注册到EG(class_table),而含继承的类需要先注册父类,即子类注册时parent指针不能为空。ZEND_FETCH_CLASS指令就是用来获取父类的。
如果父类先注册,即子类在zend_do_early_binding()时能找到父类,那么执行ZEND_DECLARE_INHERITED_CLASS指令时就会调用do_bind_inherited_class继承父类的数据,然后将子类的parent指针指向父类,然后将子类注册到EG(class_table)从而成功编译,子类成功编译后,ZEND_FETCH_CLASS、ZEND_DECLARE_INHERITED_CLASS指令会被置为空指令。此过程ZEND_FETCH_CLASS指令没有起到什么作用。
如果子类先注册,即子类在zend_do_early_bind()时找不到父类,那么ZEND_FETCH_CLASS、ZEND_DECLARE_INHERITED_CLASS指令会保留到运行时执行。
在运行时,由于父类在编译时已经完成注册,所以此时ZEND_FETCH_CLASS能找到父类。成功fetch父类后,继续执行ZEND_DECLARE_INHERITED_CLASS继承父类,这个过程仍然是do_bind_inherited_class函数完成的。
几种不同情况的指令执行顺序:
上图中只有情况2可以正常执行。上图中实际执行顺序与代码顺序不同的原因是,父类定义在编译期完成了,所以在实际执行顺序中相当于第一个执行。情况1报错的原因是,A类由于编译期没找到父类B而没有完成定义,从而没有被放入EG(class_table)。情况3报错的原因是,编译期由于没找到父类,所以A、B类全部未完成定义,从而在运行时定义A类时因找不到B类报错。
do_bind_inherited_class函数与注册非继承类的do_bind_class函数相比,只多了调用zend_do_inheritance这一步,这一步处理的就是父子类继承的逻辑,其中会合并常量、属性、方法。
7.3.1 常量的继承
父类与子类的常量名相同时,用子类的常量,否则将父类的常量合并到子类。
7.3.2 成员属性的继承
步骤如下:
1.合并非静态成员属性
首先申请一个父类+子类非静态属性大小的数组,然后先将父类的非静态属性复制到新数组,后面紧接着将子类的非静态属性数组复制过去,子类的default_properties_table指向合并后的新数组,default_properties_count更新为新数组大小,最后释放子类旧的非静态属性数组。
2.合并静态成员属性
与非静态属性相同,最后更新子类default_static_members_table到新的静态属性数组。
3.更新子类属性offset
每个子类属性的property_info->offset需加上前面父类属性的总大小。静态属性的offet为数组下标,非静态属性的offset为内存偏移值。
4.合并属性信息哈希表
索引属性时,用的是properties_info哈希表,因此需要将父类和子类的属性索引表合并,此过程决定了父类哪些属性可被子类继承和覆盖。
合并的策略在do_inherit_property()中,父类的私有属性无法被子类继承,如子类覆盖了父类中的属性,则只能放大属性的权限而不能缩小,如父类中的public属性,子类中不能覆盖为protected、private的。关于子类覆盖父类的属性:
(1)如果子类中缩小某属性权限会出现的问题:如果一个函数参数是父类类型,且其中用到了父类类型的该属性,如果传入的是父类对象,则可以正常执行,如果传入的实参是子类对象,那么由于此属性不可访问,会报错,这破坏了父类的原有契约,因此不能缩小属性权限。
(2)对于父类的private属性,它属于父类的实现细节,子类完全不可见,因此子类中可以自由定义同名属性,它与父类中的同名属性同存于两个作用域:
class base
{private $a = 1;
}class derived extends base
{protected $a = 2;
}$obj = new derived;
var_dump($obj);
执行它:
而覆盖子类中的非private属性时,实际子类中只有一个同名属性:
class base
{protected $a = 1;
}class derived extends base
{public $a = 2;
}$obj = new derived;
var_dump($obj);
执行它:
7.3.3 成员方法的继承
子类可以继承父类公有、受保护的方法。成员方法的继承的实现也是将父类的function_table合并到子类的function_table中:先扩大子类的function_table,以容纳父子类的全部方法,然后遍历父类的function_table,如果可继承,则将该方法插入子类的function_table。
7.4 动态属性
php中可通过以下方式动态创建一个没有明确定义过的成员属性:
class my_class
{public $id = 123;
}$obj = new my_class;
$obj->prop_1 = array(1, 2, 3);
print_r($obj);
执行后输出:
动态创建的属性保存在zend_object->properties哈希表中,查找一个属性时会先按普通属性在zend_class_entry->properties_info中查找,找不到时再去zend_object->properties中继续找。
在修改成员属性时,会先按普通属性查找,如果找到了,就根据其offset取出属性值后再修改。如果没找到,说明是动态属性,就将其插入zend_object->properties哈希表。首次创建动态属性时,会通过rebuild_object_properties()初始化zend_object->properties,初始化时会将普通属性也加入properties哈希表中,因为Zend中很多地方需要获取全部属性,如对象转数组、垃圾回收等,此时直接获取properties数组即可,否则还需要合并动态属性和普通属性,对象处理的handlers中,get_properties()用来获取全部属性。初始化zend_object->properties时,加入其中的普通属性并不是增加原zend_value的refcount,而是创建了一个IS_INDIRECT类型的zval,指向原zval:
成员属性的读取通过zend_object->handlers->read_property(默认是zend_std_read_property()
)函数完成,动态属性的查找与修改成员属性(通过write_property函数)时的过程相同。
7.5 魔术方法
魔术方法是PHP提供的一些特殊操作时的钩子函数,与普通方法相同,它也保存在zend_class_entry->function_table中。
编译方法时,如果发现魔术方法,除了会将其加入zend_class_entry->function_table,还会设置zend_class_entry中对应的指针:
如一个类中定义了__construct()
、__get()
,则其zend_class_entry结构如图7-12所示:
__sleep()
、__wakeup()
在zend_class_entry中没有单独的指针指向,它们主要是序列化serialize()、反序列化unserialize()时调用的。
调用serialize时,最终会调用到php_var_serialize_intern函数,这个函数会根据不同类型选择不同序列化方式,如果类型是对象,会先检查zend_class_entry.function_table中是否有__sleep()
,如果有则调用它进行序列化。
相关文章:
PHP7内核剖析 学习笔记 第七章 面向对象
面向对象编程,简称OOP,是一种程序设计思想。面向对象把对象作为程序的基本单元,一个对象包含了数据和操作数据的函数。面向对象一直是软件开发领域内比较热门的话题,它更符合人类看待事物的一般规律。与Java不同,PHP并…...
地信GIS专业关于学习、考研、就业方面的一些问题答疑
整理了地信GIS专业学生问得最多的几个问题:关于GIS专业学习、考研以及就业方面;大家可以一起来探讨一下。 学习方面 1、 作为一名GISer需要哪些核心素养或能力? 答:GIS是个交叉学科,涉及到地理学、地质学、测绘、遥感…...
构建可重复的系统 - SRE 的 IaC 与 CI/CD 基础
构建可重复的系统 - SRE 的 IaC 与 CI/CD 基础 还记得我们在第一篇提到的 SRE 核心原则之一——减少琐事 (Toil) 吗?想象一下手动配置服务器、部署应用程序、管理网络规则……这些任务不仅耗时、重复,而且极易出错。当系统规模扩大时,手动操作很快就会变得难以为继。SRE 的核…...
CQF预备知识:一、微积分 —— 1.2.2 函数f(x)的类型详解
文中内容仅限技术学习与代码实践参考,市场存在不确定性,技术分析需谨慎验证,不构成任何投资建议。 📖 数学入门全解 本教程为复习课程,旨在帮助读者复习数学知识。教程涵盖以下四个主题: 微积分线性代数微…...
PyQt学习系列03-动画与过渡效果
PyQt学习系列笔记(Python Qt框架) 第三课:PyQt的动画与过渡效果 一、动画与过渡效果概述 1.1 动画与过渡的区别 动画(Animation):用于描述对象属性随时间变化的过程(如位置、颜色、大小&…...
偏微分方程数值方法指南及AI推理
偏微分方程(PDE)是我们用来描述科学、工程和金融领域中各种现象的语言——从流体流动和热传递到波的传播和金融衍生品的定价。然而,这些方程的解析解通常难以获得,尤其是在处理复杂几何形状或非线性行为时。这时,数值方…...
flask允许跨域访问如何设置
flask允许跨域访问 在Flask中,允许跨域访问通常涉及到CORS(跨源资源共享)策略。Flask本身并不直接提供CORS支持,但你可以通过安装和使用第三方库如Flask-CORS来轻松实现跨域资源共享。 安装Flask-CORS 首先,你需要安装Flask-CORS。你可以使用pip来安装它: pip instal…...
深度学习模型部署:使用Flask将图像分类(5类)模型部署在服务器上,然后在本地GUI调用。(全网模型部署项目步骤详解:从模型训练到部署再到调用)
个人github对应项目链接: https://github.com/KLWU07/Image-classification-and-model-deployment 1.流程总览 2.图像分类的模型—Alexnet 3.服务器端部署及运行 4.本地PyCharm调用—GUI界面 一、流程总览 本项目方法还是使用Flask 库,与之前一篇机器学…...
在Pycharm中如何安装Flask
(推荐)方法一:在Pycharm中创建项目之后,再安装Flask 1:在创建Pycharm时,解释器类型选择第一个:项目venv(自动生成的虚拟环境),在左下角选择终端(…...
基于Scikit-learn与Flask的医疗AI糖尿病预测系统开发实战
引言 在精准医疗时代,人工智能技术正在重塑临床决策流程。本文将深入解析如何基于MIMIC-III医疗大数据集,使用Python生态构建符合医疗AI开发规范的糖尿病预测系统。项目涵盖从数据治理到模型部署的全流程,最终交付符合DICOM标准的临床决策支…...
解决前端路由切换导致Keycloak触发页面刷新问题
使用window.location.href进行页面跳转时,浏览器会完全刷新页面,这会导致当前的JavaScript上下文被清空。 如果你的登录状态依赖于某些临时存储(如LocalStorage或sessionStorage),而这些存储在页面刷新后未正确初始化或丢失,就会导致用户被认为未登录。触发keycloak再次登录导…...
基于大模型的胫腓骨干骨折全周期预测与治疗方案研究报告
目录 一、引言 1.1 研究背景与意义 1.2 研究目的与创新点 1.3 国内外研究现状 二、大模型技术原理与应用基础 2.1 大模型的基本架构与算法 2.2 医疗数据的收集与预处理 2.2.1 数据收集 2.2.2 数据预处理 2.3 模型训练与优化 2.3.1 模型训练过程 2.3.2 参数调整与超…...
智慧交通的核心引擎-车牌识别接口-车牌识别技术-新能源车牌识别
在数字化与智能化浪潮席卷交通运输领域的今天,车牌识别接口功能正以其精准、高效的特性,成为构建智慧交通体系的关键技术支撑。通过自动采集、识别车牌信息并实现数据互通,该功能已被深度融入交通管理、物流运输、出行服务等多个场景…...
小白的进阶之路系列之三----人工智能从初步到精通pytorch计算机视觉详解上
计算机视觉是教计算机看东西的艺术。 例如,它可能涉及构建一个模型来分类照片是猫还是狗(二元分类)。 或者照片是猫、狗还是鸡(多类分类)。 或者识别汽车出现在视频帧中的位置(目标检测)。 或者找出图像中不同物体可以被分离的位置(全视分割)。 计算机视觉应用在…...
手写简单的tomcat
首先,Tomcat是一个软件,所有的项目都能在Tomcat上加载运行,Tomcat最核心的就是Servlet集合,本身就是HashMap。Tomcat需要支持Servlet,所以有servlet底层的资源:HttpServlet抽象类、HttpRequest和HttpRespon…...
院校机试刷题第九天:P1042乒乓球、回顾代码随想录第二天
定位一下刷题计划:刷题全面——代码随想录过一遍,刷到模拟题——刷洛谷普及组-。所以还是每天刷一个代码随想录,外加两道洛谷,题目先从官方题单【算法1-1】开始。 一、P1042乒乓球 1.解题思路 关键点1:输入形式 输…...
如何在 Mac M4 芯片电脑上卸载高版本的 Node.js
文章目录 一、确认 Node.js 的安装方式二、卸载 Node.js 的通用步骤1. 通过官方安装包(.pkg)安装的 Node.js2. 通过 Homebrew 安装的 Node.js3. 通过 nvm 安装的 Node.js 三、验证是否卸载成功四、推荐使用 nvm 管理 Node.js 版本五、常见问题1. 卸载后仍…...
基础IO详解
FILE 1.FILE是文件的用户级数据结构,创建在堆上 2.FILE里有维护一个用户级缓冲区,这个用户级缓冲区是为了减少系统调用的次数 3.进程一般会有三个标准FILE*流,stdin,stdout,stderr,对应文件描述符一般是…...
QT入门基础
QT作为一个C的GUI框架,编程语法和C都差不多,上手还是比较快的。但是学习一个新的技术,总有一些新的概念是不清楚的,所以需要先了解一下这些概念。 1、QT软件系 QT:安装时会指定某个版本的QT,这个QT指QT库…...
【TI MSP430与SD NAND:心电监测的长续航解决方案】
在医疗科技飞速发展的今天,心电监测设备已成为守护人们心脏健康的关键防线。而在这一领域,Nordic、TI、ST、NXP 等行业巨头凭借其深厚的技术积累和创新精神,推出的主芯片与 SD NAND 存储组合方案,正引领着心电监测技术的变革&…...
中医方剂 - 理中汤
理中汤是中医经典方剂,出自《伤寒论》,由人参(或党参)、干姜、白术、炙甘草四味药组成。 一、核心功效与作用机理 1. 温中散寒(核心作用) 表现:脘腹冷痛、呕吐清水、腹泻完谷不化 现代对应&a…...
遨游三防科普:三防平板是什么?有什么特殊功能?
在极端环境作业与专业领域应用中,传统消费级电子设备往往因环境适应性不足而“折戟沉沙”。三防平板的诞生,正是为破解这一难题而生,它通过军用级防护标准与专业化功能设计,成为工业巡检、地质勘探、应急救援等场景的核心工具。所…...
关于数据仓库、数据湖、数据平台、数据中台和湖仓一体的概念和区别
我们谈论数据中台之前, 我们也听到过数据平台、数据仓库、数据湖、湖仓一体的相关概念,它们都与数据有关系,但他们和数据中台有什么样的区别, 下面我们将围绕数据平台、数据仓库、数据湖和数据中台的区别进行介绍。 一、相关概念…...
FPGA:CLB资源以及Verilog编码面积优化技巧
本文将先介绍Kintex-7系列器件的CLB(可配置逻辑块)资源,然后分享在Verilog编码时节省CLB资源的技巧。以下内容基于Kintex-7系列的架构特点,并结合实际设计经验进行阐述。 一、Kintex-7系列器件的CLB资源介绍 Kintex-7系列是Xilin…...
AUTOSAR AP 入门0:AUTOSAR_EXP_PlatformDesign.pdf
AUTOSAR AP官网:AUTOSAR Adaptive Platform设计AUTOSAR AP的目的,翻译版官方文档 AUTOSAR_EXP_PlatformDesign.pdf : https://mp.weixin.qq.com/s?__bizMzg2MzAyMDIzMQ&mid2247553050&idx2&sn786c3a1f153acf99b723bf4c9832acaf …...
WPF 常见坑:ContentControl 不绑定 Content 时,命令为何失效?
WPF 中的 Content“{Binding}” 到底有多重要?一次被忽视的绑定导致命令无法触发的案例分析 在使用 WPF 构建 UI 时,我们经常会使用 ContentControl、ItemsControl、DataTemplate 等机制进行灵活的界面布局。但很多开发者可能会在某些场景中遇到这样的问…...
【IC_Design】跨时钟域的寄存器更新后锁存
目录 设计逻辑框图场景概述总结电路使用注意事项***波形图代码 设计逻辑框图 场景概述 最典型的应用场景就是——在一个时钟域(比如 CPU/总线域)更新了一个多位配置字,需要把它安全地送到另一个时钟域(比如时钟发生器、串口、视频…...
腾讯2025年校招笔试真题手撕(三)
一、题目 今天正在进行赛车车队选拔,每一辆赛车都有一个不可以改变的速度。现在需要选取速度差距在10以内的车队(车队中速度的最大值减去最小值不大于10),用于迎宾。车队的选拔按照的是人越多越好的原则,给出n辆车的速…...
leetcode 83和84 Remove Duplicates from Sorted List 和leetcode 1836
目录 83. Remove Duplicates from Sorted List 82. Remove Duplicates from Sorted List II 1836. Remove Duplicates From an Unsorted Linked List 删除链表中的结点合集 83. Remove Duplicates from Sorted List 代码: /*** Definition for singly-linked l…...
【linux知识】sftp配置免密文件推送
SFTP配置免密文件推送 **一、配置 SFTP 用户****1. 创建系统用户(非登录用户)****2. 设置用户密码****3. 创建 SFTP 根目录并设置权限****4. 配置 SFTP 服务(修改 SSH 配置)****5. 重启 SSH 服务使配置生效** **二、免密 SFTP 文件…...
华为2025年校招笔试手撕真题教程(二)
一、题目 大湾区某城市地铁线路非常密集,乘客很难一眼看出选择哪条线路乘坐比较合适,为了解决这个问题,地铁公司希望你开发一个程序帮助乘客挑选合适的乘坐线路,使得乘坐时间最短,地铁公司可以提供的数据是各相邻站点…...
【Leetcode 每日一题】3362. 零数组变换 III
问题背景 给你一个长度为 n n n 的整数数组 n u m s nums nums 和一个二维数组 q u e r i e s queries queries,其中 q u e r i e s [ i ] [ l i , r i ] queries[i] [l_i, r_i] queries[i][li,ri]。 每一个 q u e r i e s [ i ] queries[i] queries[i]…...
JWT了解
JSON Web Token (JWT) 概述 JSON Web Token (JWT) 是一种开放标准(RFC 7519),用于在网络应用环境间安全地将信息作为JSON对象传输。它通常被用来在客户端和服务器之间传递声明,例如用户的身份验证信息,使得服务端可以…...
复杂项目中通过使用全局变量解决问题的思维方式
最近接手了一个公司的老系统的PHP项目,里面的代码比较混乱,排查解决了一个问题,决定将这个思路记录下来,希望能帮助更多的人。 其中一部分的代码信息如下: 备注:为了避免公司的相关数据信息暴露࿰…...
upload-labs通关笔记-第18关文件上传之条件竞争
目录 一、条件竞争 二、源码分析 1、源码分析 2、攻击原理 3、渗透思路 三、实战渗透 1、构造脚本 2、获取上传脚本URL 3、构造访问母狼脚本的Python代码 4、bp不断并发上传母狼脚本 (1)开启专业版bp (2) 上传母狼脚本…...
华为Cangjie编程技术深度解析(续篇1)
华为Cangjie编程技术深度解析(续篇) 第六章 分布式运行时深度剖析 6.1 设备虚拟化引擎 Cangjie设备抽象层(DAL)原理 // 设备能力声明式描述 @DeviceProfile(id = "AGV-0023",capabilities = {mobility: { speed: 1.5m/s, payload: 50kg },sensors: [lidar, t…...
WordPress AI插件 新增支持一键批量自动生成WooCommerce 产品描述、产品图、产品评论
Linkreate wordpressAI智能插件-自动化运营网站 文章生成与优化|多语言文章生成|关键词生成与分类管理|内容采集与管理|定时任务与自动|多任务后台运行|API集成与AI客服|媒体生成功能 一款可以24小时自动发布原创文章的WordPress插件,支持AI根据已有的长尾关键词、关…...
如何测试JWT的安全性:全面防御JSON Web Token的安全漏洞
在当今的Web应用安全领域,JSON Web Token(JWT)已成为身份认证的主流方案,但OWASP统计显示,错误配置的JWT导致的安全事件占比高达42%。本文将系统性地介绍JWT安全测试的方法论,通过真实案例剖析典型漏洞,帮助我们构建全…...
华为昇腾开发——多模型资源管理(C++)
使用ACLLite进行多模型资源管理(C++实现) 在使用Ascend ACL(Ascend Computing Language)的ACLLite库进行多模型推理时,合理的资源管理至关重要。以下是如何在C++中实现多模型资源管理的方案: 1. 资源管理基础 首先,我们需要理解Ascend平台的关键资源: 设备(Device)资…...
【开源解析】基于深度学习的双色球预测系统:从数据获取到可视化分析
基于深度学习的双色球预测系统:从数据获取到可视化分析 🌈 个人主页:创客白泽 - CSDN博客 🔥 系列专栏:🐍《Python开源项目实战》 💡 热爱不止于代码,热情源自每一个灵感闪现的夜晚。…...
【RAG】ragflow源码亮点:文档embedding向量化加权融合
引言: 最近在看ragflow源码,其中有一个较为巧妙地设计:分别将 文字 、 标题 行向量化 之后,直接根据权重,进行加法运算,得到向量融合,增强了文本向量化的表示能力,这里开始讨论一下…...
vue3+element-plus+pinia完整搭建好看简洁的管理后台
目录 一、项目介绍 二、项目结构 1.vscode的项目截图 2.项目依赖 三、项目截图 1.登录页 2.首页 3.汽车管理 4.汽车信息 5.系统管理 6.订单管理 7.数据统计 8.个人中心 四、源码分析 1.数据存储与同步 2.汽车信息 3.框架布局 五、总结 一、项目介绍 项目使用…...
新手到资深的Java开发编码规范
新手到资深的开发编码规范 一、前言二、命名规范:代码的 “第一印象”2.1 标识符命名原则2.2 命名的 “自描述性” 原则2.3 避免魔法值 三、代码格式规范:结构清晰的视觉美学3.1 缩进与空格3.2 代码块规范3.3 换行与断行 四、注释规范:代码的…...
Docker架构详解
一,Docker的四大要素:Dockerfile、镜像(image)、容器(container)、仓库(repository) 1.dockerfile:在dockerfile文件中写构建docker的命令,通过dockerbuild构建image 2.镜像:就是一个只读的模板,镜像可以用来创建docker容器&…...
VS Code中Maven未能正确读取`settings.xml`中配置的新路径
在VS Code中Maven未能正确读取settings.xml中配置的新路径,通常是由于以下原因导致的: 一、VS Code未使用你修改的settings.xml文件 VS Code的Maven插件可能使用了默认配置或指向其他settings.xml文件。解决方法: 手动指定settings.xml路径…...
Spring Boot 注解 @ConditionalOnMissingBean是什么
一句话总结: ConditionalOnMissingBean 是 Spring Boot 提供的一个 条件注解(Conditional Annotation),意思是: 只有当 Spring 容器中 不存在 某个 Bean 时,当前的 Bean 或配置才会被加载。 这是一种典型的…...
labview实现LED流水灯的第二种方法
LED流水灯的描述:写一个跑马灯程序,7个灯从左到右不停的轮流点亮,闪烁间隔由滑动条调节,并尝试拓展到任意个LED灯。 在前面的文章中,我们提到了使用labview实现LED流水灯的第一种方法。这篇文章来介绍一下实现LED流水灯的第二种方…...
Katoolin3 项目介绍:在 Ubuntu 上轻松安装 Kali Linux 工具
引言 在网络安全和渗透测试领域,Kali Linux 以其丰富的工具集成为首选操作系统。然而,Kali Linux 作为一个专为安全研究设计的系统,可能不适合日常使用或服务器环境(如 Ubuntu VPS)。Katoolin3 是一个强大的 Python 脚…...
labview设计一个虚拟信号发生器
目标:设计一个虚拟信号发生器,通过功能键的设置可以产生正弦波、三角波、方波和锯齿波,并可以通过输入控件设置采集信号的频率、幅值、相位等参数。 一、正弦波 (1)创建一个枚举 (2)点击属性后…...
java I/O
文件字符流 字符流不同于字节,字符流是以一个具体的字符进行读取,因此它只适合读纯文本的文件,如果是其他类型的文件不适用。 字节流;英文1个字节,中文3个字节。 字符流:中英文都是2个字节 public static…...