C++对象模型之编译器如何处理函数返回一个对象

1、与经验不符的输出
我们知道,当发生以下三种情况之一时,对象对应的类的复制构造函数将会被调用:
1)对一个对象做显示的初始化操作时
2)当对象被当作参数传递给某个函数时
3)当函数返回一个类的对象时

所以,当我们设计一个函数(普通或成员函数)时,经验告诉我们,出于效率的考虑,应该尽可能返回一个对象的指针或引用,而不是直接返回一个对象。因为在直接返回一个对象可能会引起对象的复制构造过程,这意味着会发生一定量的内存复制和对象创建的动作,从而降低了程序的效率。这个设计的想法是正确的,但是实际上,当函数返回一个对象时,上述的复制构造过程一定会发生吗?

例如,对于如下的代码:
class X
{
    public:
        X()
        {
            mData = 100;
            cout << "X::X()" << endl;
        }
        X(const X& rhs)
        {
            mData = rhs.mData;
            cout << "X::X(const X&rhs)" << endl;
        }
        void setData(int n)
        {
            mData = n;
        }
        void print()
        {
            cout << "X::mData == " << mData << endl;
        }
    private:
        int mData;
};

X func()
{
    X xx;
    xx.setData(101);
    return xx;
}

int main()
{
    X xx = func();
    return 0;
}

看了上面的代码片断,你认为这个程序应该输出什么呢?若按照书本上的说法进行分析,在func函数中,定义了类X的一个局部对象xx,所以类X的构造函数会被调用;在func函数返回的返回值是一个对象,那么该函数将返回对象xx的一个副本,所以类X的复制构造函数会被调用;在main函数中,同样定义了类X的一个局部对象xx,而该对象是通过函数func返回的对象作为初值进行构造的,所以类X的复制因构造该副本而被调用。也就是说,根据这个分析,输入结果应该是:
X::X()
X::X(const X&rhs)
X::X(const X&rhs)

原本我也认为输出的应该是上面的三行,但是实际的运行结果如下图所示,它完全出乎我的意料:


从运行结果来看,只输出了一行“X::X()”,也就是说它只构造出了一个类X对象,而且没有发生任何的复制构造过程。我们的分析依据都是正确的,程序代码非常简单,分析的流程也正确,但是程序的行为究竟为什么与我们的分析不符呢?其实一切都是编译器出于优化的考虑,暗中修改了我们程序的代码。下面先介绍编译器处理返回对象的一种方法,再介绍编译器的优化究竟对我们的代码动了什么手脚。

2、编译器处理返回对象的一种方法
当函数调用完毕后,会销毁其局部对象,若函数返回一个局部对象,编译器如何把这个局部对象复制出来呢?方法如下:
1)首先为函数加上一个额外的参数,类型是类对象的引用。这个参数将用来存放被“复制建构”而得的返回值
2)在return指令之前安插一个复制构造调用操作,以便将欲传回的对象的内容当做上述新参数的初值。

该方法的两个操作会重新改写函数,使它不用返回任何值。根据这个方法,func函数的操作可能转换为如下的伪代码:
// C++伪代码,模拟构造函数和复制构造函数的调用
void func(X &__result)
{
    X xx;
    xx.X::X(); // 调用类X的默认构造函数
   
    xx.setData(101);
   
    __result.X::X(xx); // 调用类X的复制构造函数
   
    return;
}

现在编译器必须转换每一个func()调用操作,以符合其新的定义,即
X xx = func();
会被转换成为下列语句:
X xx; // 并不调用类X的构造函数
func(xx);

所以,main函数会被转换成如下伪代码
// C++伪代码,模拟构造函数和复制构造函数的调用 
int main()
{
    X xx; // 并不调用类X的构造函数
    func(xx);
    return 0;
}

根据上述编译器的操作,可以得到如下的输出:
X::X()
X::X(const X&rhs)
第一行为func函数中局部对象xx的构造,第二行为为了达到返回的目的而发生的复制构造操作。这个结果虽然与第1节中的分析有所不同,从编译使用这个方法却能减少一次复制构造函数的调用,提高了效率,毕竟对同一个对象复制两次也没有什么好处。而且编译对这个程序还会做进一步的优化,在第3节会详细讲述。

考虑函数func的另一种使用情况,就是直接使用函数func的返回值,而不将其赋给一个变量。把main函数修改成如下所示:
int main()
{
    func().print();
    return 0;
}

当遇到上述情况时,为使代码正确运行,编译器可能会进行如下转换:
// C++伪代码,模拟编译器的相关处理操作
int main()
{
    {
        X __temp;
        func(__temp);
        __temp.print();
    }
}

在《深度探索C++对象模型》一书中的例子,对于这种情况,转换后的代码没有放在一个花括号内,但是我个人认为这样做更合理。因为根据C++的定义,func函数返回的是一个临时对象,所以当语句
func().print();
运行结束后,该临时变量应该被销毁。把转换后的语句放在一对花括号中,当运行完转换后的语句后,__temp临时变量就会因出了作用域而被销毁。但是编译器是否这样做,我就不知道了。

同理,如果程序中定义了一个函数指针变量,并指向了该函数,则编译器还需要改写该函数指针的定义。

3、NRV优化
在第2节中已经分析了编译器如何处理返回一个对象的函数,但是其结果与我们程序的输出还是不一样,这是因为编译器对程序做了进一步的优化,方法就是以增加的类对象引用的参数(result参数)取代返回值的名字(named return value)。

