【问题标题】:Is there *any* way to get the length of a C-style array in C++/G++?有*任何*方法来获取 C++/G++ 中 C 样式数组的长度吗?
【发布时间】:2011-09-16 10:29:22
【问题描述】:

我已经尝试实现 lengthof (T* v) 函数很长一段时间了,到目前为止没有任何成功。

对于 T v[n] 数组,有两种基本的、众所周知的解决方案,一旦数组衰减为 T* v 指针,这两种解决方案都是无用的,甚至是危险的。

#define SIZE(v) (sizeof(v) / sizeof(v[0]))

template <class T, size_t n>
size_t lengthof (T (&) [n])
{
    return n;
}

有一些涉及包装类和容器的解决方法,如 STLSoft 的 array_proxy、boost::array、std::vector 等。它们都有缺点,缺乏数组的简单性、语法糖和广泛使用。

当 delete [] 需要知道数组的长度时,编译器通常会使用涉及编译器特定调用的解决方案存在神话。根据 C++ FAQ Lite 16.14,编译器使用两种技术来了解要释放多少内存:过度分配和关联数组。在过度分配时,它会多分配一个字大小,并将数组的长度放在第一个对象之前。另一种方法显然将长度存储在关联数组中。是否可以知道 G++ 使用哪种方法,并提取适当的数组长度?开销和填充物呢?对非编译器特定的代码有任何希望吗?甚至是非平台特定的 G++ 内置函数?

还有涉及重载operator new []和operator delete []的解决方案,我实现了:

std::map<void*, size_t> arrayLengthMap;

inline void* operator new [] (size_t n)
throw (std::bad_alloc)
{
    void* ptr = GC_malloc(n);
    arrayLengthMap[ptr] = n;
    return ptr;
}

inline void operator delete [] (void* ptr)
throw ()
{
    arrayLengthMap.erase(ptr);
    GC_free(ptr);
}

template <class T>
inline size_t lengthof (T* ptr)
{
    std::map<void*, size_t>::const_iterator it = arrayLengthMap.find(ptr);
    if( it == arrayLengthMap.end() ){
        throw std::bad_alloc();
    }
    return it->second / sizeof(T);
}

在我得到一个奇怪的错误之前它工作得很好:lengthof 找不到数组。事实证明,G++ 在这个特定数组的开头分配了比它应该有的多 8 个字节。尽管 operator new [] 应该返回整个数组的开头,将其称为 ptr,但调用代码却得到了 ptr+8,因此 lengthof(ptr+8) 显然失败并出现异常(即使没有,它也可能有可能返回了错误的数组大小)。那 8 个字节是某种开销还是填充?不能是前面提到的过度分配,该函数对许多数组都正常工作。它是什么以及如何禁用或解决它,假设可以使用 G++ 特定的调用或诡计?

编辑: 由于可以通过多种方式分配 C 样式的数组,因此通常不可能通过指针来判断任意数组的长度,正如 Oli Charlesworth 所建议的那样。但是基于 Ben Voigt 的想法,非衰减静态数组(参见上面的模板函数)和使用自定义运算符 new [] (size_t, size_t) 分配的数组是可能的:

#include <gc/gc.h>
#include <gc/gc_cpp.h>
#include <iostream>
#include <map>

typedef std::map<void*, std::pair<size_t, size_t> > ArrayLengthMap;
ArrayLengthMap arrayLengthMap;

inline void* operator new [] (size_t size, size_t count)
throw (std::bad_alloc)
{
    void* ptr = GC_malloc(size);
    arrayLengthMap[ptr] = std::pair<size_t, size_t>(size, count);
    return ptr;
}

inline void operator delete [] (void* ptr)
throw ()
{
    ArrayLengthMap::const_iterator it = arrayLengthMap.upper_bound(ptr);
    it--;
    if( it->first <= ptr and ptr < it->first + it->second.first ){
        arrayLengthMap.erase(it->first);
    }
    GC_free(ptr);
}

inline size_t lengthof (void* ptr)
{
    ArrayLengthMap::const_iterator it = arrayLengthMap.upper_bound(ptr);
    it--;
    if( it->first <= ptr and ptr < it->first + it->second.first ){
        return it->second.second;
    }
    throw std::bad_alloc();
}

int main (int argc, char* argv[])
{
    int* v = new (112) int[112];
    std::cout << lengthof(v) << std::endl;
}

不幸的是,由于编译器的任意开销和填充,到目前为止还没有可靠的方法来确定自定义运算符 new [] (size_t) 中动态数组的长度,除非我们假设填充小于数组元素之一的大小。

但是,正如 Ben Voigt 所建议的那样,还有其他类型的数组也可以计算长度,因此构造一个可以接受多种数组(及其长度)的包装类应该是可能和可取的。它的构造函数,并且可以隐式或显式转换为其他包装类和数组类型。不同类型数组的不同生命周期可能是个问题,但可以通过垃圾回收来解决。

