PHP 源碼 — intval 函數源碼分析(算法:字符串轉換爲整形)

PHP 中的 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 語句的不一樣分支對不一樣類型作了各類不一樣的處理:php

    • 若是傳入的類型是「空」類型,則 intval 函數直接返回 0;
    • 若是是 true,返回 1
    • 若是是數組,空數組時返回 0;非空數組,則返回 1
    • 若是是字符串,則進一步處理
    • ……
  • 按照本文的初衷,就是要了解一下如何將字符串轉化爲整形數據,所以咱們着重看字符串的狀況:git

{
	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 操做。舉例說明:github

    • 對於字符串 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 底層相關的擴展實踐。敬請期待。
  • 若是你有更好的想法,歡迎給我提意見和建議。
相關文章
相關標籤/搜索