V2EX = way to explore
V2EX 是一个关于分享和探索的地方
现在注册
已注册用户请  登录
masteryi
V2EX  ›  C

为什么这段代码能正确执行?

  •  
  •   masteryi · 2017-08-18 14:05:39 +08:00 · 3868 次点击
    这是一个创建于 2687 天前的主题,其中的信息可能已经有所发展或是发生改变。
    /*不理解为啥这个正确*/
    #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;
    }
    
    38 条回复    2017-08-19 14:48:30 +08:00
    wevsty
        1
    wevsty  
       2017-08-18 14:12:46 +08:00   ❤️ 1
    内存空间不再使用的时候并不一定里面的内容就会马上改变。
    这个时候内存空间里究竟是什么是不确定的,引用这个空间是未定义的行为。
    masteryi
        2
    masteryi  
    OP
       2017-08-18 14:20:16 +08:00
    @wevsty 那为什么第二段代码出错
    masteryi
        3
    masteryi  
    OP
       2017-08-18 14:21:59 +08:00
    @masteryi 发错你的意思是引用这个空间是未定义的那么第一段为什么能正确执行
    tyrealgray
        4
    tyrealgray  
       2017-08-18 14:27:34 +08:00
    引用不能单独存在,返回类型也是错的
    masteryi
        5
    masteryi  
    OP
       2017-08-18 14:35:11 +08:00
    @tyrealgray 那是一个取地址的运算符,不懂别来误导新手好吗?
    wevsty
        6
    wevsty  
       2017-08-18 14:36:02 +08:00
    @masteryi
    第二段代码我的 VS2015 上编译和运行都没问题(不算编译警告),并且运行结果和第一段代码一样。
    未定义行为产生的结果本来就是不确定的,取决于你编译器的实现,没有固定的答案。
    fcten
        7
    fcten  
       2017-08-18 14:36:06 +08:00
    被释放不等于被覆盖,在没有向该地址写入新值之前,内存中的值不会改变。
    suikator
        8
    suikator  
       2017-08-18 14:41:36 +08:00 via Android
    C 允许您将指针指向已释放的内存块
    tyrealgray
        9
    tyrealgray  
       2017-08-18 15:06:41 +08:00 via iPhone
    @masteryi 看错了😂,好久没写 C++了。
    tyrealgray
        10
    tyrealgray  
       2017-08-18 15:11:43 +08:00 via iPhone
    再仔细看了一下,你如果设置个 timeout,你的 result 再打印出来的时候就不一定是那个值了,具体看编译器怎么决定的。感觉第一段和第二段都会是这种结果
    misaka20038numbe
        11
    misaka20038numbe  
       2017-08-18 15:11:56 +08:00
    第二段为什么错了,返回 a 的地址,然后取值。
    cgb1021
        12
    cgb1021  
       2017-08-18 15:28:24 +08:00
    因为 f 要返回一个指向 int 的地址的变量(新的地址),return &a 只是返回了一个 int 的地址。就是说还缺了指向 int 的地址的地址。
    zhouheyang0919
        13
    zhouheyang0919  
       2017-08-18 15:41:47 +08:00
    @masteryi

    存取 stack pointer 以下的内存地址是未定义的行为。

    “未定义”意味着所有对这块内存空间的操作可能给出正确的结果,但更可能导致不可预料的后果(错误的结果,Segfault,各种 corruption,或其他问题)。这取决于编译器内部代码生成和优化的实现。
    cloudyfsail
        14
    cloudyfsail  
       2017-08-18 15:45:37 +08:00
    a 存在与栈上,函数 f 执行完毕后虽然栈被回收了,但是返回的地址仍然是有效的,仍然可以访问,但是这是非法行为,c++里叫未定义行为,就是说可能执行起来没问题,也可能有问题,你这个例子里 result 指向的内存没那么快重新被分配使用,所以还是得到 3.另外其实两段代码是一样的。
    masteryi
        15
    masteryi  
    OP
       2017-08-18 15:58:02 +08:00
    实现个蛋我都不想一一回复了,你们真的在你的机器上跑过吗? c 标准不可能让指针指向无效内存,你的编译器不是根据 c 标准写的想咋写就咋写想咋实现就咋实现吗? c/c++不让返回局部变量的指针和引用
    hitmanx
        16
    hitmanx  
       2017-08-18 16:01:25 +08:00   ❤️ 1
    两个都是非法的访问,但是具体会不会出现运行时的错误,这个是随机的,取决于当时内存管理的情况.虽然说因实现而异,但是在一般实现中,栈上的内容从高字节到低字节依次是
    1) main 中在 f()之前定义的局部变量
    2) f()的传参. 这儿为空,所以不占空间
    3) 返回地址(PC)
    4) f()中定义的局部变量

    f()返回以后,栈指针就指向下一个空的,也就是 2)或者说 3)的位置.访问它以下的栈内存都是非法的.
    wwqgtxx
        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.
    hitmanx
        18
    hitmanx  
       2017-08-18 16:06:51 +08:00   ❤️ 1
    @masteryi 运行时错误和编译时错误是两码事,比如
    int* p = (int*)0x100;
    printf("%d", *p);
    编译器认为这是完全合理的,只有运行才会出错
    wwqgtxx
        19
    wwqgtxx  
       2017-08-18 16:08:09 +08:00   ❤️ 1
    @masteryi “ c 标准不可能让指针指向无效内存”这句话本来就不成立,C 的指针完全可以指向一个不存在的内存,你直接写 int* i = (int*)500;这样一样能编译通过,只不过运行的时候一旦访问这个指针指向的内容,就不知道程序会不会炸掉了
    “ c/c++不让返回局部变量的指针和引用”这句话也不对,应该说是不应该而不是不让,你上面自己的代码也证明了实际上是可以返回局部变量的指针和引用,并不会编译出错,但是到了运行期间一样死未定义行为
    wwqgtxx
        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 了
    234235
        21
    234235  
       2017-08-18 16:20:28 +08:00
    1. 第一段程序,返回的是一个指针指向的地址。编译器并不总能通过上下文判断你的返回值是否为非法地址。a 是局部变量,a 的地址应该是在堆栈中,不同的编译器和运行环境对堆栈的定义不同,可能就是在普通的内存段中,这时你返回了该地址,该地址是真实存在并且是可以被你这个程序访问的,那它当然能被正确打印出来。C 不负责对任何你用过的或申请到的内存的内容进行清除操作。

    2.第二段程序错误,因为编译器识别到你返回的是一个局部变量的地址,这个是否报错同样受到编译器和环境以及你的设置的影响。有些可能就是 warning,有些可能就直接通过。说到底还是和编译器将局部变量放在哪里的关系更大。

    另外,你说 'c 标准不可能让指针指向无效内存' ,这是完全的表达错误,应该是 C 规范要求你的程序中不能出现指向未知内存的操作,这样做是不可预料的,有可能造成程序崩溃,CPU 直接抛 Hard Fault。
    如果你定义一个指针并随意的赋了一个地址值,编译器是不会报错的,这个值的正确与否是你来保证的。这也是 C 的灵活性的体现,如果 C 也对指针地址有过多的限制,很多程序就没法写了。
    zhouheyang0919
        22
    zhouheyang0919  
       2017-08-18 16:21:01 +08:00
    @masteryi

    C / C++ 是“不安全”的语言。用户代码可以访问任意地址,编译器对指针的有效性不做验证。

    “ c 标准不可能让指针指向无效内存”
    “ c/c++不让返回局部变量的指针和引用”
    来源请求。
    wwqgtxx
        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
    wevsty
        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%按照文档实现的。某种程度上确实是,想怎么实现都可以,并且相当多的行为没有被写进标准文档,这种时候开发编译器的开发者可以自己决定如何处理。
    最后,给点建议,谦虚点没错的。
    cloudyfsail
        25
    cloudyfsail  
       2017-08-18 16:36:46 +08:00
    不应该做和能不能做是两回事,lz 应该是刚学习 c++吧,c++的复杂部分体现在这里,大量的未定义行为,至于你第二段代码有错误,上面有人说了,应该是编译器设置的问题,lz 要学习还很多啊,不要这么嚣张。
    irexy
        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
    wwqgtxx
        27
    wwqgtxx  
       2017-08-18 16:56:39 +08:00 via iPhone
    @irexy 你应该看看生成的汇编,你在编译 cout a 的时候 a 就已经被直接替换成 1 了,所以并不会被改变,类似于#define 的效果
    wevsty
        28
    wevsty  
       2017-08-18 17:01:18 +08:00
    @irexy
    很好解释。
    const 的大多数时候只是一个编译器用来判断的标志,编译器编译的时候在语法上拒绝对 const 修饰的变量直接进行修改。但是变量是在内存中存放的,const 并没有对这个内存空间施加写保护,所以通过指针就可以改掉 const 修饰的变量了。
    wevsty
        29
    wevsty  
       2017-08-18 17:04:58 +08:00
    @wevsty 看错了,没注意结果。

    和 @wwqgtxx 说的差不多,这个可能是编译器优化导致的行为。要看这个编译器生成的代码是怎么生成的,可能是编译器对这样简单类型的操作就直接给优化了。
    suikator
        30
    suikator  
       2017-08-18 17:10:41 +08:00 via Android
    大家散了吧 lz 已经跑路了
    ivechan
        31
    ivechan  
       2017-08-18 17:14:21 +08:00
    一条准则:永远不要试图去解释未定义(undefined)的操作。
    这种问题根本没有答案。
    wevsty
        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。
    magicO
        33
    magicO  
       2017-08-18 17:40:04 +08:00
    这就是为啥至今不敢碰 C/Cpp 的原因。。。
    carlonelong
        34
    carlonelong  
       2017-08-18 17:46:33 +08:00
    为啥这么纠结 undefined behavior
    wingkou
        35
    wingkou  
       2017-08-18 18:23:14 +08:00 via Android
    典型初学者对 undefined behavior 的迷惑
    masteryi
        36
    masteryi  
    OP
       2017-08-18 19:44:11 +08:00
    楼主躲在角落不敢说话。。。原来第一段仅仅是编译通过了
    gnaggnoyil
        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 在语义上就明确表示不可修改,为何非得要违反语义.
    wevsty
        38
    wevsty  
       2017-08-19 14:48:30 +08:00
    @gnaggnoyil 举个例子而已,当然这种做法是不规范的也不应该提倡。
    关于   ·   帮助文档   ·   博客   ·   API   ·   FAQ   ·   实用小工具   ·   1036 人在线   最高记录 6679   ·     Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 · 26ms · UTC 20:55 · PVG 04:55 · LAX 12:55 · JFK 15:55
    Developed with CodeLauncher
    ♥ Do have faith in what you're doing.