PHP_FUNCTION(basename)、dirname、pathinfo

basename

proto string basename(string path [, string suffix])
Returns the filename component of the path

返回path里的文件名,调用了php_basename函数

最外面是个while遍历字串,获取当前字符长度,不同长度有不同处理

inc_len = (*c == ‘\0’ ? 1: php_mblen(c, cnt));

php_mblen在php_string.h里有定义,依赖环境,有mb和没有mb处理是不一样的,返回结果也是不一样的。

只有inc_len是1的时候(正常的半角字符?),会正常处理,所以最好不要传入包含全角字符的path,在不了解机器环境情况下,不知道会返回什么结果。

下面是去掉win环境的代码:

if (*c == '/') {
	if (state == 1) {
		state = 0;
		cend = c;
	}
} else {
	if (state == 0) {
		comp = c;
		state = 1;
	}
}

这这个ifelse可以看出,这几个变量作用:

state:0=遇到目录分隔符 1=非分隔符
comp上个分隔符后面那个指针位置
cend最近一个分隔符的指针位置

实际上,循环内是就是得到最后一个文件名的头尾指针位置,如果指针能倒过来遍历就更快了,可惜不能,所以只能从头开始遍历到最后。

循环外:

if (state == 1) {
cend = c;
}
遍历完,如果state=1,也就是说最后一个字符不是分隔符,那么将cend指针置为字串最后一个位置

下面是处理有传入文件后缀的情况:

if (suffix != NULL && sufflen < (uint)(cend - comp) &&
			memcmp(cend - sufflen, suffix, sufflen) == 0) {
		cend -= sufflen;
	}

 

计算文件名长度
len = cend – comp;

分配空间,截串

if (p_ret) {
		ret = emalloc(len + 1);
		memcpy(ret, comp, len);
		ret[len] = '\0';
		*p_ret = ret;
	}
	if (p_len) {
		*p_len = len;
	}

把dirname放这里,其实是没找到关键的函数zend_dirname,直接用这个函数处理了,我猜过程和basename差不多吧,只不过返回str头指针到comp这段字符。

pathinfo,实际上就是调用了上面两个函数,分离出路径和文件名,然后在文件名基础上分离出扩展名。

PHP_FUNCTION(strtok)

proto string strtok([string str,] string token)
Tokenize a string

这是一个很有趣的函数。先看一下在php中应用的方式:

$string  =  "This is\tan example\nstring" ;
/* 使用制表符和换行符作为分界符 */
$tok  =  strtok ( $string ,  " \n\t" );

while ( $tok  !==  false ) {
    echo  "Word= $tok <br />" ;
     $tok  =  strtok ( " \n\t" );
}

上面是手册里提供的例子,在第一次调用strtok函数的时候,要将待处理的字串string传入,下文再使用的时候,就不需要这个string参数了,在没看源码前,猜想c层是将string存到一个全局变量里了。

看一下源码,先看怎样处理1个参数和两个参数

if (ZEND_NUM_ARGS() == 1) {
		tok = str;
		tok_len = str_len;
	} else {
		if (BG(strtok_zval)) {
			zval_ptr_dtor(&BG(strtok_zval));
		}
		MAKE_STD_ZVAL(zv);
		ZVAL_STRINGL(zv, str, str_len, 1);

		BG(strtok_zval) = zv;
		BG(strtok_last) = BG(strtok_string) = Z_STRVAL_P(zv);
		BG(strtok_len) = str_len;
	}

	p = BG(strtok_last); /* Where we start to search */
	pe = BG(strtok_string) + BG(strtok_len);

后面多切进来两行,在一个参数的时候,处理很简单,直接把传进来的第一个参数str赋给tok参数,并没有处理if后面的strtok_last,strtok_string等变量。

else里猜想就是存储第一次两个参数是传进来的string参数了,这里包括if后面,用了BG函数(还是宏?),不知道在哪定义的。G是global?

前面很多地方也用到了BG函数,一直没注意,等找到BG定义的地方,回头还要再看一遍。

接下来看:

while (token < token_end) {
		STRTOK_TABLE(token++) = 1;
	}

又一个新的东西:STRTOK_TABLE,在strtok函数前面有定义:#define STRTOK_TABLE(p) BG(strtok_table)[(unsigned char) *p]

这个小写的strtok_table又是什么呢?没找到。。。不过看起定义的形式,用了[],是一个数组,key是char指针的数组,所以可以遍历key

上面这个while是将STRTOK_TABLE数组里,key是token的置为1。

接下来跳过最前面的token字符,因为要的是被token分割的字串,不是token本身