使用此策略,func函数转换成如下的伪代码:
// C++伪代码,模拟构造函数和复制构造函数的优化
void func(X &__result)
{
    __result.X::X(); // 调用类X的默认构造函数
   
    __result.setData(101);
   
    return;
}

main函数转换后的伪代码不变,如下:
int main()
{
    X xx; // 并不调用类X的构造函数
    func(xx);
    return 0;
}

通过这个优化,可以看在在main函数的调用过程中,只会调用一次构造函数,且不会调用复制构造函数。这样的编译优化操作,被称为Named Return Value(NRV)优化。NRV优化如今被视为标准C++编译器的一个义不容辞的优化操作。所以第1节中产生的出人意料的输出,正是NRV优化的结果。

虽然NRV优化提供了重要的效率改善,但是优化是由编译器默默完成的,而它是否真的被完成,并不十分清楚,而且一旦函数变得比较复杂,优化也难以施行。


  • 2
    点赞
  • 10
    收藏
    觉得还不错? 一键收藏
  • 3
    评论
### 回答1: 《深度探索C++对象模型》是由侯捷所著的一本经典的C++图书,该书于2012年由机械工业出版社出版。本书的主要内容涵盖了C++对象模型的深入解析和探讨。 在书中,作者详细讲解了C++中的对象模型和相关的概念,如类、对象、继承、多态等。作者首先介绍了C++对象模型的基本概念和特点,包括对象的内存布局、虚函数表和虚函数指针等。然后,作者深入探讨了C++中的继承机制和多态性,包括单继承、多继承、虚继承等。作者还详细介绍了虚函数的实现原理和使用方法。 在书中,作者对C++对象模型的实现细节进行了深入的剖析,包括成员变量和成员函数的内存布局、函数指针和成员函数指针的用法等。同时,作者还讨论了C++中的一些高级特性,如模板、内存管理和异常处理等。通过对C++对象模型的深度探索,读者可以更好地理解C++的内部机制和原理,提高程序设计和开发能力。 《深度探索C++对象模型》适合具有一定的C++编程基础的读者阅读,尤其是对C++对象模型感兴趣的读者。通过阅读本书,读者可以进一步了解C++的底层实现和运行机制,从而提高自己的编程能力和代码质量。此外,本书还提供了大量的示例代码和实践案例,可以帮助读者更好地理解和应用所学知识。 总之,《深度探索C++对象模型》是一本深入探讨C++对象模型的经典著作,通过对C++的底层实现和内部机制的剖析,帮助读者深入理解C++编程语言,并提高自己的软件开发能力。 ### 回答2: 《深度探索C++对象模型》是由Stanley B. Lippman于1994年所著的一本经典畅销的C++书籍,该书详细介绍了C++对象模型的内部实现细节。 C++对象模型是指C++编译器处理对象、继承、多态等面向对象特性时所采用的具体实现方式。这本书通过对对象模型的剖析,帮助读者深入理解C++的内部工作原理,从而写出更高效、更可靠的C++代码。 在《深度探索C++对象模型》中,作者首先介绍了对象、虚函数、继承等C++核心概念,然后详细讲解了C++对象模型的构建过程,包括对象布局、成员函数指针、虚函数表等。作者逐步深入地剖析了C++对象模型在内存中的表示方式,解释了为什么C++可以支持如此强大的面向对象特性。 此外,本书还探讨了一些高级主题,如多重继承、虚拟继承、构造函数和析构函数的执行顺序等。对于想要深入学习C++的读者来说,这本书提供了一些宝贵的技术手册和实用的经验。 尽管《深度探索C++对象模型》的出版时间是1994年,但它仍然被广泛认可为学习C++对象模型的经典之作。在2012年时,由于C++的发展和演进,也许一些内容已经有些过时,但很多基本概念和原理仍然适用。 总而言之,《深度探索C++对象模型》是一本值得阅读的C++经典著作,通过深度探索C++对象模型,读者可以更加深入地了解C++的内部工作原理和实现方式,提升自己的开发技能。 ### 回答3: 《深度探索C++对象模型》是一本于2012年出版的书籍。该书的作者Andrews和Sorkin以全面的角度深入探讨了C++对象模型。该书重点介绍了C++中的对象表示、虚函数、继承、多重继承、构造函数、析构函数等内容,以及与之相关的语法、原理和底层实现。 这本书为读者揭示了C++对象模型的奥秘,让人更加深入地理解C++语言中的类和对象。作者通过分析对象布局、虚函数表、虚函数调用、多继承中的数据布局和函数调用等等,解释了C++对象模型的实现机制。 在读者了解C++对象模型的基础上,该书还介绍了如何有效地利用对象模型来提高程序的性能。作者讨论了虚函数的成本以及如何减少虚函数调用的开销,提供了一些优化技巧。此外,书中还对C++的构造函数和析构函数进行了深入的讨论,详细解释了构造函数和析构函数的执行机制和注意事项。 总的来说,《深度探索C++对象模型》是一本深入剖析C++对象模型的重要参考书籍。通过阅读该书,读者可以更加全面地了解C++的类和对象的实现原理,对于理解C++语言的底层机制和优化程序性能具有积极的作用。无论是对于初学者还是有一定C++基础的开发人员来说,该书都是一本值得阅读的重要参考书。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值