PHP内核探索之变量(7)- 不平凡的字符串

本文深入探讨了PHP中的字符串基础,包括字符串的四种表现形式、字符串结构与C语言的对比,以及字符串操作函数。文章指出PHP字符串是二进制安全的,与C语言相比更高效,且提供了丰富的函数。此外,还讨论了字符串操作函数如strlen、str_word_count和similar_text的使用,并触及了多字节字符串处理的挑战。
摘要由CSDN通过智能技术生成

切,一个字符串有什么好研究的。

别这么说,看过《平凡的世界》么,平凡的字符串也可以有不平凡的故事。试看:

(1)       在C语言中,strlen计算字符串的时间复杂度是?PHP中呢?

(2)       在PHP中,怎样处理多字节字符串?PHP对unicode的支持如何?

同样是字符串,为什么c语言与C++/PHP/Java的均不相同?

数据结构决定算法,这句话一点不假。

那么我们今天就来掰一掰,PHP中的字符串结构,以及相关字符串函数的实现。

一、  字符串基础

  字符串可以说是PHP中遇到最多的数据结构之一了(另外一个比较常用的是数组,见PHP内核探索之变量(4)- 数组操作)。而由于PHP语言的特性和应用场景,使得我们日常的很多工作,实际上都是在处理字符串。也正是这个原因,PHP为开发者提供了丰富的字符串操作函数(初步统计约有100个,这个数量相当可观)。那么,在PHP中,字符串是怎样实现的呢?与C语言又有什么区别呢?

 1.  PHP中字符串的表现形式

  在PHP中使用字符串有四种常见的形式:

(1)    双引号

         这种形式比较常见:$str=”this is \0 a string”; 而且以双引号包含的字符串中可以包含变量、控制字符等:$str = "this is $name, aha.\n";

(2)     单引号

  单引号包含的字符都被认为是raw的,因此不会解析单引号中的变量,控制字符等:

1
2
3
$string = "test" ;
$str = 'this is $string, aha\n' ;
echo $str ;

(3)     Heredoc

Heredoc比较适合较长的字符串表示,且对于多行的字符串表示更加灵活多样。与双引号表示形式类似,heredoc中也可以包含变量。常见的形式是:

1
2
3
4
5
6
7
$string = "test string" ;
$str = <<<STR
This is a string \n,
My string is $string
STR;
 
echo $str ;

(4)     nowdoc(5.3+支持)

nowdoc和heredoc是如此的类似,以至于我们可以把它们当做是一对儿亲兄弟。nowdoc的起始标志符是用单引号括起来的,与单引号相似,它不会解析其中的变量,格式控制符等:

1
2
3
4
5
6
$s = <<< 'EOT'
this is $str
this is \t test;
EOT;
 
echo $s ;

2.       PHP中字符串的结构

   之前提到过,PHP中变量是用Zval(PHP内核探索之变量(1)Zval)这样一个结构体来存储的。Zval的结构是:

struct _zval_struct {
    zvalue_value value;       /* value */
    zend_uint refcount__gc;   /* variable ref count */
    zend_uchar type;          /* active type */
    zend_uchar is_ref__gc;    /* if it is a ref variable */
};

   而变量的值是zvalue_value这样一个共用体:

typedef union _zvalue_value {
    long lval;
    double dval;
    struct {                    /* string */
        char *val;
        int len;
    } str;
    HashTable *ht;
    zend_object_value obj;
} zvalue_value;

我们从中抽取出字符串的结构:

struct {
    char *val;
    int len;
} str;

现在比较清楚了,PHP中字符串在底层实际上是一个结构体,该结构体包含了指向字符串的指针和该字符串的长度。

那么为什么这么做呢?换句话说,这样做有什么好处呢?我们接下来,将PHP的字符串与C语言的字符串做一个对比,以解释采用这样一种结构来存储字符串的优势。

3.  与c语言字符串的比较

         我们知道,在c语言中,一个字符串可以用两种常见的形式存储,一种是使用指针方式,另一种是使用字符数组。我们接下来的说明,都以c语言的字符数组的方式来存储字符串。

(1)       PHP字符串是二进制安全的,而C字符串不是

     我们经常会提到”二进制安全”这一术语,那么二进制安全究竟是什么意思呢?

     wikipedia中对二进制安全(Binary Safe)的定义是:

Binary-safe is a computer programming term mainly used in connection with string manipulating functions. 
A binary-safe function is essentially one that treats its input as a raw stream of data without any specific format. 
It should thus work with all 256 possible values that a character can take (assuming 8-bit characters).

  翻译过来就是:

     二进制安全是计算机编程的术语,主要用于字符串操作函数。一个二进制安全的函数,本质上是指它将输入看做是原始的数据流(raw)而不包含任何特殊的格式

     那么为什么C字符串不是二进制安全的?我们知道,在C语言中,以字符数组表示的字符串总是以\0结尾的,这个\0便是C字符串的specific format, 用于标识字符串的结束。更近一步说,如果一个字符串中本身包含了\0且并不是该字符串的结尾,那么在C中,\0后面的所有数据都会被忽略(感觉就像是 字符串被莫名其妙的截断了)。这也意味着,C字符串只合适保存简单的文本,而不能用于保存图片、视频、其他文件等二进制数据。而在PHP中,我们可以使用$str = file_get_contents(“filename”);保存图片、视频等二进制数据。

(2)   效率对比

     由于C字符串中使用\0来标志字符串的结束,因此,对于strlen函数而言,获取字符串长度的操作需要顺序遍历字符串,直到遇到\0为止,因此strlen函数的时间复杂度是O(n)。而在PHP中,字符串是以:

struct{
      char *val;
      int len;
} str;

 这样一种结构体来表示的,因而获取字符串的长度只需要通过常量的时间便可以完成:

