如果free()知道我的数组的长度,为什么我不能在自己的代码中询问它?

50

我知道传递动态分配数组长度给操作它们的函数是一种常见的惯例:

void initializeAndFree(int* anArray, size_t length);

int main(){
    size_t arrayLength = 0;
    scanf("%d", &arrayLength);
    int* myArray = (int*)malloc(sizeof(int)*arrayLength);

    initializeAndFree(myArray, arrayLength);
}

void initializeAndFree(int* anArray, size_t length){
    int i = 0;
    for (i = 0; i < length; i++) {
        anArray[i] = 0;
    }
    free(anArray);
}

但如果我无法从指针获取已分配内存的长度,那么当我只提供同一指针时,free()如何“自动地”知道要释放什么?作为C程序员,为什么我不能参与这个神奇过程呢?

free()从哪里获取其释放(哈哈)的知识?


2
还要注意,int length 是错误的。数组长度、偏移量、类型大小以及其他类似的东西都是 size_t 类型的,该类型在 stddef.hstdio.hstdlib.hstring.h 头文件中定义。size_tint 的主要区别在于 int 是有符号的,而 size_t 是无符号的,但在某些平台(例如 64 位)上它们的大小也可能不同。你应该始终使用 size_t - Chris Lutz
@Chris Lutz:谢谢。我会做出更改的。我经常看到“_t”后缀。它是什么意思?是“类型”吗?就像“size_type”一样?还有哪些其他例子? - Chris Cooper
1
是的,它代表类型。还有很多其他的例子,包括 int32_tregex_ttime_twchar_t 等等。 - Matthew Flaschen
3
带有“_t”后缀的类型是语言中非基本类型的一种。这意味着“size_t”是无符号整型、无符号长整型或类似类型。 - u0b34a0f6ae
9个回答

34

除了Klatchko正确指出的点,即标准没有提供这个功能,真正的malloc/free实现通常会分配比您请求的空间更多的空间。例如,如果您请求12字节,它可能会提供16字节(请参见A Memory Allocator,该文档指出16是一个常见的大小)。因此,它不需要知道您请求了12字节,只需要知道它给了您一个16字节的块。


但是C++呢?在C++中,当使用new type[n]进行分配时,运行时会知道实际大小,因为它会调用n个构造函数来进行delete [] - Viktor Sehr
1
@Viktor 对于基本类型或没有析构函数的类型,它不需要存储大小。 - Yacoby
@Yacoby:没错,但这并没有回答我的问题。 - Viktor Sehr
维克托,你的问题是为什么C++不提供一种获取数组中元素数量的方法,即使所有元素都有非空析构函数?可能是因为这会有些混淆(你必须知道类的实现细节才能知道该函数是否安全),而且这并不是一个必要的功能。但这值得单独提问(也许已经存在)。 - Matthew Flaschen
@ViktorSehr:如果一个实现分配的存储空间比请求的要多,并且可以通过任何手段确定在该存储空间上调用析构函数不会产生任何可见的副作用,则不需要跟踪实际分配。如果有一个函数来请求分配的实际大小,但在编译器确定不需要保留该信息的情况下不起作用,那将非常令人困惑。 - supercat
C++的一个设计原则是零开销。如果你不使用某个功能,你就不需要为它付出代价。如果我创建了一个int数组,并将其传递给共享库,而我的代码和共享库都没有调用这个假设的arraySize()函数,那么数组的大小就不应该被存储。然而,编译器无法知道我的共享库是否调用了arraySize()。此外,std::vector解决了真正的问题。 - James Hollis

19

你无法获取它,因为C委员会在标准中没有要求。

如果你愿意编写一些非可移植的代码,可能会有所帮助:

*((size_t *)ptr - 1)

或者可能是:

*((size_t *)ptr - 2)

但是这是否有效取决于您正在使用的malloc实现将数据存储在哪里。


