代码之家  ›  专栏  ›  技术社区  ›  Chi_Iroh Chris Dodd

从自己的参数递归调用main(用VLA滥用sizeof)是标准C99吗?

  •  1
  • Chi_Iroh Chris Dodd  · 技术社区  · 2 年前

    免责声明:我的问题根本不实用,它更多的是关于两个据称滥用规则并在一定程度上编译的代码的问题(根据编译器的说法,或多或少有警告/错误)。

    据我所知,以下代码是有效的C99:

    #include <stdio.h>
    #include <stddef.h>
    
    int main(void) {
        size_t n = sizeof(int[printf("%s", "Hello")]);
    }
    

    我知道sizeof不会计算表达式,除非它是一个VLA(因为它的大小是非恒定的)。

    然而,我不确定这段代码是否有效C99:

    #include <stdio.h>
    
    int main(int argc, char** argv);
    int main(int argc, char* argv[sizeof(int[printf("%s\n", "Hello") + main(0, NULL)])]) {}
    

    Run it on Compiler Explorer

    • 有一个大小为的数组有效吗 sizeof(some VLA) ?
    • 这样的声明有效吗 int main(int, char**) 以及类似的定义 int main(int, char*[some size]) ?
    • 从函数自身的参数递归调用函数有效吗?
    • 是否保证递归会发生,或者编译器有权对其进行优化?如果发生递归,是否保证会出现堆栈溢出?

    我想做一个大小的数组 sizeof(VLA) 是有效的,我不知道任何规则会使其无效。
    如果我没记错的话, char*[some size] 函数参数衰减为 char** ,但我不知道它是否格式错误,因为声明/定义不完全匹配。
    我不知道从参数中递归调用main是否有效,我显然没有找到任何资源或类似代码的示例。此外,main与其他函数在参数中的递归调用有不同的规则吗?
    我想编译器可能不会优化它(我想它不是UB),所以我也认为堆栈溢出总是会发生。

    我们将非常感谢标准的报价,非常感谢。

    1 回复  |  直到 2 年前
        1
  •  2
  •   John Bollinger    2 年前
    • 有一个大小为的数组有效吗 sizeof(some VLA) ?

    C99的主要相关规定如下:


    这个 [ ] 可以对表达式进行定界,或者 * 。如果他们 定界表达式(指定数组的大小) 表达式应具有整数类型。如果表达式是常量 表达式,则其值应大于零。[...]

    一种普通标识符(如6.2.3所定义),具有可变的 修改型应具有闭塞范围,且无联动或功能 原型范围。如果标识符被声明为具有的对象 静态存储持续时间,它不应具有可变长度的数组 类型

    (C99 6.7.5.2/1-2)


    请注意,指定数组大小(如果存在)的表达式的构成的唯一约束是它具有整数类型。使用上下文对所涉及的运算符或操作数没有其他要求。


    如果大小是一个整数常量表达式,并且元素类型具有 已知的常量大小,数组类型不是可变长度数组 类型否则,数组类型为可变长度数组类型。

    如果大小是一个不是整数常量的表达式 表达式:如果它出现在函数原型范围的声明中, 它被视为被*取代;否则,每次都是 经评估,其值应大于零。每个的大小 可变长度数组类型的实例在其 一生其中,大小表达式是sizeof的操作数的一部分 运算符,并且更改大小表达式的值将不会 影响运算符的结果,未指定是否 对大小表达式进行评估。

    (C99 6.7.5.2/4-5)


    请注意,最后一句话不适用于您的特定情况;我把它包括在内是为了完整性,因为它确实涉及混合 sizeof 和VLA。

    这里唯一的附加要求是该值大于0,但对于总是如此 的大小 表达式(没有零大小的对象)。

    • 这样的声明有效吗 int main(int, char**) 以及类似的定义 int main(int, char*[some size]) ?

    C99的主要相关规定如下:


    同一范围内引用同一对象或的所有声明 函数应指定兼容的类型。

    (C99 6.7/4)


    请注意,可以考虑同一函数的多个声明。


    对于要兼容的两种功能类型,两者都应指定兼容 返回类型。此外参数类型列表如果两者都存在, 应在参数数量和省略号使用方面达成一致 终止子;相应的参数应具有兼容的类型。 […](在确定类型兼容性和复合材料时 类型,用函数或数组类型声明的每个参数都被视为 具有已调整的类型和用限定的声明的每个参数 类型被视为具有其声明类型的非限定版本。)

    (C99 6.7.5.3/15)


    请特别注意,参数类型的比较使用 已调整 类型这是指


    将参数声明为“类型的数组”应调整为“指向类型的合格指针”,其中类型限定符(如果有)是在数组类型派生的[和]中指定的限定符。

    (C99 6.7.5.3/7)


    这种调整使你的 char** char*[some size] 其中它们作为函数参数的类型出现。

    • 从函数自身的参数递归调用函数有效吗?

    这取决于情况。

    C99不允许调用未声明的函数(尽管许多编译器接受这些函数作为向后兼容性扩展),并且函数标识符在声明完成后才在作用域中。因此,函数声明中的表达式不能调用该函数,除非前面有同一函数的另一个声明。通常情况并非如此,但您的示例会注意提供它。

    否则,如前所述,VLA中大小表达式形式的唯一限制是它具有整数类型。这不受VLA声明出现的上下文的限制。

    • 是否保证递归会发生,或者编译器有权对其进行优化?

    这里的主要规定是


    对一致性实施的最低要求是:

    [...]

    • 在程序终止时,写入文件的所有数据应与根据抽象语义执行程序所产生的结果相同。

    [...]

    (C99 5.1.2.3/5)


    这是关于总体上可以执行哪些优化的主要规定。假设我们进行解释 printf() 要生成文件的输出(这是通常的解释),不允许优化掉 printf 发出任何输出的调用。 然而 ,编译器被允许将其从递归转换为循环。

    如果递归会发生,是吗 保证会有堆栈溢出?

    即使编译器将其实现为递归,语言规范也没有说明堆栈或堆栈溢出。这坚定地体现在实施细节方面。

    此外,即使在使用调用堆栈的实现上,因此很容易受到堆栈溢出的影响,编译器也可以在这种特殊情况下通过观察它 递归,因此每个递归调用都可以重用相同的堆栈帧。

        2
  •  1
  •   Barmar    2 年前

    拥有一个大小为的数组(一些VLA)有效吗?

    当然当声明VLA时,大小可以是任何表达式。没有理由 sizeof 表达将被禁止。

    char new_array[sizeof VLA];
    

    相当于

    size_t n = sizeof VLA;
    char new_array[n];
    

    有一个像int main(int,char**)这样的声明和一个像int-main(int,char*[some-size])这样的定义有效吗?

    对在声明中,它们是等价的。

    从函数自身的参数递归调用函数有效吗?

    在声明之后的任何时候都可以调用该函数。所以你只需要一个正向声明,就像你需要一个在调用它的函数之后定义的函数一样。

    是否保证递归会发生,或者编译器有权对其进行优化?

    如果优化器能够确定它不会改变定义良好的结果,那么它就可以对其进行优化。如果实际递归会导致无限递归,并且优化器可以确定这一点,那么它可以对其进行优化,因为它可以假设不会有任何未定义的行为。

        3
  •  1
  •   Chi_Iroh Chris Dodd    2 年前

    是否保证递归会发生,或者编译器有权对其进行优化?如果发生递归,是否保证会出现堆栈溢出?

    规范中有一条规定:

    如果大小是一个不是整数常量表达式的表达式:如果它出现在函数原型范围的声明中,则将其视为被替换 *

    (§ 6.7.5.2.5)

    这意味着您的代码示例等效于

    int main(int argc, char* argv[*])]) {}
    

    并且根本不应该评估大小中的表达式。然而,gcc似乎并不符合这一点。