2014年7月25日 星期五

Weak References

Strong references
Strong reference是最常見的Java reference,舉例來說:
StringBuffer buffer = new StringBuffer();

  - 產生一個StringBuffer物件,並將一個strong reference存到buffer變數中。
  - strong指的是它和garbage collector的關係。
  - 如果一個物件是reachable via a chain of strong references,它就不適合被garbage collect。


When strong references are too strong
    當我們使用image cache時,若搭配strong reference,那麼這些image會一直保存在memory裡面。你必須自己決定何時不需要某個image並將它從cache中移除,但這樣的工作適合由garbage collection來處理的。


Weak references
    weak reference簡單地說就是一個不夠強的reference,無法強迫一個物件一直存留在memory中。它可以幫助garbage collector決定物件的reachability,所以你就不用自己做。以下為一個例子:

WeakReference<Widget> weakWidget = new WeakReference<Widget>(widget);

  - 可以使用weakWidget.get( )來取得實際的Widget物件。
  - 當這個widget物件沒有strong reference指向它時,可能會被garbage collection,此時weakWidget.get( )會return null。


  - 當你想追蹤Widget物件的序號,但Widget class中並沒有這個項目,而且也不允許extend它,此時可以用HashMap來處理:
serialNumberMap.put(widget, widgetSerialNumber);
  - 但如果當序號沒用的時候沒去移除entry,在沒有garbage collector的語言中會有memory leak。
  - 相同的我們也必須自己去管理這個HashMap何時該移除entry。

  - 可以改用WeakHashMap,它跟HashMap差在會把key (不是value)作為weak reference。
  -  當它的key變成garbage,則其value會自動被移除。


Reference queues
  - 一旦WeakReference開始return null,代表它所指到的物件已經變成garbage,也代表這個WeakReference不再被需要,所以此時可以做清理的動作。
  - 以WeakHashMap為例,要把已經沒用的entries移除。

  - ReferenceQueue class可便於追蹤這些dead references。
  - 若你把一個ReferenceQueue放到WeakReference的constructor,當reference所指的物件變成garbage了,則這個reference物件會自動插入reference queue中。你就可以每隔一段時間對這個ReferenceQueue做清理的動作。


Soft references
  - 一個物件只有在weakly reachable(最強的reference是WeakReference)才會在下一次的garbage collection cycle被丟棄。但softly reachable的物件還是會保留一段時間。
  - 實際上softly reachable的物件,只要memory足夠就會一直保留著。
  - 這對於製作cache是最好用的(例如image cache)。


Phantom references
  - 其get( ) method永遠return null
  - 它的用途只有在追蹤什麼時候被放入ReferenceQueue,也就是知道何時所指的object死了。

  - 它和WeakReference的差別在於enqueue的時機不同。
  - WeakReference是當object變成weakly reachable時去做enqueue,這個時間點是在finalization或garbage collection實際發生之前。理論上物件是可以藉由非正統的finalize() method來復活,但WeakReference仍然是死的。
  - PhantomReference只有在物件真正從memory中被移除時才會被enqueue。而get( ) method之所以永遠return null是為了避免你將物件復活。

  - PhantomReference的兩大用途:
    1. 得知物件何時真正從object中移除
        - 例如等到它真的發生,再去load下一張圖,確保OutOfMemory不會發生
    2. 避免在finalize( ) method中產生新的strong reference以復活物件的問題
        - 問題在於Override finalize( )的物件要在至少兩個garbage collection cycle才會被當作garbage。
        - finalization有可能不會及時執行,可能在等待finalization的過程中又經過了數個cycle,這代表實際上去清除garbage物件會有delay發生。這也是為什麼有時候,大部分的heap都是garbage但仍然發生OutOfMemory的原因。

強度:
Strong => Soft => Weak => Phantom


2014年7月21日 星期一

不用ValueAnimator改用TimerTask

問題:
有一個動畫,其中需要針對一個值的改變而畫,那個值會在650ms中照時間比例從0變到1,中間的值是float,作法如下。

mAnimator = ValueAnimator.ofFloat(0, 1);
mAnimator.setDuration(650);
mAnimator.addUpdateListener(...); // 每個animation的frame更新時都會callback
mAnimator.addListener(...); // animation的開始/結束/cancel/repeat
mAnimator.start();

但後來發現這個動畫會有卡頓的問題,並沒辦法均勻地變化比例。


解法:
所以我們改用TimerTask來做動畫,解決這個問題。

private Timer mTimer = new Timer(true); private MyTimerTask mTimerTask = new MyTimerTask(); public class MyTimerTask extends TimerTask{ @Override public void run() { if(mActivity != null){ mActivity.runOnUiThread(new Runnable(){ @Override public void run() { ..... } }); } } };

接著在要進行動畫的地方呼叫:
    mTimer.scheduleAtFixedRate(mTimerTask,...);

2014年7月18日 星期五

純虛擬函式、抽象類別(Abstract class)

C++預設函式成員都不是虛擬函式,如果要將某個函式成員宣告為虛擬函式,則要加上"virtual"關鍵字,然而C++提供一種語法定義「純虛擬函式」 (Pure virtual function),指明某個函式只是提供一個介面,要求繼承的子類別必須重新定義該函式,定義純虛擬函式除了使用關鍵字"virtual"之外,要在函 式定義之後緊跟著'='並加上一個0,例如:
class Some {
public:
    // 純虛擬函式
    virtual void someFunction() = 0;

    ....
};

一個類別中如果含有純虛擬函式,則該類別為一「抽象類別」(Abstract class),該類別只能被繼承,而不能用來直接生成實例,如果試圖使用一個抽象類別來生成實例,則會發生編譯錯誤。

