变量参数列表和空指针

Edg*_*jān 9 c pointers

请考虑以下代码:

#include <stdarg.h>
#include <stdlib.h>
#include <stdio.h>

void foo(const char *arg, ...) {
    va_list args_list;

    va_start(args_list, arg);

    for (const char *str = arg;
         str != NULL;
         str = va_arg(args_list, const char *)) {
        printf("%s\n", str);
    }

    va_end(args_list);
}

int main(int argc, char **argv) {
    foo("Some", "arguments", "for", "foo", NULL);
    foo("Some", "arguments", "for", "foo", 0);
    return 0;
}
Run Code Online (Sandbox Code Playgroud)

我们可以看到,foo()使用变量参数列表来获取字符串列表然后将它们全部打印出来.假设最后一个参数是空指针,因此处理参数列表直到NULL被检测到.

函数以两种不同的方式foo()调用main(),使用NULL0作为最后一个参数.

我的问题是:第二次调用0是否正确?

我想,我们不应该叫foo()0.原因是在这种情况下,例如,编译器无法从上下文中猜测0应该被视为空指针.所以它将它作为通常的整数处理.然后foo()处理0并投射到const char*.当空指针具有不同的内部表示时,魔法就开始了0.至于我能理解它导致失败的检查str != NULL(因为str将等于0铸造到const char*从我们的情况下空指针不同)和错误的程序的行为.

我的想法是对的吗?任何好的解释都表示赞赏.

Nat*_*dge 8

一般来说,这两个电话都不正确.

裸露的呼叫0肯定是不正确的,但不是因为你陈述的原因.在编译对foo()具有可变参数的函数的调用时,编译器无法知道foo()期望的类型.

如果是0投0 const char *,那就没事了; 即使空指针具有与all-bits-zero不同的内部表示,该语言也保证在指针上下文中使用值0会产生空指针.(这可能要求编译器为类型转换实际生成一些非平凡的代码,但如果是这样,则需要这样做.)

但它没有理由认为0本来是一个指针.会发生什么,它会传递0作为int.如果int与指针的大小不同,或者由于任何其他原因,int0具有与空指针不同的表示,或者如果此系统以与整数不同的方式传递指针参数,则会导致问题.

所以这是未定义的行为:foo用于va_arg获取const char *实际作为类型传递的类型的参数int.

怎么用NULL?根据该答案和其中的参考文献,C标准允许将宏NULL定义为简单0或任何其他"具有值0的整数常量表达式".与流行的看法相反,它不一定是(void *)0,尽管它可能是.

因此,传递裸露是不安全的NULL,因为您可能位于定义为的平台上0.然后您的代码可能会因上述原因而失败.

为了安全和便携,您可以编写以下任一项:

 foo("Some", "arguments", "to", "foo", (const char *)0);
Run Code Online (Sandbox Code Playgroud)

要么

 foo("Some", "arguments", "to", "foo", (const char *)NULL);
Run Code Online (Sandbox Code Playgroud)

但你不能放弃演员阵容.

  • @NateEldredge我刚刚检查过,POSIX要求将`NULL`定义为整数常量0转换为`void*`,因此第一次调用在所有类似POSIX的系统上都是正确的. (2认同)
  • 第二部分是正确的,但第一部分可能更糟.想象一个具有32位`int`和64位指针的平台,它们在堆栈上传递参数,并且空指针都是位0.当你传递0时,编译器将32位0压入堆栈.但是当你使用`const char*`调用`va_arg`时,编译器从该地址获取64位.其他32位可能是碰巧占用接下来几个字节的任何旧垃圾...... (2认同)
  • 所以`foo()`会得到一个非空的指针,并且不会将它看作哨兵.它会尝试将其视为字符串指针.但由于它包含垃圾,程序可能会崩溃(或做其他不受欢迎的事情).我模糊地回忆起曾经追逐过一个有人用最后一个参数叫做"execlp()"的错误,而这正是发生的事情. (2认同)
  • 因此,你甚至不能保证得到一个填充零的`const char*`.在这种情况下,整数0永远不会被**转换为指针(这意味着编译器有机会将0转换为适当的空指针).您只是从存储int的位置获取指针,并且有很多方法可以解决这个问题. (2认同)

fuz*_*fuz 7

为您传递类型的参数第二次调用是不正确的int,而你取类型的参数const char*va_arg.这是未定义的行为.

第一次调用只有在NULL声明为(void*)0或类似时才是正确的.请注意,根据标准,NULL仅需要是空指针常量.它不必定义为,((void*)0)但通常就是这种情况.一些系统NULL定义了0在这种情况下第一次调用是未定义的行为.POSIX 强制要求 "宏应该扩展为一个整数常量表达式,其值为0,类型为void* ",所以在类似POSIX的系统上你可以放心地假设它NULL((void*0).

以下是ISO 9899:2011§6.5.2.2的相关标准报价:

6.5.2.2函数调用

(......)

6如果表示被调用函数的表达式具有不包含原型的类型,则对每个参数执行整数提升,并将具有类型的参数float提升为double.这些被称为默认参数促销.如果参数数量不等于参数数量,则行为未定义.如果使用包含原型的类型定义函数,并且原型以省略号(, ...)结尾或者促销后的参数类型与参数类型不兼容,则行为未定义.如果使用不包含原型的类型定义函数,并且促销后的参数类型与促销后的参数类型不兼容,则行为未定义,但以下情况除外:

  • 一个提升类型是有符号整数类型,另一个提升类型是相应的无符号整数类型,并且该值可在两种类型中表示;
  • 这两种类型都是指向字符类型的限定或非限定版本的指针void.

7如果表示被调用函数的表达式具有包含原型的类型,则将参数隐式转换为相应参数的类型,就像通过赋值一样,将每个参数的类型作为其不合格的版本声明的类型.函数原型声明符中的省略号表示法导致参数类型转换在最后声明的参数之后停止.默认参数提升是在尾随参数上执行的.

8不会隐式执行其他转换; 特别是,参数的数量和类型不会与函数定义中不包含函数原型声明符的参数的数量和类型进行比较.

8阐明0了为...参数传递时整数常量不会转换为指针类型.