有没有*任何*方法来获得C ++ / G ++中C风格数组的长度?

时间:2011-06-16 14:21:07

标签: c++ arrays gcc g++

到目前为止,我一直在努力实现一段(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所建议的,还有其他类型的数组也可以进行长度计算,因此构建一个可以接受多种数组(及其长度)的包装类应该是可能的,也是可取的。它的构造函数,可以隐式或显式转换为其他包装类和数组类型。不同类型数组的不同生命周期可能是一个问题,但它可以通过垃圾收集来解决。

3 个答案:

答案 0 :(得分:8)

回答这个问题:

  

对非编译器特定代码有什么希望吗?

没有

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

答案 1 :(得分:4)

您的分析大多是正确的,但是我认为您忽略了这样一个事实,即使用简单析构函数的类型不需要存储长度,因此对于不同类型的分配可能会有所不同。

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

答案 2 :(得分:1)

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

当要求分配 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通常是如何在内部工作的:在返回地址之前将分配的大小放在隐藏字段上。在某些体系结构上,我甚至不需要分配更多,使用系统 malloc就足够了。

这是一种侵入性的方式......但是它有效(假设您使用这些修改过的分配例程分配所有内容,并且您知道数组的起始地址)。