【问题讨论】:

  • 您认为这比将代码转换为基于 std::vector 或 boost::array 更简单?过度分配可能有很多原因。您可以使用不同的分配器进行原型设计,例如 boost::pool_allocator
  • 是否有可能在不使用哨兵值的情况下在 C 本身中获取 C 样式数组的长度? C 中的字符串以 null 结尾的事实让我相信这通常是不可能的。
  • 请注意,您的技巧仅适用于动态分配的数组。静态和自动数组通常不会在任何地方存储任何元信息。
  • @JAB:在 C 中这是不可能的,数组会立即衰减为指针。
  • @JAB, @Matthieu:我很确定 sizeof 方法在 C 中有效。数组在 sizeof 上下文中不会衰减。

标签: c++ arrays gcc g++


【解决方案1】:

回答这个问题:

对非编译器特定的代码有任何希望吗?

没有。

更一般地说,如果您发现自己需要这样做,那么您可能需要重新考虑您的设计。例如,使用std::vector

【讨论】:

  • @Oli:虽然我同意std::vector 的建议,但我不得不反对你的断言,即这是不可能的。问题中提出的想法已经达到了 95%。
  • @Ben:没有独立于平台的方法来获取任意 C 样式数组的长度。首先,new [] 并不是获得这种东西的唯一途径。另外,OP 的方法不适用于已衰减为T * 的“真实”数组。也不是多维数组。
  • @Oli:这个问题似乎与任意数组无关,只有operator new[]。听起来他需要一些可以通过需要纯指针的 API 的东西,并且仍然能够在另一端的回调中恢复长度。
  • @Ben:可能。但他/她在他们的问题中没有这么说,所以我保留判断。我相信我的回答在一般情况下是正确的。
  • @Oli:主要问题是动态数组,我应该澄清一下。虽然现在很清楚,对于任意数组是不可能的,但为动态数组实现它仍然是好的。我相信自定义过度分配或关联数组可用于生成跨平台代码,假设对填充有一些限制。总而言之,包装类仍然是最有吸引力的解决方案,它们可以由静态数组、动态数组(有或没有长度)构造。
【解决方案2】:

您的分析大部分是正确的,但是我认为您忽略了一个事实,即具有平凡析构函数的类型不需要存储长度,因此不同类型的过度分配可能不同。

该标准允许operator new[] 窃取几个字节供自己使用,因此您必须对指针进行范围检查,而不是精确匹配。 std::map 可能不会对此有效,但应该是排序的向量(可以进行二进制搜索)。平衡树也应该工作得很好。

【讨论】:

  • 实际上范围检查对std::map 是有效的,因为间隔是不相交的。因此,您只需要按下限或上限排序并将另一个存储在值中以确保您在范围内。请记住,一个排序的向量意味着移动(可能很多)指针,最好的数据结构可能是一个 B 树。
  • std::map 的 lower_bound 函数似乎以对数大小完成了这项工作。由于我们存储大小,我们可以轻松计算间隔。仍然存在编译器放入任意开销和填充的问题,我不确定标准中是否有任何关于它的内容。
  • @Matthieu:是的,我一直认为std::map 好像它是一个哈希表,但事实并非如此。无论如何,我们一致认为平衡树是最有效的。
  • @Frigo:标准明确允许过度分配new[],没有限制。 “新表达式将请求的空间量作为 std::size_t 类型的第一个参数传递给分配函数。该参数不应小于正在创建的对象的大小;它可能大于仅当对象是数组时才创建对象。” -- 部分[expr.new]
【解决方案3】:

前段时间,我用一个类似的东西来监控内存泄漏:

当被要求分配 size 个字节的数据时,我会分配 size + 4 个字节并将分配的长度存储在前 4 个字节中:

static unsigned int total_still_alloced = 0;
void *sys_malloc(UINT size)
{
#if ENABLED( MEMLEAK_CHECK )
  void *result = malloc(size+sizeof(UINT )); 
  if(result)
  {
    memset(result,0,size+sizeof(UINT ));
    *(UINT *)result = size;
    total_still_alloced += size;
    return (void*)((UINT*)result+sizeof(UINT));
  }
  else
  {
    return result;
  }
#else
  void *result = malloc(size);
  if(result) memset(result,0,size);
  return result;
#endif
}

void sys_free(void *p)
{
  if(p != NULL)
  {
#if ENABLED( MEMLEAK_CHECK )
    UINT * real_address = (UINT *)(p)-sizeof(UINT);
    total_still_alloced-= *((UINT *)real_address);

    free((void*)real_address);
#else
    free(p);
#endif
  }
}

在您的情况下,检索分配的大小是将提供的地址移动 4 并读取值。

请注意,如果您在某处出现内存损坏......您将得到无效的结果。 另请注意,malloc 在内部通常是如何工作的:将分配的大小放在一个隐藏字段返回的地址之前。在某些架构上,我什至不必分配更多,使用 system malloc 就足够了。

这是一种侵入性的做法……但它可以工作(前提是您使用这些修改后的分配例程分配所有内容,并且您知道数组的起始地址)。

【讨论】:

    猜你喜欢
    • 2020-02-10
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2014-03-27
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多