如何通过程序确定 C++ 数组的大小?

63

这个问题是受到一个类似的问题的启发:How does delete[] “know” the size of the operand array?

我的问题略有不同:在 C++ 中是否有办法通过程序来确定数组的大小?如果没有,为什么? 我见过的每个接受数组作为参数的函数都需要一个整数参数来指定其大小。但正如链接的问题所指出的那样,delete[] 必须知道要释放的内存的大小。

考虑下面这段 C++ 代码:

int* arr = new int[256];
printf("Size of arr: %d\n", sizeof(arr));

这个代码打印出来的是"Size of arr: 4",它只是指针的大小。虽然很希望有一个函数可以打印出256,但我不认为C++里面有这样的函数。(同样,问题的一部分是为什么它不存在。)

澄清: 我知道如果我在栈上声明数组而不是堆上(即 "int arr [256]; "),sizeof 运算符将返回1024(数组长度 * sizeof (int))。


实际上,如果你在栈上分配了该数组,sizeof运算符将返回1024 -- 这是256(元素数量)* 4(单个元素的大小)。(sizeof(arr)/sizeof(arr[0])) 将给出结果256。 - Kevin
谢谢,我之前忽略了这个问题,因为我在测试代码中实际上使用的是char[](而且sizeof(char) == 1)。 - Kip
虽然这只是假设性的,因为它不起作用,但我必须指出,你应该写成printf("Size of arr: %d\n", sizeof(*arr));而不是printf("Size of arr: %d\n", sizeof(*arr));,因为你想要检索解引用指针的大小。 - mg30rg
20个回答

70

delete [] 知道分配的大小,但是这个信息存储在运行时或操作系统的内存管理器中,在编译期间编译器无法访问。而 sizeof() 不是一个真正的函数,实际上它被编译器计算为常量,对于动态分配的数组,其大小在编译期间是未知的,因此编译器无法计算其大小。

另外,请考虑以下示例:


int *arr = new int[256];
int *p = &arr[100];
printf("Size: %d\n", sizeof(p));
编译器如何知道p的大小呢?问题的根源在于,在C和C++中,数组不是一等对象。它们会衰减为指针,并且编译器或程序本身无法知道指针是指向由new分配的内存块的开头,还是单个对象,或者是指向由new分配的内存块的中间位置。
其中一个原因是,C和C ++将内存管理留给程序员和操作系统,这也是它们没有垃圾回收的原因之一。newdelete的实现不是C++标准的一部分,因为C++旨在用于各种平台,这些平台可能以非常不同的方式管理其内存。如果您正在为运行在最新的Intel CPU上的Windows框中的文字处理器编写代码,则可能可以让C ++跟踪所有已分配的数组及其大小,但如果您正在为运行在DSP上的嵌入式系统编写代码,则完全不可行。

7
C++ 中绝对存在数组。否则你怎么解释这个代码 "char x[4]; size_t sz = sizeof(x);" 会使 'sz' 被赋值为 4 呢? - Kevin
11
Dima,绝对存在数组。数组与指针不同。不幸的是,许多老师会混淆它们并告诉他们的学生它们只是指针。不,它们不是。你怎么解释这个:char const**s = &"bar"; 无法编译?[...] - Johannes Schaub - litb
8
"litb,char const **s =&“bar”无法编译的原因是“bar”是常量而不是lvalue,因此您无法获取其地址。这与int *p =&5相同;也无法编译。" - Dima
4
很明显,但几乎所有东西都是错误的。现在已经有一种情况,即sizeof是运行时而不是编译时,数组确实存在,实现方式可以知道所有数组的大小。即使是 DSP 进行分配也必须保留大小信息。 - Mooing Duck
8
void foo(int *a); 接收一个指针,void foo(int (&a)[5]); 接收一个数组。数组名衰减为指针,这很糟糕,但这并不意味着数组和指针是同一种东西。 - Cat Plus Plus
显示剩余23条评论

20

实际上有一种方法可以确定大小, 但它不是"安全的", 并且会因编译器而异...所以根本不应该使用.

当你这样做时: int* arr = new int[256];

256是无关紧要的,假设对于这种情况为1024,您将获得256*sizeof(int)的值,该值将存储在(arr - 4)中。

所以给你"项"的数量

int* p_iToSize = arr - 4;

printf("Number of items %d", *p_iToSize / sizeof(int));

对于每个malloc、new或者其他连续内存块之前,还分配了一个空间,用于保留有关给定内存块的一些信息。


11
不过,这实际上回答了这个问题。 - A. Rex
1
作为额外的建议,你可以重载“new”并实现内存管理,你可以像Joao描述的那样,或者将每个指针与其对应的大小存储在映射中...总之有很多疯狂的方法可以实现它,但我不会使用它们 :p - chrispepper1989
字符数组怎么样?char * arr = new char[100]; - Jai

19

不,标准C++中没有这样的方法。

我不知道为什么没有这个功能,可能是因为尺寸被视为实现细节,最好不要公开。请注意,当您使用malloc(1000)时,不能保证返回的块是1000个字节,只能保证它至少是1000个字节。 最有可能是1020个字节(1K减去4个字节的开销)。 在这种情况下,“1020”大小是运行时库需要记住的重要内容。 当然,这会因实现而异。

