2017年5月5日 星期五

如何降低編譯的依存關係

如何降低編譯的依存關係

tags: C++ Concept2

依存關係

依存關係指的是A class的實作必須在寫在B class之前,他們之間存在相依關係,A一定要寫在B之前;或A檔案一定要先被編譯,他們之間不能夠單純依靠宣告來通過編譯。
以下降低解說難易度將檔案縮減至一個單一檔案敘述
假如有一個自訂類別為 Name
// Name class 前置性宣告
class Name;
// Name class 定義 (不是成員函式定義)
class Name{
public:
    Name(string n);
    friend ostream& operator<<(ostream& out, Name const & n);
private:
    string n;
};
那麼要使用這個型別的時候,該class定義必須出現在使用之前
// Name class 宣告
// Name class 定義
Name a("chg");
如果你把class定義移動到下方則會出問題
(Name的函式定義可以移動到下方)
// Name class 宣告
Name a("chg");
// Name class 定義
則會出現相依的問題,產生Name的時候找不到定義;其他class如果要使用Name這個class,會造成Name一定要擺在那之上才可以編譯,如果要交錯互相使用麻煩就大了,誰在上都不對。


Handle Class

解決辦法,可以使用指針來占個位置不把它實作,這樣就不會需要他的實作了
只需要前置性宣告。
// Name class 宣告
Name* a;
// Name class 定義
應用在剛剛說的問題上代碼大概就長這個樣子
// Tw class定義
class Name;
class Tw{
public:
    Tw(string name);
private:
    Name* n;
};
// Name class定義
class Name{
public:
    Name(string n);
    friend ostream& operator<<(ostream& out, Name const & n);
private:
    string n;
};

// Tw建構子
Tw::Tw(string name): n(new Name(name)){
    cout << "n=" << *n << endl;
}
// Name 函式定義
Name::Name(string n): n(n){}
ostream& operator<<(ostream& out, Name const & n){
    out << n.n;
    return out;
}
如此一來順序的相依性就變得兩者皆可(函式的定義也是)
此外這裡的指針也可以使用沒有初始化的vector取代
vector<Name> n;
不要初始化內容
vector<Name> n("name");
也不要初始化長度
vector<Name> n(1);
反例中的代碼因為Name沒有預設建構子(引數可為無的建構子),僅能使用 push_back() 初始化
// Tw建構子
Tw::Tw(string name){
    n.push_back(name);
    cout << "n=" << n[0] << endl;
}


Protocol Class

利用虛擬函式的特性,不需要先做,藉此避開宣告時必須要要定義的狀況。
// Name class 宣告
// 純虛擬類別定義(抽象類別宣告內含Name)

// Name class 定義
// 繼承抽象類別


參考代碼

Handle Class
/*****************************************************************
Name : Handle Class
Date : 2017/05/04
By   : CharlotteHonG
Final: 2017/05/04
*****************************************************************/
#include <iostream>
#include <string>
#include <vector>
using namespace std;

// Tw class定義
class Name;
class Tw{
public:
    Tw(string name);
private:
    Name* n;
    // vector<Name> n;
};

//================================================================
int main(int argc, char const *argv[]){
    Tw a("chg");
    return 0;
}
//================================================================

// Name class定義
class Name{
public:
    Name(string n);
    friend ostream& operator<<(ostream& out, Name const & n);
private:
    string n;
};
// Tw建構子
Tw::Tw(string name){
    n = new Name(name);   // point
    // n.push_back(name); // vector
    cout << "n=" << n[0] << endl;
}
// Name 函式定義
Name::Name(string n): n(n){}
ostream& operator<<(ostream& out, Name const & n){
    out << n.n;
    return out;
}
Protocol Class
/*****************************************************************
Name :
Date : 2017/05/04
By   : CharlotteHonG
Final: 2017/05/05
*****************************************************************/
#include <iostream>
using namespace std;
// Tw class定義
class Name;
class Tw{
public:
    virtual ~Tw(){}
    virtual string name()=0;
    static Tw* makeTw(Name const & name);
};

// Name class定義
class Name{
public:
    Name(string const & n);
    operator string&(){
        return n;
    }
    friend ostream& operator<<(ostream& out, Name const & n);
private:
    string n;
};
//================================================================
int main(int argc, char const *argv[]){
    Tw* p;
    Name chg = string("chg");
    p = Tw::makeTw(chg);
    cout << p->name() << endl;
    return 0;
}
//================================================================

