天天看点

C++ 使用强制类型转换引入临时变量的问题(续)

今天没事回顾了一下以前写的东西,发现这一篇写的模棱两可

https://blog.csdn.net/niino/article/details/6286558

时隔多年,再来回来看这篇文章只能说还是太年轻,看待问题太片面了

问题确实是出在强转上,而且也确实会产生一个临时变量。

如何证明产生了临时变量?只需要将 const int &tt 的 const 修饰符去掉,就编译不过了,这时候会报错

Non-const lvalue reference to type 'int' cannot bind to a temporary of type 'int'
非const修饰的左值int类型引用不能绑定到一个int类型的临时变量
           

而且通过汇编代码,也可以更明显的看出来.先补一下汇编的基础知识

rbp 堆栈基指针
eax 32位累加寄存器
rax 64位累加寄存器

movl src dst 32位移动
movq src dst 64位移动
leaq src dst 取src的地址放入dst
           

我们可以先简化一下源代码,将强转直接放到 main 函数里,再来看汇编

#include <stdio.h>
#include <stdlib.h>

int main()
{
	int d = 20;
    const int& p = (int)d;
    printf("4. d = %d, d:%p\n", d, &d);
    printf("4. p = %d, p:%p\n", p, &p);
    return 0;
}
           

输出:

4. d = 20, d:0x7fffc1011e88
4. p = 20, p:0x7fffc1011e8c
           

可以看到地址不同,我们再生成汇编代码

g++ -S -fverbose-asm -O0 test.cpp -o test.s
           

test.s 内容如下:

...
# test.cpp:5: {
	movq	%fs:40, %rax	#, tmp97
	movq	%rax, -8(%rbp)	# tmp97, D.3696
	xorl	%eax, %eax	# tmp97
# test.cpp:6: 	int d = 20;
	movl	$20, -24(%rbp)	#, d
# test.cpp:7:     const int& p = (int)d;
	movl	-24(%rbp), %eax	# d, _6
	movl	%eax, -20(%rbp)	# _6, D.3689
	leaq	-20(%rbp), %rax	#, tmp92
	movq	%rax, -16(%rbp)	# tmp92, p
# test.cpp:8:     printf("4. d = %d, d:%p\n", d, &d);
	movl	-24(%rbp), %eax	# d, d.0_1
	leaq	-24(%rbp), %rdx	#, tmp93
	movl	%eax, %esi	# d.0_1,
	leaq	.LC0(%rip), %rdi	#,
	movl	$0, %eax	#,
	call	[email protected]	#
# test.cpp:9:     printf("4. p = %d, p:%p\n", p, &p);
	movq	-16(%rbp), %rax	# p, tmp94
	movl	(%rax), %eax	# *p_8, _2
	movq	-16(%rbp), %rdx	# p, tmp95
	movl	%eax, %esi	# _2,
	leaq	.LC1(%rip), %rdi	#,
	movl	$0, %eax	#,
	call	[email protected]	#
# test.cpp:10:     return 0;
	movl	$0, %eax	#, _11
# test.cpp:11: }
...
           

我们去掉强转部分的代码:

...
int main()
{
    int d = 20;
    const int &p = d;
    ...
    return 0;
}
           

再生成汇编

去掉强转后的汇编
# test.cpp:6: 	int d = 20;
	movl	$20, -20(%rbp)	#, d
# test.cpp:7:     const int& p = d;
	leaq	-20(%rbp), %rax	#, tmp91
	movq	%rax, -16(%rbp)	# tmp91, p
           
加上强转后的汇编
# test.cpp:6: 	int d = 20;
	movl	$20, -24(%rbp)	#, d
# test.cpp:7:     const int& p = (int)d;
	movl	-24(%rbp), %eax	# d, _6
	movl	%eax, -20(%rbp)	# _6, D.3689
	leaq	-20(%rbp), %rax	#, tmp92
	movq	%rax, -16(%rbp)	# tmp92, p
           

可以发现,变量 d 的地址偏移在两个版本上是不同的,一个是在rbp地址上偏移20,一个是偏移24,正好多出4个字节(一个int临时变量).

没有强转的代码分析:

根据上下文, -20(%rbp) 表示变量 d 的地址, -16(%rbp) 表示变量 p 的地址

leaq	-20(%rbp), %rax	 计算 d 的地址,放到 rax 寄存器
movq	%rax, -16(%rbp)	 把 rax 中的内容, 放到 -16(%rbp) 中

所以 p 保存的是 d 的地址

 
           

强转后的代码分析:

根据上下文 -24(%rbp)	表示 d 的地址, -16(%rbp) 表示 p 的地址
movl	-24(%rbp), %eax	 将 d 的地址,放到 eax 寄存器中
movl	%eax, -20(%rbp)	 将 eax 的内容放到 -20(%rbp) 位置上,这里产生了临时变量 -20(%rbp)
leaq	-20(%rbp), %rax	 计算 -20(%rbp)的地址,放入 rax
movq	%rax, -16(%rbp)	 将 rax 的内容放到 p 的寄存器中,此时 p 指向临时变量的地址
           

上面是基础类型 int 的分析,我们可以再来看看自建类型

#include <stdio.h>
#include <stdlib.h>

class MyClass
{
	public:
		MyClass() {
			printf("Constructor...\n");
			this->val = 0;
		}
		MyClass(const MyClass &c) {
			printf("Copy Constructor...\n");
			this->val = c.val;
		}
		~MyClass() {
			printf("Destructor...\n");
		}
	private:
		int val;
};

int main()
{
    MyClass d;
	const MyClass &p = (MyClass)d;
    printf("d:%p\n", &d);
    printf("p:%p\n", &p);
    return 0;
}
           

输出:

Constructor...
Copy Constructor...
d:0x7fffd98fa108
p:0x7fffd98fa10c
Destructor...
Destructor...
           

可以看到地址是不同的,由于自建类型的汇编代码太多,偷个懒.首先我们猜测在强转中产生了临时变量,那么根据C++的规则,那么必然会调用拷贝构造函数,所以我们定义一个拷贝构造函数,然后加上输出(如上图),可以看到在中间确实调用了拷贝构造,自然坐实了强转会产生临时变量的说法.这也是我们前面提到,不加 const 编译报错的由来. 只有使用 const,才能持有一个临时变量的地址.

默认的引用赋值语句,如

int d = 20;
int &p = d;

MyClass a;
MyClass &p = a;
           

会直接使用右值的地址为左值引用赋值, 但是如果加上类型强转,则会破坏这种语义的解析过程,即使是同类型的强转,在代码编写来看,是一种冗余的保护,但是编译器会一视同仁.所以在进行类型强转时,务必注意这一点。

继续阅读