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

6
我想知道指针分配的大小。
所以我找到了这个答案: 如何在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])可以完美运行,但我不明白为什么会这样。请问有人能帮我理解一下吗?谢谢!

1
这里的一个问题是,size_t 的大小可能 等于所有类型的最大对齐要求。 - Antti Haapala -- Слава Україні
1
return &ret[1]; 哎呀,这是非常糟糕的代码。继续 @AnttiHaapala 的说法,由于 size_t 可能不适合最大对齐限制,因此使用从 my_malloc() 返回的指针可能会失败,因为它不再满足 C 标准的要求,即“适当地对齐,以便可以将其分配给具有基本对齐要求的任何类型对象的指针”。 - Andrew Henle
2
我希望投票者在点赞回答之前先阅读并理解问题,而不是仅仅因为某些事实(无论是否包含)而对与问题实际无关的答案进行点赞。 - Arkku
5个回答

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

不要使用这样的技巧!如果程序正在动态分配内存,它应该能够跟踪所分配的内存大小,而不依赖于未定义的行为。

malloc等实际分配的内存块的大小可能大于请求的大小,因此您可能想知道已分配的块的实际大小,包括块末尾的额外内存。可移植代码不需要知道这一点,因为访问超出请求大小的位置也是未定义行为,但是您可能想出于好奇或调试目的而知道这个大小。


1
谢谢你的警告,现在我知道它是如何工作的,我意识到这不是一个非常稳定和安全的解决方案。 - Firerazzer
可能你会发现,在同一种实现下,它只对某些块大小“有效”,而对另一些块大小则“无效”! - Ian Abbott
3
在问题中展示的代码中,ptr 没有指向由 malloc 分配的内存块。my_malloc 程序从 malloc 获取内存,存储大小,并返回指向刚超出该大小的内存的指针。 - Eric Postpischil
@EricPostpischil 我回答时那里没有它。此外,该代码可能存在对齐问题。 - Ian Abbott