// Tw class定義
class RealTw: public Tw{
public:
    RealTw(Name const & name): n(name){}
    string name(){return n;}
private:
    Name n;
};

// makeTw 函式定義
Tw* Tw::makeTw(Name const & name){
    return new RealTw(name);
}

// Name 函式定義
Name::Name(string const & n): n(n){}
ostream& operator<<(ostream& out, Name const & n){
    out << n.n;
    return out;
}

operator 運算子的各式標準寫法與原因

operator 運算子的各式標準寫法與原因

tags: C++ Concept2
operator存在著許多應該遵守的公約,這裡會列出常見的operator如何撰寫標準公約,並說明為何要如此撰寫與舉例


宣告

/* 那些只有一行的函式可以直接上inline */

// +運算子
Arr & operator+=(Arr const &rhs);
friend Arr const operator+(Arr const &lhs, Arr const &rhs);
    return Arr(lhs) += rhs; // op+的定義

// 下標運算子
int & operator[](size_t idx){
    return const_cast<int&>(static_cast<const Arr&>(*this)[idx]);
}
const int & operator[](size_t idx) const;

// <<運算符號
friend ostream & operator<< (ostream& s, const Rati & r);

// 複製建構子
List(List const & rhs);
// 複製函式
List & operator=(List const & rhs);

// 移動建構子
List(List && rhs){
    (*this) = std::move(rhs);
}
// 移動函式
List & operator=(List && rhs);

// 取址
T* operator&();
// 取值(轉型)
operator T&();

// ++T
Arr & operator++();
// T++
Arr operator++(int);


公約

盡可能記住他,在大多數的可以幫你避開很多坑

參數

看一下以下的例子,這樣的寫法可以同時接收const與non-const
void fun(int const & num){
    cout << "num=" << num << endl;
}

int i=0;
fun(i);
  1. 避免寫兩個 fun() 函式
  2. 不小心修改到時編譯器會提醒你
參數能夠加上 const 就加上 const

返回的物件

假設一個沒有被加上const物件被返回可能會發生這種事情
Arr operator+(Arr const &lhs, Arr const &rhs);

Arr a, b;
a+a=b;
或許你不會這寫,但不能保證沒有人會這樣寫,嘗試對一個即將被解構的物件修改,其結果只是浪費效能。
返回的物件能夠加上 const 就加上 const

返回 (*this) 的參考

我們應該效仿基本的型別操作,int這些基礎型別是可以連續被指定的
int a, b, c, d;
a=b=c=d=1;
我們只要傳出 (*this) 的參考就可以做到這樣的操作,可以把他們拆解成原本的樣態,可以更容易理解
Arr a, b, c;
a=b=c;

a.operator=(b.operator=(c));

適當的使用全域函式

當我們使用 op+() 的時候要注意一種情況
Arr a;
a+1;
這把它還原看起來沒問題
a.operator+(1);
可是反過來就出問題
1.operator+(a);
這可以使用全域函式處理
Arr const operator+(int lhs, Arr const &rhs);
Arr const operator+(Arr const &lhs, int rhs);

適當的引用重複的函式

可以觀察到某些函式其實是具有重複性的
  • op+() 可以拆解為兩個已有的函式,複製後+=
  • 下標負號的 const 與 non-const
  • 移動建構子的內容與移動函式相等
  • 複製建構子與複製函式部分相同(額外拆一個函式呼叫)

拆解

一個相加的函式可以拆解為
int a=1, b=2;

int temp=a;
temp+=b;
下標符號比較特別需要特別為了 const 與 non-const 寫兩個完全一樣的函式
int & operator[](size_t idx);
const int & operator[](size_t idx) const;

Arr a;
const Arr b;

a[0]=1;
b[0];
解決方法長這個樣子
int & operator[](size_t idx){
    return const_cast<int&>(static_cast<const Arr&>(*this)[idx]);
}
簡單來說從右邊看回來
  1. 先加上const屬性
  2. 然後讓它去呼叫const的op[]()函式
  3. 再強制解除他的 const 屬性
順序不要反過來了把主代碼寫在 non-const (是可以正常運行的)
讓 const 去呼叫 non-const 的函式
這樣會導致函式運行的時候 const 屬性被解除可以被修改。
難保後面的人沒察覺到…(通常這個人是幾個月後的自己)

補上 friend 開放 private 的存取權限

某些函式寫在全域函式會比寫在成員函式還要來的好,可是一旦寫到全域函式去之後,相對的就會失去對private成員的存取權限
在類別的定義裡面補上該全域函式的 friend宣告,讓全域函式可以有應有的權限操作成員函式

