2014年8月18日 星期一

NFC Basics

  - NDEF的data有兩個主要用途:
    1. 從NFC tag讀取NDEF data
    2. 利用Android Beam將NDEF messages從一台機器傳送到另一台機器

The Tag Dispatch System
  - 當Android機台發現一個NFC tag時,最好是可以直接叫起最適當的activity來處理它而不用讓使用者選擇哪個application來處理,因為選擇的動作可能會讓使用者必須將機台離開tag。

  - Android提供一個特別的tag dispatch system
    1. Parse這個tag,找出data的MIME type或URI
    2. 封裝這個MIME type或URI到一個intent裡面
    3. 根據這個intent啟動activity


NFC tags如何map到MIME types跟URIs
  - NDEF data
    - 被封裝在一個message (NdefMessage)內
    - 包含一個或多個record (NdefRecord)
  - Android也能支援沒有NDEF data的tags,透過android.nfc.tech package來處理

  - 在標準的NDEF message中,第一個NdefRecord包含下列欄位:
     1. 3-bit TNF (Type Name Format)
        決定如何解譯variable length type欄位,如下表
Type Name Format (TNF)Mapping
TNF_ABSOLUTE_URIURI based on the type field.
TNF_EMPTYFalls back to ACTION_TECH_DISCOVERED.
TNF_EXTERNAL_TYPEURI based on the URN in the type field. The URN is encoded into the NDEF type field in a shortened form: <domain_name>:<service_name>. Android maps this to a URI in the form: vnd.android.nfc://ext/<domain_name>:<service_name>.
TNF_MIME_MEDIAMIME type based on the type field.
TNF_UNCHANGEDInvalid in the first record, so falls back to ACTION_TECH_DISCOVERED.
TNF_UNKNOWNFalls back to ACTION_TECH_DISCOVERED.
TNF_WELL_KNOWNMIME type or URI depending on the Record Type Definition (RTD), which you set in the type field. See Table 2. for more information on available RTDs and their mappings.
     2. Variable Length Type
         - 描述這個record的type
         - 若在上個欄位使用TNF_WELL_KNOWN,則此欄位用來描述Record Type Definition (RTD),如下表所示:
Record Type Definition (RTD)Mapping
RTD_ALTERNATIVE_CARRIERFalls back to ACTION_TECH_DISCOVERED.
RTD_HANDOVER_CARRIERFalls back to ACTION_TECH_DISCOVERED.
RTD_HANDOVER_REQUESTFalls back to ACTION_TECH_DISCOVERED.
RTD_HANDOVER_SELECTFalls back to ACTION_TECH_DISCOVERED.
RTD_SMART_POSTERURI based on parsing the payload.
RTD_TEXTMIME type of text/plain.
RTD_URIURI based on payload.
     3. Variable Length ID
        - 這個record的unique ID
        - 這個欄位並不常用,只有當想標明唯一tag時才需要產生這個ID

     4. Variable Length Payload
        - 想要讀/寫的實際data payload
        - 由於一個NDEF message可能有好幾個record,所以第一個record並不一定包含全部的payload

  - 若可mapping成功,則tag dispatch system會把這個map的資訊以及實際的payload封裝到一個ACTION_NDEF_DISCOVERED intent
  - 若無法辨別,則會將一 Tag 物件(包含tag的技術以及payload等資訊)封裝到一個ACTION_TECH_DISCOVERED intent

  - 舉例來說,當tag dispatch system遇到TNF_ABSOLUTE_URI,則它會將variable length type欄位map到一個URI,接著把這個UIR封裝到ACTION_NDEF_DISCOVERED intent中,同時也會把其他的資訊(如payload)一起包進去。


NFC tags如何dispatch到application
  - tag dispatch system定義了三個intent,優先權由高到低如下:
     1. ACTION_NDEF_DISCOVERED
     2. ACTION_TECH_DISCOVERED
     3. ACTION_TAG_DISCOVERED




Requesting NFC Access in the Android Manifest
  - 必須使用<uses-permission> element以存取NFC hardware
<uses-permission android:name="android.permission.NFC" />

  - 使用<uses-feature> element則有NFC的device才能在Google Play上看到你的AP
<uses-feature android:name="android.hardware.nfc" android:required="true" />

  - 如果不想用<uses-feature>,那就用NfcAdapter.getDefaultAdapter()動態檢查NFC是否存在



Filtering for NFC Intents
ACTION_NDEF_DISCOVERED
  - 例子: 用intent filter指出你想處理MIME type為text/plain的intent

<intent-filter>
    <action android:name="android.nfc.action.NDEF_DISCOVERED"/>
    <category android:name="android.intent.category.DEFAULT"/>
    <data android:mimeType="text/plain" />
</intent-filter>

  - 例子:處理 http://developer.android.com/index.html
<intent-filter>
    <action android:name="android.nfc.action.NDEF_DISCOVERED"/>
    <category android:name="android.intent.category.DEFAULT"/>
   <data android:scheme="http"
              android:host="developer.android.com"
              android:pathPrefix="/index.html" />
</intent-filter>

ACTION_TECH_DISCOVERED
  - 必須建立一個xml resource檔指出你的activity支持那些技術
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
    <tech-list>
        <tech>android.nfc.tech.IsoDep</tech>
        <tech>android.nfc.tech.NfcA</tech>
        <tech>android.nfc.tech.NfcB</tech>
        <tech>android.nfc.tech.NfcF</tech>
        <tech>android.nfc.tech.NfcV</tech>
        <tech>android.nfc.tech.Ndef</tech>
        <tech>android.nfc.tech.NdefFormatable</tech>
        <tech>android.nfc.tech.MifareClassic</tech>
        <tech>android.nfc.tech.MifareUltralight</tech>
    </tech-list>
</resources>





























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; 
};