PHP 原始碼 — intval 函式原始碼分析(演演算法:字串轉換為整形)
阿新 • • 發佈:2019-12-31
- 文章來源: github.com/suhanyujie/…
- 作者: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,];
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),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,int allow_errors,int *oflow_info)
{
if (*str > '9') {
return 0;
}
return _is_numeric_string_ex(str,oflow_info);
}
ZEND_API zend_uchar ZEND_FASTCALL _is_numeric_string_ex(const char *str,int *oflow_info) { // ... }
複製程式碼
- 而在這段邏輯裡,最能體現字串轉整形演演算法的還是隱藏在
is_numeric_string(Z_STRVAL_P(op),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 底層相關的擴充套件實踐。敬請期待。
- 如果你有更好的想法,歡迎給我提意見和建議。