引用一个超出范围的char *
我最近开始用C语言编程一段时间后,再次用C编程,而我对指针的理解有点生疏。
我想问问为什么这个代码不会造成任何错误:
char* a = NULL; { char* b = "stackoverflow"; a = b; } puts(a);
我认为,因为b
超出了范围, a
应该引用一个不存在的内存位置,因此在调用printf
时它们将是运行时错误。
我在MSVC中运行这个代码大约20次,没有显示错误。
在b
定义的范围内,它被赋予一个string文字的地址。 这些文字通常位于内存的只读部分,而不是堆栈。
当你做a=b
你把a=b
的值赋给a
,即现在包含一个string的地址。 b
超出范围后,该地址仍然有效。
如果你采取了b
的地址 ,然后试图取消引用该地址,那么你会调用未定义的行为 。
所以你的代码是有效的,不会调用未定义的行为,但是下面是:
int *a = NULL; { int b = 6; a = &b; } printf("b=%d\n", *a);
另一个更微妙的例子是:
char *a = NULL; { char b[] = "stackoverflow"; a = b; } printf(a);
这个例子和你的区别在于, b
是一个数组,在分配给a
时衰减到指向第一个元素的指针。 所以在这种情况下, a
包含一个局部variables的地址,然后超出范围。
编辑:
作为一个方面说明,传递一个variables作为printf
的第一个参数是不好的做法,因为这可能导致格式string漏洞 。 最好使用如下的string常量:
printf("%s", a);
或者更简单地说:
puts(a);
一行一行,这就是你的代码所做的事情:
char* a = NULL;
a
是一个不引用任何东西的指针(设置为NULL
)。
{ char* b = "stackoverflow";
b
是引用静态常量string文字"stackoverflow"
的指针。
a = b;
a
被设置为也引用静态常量string文字"stackoverflow"
。
}
b
超出范围。 但是因为a
没有引用b
,所以这并不重要(它只是引用与b
引用相同的静态常量string)。
printf(a);
打印由a
引用的静态常量string文字"stackoverflow"
。
string文字是静态分配的,所以指针无限期地有效。 如果你说过char b[] = "stackoverflow"
,那么你将在堆栈中分配一个char数组,当范围结束时将会变为无效。 这个差异也显示了修改string: char s[] = "foo"
stack分配一个string,你可以修改,而char *s = "foo"
只给你一个指向一个string的指针,可以放在只读内存,所以修改它是未定义的行为。
其他人解释说这个代码是完全有效的。 这个答案是关于你的期望, 如果代码是无效的 ,调用printf
时会出现运行时错误。 这不一定如此。
让我们看看你的代码中的这种变化,这是无效的:
#include <stdio.h> int main(void) { int *a; { int b = 42; a = &b; } printf("%d\n", *a); // undefined behavior return 0; }
这个程序有一个未定义的行为,但事实上,由于几个不同的原因,它实际上会打印42个字符 – 很多编译器将离开分配给整个主体的b
的堆栈槽,因为没有其他的需要空间并且最小化堆栈调整的次数简化了代码生成; 即使编译器正式释放堆栈槽,数字42可能仍然保留在内存中,直到其他内容覆盖它为止,并且a = &b
和*a
之间没有任何*a
可以这样做; 标准优化(“常量和副本传播”)可以消除这两个variables,并将*a
的最后一个已知值直接写入printf
语句中(就像写入printf("%d\n", 42)
)。
理解“未定义的行为”并不意味着“程序会崩溃”。 这意味着“任何事情都可能发生”,并且任何东西都包含在程序员可能想要的工作中 (在这台计算机上, 就是今天这个编译器)。
作为最后一个注意事项,我没有一个能够方便地访问(Valgrind,ASan,UBSan)的激进debugging工具,足够详细地跟踪“自动”variables的生命周期以捕获这个错误,但是GCC 6却产生了这个有趣的警告:
$ gcc -std=c11 -O2 -W -Wall -pedantic test.c test.c: In function 'main': test.c:9:5: warning: 'b' is used uninitialized in this function printf("%d\n", *a); // undefined behavior ^~~~~~~~~~~~~~~~~~
我相信这里发生的是,它做了我上面描述的优化 – 复制b
的最后已知值到*a
然后到printf
– 但是它的“最后已知值”是b
“这个variables是未初始化的”sentinel然后生成相当于printf("%d\n", 0)
代码printf("%d\n", 0)
。)
该代码不会产生任何错误,因为您只是将字符指针b
分配给另一个字符指针a
,这是完全正确的。
在C中,您可以将指针引用分配给另一个指针。 这里实际上string“stackoverflow”被用作一个文字,该string的基地址将被分配给a
variables。
虽然你已经超出了variablesb
的范围,但是这个任务仍然是用a
指针完成的。 所以它会打印结果没有任何错误。
string文字总是静态分配的,程序可以随时访问,
char* a = NULL; { char* b = "stackoverflow"; a = b; } printf(a);
我认为,为了certificate以前的答案,最好看一下代码中真正的内容。 人们已经提到string文字位于.text部分。 所以,他们(文字)总是在那里。 你可以很容易地find这个代码
#include <string.h> int main() { char* a = 0; { char* b = "stackoverflow"; a = c; } printf("%s\n", a); }
使用以下命令
> cc -S main.c
在main.s里面,你会发现,在最底层
... ... ... .section __TEXT,__cstring,cstring_literals L_.str: ## @.str .asciz "stackoverflow" L_.str.1: ## @.str.1 .asciz "%s\n"
您可以阅读更多关于汇编程序部分(例如): https : //docs.oracle.com/cd/E19455-01/806-3773/elf-3/index.html
在这里你可以find准备好的Mach-O可执行文件: https: