深入理解c++構造函數, 復制構造函數和賦值函數重載(operator=)


以下代碼編譯及運行環境均為 Xcode 6.4, LLVM 6.1 with GNU++11 support, Mac OS X 10.10.2

調用時機

看例子

//
//  main.cpp
//  test
//
//  Created by dabao on 15/9/30.
//  Copyright (c) 2015年 Peking University. All rights reserved.
//

#include <iostream>

class Base
{
public:
    Base()
    {
        std::cout<<"constructor"<<std::endl;
    }
    
    Base(Base &copy)
    {
        std::cout<<"copy constructor"<<std::endl;
    }
    
    const Base &operator=(Base &copy)
    {
        std::cout<<"operator="<<std::endl;
        return *this;
    }
};

int main(int argc, const char * argv[])
{

    Base a;     // 1
    Base b = a;  // 2
    Base c(a);   // 3
    
    Base d;       // 4
    d = a;
    
    return 0;
}

  輸出

constructor
copy constructor
copy constructor
constructor
operator=

  1,2,3,4 是我們創建一個變量的最主要的方法(構造序列本文不討論), 其中1,2,3是變量定義, 4是賦值. 因此很明顯:

  1. 定義會調用構造函數, 賦值會調用賦值函數(operator=)
  2. 復制構造函數是一種特殊的構造函數, 參數是一個變量實例而已
  3. 2和3等價, 3不會調用賦值函數(手誤) 2不會調用賦值函數, 出現等號未必就是賦值
  4. 如果沒有重載以上函數, 3和4效果會一樣, 但會少一次函數調用

 const來搗亂

那么const又起到什么作用了呢?

繼續來看例子

//
//  main.cpp
//  test
//
//  Created by dabao on 15/9/30.
//  Copyright (c) 2015年 Peking University. All rights reserved.
//

#include <iostream>

class Base
{
public:
    Base()
    {
        std::cout<<"constructor"<<std::endl;
    }
    
    Base(Base &copy)
    {
        std::cout<<"copy constructor"<<std::endl;
    }
    
    const Base &operator=(Base &copy)
    {
        std::cout<<"operator="<<std::endl;
        return *this;
    }
};

Base creator()
{
    Base ret;
    return ret;
}

int main(int argc, const char * argv[])
{

    Base a = creator();    // 1
    
    Base b;
    b = creator();     // 2
    
    return 0;
}

  上述代碼都會編譯出錯, 原因是 "No matching constructor". 看代碼不難發現原因, creator函數返回的是Base類型, 在c++11里面, 這個稱為右值(rvalue), 但是我們的復制構造函數和賦值函數的參數類型都是非const引用類型, 而右值是不允許做這種類型參數的, 所以就編譯出錯了. 解決方案有兩個:

  1. 使用const引用類型
  2. 使用右值類型

如下所示

    Base(const Base &copy)
    {
        std::cout<<"copy constructor"<<std::endl;
    }
    
    const Base &operator=(Base &&copy)
    {
        std::cout<<"operator="<<std::endl;
        return *this;
    }

  其中, const引用類型是最通用的作法, 它可以兼容左值和右值, 也兼容古老的編譯器, 右值類型則是c++11引進的新特性(使用&&表明), 可以針對左值和右值選擇不同的實現, 比如使用std::move替代operator=, 從而減少內存的申請. 因此, 如果沒有特殊需要, 使用const引用類型作為復制構造函數與賦值函數的參數類型.

 

至此, 構造函數的坑基本說完了, 因為不牽扯到返回值和函數類型的問題, 但是賦值函數(operator=)還有更多的坑來理一理.

const繼續攪局

在一個類的成員函數中, const可以出現三個地方: 返回值, 參數, 函數.

const A& operator=(const A& a) const

因此一個函數可以有8個變種, 但是c++不允許參數類型相同,返回值類型不同的重載, 因此一個函數最多有4種實現. 

我們先考慮返回const類型的情況

//
//  main.cpp
//  test
//
//  Created by dabao on 15/9/30.
//  Copyright (c) 2015年 Peking University. All rights reserved.
//

#include <iostream>

class A
{
public:
    const A& operator=(const A& a) const
    {
        std::cout<<"const A& operator=(const A& a) const ["<<a.x<<" > "<<x<<"]"<<std::endl;
        return *this;
    }
    
