PHP 源码 — intval 函数源码分析

PHP 中的 intval

  1. intval ( mixed $var [, int $base = 10 ] ) : int
  • 它的作用是将变量转换为整数值。其第二个参数 $base 用的不是很多。它代表转化所使用的进制。默认是 10 进制
  • 可以通过如下简单示例,了解如何使用它:
  1. $var1 = '123';
  2. $var2 = '-123';
  3. $var3 = [1, 2, ];
  4. $var4 = [-1, 2, ];
  5. var_dump(
  6. intval($var1),
  7. intval($var2),
  8. intval($var3),
  9. intval($var4)
  10. );
  11. // 输出如下:
  12. // int(-123)
  13. // int(1)
  14. // int(1)
  • 这个函数不是从 100 个函数中选出来的,而是偶然的在 LeetCode 刷题,碰到将字符串转换为数字的算法题中得到的想法,PHP 有 intval,其底层是如何实现的呢?

intval 实现源码

  • 函数 intval 在位于 php-7.3.3/ext/standard/type.c 中,可以点击查看
  • 函数源码不多,直接贴出:
  1. PHP_FUNCTION(intval)
  2. {
  3. zval *num;
  4. zend_long base = 10;
  5. ZEND_PARSE_PARAMETERS_START(1, 2)
  6. Z_PARAM_ZVAL(num)
  7. Z_PARAM_OPTIONAL
  8. Z_PARAM_LONG(base)
  9. ZEND_PARSE_PARAMETERS_END();
  10. if (Z_TYPE_P(num) != IS_STRING || base == 10) {
  11. RETVAL_LONG(zval_get_long(num));
  12. return;
  13. }
  14. if (base == 0 || base == 2) {
  15. char *strval = Z_STRVAL_P(num);
  16. size_t strlen = Z_STRLEN_P(num);
  17. while (isspace(*strval) && strlen) {
  18. strval++;
  19. strlen--;
  20. }
  21. /* Length of 3+ covers "0b#" and "-0b" (which results in 0) */
  22. if (strlen > 2) {
  23. int offset = 0;
  24. if (strval[0] == '-' || strval[0] == '+') {
  25. offset = 1;
  26. }
  27. if (strval[offset] == '0' && (strval[offset + 1] == 'b' || strval[offset + 1] == 'B')) {
  28. char *tmpval;
  29. strlen -= 2; /* Removing "0b" */
  30. tmpval = emalloc(strlen + 1);
  31. /* Place the unary symbol at pos 0 if there was one */
  32. if (offset) {
  33. tmpval[0] = strval[0];
  34. }
  35. /* Copy the data from after "0b" to the end of the buffer */
  36. memcpy(tmpval + offset, strval + offset + 2, strlen - offset);
  37. tmpval[strlen] = 0;
  38. RETVAL_LONG(ZEND_STRTOL(tmpval, NULL, 2));
  39. efree(tmpval);
  40. return;
  41. }
  42. }
  43. }
  44. RETVAL_LONG(ZEND_STRTOL(Z_STRVAL_P(num), NULL, base));
  45. }
  • 从PHP 用户态的角度看,intval 函数原型中,输入参数 $var 变量类型是 mixed,这也就意味着,输入参数可以是 PHP 中的任意一种类型,包括整形、字符串、数组、对象等。因此,在源码中直接使用 zval 接收输入参数 zval *num;

