【问题标题】:Variable length array parameter size expression with side effects具有副作用的可变长度数组参数大小表达式
【发布时间】:2019-06-20 04:12:43
【问题描述】:

这个问题源于 Eric Postpischil in another thread. 的评论

我很难理解使用可变长度数组 (VLA) 作为函数参数:

  • 未检查数组大小。
  • 数组大小无法从数组中恢复,因为标准类型调整数组 -> 指针也适用于 VLA,如下面的 sizeof() 调用所示;即使完全有可能将整个数组传递到堆栈上,就像 VLA 在定义时在堆栈上创建一样。
  • 大小必须作为附加参数传递,与指针一样。

那么为什么语言允许使用 VLA 参数声明函数,如果它们没有提供任何优势并且像指针的任何其他数组参数一样被调整?如果语言不使用大小表达式(例如检查实际参数的大小)并且在函数内部无法获得(仍然必须为此传递显式变量),为什么要评估大小表达式??

为了更清楚我的困惑,请考虑以下程序(现场示例here)。所有函数声明显然是等价的。但正如 Eric 在另一个线程中指出的那样,函数声明中的参数大小表达式在运行时评估。大小表达式不会被忽略。

我不清楚这会有什么好处,因为大小及其评估没有任何影响(除了可能的副作用)。特别是,重复我自己,该信息不能被函数内部的代码使用。最明显的变化是在堆栈上传递 VLA,就像结构一样。毕竟,它们通常也在调用方的堆栈上。但是就像固定长度的数组一样,类型在声明时就已经调整为一个指针——下面的所有声明都是等价的。尽管如此,还是会评估无用和丢弃的数组大小表达式。

#include <stdio.h>

// Nothing to see here.
extern void ptr(int *arr);

// Identical to the above.
extern void ptr(int arr[]);

// Still identical. Is 1 evaluated? Who knows ;-).
extern void ptr(int arr[1]);

// Is printf evaluated when called? Yes.
// But the array is still adjusted to a pointer.
void ptr(int arr[printf("Call-time evaluation of size parameter\n")]){}

// This would not compile, so the declarations above must be equivalent.
// extern void ptr(int **p);

int main()
{
    ptr(0);
    ptr(0);

    return 0;
}

【问题讨论】:

  • @ChronoKitsune:这个问题主要不是关于阵列衰减。实际上,它根本不涉及数组衰减;涉及的具体问题是另一方面,参数类型调整。但这不是主要问题。
  • @ChronoKitsune 问题涉及数组衰减(或者更确切地说,参数类型调整),但这不是问题的主要焦点。该问题询问评估大小参数的时间。如果将 VLA 视为普通数组,我还想知道允许 VLA 作为函数参数的基本原理。你知道,它们可以像结构一样按值传递;如a Q&A I posted a while ago. 中所述,使用固定大小的数组仅具有有限的意义。
  • @ChronoKitsune 那么为什么根本不使用 VLA 参数的大小,甚至不是实际参数的大小??
  • @JeanFrancois 这不是您链接的众所周知的问题的重复。
  • @JeanFrancois:这个问题不是关于何时或为何将数组转换为指针或何时将数组参数类型调整为指针类型。这个问题是关于数组参数中大小表达式的评估。请不要不恰当地将问题标记为重复。

标签: c parameter-passing language-lawyer variable-length-array


【解决方案1】:

... 可以省略 size 变量和 ... 将在运行时评估的大小,...;但有什么用呢?

顶级尺寸信息丢失了?,在数组参数中现在是一个指针参数。然而,使用 2D VLA 函数参数变成指向 1D 数组的指针,代码知道该数组维度。

void g(size_t size, size_t size2, int arr[size][size2]) {
  printf("g: %zu\n", sizeof(arr));
  printf("g: %zu\n", sizeof(arr[0]));
}

int main(void) {
  int arr[10][7];
  g(10, 7, arr);
}

输出

g: 8   pointer size
g: 28  7 * int size

或者,传递一个指向数组的指针。

void g2(size_t size, size_t size2, int (*arr)[size][size2]) {
  printf("g2: %zu\n", sizeof(arr));
  printf("g2: %zu\n", sizeof(*arr));
}

int main(void) {
  int arr[10][7];
  g2(10, 7, &arr);
}

输出

g2: 8    pointer size
g2: 280  10 * 7 * int size