/* Skip leading delimiters */
	while (STRTOK_TABLE(p)) {
		if (++p >= pe) {
			/* no other chars left */
			BG(strtok_last) = NULL;
			RETVAL_FALSE;
			goto restore;
		}
		skipped++;
	}

因为上面已经把所有前置token都跳过去了,所以接下来p指针位置要么是结束,要么是第一个非token字符。继续遍历p,直到再次遇到token字符,这样在两个token之间的字串就是本次要返回的内容

/* We know at this place that *p is no delimiter, so skip it */
	while (++p < pe) {
		if (STRTOK_TABLE(p)) {
			goto return_token;
		}
	}

看一下goto处的代码

return_token:
		RETVAL_STRINGL(BG(strtok_last) + skipped, (p - BG(strtok_last)) - skipped, 1);
		BG(strtok_last) = p + 1;
	} else {
		RETVAL_FALSE;
		BG(strtok_last) = NULL;
	}

BG(strtok_last) + skipped,如果没有前置token,skipped实际是0,也就是从上次处理的位置开始,长度是(p – BG(strtok_last)) – skipped,同理,如果skipped=0,长度就是当前p位置-上次处理strtok_last位置之间的长度。如果skipped不是0,指针位置要跳过skipped,长度也要减掉skipped。

补充一下,想一下传入的token参数是”各类空格+各类换行+各类标点符号”,不就是一个分词功能,当然中文是分不了的,中文词组中间不用这些分隔符。

PHP_FUNCTION(implode)

proto string implode([string glue,] array pieces)
Joins array elements placing glue string between items and return one string

将数组用指定字符串拼接成一个串。

从源码来看,可以只传一个数组变量,这时拼接串默认为””,即直接把数组各元素串起来。

如果是两个参数的话,其中必须有一个是数组,并将另一个强制转换成字串。

主处理函数:php_implode

一次遍历数组,顺序取出数组元素,对于元素类型的处理如下:

switch ((*tmp)->type) {
			case IS_STRING:
				smart_str_appendl(&implstr, Z_STRVAL_PP(tmp), Z_STRLEN_PP(tmp));
				break;

			case IS_LONG: {
				char stmp[MAX_LENGTH_OF_LONG + 1];
				str_len = slprintf(stmp, sizeof(stmp), "%ld", Z_LVAL_PP(tmp));
				smart_str_appendl(&implstr, stmp, str_len);
			}
				break;

			case IS_BOOL:
				if (Z_LVAL_PP(tmp) == 1) {
					smart_str_appendl(&implstr, "1", sizeof("1")-1);
				}
				break;

			case IS_NULL:
				break;

			case IS_DOUBLE: {
				char *stmp;
				str_len = spprintf(&stmp, 0, "%.*G", (int) EG(precision), Z_DVAL_PP(tmp));
				smart_str_appendl(&implstr, stmp, str_len);
				efree(stmp);
			}
				break;

			case IS_OBJECT: {
				int copy;
				zval expr;
				zend_make_printable_zval(*tmp, &expr, &copy);
				smart_str_appendl(&implstr, Z_STRVAL(expr), Z_STRLEN(expr));
				if (copy) {
					zval_dtor(&expr);
				}
			}
				break;

			default:
				tmp_val = **tmp;
				zval_copy_ctor(&tmp_val);
				convert_to_string(&tmp_val);
				smart_str_appendl(&implstr, Z_STRVAL(tmp_val), Z_STRLEN(tmp_val));
				zval_dtor(&tmp_val);
				break;

		}

string long double都处理成字符串处理,bool型,只把true当成1来处理,false直接抛弃了,null也直接抛弃,因为拼接delim的语句在switch语句后面,所以switch里不处理相当于当成空串””处理。在false或null处会连续输出拼接符。

object使用了zend_make_printable_zval函数将对象强制转换成字串。

php测试代码1:

$arr = array(1,array(2,3));
echo implode(",",$arr);

php测试代码2:

class obj1{}
class obj2{
	function __tostring(){
		return 'myobj';
	}
}
$arr = array(1,new obj1());
echo implode(",",$arr);

$arr = array(1,new obj2());
echo implode(",",$arr);

 

PHP_FUNCTION(explode)

proto array explode(string separator, string str [, int limit])
Splits a string on string separator and return array of components. If limit is positive only limit number of components is returned. If limit is negative all components except the last abs(limit) are returned.

原来limit还可以是负数,如果是负数,返回的将是不传limit返回数组前的leng-abs(limit)个

具体实现代码也是根据limit的正负分成两个函数处理,不传limit,将会置limit=LONG_MAX

if (limit > 1) {
		php_explode(&zdelim, &zstr, return_value, limit);
	} else if (limit < 0) {
		php_explode_negative_limit(&zdelim, &zstr, return_value, limit);
	} else {
		add_index_stringl(return_value, 0, str, str_len, 1);
	}

