我最近在用C ++编程了一段时间之后再次开始用C编程,而且我对指针的理解有点生疏。
我想问一下为什么这段代码没有导致任何错误:
char* a = NULL;
{
char* b = "stackoverflow";
a = b;
}
puts(a);
我认为因为b
超出了范围,a
应该引用一个不存在的内存位置,因此在调用printf
时它们将是一个运行时错误。
我在MSVC中运行此代码大约20次,并且没有显示任何错误。
答案 0 :(得分:45)
在定义b
的范围内,为其分配字符串文字的地址。这些文字通常位于内存的只读部分,而不是堆栈。
执行a=b
后,您将b
的值分配给a
,即a
现在包含字符串文字的地址。 b
超出范围后,此地址仍然有效。
如果您使用了b
的地址然后尝试取消引用该地址,那么您将调用undefined behavior。
所以你的代码是有效的,并且不调用未定义的行为,但以下是:
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
包含一个局部变量的地址,然后超出范围。
编辑:
作为旁注,将变量作为printf
的第一个参数传递是不好的做法,因为这可能导致format string vulnerability。最好使用字符串常量,如下所示:
printf("%s", a);
或更简单:
puts(a);
答案 1 :(得分:11)
逐行,这是您的代码所做的:
char* a = NULL;
a
是一个不引用任何内容的指针(设置为NULL
)。
{
char* b = "stackoverflow";
b
是一个引用静态常量字符串文字"stackoverflow"
的指针。
a = b;
a
设置为也引用静态的常量字符串文字"stackoverflow"
。
}
b
超出了范围。但由于a
不引用b
,因此无关紧要(它只是引用与b
相同的静态,常量字符串文字,参考)。
printf(a);
打印由"stackoverflow"
引用的静态常量字符串文字a
。
答案 2 :(得分:10)
字符串文字是静态分配的,因此指针无限期有效。如果您说过char b[] = "stackoverflow"
,那么您将在堆栈上分配一个char数组,当该范围结束时该数组将变为无效。这种差异也表现为修改字符串:char s[] = "foo"
stack分配一个你可以修改的字符串,而char *s = "foo"
只给你一个指向一个字符串的指针,该字符串可以放在只读内存中,所以修改它是未定义的行为。
答案 3 :(得分:9)
其他人已经解释说这段代码完全有效。这个答案是关于你的期望,如果代码无效,调用printf
时会出现运行时错误。它不一定如此。
让我们看一下代码中的这种变体, 无效:
#include <stdio.h>
int main(void)
{
int *a;
{
int b = 42;
a = &b;
}
printf("%d\n", *a); // undefined behavior
return 0;
}
这个程序有未定义的行为,但发生很可能它实际上会打印42,原因有很多 - 许多编译器会离开{{1}的堆栈槽分配给b
的整个主体,因为没有其他任何东西需要空间并且最小化堆栈调整的数量简化了代码生成;即使编译器正式释放了堆栈槽,数字42也可能保留在内存中,直到其他东西覆盖它为止,并且main
和a = &b
之间没有任何内容可以做到这一点;标准优化(“常量和复制传播”)可以消除这两个变量,并将*a
的最后已知值直接写入*a
语句(就像您编写了printf
)。 / p>
理解“未定义的行为”并不意味着“程序会以可预测的方式崩溃”,这一点至关重要。这意味着“任何事情都可能发生”,任何事情包括似乎正在以程序员的意图工作(在此计算机上,使用此编译器,今天)。
作为最后一点,我没有方便访问(Valgrind,ASan,UBSan)的积极调试工具,没有足够详细的跟踪“自动”变量生命周期来捕获此错误,但是GCC 6确实产生了这个有趣的警告:< / p>
printf("%d\n", 42)
我相信这里发生的事情是,它完成了我上面描述的优化 - 将$ 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
- 但是它的“最后一个已知值“for printf
是”这个变量是未初始化的“sentinel而不是42.(然后生成相当于b
的代码。)
答案 4 :(得分:3)
代码不会产生任何错误,因为您只是将字符指针b
分配给另一个字符指针a
,这非常好。
在C中,您可以将指针引用分配给另一个指针。实际上,字符串“stackoverflow”用作文字,该字符串的基地址位置将分配给a
变量。
虽然您不在变量b
范围内,但仍然使用a
指针完成了分配。因此它会打印结果而不会出现任何错误。
答案 5 :(得分:2)
字符串文字总是静态分配,程序可以随时访问,
char* a = NULL;
{
char* b = "stackoverflow";
a = b;
}
printf(a);
这里内存到字符串文字stackoverflow由编译器分配,因为它将内存分配给int / char变量或指针
区别在于字符串文字是READONLY section / segment中的位置。 变量b在堆栈处分配,但它保存只读段/ segmemt的存储器地址。
在代码中,var'b'具有字符串文字的地址。即使b失去其范围,字符串文字的内存也将始终被分配
注意:分配给字符串文字的内存是二进制文件的一部分,一旦卸载程序将被删除
参考ELF二进制规范以更详细地了解
答案 6 :(得分:2)
我认为,作为以前答案的证明,最好先了解代码中的内容。人们已经提到字符串文字位于.text部分。所以,他们(文字)总是那么简单。您可以轻松找到代码
#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
在这里,您可以找到有关Mach-O可执行文件的准备好的报道: https://www.objc.io/issues/6-build-tools/mach-o-executables/