【讨论】:

  • 我知道 2D 数组的语法和语义以及数组参数中最左边的大小无关紧要(因为它实际上是一个指针声明)。我的问题是为什么没有更聪明地处理 VLA,例如像结构一样按值传递。
  • @PeterA.Schneider 公平地说,您编辑的帖子现在有“我的问题是为什么 VLA 是……”在 2 个答案后几个小时。最好在初始帖子中添加明确的问题。我只找到了这个答案中解释过的那个。
  • 抱歉编辑迟了。我只在问题的第一个版本中表达了困惑,并且只有在我明白我不清楚我在问什么(如果有的话)之后才添加反映困惑的明确问题(也许它比任何东西都更重要的是发泄和摸索精神保持)。这个问题是由以下认识引发的:在现代 C 中,函数的 declaration 中的大小表达式完全被评估(而在 C89 中它被忽略,因为它甚至不是数组声明,并且正如您正确解释的那样,信息丢失了)。
  • 好点是你可以有一个通用的函数,它带有一个指向二维数组的指针,它的第二维也是可变的。 对于固定大小的数组确实是不可能的(对于更复杂且通常需要动态分配的数组,需要一个指针数组)。
【解决方案2】:

C 2018 6.9.1 讨论函数定义并在第 10 段中告诉我们:

在进入函数时,会评估每个可变修改参数的大小表达式……

根据 6.7.6 3,可变修改类型是在其声明符中具有可变长度数组类型的类型,可能是嵌套的。 (所以int a[n] 是可变长度的,因此它是可变长度的数组,固定长度的int (*a[3])[n] 也是可变长度的,因为它嵌套在其中的是可变长度的数组类型。)

void foo(int n, int a[][n]) 的情况下,我们看到必须计算n,因为编译器需要大小来计算a[i][j] 等表达式的地址。但是,对于void foo(int n, int a[n]),这个需求是不存在的,我不清楚上面引用的文字是适用于调整前(int a[n])还是调整后(int *a)的参数类型。

我记得,几年前当我第一次注意到这个问题时,我发现一个编译器对直接数组参数计算表达式和一个没有计算表达式的编译器。根据编译器,调用 foo 定义的 void foo(int a[printf("Hello, world.\n")]) {} 会或不会打印字符串。目前,在 macOS 10.14.2 上使用 Apple LLVM 10.0.0 和 clang-1000.11.45.5 编译会打印字符串。 (如上所述,对于嵌套数组类型,必须计算表达式,并且我尝试的所有编译器都展示了这一点。不幸的是,我目前不记得这些是哪些编译器。)

尚不清楚数组大小对编译器是否有用。 C 标准的这一方面可能还没有完全解决。有一个功能可以为尺寸增加一些含义;如果使用static 声明大小:

void foo(int a[static SomeExpression]) { … }

那么,根据 6.7.6.3 7,a 必须至少指向 SomeExpression 元素。这意味着a 不能为空,编译器可以使用它来优化某些东西。但是,我没有任何例子说明数字本身如何帮助优化或编译的其他方面。

【讨论】:

  • 这个问题当然是您在另一个线程中的评论的衍生。卓越的 C 功能,直到今天我才知道。感谢您的回答。
  • @PeterA.Schneider:int (puts) (); int main(p, q) int p; char *q [(puts) (&amp;*"Hello, world.")]; {} 是一个完整的 C 程序,即使使用 -std=c11 -pedantic -Wall,它在 Clang 中编译时也不会出现警告。永远不要使用它。 :-)
【解决方案3】:

我认为 VLA 作为函数参数没有任何实际用途。

只有指向数组的指针才有意义,因为这样可以更轻松地遍历数组并提供正确的大小信息

int foo(int (*p)[2])
{

    printf("sizeof int = %zu\n", sizeof(int));
    printf("p + 0:%p\n", (void *)p);
    printf("p + 1:%p\n", (void *)(p + 1));
}

void g(size_t size, size_t size2, int (*arr)[size][size2]) 
{
  printf("g: %zu\n", sizeof(*arr));
  printf("g: %zu\n", sizeof(*arr[0]));
}


int main()
{
    foo(0);
    g(5,5,0);

    return 0;
}

sizeof int = 4                                                                                                                                                                                                                                              
p + 0:(nil)                                                                                                                                                                                                                                                 
p + 1:0x8                                                                                                                                                                                                                                                   
g: 100                                                                                                                                                                                                                                                      
g: 20     

【讨论】:

    猜你喜欢
    • 2022-09-23
    • 1970-01-01
    • 1970-01-01
    • 2011-11-27
    • 1970-01-01
    • 1970-01-01
    • 2022-09-23
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多