5
个人而言,我会将其简化为 ((size_t *)ptr)[-1] - Chris Lutz
@Chris Lutz:[-1] 表示什么? - Chris Cooper
它假装指针是一个数组,然后将元素索引为开始位置的前一个。 - Simon Buchan
1
@Simon:哎呀。傻了吧. 谢谢。我以为那是在声明一个类型。当我看到所有的 * 和 ( 时,我觉得我的眼睛有点发直。= P - Chris Cooper
1
你能详细说明这个程序预计在哪些平台上运行吗? - einpoklum

10

在阅读了Klatchko的回答之后,我自己尝试了一下,ptr[-1]确实存储了实际的内存(通常比我们要求的内存多,可能是为了避免分段错误)。

{
  char *a = malloc(1);
  printf("%u\n", ((size_t *)a)[-1]);   //prints 17
  free(a);
  exit(0);
}

尝试不同的大小,GCC分配内存如下:

最初分配的内存为17字节。
分配的内存至少比请求的大小多5个字节,如果请求更多,则会分配8个字节。

  • 如果大小为[0,12],则分配的内存为17。
  • 如果大小为[13],则分配的内存为25。
  • 如果大小为[20],则分配的内存为25。
  • 如果大小为[21],则分配的内存为33。

请注意,不同的分配器可能会在其他地方存储大小。也许一整组分配与存储在竞技场第一个块中的大小共享一个64 KB的竞技场。 - Zan Lynx
这是一个完全误导性的答案。GCC不会分配任何东西。你的C库会做到这一点(甚至是操作系统)。像这样逆向工程实现是“通过试验编程”,这注定会失败。 - Jens

9
虽然可以获取内存分配器放置在分配块之前的元数据,但这只有在指针确实是指向动态分配块的情况下才有效。这将严重影响需要所有传递的参数都是指向这些块而不是简单的自动或静态数组的函数的效用。
关键在于没有可移植的方法可以通过检查指针来知道它指向的内存类型。因此,虽然这是一个有趣的想法,但并不是一个特别安全的建议。
一种安全且可移植的方法是保留分配的第一个字来保存长度。GCC(以及其他一些编译器)支持使用具有零长度数组的结构来实现这一点的非可移植方法,这比可移植解决方案简化了代码。
typedef struct
{
    size_t length ;
    char alloc[0] ;   // Compiler specific extension!!!
} tSizedAlloc ;

// Allocating a sized block
tSizedAlloc* blk = malloc( sizeof(tSizedAlloc) + length ) ;
blk->length = length ;

// Accessing the size and data information of the block
size_t blk_length = blk->length ;
char*  data = blk->alloc ;

难道不应该是 char alloc[0] 而不是 char *alloc[0] 吗? - Fayeure
1
@Fayeure:终于在11年后发现了这个问题!已经修复。还有typedef语法。 - Clifford

4

我知道这个帖子有点老,但我还是想说几句。有一个函数(或宏,我还没有检查过库)malloc_usable_size() - 获取从堆中分配的内存块的大小。手册说明它仅用于调试,因为它输出的不是您要求的数字,而是它已经分配的数字,这个数字可能会略微偏大。请注意,它是GNU扩展。

另一方面,可能甚至不需要此功能,因为我认为释放内存块时不必知道其大小。只需删除负责该块的句柄/描述符/结构即可。


3

一种非标准的方法是使用_msize()。使用此函数将使您的代码不可移植。而且文档并没有很清楚地说明它是否会返回传递给malloc()的数字或实际块大小(可能更大)。


2

malloc的实现者可以自行决定如何存储数据。通常情况下,长度直接存储在分配的内存前面(也就是说,如果您要分配7个字节,则实际上会分配7+x个字节,其中x个额外字节用于存储元数据)。有时,为了检查堆破坏,元数据同时存储在分配的内存之前和之后。但实现者也可以选择使用额外的数据结构来存储元数据。


1
我认为长度必须存储在前面。你需要知道缓冲区的大小才能找到任何尾部元数据。如果大小在尾部元数据中,那么获取该数据就会出现先有鸡还是先有蛋的问题。 - R Samuel Klatchko
可想而知,分配器可以单独存储哈希表,将指针映射到释放的大小。另一种选择是拥有大量具有不同桶大小的竞技场,分配大小可以根据指针是否落在该竞技场的区域内来确定。 - Demur Rumed

1

你可以分配更多的内存来存储大小:

void my_malloc(size_t n,size_t size ) 
{
    void *p = malloc( (n * size) + sizeof(size_t) );
    if( p == NULL ) return NULL;
    *( (size_t*)p) = n;
    return (char*)p + sizeof(size_t);
}

void my_free(void *p)
{
    free( (char*)p - sizeof(size_t) );
}

void my_realloc(void *oldp,size_t new_size)
{
    // ...
}

int main(void)
{
    char *p = my_malloc( 20, 1 );
    printf("%lu\n",(long int) ((size_t*)p)[-1] );
    return 0;
}

0
关于delete[]的问题,早期版本的C++实际上要求您调用delete[n]并告诉运行时大小,以便它不必存储它。不幸的是,这种行为被删除了,因为它“太令人困惑”。
(有关详细信息,请参见D&E。)

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