使用内存引用比较PHP数组

6

如何判断两个数组变量是否指向同一内存位置?(它们是同一个数组)


6
因为如果要得到一份超越简单的“是”或“不是”的合格答案,你需要了解Zend Engine(例如推动PHP的东西)如何处理变量和内存。 - Gordon
2
c0rnh0li0 - 他只是想这么做。就算他没有充分的理由,那又怎样呢。重要的是理解和解决问题,而不是改变别人的思考方式。 - Christian
1
有时候需要的是修正某人的思维方式。程序员花费了大量时间重新发明别人已经解决的轮子。不确定这里是否是这种情况,但通常都是如此。 - Toby Allen
相关链接:https://dev59.com/om445IYBdhLWcg3wg6tm - Pacerier
@TobyAllen 当有人认为他们需要修正我的思维方式时,这非常令人烦恼,尤其是在这个问题中没有任何迹象表明提问者是一名经验不足的程序员时。 (我知道你的评论已经很久了,但它仍然具有相关性)。 - frodeborli
显示剩余3条评论
8个回答

16

实际上,这可以通过php扩展完成。

文件: config.m4

PHP_ARG_ENABLE(test, 是否启用测试扩展支持, [ --enable-test   启用测试扩展支持])
if test "$PHP_TEST" = "yes"; then AC_DEFINE(HAVE_TEST, 1, [启用TEST扩展]) PHP_NEW_EXTENSION(test, test.c, $ext_shared) fi

文件: php_test.h

#ifndef PHP_TEST_H
#define PHP_TEST_H 1
#define PHP_TEST_EXT_VERSION "1.0" #define PHP_TEST_EXT_EXTNAME "test"
PHP_FUNCTION(getaddress4); PHP_FUNCTION(getaddress);
extern zend_module_entry test_module_entry; #define phpext_test_ptr &test_module_entry
#endif

文件: test.c

