((size_t*)ptr)[-1] 在 C 中是什么意思?

What ((size_t*)ptr)[-1] mean in C?

我想知道分配给指针的大小。

所以我找到了这个答案: how can i know the allocated memory size of pointer variable in c

它有下面的代码。

#include <stdlib.h>
#include <stdio.h>

void * my_malloc(size_t s) 
{
  size_t * ret = malloc(sizeof(size_t) + s);
  *ret = s;
  return &ret[1];
}

void my_free(void * ptr) 
{
  free( (size_t*)ptr - 1);
}

size_t allocated_size(void * ptr) 
{
  return ((size_t*)ptr)[-1];
}

int main(int argc, const char ** argv) 
{
  int * array = my_malloc(sizeof(int) * 3);
  printf("%u\n", allocated_size(array));
  my_free(array);
  return 0;
}

(((size_t*)ptr)[-1]) 行完美运行,但我不明白为什么...

谁能帮我理解这条魔法线?谢谢!

似乎您的编译器的 C malloc 实现将分配的大小(以字节为单位)保持在地址 returns.

之前的 4 个字节中

通过将返回的地址 (ptr) 转换为指向 size_t 的指针(即 ((size_t*)ptr)),然后取其之前的对齐地址(即 ' [-1]',实际上只是指针运算 - 与编写 *(((size_t*)ptr) - 1)) 相同 - 您可以访问分配的大小(size_t 类型)。

这是为了解释 ((size_t*)ptr)[-1] 的含义以及它似乎有效的原因,但这绝不是使用它的建议。获取分配给指针的大小是应用程序代码要求的一个数量,如果需要应该由它管理,而不依赖于编译器实现。