以下舉個實際的例子,先假設您設計了兩個類別:ConcreteCircle與HollowCircle:

class ConcreteCircle {
public:
    void radius(double radius) {
         _radius = radius;
    }
    double radius() {
        return _radius;
    }
    void render() {
         cout << "畫一個半徑 "
              << _radius
              << " 的實心圓"
              << endl;
    }
private:
    double _radius;
};

class HollowCircle {
public:
    void radius(double radius) {
         _radius = radius;
    }
    double radius() {
        return _radius;
    }
    void render() {
         cout << "畫一個半徑 " 
              << _radius 
              << " 的空心圓"
              << endl;
    }
private:
    double _radius;
};

顯然的,這兩個類別除了render()方法的實作內容不同之外,其它的定義是一樣的,而且這兩個類別所定義的顯然都是「圓」的一種類型,您可以定義一個 抽象的AbstractCircle類別,將ConcreteCircle與HollowCircle中相同的行為與定義提取(Pull up)至抽象類別中:

  • AbstractCircle.h
#ifndef ABSTRACTCIRCLE
#define ABSTRACTCIRCLE

class AbstractCircle {
public:
    void radius(double radius) {
        _radius = radius;
    }
    double radius() {
        return _radius;
    }
    // 宣告虛擬函式
    virtual void render() = 0;
 
protected:
    double _radius;
};

#endif

注意到在類別宣告了虛擬函式render(),所以AbstractCircle是個抽象類別,它只能被繼承,繼承了AbstractCircle的類別 必須實作render()函式,接著您可以讓ConcreteCircle與HollowCircle類別繼承AbstractCircle方法並實作 render()函式:

  • HollowCircle.h
#include <iostream> 
#include "AbstractCircle.h"
using namespace std; 

class HollowCircle : public AbstractCircle {
public:
    void render() {
        cout << "畫一個半徑 " 
             << _radius 
             << " 的空心圓"
             << endl;
    }
};
  • ConcreteCircle.h
#include <iostream> 
#include "AbstractCircle.h"
using namespace std; 

class ConcreteCircle : public AbstractCircle {
public:
    void render() {
        cout << "畫一個半徑 " 
             << _radius 
             << " 的實心圓"
             << endl;
    }
};

由於共同的定義被提取至AbstractCircle類別中,並於衍生類別中繼承了下來,所以在ConcreteCircle與HollowCircle 中無需重覆定義,只要定義個別對render()的處理方式就行了,而由於ConcreteCircle與HollowCircle都是 AbstractCircle的子類別,因而可以使用AbstractCircle上所定義的虛擬操作介面,來操作子類別實例上的方法,如下所示:

  • main.cpp
#include <iostream> 
#include "AbstractCircle.h"
#include "ConcreteCircle.h"
#include "HollowCircle.h"
using namespace std; 

void render(AbstractCircle &circle) {
    circle.render();
}

int main() {
    ConcreteCircle concrete;
    concrete.radius(10.0);
    render(concrete);
 
    HollowCircle hollow;
    hollow.radius(20.0);
    render(hollow);
 
    return 0;
}

執行結果:

畫一個半徑 10 的實心圓
畫一個半徑 20 的空心圓


Gossip: 虛擬函式(Virtual function)

之前曾經介紹過函式與運算子的重載(Overload),重載可以使用一個函式名稱來執行不同的實作,這是一種「編譯時期」就需決定的方式,這是「早期繫 結」(Early binding)、「靜態繫結」(Static binding),因為在編譯時就可以決定函式的呼叫對象,它們的呼叫位址在編譯時就可以得知。

「虛擬函式」(Virtual function)可以實現「執行時期」的多型支援,是一個「晚期繫結」(Late binding)、「動態繫結」(Dynamic binding),也就是指必須在執行時期才會得知所要調用的物件或其上的公開介面。

在談虛擬函式之前必須先知道,一個基底類別的物件指標,可以用來指向其衍生類別物件而不會發生錯誤,例如若基底類別是Foo1,而衍生類別是Foo2,則 下面這個指定是可以接受的: 

Foo1 *fptr; 
Foo2 f2; 
fptr = &f2;

多型與動態繫結的基礎從這開始,它們只有在使用指標或參考時才得以發揮它們的特性,然而由於fptr仍是Foo1類型的指標,它只能存取Foo1中有定義 的成員,目前來說也只能操作Foo1中的成員。

注意將衍生類別型態的指標指向基底類別的物件基本是不可行的(雖然可以使用型態轉換的方式來勉強達成,但並不鼓勵),衍生類別的指標並不能存取基底類別的 成員。

虛擬函式是一種成員函式,它在基底類別中使用關鍵字"virtual"宣告(定義),並在衍生類別中重新定義虛擬函式,這將成員函式的操作決議 (Resolution)推遲至執行時期再決定。

虛擬函式可以實現執行時期的「多型」,也就是「一個介面,多種函式」,一個含有虛擬函式的類別被稱為「多型的類別」(Polymorphic class),當一個基底類別型態的指標指向一個含有虛擬函式的衍生類別,您就可以使用這個指標來存取衍生類別中的虛擬函式,下面這個例子是個簡單的示 範:
 

#include <iostream> 
using namespace std; 

class Foo1 { 
public: 
    virtual void show() { // 虛擬函式 
        cout << "Foo1's show" << endl; 
    } 
}; 

class Foo2 : public Foo1 { 
public: 
    virtual void show() { // 虛擬函式 
        cout << "Foo2's show" << endl; 
    } 
}; 

void showFooByPtr(Foo1 *foo) {
    foo->show();
}

void showFooByRef(Foo1 &foo) {
    foo.show();
}

