为什么printf打印了一个非空终止的字符串?

7
C编程书上说,要使用printf打印字符串,它必须以空字符结尾。但是,即使字符串没有以空字符结尾,下面的程序仍然能够打印出该字符串!
#include <stdio.h>
#include <stdlib.h>

int main() {
    int i;
    char str[10];
    for(i = 0; i < 10; i++) {
        str[i] = (char)(i+97);
    }

    printf("%s", str);
}

我正在使用Code::Blocks集成开发环境。

6
你碰巧运气好,在那个数组后面刚好有一个“0”。你刚刚利用了经验丰富的程序员所称的“未定义行为”。不要指望能在所有地方都这样做,大多数程序会崩溃。 - Chris Eberle
我已经运行了1000次循环,但它总是以相同的方式运行,没有任何问题。我应该考虑从CodeBlocks更改我的编译器吗?谢谢。 - Nikunj Banka
4
"undefined"并不意味着不可重复。尝试使用不同的标志进行编译,或在不同的体系结构上进行编译。 - Chris Eberle
4
这就像酒后驾车一样。如果你这样做,大多数时候你可能会逃脱惩罚。但你只是在危及自己和他人的安全。 - Jens Gustedt
https://dev59.com/dG865IYBdhLWcg3wivGi - Ciro Santilli OurBigBook.com
5个回答

10

读取数组边界之外是 未定义行为。你实际上是运气不好它没有崩溃。如果你多次运行或在函数中调用它,它可能(或可能不会)崩溃。

你应该始终终止字符串,或使用宽度说明符:

printf("%.10s", str);

我认为我有一个新的疑问,如果我不用空字符终止字符串并使用宽度限定符,那么它就永远不会崩溃吗? - Nikunj Banka
@NikunjBanka 不会的,只要不超出数组边界就没问题。 - iabdalkader

5
< p > str的第10个元素之后发生的任何事情都会变成空值。那个空值在数组的定义范围之外,但是C语言没有数组边界检查。在你的情况下,它只是运气好,才能如此工作。< /p >

谢谢,但现在我已经将相同的代码循环运行了1000次,每次都没有任何问题。那么现在我应该考虑这是否是编译器本身的问题?(它是否自动放置了一个空字符) - Nikunj Banka
3
每当你开始怀疑编译器是否出了问题时,说明你还没有充分地审视自己的代码或对问题的理解。作为一个新手,永远要假设问题出在自己身上。 - Andy Lester
#include<stdio.h>#include<math.h>#include<stdlib.h>int main(){int i ; for(i = 0 ; i < 1000 ; i++ ) { char str[10] ; for(i = 0 ; i < 10 ; i++ ) { str[i] = (char)(i+97) ; } printf("%s",str) ; } return 0 ;} - Nikunj Banka
3
char str[10]的下一行声明另一个数组。将其命名为char str2[10],并用'B'或其他你想要的字符填充它。现在打印str并查看结果。 - indiv
是的,那就是问题所在!现在它也打印了一个垃圾值!你是怎么发现的? - Nikunj Banka
1
@NikunjBanka:嗯,从技术上讲,打印垃圾信息是运气问题。我想说的是在*str [10]之前的行上声明一个数组。这取决于您的机器架构。但无论如何,它与本地变量如何在内存中依次出现有关。因此,当“printf”超出您的数组边界进入la-la land时,它最终会打印内存中的另一个变量。但这超出了C的范围。语言C只是说如果您走到la-la land,行为是未定义的。 - indiv

1
如果在那个电话之前做其他事情,你的堆栈区域将包含与未使用的不同的数据。想象一下:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

int use_stack(void) {
   char str[500];
   memset(str, 'X', sizeof(str));
   printf("Filled memory from %p to %p.\n", &str, &str + sizeof str);
}

void print_stuff() {
    int i;
    char str[16]; // Changed that so that 10..15 contain X
    for(i = 0; i < 10; i++) {
        str[i] = (char)(i+97);
    }

    printf("%s<END>", str); // Have a line break before <END>? Then it comes from i.
    printf("&str: %p\n", &str);
    printf("&i: %p\n", &i);
    // Here you see that i follows str as &i is &str + 16 (0x10 in hex)
}

int main() {
    use_stack();
    print_stuff();
}

你的堆栈区域将会被填满X,而printf()会看到它们。
在你的情况和环境中,程序启动时恰好堆栈是"清空"的。
这可能会发生也可能不会。如果编译器将变量i放置在数组之后,你的数据仍然会以NUL结尾,因为第一个字节是i的值(你碰巧也会打印出来,它可能是你的情况下的换行符),而第二个字节是一个NUL字节。即使是这种情况,你的代码会引发{{link1:UB}}(未定义行为)。
通过将程序输出导入{{link2:hexdump}}或类似的工具查看一下,你能否发现输出中包含一个0A字符?如果是这样,那么我的猜测是正确的。我刚刚测试了一下,在我的编译器(GCC)上似乎是这样的。
如前所述,这是你不应该依赖的内容。 如果在<END>之前看到了一个换行符,那么我的猜测是正确的。而且,如果你现在查看打印出来的指针,你可以比较它们在内存中的地址。

我不知道什么是十六进制转储,但你提供的代码能够在我的Windows 7上的Code Blocks IDE上运行而不崩溃或打印任何垃圾值。 - Nikunj Banka
@NikunjBanka 是的,这是因为i在堆栈上跟随str,并且str[]已经被完全使用了。如果您执行str[15],则str[]的元素10..14将被填充为X - glglgl

1
根据C标准,printf函数会打印字符串中的字符,直到遇到空字符为止。否则,在定义的数组索引之后,它的行为是未定义的。
我已经测试了你的代码。在打印"abcdefghij"之后,它会打印一些垃圾值。

我正在使用CodeBlocks集成开发环境,没有得到任何垃圾值。你在使用哪个编译器?我应该考虑更换我的编译器吗?谢谢。 - Nikunj Banka
3
你的代码无效。你的代码行为是未定义的。C语言规范表明,超出数组末尾进行读取是未定义的行为,这意味着编译器可以导致任何情况发生。编译器没有问题,是你的代码有问题。 - Andy Lester

0
因为在调试模式下,*(str+10)和整个未使用的空间都有一个初始化值'0',所以它看起来像是以0结尾的。
bash-3.2$ clang -O0 t.c -o t #compile in debug mode
bash-3.2$ ./t
abcdefghij
bash-3.2$ clang -O2 t.c -o t #compile with optimization
bash-3.2$ ./t
abcdefghij2÷d=

但是期望NUL字符位于数组的后面。因此,即使将str[]初始化为\0(您认为这会发生在哪里?),奇怪之处也在数组边界之外,这并不重要。 - glglgl
我指的是 *(str+10),而不是整个数组在边界内。 - Haocheng

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接