%p说明符仅用于有效指针吗?

sha*_*oth 20 c++ pointers reinterpret-cast

假设在我的平台上sizeof(int)==sizeof(void*),我有这个代码:

printf( "%p", rand() );
Run Code Online (Sandbox Code Playgroud)

这是不确定的行为,因为传递一个不是有效指针的值代替%p

Ray*_*hen 20

为了扩展@ larsman的答案(这说明,因为你违反了约束,行为是未定义的),这里是一个实际的C实现sizeof(int) == sizeof(void*),但代码不等同于printf( "%p", (void*)rand() );

摩托罗拉68000处理器有16个寄存器,用于一般计算,但它们不相同.它们(命名的八a0a7)用于存取存储器(地址寄存器),另一个八(d0通过d7)用于算术(数据寄存器).这种架构的有效调用约定是

  1. 传递前两个整数参数d0d1; 将其余部分传递给堆栈.
  2. 传递前两个指针参数a0a1; 将其余部分传递给堆栈.
  3. 无论大小如何,都将所有其他类型传递给堆栈.
  4. 无论类型如何,在堆栈上传递的参数都是从右向左推送的.
  5. 基于堆栈的参数在4字节边界上对齐.

这是一个完全合法的调用约定,类似于许多现代处理器使用的调用约定.

例如,要调用的函数void foo(int i, void *p),你会通过id0pa0.

需要注意的是要调用的函数void bar(void *p, int i),你也将通过id0pa0.

根据这些规则,printf("%p", rand())将传入格式字符串a0和随机数参数d0.另一方面,printf("%p", (void*)rand())将传入格式字符串a0和随机指针参数a1.

va_list结构是这样的:

struct va_list {
    int d0;
    int d1;
    int a0;
    int a1;
    char *stackParameters;
    int intsUsed;
    int pointersUsed;
};
Run Code Online (Sandbox Code Playgroud)

前四个成员使用寄存器的相应输入值进行初始化.的stackParameters到所述第一基于堆栈的参数点经由传递...,并且intsUsedpointersUsed被初始化为命名的参数,它们分别是整数和指针的数目.

所述va_arg宏是一个编译器本征,其生成基于预期参数类型不同的代码.

  • 如果参数类型是指针,则va_arg(ap, T)展开到(T*)get_pointer_arg(&ap).
  • 如果参数类型是整数,则va_arg(ap, T)展开为(T)get_integer_arg(&ap).
  • 如果参数类型是其他,则va_arg(ap, T)扩展为*(T*)get_other_arg(&ap, sizeof(T)).

get_pointer_arg功能是这样的:

void *get_pointer_arg(va_list *ap)
{
    void *p;
    switch (ap->pointersUsed++) {
    case 0: p = ap->a0; break;
    case 1: p = ap->a1; break;
    case 2: p = *(void**)get_other_arg(ap, sizeof(p)); break;
    }
    return p;
}
Run Code Online (Sandbox Code Playgroud)

get_integer_arg功能是这样的:

int get_integer_arg(va_list *ap)
{
    int i;
    switch (ap->intsUsed++) {
    case 0: i = ap->d0; break;
    case 1: i = ap->d1; break;
    case 2: i = *(int*)get_other_arg(ap, sizeof(i)); break;
    }
    return i;
}
Run Code Online (Sandbox Code Playgroud)

get_other_arg函数是这样的:

void *get_other_arg(va_list *ap, size_t size)
{
    void *p = ap->stackParameters;
    ap->stackParameters += ((size + 3) & ~3);
    return p;
}
Run Code Online (Sandbox Code Playgroud)

如前所述,调用printf("%p", rand())将传入格式字符串a0和随机整数d0.但是当printf函数执行时,它将看到%p格式并执行a va_arg(ap, void*),它将使用get_pointer_arg和读取参数a1而不是d0.由于a1没有初始化,它包含垃圾.您生成的随机数将被忽略.

举一步的例子,如果你有printf("%p %i %s", rand(), 0, "hello");这个将被称为如下:

  • a0 =格式字符串的地址(第一个指针参数)
  • a1=字符串的地址"hello"(第二个指针参数)
  • d0 =随机数(第一个整数参数)
  • d1 = 0(第二个整数参数)

printf函数执行时,它读取格式字符串a0预期.当它看到%p它将从中检索a1并打印它时,所以你得到字符串的地址"hello".然后它会看到%i并从中检索参数d0,因此它会打印一个随机数.最后,它看到%s并从堆栈中检索参数.但是你没有在堆栈上传递任何参数!这将读取未定义的堆栈垃圾,当它尝试打印时,很可能会使程序崩溃,就像它是一个字符串指针一样.


Fre*_*Foo 13

C标准,7.21.6.1,该fprintf功能,仅表示

p 参数应该是指针void.

通过附录J.2,这是一个约束,违反约束会导致UB.

(以下是我之前的推理,为什么这应该是UB,这太复杂了.)

该段没有描述如何void*从中检索...,但C标准本身为此目的提供的唯一方法是7.16.1.1,该va_arg宏警告我们

如果type与实际的下一个参数的类型不兼容(根据默认参数提升而提升),则行为未定义

如果您阅读6.2.7,兼容类型和复合类型,那么无论其大小如何,都没有提示void*并且int应该兼容.所以,我要说,因为这va_argprintf 在标准C中实现的唯一方法,所以行为是未定义的.


Mik*_*our 5

是的,这是未定义的.从C++ 11,3.7.4.2/4开始:

使用无效指针值(包括将其传递给释放函数)的效果未定义.

用脚注:

在某些实现中,它会导致系统生成的运行时故障.

  • @larsmans:不,推断它被称为"解除引用",而不是"使用".使用对象的值意味着它出现在需要_rvalue_的表达式中; 例如,作为函数参数. (2认同)