CPython中id(obj)和ctypes.addressof(obj)有什么区别

10 投票
1 回答
1675 浏览
提问于 2025-04-18 06:15

假设我用 ctypes模块 定义了一个变量。

i = c_int(4)

然后我想通过以下方式找出这个变量 i 的内存地址:

id(i)

或者

ctypes.addressof(i)

这两种方法现在得到的结果却不一样。为什么会这样呢?

1 个回答

19

你所提到的情况其实是CPython的一个实现细节。

id()函数的作用是:

返回一个对象的“身份”。这个身份是一个整数,保证在对象的生命周期内是唯一且不变的。

CPython实现细节:这个身份实际上是对象在内存中的地址。

虽然在CPython中它们可能是相同的,但在其他Python的实现中并不一定如此。


为什么即使在CPython中它们的值也不同呢?

需要注意的是,c_int

  • 一个Python对象。CPython的id()会返回这个对象的地址。

  • 包含一个4字节的C兼容int值。ctypes.addressof()会返回这个值的地址。

Python对象中的元数据会占用空间。因此,这个4字节的值可能不会在Python对象的最开始位置。

看这个例子:

>>> import ctypes
>>> i = ctypes.c_int(4)
>>> hex(id(i))
'0x22940d0'
>>> hex(ctypes.addressof(i))
'0x22940f8'

我们看到addressof的结果比id()的结果高出0x28字节。多试几次,我们会发现这种情况总是如此。因此,我认为在整体c_int中,实际的int值前面有0x28字节的Python对象元数据。

在我上面的例子中:

   c_int
 ___________
|           |   0x22940d0   This is what id() returns
| metadata  |
|           |
|           |
|           |
|           |
|___________|
|   value   |   0x22940f8   This is what addressof() returns
|___________|

编辑:

在CPython的ctypes实现中,基本的CDataObject(2.7.6源代码)有一个b_ptr成员,指向用于对象C数据的内存块:

union value {
                char c[16];
                short s;
                int i;
                long l;
                float f;
                double d;
#ifdef HAVE_LONG_LONG
                PY_LONG_LONG ll;
#endif
                long double D;
};

struct tagCDataObject {
    PyObject_HEAD
    char *b_ptr;                /* pointer to memory block */
    int  b_needsfree;           /* need _we_ free the memory? */
    CDataObject *b_base;        /* pointer to base object or NULL */
    Py_ssize_t b_size;          /* size of memory block in bytes */
    Py_ssize_t b_length;        /* number of references we need */
    Py_ssize_t b_index;         /* index of this object into base's
                                   b_object list */
    PyObject *b_objects;        /* dictionary of references we need 
                                   to keep, or Py_None */
    union value b_value;
};

addressof返回这个指针作为一个Python整数:

static PyObject *
addressof(PyObject *self, PyObject *obj)
{
    if (CDataObject_Check(obj))
        return PyLong_FromVoidPtr(((CDataObject *)obj)->b_ptr);
    PyErr_SetString(PyExc_TypeError,
                    "invalid type");
    return NULL;
}

小型C对象使用CDataObject的默认16字节b_value成员。正如上面的例子所示,这个默认缓冲区用于c_int(4)实例。我们可以利用ctypes来检查在32位进程中的c_int(4)

>>> i = c_int(4)
>>> ci = CDataObject.from_address(id(i))

>>> ci
ob_base: 
    ob_refcnt: 1
    ob_type: py_object(<class 'ctypes.c_long'>)
b_ptr: 3071814328
b_needsfree: 1
b_base: LP_CDataObject(<NULL>)
b_size: 4
b_length: 0
b_index: 0
b_objects: py_object(<NULL>)
b_value: 
    c: b'\x04'
    s: 4
    i: 4
    l: 4
    f: 5.605193857299268e-45
    d: 2e-323
    ll: 4
    D: 0.0

>>> addressof(i)
3071814328
>>> id(i) + CDataObject.b_value.offset
3071814328

这个技巧利用了CPython中id返回对象基本地址的事实。

撰写回答