2
首先,让我们从((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。它将s的值作为size_t写入malloc缓冲区的开头,然后返回指向size_t对象之后的内存的指针。

因此,实际分配的内存和返回的指针看起来像这样(假设sizeof(size_t)为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获取对象也是有效的。正如其他人所建议的那样,这不是未定义的行为,因为指针正在转换为/从void *并指向指定类型的有效对象。

在这个实现中,只有请求缓冲区的大小存储在返回的指针之前,但是如果分配足够的额外空间,您可以在那里存储更多的元数据。

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


潜在有趣的相关趣闻:由于 A[B]*(A + B) 的等价性,也可以使用 1[arr] 代替 arr[1]。(但请不要这样做。)然而,理解这一点说明为什么 ptr[-1] 不会自动破坏,就像 *(ptr - 1) 一样。 - Arkku
关于对齐问题,可以使用 max_align_t 来调整返回地址/传递地址的偏移和对齐,而不是像原始的 my_alloc 一样假定 size_t 具有最大的对齐要求。这样做可以解决对齐问题。 - Arkku
@Arkku 可能需要一个包含 size_tmax_align_t 成员的联合体,因为我认为不能保证 max_align_t 能够表示所有的 size_t 值。 - Ian Abbott
@IanAbbott 是的,我在另一个答案的评论中建议了这个(联合)。=) - Arkku

2
首先,我们来解释一下(((size_t*)ptr)[-1])的含义,假设它是有效的:
  • (size_t*)ptrptr转换为“指向size_t的指针”类型。
  • ((size_t *)ptr)[-1]根据定义1等同于*((size_t *) ptr - 1)2。也就是说,它从(size_t *) ptr中减去1,并“间接引用”结果指针。
  • 指针算术运算是以数组元素为基础定义的,并将单个对象视为一个元素的数组。2如果(size_t *) ptr指向的是一个size_t对象的“刚好超出”,那么*((size_t *) ptr - 1)指向该size_t对象。
  • 因此,(((size_t*)ptr)[-1])ptr之前的那个size_t对象。
现在,让我们讨论一下这个表达式是否有效。 ptr是通过以下代码获得的:
void * my_malloc(size_t s) 
{
  size_t * ret = malloc(sizeof(size_t) + s);
  *ret = s;
  return &ret[1];
}

如果malloc成功,它将为所请求的大小分配任何对象的空间。4因此,我们肯定可以在那里存储一个size_t,除非这段代码应该检查返回值以防止分配失败。此外,我们可能会返回&ret[1]

  • &ret[1]等同于&*(ret + 1),等同于ret + 1。这指向了我们在ret中存储的size_t之外的一个位置,这是有效的指针算术。
  • 指针转换为函数返回类型void *,这是有效的。5

my_malloc返回的值只做两件事:使用((size_t*)ptr)[-1]检索存储的大小,并使用(size_t*)ptr - 1释放空间。这两个操作都是有效的,因为指针转换是适当的,并且它们在指针算术的限制范围内运行。

然而,有一个问题,即返回值可以被用于什么进一步的用途。正如其他人所指出的那样,虽然从malloc返回的指针适用于任何对象,但size_t的添加产生的指针仅适用于对齐要求不比size_t更严格的对象。例如,在许多C实现中,这意味着指针不能用于double,后者通常需要8字节对齐而size_t仅为4字节。

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

我认为许多C实现在这方面没有问题,但技术上存在一个问题:malloc被指定为返回所请求大小的一个对象的空间。该对象可以是数组,因此该空间可以用于同一类型的多个对象。然而,未指定该空间可用于不同类型的多个对象。因此,如果存储在由my_malloc返回的空间中的某个对象不是size_t,则我认为C标准未定义行为。正如我所指出的,这是一个迂腐的区别;我不希望C实现会有问题,尽管多年来越来越激进的优化使我感到惊讶。

使用结构体是在malloc返回的空间中存储多个不同对象的一种方法。然后,我们可以将intfloatchar *放置在size_t之后的空间中。但是,我们不能通过指针算术来实现这一点——使用指针算术来导航结构体的成员并没有完全定义。正确地通过名称寻址结构体成员,而不是指针操作。因此,从my_malloc返回&ret[1]不是一种有效的方式(由C标准定义)来提供可用于任何对象的空间指针(即使满足对齐要求)。
其他注意事项:
该代码错误地使用%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的一个非常迂腐的读者可能会反对size_t不是请求大小的对象,而是更小大小的对象。我不认为这是预期的意义。

6 C 2018 6.3.2.3 1。


也许可以有一个struct allocation { union { size_t size; max_align_t foo; }; max_align_t data[]; },并返回/传递data的地址。(编辑:或者实际上,只需传递一个union { size_t size; max_align_t foo; }数组的第二个元素,并使用第一个元素作为大小。) - Arkku
@Arkku:那么你可能存在别名问题。如果你有一个指向max_align_t的指针,编译器通常可以假定对于不兼容类型指针所指向的对象的访问不会与指向max_align_t *的对象的访问相互影响。 - Eric Postpischil
使用仅包含(至少)size_tmax_align_t的联合数组如何?如果我们向联合中添加一个char数组会怎样? - Arkku

1
这实际上是一段非常糟糕的代码,会引发未定义行为。
如果他想要保存分配的空间大小,应该使用结构体,其中第一个字段是大小,第二个是零大小数组(或可变长度数组)用于实际数据。

1
什么是UB? - Mark Lakata
使用搜索栏:https://dev59.com/63E95IYBdhLWcg3wPbZ7 - 0___________
1
我的意思是,哪一行代码是未定义行为? - Mark Lakata

-1

看起来你的编译器的C语言malloc实现将分配的大小(以字节为单位)存储在返回的地址前面的4个字节中。

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

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


这适用于所有的malloc实现还是只适用于GNU libc? - Ivan Todorović
size_t 的大小不是未定义的吗?因为 ((size_t*)ptr)[-1] 通过所提到的大小从 ptr 跳过,所以它并不能确定它总是4个字节。 - nm_tp
2
不好意思,但是这个答案是不正确的。在某些实现中,malloc() 可能存储大小的方式是这样的,但这绝非“常见”——肯定有不这样做的实现。在 C 中,这是正式未定义的行为。 - Peter
2
这是一个糟糕的回答。向读者解释C的某些实现可能会在块之前存储分配块的大小是可以的,但是以不合格的语句开头来说malloc确实将大小保留在那里是可怕的。没有资格的情况下这样陈述是不正确的。此外,即使实现确实将大小存储在那里,也并不意味着它支持通过((size_t*)ptr)[-1]访问它,这违反了标准C规则。 - Eric Postpischil
4
这个回答甚至没有解决问题。在问题中,使用了一个名为my_malloc的用户例程,而不是malloc,并且正在检查的大小是由my_malloc存储的大小,而不是由malloc存储的大小。 - Eric Postpischil

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