【问题标题】:Behaviour of printf when printing a %d without supplying variable name在不提供变量名的情况下打印 %d 时 printf 的行为
【发布时间】:2010-10-01 01:41:09
【问题描述】:

我刚刚遇到一个奇怪的问题,我正在尝试 printf 一个整数变量,但是我忘记指定变量名,即

printf("%d");

而不是

printf("%d", integerName);

令人惊讶的是程序编译,有输出而且不是随机的。事实上,它恰好是我最初想要打印的整数,恰好是 m-1。

只要程序继续运行,错误的printf 语句将始终输出 m-1...换句话说,它的行为就像语句读取一样

printf("%d", m-1);

有人知道这种行为背后的原因吗?我正在使用没有任何命令行选项的 g++。

#include <iostream>
#define maxN 100
#define ON 1
#define OFF 0

using namespace std;

void clearArray(int* array, int n);
int fillArray(int* array, int m, int n);

int main()
{
    int n = -1, i, m;
    int array[maxN];
    int found;

    scanf("%d", &n);

    while(n!=0)
    {
        found=0;
        m = 1;
        while(found!=1)
        {
            if(m != 2 && m != 3 && m != 4 && m != 6 && m != 12)
            {
                clearArray(array, n);
                if(fillArray(array, m, n) == 0)
                {
                    found = 1;
                }
            }
            m++;
        }

        printf("%d\n");

        scanf("%d", &n);
    }

    return 0;
}

void clearArray(int* array, int n)
{
    for(int i = 1; i <= n; i++)
        array[i] = ON;
}

int fillArray(int* array, int m, int n)
{
    int i = 1, j, offCounter = 0, incrementCounter;

    while(offCounter != n)
    {
        if(*(array+i)==ON) 
        {
            *(array+i) = OFF;
            offCounter++;       
        }
        else 
        {
            j = 0;
            while((*array+i+j)==OFF)
            {
                j++;
            }
            *(array+i+j) = OFF;
            offCounter++;           
        }
        if(*(array+13) == OFF && offCounter != n) return 1;
        if(offCounter ==n) break;

        incrementCounter = 0;       
        while(incrementCounter != m)
        {
            i++;
            if(i > n) i = 1;
            if(*(array+i) == ON) incrementCounter++; 
        }       
    }

    return 0;
}

【问题讨论】:

    标签: c printf


    【解决方案1】:

    您说“令人惊讶的是程序编译”。事实上,这并不奇怪。 C 和 C++ 允许函数具有可变参数列表。 printf 的定义是这样的:

    int printf(char*, ...);
    

    “...”表示该函数有零个或多个可选参数。事实上,C 具有可选参数的主要原因之一是支持 printf 和 scanf 系列函数。

    C 没有 printf 函数的特殊知识。在您的示例中:

    printf("%d");
    

    编译器不会分析格式字符串并确定缺少整数参数。这是完全合法的 C 代码。您缺少参数的事实是仅在运行时出现的语义问题。 printf 函数将假定您已提供参数并在堆栈中查找它。它会拾取那里发生的任何事情。碰巧在您的特殊情况下,它正在打印正确的内容,但这是一个例外。一般来说,你会得到垃圾数据。这种行为会因编译器而异,并且还会根据您使用的编译选项而改变;如果你开启编译器优化,你可能会得到不同的结果。

    正如我在回答中的一个 cmets 中所指出的,一些编译器具有类似“lint”的功能,可以实际检测错误的 printf/scanf 调用。这涉及编译器解析格式字符串并确定预期的额外参数的数量。这是非常特殊的编译器行为,在一般情况下不会检测到错误。即,如果您编写自己的“printf_better”函数,它与 printf 具有相同的签名,编译器将不会检测是否缺少任何参数。

    【讨论】:

    • 我不知道编译器不分析 printf 参数...谢谢你告诉我
    • 您可以告诉编译器检查参数。我们知道这种情况下的编译器是 g++,所以使用 -Wformat 选项,该选项包含在 -Wall 中。
    • 使用 -Wformat 将是一个相当非标准的扩展(尽管它很有用)。大概这仅适用于预定义的函数列表。
    • GCC 允许您将自己的函数注释为类似 printf/scanf。虽然不允许您定义新格式
    • Rust 很有趣,因为格式是在编译时处理的。
    【解决方案2】:

    发生的事情看起来像这样。

    printf("%d", m);
    

    在大多数系统上,字符串的地址将被压入堆栈,然后'm' 作为整数(假设它是 int/short/char)。没有警告,因为 printf 基本上被声明为 'int printf(const char *, ...);' - ...的意思是“任何事情都会发生”。

    因此,由于“一切顺利”,当您将变量放在那里时,会发生一些奇怪的事情。任何小于 int 的整数类型都以 int 形式出现 - 类似的事情。什么都不发送也可以。

    在 printf 实现(或至少一个“简单”实现)中,您会发现va_listva_arg 的用法(名称有时会因一致性而略有不同)。这些是实现用来绕过参数列表的“...”部分的内容。这里的问题是没有类型检查。由于没有类型检查,printf 在查看格式字符串("%d") 并认为接下来应该有一个'int' 时,会从执行堆栈中拉出随机数据。

    在黑暗中随机拍摄会说您在 printf 之前进行的函数调用可能通过 'm-1' 作为第二个参数?这是许多可能性之一——但如果发生这种情况会很有趣。 :)

    祝你好运。

    顺便说一句 - 大多数现代编译器(我相信是 GCC?)都有警告,可以启用以检测此问题。我相信 Lint 也一样。不幸的是,我认为对于 VC,您需要使用 /analyze 标志而不是免费获得。

    【讨论】:

    • 我明白了。通常我不会被打扰,只是将其视为垃圾值,但由于它恰好输出了合理的数据,我想也许还有更多的东西。你已经说得很清楚了,谢谢你的帮助乔。
    • 参数通常从右到左压入堆栈。然后当字符串被弹出时,根据需要参数从栈中弹出。
    • 不是真的凯德。这对于 stdcall 是正确的 - 但对于 vararg cdecl 调用约定,如果不遍历所有 ... 参数,就无法找到“第一个”参数。 cdecl/vararg 从左到右。
    • 为了保证第一个参数是栈顶的,参数从右向左推送。在 cdecl 和 stdcall 中相同。 cdecl 和 stdcall 之间的唯一区别是谁负责清理堆栈。
    • Rob - 我很谦虚。我不知道我在想什么——你当然是对的。抱歉,凯德。
    【解决方案3】:

    虽然我非常怀疑这会导致内存违规,但您得到的整数是未定义的垃圾。

    【讨论】:

      【解决方案4】:

      它从堆栈中取出一个 int。

      http://en.wikipedia.org/wiki/X86_calling_conventions

      【讨论】:

      • 怪异的反对意见:假设堆栈,假设 x86,两者都可能无效。
      【解决方案5】:

      您正在查看堆栈。更改优化器值,这可能会改变。更改变量声明的顺序(特别是)m。将m 设为寄存器变量。将m 设为全局变量。

      您会看到发生的一些变化。

      这类似于您在执行简单 I/O 时遇到的著名的缓冲区溢出攻击。

      【讨论】:

        【解决方案6】:

        您发现一个行为。这可能是任何其他行为,包括无效的内存访问。

        【讨论】:

        • 是的。我意识到它可能不是所有情况下的 m-1。
        猜你喜欢
        • 1970-01-01
        • 1970-01-01
        • 2020-12-17
        • 1970-01-01
        • 1970-01-01
        • 2018-07-25
        • 1970-01-01
        • 2011-11-20
        相关资源
        最近更新 更多