2017年3月6日 星期一

重載複製建構子 與 複製函數 operator=()

重載複製建構子與複製函數

tags: C++ Concept

自動生成

如果你沒有建立這些函式,編譯器會自動幫你建立這些函式
class T {
public:
    T();
    ~T();
    T(const T &);
    T& operator=(const T &);
};

拷貝問題

預設建立的這些函式在某些情況下並不適合,可以用深層拷貝與淺層拷貝的概念來看,預設的僅僅只會做淺層拷貝,複製當前的容器型別。如果你的代碼中存在著指針,可能會出一些問題,預設函式不會拷貝指針所指之處。
就是說當利用預設複製函式複製一個含指針的自訂類別
T a;
T b=a;
當你嘗試修改a資料成員時,由於a與b的資料成員指向同一處,因此會連動造成改a時b也一起被更動了。很多時候這可能不是我們所要的結果。
/*****************************************************************
Name : 
Date : 2017/03/06
By   : CharlotteHonG
Final: 2017/03/06
*****************************************************************/
#include <iostream>
#include <numeric>
using namespace std;

class List{
public:
    // 建構子
    List(size_t len=3): len(len){
        this->list = new int[len];
        // 初始化資源
        iota(list,list+len,1);
    }
    // 解構子
    ~List(){
        delete [] this->list;
    }
public:
    List & pri(string name=""){
        if(name != "")
            cout << name << endl;
        for(unsigned i = 0; i < this->len; ++i) {
            cout << "  " << (*this).list[i] << ", ";
        } cout << endl;
        return (*this);
    }
public:
    int* list;
    size_t len;
};
/*==============================================================*/
int main(int argc, char const *argv[]){
    List a, b=a;
    a.pri("origin");
    b.pri();

    a.list[0]=7;
    a.pri("a[0]=7");
    b.pri();

    return 0;
}
/*==============================================================*/
你會發現僅更改a的數值結果卻連b也一起更動了。

複製建構函式與複製函式差別

建立時賦值與建立後賦值是不一樣的

來看這樣的例子
int i=0;
int i(0);
原始的C語言在宣告時用 = 賦值就是初始化的意思。C++多了一種比較不會產生閱讀含意誤會的方式,避免原本的寫法。
int i=0;
含有
先建立 i 再建立常數 0 ,然後在更改 i 的數值(複製)
這樣的閱讀含意
注意:閱讀含意不代表編譯器真這樣做,僅僅只是依照看起來的樣式去推論,也有不少人忍為 i=0i(0) 閱讀含意沒有區別且前者更容易被閱讀與理解。這沒有什麼影響與爭論純屬個人習慣。

初值陣列

建立時直接給定值(初值陣列),而不是建立後再複製給值
當成員函數具備const修飾時可以明顯看出差異。因為一個已經被建立的const物件是不能夠被修改的,只能在建立的時候就賦予初值。
class T {
public:
    T(int i):i(i){
        cout << "i=" << this->i << endl;
    }
private:
    const int i;
};
你可以試著移除初值陣列,使用事後賦值的方式,會發現無法修改
class T {
public:
    T(int i){
        this->i = i;
        cout << "i=" << this->i << endl;
    }
private:
    const int i;
};
我想試著從這個例子告訴你初值陣列的優點,並從此說明建立時賦值與建立後賦值是兩回事。

建構複製函式

原意就是直接在建立時就給定別人已經建好的規格
T a, a(b);
而不是
T a, T b;
a=b;
你可以想像一個物品在生產工廠就幫你客製化好拿到手直接用比較有效率;還是工廠出來你又自己修改比較有效率呢?
後者在極端的情況下,你會發現你手上多了換下來似乎用不大上的零件(你還多花錢買這換下來,卻暫時完全用不上的零件)。
他的函式長這個樣子
List::List(const List & rhs): len(rhs.len){...}
可以看出呼叫條件就是在宣告時呼叫
List a;
List b(a);
現在你應該可以解為什麼這樣的寫法閱讀含意更清楚了吧
int i(0);
你也可以寫作
List a;
List b=a;
int i=0;
他們是等價的。

operator複製函式

已經生成好了,既有物件間的複製
List a, b;
b=a;
他的函式長這個樣子
List & List::operator=(const List & rhs){...}

重載複製函式

建構子

  • 要求記憶體空間
  • 給定初值

複製建構子

  • 要求記憶體空間
  • 複製資源(或給定出值)

複製函式

  • 要求記憶體空間
  • 複製資源
  • 刪除舊有資源
  • 判定來源是否相同
/*****************************************************************
Name : 
Date : 2017/03/06
By   : CharlotteHonG
Final: 2017/03/06
*****************************************************************/
#include <iostream>
#include <numeric>
using namespace std;