待續

2017年5月1日 星期一

繼承的虛擬函式與純虛擬函式的實際作用

繼承的虛擬函式與純虛擬函式的實際作用

tags: C++ Concept2
一句話形容虛擬函式的意思
該函式可有可無
這在繼承的時候起了一個極大的用處,基層類別的函式可有可無的特性會讓編譯器自動選擇正確的函式,稍後會繼續說明。

需要遵守的原則
總是讓你的Base擁有虛擬解構子
否則一個向上轉型的指針會被錯誤的解構

一句話形容純虛擬函式
禁止使用者操作你的父類別
你不會希望使用者可以操作你的父類別的,它只是用來定義或抽出各個類別相同處。

繼承

以下均是討論在繼承的情況下會遇到的問題
繼承的參考代碼
class Base{
public:
    Base(){}
    ~Base(){
        cout << "Base dtor" << endl;
    }
    void fun(){
        cout << "Base" << endl;
    }
};

class Drived: public Base{
public:
    Drived(){}
    ~Drived(){
        cout << "Drived dtor" << endl;
    }
    void fun(){
        cout << "Drived" << endl;
    }
};

函式多載

如果一個子類別加載了與父類別相同名稱的函式,那麼那個函式將會被覆蓋,子類別宣告的物件依子類別函式為主。
Base ba;
Drived dr;
ba.fun();
dr.fun();
結果會個別呼叫父類別與子類別的 fun()函式,如果子類別沒有宣告該函式則會繼承父類別的函式。

向上轉型

使用父類別指針來宣告子類別非常常見;有些時候也有必要進行安全的向上轉型(子類別指針轉成父類別),這種情況下,會出現一個問題
這個子類別會呼叫父類別的函式
試著在上述的代碼加入
Base* p = new Drived;
p->fun();
結果將呼叫父類別的 fun()函式
這可能不是我們要的結果,將fun()設置成虛擬函式,可以讓編譯器選擇正確的函式運行。
virtual void fun(){
    cout << "Base" << endl;
}
現在可以正確地呼叫子類別的 fun()函式 了。

不正確的解構

上述情況呼叫錯誤的函式這聽起來可能沒什麼事情,但是如果呼叫錯解構子可就有問題了。
Base* p = new Drived;
delete p;
這是一個未定義行為,讓一個子類別呼叫父類別的解構子,其結果可能導致記憶體遺失。必須為你的父類別的解構子加上 vitual,才能呼叫正確的解構子

不想讓使用者使用父類別

繼承最重要的目的之一就是抽出各個函式相同之處,集中寫到同一個類別內,再分別讓大家既成。
可問題就在於集中到的那個類別如果被使用者拿去使用宣告會發生什麼事情?可能不會發生什麼,不過你不會希望他被使用者拿去操作,因為這一點實用性都沒有。

抽象類別的定義

抽象類別指的是其成員函式至少具有一個純虛擬函式,純虛擬函式的寫法是另一個虛擬函式=0。
virtual void fun()=0;
抽象類別不能夠實際產生出一個物件,僅能夠被繼承。基於這一點可以完美的阻止使用者操作Base類別。

找不到適當的函式當作純虛擬函式?

還記得前面提到的總是讓你的Base擁有虛擬解構子嗎?我們可以從這裡下手,反正他都已經必須是了,並在代碼之後補上其定義,不讓使用者定義,可以完整的解決這個問題。
virtual ~Base()=0;
Base::~Base(){
    cout << "Base dtor" << endl;
}
現在使用者構不著你的Base類別了,並且你也不用為了純虛擬函式煩惱要怎麼生出一個新函式,對使用者而言也不需要自行補上純虛擬函式的定義。

範例代碼

/*****************************************************************
Name : 
Date : 2017/04/30
By   : CharlotteHonG
Final: 2017/04/30
*****************************************************************/
#include <iostream>
using namespace std;

class Base{
public:
    Base(){}
    virtual ~Base()=0;
    virtual void fun(){
        cout << "Base" << endl;
    }
};
Base::~Base(){
    cout << "Base dtor" << endl;
}

class Drived: public Base{
public:
    Drived(){}
    ~Drived(){
        cout << "Drived dtor" << endl;
    }
    void fun(){
        cout << "Drived" << endl;
    }
};
//================================================================
int main(int argc, char const *argv[]){
    Base* p = new Drived;
    delete p;
    return 0;
}
//================================================================