More than code

More Than Code
The efficiency of your iteration of reading, practicing and thinking decides your understanding of the world.
  1. 首页
  2. daily
  3. 正文

Daily C/C++ 初始化

2021年5月12日 529点热度 0人点赞 0条评论

C++ 初始化

我们从这样一个问题引入

#include <iostream>

int ans = 1 ? std::cout << "hello", 1 : 100;
int main() {
    std::cout << ans;
    return 0;
}

对C++有一定了解的同学可能会知道,这个程序的输出是hello1

因为在main函数之前,我们会有一个初始化的过程,在这个过程中,我们就会输出hello,然后在初始化结束后,我们进入main,就会输出1

这种运行时的表达式会让我们感觉怪怪的,有点空中楼阁的感觉,不是么?

那么就来到了我们今天的主题,这里主要是从汇编的角度简要说一下静态初始化

首先可以看一下我们刚才生成的汇编代码

ans:
        .zero   4
main:
        push    rbp
        mov     rbp, rsp
        mov     eax, DWORD PTR ans[rip]
        mov     esi, eax
        mov     edi, OFFSET FLAT:_ZSt4cout
        call    std::basic_ostream<char, std::char_traits<char> >::operator<<(int)
        mov     eax, 0
        pop     rbp
        ret
.LC0:
        .string "hello"
__static_initialization_and_destruction_0(int, int):
        push    rbp
        mov     rbp, rsp
        sub     rsp, 16
        mov     DWORD PTR [rbp-4], edi
        mov     DWORD PTR [rbp-8], esi
        cmp     DWORD PTR [rbp-4], 1
        jne     .L5
        cmp     DWORD PTR [rbp-8], 65535
        jne     .L5
        mov     edi, OFFSET FLAT:_ZStL8__ioinit
        call    std::ios_base::Init::Init() [complete object constructor]
        mov     edx, OFFSET FLAT:__dso_handle
        mov     esi, OFFSET FLAT:_ZStL8__ioinit
        mov     edi, OFFSET FLAT:_ZNSt8ios_base4InitD1Ev
        call    __cxa_atexit
        mov     esi, OFFSET FLAT:.LC0
        mov     edi, OFFSET FLAT:_ZSt4cout
        call    std::basic_ostream<char, std::char_traits<char> >& std::operator<< <std::char_traits<char> >(std::basic_ostream<char, std::char_traits<char> >&, char const*)
        mov     DWORD PTR ans[rip], 1
.L5:
        nop
        leave
        ret
_GLOBAL__sub_I_ans:
        push    rbp
        mov     rbp, rsp
        mov     esi, 65535
        mov     edi, 1
        call    __static_initialization_and_destruction_0(int, int)
        pop     rbp
        ret

可以看到有一个标号叫_static_initialization_and_destruction_0(int, int)

这里猜测应该就是静态初始化的地方了,我们沿着这段代码往下看,就可以看到

    mov     esi, OFFSET FLAT:.LC0
    mov     edi, OFFSET FLAT:_ZSt4cout
    call    std::basic_ostream<char, std::char_traits<char> >& std::operator<< <std::char_traits<char> >(std::basic_ostream<char, std::char_traits<char> >&, char const*)
    mov     DWORD PTR ans[rip], 1

.LC0是我们的字符串标号

这里就是将我们的字符串作为参数传入进去,输出了hello

之后再把ans的值赋为了1

然后我们可以看cpp reference

20210512141229

与这章有关的内容就在这里,作为程序的一部分,在main函数的执行之前被初始化

而这里我们做的就是静态初始化

所以这样也就可以解释刚才程序输出的结果了

那么之后再看这样一段程序

#include<iostream>

class test {
    int *x = new int;
};

int main() {
    test t;
    return 0;
}

将运行时表达式写到这个变量的声明中,初步猜测就是作为了变量的默认值

这里是对应的汇编代码

test::test() [base object constructor]:
        push    rbp
        mov     rbp, rsp
        sub     rsp, 16
        mov     QWORD PTR [rbp-8], rdi
        mov     edi, 4
        call    operator new(unsigned long)
        mov     rdx, rax
        mov     rax, QWORD PTR [rbp-8]
        mov     QWORD PTR [rax], rdx
        nop
        leave
        ret
