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
与这章有关的内容就在这里,作为程序的一部分,在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 动态初始化)
所以还是让代码各司其职就好,不要试图依赖运行时表达式来初始化变量,或者在类的声明处进行初始化,显式的把任务给构造函数就好。
文章评论