int main() { 
    Foo1 f1; 
    Foo2 f2; 

    // 動態繫結 
    showFooByPtr(&f1); 
    showFooByPtr(&f2);
    cout << endl;
 
    // 動態繫結 
    showFooByRef(f1); 
    showFooByRef(f2);
    cout << endl; 

    // 靜態繫結 
    f1.show(); 
    f2.show(); 

    return 0;
}

執行結果:

Foo1's show
Foo2's show

Foo1's show
Foo2's show

Foo1's show
Foo2's show

showFooByPtr()與showFooByRef()函式並無法事先知道要操作的是哪一個物件的哪一個公開介面,最後的操作要在執行時期才能決 定。

衍生類別中重新定義虛擬函式時,virtual可以根據需求加上,如果再接下來的衍生類別仍想進行多型操作,則加上virtual,如果不打算進行多型操 作,則可以不加上。

保護(protected)繼承、私用(private)繼承

在繼承時採公開(public)繼承的方式來繼承一個類別時,父類別與子類別為"is-a"的 關係,子類別繼承父類別的公開(public)介面及受保護(protected)的成員,子類別是父類別的細化型態。

保護(protected)繼承可以改變繼承下來的基底類別成員權限,保護的意思就是讓這些成員繼承下來之後,保護它們僅能在類別與衍生類別中使用,保護 繼承的語法如下所示: 

class B : protected A { 
    // 實作 
};

保護繼承時使用protected來繼承基底類別,繼承下來的成員在衍生類別中的權限變為如下:

基 底類別衍 生類別
private不繼承
protectedprotected
publicprotected

簡單的說,原來的權限在protected以下的保留其原來權限,而在protected以上的就降為protected,子類別protected繼承 的目的在只希望保留父類別中已實作的公開成員與受保護的成員為己用或接下來的衍生類別使用,並提供自己的公開介面。

您也可以在繼承基底類別之後,將它所有的成員一律改為私用(private),使用私用(private)繼承可以達到這個目的,其語法如下: 

class B : private A { 
    // 實作 
}; 


基底類別中的成員在被繼承之後,其權限如下所示: 

基 底類別衍 生類別
private不繼承
protectedprivate
publicprivate

private繼承被稱為「實作繼承」,意味著子類別只想保留父類別中已實作的公開與受保護的成員為己用,並提供自己的公開介面與接下來會被繼承的受保護 的成員。

使用者自訂型態轉換(User-Defined Conversions)

在 使用 friend 函式重載運算子 中,您使用friend函式解決基本型態與自訂型態相加、相減等運算的問題,也就是像10 + someObject、10 - someObject這類的運算可透過friend函式來重載相對應的運算子。

然而想想在更多的運算需求中,您可能對+、-、*、/、%等等的運算子都想有基本型態與自訂型態運算的需求,為每一個需求都重載相對應的運算子似乎是不合 效率的,內建的型態轉換行為在 算術 (Arithmetic)運算、型態轉換(Type conversion) 中有介紹過,所以您該提供的是一個自動型態轉換,在需要的時候,編譯器會根據您的自訂型 態轉換,自動將您的自訂型態轉換為基本型態或您指定的型態。

 直接以實例說明如何自訂型態轉換:
 
  • Integer.h
class Integer {
public:
    Integer(int value) : _value(value) {
    }
 
    int value() {
        return _value;
    }
 
    // 自訂型態轉換
    // 當需要將Integer轉換為int時如何執行 
    operator int() {
        return _value;
    }
 
    int compareTo(Integer);
 
private:
    int _value;
};

  • Integer.cpp
#include "Integer.h"

int Integer::compareTo(Integer integer) {
    if(_value > integer._value) {
        return 1;
    }
    else if(_value < integer._value) {
        return -1;
    }
 
    return 0;
}

Integer類別將int基本型態包裝為物件,以提供更多物件導向上的操作行為,例如提供compareTo()函式支援兩個Integer實例的比 較,而為了支援與int基本型態的直接算術行為,您使用operator int()定義了如何轉換,當編譯器需要作int型態轉換時,就會使用您自定義的行為,例如:
  • main.cpp
#include <iostream> 
#include "Integer.h"
using namespace std; 

int main() {
    Integer i1(10);
    Integer i2(20);
 
    cout << i1.compareTo(i2) << endl;
 
    cout << i1 + 10 << endl;
 
    return 0;
}

執行結果:
-1
20


operator後緊跟著的即是轉換的目標型態,自定義型態轉換不只可以轉換至基本型態,您也可以指定轉換為自訂義型態,例如:
class Some {
public:
    ....
    operator Other() {
        ....
    }

};

要注意的是轉換函式不能有參數列。



使用 friend 函式重載運算子

使用類別成員來超載二元運算子時,會有一個限制,就是運算子的左邊一定要是原物件,您可以使用類別成員重載運算子具有以下的功能: 
Point2D p1(10, 10);
Point2D p2; 
p2 = p1 + 10;

但是使用類別成員重載,您就無法使用這個方法讓運算子重載有以下的功能: 

Point2D p1(10, 10);
Point2D p2; 
p2 = 1 + p1;

您可以規避這個問題,但每次都要讓其它型態的運算元置於運算子右邊也是蠻麻煩的,而且有時會容易出錯,這時您可以使用friend函式來重載運算子,使用 friend函式重載二元運算子時,您要指定兩個參數型態,分別表式運算子左右邊的運算元型態,您可以藉由安排這兩個參數來解決以上的問題,例如先如下定 義Point2D.h:

class Point2D { 
public: 
    ....
    friend Point2D operator+(const Point2D&, int); // 例如p+10 
    friend Point2D operator+(int, const Point2D&); // 例如10+p 
    
private:
    int _x;
    int _y;        
}; 

