【编程篇】C++11系列之——临时对象分析

/*C++中返回一个对象时的实现及传说中的右值——临时对象*/

如下代码:
 1 /**********************************************/
 2 class CStudent;
 3 CStudent GetStudent()
 4 {
 5    CStudent loc_stu;
 6    return loc_stu;
 7 }
 8 
 9 int main()
10 {
11    CStudent stu = GetStudent();
12 }
13 /**********************************************/

 来看一下main函数中调用GetStudent()的汇编实现:

看上去,GetStudent()是无参函数,可是为什么在call之前push了一个参数到堆栈里呢?并且call完之后还add esp, 4来使堆栈平衡。

这个调用是这么实现的:
首先,在main函数中,stu是作为局部变量存在的,因为是个对象,会调用构造函数。
但是请注意!!!!:这里并不会在main函数中调用构造函数来构造stu,而是仅仅为stu预留了空间sizeof(CStudent)大小,并将这个空间的地址通过
mov eax, address
push eax
传递给了GetStudent()。

在GetStudent()内部定义了它的局部变量loc_stu,GetStudent()内部构造了它,在返回的时候,通过 <拷贝构造函数> 拷贝到前面提到的那个通过eax传递的隐含的参数。

总结就是说虽然stu在main里面定义,但其内存区域的内容是由GetStudent()完成的。这里需要注意的是这种调用方式,因为返回的是个对象,所以做法是调用者提前准备好一个对象空间,然后把地址告诉被调用者,被调用者得到这个地址后对其赋值,从而完成对象的返回。我姑且将这种调用方式称之为__robjcall:返回对象调用方式!呵呵

注意!!!
如果main函数改成下面这样:
/***************************************************************************/
int main()
{
   //CStudent stu = GetStudent();  原来的写法
   CStudent stu;
   stu = GetStudent();
}
/***************************************************************************/
这样先定义再赋值则大为不同!!!
stu此时作为main函数的局部变量则会调用构造函数进行初始化。另外调用GetStudent()时和上面一样,因为返回的是对象,则需要一个对象的地址,这里并不是将stu的地址交给它,因为stu已经构造好了,不能交给别的函数随意处理。而是会产生一个临时的匿名对象!!!
这个临时的匿名对象将代替之前第一种写法的做法,将自己地址交给GetStudent(),完成自己
内存的赋值。最后,通过stu的赋值操作符operator=将临时对象的值交给stu自己。再然后,临时对象完成自己的使命,立即析构,而不会等到main函数退出时。当然这个匿名的临时对象同样作为main函数的局部变量,在main函数栈帧建立时就预留了空间,这是编译器在编译的时候发现了需要一个临时对象来完成任务所以为其预留了位置。

所以,由上看出,第二种写法将增加临时对象的开销,还有进行赋值操作的开销。
如果对象比较复杂,这个开销是不能忍受的。比如下面的类型,赋值和拷贝都需要进行堆内存的操作,消耗时间。
/***************************************************************************/
class Array
{
public:
    Array(int l)                    //构造函数
    {
        pData = new int[l];
     len = l;
    }

    ~Array()                         //析构函数
    {
     delete pData;
     len = 0;
    }

    Array(const Array& other)                //拷贝构造函数
    {
     pData = new int[other.len];
     memcpy(pData, other.pData, other.len);
     len = other.len;
    }

    Array& operator=(const Array& other)          //赋值操作符
    {
     pData = new int[other.len];
     memcpy(pData, other.pData, other.len);
     len = other.len;
     return *this;
    }

private:
    int* pData;
    int len;
};
/***************************************************************************/
这便是C++中臭名昭著的临时对象性能问题!
在C++11中为了解决这个问题,引入了右值引用和move语意!
C++98规定了左值引用,如
int a;
int &b = a;
这里,b作为一个引用,引用了变量a的值,这里a是一个左值。
C++11引入了右值引用:
int &&m = 5;
这里会将5转换为一个临时对象(int型变量),然后m引用这个临时对象。
既然是右值引用,只可以引用临时对象——即右值!

这样,在C++11里,类引入了转移构造函数和转移赋值操作符,如下:
/***************************************************************************/
class Array
{
public:
    Array(int l);                          //构造函数
    ~Array();                              //析构函数
    Array(const Array& other);                //拷贝构造函数
    Array& operator=(const Array& other);     //赋值操作符


    Array(Array&& other)                   //转移构造函数
    {
     pData = other.pData;
     len = other.len;
    
     /*将资源转移过来,避免资源拷贝*/
     other.pData = NULL;
     other.len = 0;
    }

    Array& operator=(Array&& other)          //转移赋值操作符
    {
     pData = other.pData;
     len = other.len;
    
     /*将资源转移过来,避免资源拷贝*/
     other.pData = NULL;
     other.len = 0;

     return *this;
    }

private:
    int* pData;
    int len;
};

Arrar GetArray()
{
    Array loc_num(10);

    /*some operation*/

    return loc_num;
}

int main()
{
    Array num(10);
    num = GetArray();   
}
/***************************************************************************/
这样,当main函数中调用GetArray()时,虽然会有一个临时匿名对象产生(前面说了,用于GetArray填充返回值用),但这里在将临时对象的内容赋值给局部对象num时,将自动识别为右值赋值,不会调用原始赋值函数Array& operator=(const Array& other);转而调用转移赋值函数Array& operator=(const Array&& other);不会进行内存二次分配,从而节省开销。
posted @ 2014-08-03 19:55  轩辕之风  阅读(1355)  评论(0编辑  收藏  举报