#ifdef HAVE_CONFIG_H
#include "config.h"
#endif
#include "php.h" #include "php_test.h"
ZEND_BEGIN_ARG_INFO_EX(func_args, 1, 0, 0) ZEND_END_ARG_INFO()
static function_entry test_functions[] = { PHP_FE(getaddress4, func_args) PHP_FE(getaddress, func_args) {NULL, NULL, NULL} };
zend_module_entry test_module_entry = { #if ZEND_MODULE_API_NO >= 20010901 STANDARD_MODULE_HEADER, #endif PHP_TEST_EXT_EXTNAME, test_functions, NULL, NULL, NULL, NULL, NULL, #if ZEND_MODULE_API_NO >= 20010901 PHP_TEST_EXT_VERSION, #endif STANDARD_MODULE_PROPERTIES };
#ifdef COMPILE_DL_TEST ZEND_GET_MODULE(test) #endif
PHP_FUNCTION(getaddress4) { zval *var1; zval *var2; zval *var3; zval *var4; char r[500]; if (zend_parse_parameters(ZEND_NUM_ARGS() TSRMLS_CC, "aaaa", &var1, &var2, &var3, &var4) == FAILURE) { RETURN_NULL(); }
PHP_FUNCTION(getaddress)
{
    zval *var;
    char r[100];
    if( zend_parse_parameters(ZEND_NUM_ARGS() TSRMLS_CC, "a", &var) == FAILURE ) {
      RETURN_NULL();
    }
    sprintf(r, "%p", Z_ARRVAL_P(var));
    RETURN_STRING(r, 1);
}

以上函数用于获取变量在内存中的地址,其中:

  • 第一段代码用于输出四个变量及其数组在内存中的地址。
  • 第二段代码是一个 PHP 函数,用于获取传入参数在内存中的地址。
  • 最后的 PHP 代码用于调用这些函数并输出结果。

使用方法:

  • 将第一段代码编译为扩展(extension)文件,然后在 php.ini 文件中添加 "extension=/path/to/so/file/modules/test.so"。
  • 调用上述 PHP 函数 getaddress 可以获取任何变量在内存中的地址。
  • 在本例中,调用该函数分别输出了四个变量及其数组在内存中的地址和单独一个变量在内存中的地址。
感谢Artefacto指出了我的错误,我的原始代码将数组按值传递,从而重新创建包含被引用的数组,导致内存值不正确。我已经更改了代码以强制所有参数通过引用传递。这将允许引用、数组和对象不受php引擎的影响。$w/$z是相同的东西,但$w/$x/$y不一样。旧代码实际上显示了引用的破坏和当所有变量都被传入同一个函数多次调用时,内存地址会改变或匹配的事实。这是因为PHP在执行多个调用时会重复使用相同的内存。比较原始函数的结果将是无用的。新代码应该解决这个问题。
FYI- 我使用的是PHP 5.3.2版本。

+1,但你应该使用Z_ARRVAL_P而不是打破zval抽象(有时字段名称确实会更改,例如在5.3中的refcount和is_ref)。 - Artefacto
你说得对,我不知道Z *预处理器定义。PHP对其内部信息并不太公开,而且我也不是这方面的专家,所以获取结构值比找到一些晦涩难懂的预处理器要容易得多。但我会更改代码以反映这一点,谢谢。 - Rahly
2
@Jeremy 如果你执行 $x = array(); $z = $x;,那么 $z$x 确实会指向同一个 zval *,直到强制分离(写时复制机制)。你的例子略有不同--当你执行 $w = $x; 时,$w$x 指向同一个 zval *(refcount=2,is_ref=0)。但是当你执行 $z =& $x; 时,你正在强制分离,因为 $z$x 是引用,而 $w 不是,一个 zval 不能对 is_ref 字段有两个不同的值。所以第一个 zval($w/$x)被复制并且它的 refcount 被减少。新的副本($z/$x; after)的 refcount 设置为 2,并且 is_ref=1。 - Artefacto
@Jeremy,地址相同只是巧合。当使用is_ref变量调用函数时,您强制进行分离。结果产生的新zval在函数结束时被销毁。当第二次调用使用引用进行新的分离时,新的zval从zend内存管理器获取相同的内存块。 - Artefacto
1
是的,你对我的当前代码正确。我将更新我的代码以使其正确。 - Rahly
显示剩余15条评论

9

你可能也想看一下:http://php.net/manual/zh/function.spl-object-hash.php - Wouter Dorgelo
另外需要注意的是,PHP语言无法访问引擎内存,你必须记住PHP语言是由引擎解析的,因此你的脚本在任何时候都不会变成机器码,它只是运行引擎的机器码,所以你的脚本文件不像程序一样使用内存,只有引擎才会使用内存。我怀疑他们是否会允许脚本访问原始内存,因为这可能会破坏服务器或者在服务器上写入病毒,然后被搜索主机无法使用。 - Barkermn01
1
-1:答案不正确。虽然引用不是指针,不能进行指针算术运算,但它们仍具有使它们成为引用的特征,因此“不,你不能”是不正确的,事实上,“你可以”。请参阅我的回复以了解其工作原理。 - Christian

8
你的问题有些误导性。“指向相同的内存位置”和“是同一个数组”(至少在PHP中意味着引用)并不是同一件事情。
内存位置指的是指针。指针在PHP中不可用。引用不是指针。
无论如何,如果你想检查$b是否实际上是$a的引用,这是你可以得到的最接近的答案:
function is_ref_to(&$a, &$b) {
    if (is_object($a) && is_object($b)) {
        return ($a === $b);
    }

    $temp_a = $a;
    $temp_b = $b;

    $key = uniqid('is_ref_to', true);
    $b = $key;

    if ($a === $key) $return = true;
    else $return = false;

    $a = $temp_a;
    $b = $temp_b;
    return $return; 
}

$a = array('foo');
$b = array('foo');
$c = &$a;
$d = $a;

var_dump(is_ref_to($a, $b)); // false
var_dump(is_ref_to($b, $c)); // false
var_dump(is_ref_to($a, $c)); // true
var_dump(is_ref_to($a, $d)); // false
var_dump($a); // is still array('foo')

2
是的。 基本上问题应该是:“是否有可能看到两个数组变量指向相同的内存位置?”而没有“(它们是相同的数组)” 的说明。 然后答案是; “不”。 - Inga Johansson
2
@stereofrog: “你也可以使用PHP5对象(它们是指针)”,不,它们不是指针。 - Inga Johansson
1
@stereofrog:对象不是引用,它们是对象。对象是通过引用传递的。 - netcoder
1
@Inga 是的,在PHP 5中,对象是指针。变量仅保存对象的ID,而不是地址。对象存储在其他地方。这并不意味着“对象通过引用传递”;对象(默认情况下)像其他所有内容一样按值传递,只是按值传递的是一种指针。 - Artefacto
@netcoder,你认为 https://dev59.com/y1HTa4cB1Zd3GeqPTqw9#4278286 下面的函数怎么样?我测试过了,它似乎可以工作。如果它能工作,那么它不使用 uniqid 会更加简洁,不是吗? - Pacerier
显示剩余5条评论

3
        function check(&$a,&$b){
            // perform first basic check, if both have different values
            // then they're definitely not the same.
            if($a!==$b)return false;
            // backup $a into $c
            $c=$a;
            // get some form of opposite to $a
            $a=!$a;
            // compare $a and $b, if both are the same thing,
            // this should be true
            $r=($a===$b);
            // restore $a from $c
            $a=$c;
            // return result
            return $r;
        }

        $a=(object)array('aaa'=>'bbb'); $b=&$a;
        echo check($a,$b) ? 'yes' : 'no'; // yes
        $c='aaa'; $d='aaa';
        echo check($c,$d) ? 'yes' : 'no'; // no
        $e='bbb'; $f='ccc';
        echo check($e,$f) ? 'yes' : 'no'; // no

“check”函数是在大约2分钟内创建的。它假设如果您更改引用的值,则第二个引用也将具有新添加的值。

此函数仅适用于变量。您可以对常量值、函数返回(除非通过引用)等使用它。

编辑:在测试过程中,我有些初步的困惑。我一直在重复使用相同的变量名称($a和$b),这导致所有条件都是“是”。原因如下:

$a='aaa'; $b=&$a;     // a=aaa b=aaa
$a='ccc'; $b='ddd';   // a=ddd b=ddd   <= a is not ccc!

为了解决这个问题,我给它们取了一个不同的名称:
$a='aaa'; $b=&$a;     // a=aaa b=aaa
$c='ccc'; $d='ddd';   // c=ccc d=ddd   <= c is now correct

编辑:为什么答案是“是”而不是“否”

PHP通过脚本不会透露指针信息(包括指针操作等)。 然而,它允许使用引用运算符'&'来创建别名变量(引用)。 这个特性通常在指针中找到,这也解释了普遍的困惑。 话虽如此,指针并不是别名。

然而,如果我们看一下原始问题,那个人想知道$a是否与$b相同,而不是$a(或$b)在内存中的位置。虽然之前的要求适用于引用和指针,但后一种要求只适用于指针。


2

PHP中的引用比较

我知道这个问题已经很老了,但它仍然很相关,这就是为什么我来到这里的原因。可能有几种测试方法,但我想出了另外几种方法。

PHP 7.4引用相等性测试

ReflectionReference为数组元素提供了引用ID:

function is_same(&$a, &$b): bool {
  $_ = [ &$a, &$b ];
  return
    \ReflectionReference::fromArrayElement($_, 0)->getId() ===
    \ReflectionReference::fromArrayElement($_, 1)->getId();
}

PHP 5、7和8版本

该函数将依赖于PHP序列化检测到循环引用的事实来识别实际引用。缺点是对于大数组,它需要暂时占用内存和时间来序列化数据。对于大数组,使用下面的实用数组相等性测试可能更好。

function is_same(&$a, &$b) {
    $_ = [ &$a, &$b ];
    // PHP >= 7.4
    if (\class_exists(\ReflectionReference::class)) {
      return
        \ReflectionReference::fromArrayElement($_, 0)->getId() ===
        \ReflectionReference::fromArrayElement($_, 1)->getId();
    }

    // Faster, for objects
    if (\is_object($a) && \is_object($b) && $a === $b) return true;

    // Stop if they aren't identical, this is much faster.
    if ($a !== $b) return false;

    // Resources can't be serialized
    if (\is_resource($a) && \is_resource($b) && "".$a === "".$b) return true;

    // Serialization supports references, so we utilize that
    return \substr(\serialize($_), -5) === 'R:2;}';
}

PHP < 7.4的内存友好型数组引用检查

这个测试应该不会浪费太多内存。副作用是PHP使用写时复制技术来节省数组所占用的内存 - 因此当这个函数向数组追加内容时,它将触发该机制。

function array_is_same(array &$a, array &$b): bool {
  // Fastest test first
  if ($a !== $b) {
    return false;
  }
  // Then the reference test
  try {
    // Need a unique key
    while (
      array_key_exists($key = '#! '.mt_rand(PHP_INT_MIN, PHP_INT_MAX), $a) || 
      array_key_exists($key, $b)
    );
    $a[$key] = true;
    return isset($b[$key]);
  } finally {
    // cleanup
    unset($a[$key], $b[$key]);
  }
}

2
首先,你的问题比较模糊,可能有几种不同的情况:
- 变量内容是否相同?可以使用===来判断。 - 这些变量是否在内部使用相同的内存? - 这些变量是否在同一引用集中?也就是说,如果给定两个变量$a$b,如果我更改了$a,那么$b会发生变化吗?
第二个问题的答案并不容易确定。Jeremy Walton的答案有一个重要的问题——他的函数通过值传递,所以如果你传递一个引用,你将强制分离并获得一个新临时值的地址。你可以让函数通过引用接收参数,但这样你就会遇到相反的问题——如果你传递一个值(refcount >= 2),你也会强制进行分离。
更重要的是,第二个问题是一个无关紧要的内部细节。考虑以下脚本:
$a = 1;
$b = $a; //addresses of $a and $b are the same
function force_sep(&$a) { }
force_sep($b);
//force_sep is a no-op, but it forced a separation; now addresses are not equal

因此,重要的问题是第三个。不幸的是,没有直接的方法来确定这一点。已经有人多次请求了这个功能;例如,参见这个请求
然而,有几个选择:
- 您可以接收变量名称并在符号表中查找它。这也是使xdebug_debug_zval比有缺陷的debug_zval_dump更有趣的原因之一。对于简单的变量,这是在EG(active_symbol_table)中进行的简单查找(但如果您想包括对象属性和维度等,则会变得更加复杂),这还将允许您为第二个问题实现解决方案。 - 您还可以修改Jeremy Walton的答案,使函数通过引用接收两个值(您需要一个arginfo结构),并同时接收两个值。同时接收它们可以避免由于重复使用的内存地址导致的误报(尽管是否存在问题取决于函数的使用方式;另一方面,Jeremy Walton的函数在接收引用时总是存在这个问题——如果需要,我可以详细说明一下,但请看他的回答下面的评论)。 - netcoder的答案虽然有些hackish,但也可以工作。其思想是通过引用接收两个变量,更改一个变量,并查看另一个变量是否更改,在最后恢复值。

-1
function var_name(&$ref){
    foreach($GLOBALS as $key => $val){
       if($val === $ref) return $key;
    }
}

这是未经测试的,但根据我所知的 PHP,变量在加载到系统中时会被添加到 GLOBALS 中,因此它们相同的第一次出现应该是原始变量,但如果您有两个完全相同的变量,我不确定它会如何反应


2
这样不行。=== 运算符检查类型,但不检查引用。这意味着如果你有 $a = 1; $b = 1; $c = &$a,那么以下是正确的:$a === $b === $c,即使 $b 不是一个引用。知道变量是否是引用的唯一方法是 a) 查看代码;或 b) 修改它并查看原始变量是否更改。至于 $GLOBALS,它与此无关。在函数中声明的引用仍然是引用,但不会成为 $GLOBALS 的一部分。 - netcoder
@Barkermn01 - 当比较对象时,相同运算符只能按照这种方式工作。对于其他情况,"aaa" === "aaa"是正确的(即使它们是不同的常量值)。至于你提到的 GLOBALS 想法,只有在全局范围内才能起作用(据我所知)。 - Christian
"aaa" === "aaa" 是完全相同的,就像你试图证明的 "1" === 1 或者 "1" === "hello" 都返回 false。去学习 PHP 吧。 - Barkermn01
1
不是这样的。你的代码甚至没有检查对象类型,如果你没有注意到,身份运算符在对象上的工作方式与其他变量不同。 身份检查内容,而不是引用,因此您的代码最终在某些情况下会失败。 - Christian
1
我非常怀疑你以“我对PHP的了解”为开头,接着侮辱那些不懂PHP的人。既然你甚至不想理解,这是我的最后回答。傻瓜是那些不愿意学习的人,而不是那些最终会学习的人。请记住,有三个人说同样的话,而你却说相反的话... - Christian
显示剩余4条评论

