PHP 源码 — intval 函数源码分析
PHP 源码 — intval 函数源码分析
- 文章来源: https://github.com/suhanyujie/learn-computer/
- 作者:suhanyujie
- 基于PHP 7.3.3
PHP 中的 intval
- intval 函数的签名从官方文档可见:
intval ( mixed $var [, int $base = 10 ] ) : int
- 它的作用是将变量转换为整数值。其第二个参数
$base
用的不是很多。它代表转化所使用的进制。默认是 10 进制 - 可以通过如下简单示例,了解如何使用它:
$var1 = '123';
$var2 = '-123';
$var3 = [1, 2, ];
$var4 = [-1, 2, ];
var_dump(
intval($var1),
intval($var2),
intval($var3),
intval($var4)
);
// 输出如下:
// int(-123)
// int(1)
// int(1)
- 这个函数不是从 100 个函数中选出来的,而是偶然的在 LeetCode 刷题,碰到将字符串转换为数字的算法题中得到的想法,PHP 有 intval,其底层是如何实现的呢?
intval 实现源码
- 函数 intval 在位于
php-7.3.3/ext/standard/type.c
中,可以点击查看 - 函数源码不多,直接贴出:
PHP_FUNCTION(intval)
{
zval *num;
zend_long base = 10;
ZEND_PARSE_PARAMETERS_START(1, 2)
Z_PARAM_ZVAL(num)
Z_PARAM_OPTIONAL
Z_PARAM_LONG(base)
ZEND_PARSE_PARAMETERS_END();
if (Z_TYPE_P(num) != IS_STRING || base == 10) {
RETVAL_LONG(zval_get_long(num));
return;
}
if (base == 0 || base == 2) {
char *strval = Z_STRVAL_P(num);
size_t strlen = Z_STRLEN_P(num);
while (isspace(*strval) && strlen) {
strval++;
strlen--;
}
/* Length of 3+ covers "0b#" and "-0b" (which results in 0) */
if (strlen > 2) {
int offset = 0;
if (strval[0] == '-' || strval[0] == '+') {
offset = 1;
}
if (strval[offset] == '0' && (strval[offset + 1] == 'b' || strval[offset + 1] == 'B')) {
char *tmpval;
strlen -= 2; /* Removing "0b" */
tmpval = emalloc(strlen + 1);
/* Place the unary symbol at pos 0 if there was one */
if (offset) {
tmpval[0] = strval[0];
}
/* Copy the data from after "0b" to the end of the buffer */
memcpy(tmpval + offset, strval + offset + 2, strlen - offset);
tmpval[strlen] = 0;
RETVAL_LONG(ZEND_STRTOL(tmpval, NULL, 2));
efree(tmpval);
return;
}
}
}
RETVAL_LONG(ZEND_STRTOL(Z_STRVAL_P(num), NULL, base));
}
- 从PHP 用户态的角度看,intval 函数原型中,输入参数
$var
变量类型是mixed
,这也就意味着,输入参数可以是 PHP 中的任意一种类型,包括整形、字符串、数组、对象等。因此,在源码中直接使用 zval 接收输入参数zval *num;
十进制的情况
- 源码中,大部分的内容是针对非 10 进制的处理。我们先着重看一下 10 进制的情况。对数据转化为 10 进制的整数时,源码所做处理如下:
if (Z_TYPE_P(num) != IS_STRING || base == 10) {
RETVAL_LONG(zval_get_long(num));
return;
}
static zend_always_inline zend_long zval_get_long(zval *op) {
return EXPECTED(Z_TYPE_P(op) == IS_LONG) ? Z_LVAL_P(op) : zval_get_long_func(op);
}
ZEND_API zend_long ZEND_FASTCALL zval_get_long_func(zval *op) /* {{{ */
{
return _zval_get_long_func_ex(op, 1);
}
- 只要传入的数据不是整数情况,那么源码中最终会调用
_zval_get_long_func_ex(op, 1);
。在这个函数中,处理了各种 PHP 用户态参数类型的情况:
switch (Z_TYPE_P(op)) {
case IS_UNDEF:
case IS_NULL:
case IS_FALSE:
return 0;
case IS_TRUE:
return 1;
case IS_RESOURCE:
return Z_RES_HANDLE_P(op);
case IS_LONG:
return Z_LVAL_P(op);
case IS_DOUBLE:
return zend_dval_to_lval(Z_DVAL_P(op));
case IS_STRING:
// 略 ……
case IS_ARRAY:
return zend_hash_num_elements(Z_ARRVAL_P(op)) ? 1 : 0;
case IS_OBJECT:
// 略 ……
case IS_REFERENCE:
op = Z_REFVAL_P(op);
goto try_again;
EMPTY_SWITCH_DEFAULT_CASE()
}
通过 switch 语句的不同分支对不同类型做了各种不同的处理:
- 如果传入的类型是“空”类型,则 intval 函数直接返回 0;
- 如果是 true,返回 1
- 如果是数组,空数组时返回 0;非空数组,则返回 1
- 如果是字符串,则进一步处理
- ……
按照本文的初衷,就是要了解一下如何将字符串转化为整形数据,因此我们着重看字符串的情况:
{
zend_uchar type;
zend_long lval;
double dval;
if (0 == (type = is_numeric_string(Z_STRVAL_P(op), Z_STRLEN_P(op), &lval, &dval, silent ? 1 : -1))) {
if (!silent) {
zend_error(E_WARNING, "A non-numeric value encountered");
}
return 0;
} else if (EXPECTED(type == IS_LONG)) {
return lval;
} else {
/* Previously we used strtol here, not is_numeric_string,
* and strtol gives you LONG_MAX/_MIN on overflow.
* We use use saturating conversion to emulate strtol()'s
* behaviour.
*/
return zend_dval_to_lval_cap(dval);
}
}
static zend_always_inline zend_uchar is_numeric_string(const char *str, size_t length, zend_long *lval, double *dval, int allow_errors) {
return is_numeric_string_ex(str, length, lval, dval, allow_errors, NULL);
}
static zend_always_inline zend_uchar is_numeric_string_ex(const char *str, size_t length, zend_long *lval, double *dval, int allow_errors, int *oflow_info)
{
if (*str > '9') {
return 0;
}
return _is_numeric_string_ex(str, length, lval, dval, allow_errors, oflow_info);
}
ZEND_API zend_uchar ZEND_FASTCALL _is_numeric_string_ex(const char *str, size_t length, zend_long *lval, double *dval, int allow_errors, int *oflow_info) { // ... }
- 而在这段逻辑里,最能体现字符串转整形算法的还是隐藏在
is_numeric_string(Z_STRVAL_P(op), Z_STRLEN_P(op), &lval, &dval, silent ? 1 : -1)
背后的函数调用,也就是函数_is_numeric_string_ex
- 对于一段字符串,将其转为整形,我们的规则一般如下:
- 去除前面的空格字符,包括空格、换行、制表符等
- 妥善处理字符串前面的
+/-
符号 - 处理靠前的
'0'
字符,比如字符串'001a'
,转换为整形后,就是1
,去除了前面的'0'
字符 - 处理余下的字符串中前几位是数字字符串的值,并抛弃非数字字符。所谓数字字符,就是
'0'-'9'
的字符
空白符号处理
- 源码中的处理如下:
while (*str == ' ' || *str == '\t' || *str == '\n' || *str == '\r' || *str == '\v' || *str == '\f') {
str++;
length--;
}
\n
、\t
、\r
这几个用的多一些。\v
是指竖向跳格;\f
是换页符。针对这种空白符,不做处理,选择跳过。然后使用指针运算str++
指向下一个字符
正、负号的处理
- 由于正、负号在数值中是有意义的,因此需要保留,但是数值中
+
号是可以省略的:
if (*ptr == '-') {
neg = 1;
ptr++;
} else if (*ptr == '+') {
ptr++;
}
跳过任意个字符 0
- 因为十进制数值前的 0 值是没有意义的,因此需要跳过:
while (*ptr == '0') {
ptr++;
}
处理完以上的 3 种情况后,就会对接下里的字符逐个转换为整数。由于最先遍历到的字符数字是处于高位的,所以在计算下一个字符前,需要对之前的数值
*10
操作。举例说明:- 对于字符串
231aa
,遍历到第一个字符'2'
时,将其作为临时值存储到变量 tmp 中 - 第二次遍历到
'3'
,需要*10
,也就是tmp * 10 + 3
,此时 tmp 值为 23 - 第三次遍历到
'1'
,需要tmp * 10 + 1
,此时 tmp 值为 231。
- 对于字符串
因此,源码中判断字符是否是数字字符:
ZEND_IS_DIGIT(*ptr)
,是的话则按照上述方式计算
- ZEND_IS_DIGIT 宏的实现是
((c) >= '0' && (c) <= '9')
,位于'0'
和'9'
之间的字符就是我们需要找的数字字符。
小数的情况
_is_numeric_string_ex
函数在底层会被多种 PHP 函数调用,包括floatval
。如果在遍历字符串的字符时,遇到小数点该如何处理呢?个人观点看,由于我们要实现的是intval
函数,所以我觉得遇到小数点时,可以将其当作非数字字符来处理。例如"3.14abc"
字符串,intval 之后就直接是 3。然而实际上,_is_numeric_string_ex
的实现不是这样的,因为它是一个通用函数。在遇到小数点时,有一些特殊处理:- 在遇到小数点的情况下,c 会进行 goto 跳转,跳转到
process_double
:
process_double:
type = IS_DOUBLE;
/* If there's a dval, do the conversion; else continue checking
* the digits if we need to check for a full match */
if (dval) {
local_dval = zend_strtod(str, &ptr);
} else if (allow_errors != 1 && dp_or_e != -1) {
dp_or_e = (*ptr++ == '.') ? 1 : 2;
goto check_digits;
}
_is_numeric_string_ex
函数最后会将得到的浮点数返回:
if (dval) {
*dval = local_dval;
}
return IS_DOUBLE;
- 浮点数的值被赋给
dval
指针。并将数据标识IS_DOUBLE
返回。 - 随后执行栈跳转回函数
_zval_get_long_func_ex
继续执行,也就是return zend_dval_to_lval_cap(dval);
。该函数定义如下:
static zend_always_inline zend_long zend_dval_to_lval_cap(double d)
{
if (UNEXPECTED(!zend_finite(d)) || UNEXPECTED(zend_isnan(d))) {
return 0;
} else if (!ZEND_DOUBLE_FITS_LONG(d)) {
return (d > 0 ? ZEND_LONG_MAX : ZEND_LONG_MIN);
}
return (zend_long)d;
}
- 也就是说,从浮点数到整数,是底层进行了类型强制转换的结果:
(zend_long)d
。
结语
- PHP 底层将很多小段逻辑进行了封装,很大程度的提高了代码复用性。但也给源码的维护和学习带来了一些额外的成本。一个类型转换的函数就进行了 10 余种函数调用。
- 下一篇,将进行 intval 底层相关的扩展实践。敬请期待。
- 如果你有更好的想法,欢迎给我提意见和建议。
PHP 源码 — intval 函数源码分析的更多相关文章
- PHP 源码 —— is_array 函数源码分析
is_array 函数源码分析 本文首发于 https://github.com/suhanyujie/learn-computer/blob/master/src/function/array/is ...
- Vue中之nextTick函数源码分析
Vue中之nextTick函数源码分析 1. 什么是Vue.nextTick()?官方文档解释如下:在下次DOM更新循环结束之后执行的延迟回调.在修改数据之后立即使用这个方法,获取更新后的DOM. 2 ...
- 序列化器中钩子函数源码分析、many关键字源码分析
局部钩子和全局钩子源码分析(2星) # 入口是 ser.is_valid(),是BaseSerializer的方法 # 最核心的代码 self._validated_data = self.run_v ...
- 【C++】【源码解读】std::is_same函数源码解读
std::is_same使用很简单 重点在于对源码的解读 参考下面一句静态断言: static_assert(!std::is_same<bool, T>::value, "ve ...
- lodash框架中的chunk与drop函数源码逐行分析
lodash是一个工具库,跟underscore差不多 chunk函数的作用: 把一维数组,按照固定的长度分段成二维数组 如: chunk( [ 10, 20, 30, 40 ], 2 ) 结 ...
- Spark GraphX的函数源码分析及应用实例
1. outerJoinVertices函数 首先给出源代码 override def outerJoinVertices[U: ClassTag, VD2: ClassTag] (other: RD ...
- 巡风视图函数源码学习--view.py
记录一下巡风扫描器view.py这个脚本里的视图函数的学习,直接在代码里面做的注释,里面有一些print 代码是为了把数据打印出来小白我自己加的,勿怪勿怪.可能存在一些理解错误和不到位的地方,希望大佬 ...
- mongodb操作:利用javaScript封装db.collection.find()后可调用函数源码解读
{ "_mongo" : connection to YOURIP:27017{ SSL: { sslSupport: false, sslPEMKeyFile: "&q ...
- python 内置函数源码查看
如果是用python 实现的模块可以直接在IDE里面追踪到源码 也可以使用help内置函数,例如: help(os) 如果是c 语言实现的模块,则不能直接在IDE里面查看,如果直接在IDE里面查看,会 ...
随机推荐
- PWA - service worker - Workbox(未完)
Get Started(开始) 只有get请求才能cache缓存吗? Create and Register a Service Worker File(创建和注册 Service Worker) B ...
- iframe刷新另一个iframe
如果是程序: Response.Write("<script language=javascript>"); Response.Write ...
- 08-SV面向对象编程的高级技巧指南
1.原始类与扩展类 (1)原始类被称为父类或者超类,扩展类被称为派生类或者子类.扩展类可以直接访问原始类和其本身的所有变量,应该将原始类中的子程序定义成虚拟的,这样它们就可以在扩展类中重定义.new函 ...
- springBoot代码和依赖分开打包
目的: 1.分离代码和依赖(包括第三方的引入包) 2.分离配置文件到外部 3.修改/META-INF/MANIFEST.MF中加载依赖包的位置和启动类,包含第三方依赖jar包 先查看项目的结构 由于我 ...
- 【算法】用两个栈来实现一个队列,完成队列的Push和Pop操作。 队列中的元素为int类型。
public class Solution { Stack<Integer> stack1 = new Stack<Integer>(); Stack<Integer&g ...
- C#接口与抽象类学习笔记
本笔记摘抄自:https://www.cnblogs.com/solan/archive/2012/08/01/CSharp06.html,记录一下学习过程以备后续查用. 摘要: 抽象类:是一种特殊的 ...
- nginx技术
Nginx 处理高并发,单台服务器存在服务瓶颈 Nginx属于nio ,noblocking Io非阻塞式的 Apache属于Bio,Blocking IO 阻塞式的 安装部分 依赖安装:yum -y ...
- [POI2015]PUS [线段树优化建图]
problem 线段树优化建图,拓扑,没了. #include <bits/stdc++.h> #define ls(x) ch[x][0] #define rs(x) ch[x][1] ...
- 强烈推荐一款强大的公式编辑器软件AxMath
Axmath教程请移步:https://www.cnblogs.com/coco56/p/11759578.html
- [tensorflow] tf.gather使用方法
tf.gather:用一个一维的索引数组,将张量中对应索引的向量提取出来 import tensorflow as tf a = tf.Variable([[1,2,3,4,5], [6,7,8,9, ...