这就是为什么标准委员会添加了std:vector<>,它确实可以跟踪其确切大小。


4
需要注意的一点是,new[]也会存储所请求的项目数量,以便为数组调用正确数量的构造函数和析构函数。这个存储位置再次取决于具体实现。不包括获取它的方法超出了我的理解范围。 - workmad3
1
我认为“好的理由”是数组根本不是对象。数组只是一个原始的内存块。大小是内存管理数据,而不是对象数据。你可以编写一个Array类来跟踪内存和大小,但你也可以使用std::vector而不必担心它。 - Herms
3
啊哈...当然。一个int*无法知道它所指向的数组是一个new出来的数组还是一个局部数组,或者是数组中间的某个位置。 - James Curran
2
@Herms:std::string[10] 绝对不是原始内存,但它是一个数组。 - MSalters
1
对于 workmad3,可能仅适用于具有非平凡析构函数的项目和具有用户定义的 operator delete 的类型,该操作符希望知道大小。对于其他任何内容,不存储数字就足够了。 - Johannes Schaub - litb
如果有一个函数可以返回分配的大小,尤其是当它大于所需大小时,那将非常方便。 - Mooing Duck

5

处理这种情况的常见方法是使用向量(vector)。

int main()
{
   std::vector<int> v(256);
   printf("size of v is %i capacity is %i\n", sizeof(int) * v.size(), sizeof(int) * v.capacity());
}

或者预定义大小

const int arrSize = 256;
int main()
{
    int array[arrSize];
    printf("Size of array is %i", sizeof(int) * arrSize);
}

sizeof(int) * arrSizemalloc('sizeof(int) * arrSize') 是一样的,不是吗? - kAmol

4

C++决定添加新的类型安全的malloc,因此new必须知道元素的大小和数量以调用构造函数,而delete则是为了调用析构函数。在早期,您必须实际上将您传递给new的对象数量传递给delete。

string* p = new string[5];
delete[5] p;

然而,他们认为如果使用new<type>[],则数字的开销很小。因此,他们决定new[n]必须记住n并将其传递给delete。有三种主要的实现方式。

  1. 保留指向大小的哈希表
  2. 直接在向量附近写入它
  3. 完全做些不同的事情

也许可以通过以下方式获取大小:

size_t* p = new size_t[10];
cout << p[-1] << endl;
// Or
cout << p[11] << endl;

或者根本不是这些。

3

根据您的应用程序,您可以在数组末尾创建一个“哨兵值”。

哨兵值必须具有某些独特的属性。

然后,您可以对数组进行处理(或进行线性搜索)以查找哨兵值,并随着搜索计数。一旦到达哨兵值,您就得到了数组计数。

对于简单的C字符串,终止符\0是哨兵值的一个例子。


3

一些神奇的东西:

template <typename T, size_t S>
inline
size_t array_size(const T (&v)[S]) 
{ 
    return S; 
}

这就是我们在C++11中的实现方式:

template<typename T, size_t S>
constexpr 
auto array_size(const T (&)[S]) -> size_t
{ 
    return S; 
}

非常有用且美观的解决方案。只有一件事:我会使用size_t作为第二个模板参数。 - besworland

2
那是因为您的变量arr只是一个指针。它保存了内存中特定位置的地址,但并不知道该地址所代表的内容。您声明它为int*,这使得编译器在您增加指针时有一些提示。除此之外,您可能会指向数组的开头或结尾,也可能指向堆栈或无效内存。但我同意您的观点,无法调用sizeof确实很烦人 :)

1
但是系统不知何故知道数组的大小,否则“delete[] arr”将无法工作。 - Kip
系统在运行时知道,但sizeof是编译时调用。 - Peter Kühne
delete[] arr 只能知道数组的大小,但无法确定数组是否在堆上分配的。 - Alexander

2

在C++中,仅通过指针,无法确定动态分配数组的大小。C++非常灵活,赋予用户更多权力。例如,标准没有定义内存分配器必须如何工作,例如添加所需大小头文件。不需要头文件可以提供更多的灵活性。

举个例子,考虑一个char *数组实现的字符串。通常使用指向数组中间的指针来选择子字符串。例如,在标准C库中查看strtok函数。如果需要在每个数组之前嵌入某些头文件,则需要破坏子字符串之前的部分数组。

处理头文件的另一种方法是,在一个内存块中具有数组头,并将它们指向其他地方的原始数组内存。在许多情况下,这将需要为每个引用进行两个指针查找,这将对性能产生很大的影响。有克服这些缺陷的方法,但它们增加了复杂性并降低了实现的灵活性。

std::vector模板是我最喜欢的将数组大小绑定到数组本身的方式。

C是具有更好语法的可移植汇编语言。


strtok如果数组有头文件也会完全相同,因为strtok接受字符指针而不是数组。 - Mooing Duck

2
现在有一个名为std::array的高效编译时常量大小数组封装:std::array
#include <array>

int main (int argc, char** argv)
{
    std::array<int, 256> arr;
    printf("Size of arr: %ld\n", arr.size());
}

参数是<类型,#元素>
您还可以获得一些其他便利功能,例如迭代器、empty()和max_size()。

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