注意limit=[0,1]时,都跑到else里了,也就是返回只有一个元素,值为原串的数组,0可以理解,1为什么也返回原串?费解,但是要注意。

简单看一下php_explode

p2 = php_memnstr(Z_STRVAL_P(str), Z_STRVAL_P(delim), Z_STRLEN_P(delim), endp);

这里用了php_memnstr(zend_memnstr)函数,此函数在zend_operators.h里,在str里查找delim,返回第一次匹配到的位置的指针。

if (p2 == NULL) {
		add_next_index_stringl(return_value, p1, Z_STRLEN_P(str), 1);
	} else {
		do {
			add_next_index_stringl(return_value, p1, p2 - p1, 1);
			p1 = p2 + Z_STRLEN_P(delim);
		} while ((p2 = php_memnstr(p1, Z_STRVAL_P(delim), Z_STRLEN_P(delim), endp)) != NULL &&
				 --limit > 1);

		if (p1 <= endp)
			add_next_index_stringl(return_value, p1, endp-p1, 1);
	}

如果找不到delim也就是p2=null时,又是原串了。else里用了一个do – while,逐段查找切割放到数组里,最后if (p1 <= endp)把剩余的尾巴收进去,结束。

php_explode_negative_limit里用到了内存分配的技巧,c语言这块不是太熟悉,在分割算法上基本同php_memnstr

 

PHP_FUNCTION(wordwrap)

proto string wordwrap(string str [, int width [, string break [, boolean cut]]])

Wraps buffer to selected number of characters using string break char

还是看源码里的注释吧,中文手册里不靠谱的多。

按我的理解,这个函数的作用是按照指定的长度(默认75)分行,第四参数cut默认false,不强行切断单词,否则到达指定长度,切断单词强行分行。从对齐度上来看,cut=true要好看些,从阅读角度看,当然是false比较好。

这里有个分割单词的默认字符,那就是空格,以及指定的break。

看一下具体代码,先看默认参数的情况,即break默认为\n或长度为1,cut=false的情况:

if (breakcharlen == 1 && !docut) {
		newtext = estrndup(text, textlen);

		laststart = lastspace = 0;
		for (current = 0; current < textlen; current++) {
			if (text[current] == breakchar[0]) {
				laststart = lastspace = current + 1;
			} else if (text[current] == ' ') {
				if (current - laststart >= linelength) {
					newtext[current] = breakchar[0];
					laststart = current + 1;
				}
				lastspace = current;
			} else if (current - laststart >= linelength && laststart != lastspace) {
				newtext[lastspace] = breakchar[0];
				laststart = lastspace + 1;
			}
		}

		RETURN_STRINGL(newtext, textlen, 0);
	}

为什么要把这种情况单独处理呢,因为这种情况下换行符长度和单词分割符(空格)长度一致,遇到需要分行的地方,只要将空格替换成换行符就可以了,不需要另外分配临时空间存放分行后的字串。

注释里是这样写的:Special case for a single-character break as it needs no additional storage space

这里newtext用到了estrndup函数,在trim里也用到了。
先看这个if

if (text[current] == breakchar[0])
如果遇到字符串里原有的换行符,直接将最近处理的起点和最近一个空格置为current+1,起点current+1可以理解,空格为什么也置current+1这个位置?这要结合第二个elseif来看,原来第二个elseif里判断了这种情况,也就是laststart != lastspace的时候才处理,考虑一下current=0时,laststart不也是等于lastspace,相当于一个新的起点。

else if (text[current] == ‘ ‘)//这个引号中间是个空格
遇到空格,也就是有可能要换行的地方,如果当前处理的长度还不够,也就是不满足if (current – laststart >= linelength)的时候,只是将lastspace移到当前位置,反之还要将空格替换成换行符。同时将laststart 移到current+1的位置。

第二个elseif:else if (current – laststart >= linelength && laststart != lastspace)

什么情况下会进入这里呢?显然是第一个elseif的临界情况,也就是再读一个单词就要超过,少读一个单词长度又不够的时候。注意替换空格用的是lastspace下标,也就是回退到上一个空格,而第一个elseif里用的是current。

上面是简单情况的处理,复杂情况的处理其实也基本类似,只是注意这个变量:chk

 

PHP_FUNCTION(trim)、rtrim、ltrim

都是调用php_do_trim函数,第二个参数不同,分别是3,2,1

而在php_do_trim内部又是调用这个函数:php_trim。

核心代码如下:

