/*不理解为啥这个正确*/
#include <stdio.h>
int * f();
int main(){
int* result= f();
printf("%d\n",*result);
return 0;
}
int * f(){
int a=3;
int* i=&a;
return i;
}
为什么上面这段代码能正确执行? a (左值)是不是一个内存地址里面存了 3 (右值)?那么上面这段代码 i 和 a 是不是指向了同一块内存,内存里的值是 3 ?然后函数结束是不是这块内存就被释放了 3 就没了?为啥还能通过?
/*这段代码错的可以理解*/
#include <stdio.h>
int * f();
int main(){
int* result= f();
printf("%d\n",*result);
return 0;
}
int * f(){
int a=3;
return &a;
}
1
wevsty 2017-08-18 14:12:46 +08:00 1
内存空间不再使用的时候并不一定里面的内容就会马上改变。
这个时候内存空间里究竟是什么是不确定的,引用这个空间是未定义的行为。 |
4
tyrealgray 2017-08-18 14:27:34 +08:00
引用不能单独存在,返回类型也是错的
|
5
masteryi OP @tyrealgray 那是一个取地址的运算符,不懂别来误导新手好吗?
|
6
wevsty 2017-08-18 14:36:02 +08:00
|
7
fcten 2017-08-18 14:36:06 +08:00
被释放不等于被覆盖,在没有向该地址写入新值之前,内存中的值不会改变。
|
8
suikator 2017-08-18 14:41:36 +08:00 via Android
C 允许您将指针指向已释放的内存块
|
9
tyrealgray 2017-08-18 15:06:41 +08:00 via iPhone
@masteryi 看错了😂,好久没写 C++了。
|
10
tyrealgray 2017-08-18 15:11:43 +08:00 via iPhone
再仔细看了一下,你如果设置个 timeout,你的 result 再打印出来的时候就不一定是那个值了,具体看编译器怎么决定的。感觉第一段和第二段都会是这种结果
|
11
misaka20038numbe 2017-08-18 15:11:56 +08:00
第二段为什么错了,返回 a 的地址,然后取值。
|
12
cgb1021 2017-08-18 15:28:24 +08:00
因为 f 要返回一个指向 int 的地址的变量(新的地址),return &a 只是返回了一个 int 的地址。就是说还缺了指向 int 的地址的地址。
|
13
zhouheyang0919 2017-08-18 15:41:47 +08:00
@masteryi
存取 stack pointer 以下的内存地址是未定义的行为。 “未定义”意味着所有对这块内存空间的操作可能给出正确的结果,但更可能导致不可预料的后果(错误的结果,Segfault,各种 corruption,或其他问题)。这取决于编译器内部代码生成和优化的实现。 |
14
cloudyfsail 2017-08-18 15:45:37 +08:00
a 存在与栈上,函数 f 执行完毕后虽然栈被回收了,但是返回的地址仍然是有效的,仍然可以访问,但是这是非法行为,c++里叫未定义行为,就是说可能执行起来没问题,也可能有问题,你这个例子里 result 指向的内存没那么快重新被分配使用,所以还是得到 3.另外其实两段代码是一样的。
|
15
masteryi OP 实现个蛋我都不想一一回复了,你们真的在你的机器上跑过吗? c 标准不可能让指针指向无效内存,你的编译器不是根据 c 标准写的想咋写就咋写想咋实现就咋实现吗? c/c++不让返回局部变量的指针和引用
|
16
hitmanx 2017-08-18 16:01:25 +08:00 1
两个都是非法的访问,但是具体会不会出现运行时的错误,这个是随机的,取决于当时内存管理的情况.虽然说因实现而异,但是在一般实现中,栈上的内容从高字节到低字节依次是
1) main 中在 f()之前定义的局部变量 2) f()的传参. 这儿为空,所以不占空间 3) 返回地址(PC) 4) f()中定义的局部变量 f()返回以后,栈指针就指向下一个空的,也就是 2)或者说 3)的位置.访问它以下的栈内存都是非法的. |
17
wwqgtxx 2017-08-18 16:03:20 +08:00 1
看我这个代码运行后的结果就知道了
#include <stdio.h> int * f(); int * f2(); int main(){ int* result= f(); f2(); printf("%d\n",*result); return 0; } int * f(){ int a=3; int* i=&a; return i; } int * f2(){ int a=4; int* i=&a; return i; } ----------------------------------------- 4 Process returned 0 (0x0) execution time : 0.188 s Press any key to continue. |
18
hitmanx 2017-08-18 16:06:51 +08:00 1
|
19
wwqgtxx 2017-08-18 16:08:09 +08:00 1
@masteryi “ c 标准不可能让指针指向无效内存”这句话本来就不成立,C 的指针完全可以指向一个不存在的内存,你直接写 int* i = (int*)500;这样一样能编译通过,只不过运行的时候一旦访问这个指针指向的内容,就不知道程序会不会炸掉了
“ c/c++不让返回局部变量的指针和引用”这句话也不对,应该说是不应该而不是不让,你上面自己的代码也证明了实际上是可以返回局部变量的指针和引用,并不会编译出错,但是到了运行期间一样死未定义行为 |
20
wwqgtxx 2017-08-18 16:19:45 +08:00
如果你想探根究底的话,就看看 GCC 生产的汇编
.file "test.cpp" .text .globl __Z1fv .def __Z1fv; .scl 2; .type 32; .endef __Z1fv: pushl %ebp movl %esp, %ebp subl $16, %esp movl $3, -8(%ebp) leal -8(%ebp), %eax movl %eax, -4(%ebp) movl -4(%ebp), %eax leave ret .def ___main; .scl 2; .type 32; .endef .section .rdata,"dr" LC0: .ascii "%d\12\0" .text .globl _main .def _main; .scl 2; .type 32; .endef _main: leal 4(%esp), %ecx andl $-16, %esp pushl -4(%ecx) pushl %ebp movl %esp, %ebp pushl %ecx subl $20, %esp call ___main call __Z1fv movl %eax, -8(%ebp) movl -8(%ebp), %eax movl (%eax), %eax subl $8, %esp pushl %eax pushl $LC0 call _printf addl $16, %esp movl $0, %eax movl -4(%ebp), %ecx leave leal -4(%ecx), %esp ret .def _printf; .scl 2; .type 32; .endef 看懂了这段汇编就能解释为什么函数结束了还能访问到那个 3 了 |
21
234235 2017-08-18 16:20:28 +08:00
1. 第一段程序,返回的是一个指针指向的地址。编译器并不总能通过上下文判断你的返回值是否为非法地址。a 是局部变量,a 的地址应该是在堆栈中,不同的编译器和运行环境对堆栈的定义不同,可能就是在普通的内存段中,这时你返回了该地址,该地址是真实存在并且是可以被你这个程序访问的,那它当然能被正确打印出来。C 不负责对任何你用过的或申请到的内存的内容进行清除操作。
2.第二段程序错误,因为编译器识别到你返回的是一个局部变量的地址,这个是否报错同样受到编译器和环境以及你的设置的影响。有些可能就是 warning,有些可能就直接通过。说到底还是和编译器将局部变量放在哪里的关系更大。 另外,你说 'c 标准不可能让指针指向无效内存' ,这是完全的表达错误,应该是 C 规范要求你的程序中不能出现指向未知内存的操作,这样做是不可预料的,有可能造成程序崩溃,CPU 直接抛 Hard Fault。 如果你定义一个指针并随意的赋了一个地址值,编译器是不会报错的,这个值的正确与否是你来保证的。这也是 C 的灵活性的体现,如果 C 也对指针地址有过多的限制,很多程序就没法写了。 |
22
zhouheyang0919 2017-08-18 16:21:01 +08:00
@masteryi
C / C++ 是“不安全”的语言。用户代码可以访问任意地址,编译器对指针的有效性不做验证。 “ c 标准不可能让指针指向无效内存” “ c/c++不让返回局部变量的指针和引用” 来源请求。 |
23
wwqgtxx 2017-08-18 16:26:44 +08:00
另外就是你的第一段代码如果用 GCC 的-O3 编译的话,编译器甚至都可以推测出来这里的值为 3
.file "test.cpp" .text .p2align 2,,3 .globl __Z1fv .def __Z1fv; .scl 2; .type 32; .endef __Z1fv: pushl %ebp movl %esp, %ebp subl $16, %esp leal -4(%ebp), %eax leave ret .def ___main; .scl 2; .type 32; .endef .section .rdata,"dr" LC0: .ascii "%d\12\0" .text .p2align 2,,3 .globl _main .def _main; .scl 2; .type 32; .endef _main: leal 4(%esp), %ecx andl $-16, %esp pushl -4(%ecx) pushl %ebp movl %esp, %ebp pushl %ecx subl $4, %esp call ___main subl $8, %esp pushl $3 pushl $LC0 call _printf xorl %eax, %eax movl -4(%ebp), %ecx leave leal -4(%ecx), %esp ret .def _printf; .scl 2; .type 32; .endef |
24
wevsty 2017-08-18 16:36:10 +08:00
@masteryi
C 中从来没有规定指针必须指向有效的地址,事实上指针可以指向任何地址。为了最大的运行速度,C 标准中不存在运行时检查这种东西,编译时只检查语法错误和语法上就能发现的简单的逻辑错误。 这给了开发者最大限度的自由,也允许开发者使用一些不符合规范但是又确实有用的技巧。 比如: const int i = 1; int* p = (int*)&i; *p = 2; const 修饰过的变量规定上是只读不可修改的,但是通过这样的方法其实又是可以修改的。 编译器开发实现最大的依据当然是标准文档,但是基本上都不是 100%按照文档实现的。某种程度上确实是,想怎么实现都可以,并且相当多的行为没有被写进标准文档,这种时候开发编译器的开发者可以自己决定如何处理。 最后,给点建议,谦虚点没错的。 |
25
cloudyfsail 2017-08-18 16:36:46 +08:00
不应该做和能不能做是两回事,lz 应该是刚学习 c++吧,c++的复杂部分体现在这里,大量的未定义行为,至于你第二段代码有错误,上面有人说了,应该是编译器设置的问题,lz 要学习还很多啊,不要这么嚣张。
|
26
irexy 2017-08-18 16:49:40 +08:00
@wevsty
请教下结果怎么解释呢? int main() { const int a = 1; int *p = (int*)&a; cout << a << " " << *p << " " << p << " " << &a << endl; *p = 2; cout << a << " " << *p << " " << p << " " << &a << endl; return 0; } 1 1 0x7fff5151cf68 0x7fff5151cf68 1 2 0x7fff5151cf68 0x7fff5151cf68 |
27
wwqgtxx 2017-08-18 16:56:39 +08:00 via iPhone
@irexy 你应该看看生成的汇编,你在编译 cout a 的时候 a 就已经被直接替换成 1 了,所以并不会被改变,类似于#define 的效果
|
28
wevsty 2017-08-18 17:01:18 +08:00
@irexy
很好解释。 const 的大多数时候只是一个编译器用来判断的标志,编译器编译的时候在语法上拒绝对 const 修饰的变量直接进行修改。但是变量是在内存中存放的,const 并没有对这个内存空间施加写保护,所以通过指针就可以改掉 const 修饰的变量了。 |
29
wevsty 2017-08-18 17:04:58 +08:00
|
30
suikator 2017-08-18 17:10:41 +08:00 via Android
大家散了吧 lz 已经跑路了
|
31
ivechan 2017-08-18 17:14:21 +08:00
一条准则:永远不要试图去解释未定义(undefined)的操作。
这种问题根本没有答案。 |
32
wevsty 2017-08-18 17:23:04 +08:00
@irexy 还有一点需要注意,C 和 C++是不太一样的。
C 中 const 强调的是只读不能修改,实际上但是实际上是变量。 C++中 const 强调的是这是一个常量,和#define 差不多,虽然最后也会有实际的内存空间,但是未必就会从内存里去读取这个值。因为在编译阶段就可以确定变量 a 的值了,所以可能编译器直接就编译成 mov eax,1 这样的东西了。 举个例子: #include <stdio.h> int main() { const int a = 1; int *p = (int*)&a; printf("%d", a); *p = 2; printf("%d", a); return 0; } 你用.c 的后缀,使用 C 编译器得到的结果就是 12。但是如果使用.cpp 后缀用 C++编译器来编译,那么结果可能就是 11。 更进一步的如果加上 volatile 来修饰 a,那么用 C++编译器编译出来则又可能是 12。 |
33
magicO 2017-08-18 17:40:04 +08:00
这就是为啥至今不敢碰 C/Cpp 的原因。。。
|
34
carlonelong 2017-08-18 17:46:33 +08:00
为啥这么纠结 undefined behavior
|
35
wingkou 2017-08-18 18:23:14 +08:00 via Android
典型初学者对 undefined behavior 的迷惑
|
36
masteryi OP 楼主躲在角落不敢说话。。。原来第一段仅仅是编译通过了
|
37
gnaggnoyil 2017-08-19 14:45:12 +08:00
@cloudyfsail C 和 C++语言标准本身并不需要借助栈 /堆的概念来说明对象的生存期.你说的这两个玩意在 C 和 C++标准中都有自己专门的称呼的: automatic storage duration/allocated storage duration(C)或者 automatic storage duration/dynamic storage duration(C++)
@wevsty 无论是 C 还是 C++,试图通过 cast 来修改一个 const 左值的值都是未定义行为.const 在语义上就明确表示不可修改,为何非得要违反语义. |
38
wevsty 2017-08-19 14:48:30 +08:00
@gnaggnoyil 举个例子而已,当然这种做法是不规范的也不应该提倡。
|