#define Z_STRLEN(zval)          (zval).value.str.len

当然,仅仅是strlen函数的性能,无法支持“PHP中string比c字符串的效率更高”的结论(一个很明显的原因是PHP是构建在C语言之上的高级语言),而仅仅说明,在时间复杂度上,PHP字符串比C字符串更加高效。

(3)      很多C字符串函数存在缓冲区溢出的漏洞

缓存区溢出是C语言中常见的漏洞,这种安全隐患经常是致命的。一个典型的缓存区溢出的例子如下:

void str2Buf(char *str) {
    char buffer[16];
    strcpy(buffer,str);
}

 这个函数将str的内容copy到buffer数组中,而buffer数组的大小是16,因此如果str的长度大于16,便会发生缓冲区溢出的问题。

除了strcpy,还有gets, strcat, fprintf等字符串函数也会有缓冲区溢出的问题。

PHP中并没有strcpy与strcat之类的函数,实际上由于PHP语言的简洁性,并不需要提供strcpy和strcat之类的函数。例如我们要复制一个字符串,直接使用=即可:

$str = "this is a string";
$str_copy = $str;

  由于PHP中变量共享zval的特性,并不会有空间浪费.而简单的.连接符可以轻松实现字符串连接:

$str = "this is";
$str .= "test string";
echo $str;

  关于字符串连接符过程中的内存分配和管理,可以查看zend引擎部分的实现,这里暂时忽略。

二、   字符串操作相关函数(部分)

         毫无疑问,研究字符串的目的并不只是为了知道它的结构和特性,而是为了更好的使用它。我们日常的工作中,恐怕有一般以上的工作都是在与字符串打交道:如处理一个日期串、加密一个密码、获取用户信息、正则表达式匹配替换、字符串替换、格式化一个串等等。可以说,在PHP开发中,你无法避免与字符串的直接或者间接接触(就像无法摆脱呼吸)。正因为如此,PHP为开发者提供了大量的、丰富的字符串操作函数( http://cn2.php.net/manual/en/ref.strings.php),这对于90%以上的字符串操作,已经基本足够。

         由于字符串函数众多,不可能一一说明。这里只挑选几个比较典型的字符串操作函数     来做简单的说明(我相信80%以上的PHPer对于字符串的操作函数掌握的非常的好)。

在开始说明之前,有必要强调一下字符串函数的使用原则,理解和掌握这些原则对于高效、熟练使用字符串函数非常关键,这些原则包括(不仅限于):

(1)       如果你的操作既可以使用正则表达式,也可以使用字符串。那么优先考虑字符串操作

         正则表达式是处理文本的绝好工具,尤其对于模式查找、模式替换这一类应用,正则可以说是无往不利。正因为如此,正则表达式在很多场合都被滥用。如果对于你的字符串操作,既可以使用字符串函数完成,也可以使用正则表达式完成,那么,请优先选择字符串操作函数,因为正则表达式在一定场合下会有严重的性能问题。

(2)       注意false与0

    PHP是弱变量类型,相信不少phper开始都深受其害

var_dump( 0 == false);//bool(true)
var_dump( 0 === false);//bool(false)

  等等,这与字符串操作函数有什么关系?

  在PHP中,有一类函数用于查找(如strpos, stripos),这类查找函数在查找成功时,返回的是子串在原串中的index,如strpos:

var_dump(strpos("this is abc", "abc"));

  而在查找不成功时,返回的是false:

var_dump(strpos("this is abc", "angle"));//false

  这里便有一个坑:字符串的索引也是以0开始的!如果子串刚好在源串的起始位置出现,那么,简单的==比较便无法区分究竟strpos是不是成功:

var_dump(strpos("this is abc", "this"));

  因此我们一定是要用===来比较的:

if((strpos("this is abc", "this")) === false){
    // not found
}

  (3)       多看手册,避免重复造轮子

  相信不少PHPer面试都碰到过这样的问题:如何翻转一个字符串?由于题目中只提及“如何“,而并没有限制”不使用PHP内置函数“。那么对于本题,最简洁的方法自然是使用strrev函数。另一个说明不应该重复造轮子的函数是levenshtein函数,这个函数如同其名字一样,返回的是两个字符串的编辑距离。作为动态规划(DP)的典型代表案例之一,我想编辑距离很多人都不陌生。碰到这类问题,你还准备DP搞起吗?一个函数搞定它:

$str1 = "this is test";
$str2 = "his is tes";
echo levenshtein($str1, $str2);

在某些情况下,我们都应该尽可能的“懒“,不是吗。

以下是字符串操作函数节选(对于最常见的操作,请直接参考手册)

1.  strlen

此标题一出,我猜想大多数人的表情是这样的:

或者是这样的:

 

我要说的,并不是这个函数本身,而是这个函数的返回值。

1
2
int strlen ( string $ string )
Returns the length of the given string .

虽然手册上明确指出“strlen函数返回给定字符串的长度”,但是,并没有对长度单位做任何说明,长度究竟是指”字符的个数“还是说”字符的字节数“。而我们要做的,并不是臆想,而是测试:

在GBK编码格式下:

echo strlen(“这是中文”);//8

说明strlen函数返回的是字符串的字节数。那么又有问题了,如果是utf-8编码,由于中文在utf8编码的情况下,每个中文使用3个byte,因而,我们期望的结果应该是12:

echo strlen(“这是中文”);//12

这说明:strlen计算字符串的长度依赖于当前的编码格式,其值并不是唯一的!这在某些情况下,自然是无法满足要求的。这时,多字节扩展mbstring便有它的发挥余地了:

echo mb_strlen("这是中文", "
  • 2
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值