class List{
public:
    // 建構子
    List(size_t len=3): len(len){
        this->list = new int[len];
        // 初始化資源
        iota(list,list+len,1);
    }
    // 複製建構子
    List(const List & rhs): len(rhs.len){
        this->list = new int[len];
        // 複製
        for(unsigned i = 0; i < len; ++i)
            (*this).list[i] = rhs.list[i];
    }
    // 解構子
    ~List(){
        delete [] this->list;
    }
public:
    List & pri(string name=""){
        if(name != "")
            cout << name << endl;
        for(unsigned i = 0; i < this->len; ++i) {
            cout << "  " << (*this).list[i] << ", ";
        } cout << endl;
        return (*this);
    }
public:
    // 重載賦值符號
    List & operator=(const List & rhs){
        // 相同則離開
        if(this == &rhs)
            return (*this);
        // 清除原始資源
        this->~List();
        // 重建資源
        this->list = new int[len];
        this->len = rhs.len;
        // 複製
        for(unsigned i = 0; i < this->len; ++i)
            (*this).list[i] = rhs.list[i];
        return (*this);
    }
public:
    int* list;
    size_t len;
};
/*==============================================================*/
int main(int argc, char const *argv[]){
    List a(2);
    List b=a;
    List c(4);

    a.pri("origin");
    b.pri();
    c.pri();

    c=a;

    a.list[0]=7;
    a.pri("a[0]=7");
    b.pri();
    c.pri();

    return 0;
}
/*==============================================================*/

參考

  1. C++中复制构造函数与重载赋值操作符总结
  2. 複製建構函式、物件的指定
  3. C++ 快速導覽 - 類別 Copy 建構函數

2017年3月4日 星期六

const 屬性規則與可能出現的問題

const 屬性規則與可能出現的問題

tags: C++ Concept

規則

  • 引入參數為 const 也可以導入 非const 版本
  • 物件帶 const 屬性,使用函數時只能呼叫 const 版本函式
  • 一般條件下 const 屬性必須被從頭到尾繼承
  • 使用 const_cast<typename> 可移除 const 屬性
  • 被宣告為 const 屬性只能在宣告時賦值

宣告類別時的放置位置

const 的放置位置在宣告時可以跟型別換位置
這樣有助於提升閱讀,不然一整排開頭都 const 還要往右邊看一下才知道是什麼型別。
const int i;
int const i;

要注意的是當指針符號(derefrence)加入時有些微的不同。
可以分成3種不同
const int *i;
int const *i;

int* const i;

const int* const i;

分辨方式簡單用一條規則,看const右邊有沒有 *
const 有修飾到 * 所以 * 不可被修改
const int *i;
int const *i;

*i=0; // Error 不可被修改

const 沒有修飾到 * 所以 沒有 * 不可被修改
int * const i;
i=0;  // Error 不可被修改

指針本身指針所指 之處都不能變動
const int* const i;
i=0;  // Error 不可被修改
*i=0; // Error 不可被修改
可能還有其他樣式大概就這條主規則去判定

宣告函式時的放置位置

回傳型別可以交換位置
指的是傳出的參考在這之後,可不可以被更改
const int & fun(){...}
int const & fun(){...}

放置在引入參數
可以接收 const非const 參數
void fun(const int i){
    i=0; // Error 不可修改
}

const int i=0;
fun(i);

int j=0;
fun(j);

放置在引入參數後(成員函式)

本地成員(this)不可被修改
void T::fun() const{
    this->data = 0; // Error 不可修改
}

這裡可能會遇到一個問題
存取函式成員,如果定義了非const版本,建議補上const版本
int & T::get(){...}
在某些狀況可能會用到 const 版本
void T::add(const T& obj){
    this->get() += obj.get()
}
報錯
error: passing '...' as '...' argument discards qualifiers [-fpermissive]
因為引入的參數是有 const 修飾,他找不到 const版本 的函式可用。
const屬性必須被繼承下去

 放棄const修飾
void T::add(T& obj){
            ^
    this->get() += obj.get()
}

多載 const 版本的 get()
int & T::get() const{...}
                 ^
重寫兩份一樣的代碼的問題可以參考:重載operator[]

移除 const 屬性
void T::add(const T& obj){
    this->get() += const_cast<T&>(obj).get()
}
多載的可能是比較好的解法,不會干預到其他地方。

const 屬性只能在宣告時賦值

const int i=0;
const int i;
i=0; // Error 不可修改

成員函式具備const屬性如何初始化

成員具備const屬性
class T {
    T(int num){
        this->i = num; // Error 不可修改
    }
    const int i;
};

使用初值建構(initializer)
class T {
    T(int num): i(num){}
    const int i;
};