C++ 参数堆与堆栈,基元类型的数组

C++ Arguments Heap vs Stack, Arrays of primitive types

本文关键字:类型 数组 参数 堆栈 C++      更新时间:2023-10-16

我发现了一些关于对象和STL的很棒的"堆与堆栈"问题和答案。 (c ++参数堆与堆栈) (请让我知道STL对象(如向量或队列等)的行为是否与自定义类对象不同。

在处理编程竞赛问题之一时,我突然想到原始类型的数组也作为参考传递,而不是手动复制(例如,f(bool boo[10][10]))。

我通过使用基元类型的本地数组并将参数复制到本地数组中来解决此问题。

布尔local_boo[10][10]; memcpy(local_boo, boo, sizeof(bool) * 10 * 10);

此外,可以安全地假设,我只会在已知且固定基元类型的数组大小时尝试为每个函数调用提供boo的单个副本。

这是我的问题。

1)有没有更好的方法?

2)有谁知道任何好的参考专门针对参数在函数调用期间是否将通过堆或堆栈传递?

3)除了打印变量的地址并通过检查地址来确定之外,是否有更好的方法来确定是否在堆或堆栈上声明了某些内容?

谢谢。

2)有谁知道任何好的参考专门针对参数在函数调用期间是否将通过堆或堆栈传递?

C++标准中没有提到调用堆栈(它是一个实现细节;通过阅读 n3337 或一些更新的标准进行检查)。调用约定特定于某些实现,因此请研究系统的 ABI。自动变量可能不会位于堆栈上(例如,它们可能只在寄存器中,或者在编译时完全消失)。

在 x86-64 上,大多数 ABI(请参阅此处)定义前几个(通常为 6 个)参数,当它们是"标量"时,通过寄存器传递(细节,例如参数类型,非常重要:浮点值的传递方式与整数不同)。此外,两个标量分量的普通结构在返回时,会在大多数这些 ABI 中的两个寄存器中传递。

此外,大多数C++编译器在实践中都在优化编译器,它们可以(并且确实)以不同的方式编译调用(例如,它们经常执行一些函数内联 - 即使没有任何inline注释),因此参数的传递实际上是特定于实现的(并且可能从一个调用站点到另一个相同调用函数的调用站点而异)。

但是,原始数组会衰减为指针。另请阅读有关普通旧数据的信息。

您可能会想到"在堆上传递的参数"(但这是一种简化,而不是真实的东西),如果涉及一些通过堆分配的内存传递一些数据的复制构造函数。另请阅读有关五法则和 RAII 的信息。

在某些情况下,根据 as-if 规则,允许优化编译器用其他内容(包括一些"堆栈分配")替换一些内部堆分配。

与问题无关,但名称 STL已被弃用多年,正确的名称是标准C++库。

接下来,关于如何将对象传递给函数,您应该知道数组不是C++中的一类元素。自 C++ 11 起(草案 n3337):

数组到指针的转换 [conv.array]

可以将类型为"N T 数组"或"未知 T 边界数组"的左值或右值转换为 prvalue 类型为"指向 T 的指针"。结果是指向数组的第一个元素的指针。

因此,当您尝试将数组传递给函数时,它会衰减(如果是多维的,则递归)到指向其第一个元素的指针。然后传递该指针(按值),这几乎等同于数组的引用调用传递

但所有其他对象都是按值传递的,无论是自定义对象还是标准库中的对象。

对于堆栈与堆的问题,它与标准无关,因为C++没有头和堆栈的概念,而只有自动变量与动态变量。简单的常见实现将堆栈用于自动变量,将堆用于动态内存。

让我们更进一步:

void f() {
double arr_auto[1000];       // automatic array (stack on common implementations)
//  will be automatically destroyed at end of block
double* arr_heap = new int[1000]; // dynamically allocated array
//  will require an explicit delete[]
std::vector<int> vec(1000);  // vec is an automatic object (stack), 
//  hosting a dynamic array of 1000 doubles
// everything (vector and content) will be destroyed at end of block
g1(arr_auto);    // actually passes &(arr_auto[0]) a mere pointer to an automatic array
g1(arr_heap);    // equally passes a pointer but to a dynamically allocated array
g2(vec);         // assuming g2 is declared g2(vector<int>& v) passes a reference
g3(vec);         // assuming g3 is declared g3(vector<int> v) constructs a copy of vec
...
}

对于最后一种情况(按值传递对象),通过复制构造函数构造对象的自动副本。标准库中的容器确实为其内容分配动态内存 - 它们的析构函数确保在其生命周期结束时释放动态内存。因此,您可以获得在动态内存(常见实现的堆)中分配的原始值的完整副本,而无需关心显式删除。

希望现在更清楚...

1)有更好的方法吗?

声明数组时,不能按值将其传递给函数,因为您没有复制构造函数和operator=。 要解决此问题,您最好使用std::array. 在您的情况下,它可以是:

typedef std::array<std::array<bool,10>,10> bool10x10Array;

您将获得默认值和复制构造函数,operator=内存将在堆栈上分配给对象声明。

2)有谁知道有什么好的参考资料专门针对参数是在 函数调用?

参数永远不会通过堆传递。参数传递的方式取决于调用约定,当你传递指针或引用时,你只是传递内存块的地址,你不是复制所有的内存。如果要处理副本,则必须复制内存块并传递副本地址。如果您使用structclass您可以使用它们的复制构造函数,并在按通常复制到堆栈的值传递任何内容时按值传递它们。

3)有没有更好的方法来确定某事是否被声明在 堆或堆栈,而不是打印变量的地址和 通过检查地址来确定?

声明局部变量时,对象将始终在堆栈上分配。 使用动态分配时,它将始终在堆上分配。 但您必须注意,对于基元类型的标量和数组也是如此。类可以在堆栈上声明,也可以使用动态内存。

参数总是在堆栈上传递。唯一一次参数"在堆上传递"(它们从未真正传递过)是在创建对象的新副本时,并且该对象使用 new/malloc 在堆上分配内存。

按值发送数组的两种方法是将数组封装在结构中(C 方式),或使用 std::array(C++方式)。

其中大部分已经在评论中得到了回答(感谢@Richard Critten,@Bo Persson和@Lundin),但我想一个完整的答案会更好。