main:
        push    rbp
        mov     rbp, rsp
        sub     rsp, 16
        lea     rax, [rbp-8]
        mov     rdi, rax
        call    test::test() [complete object constructor]
        mov     eax, 0
        leave
        ret
__static_initialization_and_destruction_0(int, int):
        push    rbp
        mov     rbp, rsp
        sub     rsp, 16
        mov     DWORD PTR [rbp-4], edi
        mov     DWORD PTR [rbp-8], esi
        cmp     DWORD PTR [rbp-4], 1
        jne     .L6
        cmp     DWORD PTR [rbp-8], 65535
        jne     .L6
        mov     edi, OFFSET FLAT:_ZStL8__ioinit
        call    std::ios_base::Init::Init() [complete object constructor]
        mov     edx, OFFSET FLAT:__dso_handle
        mov     esi, OFFSET FLAT:_ZStL8__ioinit
        mov     edi, OFFSET FLAT:_ZNSt8ios_base4InitD1Ev
        call    __cxa_atexit
.L6:
        nop
        leave
        ret
_GLOBAL__sub_I_main:
        push    rbp
        mov     rbp, rsp
        mov     esi, 65535
        mov     edi, 1
        call    __static_initialization_and_destruction_0(int, int)
        pop     rbp
        ret

查看汇编代码我们可以发现,x的初始化是在test的构造函数中的,并且和

#include<iostream>

class test {
    int *x;
public:
    test() {
        x = new int;
    }
};

int main() {
    test t;
    return 0;
}

这段代码没什么区别

那么也就是说,我们给这些变量设置的默认值其实就是相当于在构造函数中进行了初始化

我们将代码改写成这样

class test {
    int *x;
public:
    test():x(new int) {
    }
};

得到的汇编代码也是没有区别的

那这时候有的同学可能就会有疑问了,如果我两个同时设置,那到底是取谁的呢?

这里我们来做个实验

class test {
    int *x = new int;
public:
    test():x(NULL) {
    }
};

对于这段程序来说,生成的汇编如下

test::test() [base object constructor]:
        push    rbp
        mov     rbp, rsp
        mov     QWORD PTR [rbp-8], rdi
        mov     rax, QWORD PTR [rbp-8]
        mov     QWORD PTR [rax], 0
        nop
        pop     rbp
        ret

可以看出结果是构造函数中的初始化列表优先级更高一些

那再看这个

class test {
    int *x = new int;
public:
    test() {
        x = NULL;
    }
};

得到的是

test::test() [base object constructor]:
        push    rbp
        mov     rbp, rsp
        sub     rsp, 16
        mov     QWORD PTR [rbp-8], rdi
        mov     edi, 4
        call    operator new(unsigned long)
        mov     rdx, rax
        mov     rax, QWORD PTR [rbp-8]
        mov     QWORD PTR [rax], rdx
        mov     rax, QWORD PTR [rbp-8]
        mov     QWORD PTR [rax], 0
        nop
        leave
        ret

这里我们可以发现,虽然优先级更高的是变量的初始化, 但是在这之后程序也把x设为了0

也就是说我们这样写会造成内存泄漏

同样的如果我们在构造函数中写 x = new int

就会申请两次空间,第一次的空间没有释放,然后使用了第二次申请的空间,造成了内存泄漏

但是使用初始化列表进行空间的申请就不会出现这个问题

所以优先级是初始化列表 > 成员变量的默认值 > 构造函数内的赋值

同时,为了防止出现类似的问题,还是建议大家不要使用这种写法

我的一个猜测是当我们涉及到申请用户自定义的空间时,可能会出现初始化次序的问题(详见cpp reference 动态初始化)

所以还是让代码各司其职就好,不要试图依赖运行时表达式来初始化变量,或者在类的声明处进行初始化,显式的把任务给构造函数就好。

标签: c++
最后更新:2021年5月12日

sheep

think again

点赞
< 上一篇
下一篇 >

文章评论

取消回复

COPYRIGHT © 2021 heavensheep.xyz. ALL RIGHTS RESERVED.

THEME KRATOS MADE BY VTROIS