再實作Ball.cpp:

#include "Point2D.h"
....
Point2D operator+(const Point2D &p, int i) {
    Point2D tmp(p._x + i, p._y + i);

    return tmp;
}

Point2D operator+(int i, const Point2D &p) {
    Point2D tmp(i + p._x, i + p._y);

    return tmp;
}

接著您就可以直接如下進行運算:

Point2D p1(5, 5);
Point2D p2; 
p2 = 10 + p1;

您也可以使用friend函式來重載++或--這類的一元運算子,但要注意的是,friend不會有this指標,所以為了讓它具有++或--的遞增遞減 原意,您要使用傳參考的方式,將物件的位址告訴函式,例如: 

class Point2D { 
public: 
    ....
    friend Point2D operator++(Point2D&);  // 前置 
    friend Point2D operator++(Point2D&, int); // 後置 
    
private:
    int _x;
    int _y;        
}; 

實作時如下:

#include "Point2D.h"
....
Point2D operator++(Point2D &p) { 
    p._x++; 
    p._y++; 
  
    return p; 


Point2D operator++(Point2D &p, int) { 
    Point2D tmp(p._x, p._y); 

    p._x++; 
    p._y++; 

    return tmp; 

物件的複製

當您宣告一個物件時,您可以使用另一個物件將之初始化,例如:
SomeClass s1;
SomeClass s2 = s1;

這麼作的話,s1的屬性會一一的「複製」至s2的每一個屬性,下面這個程式是個簡單的示範,您進行物件的指定,而最後用&運算子取出物件 的記憶體 位址,您可以看到兩個物件所佔的位址並不相同: 


#include <iostream>
using namespace std;

class Employee { 
public: 
    Employee() {
        _num = 0; 
        _years = 0.0;
    } 
 
    Employee(int num, double years) { 
        _num = num; 
        _years = years; 
    }
 
    int num() {
        return _num;
    }
 
    double years() {
        return _years;
    }
 
private:
    int _num;
    double _years; 
}; 

int main() {
    Employee p1(101, 3.5); 
    Employee p2 = p1; 

    cout << "p1 addr:\t" << &p1 << endl;
    cout << "p1.num: \t" << p1.num() << endl;
    cout << "p1.years:\t" << p1.years() << endl;
 
    cout << "p2 addr:\t" << &p2 << endl; 
    cout << "p2.num: \t" << p2.num() << endl;
    cout << "p2.years:\t" << p2.years() << endl;
 
    return 0;
}

執行結果:

p1 addr:        0x22ff60
p1.num:         101
p1.years:       3.5
p2 addr:        0x22ff50
p2.num:         101
p2.years:       3.5

然而這中間潛藏著一個危機,尤其是在屬性成員包括指標時,以 建 構函式、 解構函式 中的SafeArray類別來說,看看下面的程式問題會出在哪邊: 
SafeArray arr1(10);
SafeArray arr2 = arr1;

表面上看起來沒有問題,但記得_array是int型態指標,而在解構函式是這麼寫的:

SafeArray::~SafeArray() {
    delete [] _array;
}

arr2複製了arr1的屬性,當然也包括了_array指標,如果arr1資源先被回收了,但arr2的_array仍然參考至一個已被回收資源的位 址,這時再存取該位址的資料就有危險,例如下面這段程式就可能造成程式不可預期的結果:

SafeArray *arr1 = new SafeArray(10);
SafeArray arr2 = *arr1;
delete arr1;

為了避免這樣的錯誤,您可以定義一個複製建構函式,當初始化時如果有提供複製建構函式,則會使用您所定義的複製建構函式,您可以在定義複製建構函式時,當 遇到指標成員時,產生一個新的資源並指定位址給該成員,例如:

  • SafeArray.h
class SafeArray { 
public: 
    int length; 

    // 複製建構函式 
    SafeArray(const SafeArray&);
    // 建構函式 
    SafeArray(int); 
    // 解構函式 
    ~SafeArray();
 
    int get(int); 
    void set(int, int); 

private:
    int *_array; 

    bool isSafe(int i); 
};

  • SafeArray.cpp
#include "SafeArray.h"

// 複製建構函式 
SafeArray::SafeArray(const SafeArray &safeArray) 
                                 : length(safeArray.length) {
    _array = new int[safeArray.length];
 
    for(int i = 0; i < safeArray.length; i++) {
        _array[i] = safeArray._array[i];
    }
}

// 動態配置陣列
SafeArray::SafeArray(int len) {
    length = len;
    _array = new int[length];
}

// 測試是否超出陣列長度
bool SafeArray::isSafe(int i) {
    if(i > length || i < 0) {
        return false;
    } 
    else {
        return true;
    }
}

// 取得陣列元素值
int SafeArray::get(int i) {
    if(isSafe(i)) {
        return _array[i];
    }
 
    return 0;
}

// 設定陣列元素值
void SafeArray::set(int i, int value) {
    if(isSafe(i)) {
        _array[i] = value;
    }
}

// 刪除動態配置的資源
SafeArray::~SafeArray() {
    delete [] _array;
}

如果屬性成員中有指標型態,除了為物件始化撰寫複製建構函式之外,最好再重載=指定運算子,因為=指定運算子預設也是將物件的屬性值一一複製過去,您應該 重載=指定運算子,在遇到指標成員時,產生位址上的資源複本,例如:
  • SafeArray.h
class SafeArray { 
public: 
    int length; 
 
    // 複製建構函式 
    SafeArray(const SafeArray&);
    // 建構函式 
    SafeArray(int); 
    // 解構函式 
    ~SafeArray();
 
    int get(int); 
    void set(int, int);
 
    // 重載=運算子 
    SafeArray& operator=(const SafeArray&);

private:
    int *_array; 

    bool isSafe(int i); 
};

  • SafeArray.cpp
#include "SafeArray.h"

// 複製建構函式 
SafeArray::SafeArray(const SafeArray &safeArray) 
                         : length(safeArray.length) {
    _array = new int[safeArray.length];
 
    for(int i = 0; i < safeArray.length; i++) {
        _array[i] = safeArray._array[i];
    }
}

// 重載=指定運算子 
SafeArray& SafeArray::operator=(const SafeArray &safeArray) {
    if(this != &safeArray) {
        length = safeArray.length;

        // 先清除原有的資源 
        delete [] _array;
 
        _array = new int[safeArray.length];
        for(int i = 0; i < safeArray.length; i++) {
            _array[i] = safeArray._array[i];
        } 
    }
 
    return *this;
}

// 動態配置陣列
SafeArray::SafeArray(int len) {
    length = len;
    _array = new int[length];
}

// 測試是否超出陣列長度
bool SafeArray::isSafe(int i) {
    if(i > length || i < 0) {
        return false;
    } 
    else {
        return true;
    }
}

// 取得陣列元素值
int SafeArray::get(int i) {
    if(isSafe(i)) {
        return _array[i];
    }
 
    return 0;
}

// 設定陣列元素值
void SafeArray::set(int i, int value) {
    if(isSafe(i)) {
        _array[i] = value;
    }
}

// 刪除動態配置的資源
SafeArray::~SafeArray() {
    delete [] _array;
}


重載運算子

在C++中,預設除了基本資料型態可以使用運算子進行運算,例如int、double、char等,如果您要將兩個物件相加,預設上是不可行的。

然而很多情況下,您會想要將兩個物件的某些屬性值相加,並傳回運算後的結果,例如座標相加,如果您定義了Point2D類別,當中有x與y兩個屬性成員, 您會想要透過+或-運算子的動作得到座標相加或相減的動作,或是透過++與--來達到遞增或遞減的運算,在C++中,這可以透過重載運算子來達到目的。

運算子的重載其實是函式重載的一個延伸應用,您指定要重載哪一個運算子,並在類別中定義運算子如何動作,運算子重載的語法宣告如下所示: 

傳 回值 類別名稱::operator#(參數列) { 
    // 實作重載內容 
}

其中#中需指明您要重載哪一個運算子,例如重載一個+運算子,#處就替換為+運算子。

如果要重載++或--運算子,必須要注意到前置與後置的問題,例如一個變數x,您知道++前置時(++x)與++後置時(x++)實際上意義並不相同,在 重載時為了要區別前置與後置,C++中使用一個int參數來作區別:

傳 回型態 operator++();  // 前置,例如++x
傳 回型態 operator++(int); // 後置,例如x++
傳 回型態 operator--();  // 前置 ,例如 --x
傳 回型態 operator--(int); // 後置,例如 x--

在後置中會傳入一個0,但實質上沒有作用,只是作為識別前置與後置之用,通常在重載++與--運算子時,前置與後置都要重載;下面這個範例告訴您如何重載 +與-運算子,以及++與--運算子,以完成上面所提及的座標相加、相減、遞增、遞減的運算: 

  • Point2D.h
class Point2D { 
public: 
    Point2D();
    Point2D(int, int);
    int x() {return _x;} 
    int y() {return _y;} 
    Point2D operator+(const Point2D&); // 重載+運算子 
    Point2D operator-(const Point2D&); // 重載-運算子 
    Point2D& operator++(); // 重載++前置,例如 ++p 
    Point2D operator++(int); // 重載++後置,例如 p++
    Point2D& operator--(); // 重載--前置,例如 --p 
    Point2D operator--(int); // 重載--後置,例如 p--
 
private:
    int _x;
    int _y; 
}; 

  • Point2D.cpp
#include "Point2D.h"

Point2D::Point2D() {
    _x = 0;
    _y = 0;
}

Point2D::Point2D(int x, int y) {
    _x = x;
    _y = y;
}

Point2D Point2D::operator+(const Point2D &p) { 
    Point2D tmp(_x + p._x, _y + p._y); 
    return tmp; 
} 

Point2D Point2D::operator-(const Point2D &p) { 
    Point2D tmp(_x - p._x, _y - p._y); 
    return tmp; 
} 

Point2D& Point2D::operator++() { 
    _x++; 
    _y++; 

    return *this; 
} 

Point2D Point2D::operator++(int) { 
    Point2D tmp(_x, _y); 
    _x++; 
    _y++; 

    return tmp; 
} 

Point2D& Point2D::operator--() { 
    _x--; 
    _y--; 

    return *this; 
} 

Point2D Point2D::operator--(int) { 
    Point2D tmp(_x, _y); 
    _x--; 
    _y--; 

    return tmp; 
} 
  • main.cpp
#include <iostream>
#include "Point2D.h"
using namespace std;

int main() {
    Point2D p1(5, 5);
    Point2D p2(10, 10);
    Point2D p3; 

    p3 = p1 + p2; 
    cout << "p3(x, y) = (" 
         << p3.x() << ", " << p3.y() 
         << ")" << endl; 

    p3 = p2 - p1; 
    cout << "p3(x, y) = (" 
         << p3.x() << ", " << p3.y() 
         << ")" << endl;
 
    p3 = ++p1;
    cout << "p3(x, y) = (" 
         << p3.x() << ", " << p3.y() 
         << ")" << endl; 

    return 0;
}

執行結果:

p3(x, y) = (15, 15)
p3(x, y) = (5, 5)
p3(x, y) = (6, 6)

在重載+與-號運算子時,所接收的物件引數來自被重載的運算子右邊,例如在程式碼中加法運算時,+右邊是p2,所以傳入的物件引數就是p2物件,減法運算 時-號右邊是p1,所以傳入的就是p1物件,在傳入引數時,您使用傳參考的方式進行,這可以省去物件複製的動作,您也可以不使用傳參考,這對這個程式並不 造 成結果的差異,但使用傳參考方式可以節省CPU在複製物件時的處理時間。

大部份的運算子都是可以被重載的,除了以下的運算子之外: 

.   ::   .*   ?:


區域類別(Local classes)

類別也可以定義於函式之中,稱之為區域類別(Local classes),定義於函式中的區域類別只在該函式範圍(Scope)中有作用,也因此區域類別的宣告與定義都必須在該函式中完成。

區域類別中的成員通常宣告為"public",作用常是為了方便資料的群組管理,區域類別的定義常是很簡單的,例如:
 
#include <iostream>
using namespace std;


int main() {
    // 區域類別
    class Point {
    public:
        int x;
        int y;
        Point(int x, int y) {
            this->x = x;
            this->y = y;
        }
    };
 
    Point p(10, 10);

    cout << "(x, y) = (" 
         << p.x << ", " 
         << p.y << ")" 
         << endl;
 
    return 0;
}

執行結果:
(x, y) = (10, 10)

區域類別不可以直接存取所在函式中的變數,可以存取外圍類別的私用成員。


巢狀類別(Nested Classes)

1.
類別可以定義在另一個類別之中,這樣的類別稱之為巢狀類別或內部類別,內部類別只被外部包裹的類別所見,當某個Slave類別完全只服務於一個 Master類別時,您可以將之設定為內部類別,如此使用Master類別的人就不用知道Slave的存在。

一個巢狀類別通常宣告在"private"區域,也可以宣告在"protected"或"public"區域,一個宣告的例子如下:

class OuterClass {
private:
        class InnerClass {
            //  ....
        };
};



2.
在巢狀類別結構中,外部類別不能存取內部類別的私用成員,如果想要存取內部類別的私用成員的話,必須宣告外部類別為friend,例如:
class PointDemo {
    ...

private:    // Nested Class
    class Point {
        
friend class PointDemo;
        ....
    };
    ....
};

同樣的,內部類別不可存取外部類別的私用成員,如果要存取私用成員的話,必須宣告其為friend,例如:

class PointDemo {
public:
    ...
    friend class Point;

private:
    // Nested Class
    class Point {
        ....
    };
    ....
};

friend 函式、friend 類別

在定義類別成員時,私用成員只能被同一個類別定義的成員存取,不可以直接由外界進行存取,然而有些時候,您希望提供私用成員給某些外部函式來存取,這時您 可以設定類別的「好友」,只有好友才可以直接存取自家的私用成員。 

下面這個程式中使用friend關鍵字來設定類別的好友函式,該好友可以直接存取該類別的私用成員: 

  • Ball.h
class Ball;

int compare(Ball&, Ball&);

class Ball { 
public: 
    Ball(double, char*); 
 
    double radius() {
        return _radius;
    }
 
    char* name() {
        return _name; 
    }
 
    void radius(double radius) {
        _radius = radius;
    } 
 
    void name(char *name) {
        _name = name;
    }
 
    // 宣告朋友函式 
    friend int compare(Ball&, Ball&);
 
private:
    double _radius; // 半徑 
    char *_name; // 名稱 
};

  • Ball.cpp
#include "Ball.h"

// compare 為 Ball 的 friend 
int compare(Ball &b1, Ball &b2) {
    // 可直接存取私用成員
    if(b1._radius == b2._radius)
        return 0;
    else if(b1._radius > b2._radius)
        return 1;
    else
        return -1;
}

Ball::Ball(double radius, char *name) { 
    _radius = radius; 
    _name = name;
}

  • main.cpp
#include <iostream>
#include "Ball.h"
using namespace std;

int main() {
    Ball b1(10, "RBall");
    Ball b2(20, "GBall");
 
    switch(compare(b1, b2)) {
        case 1:
            cout << b1.name() << " 較大" << endl;
            break;
        case 0:
            cout << b1.name() << " 等於 " << b2.name() << endl;
            break;
        case -1:
            cout << b2.name() << " 較大" << endl;
            break;
    }
 
    return 0;
}

執行結果:
GBall 較大

使用friend函式通常是基於效率的考量,以直接存取私用成員而不透過函式呼叫的方式,來省去函式呼叫的負擔,另外您也可以使用friend來重載 (Overload)運算子,之後的主題中會介紹。

您也可以將某個類別宣告為friend類別,被宣告為friend的類別可以直接存取私用成員,例如:

class Ball;

int compare(Ball&, Ball&);

class Ball { 
public: 
    ....
    
    // 宣告朋友類別
    friend class SomeClass;
    
private:
    ....
};

如上宣告的話,SomeClass的實例就可以存取Ball實例的私用成員。




const 與 mutable

假設您設計了一個Ball類別:
  • Ball.h
#include <string>
using namespace std;

class Ball { 
public: 
    Ball(); 
    Ball(double, const char*); 
    Ball(double, string&); 
 
    double radius() {
        return _radius;
    }
 
    string& name() {
        return _name; 
    }
 
    void radius(double radius) {
        _radius = radius;
    } 
 
    void name(const char *name) {
        _name = name;
    }
 
    void name(string& name) {
        _name = name;
    }
 
    double volumn() {
        return (4 / 3 * 3.14159 * _radius * _radius * _radius); 
    }
 
private:
    double _radius; // 半徑 
    string _name; // 名稱 
};

假設您現在設計了一個somefun()函式:
void somefun(const Ball &ball) {
     ball.radius();
}

在函式的參數列上,您使用const宣告了ball參數,在編譯時會出現以下的訊息:

passing `const Ball' as `this' argument of `double Ball::radius()' discards qualifiers 

由於參數列上ball使用了const來宣告,這表示您不可以更動ball實例的狀態,也就是不得(在呼叫函式時)更動ball的資料成員,為了讓編譯器 得知這項訊息,您要在所呼叫的函式上加上const,例如:

void radius() const {
    return _radius;
}

編譯器會檢查每個被標示為const的成員函式,看看當中的陳述有無更動物件的資料成員。

另一方面還有一個問題,假設您在somefun()函式中如下呼叫:

void somefun(const Ball &ball) {
     ball.name();
}

則編譯時會出現以下的錯誤訊息:

`const Ball' as `this' argument of `std::string& Ball::name()' discards qualifiers 

即使您在name()函式後加上const,編譯時照樣無法通過,原因在於name()是以傳參考的方式傳回,而不是以傳值的方式傳回,由於以傳參考的方 式傳回,接受傳回值的物件可以直接更改傳回值,因而影響被呼叫物件的狀態,為了保證傳回值不被修改,您要在傳回值宣告上加上const,也就是:

const string& name() const {
    return _name; 
}

有時候您會希望大部份成員在const成員函式中不被更改,但少數幾個資料成員允許它們在const成員函式中被更動,因為這 些資料成員被 變動並不被視為改變了物件的狀態,這時候您可以在該資料成員宣告時加上mutable,表示對該成員的變動並不代表對物件狀態的改變,例如: 

class SomeClass { 
public: 
    ....
    double increment() const { 
        return _index++; 
     } 

private:
    .... 
    mutable double _index; 
};


auto_ptr 自動管理配置資源

對於使用new動態配置的資源,在不使用時必須記得delete,以釋放記憶體空間,然而動態記憶體配置很容易發生忘了delete,或是對同一個記憶體 位址delete兩次(例如一個物件被指定給兩個指標),或是對一個已經被delete的位址再作讀寫動作。

C++標準函式庫中提供auto_ptr,可以協助您動態管理new而建立的物件,要使用auto_ptr,您要含入memory表頭檔,例如:

#include <memory>

auto_ptr可以指向一個以new建立的物件,當auto_ptr的生命週期結束後,所指向的物件之資源也會被釋放,在建立auto_ptr時必須指 定目標物件之型態,例如:

auto_ptr<int> iPtr (new int(100));
auto_ptr<string> sPtr (new string("caterpillar"));

操作auto_ptr就像操作沒有使用auto_ptr的指標一樣,例如:
cout << *iPtr << endl; // 顯示100
if(sPtr->empty())
    cout << "字串為空" << endl;


您也可以建立一個未指向任何物件的auto_ptr,例如:

auto_ptr<int> iPtr;

未指向任何物件的auto_ptr不可以取值,否則會發生不可預期之結果,既然不可取值,如何判斷它是否有指向物件呢?您可以使用get()函式,它會傳 回所指向物件的位址,如果傳回0,表示不指向任何物件,如果不指向任何物件,您可以使用reset()來讓它指向一個物件,例如:

if(iPtr.get() == 0) {
    iPtr.reset(new int(100));
}

reset()可以接受一個指標或是0表示不指向任何物件,reset()會先delete目前指向的物件,然後重新指向新的物件,您也可以使用 release()釋放auto_ptr管理所指向物件的職責。


auto_ptr可以使用另一個auto_ptr來建立,這會造成所有權的轉移,例如:
auto_ptr<SafeArray> ptr1(new SafeArray(19));
auto_ptr<SafeArray> ptr2(ptr1);

當使用ptr1來建立ptr2時,ptr1不再對所指向物件的資源釋放負責,職責交給了ptr2,在使用指定運算時,也有類似的行為,例如:

auto_ptr<SafeArray> ptr1(new SafeArray(19));
auto_ptr<SafeArray> ptr2(new SafeArray(20));
ptr2 = ptr1;

ptr2所指向的物件會先被delete,然後ptr1的屬性會複製至ptr2,也就是ptr1所指向的物件,現在由ptr2指向它了,ptr1不再負責 所指向物件的資源釋放。

auto_ptr的資源維護動作是以inline的方式來完成,也就是在編譯時會被擴展開來,所以使用auto_ptr並不會犧牲效率。

最後要注意的是,auto_ptr不能用來管理動態配置而來的陣列,如果用它來管理動態配置而來的陣列,結果是不可預期的。



2014年7月17日 星期四

類別簡介

1. 對於簡單的成員函式,您可以將之實作於類別定義中,在類別定義中即實作的函式會自動成為inline函式,例如:
  • Ball.h
#include <string>
using namespace std;

class Ball { 
public: 
    Ball(); 
    Ball(double, const char*); 
    Ball(double, string&); 
 
    // 實作於類別定義中的函式會自動inline
    double radius() {
        return _radius;
    }
 
    string& name() {
        return _name; 
    }
 
    void radius(double radius) {
        _radius = radius;
    } 
 
    void name(const char *name) {
        _name = name;
    }
 
    void name(string& name) {
        _name = name;
    }
 
    double volumn() {
        return (4 / 3 * 3.14159 * _radius * _radius * _radius); 
    }
 
private:
    double _radius; // 半徑 
    string _name; // 名稱 
};

2. 在定義類別時,如果您只是需要使用到某個類別來宣告指標或是參考,但不涉及類別的生成或操作等訊息,則您可以作該類別的前置宣告(Forward declaration),而不用含入該類別的定義,例如:
  • Test.h
class Ball;

class Test { 
public:
    Test();
    Test(Ball*); 
 
    Ball* ball(); 
    void ball(Ball*);

private:
    Ball *_ball; // 名稱 
};


3. 如果您的類別定義有單一參數的建構函式(或除了第一個參數之外,其它參數都有預設值的建構函式),則預設會有自動轉換的作用,例如:

class Ball { 
public: 
    Ball(const char*); 
   ...
};

則您可以使用以下的方式來建構物件並初始化:

Ball ball = "Green ball";

預設的轉換行為是由編譯器施行的,但有時是有危險的,如果您不希望編譯器自作主張,則您可以使用explicit修飾,告訴編譯器不要自作主張:
class Ball { 
public: 
    explicit Ball(const char*); 
   ...
};


4. 在建構函式的初始化設定語法中,您還可以使用成員初始化列表(Member initialization list),例如:
  • SafeArray.cpp
#include "SafeArray.h"

SafeArray::SafeArray(int len) : length(len) {
    _array = new int[length];
}

...略

要被初始化的成員跟在參數列之後,要被設定給成員的引數被放在括號中,如果有多個成員要初始化,則以逗號分隔。

參考(Reference)

資料來源: http://openhome.cc/Gossip/CppGossip/Reference.html
參考(Reference)型態代表了變數或物件的一個別名(Alias),參考型態可以直接取得變數或物件的位址,並間接透過參考型態別名來操作物件, 作用類似於指標,但卻不必使用指標語法,也就是不必使用*運算子來提取值。

要定義參考型態,在定義型態時於型態關鍵字後加上&運算子,例如:

int var = 10;  // 定義變數
int *ptr = &var; // 定義指標,指向var的位址
int &ref = var;  // 定義參考,代表var變數

上面的程式中,最後一行即是在定義參考型態,注意參考型態一定要初始化,例如下面的定義是不能通過編譯的:

int &ref; // error, `ref' declared as reference but not initialized 

為何參考型態一定要初始化?因為參考初始化後就不能改變它所代表的物件,任何指定給參考的值,就相當於指定給原來的物件,例如:


#include <iostream>
using namespace std;

int main() {
    int var = 10;
    int &ref = var;
 
    cout << "var: " << var 
         << endl;
    cout << "ref: " << ref
         << endl;
 
    ref = 20;

    cout << "var: " << var 
         << endl;
    cout << "ref: " << ref
         << endl;

    return 0;
}

執行結果:
var: 10
ref: 10
var: 20
ref: 20

您也可以參考至一個字面常量,例如:
const int &ref = 10;

為什麼要在前面加上const才能參考至一個字面常量呢?您知道字面常量是不可定址的,為了能夠讓符合參考定址的語義,上面這段程式編譯器會傳如下的轉 換:

int tmp = 10;
const int &ref = tmp;

先想想沒有加上const的情況,如果您對ref重新指定值,則實際改變的是tmp的值,而不是字面常量10,這就在符合字面常量無法取址(也就無法改變 位址上的值)的語義,但使用者可能困惑明明改變了ref,為何字面常量沒有改變,所以加上const,明確指示不可以再重新指定值給ref,例如:

const int &ref = 10;
ref = 20; // error, assignment of read-only reference `ref' 

如果要定義指標型態的參考該如何呢?很簡單,指標型態是使用type*來宣告,而參考則是在名稱前加上&,所以指標型態的參考就如下所 示:

type *&refOfPtr = somePtr;

一個具體的例子如下:

int var = 10;
int *ptr = &var;
int *&ref = ptr;

舉一反三的話,如果有個const變數,您可以使用一個const指標,並可以如下宣告一個指標的參考:

const int var = 10;
const int *ptr = &var;
const int *&ref = ptr;

事實上很少會直接如上的方式來使用參考,而是用於函式傳遞時一種「傳參考」(Pass by reference)方式,目的在於可於函式中直接操作目標變數或物件,或者是避免複製一個大型物件,在之後要紹函式時會見到相關應用


指標與陣列

1.
被const宣告的變數一但被指定值,就不能再改變變數的值,您也無法對該變數如下取值:


const int var = 10;
var = 20; // error, assignment of read-only variable `var' 
int *ptr = &var; // error,  invalid conversion from `const int*' to `int*'


2.
用const宣告的變數,必須使用對應的const型態指標才可以:
const int var = 10;
const int *vptr = &var;

同樣的vptr所指向的記憶體中的值一但指定,就不能再改變記憶體中的值,您不能如下試圖改變所指向記憶體中的資料:

*vptr = 20; // error, assignment of read-only location 


3. 
另外還有指標常數,也就是您一旦指定給指標值,就不能指定新的記憶體位址值給它,例如:
int x = 10;
int y = 20;
int* const vptr = &x;
vptr = &x;  // error,  assignment of read-only variable `vptr' 


4.
在某些情況下,您會想要改變唯讀區域的值,這時您可以使用const_cast改變指標的型態,例如:
void foo(const int* p) {
    int* v = const_cast<int*> (p); 
    *v = 20; 
}

5. 
陣列的動態配置
int *arr = new int[1000];

用完要delete,記得要加[]
delete [] arr;


6.
#include <iostream> 
using namespace std; 

int main() {
    char *str = "hello"; 
    void *add = 0; 

    add = str; 
    cout << str << "\t" 
         << add << endl; 

    str = "world"; 
    add = str; 
    cout << str << "\t" 
         << add << endl; 
 
    return 0; 
}

執行結果:

hello    0x440000
world   0x440008