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

为什么 const 引用可以指向常量还可以取到地址?

  •  
  •   ngg0707 · 2018-09-24 00:09:59 +08:00 · 5745 次点击
    这是一个创建于 2254 天前的主题,其中的信息可能已经有所发展或是发生改变。
    #include <iostream>
    using namespace std;
    
    int main(int argc, char const* argv[])
    {
        const int& a = 1;
        cout << &a << endl;
        return 0;
    }
    

    不知道该怎么搜索这个问题,没查到。有人知道吗?

    44 条回复    2018-10-05 11:58:39 +08:00
    jmc891205
        1
    jmc891205  
       2018-09-24 00:23:41 +08:00
    编译器生成了一个临时对象
    ngg0707
        2
    ngg0707  
    OP
       2018-09-24 00:27:39 +08:00
    ```cpp
    #include <iostream>
    using namespace std;

    int main(int argc, char const* argv[])
    {
    int&& a = 1;
    cout << &a << endl;
    return 0;
    }
    ```

    这样居然也可以。
    ngg0707
        3
    ngg0707  
    OP
       2018-09-24 00:28:23 +08:00
    @jmc891205 您好,如果不这样做,是否编译器就不会给这个右值分配内存呢?
    jmc891205
        4
    jmc891205  
       2018-09-24 00:36:45 +08:00   ❤️ 1
    @ngg0707 对你给的这个例子来说是的 你不声明 const int&a 和 int &&a 的话 那是不会给常量 1 分配内存
    anonymous256
        5
    anonymous256  
       2018-09-24 00:38:03 +08:00 via Android   ❤️ 2
    对于 1,2.55 ,'a',这类的数据,它们是存放在寄存器上的,用于初始化等操作。没有所谓的内存地址,属于字面常量(literal constant)。如 int x = 5,x 是左值,5 是右值。x 是变量(左值)有地址,5 是字面常量(右值)无地址。

    C/C++能凡是能取到地址的,该类被认为是左值。字面值常量被认为是右值,不允许取地址。

    至于楼主提到的 const 常量,const 只是修饰符,只能说是 const 修饰后,该变量不能被修改,它和字面值常量(右值)有着本质的不同。

    我了解的是这样了,欢迎指正。
    ngg0707
        6
    ngg0707  
    OP
       2018-09-24 00:40:20 +08:00
    @anonymous256 我觉得楼上解决了我的问题。你可以看看楼上的回答。
    snnn
        7
    snnn  
       2018-09-24 00:47:34 +08:00
    @anonymous256 完全错误!!!
    jmc891205
        8
    jmc891205  
       2018-09-24 01:04:01 +08:00
    @jmc891205 我在 4 楼有地方说的可能有歧义
    不是「不会给常量 1 分配内存」
    而是不会分配内存去存储常量 1
    429839446
        9
    429839446  
       2018-09-24 01:04:54 +08:00
    如果我没记错。
    常量引用可以绑定右值。
    常量引用本身是左值。
    Sparetire
        10
    Sparetire  
       2018-09-24 01:41:48 +08:00 via Android
    常量和变量不可变还是有区别的吧,我的理解是 const 还是运行时创建,而常量是编译时就确定的
    raysonx
        11
    raysonx  
       2018-09-24 02:00:02 +08:00 via iPad   ❤️ 1
    @anonymous256 然而你的理解是错误的。
    另外 @ngg0707
    其他架构的 cpu 我不敢说,在 x86 架构下,整数的右值通常直接对应汇编的立即数寻址,也就是直接保存在编译后的机器指令中,并不会被保存在寄存器。以你举的 int x=5 为例,编译后的汇编指令通常是这样的:

    mov [x 变量的内存地址], 5

    在实际的机器代码中就是保存在了二进制指令的几个字节中。在 cpu 执行到这一句的时候,动态载入到指定的内存地址(或寄存器)。

    对于字符串,极有可能是保存在一个单独的段中。
    xupefei
        12
    xupefei  
       2018-09-24 02:28:44 +08:00
    @raysonx #11 字符串赋值是是 mov dword ptr,本身存在可执行文件的 rdata 段里,通过 HEX 编辑器就能找到。
    shoujiaxin
        13
    shoujiaxin  
       2018-09-24 03:08:17 +08:00 via iPad
    C++ Primer (第五版)中文版的第 55 页有解释
    这是一种例外情况,初始化常量引用时允许使用任意表达式作为初始值!包括非常量的对象、字面值或者一般的表达式!只要该表达式的结果能转换成引用的类型即可。
    字面值的常量引用实际是绑定到一个临时量
    geelaw
        14
    geelaw  
       2018-09-24 03:51:37 +08:00 via iPhone
    那你觉得 int x; int const &y = x; 的话 &y 有没有意义呢?
    snnn
        15
    snnn  
       2018-09-24 05:20:23 +08:00
    这种垃圾代码你们纠结一半天有什么意义?现实工作中谁把代码写成这样先去好好反省下正确的该怎么写。
    mintist
        16
    mintist  
       2018-09-24 09:51:45 +08:00
    楼主,来看看汇编代码就晓得为啥还是能取到地址了,把代码再精简下,然后结合汇编来看下。


    精简的 C++代码:

    ```c
    int main(void)
    {

    const int &a = 1; // 将变量 a 指向常量的地址,后面使用时可直接引用使用


    return 0;
    }
    ```

    对应在 ARM-gcc 下的汇编代码:注释是后面添加的

    ```asm
    main:
    sub sp, sp, #8 ; 在函数内申请栈空间,通过偏移 sp 来实现

    mov r3, #1 ; 将立即数放到寄存器 r3 中
    str r3, [sp] ; 将 r3 的值推到栈中
    mov r3, sp ; 将 sp 值也就是栈地址保存到 r3 中,也就是放置立即数 1 的内存地址
    str r3, [sp, #4] ; 将放置立即数 1 的内存地址放在栈中(带偏置),也就是我们所需要的 a 变量的值,后面需要引用 a 所指向的值时,编译器就去这个地址取出来,再指过去就好了。

    ; ARM 体系结果默认把第 1 个返回值放到 r0 中,所以在 bx 之前把 r0 的值 0 准备好就可以了
    mov r3, #0
    mov r0, r3
    add sp, sp, #8
    bx lr
    ```

    所以,回到“为什么 const 引用可以指向常量还可以取到地址?”这个问题,在汇编代码看来,就是先把立即数 1 放到存储空间(这里是栈空间,如果是全局变量,那么就会在链接时到.rodata 段内存空间),然后再把 a 变量的本身也存下(存的值就是 1 的地址),用的时候取出来指过去就可以了。



    详见如下的链接: https://godbolt.org/#g:!((g:!((g:!((h:codeEditor,i:(j:1,lang:c%2B%2B,source:'int+main(void)%0A%7B%0A++++const+int+%26a+%3D+1%3B%0A++++return+0%3B%0A%7D%0A'),l:'5',n:'0',o:'C%2B%2B+source+%231',t:'0')),k:50,l:'4',n:'0',o:'',s:0,t:'0'),(g:!((h:compiler,i:(compiler:arm710,filters:(b:'1',binary:'1',commentOnly:'0',demangle:'0',directives:'0',execute:'1',intel:'0',trim:'0'),lang:c%2B%2B,libs:!(),options:'-fomit-frame-pointer',source:1),l:'5',n:'0',o:'ARM+gcc+7.2.1+(none)+(Editor+%231,+Compiler+%231)+C%2B%2B',t:'0')),k:50,l:'4',n:'0',o:'',s:0,t:'0')),l:'2',n:'0',o:'',t:'0')),version:4
    mintist
        17
    mintist  
       2018-09-24 09:54:43 +08:00
    @snnn 可能楼主和我一样,是给自己设计的芯片写代码,然后给别人用的,所以需要比较纠结这些细节,,,
    iwtbauh
        18
    iwtbauh  
       2018-09-24 09:57:47 +08:00 via Android
    @raysonx #11

    可能他说的是 RISC 架构的计算机吧,你说的 x86 这种 CISC 计算机自然不一样。

    int x = 5,在 RISC 上是这样工作的

    mov 5, 寄存器 1
    store 寄存器 1, (寄存器 2)

    确实需要“放在寄存器”上

    @anonymous256 #5

    但是说法非常有歧义且容易混淆,立即常量是先在机器指令里,再在寄存器里。
    iwtbauh
        19
    iwtbauh  
       2018-09-24 10:05:23 +08:00 via Android
    再回到 lz 的问题

    为什么会有地址,其实编译器也很无奈啊,本来设计的当的编译器不会给 const 分配地址,让他做立即常量来避免多一次的内存访问(要知道访问内存可是开销很大的操作呀),但是 lz 偏偏要 &const,这相当于创建 const 变量的一个引用,于是编译器就不能把它优化掉了,所以 lz 才看到地址输出
    wizardforcel
        20
    wizardforcel  
       2018-09-24 10:29:14 +08:00
    const 那种东西,你把它当成只读变量就好了。。
    wizardforcel
        21
    wizardforcel  
       2018-09-24 10:29:33 +08:00
    @jmc891205 `a + 1` 也是临时变量,但它没有地址。
    pagict
        22
    pagict  
       2018-09-24 15:04:00 +08:00 via iPhone
    @snnn 单这个例子的代码是没啥好探讨的垃圾代码,但以此可以摸索编译器的内在原理,也不失为一种有意义的讨论
    wizardforcel
        23
    wizardforcel  
       2018-09-24 15:29:15 +08:00 via Android
    #20 #21 原来是 const& 啊,我看错了。。。
    sfqtsh
        24
    sfqtsh  
       2018-09-24 15:54:13 +08:00 via Android
    右值引用和 const 左值引用都可以指向一个左值。引用本身是右值,而右值可以被取地址。
    kingcc
        25
    kingcc  
       2018-09-24 19:08:26 +08:00
    学习到了
    agagega
        26
    agagega  
       2018-09-24 20:09:44 +08:00 via iPhone
    特殊操作
    onemoo
        27
    onemoo  
       2018-09-24 21:00:25 +08:00
    只从语法角度考虑这个问题:
    你知道等号右侧的字面量 1 是右值。你也知道“ const 引用”是可以引用右值的。

    @sfqtsh 但是“取地址&运算符”的操作数需要是左值。

    记得 C++ 中有这样一条规则“具名引用为左值”。所以即便这个 a 是右值引用(&&),a 也被视为左值表达式,可以对其应用 & 运算符。
    ngg0707
        28
    ngg0707  
    OP
       2018-09-24 21:01:01 +08:00
    @mintist 应聘做题做到这个,就想了解一下……
    sfqtsh
        29
    sfqtsh  
       2018-09-24 21:36:44 +08:00 via Android
    @onemoo 衰,,,写错了。应该是:

    "右值引用和 const 左值引用都可以指向一个右值。引用本身是左值,而左值可以被取地址。"
    iceheart
        30
    iceheart  
       2018-09-24 22:06:28 +08:00 via Android
    都引用了,咋能没有地址呢?
    SKull4
        31
    SKull4  
       2018-09-24 22:27:24 +08:00
    @ngg0707 应聘的 iOS 开发么。。。
    dangyuluo
        32
    dangyuluo  
       2018-09-25 01:46:24 +08:00
    @anonymous256 大哥你是认真的么。。一本正经的胡说八道
    dangyuluo
        33
    dangyuluo  
       2018-09-25 01:49:20 +08:00
    xuanbg
        34
    xuanbg  
       2018-09-25 06:26:42 +08:00
    变量怎么能没有地址?
    innoink
        35
    innoink  
       2018-09-25 07:47:51 +08:00 via Android
    @xuanbg int a = 2; int& b = a; 那么&b 是 a 的地址还是 b 的地址?
    xuanbg
        36
    xuanbg  
       2018-09-25 08:50:28 +08:00
    @innoink &b 是变量 b 的指针,也就是变量 b 在栈上的地址,b 才是指向 a 的地址
    wutiantong
        37
    wutiantong  
       2018-09-25 10:16:50 +08:00   ❤️ 1
    liuminghao233
        38
    liuminghao233  
       2018-09-25 13:30:11 +08:00
    @xuanbg
    int a = 2;
    int& b = a;
    这样的话
    &a == &b
    innoink
        39
    innoink  
       2018-09-25 14:00:46 +08:00
    @xuanbg 你错了,int&b = a;在这一刻以后 b 和 a 完全等价,包括&b 其实就是&a,所以楼主才有"如何对字面量取地址"的疑问。
    innoink
        40
    innoink  
       2018-09-25 14:01:49 +08:00
    @xuanbg 我估计你搞混了引用类型和指针类型。你无法对一个引用类型的变量取地址。
    cyspy
        41
    cyspy  
       2018-09-25 14:56:11 +08:00
    虽然不懂 C++,真的没人考虑这是不是 UB 吗。。
    ngg0707
        42
    ngg0707  
    OP
       2018-09-25 16:41:54 +08:00
    @cyspy 但是特么考试考啊
    across
        43
    across  
       2018-09-27 10:25:47 +08:00   ❤️ 1
    more effective C++似乎讲过。临时值生成的那节。

    const int& a = 1;属于 ref to const,目标值为常量,允许。
    int& a = 1,ref to non-const,目标值可能被修改,不允许。
    FrankHB
        44
    FrankHB  
       2018-10-05 11:58:39 +08:00
    @xuanbg 初始化一个函数引用,你告诉我这货地址是啥?
    @cyspy 不是 UB。
    const lvalue reference 用 prvalue 初始化类型兼容,temporary materialization conversion 过了自然就有个对象,但是抽象机语义上有对象和实现是不是给对象分配存储是两回事。就算分配,这种没 ABI 限制的东西不放寄存器里要放主存可以算是没事找事。
    至于&的结果还真不一定,因为<<出来的内容是 implementation-defined,编译器完全可以不生成一个对象给你随便一个幺蛾子,只要给文档就行。只不过通常实现都懒得和 iostream 这种破烂有一腿日 builtin 所以看不到而已……
    关于   ·   帮助文档   ·   博客   ·   API   ·   FAQ   ·   实用小工具   ·   887 人在线   最高记录 6679   ·     Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 · 25ms · UTC 21:38 · PVG 05:38 · LAX 13:38 · JFK 16:38
    Developed with CodeLauncher
    ♥ Do have faith in what you're doing.