一.问题引入
1. 如下代码会输出什么?
class String{
public:
explicit String(const char* p){
size_t size = std::strlen(p) + 1;
data = new char[size];
std::memcpy(data,p,size);
cout<<"1"<<endl;
}
~String(){
delete[] data;
}
String(const String& that){
size_t size = strlen(that.data) + 1;
data = new char[size];
std::memcpy(data,that.data,size);
cout<<"2"<<endl;
}
private:
char* data;
};
int main(){
auto temp = String(String("hi"));
return 0;
}
2.答案
3. 代码解读,问题提出
- 定义了两个
String
构造函数,一个使用const char*
进行初始化,另一个使用String
变量进行拷贝初始化 - 主函数里,我们用
const char*
的“hi”
初始化了一个临时String
变量, 再用这个临时String
变量初始化另一个String
变量 - 如果是按照上述思路执行的话,应该是输出
1和2和2
,但是最后只输出了1
,说明,没有执行临时量copy的过程,问题出在哪了?编译器优化?
二.借助汇编分析问题
1. 命令
g++ -o main.s -S main.cpp
2.内容如下
- const char* 构造函数
- main函数
3.汇编代码解读
-
保存寄存器入栈
-
开辟24字节的栈空间,
-
其中24字节用于存储金丝雀值(不知道金丝雀的可以去百度)
-
随后8字节,存储的是temp类,记住,类成员函数不占空间,我们的类A,sizeof(A) == 8
4. 汇编层面解释调用成员函数
多说一句,类的成员函数如何被调用?
- 其实成员函数存储在
.text节
中,- 我们调用成员函数时,先在栈/堆上分配sizeof(类)的空间(设其地址为A),空类大小为1(而不是0),
- 此时这个空间的地址(地址A),就代表了
this
指针,- 我们将这个A(或称为
this
指针),传递到.text
节中的某个成员函数,- 成员函数隐式的第一个参数就为this,所以成员函数可以区分是哪个类在构造对象或调用对象,
- 成员函数随后在this(A地址)上进行各种操作,就是对A这块地址操作,就是对我们分配出的类实例在操作
- 不同的类实例有不同的this,即有不同的地址空间,所以我们可以使用一个成员函数操纵所有的类
5. 本函数调用的构造函数
可以看出,是const char*,而随后并没有再调用其他的构造函数了。
为什么没有调用拷贝构造函数?
原来是编译器帮我们做了优化!!!
三.问题解答:RVO优化
1. RVO优化是什么
-
返回值优化,
return value optimization
, 这是一种编译器优化机制,当函数返回一个对象的时候,如果自己创造一个临时对象进行返回(对应于main函数里,我们的String("hi")
),那么这个临时对象会消耗一个构造函数的调用(String(const char*)
),一个复制构造函数的调用(String(const String& s)
),以及一个析构函数的调用(析构掉临时值) -
经过返回值优化,就可以将成本降低到一个构造函数的代价。这样就省去了一次拷贝构造函数的调用和依次析构函数的调用。
-
注意从C++17开始,RVO优化不再是可选的,而是默认的
2. 关闭RVO优化
-fno-elide-constructors
选项可以取消编译器的 copy-elision
优化策略
3. NRVO优化是什么
- (Named Return Value Optimization)。具名返回值优化(NRVO),是对于按值返回“具名对象”(就是有名字的变量)时的优化手段,其实道理是一样的,但由于返回的值是具名变量,情况会复杂很多。所以,能执行优化的条件更苛刻。
4. 结合本例谈RVO优化底层如何实现
- 其实从上面我们对于汇编代码的讲解就已经提到了
- 我们直接从temp的地址进行构造,而不是先构造出一个临时变量,再把该变量的地址传给temp的拷贝构造函数,
- 编译器采取的是直接把
“hi”
的地址传给temp - 编译器足够智能!!!
四. 验证结果
1. 关掉RVO优化
- 输出了1,2,2
- 第一个2:临时变量传入拷贝构造函数,作为拷贝构造函数的参数
- 第二个2:构造完成,赋值给temp,调用拷贝构造函数
2. 查看底层汇编
- main函数的汇编语句
- 自己去尝试实现,读个两遍,再来看下面解释
- 可以看出,关闭优化后,拷贝构造函数也被加入了
.text
中,命名为_ZN6StringC1ERKS_
- 调用顺序也是符合预期
- 说明我们最开始的问题得到了验证。
- 如有错误,欢迎指正。