【问题标题】:How to make the compiler check an array length?如何让编译器检查数组长度?
【发布时间】:2020-10-10 00:32:07
【问题描述】:

我定义了一个函数:

void myFunction(char string[20])
{
    // update my string here, maximal size is 20.
}

调用函数

char st[5];
myFunction(st);

是一个错误,因为myFunction 破坏了导致未定义行为的堆栈。当然st 的大小应该是 20(或更大)。

有没有办法让 c 编译器在发生此类错误时抱怨,以及编写此类调用的推荐方法是什么?

【问题讨论】:

  • 知道数组长度(作为函数参数实际上是一个指针)的唯一方法是将长度作为另一个参数传递。编译器将忽略定义中的20:它仅用于多维数组,以便编译器知道如何对其进行索引,例如char string[][20]。不破坏数组边界是 C 语言程序员的全部责任(初始化数据除外,作为数组定义的一部分)。
  • 通常有帮助的方法是定义计数/长度并在任何地方使用(例如):#define COUNT 20 并执行:char st[COUNT];void myFunction(char string[COUNT]) { for (int i = 0; i < COUNT; ++i) string[i] = ...; }。这并不完全是您所要求的,但它比在所有地方硬连线计数要好。它还允许轻松更改计数(例如#define COUNT 37)并自动调整所有位置。

标签: c arrays string definition


【解决方案1】:

当然 st 的大小应该是 20(或更大)。

您可以利用 C99 的静态数组索引功能,可在 function declarators 中使用。

如果关键字 static 也出现在数组类型派生的 [ 和 ] 中,那么对于函数的每次调用,对应的实际参数的值应提供对数组第一个元素的访问,该数组的第一个元素至少与由大小表达式指定的元素。

所以,你可以将函数声明为

void myFunction(char string[static 20])
{
    /* ... */
}

相反。如果传递的数组参数包含少于 20 个元素,这将触发编译器的警告。

例如,当使用st 类型为char [5] 调用myFunction 时,clang 10 给我warning: array argument is too small; contains 5 elements, callee requires at least 20 [-Warray-bounds]

不过,目前似乎只有 clang 支持 -Warray-bounds,而 GCC 计划在 future 中添加支持。

请注意,这仅适用于数组的编译时,但它也会在传入 NULL 指针时发出警告(如果可以在编译时确定)。但是,有人可能会将指针传递给一个动态分配的小于 20 个元素的数组,而编译器无法在编译时对其进行静态检查。

所以对于这种情况,只有当参数的声明类型为 char [N] 时,它才能正常工作,其中 N 是它的大小,N 至少为 20。如果您希望处理这两种情况,则需要显式的大小参数, 并在函数内部进行检查以在传入的大小小于 20 的情况下返回错误。

【讨论】:

  • 可能会触发来自编译器的警告。 C 标准不需要实现来诊断这一点,编译器可能会也可能不会,即使它可以看到用作参数的数组的维度。而且,当然,根据情况,正如您所提到的,编译器可能无法看到这一点,因为它正在传递从其他地方接收到的指针或以复杂的方式计算。例如,Apple Clang 11.0 在使用 myFunction(st) 时会诊断此问题,但在使用 myFunction(st+1) 时不会诊断。
  • @EricPostpischil 正确。将参数定义为char string[static 20] 而不是char string[20] 所做的所有事情都会导致 将指针传递给至少20 个元素的数组的初始元素的任何调用具有未定义的行为。它允许警告,但符合标准的编译器可以默默地忽略static(和20)。
  • 对,但请注意,原始问题提出了两个要求,编译器在编译时检查传入的数组的大小,并且它至少 20 (第一个引号),这两者都只能通过使用静态和实现警告的编译器来实现。因此,尽管该标准确实不需要符合标准的编译器来实现警告,但将其用于执行警告的编译器很有用。
【解决方案2】:

你可以封装数组:

struct string20 { char string[20]; };
void myFunction(struct string20 *string)
{
}

或传递指向数组的指针(!=指向第一项的指针):

void myFunction( char (*string)[20] )
{
}
//....
char st[5];
char st20[20];
myFunction(&st); //WRONG; diagnosed error
myFunction(&st20); //OK

使用这两种方法中的任何一种,您都可以获得编译器诊断,无论您的平台如何 (只要是符合标准的平台)。

【讨论】:

    【解决方案3】:

    您的函数如下所示:

    void myFunction(char string[20])
    {
        // update my string here, maximal size is 20.
    }
    

    但是,编译器是这样看的:

    void myFunction(char *string)
    {
        // update my string here, maximal size is 20.
    }
    

    大小“20”对编译器没有任何意义。您可以使用任意长度的数组调用函数。

    关于通话:

    char st[5];
    myFunction(st);
    

    没关系。

    如果你想强制一个特定的数组大小,你可以将数组包装在一个结构中。

    【讨论】:

    • 大小“20”对编译器来说并不意味着什么,编译器也不会将数组参数声明仅仅视为指针参数声明。尽管 C 标准规定将数组参数声明调整为指针声明,但该声明在调整之前受到约束和处理。例如void MyFunction(char string[20][])会因为数组元素类型不完整而产生错误,但void MyFunction(char (*string)[])不会。
    • void MyFunction(char string[puts("foo")]) 的调用可能会打印“foo”(如果我没记错的话,在 GCC 和 Clang 中会打印,但 C 标准对所需行为并不清楚),表明数组声明有效果除了只是一个指针声明。
    • @EricPostpischil,C 标准说“如果大小是一个不是整数常量表达式的表达式:如果它出现在函数原型范围的声明中,它被视为被替换为*”。我认为这意味着符合标准的实现不会评估您示例中的 puts 调用。
    • @JohnBollinger:函数原型范围仅用于非定义声明,根据 C 2018 6.2.1 4. 在函数定义中,参数声明具有块范围。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2016-10-29
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多