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函数的操作可能转换为如下伪代码:

void func(X &__result)  
{  
    extern X xx;  
    xx.X::X(); // 调用类X的默认构造函数  

    xx.setData(101);  

    __result.X::X(xx); // 调用类X的复制构造函数  

    return;  
}

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

所以,main函数会被转换成如下伪代码

    // C++伪代码,模拟构造函数和复制构造函数的调用   
    int main()  
    {  
        extern 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()  
    {  
        {  
            extern 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()  
{  
    extern X xx; // 并不调用类X的构造函数  
    func(xx);  
    return 0;  
} 

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

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

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值