-2
 $a["unqiue-thing"] = 1;
 if($b["unique-thing"] == 1) // a and b are the same

6
仅仅因为 a = b 且 b = c 就意味着 a = c,并不代表它们指向同一内存块。 - Gordon
@stereofrog 有点像。但感觉很不正规。有没有更官方的获取内存引用的方法? - Kirk Ouimet
3
你到底在说什么?这跟内存位置有什么关系? - Hamish
3
@stereofrog这个回答在很多方面都是错误的。首先,如果你按值分配一个数组,直到在一个数组中改变了一个值,它实际上是一个引用。也就是说,在某些情况下,你的解决方案实际上会导致数组“取消引用”自身。另外,要正确地执行此操作,您需要检查测试值的唯一性,然后覆盖该值,然后测试相等性,最后将旧值重新写入。这样做充满了失败。 - Hamish
4
@sterofrot我已经给正确答案点了赞。如果你熟悉PHP在底层是如何管理内存的话,这个答案是有道理的。如果你将$a创建为一个数组,然后执行$b = $a,那么$b将实际上是引用 $b - 也就是说,没有额外的内存被分配。如果你随后执行$b[] = "new item",那么PHP才会复制整个数组并进行修改。你可以通过检查大型数组的内存使用情况来测试这一点。内存使用情况只有在修改第二个数组时才会增加,而不是在赋值过程中增加。你可以试试看。 - Hamish
显示剩余4条评论

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接