十进制的情况

  • 源码中,大部分的内容是针对非 10 进制的处理。我们先着重看一下 10 进制的情况。对数据转化为 10 进制的整数时,源码所做处理如下:
  1. if (Z_TYPE_P(num) != IS_STRING || base == 10) {
  2. RETVAL_LONG(zval_get_long(num));
  3. return;
  4. }
  5. static zend_always_inline zend_long zval_get_long(zval *op) {
  6. return EXPECTED(Z_TYPE_P(op) == IS_LONG) ? Z_LVAL_P(op) : zval_get_long_func(op);
  7. }
  8. ZEND_API zend_long ZEND_FASTCALL zval_get_long_func(zval *op) /* {{{ */
  9. {
  10. return _zval_get_long_func_ex(op, 1);
  11. }
  • 只要传入的数据不是整数情况,那么源码中最终会调用 _zval_get_long_func_ex(op, 1);。在这个函数中,处理了各种 PHP 用户态参数类型的情况:
  1. switch (Z_TYPE_P(op)) {
  2. case IS_UNDEF:
  3. case IS_NULL:
  4. case IS_FALSE:
  5. return 0;
  6. case IS_TRUE:
  7. return 1;
  8. case IS_RESOURCE:
  9. return Z_RES_HANDLE_P(op);
  10. case IS_LONG:
  11. return Z_LVAL_P(op);
  12. case IS_DOUBLE:
  13. return zend_dval_to_lval(Z_DVAL_P(op));
  14. case IS_STRING:
  15. // 略 ……
  16. case IS_ARRAY:
  17. return zend_hash_num_elements(Z_ARRVAL_P(op)) ? 1 : 0;
  18. case IS_OBJECT:
  19. // 略 ……
  20. case IS_REFERENCE:
  21. op = Z_REFVAL_P(op);
  22. goto try_again;
  23. EMPTY_SWITCH_DEFAULT_CASE()
  24. }
  • 通过 switch 语句的不同分支对不同类型做了各种不同的处理:

    • 如果传入的类型是“空”类型,则 intval 函数直接返回 0;
    • 如果是 true,返回 1
    • 如果是数组,空数组时返回 0;非空数组,则返回 1
    • 如果是字符串,则进一步处理
    • ……
  • 按照本文的初衷,就是要了解一下如何将字符串转化为整形数据,因此我们着重看字符串的情况:

  1. {
  2. zend_uchar type;
  3. zend_long lval;
  4. double dval;
  5. if (0 == (type = is_numeric_string(Z_STRVAL_P(op), Z_STRLEN_P(op), &lval, &dval, silent ? 1 : -1))) {
  6. if (!silent) {
  7. zend_error(E_WARNING, "A non-numeric value encountered");
  8. }
  9. return 0;
  10. } else if (EXPECTED(type == IS_LONG)) {
  11. return lval;
  12. } else {
  13. /* Previously we used strtol here, not is_numeric_string,
  14. * and strtol gives you LONG_MAX/_MIN on overflow.
  15. * We use use saturating conversion to emulate strtol()'s
  16. * behaviour.
  17. */
  18. return zend_dval_to_lval_cap(dval);
  19. }
  20. }
  1. static zend_always_inline zend_uchar is_numeric_string(const char *str, size_t length, zend_long *lval, double *dval, int allow_errors) {
  2. return is_numeric_string_ex(str, length, lval, dval, allow_errors, NULL);
  3. }
  4. 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)
  5. {
  6. if (*str > '9') {
  7. return 0;
  8. }
  9. return _is_numeric_string_ex(str, length, lval, dval, allow_errors, oflow_info);
  10. }
  11. 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' 的字符

空白符号处理

  • 源码中的处理如下:
  1. while (*str == ' ' || *str == '\t' || *str == '\n' || *str == '\r' || *str == '\v' || *str == '\f') {
  2. str++;
  3. length--;
  4. }
  • \n\t\r 这几个用的多一些。\v 是指竖向跳格;\f 是换页符。针对这种空白符,不做处理,选择跳过。然后使用指针运算 str++ 指向下一个字符

正、负号的处理

  • 由于正、负号在数值中是有意义的,因此需要保留,但是数值中 + 号是可以省略的:
  1. if (*ptr == '-') {
  2. neg = 1;
  3. ptr++;
  4. } else if (*ptr == '+') {
  5. ptr++;
  6. }

跳过任意个字符 0

  • 因为十进制数值前的 0 值是没有意义的,因此需要跳过:
  1. while (*ptr == '0') {
  2. ptr++;
  3. }
  • 处理完以上的 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
  1. process_double:
  2. type = IS_DOUBLE;
  3. /* If there's a dval, do the conversion; else continue checking
  4. * the digits if we need to check for a full match */
  5. if (dval) {
  6. local_dval = zend_strtod(str, &ptr);
  7. } else if (allow_errors != 1 && dp_or_e != -1) {
  8. dp_or_e = (*ptr++ == '.') ? 1 : 2;
  9. goto check_digits;
  10. }
  • _is_numeric_string_ex 函数最后会将得到的浮点数返回:
  1. if (dval) {
  2. *dval = local_dval;
  3. }
  4. return IS_DOUBLE;
  • 浮点数的值被赋给 dval 指针。并将数据标识 IS_DOUBLE 返回。
  • 随后执行栈跳转回函数 _zval_get_long_func_ex 继续执行,也就是 return zend_dval_to_lval_cap(dval);。该函数定义如下:
  1. static zend_always_inline zend_long zend_dval_to_lval_cap(double d)
  2. {
  3. if (UNEXPECTED(!zend_finite(d)) || UNEXPECTED(zend_isnan(d))) {
  4. return 0;
  5. } else if (!ZEND_DOUBLE_FITS_LONG(d)) {
  6. return (d > 0 ? ZEND_LONG_MAX : ZEND_LONG_MIN);
  7. }
  8. return (zend_long)d;
  9. }
  • 也就是说,从浮点数到整数,是底层进行了类型强制转换的结果:(zend_long)d

结语

  • PHP 底层将很多小段逻辑进行了封装,很大程度的提高了代码复用性。但也给源码的维护和学习带来了一些额外的成本。一个类型转换的函数就进行了 10 余种函数调用。
  • 下一篇,将进行 intval 底层相关的扩展实践。敬请期待。
  • 如果你有更好的想法,欢迎给我提意见和建议。