if (mode & 1) {
		for (i = 0; i < len; i++) {
			if (mask[(unsigned char)c[i]]) {
				trimmed++;
			} else {
				break;
			}
		}
		len -= trimmed;
		c += trimmed;
	}
	if (mode & 2) {
		for (i = len - 1; i >= 0; i--) {
			if (mask[(unsigned char)c[i]]) {
				len--;
			} else {
				break;
			}
		}
	}

mask数组是要去掉的空格字符数组集合,默认是这几个:” \n\r\t\v\0″,注意第一个是一个空格,共6个吧。

第一个if是去左边空格,第二个if是去右边空格,这个时候传入php_do_trim的第二个参数mode,就非常有用了,3&1=1,3&2=2,所以trim的mode=3,而2&1=0,所以,mode=2只能进2,mode=1只能进1。

当mode=3时,也就是trim。

先进第一个if,从左往右遍历字串,直到访问mask数组失败,也就是非空格字符。第一个for后面,这两个语句是很有意思的:

len -= trimmed;
c += trimmed;

长度,减去左边空格的长度,char指针位置也移到第一个非空字符位置。

进入第二个if里,第二个if是从右往左遍历,如果有空格的话,就直接减len,而不是临时变量trimmed。

最后返回的是:

if (return_value) {
		RETVAL_STRINGL(c, len, 1);
	} else {
		return estrndup(c, len);
	}

estrndup这个函数没找到,好像也不是c内部函数。其功能大概也能猜到,应该是返回一个等同于c指针当前位置len长度的char指针。

PHP_FUNCTION(nl_langinfo)、PHP_FUNCTION(strcoll)

将这两函数放一起,是因为他们都直接调用了c里的同名函数,所以就没什么好讲的了。

看一下nl_langinfo的说明:Query language and locale information。参数是一大堆给定的常量,返回不同的属性信息,具体有哪些item呢,前面是一些日期格式,后面没仔细看。

strcoll:Compares two strings using the current locale,说如果当前区域为 C 或 POSIX,该函数等同于 strcmp()

所以还是用strcmp吧,后面看到再细分析

 

PHP_FUNCTION(strcspn)

和strspn正好相反的一个函数,这个函数是寻找不在s2集合里的连续字符个数

调用的是这个函数:php_strcspn,定义的临时变量其实和php_strspn是差不多的,函数实现代码如下:

PHPAPI size_t php_strcspn(char *s1, char *s2, char *s1_end, char *s2_end)
{
	register const char *p, *spanp;
	register char c = *s1;

	for (p = s1;;) {
		spanp = s2;
		do {
			if (*spanp == c || p == s1_end) {
				return p - s1;
			}
		} while (spanp++ < (s2_end - 1));
		c = *++p;
	}
	/* NOTREACHED */
}

用了两层循环,其思想还是外面移动s1,里面移动s2,逐个比较,直到有一个相等,或者p移到末尾。

 

PHP_FUNCTION(strspn)

先来看看某人翻译的php手册怎么说的:计算字符串中全部字符都存在于指定字符集合中的第一段子串的长度。

这是神马意思?再看说明里进一步解释:返回 subject 中全部字符存在于 mask 中的第一组连续字符(子字符串)的长度。

还是很糊涂是吧?

还是看源码吧。

直接调用这个函数php_spn_common_handler(INTERNAL_FUNCTION_PARAM_PASSTHRU, STR_STRSPN);

进去后,发现调用的是这个函数:php_strspn,一个只有几行的简单函数:

PHPAPI size_t php_strspn(char *s1, char *s2, char *s1_end, char *s2_end)
{
	register const char *p = s1, *spanp;
	register char c = *p;

cont:
	for (spanp = s2; p != s1_end && spanp != s2_end;) {
		if (*spanp++ == c) {
			c = *(++p);
			goto cont;
		}
	}
	return (p - s1);
}

s1是strspn的第一个参数,也就是被查找的目标串,s2是第二个参数,我把他理解成字符集合,php_strspn里用了几个临时指针变量,p指向s1,spanp指向s2,c是p也就是s1指针当前位置的字符。

这样整理一下,就容易看懂这个for循环的意思了,移动spanp指针(也就是s2),其当前位置字符挨个和c进行比较,不相等的话,就一直移动spanp,直到结束(==s2_end),中间只要有一个s2的字符和c相当,就移动p指针(s1),并将p指针当前位置字符重置给c,接着是一个goto语句,跳到for循环的开始,这样的话,spanp又指向s2的开始位置,再来新一轮的比较,直到p到结束位置或者spanp到结束位置。

最后返回p-s1,也就是p相对s1头位置移动的步数。

解释的有点复杂,简单讲,就是s1从头开始,挨个字符和s2里所有字符进行比较,只要包含在s2里,就一直比下去,直到有一个字符不在s2集合里,前面所有在集合内字符的个数,就是本函数的返回结果。