如果 ptr 指向由 malloccallocrealloc 等分配的内存块,则 (((size_t*)ptr)[-1] 调用 未定义的行为。我的猜测是,它依赖于一些随机供应商的标准库实现的行为,恰好将内存块的大小存储在 malloc 等返回的位置之前的位置

请勿使用此类 HACKS!如果程序动态分配内存,它应该能够跟踪它分配的内存大小,而不依赖于未定义的行为。

malloc等实际分配的内存块的大小可能比申请的大,所以也许你有兴趣知道实际分配的块的大小,包括多余的内存在块的末尾。可移植代码不需要知道这一点,因为访问超出请求大小的位置也是未定义行为,但出于好奇或调试目的,您可能想知道这个大小。

这实际上是一个调用 UB 的非常糟糕的代码。

如果他想保存分配的 space 大小,他应该使用结构,其中第一个字段是大小,第二个零大小数组(或 vla)用于实际数据

首先,让我们从((size_t*)ptr)[-1]的含义开始。

当您使用数组下标运算符作为(例如)A[B] 时,这完全等同于 *(A + B)。所以这里真正发生的是指针运算,然后是取消引用。这意味着具有负数数组索引是有效的,前提是有问题的指针不指向数组的第一个元素。

举个例子:

int a[5] = { 1, 2, 3, 4, 5 };
int *p = a + 2;
printf("p[0] = %d\n", p[0]);      // prints 3
printf("p[-1] = %d\n", p[-1]);    // prints 2
printf("p[-2] = %d\n", p[-2]);    // prints 1

所以将它应用到 ((size_t*)ptr)[-1],这表示 ptr 指向一个或多个 size_t 类型的对象数组的一个元素(或指向末尾后面的一个元素数组的),下标 -1 获取 就在 之前 ptr 指向的对象。

现在这在示例程序的上下文中意味着什么?

函数 my_mallocmalloc 的包装器,它分配 s 字节 加上 足够的字节用于 size_t。它在 malloc 缓冲区的开头写入 s 的值作为 size_t,然后 returns 指向内存的指针 之后size_t 对象。

所以实际分配的内存和返回的指针看起来像这样(假设sizeof(size_t) is 8):

        -----
0x80    | s |
0x81    | s |
0x82    | s |
0x83    | s |
0x84    | s |
0x85    | s |
0x86    | s |
0x87    | s |
0x88    |   |   <--- ptr
0x89    |   |
0x8A    |   |
...

当从 my_malloc 返回的指针传递给 allocated_size 时,函数可以使用 ((size_t*)ptr)[-1]:

读取请求的缓冲区大小
        -----
0x80    | s |   <--- ptr[-1]
0x81    | s |
0x82    | s |
0x83    | s |
0x84    | s |
0x85    | s |
0x86    | s |
0x87    | s |
0x88    |   |   <--- ptr[0]
0x89    |   |
0x8A    |   |

转换后的ptr指向大小为1的size_t数组后的一个元素,所以指针本身是有效的,随后得到数组下标为-1的对象也是有效的。这是不是其他人建议的未定义行为,因为指针正在转换to/from一个void *并指向指定类型的有效对象。

在此实现中,只有请求缓冲区的大小存储在返回的指针之前,但是您可以在那里存储更多元数据,前提是您为其分配足够的额外 space。

没有考虑的一件事是 malloc 返回的内存适合任何目的对齐,而 my_malloc 返回的指针可能不符合该要求。因此,放置在返回地址的对象可能存在对齐问题并导致崩溃。为了解决这个问题,需要分配额外的字节来满足该要求,并且 allocated_sizemy_free 也需要进行调整以解决这个问题。

首先,让我们解释一下 (((size_t*)ptr)[-1]) 的作用,假设它是有效的:

  • (size_t*)ptrptr 转换为“指向 size_t 的指针”类型。
  • ((size_t *)ptr)[-1],根据定义1,等价于*((size_t *) ptr - 1).2 即减去1 来自 (size_t *) ptr 并“引用”结果指针。
  • 指针运算是根据数组元素定义的,并将单个对象视为一个元素的数组。2 如果 (size_t *) ptr 指向“刚好超出”一个size_t对象,则*((size_t *) ptr - 1)指向size_t对象。
  • 因此,(((size_t*)ptr)[-1])size_t 对象,就在 ptr 之前。

现在,让我们来讨论一下这个表达式是否有效。 ptr是通过这段代码得到的:

void * my_malloc(size_t s) 
{
  size_t * ret = malloc(sizeof(size_t) + s);
  *ret = s;
  return &ret[1];
}

如果malloc成功,它将为请求大小的任何对象分配space。4所以我们当然可以存储一个size_t那里 5,除了这段代码应该检查 return 值以防止分配失败。此外,我们可能 return &ret[1]:

  • &ret[1]相当于&*(ret + 1),相当于ret + 1。这指向我们存储在 retsize_t 之外的一个,这是有效的指针算法。
  • 指针转换为函数return类型,void *,有效5

问题中显示的代码只用 my_malloc 中的值 return 做了两件事:使用 ((size_t*)ptr)[-1] 检索存储的大小并使用 space 释放 space (size_t*)ptr - 1。这些都是有效的,因为指针转换是适当的,并且它们在指针算法的限制内运行。

但是,还有一个问题是 returned 值的进一步用途。正如其他人指出的那样,虽然 malloc 中的指针 return 适合任何对象对齐,但添加 size_t 会产生一个指针,该指针仅适合其对齐要求的对象并不比 size_t 严格。例如,在许多 C 实现中,这意味着指针不能用于 double,这通常需要八字节对齐,而 size_t 只是四个字节。

所以我们立即看到 my_malloc 并不是 malloc 的完全替代。尽管如此,也许它只能用于具有令人满意的对齐要求的对象。让我们考虑一下。

我认为许多 C 实现对此没有问题,但是,从技术上讲,这里有一个问题:malloc 被指定为 return space 的一个对象要求的尺寸。该对象可以是数组,因此 space 可用于同一类型的多个对象。但是,没有指定 space 可以用于不同类型的多个对象。因此,如果 size_t 以外的某个对象存储在 my_malloc 编辑的 space return 中,我看不到 C 标准定义了该行为。正如我所指出的,这是一种迂腐的区分;我不希望 C 实现有这方面的问题,尽管多年来越来越积极的优化让我感到惊讶。

malloc 编辑的 space return 中存储多个不同对象的一种方法是使用结构。然后我们可以在 size_t 之后的 space 中放置一个 intfloatchar *。然而,我们不能通过指针运算来实现——使用指针运算来导航结构的成员还没有完全定义。寻址结构成员是通过名称正确完成的,而不是指针操作。因此 returning &ret[1] from my_malloc 不是一种有效的方式(由 C 标准定义)来提供指向 space 的指针,该指针可用于任何对象(即使满足对齐要求)。

其他注意事项

此代码不正确地使用 %u 来格式化 size_t 类型的值:

printf("%u\n", allocated_size(array));

size_t 的特定整数类型是实现定义的,可能不是 unsigned。 C 标准可能未定义由此产生的行为。正确的格式说明符是 %zu.

脚注

1 C 2018 6.5.2.1 2.

2更准确的说是*((((size_t *) ptr)) + (-1)),但是这些是等价的

3 C 2018 6.5.6 8 和 9.

4 C 2018 7.22.3.4.

5 C 2018 7.22.3.4 的一个非常迂腐的 reader 可能会反对 size_t 不是请求大小的对象,而是一个对象较小的尺寸。我不认为这是预期的意思。

6 C 2018 6.3.2.3 1.