PHP 源码 — intval 函数源码分析的更多相关文章

  1. PHP 源码 —— is_array 函数源码分析

    is_array 函数源码分析 本文首发于 https://github.com/suhanyujie/learn-computer/blob/master/src/function/array/is ...

  2. Vue中之nextTick函数源码分析

    Vue中之nextTick函数源码分析 1. 什么是Vue.nextTick()?官方文档解释如下:在下次DOM更新循环结束之后执行的延迟回调.在修改数据之后立即使用这个方法,获取更新后的DOM. 2 ...

  3. 序列化器中钩子函数源码分析、many关键字源码分析

    局部钩子和全局钩子源码分析(2星) # 入口是 ser.is_valid(),是BaseSerializer的方法 # 最核心的代码 self._validated_data = self.run_v ...

  4. 【C++】【源码解读】std::is_same函数源码解读

    std::is_same使用很简单 重点在于对源码的解读 参考下面一句静态断言: static_assert(!std::is_same<bool, T>::value, "ve ...

  5. lodash框架中的chunk与drop函数源码逐行分析

    lodash是一个工具库,跟underscore差不多 chunk函数的作用: 把一维数组,按照固定的长度分段成二维数组 如: chunk( [ 10, 20, 30, 40 ], 2 )     结 ...

  6. Spark GraphX的函数源码分析及应用实例

    1. outerJoinVertices函数 首先给出源代码 override def outerJoinVertices[U: ClassTag, VD2: ClassTag] (other: RD ...

  7. 巡风视图函数源码学习--view.py

    记录一下巡风扫描器view.py这个脚本里的视图函数的学习,直接在代码里面做的注释,里面有一些print 代码是为了把数据打印出来小白我自己加的,勿怪勿怪.可能存在一些理解错误和不到位的地方,希望大佬 ...

  8. mongodb操作:利用javaScript封装db.collection.find()后可调用函数源码解读

    { "_mongo" : connection to YOURIP:27017{ SSL: { sslSupport: false, sslPEMKeyFile: "&q ...

  9. python 内置函数源码查看

    如果是用python 实现的模块可以直接在IDE里面追踪到源码 也可以使用help内置函数,例如: help(os) 如果是c 语言实现的模块,则不能直接在IDE里面查看,如果直接在IDE里面查看,会 ...

随机推荐

  1. openlayers图层加标注

    <!DOCTYPE html> <html> <head> <meta charset="utf-8" /> <title&g ...

  2. 神经网络反向传播算法&&卷积神经网络

    听一遍课程之后,我并不太明白这个算法的奇妙之处?? 为啥? 神经网络反向传播算法 神经网络的训练依靠反向传播算法,最开始输入层输入特征向量,网络层计算获得输出,输出层发现输出和正确的类号不一样,这时就 ...

  3. gulp常用插件之gulp-notify使用

    更多gulp常用插件使用请访问:gulp常用插件汇总 gulp-notify这是一款gulp通知插件. 更多使用文档请点击访问gulp-notify工具官网. 安装 一键安装不多解释 npm inst ...

  4. hdu 1005 Number Sequence(循环节)

    题意,f(1)=1,f(2)=1,f(n)=a*f(n-1)+b*f(n-2),求f(n)%7 这个题可能数据不够严谨,所以有些错误的做法也可以通过,比如7 7 50,应该输出0而不是1 解:找到关键 ...

  5. C++11智能指针(unique_ptr、shared_ptr、weak_ptr)(转)

    原文地址:https://blog.csdn.net/king_way/article/details/95536938

  6. 为什么SSL证书要设有效期?

    1.首先是为了安全考虑,CA机构不能保证一个网站永远是合法的,因此它需要定期检查网站. 2.其次,以往CA证书都非常贵,签发证书的机构通过设置期限来收费,是一种商业途径. 3.最后,还有最重要的原因就 ...

  7. 【巨杉数据库SequoiaDB】企业级和开源领域“两开花”,巨杉引领国产数据库创新

    2019年12月15日,OSC 源创会·年终盛典在深圳圆满举行.巨杉数据库作为业界领先的金融级分布式数据库厂商, 获得 “2019年开源数据库先锋企业” 及 “2019 GVP-Gitee最有价值开源 ...

  8. IOU 选框和真实框重叠部分占两个总框并集的比例

    IOU 选框和真实框重叠部分占两个总框并集的比例 IOU 召回率:表示在预测为的正类中,有多少正类被预测为正类 https://blog.csdn.net/qq_36653505/article/de ...

  9. [ZJOI2007] 矩阵游戏 - 二分图匹配

    题意:问一个\(0-1\)方阵是不是非奇异的 其实我真的很想求行列式 #include <bits/stdc++.h> using namespace std; #define N 505 ...

  10. 我的翻译--针对Outernet卫星信号的逆向工程

    前言 Outernet[1]是一家旨在让访问国际互联网更加方便自由的公司,他们使用卫星来广播维基百科或者其他网站.目前,他们的广播主要使用三颗国际海事卫星[3]的L波段[2],使其广播覆盖全球,大多数 ...