    const A& operator=(const A& a)
    {
        std::cout<<"const A& operator=(const A& a) ["<<a.x<<" > "<<x<<"]"<<std::endl;
        return *this;
    }
    
    const A& operator=(A& a) const
    {
        std::cout<<"const A& operator=(A& a) const ["<<a.x<<" > "<<x<<"]"<<std::endl;
        return *this;
    }
    
    const A& operator=(A& a)
    {
        std::cout<<"const A& operator=(A& a) ["<<a.x<<" > "<<x<<"]"<<std::endl;
        return *this;
    }
    
    std::string x;
    
    A() : x(""){}
    A(std::string x_) : x(x_) {}
};

int main(int argc, const char * argv[])
{

    A a("a"), b("b");
    const A c("const c"),d("const d");
    
    c = d;
    c = b;
    a = d;
    a = b;
    
    return 0;
}

 輸出結果

const A& operator=(const A& a) const [const d > const c]
const A& operator=(A& a) const [b > const c]
const A& operator=(const A& a) [const d > a]
const A& operator=(A& a) [b > a]

結果很明顯, 被賦值變量決定函數, 賦值變量決定參數, a=b 等價於 a.operator(b), 這里沒什么問題.

但是, 有一個很奇怪的地方, a=d 這一句, a是非const的, 調用了 const A& operator=(const A& a) [const d > a], 返回值是個const類型, 這怎么可以呢? 返回值的const是什么意思呢? 這是非常有迷惑性的. 這個問題的關鍵點在於:

a是這個函數的一部分, 並不是返回值的承接者. 因此 a=d 實際上是等價於 const A& ret = a.operator=(d), 也就是說, operator=的返回值類型和被賦值的變量是沒有任何關系的! 

加入以下代碼

    const A &m = (a = d);  // 1
    A &n = (a = d);      // 2

2會編譯錯誤, 原因就在於把 const A& 綁定給 A&, 這肯定是錯誤的. 因此再重復一遍, operator=的返回值和被賦值變量沒有任何關系.

那么返回值有什么意義呢? 這就和iostream類似了, 是為了進行串聯賦值, 亦即 a=b=c

來看最后的例子

//
//  main.cpp
//  test
//
//  Created by dabao on 15/9/30.
//  Copyright (c) 2015年 Peking University. All rights reserved.
//

#include <iostream>

class A
{
public:
    const A& operator=(const A& a) const
    {
        std::cout<<"const A& operator=(const A& a) const ["<<a.x<<" > "<<x<<"]"<<std::endl;
        return *this;
    }
    
    const A& operator=(const A& a)
    {
        std::cout<<"const A& operator=(const A& a) ["<<a.x<<" > "<<x<<"]"<<std::endl;
        return *this;
    }
    
    const A& operator=(A& a) const
    {
        std::cout<<"const A& operator=(A& a) const ["<<a.x<<" > "<<x<<"]"<<std::endl;
        return *this;
    }
    
    const A& operator=(A& a)
    {
        std::cout<<"const A& operator=(A& a) ["<<a.x<<" > "<<x<<"]"<<std::endl;
        return *this;
    }
    
    std::string x;
    
    A() : x(""){}
    A(std::string x_) : x(x_) {}
};

int main(int argc, const char * argv[])
{

    A a("a"), b("b");
    
    const A c("const c"),d("const d");
    
    (a = b) = c;    // 1
    
    (a = c) = b;    // 2
    
    a = b = c;     // 3
    
    return 0;
}

輸出

const A& operator=(A& a) [b > a]
const A& operator=(const A& a) const [const c > a]
const A& operator=(const A& a) [const c > a]
const A& operator=(A& a) const [b > a]
const A& operator=(const A& a) [const c > b]
const A& operator=(const A& a) [b > a]

  

可以得出如下結論:

  1. 1和3比較可以發現, 賦值的順序是從右往左執行的
  2. 返回值是const類型, 那么再被賦值就會調用const函數了

總結

  1. 復制構造函數和賦值函數出現在兩種不同的場景里, 不是出現等號就會調用賦值函數
  2. 賦值函數的返回值和被賦值變量是完全獨立的

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM