2011年12月6日 星期二

Public, Private, and Protected 繼承


Public, Private, and Protected 繼承


 衍生類別 (Derived)






(Base)


Public繼承


Protected繼承


Private繼承

Public
Public
Friend function
Member function
non-member function(外部函式) 可直接存取
Protected
Friend function
Member function
可直接存取
Private
Friend function
Member function
可直接存取

Protected
Protected
Friend function
Member function
可直接存取
Protected
Friend function
Member function
可直接存取
Private
friend function
member function
可直接存取

Private
可藉由基礎類別 publicprotect的成員函數存取 member function friend function
Hidden
可藉由基礎類別 publicprotect的成員函數存取 member function friend function
Hidden
可藉由基礎類別 publicprotect的成員函數存取 member function friend function

2011年12月5日 星期一

類別的繼承與指標應用

甚麼是繼承(inherit)?
C++繼承是一種抽象的概念,有點將軟體元件(類別)擬人化的味道,先就字面意義來說,繼承是一種父子關係,宣告子類別指定繼承自某父類別意即子類別擁有父類別的特徵,在 C++ 語言中的具體表現是子類別可以使用父類別的所有 public 與 protected 資料成員與函式成員。
繼承的另一層面意義是「世襲」,子類別可以世襲父類別的位置意即子類別可以在系統中扮演父類別的角色,在 C++ 語言中的具體表現是父類別的指標可以指向子類別的 instance,易言之,在一套軟體當中,經過前面幾篇的介紹,你可以想像得到以 C++ 撰寫的軟體系統,經常設計了許許多多的類別,然後宣告或 new 出這些類別的 instance 來使用、串連、傳遞。例如前一篇的 Grade Class  範例當中我們設計了 GradeList 類別來提供班級的成績儲存與計算,並在 UserApplication.app 中使用它的 instance 來裝載資料,由於 C 語言的繼承特性,就算我們在設計這套系統的時候並不知道日後會有哪些新類別的存在,後來開發的新類別只要繼承自原來既有的類別,它們的 instance 也能夠在系統上被使用,舊系統不用改一行程式碼,例如本篇的 Grade Sheet 範例,我們設計了繼承自 GradeList 類別的 GradeSheet 類別,那麼它的 instance 也能夠在原來的 UserApplication.app 使用。
繼承的語法僅是在設計新類別的時候,在新的子類別名稱後面以 : 號指定以甚麼樣的權限繼承自哪個父類別,如下所示,父類別也稱為子類別的基礎類別(base class),子類別也稱為父類別的衍生類別(derived class)。
class 子類別名稱 : 繼承權限 父類別名稱
{
    ...子類別宣告...
};

繼承權限也分為 public 或protected 或 private 三種,它們會決定父類別中的資料與函式成員在這個子類別中的授權程度,最常見 public 繼承就是父類別的成員在子類別中都還是維持原權限,而 private 繼承則讓父類別的成員不能再讓被子類別的子類別繼承,以下是一個 public 繼承的例子,左邊的 GradeList 類別是前篇的範例,內容是一個班級的教師名稱、學生成績(動態陣列)、以及學生人數,右邊的 GradeSheet 類別繼承自 GradeList,所以 GradeSheet 類別已經擁有了 GradeList 所有的資料和函式成員,它只有再增加一個 index 代表班級的編號。
GradeList.h GradeSheet.h
class GradeList
{
   public:
   struct GRADE
    {   int id; 
        float value;  };

   char teacher[16]; 
   GRADE * grade; 
   int students;  

   ...函式成員宣告...
};     
class GradeSheet : public GradeList
{
    public:
    int index;

   ...函式成員宣告...
};      
在這個例子當中,讓我們觀察父子類別 GradeList 與 GradeSheet 在記憶體中的儲存狀況,你可以看到,由於 GradeSheet 繼承自 GradeList 類別,所以編譯器直接給了它一份父類別 GradeList 的記憶體布局(memory layout),然後把子類別新增加的 index 附加在後面,無怪乎父類別 GradeList * 指標也可以用來存取子類別的 instance。

註: 這個簡單的例子是因為 GradeList 與 GradeSheet 都沒有虛擬函式(virtual function),而且 GradeSheet 乃是單一繼承的子類別,記憶體才會如此單純,有虛擬函式或多重繼承(一個子類別有兩個以上的父類別)的記憶體布局較為複雜,但原理大同小異。
從這個例子我們可以知道類別繼承的效果之一就是子類別的結構包含父類別的結構,所以父類別的指標可以指向子類別的 instance。上面的例子是公開繼承,所以 GradeSheet 原封不動地接收了 GradeList 的資料與函式成員,也就是 GradeSheet 的 instance 完全可以被當作是一個 GradeList 的 instance 來使用,其他繼承權限的效果如下所述,可以營造出不同的效果。

公開繼承 (例如 class GradeSheet : public GradeList)

子類別繼承的資料與函式成員保持父類別的權限,public 還是 public, protected 還是 protected。

保護繼承 (例如 class GradeSheet : protected GradeList)

子類別繼承的資料與函式成員都變成保護的成員,public 變成 protected, protected 還是 protected。

私有繼承 (例如 class GradeSheet : private GradeList)

子類別繼承的資料與函式成員都變成私有的成員,public 變成 private, protected 也變成 private,讓父類別的成員無法再被繼承。
子類別的建構與解構
前篇提 到,建構函式和解構函式是類別的 instance 會自動執行的初始化與清理行為,當類別之間有繼承的父子關係的時候,理應執行父子類別的建構函式和解構函式,以保證不論是父類別或子類別的資料成員都會正 確地被初始化或清理,然而執行的順序會攸關這個行為的正確與否,還好編譯器會自動地做好了完善的安排。讓我們看下面這個實驗例子,當中父子類別的建構函式 和解構函式都包含有 printf( ) 述句,所以我們可以藉由觀察執行結果得知它們執行的順序。
GradeList::GradeList()
{  printf("GradeList::GradeList()\n");  }

GradeList::~GradeList()
{  printf("GradeList::~GradeList()\n");  }

GradeSheet::GradeSheet()
{  printf("GradeSheet::GradeSheet()\n");  }

GradeSheet::~GradeSheet()
{  printf("GradeSheet::~GradeSheet()\n");  }

void main()
{  GradeSheet sheet;  }
執行結果是:
GradeList::GradeList()
GradeSheet::GradeSheet()
GradeSheet::~GradeSheet()
GradeList::~GradeList()

所以我們可以得到結論如下:

建構順序是 父→子

先執行父類別的建構函式,再執行子類別的建構函式。

解構順序是 子→父

先執行子類別的解構函式,再執行父類別的解構函式。
道理很簡單,子類別建構函式當中可能去使用父類別的資料成員,甚至更改它的初始值,所以父類別的建構函式理應先執行,才能夠準備好以初始化的資料成員讓子 類別使用。而解構函式則相反,因為子類別在解構函式當中仍可能去使用父類別的資料成員,所以在那當下還不能夠清理掉父類別的資料成員,所以子類別的解構函 式理應先執行,最後才執行父類別的解構函式。
is a 與 has a 的概念
這是在 C++ 的教學與探討書籍中經常會宣揚的觀念,繼承是一種 is-a 關係(子類別是一種父類別),所以子類別有兩個重要的性質: 1. 子類別也具有父類別的特徵(意指資料與函式成員),2. 子類別也可以被當作父類別使用。類別之間的另一種相比較的關係是 has-a 關係,某個 A 類別 has a B  類別說穿了只不過是 A 類別的資料成員裡面有一個 B 的 instance 罷了,你可以把 A 稱為 B 的容器類別,如下面例子所示。
GradeSheet is a GradeList Course has a GradeList
class GradeList
{
    public:
    char teacher[16]; 
    GRADE * grade; 
    int students; 

    ...函式成員宣告...
};

class GradeSheet : public GradeList
{
    public:
    int index;

    ...函式成員宣告...
};     
class GradeList
{
    public:
    char teacher[16]; 
    GRADE * grade; 
    int students; 

    ...函式成員宣告...
};

class Course
{
   public:
   char name[16];
   GradeList list;

   ...函式成員宣告...
};      
在這個例子當中,GradeSheet is a GradeList 的意義是成績報表(grade sheet)也是一種成績清單(grade list),在實務上的意義是 GradeSheet 承襲了 GradeList的記憶體佈局,而 Course has a GradeList 的意義是課程資料(Course)包括了成績清單(grade list),在實務上的意義是 Course 包括了 GradeList的資料,兩者有甚麼差異呢?首先請看 GradeList、GradeSheet、Course 這三者的記憶體布局,由於GradeSheet 繼承自 GradeList,所以它自己的 index 資料成員附加在 GradeList 的資料成員之後,而 Course 類別同樣也包含了 GradeList 的資料,但它在記憶體中的位置則依宣告順序而定。

接下來看建構函式和解構函式的執行順序,我們同樣以一段程式碼做實驗:
GradeList::GradeList()
{  printf("GradeList::GradeList()\n");  }

GradeList::~GradeList()
{  printf("GradeList::~GradeList()\n");  }

Course::Course()
{  printf("Course::Course()\n");  }

Course::~Course()
{  printf("Course::~Course()\n");  }

void main()
{  Course course; }
執行結果如下:
GradeList::GradeList()
Course::Course()
Course::~Course()
GradeList::~GradeList()
結果是 has-a 這種關係,容器類別建構函式當中可能去使用資料成員,所以成員的建構函式理應先執行,才能夠準備好以初始化的資料成員讓容器類別使用。而解構函式則相反,因為容器類別在解構函式當中仍可能去使用資料成員,所以容器類別的解構函式理應先執行,最後才執行成員的解構函式。

建構順序是: 成員→容器

先執行成員類別的建構函式,再執行容器類別的建構函式。

解構順序是: 容器→成員

先執行容器類別的解構函式,再執行成員類別的解構函式。
使用父類別的指標
前面一再提到類別繼承的意義在於,子類別也具有父類別的特徵(意指資料與函式成員),以及子類別也可以被當作父類別使用,例子如下,子類別可使用父類別的資料與函式:
void main(void)
{
    // 宣告一個GradeSheet 類別的instance:
    GradeSheet sheet;

    // 使用 GradeList 和 GradeSheet 的資料成員:
    printf(" School: %s\n", GradeList::school);
    sheet.index = 5;
    printf(" Sheet index: %d\n", sheet.index);

    // GradeSheet 使用 GradeList 的函式成員:
    sheet.adjust_grade_list(); 
    sheet.sort_grade_list();
}
在這個例子當中,你可以看到 main( ) 當中根本就把 GradeSheet 的 instance 當作 GradeList 使用,但若僅僅如此,你仍然得把原程式碼當中宣告使用 GradeList instance 的部分都改為宣告 GradeSheet instance,所以要發揮繼承的優點,你應該盡量用父類別的指標來撰寫程式,如下例:
void main(void)
{
    // 宣告一個 GradeSheet 類別的 instance:
    GradeSheet sheet;

    // 用一個 GradeList 指標去指向這個 instance:
    GradeList * ptr = &sheet;

    // 透過 GradeList * 指標來使用此 instance:
    ptr->adjust_grade_list();
    ptr->sort_grade_list();
}
用父類別的指標來寫程式有甚麼好處?以下面的校務系統為例,在設計的時候,系統的行為完全是由父類別的指標所組成的,所以開發的時候不需要知道衍生類別的設計,新類別自然可參與舊系統,從這個例子你也可以看到,C++ 中的繼承代表了向後的擴充性
// 假設這三個類別都繼承 GradeList:
GradeSheet *pSheet = new GradeSheet();
OtherSheet *pOther = new OtherSheet();
SpecialSheet *pSpecial = new SpecialSheet();
...其他衍生類別的 instance...

// 用父類別指標陣列來裝子類別instances 的指標:
GradeList * pList[10];
pList[0] = pSheet;
pList[1] = pOther;
pList[2] = pSpecial;
pList[3] = ...;

// 假設校務系統的設計是呼叫所有班級的分數調整函式:
for (int i=0; i<10; ++i)
{
    pList[i]->adjust_grade_list();
}
透過 C++ 的繼承語法和父類別指標的使用,設計父類別的時候,不需要知道子類別的設計,但是子類別可以參與父類別的 frame work。設計子類別的時候,可以重複使用父類別的設計,而且透過繼承的方式,設計子類別的時候不需要更動父類別的程式碼,可說是最佳的程式碼 reuse 方式。

資料來源: http://www.csie.nctu.edu.tw/~skyang/inherit.zhtw.htm

2011年10月25日 星期二

容易混淆的const with point

const宣告的變數,被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*'


用const宣告的變數,必須使用對應的const型態指標才可以:

const int var = 10;
const int *vptr = &var;

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

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

另外還有指標常數,也就是您一旦指定給指標值,就不能指定新的記憶體位址值給它,例如:

int x = 10;
int y = 20;
int* const vptr = &x;
vptr = &x;  // error,  assignment of read-only variable `vptr'

在某些情況下,您會想要改變唯讀區域的值,這時您可以使用const_cast改變指標的型態,例如:



#include <iostream> 
using namespace std; 

void foo(const int*); 
 
int main() { 
    int var = 10; 
 
    cout << "var = " << var << endl; 
 
    foo(&var); 
 
    cout << "var = " << var << endl; 
 
    return 0; 
}

void foo(const int* p) {
    int* v = const_cast<int*> (p); 
    *v = 20; 
}


由於函式(Function)定義中傳入的指標使用const加上唯讀性,所以您不能對傳入的指標進行以下的動作:

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

但在某種原因下,您真的必須改變指標指向的位址處之資料,則您可以使用const_cast改變指標的唯讀性,

int* v = const_cast<int*> (p);
*v = 20;

執行結果:


var = 10
var = 20


您也可以使用舊風格的轉型語法,例如:

void foo(const int* p) {
    int* v = (int*) (p);
    *v = 20;
}



// Attempting to modify a constant pointer to nonconstant data.


int main()
{
      int x, y;
                // ptr is a constant pointer to an integer that can
                // be modified through ptr, but ptr always points to the
                // same memory location.
                int * const ptr = &x; // const pointer must be initialized
      *ptr = 7; // allowed: *ptr is not const
      ptr = &y; // error: ptr is const; cannot assign to it a new address
} // end main
// Attempting to modify a constant pointer to constant data.
#include <iostream>
using namespace std;


int main()
{
     int x = 5, y;


     // ptr is a constant pointer to a constant integer.
     // ptr always points to the same location; the integer
     // at that location cannot be modified.
     const int *const ptr = &x;


     cout << *ptr << endl;


     *ptr = 7; // error: *ptr is const; cannot assign new value
     ptr = &y; // error: ptr is const; cannot assign new address



} // end main

2011年9月22日 星期四

資訊隱藏的障礙

1. 資訊過份分散:
  • 如直接將100這個數字當成程式碼,將100當成常值,將導致對它的參數過度分散,最好將此部份的資訊隱藏起來,例如:寫入稱為MAX_EMPLOYESS的常數中,僅對於一處對其直進行變更。  
  • 最好將與使用者的互動集中在單獨的類別、封裝或系統中。
  •  
2. 循環相依性:

  • ex. A類別的常式呼叫了B類別的常式,B類別的常式又呼叫了A類別的常式。應該避免形成這種依賴的迴圈
3. 將類別資料誤認為全域資料
  • 全域資料通常受到兩種問題影響:
  1. 常式在全域資料上操作,但卻不知道還有其他常式也正用這些全域資料操作。
  2. 常式知道其他常式正用全域資料進行操作,但卻無法清楚知道它們所進行的操作。
  • 類別資料就不會遇到這些問題,因為只有類別內的少數常數才能直接存取這些資料。

4. 可察覺的效能減損

2011年9月21日 星期三

軟體分層設計的益處

1. 可區分文亂的劣等程式碼 。
2. 若最終能捨棄就程式碼或進行重整,除介面層外,將不必修改新程式碼。

2011年9月20日 星期二

上游的前置作業:依照程式前置作業所需完備度 選擇使用重複法或循序法

1. 選擇使用循序性的方法
  • 需求相當穩定時。
  • 設計是平鋪直敘,且相當容易瞭解。
  • 開發團隊十分熟悉該應用領域。
  • 專案所含風險極少。
  • 長期的可預測性十分重要。
  • 在下遊階段變更需求、設計及程式碼的成本可能會很高。

2. 選擇使用重複性的方法
  • 需求不易瞭解,或因為其他原因,您預期它們會不穩定。
  • 設計很複雜、很艱難,或兩者皆有。
  • 開發團隊十分不熟悉該應用領域。
  • 專案含有大量風險
  • 長期的可預測性不重要。
  • 在下遊階段變更需求、設計及程式碼的成本可能不會很高。 
由於軟體本身的性質使然,重複法通常會比循序法更有用。

    2011年9月9日 星期五

    c 分析命令列參數

    標頭檔: unistd.h

    原型:
    int getopt(int argc, char * const argv[], const char *optstring);
    說明:
    用來分析命令列參數,引數argc和argv是由 main()傳遞的參數個數和內容。
    參數 optstring 是有效選項(字母)。如 " a:bf"。代表要接受的option有 -a, -b, -f。
    其中option a的後面是":"號,代表-a option後還有參數(例如-aext2)。
              getopt提供三個global variable :
              optarg: 像a:這樣的參數,getopt會將-a後面的字串放到optarg中。
              optopt: 有額外參數時,optopt 會儲存option,如果getopt()找不到符合的參數則會
                           列印錯誤,並將全域變數optopt設為'?'字元。
              opterr: 如果不希望getopt()印出錯誤訊息則只要將全域變數opterr設為0即可。

    傳回值:
             參數字母: 如果找到符合的參數
             '?':  如果參數不包含在引數optstring 的選項字母
             -1:  分析結束

    範例:
    #include <stdio.h>
    #include <unistd.h>
    
    int main(int argc, char *argv[])
    {
      int ch;
      opterr = 0; //不印出錯誤訊息
    while((ch=getopt(argc,argv,"a:cef"))!=-1)
      switch(ch)
     {
      case 'a':
         printf("choose a: %s\n",optarg);
         break;
      case 'c':
         printf("option c\n");
         break;
      case 'f':
        printf("option f\n");
        break;
      default:
        printf("other option: %c\n",ch);
        break;
    
     }
    printf("optopt = %c\n",optopt);
    }
    
    
    結果:
    [root@localhost shelltest]# ./test -acef   
    choose a: cef
    optopt = 
    [root@localhost shelltest]# ./test -a   
    other option: ?
    optopt = a 
    [root@localhost shelltest]# ./test -aaa
    choose a: aa
    optopt =
    [root@localhost shelltest]# ./test -c
    option c
    optopt =
    [root@localhost shelltest]# ./test -cc
    option c
    option c
    optopt =
    source code http://opencobol.add1tocobol.com/doxy/dc/d53/getopt_8c.html

    2011年9月7日 星期三

    shell script po文測試標題

    一 基本觀念
    1. 回應目前所使用的shell為何?
      $echo $SHELL
    2. 通配符號
      ? 任何一個字符
      * 任何字符(一串字符)
      [set] 在set中的任何字符
      [!set] 不在set中的任何字符
    3. 大括弧擴展
      {,{,},}
      ex. echo b{ed,olt,ar}s   結果列印 beds bolts bars
      ex. echo b{ar{d,n,k},ed}s 結果列印bards barns barks beds
      ex. echo {d..h} 結果看到 d e f g h  
      至少要有一個逗號 否則所產生的單字會維持原狀 ex. b{o}lt 還是被當成b{o}lt
     
    4. 背景執行
      指令後面加&
     
    5. 檢視背景執行
      $jobs
     
    6. nice command
      $nice command
      command 以比較低的優先權處理
    7. 特殊字符與加引號
      ~ 主目錄
      ` 命令替換(舊式)
      # 註解
      $ 變數表示示
      & 背景工作
      * 字串通配符
      ( 啟動subshell
      ) 終止subshell
      \ 引述下一個字符
      | 導管
      [ 起始字符集通配符
      ] 終止字符集通配符
      { 起始命令區塊
      } 終止命令區塊
      ; shell命令之區隔符
      ' 強引號
      " 弱引號
      < 輸入轉向
      > 輸出轉向
      / 路徑名稱之目錄區隔符
      ? 單字符通配符
      ! 管線之邏輯NOT算符
     
    8. 去除特殊字符的字面意義
      a.)加單引號(quoting)---
      去除特殊字符的字面意義
      ex. $echo '2 * 3 > 5 is a valid inequality.'

      b.)以倒斜線規避
      ex. $echo 2 \* 3 \> 5 is a valid inequality.

    9. 控制鍵
      控制鍵    相應的stty名稱    功能
      ^C        intr              結束現行命令
      ^D        eof               結束輸入
      ^\        quit              結束現行命令,如果CTRL-C管用的話
      ^S        stop              停止輸出到螢幕上
      ^Q                          開始輸出到螢幕上
      ^?或DEL   erase             刪除最後一個字符
      ^U        kill              刪除整個命令列
      ^Z/^Y     susp              暫停現行的命令
      ^W                          werase
      ^R                          rprnt
      ^O                          flush
      ^V                          lnext
      可以使用stty -a 查看設定


    二 編輯命令列
    emacs 編輯模式
    1. 基本指令
    ^B  將游標往後(backward)移一個字符(不會進行刪除動作)
    ^F  將游標往前(forward)移一個字符
    DEL 往後(backward)刪除一個字符
    ^D  往前(forward)刪除一個字符

    2. 單字處理命令
    ESC-B   將游標往後(backward)移動一個單字
    ESC-F   將游標往前(forward)移動一個單字  
    ESC-DEL 往後刪除(kill)一個單字
    ESC-^-H 往後刪除一個單字
    ESC-D   往前刪除一個單字
    ^-Y     取回(yank)上一次所刪除的項目

    3. 列處理命令
    ^-A   移到列的開頭
    ^-E   移到列的結尾
    ^-K   往前刪除一整列

    4. 在歷程檔中遊走
    ^-P     移到上一頁
    ^-N     移到下一頁
    ^-R     往回搜尋
    ESC-<   移到歷程的第一列
    ESC->   移到歷程的最後一列

    5. 文字補全
    tab     試圖進行一般的補全功能
    ESC-?   列出可能的補全結果

    ESC-/   試圖補全檔案名稱
    ^-X/    列出可能的『檔案名稱』補全結果
    ESC-~   試圖補全用戶名稱
    ^-X~    列出可能的『用戶名稱』補全結果
    ESC-$   試圖補全變數
    ^-X$    列出可能的變數補全結果
    ESC-@   試圖補全主機名稱
    ^-X@    列出可能的『主機名稱』補全結果
    ESC-!   試圖補全命令
    ^-X!    列出可能的『命令』補全結果
    ESC-TAB 試圖以歷程紀錄(history list)中之前所鍵入的命令進行補全

    6. 雜項命令
    ^-J     同RETURN
    ^-L     清除螢幕,將現行(current)列置於螢幕頂端
    ^-M     同RETURN
    ^-O     同RETURN,但會接著顯示命令歷程中的下一列
    ^-T     將標點兩邊的字符對調,並將標點向右移動一個位置
    ^-U     刪除從列的開頭到標點為止的部份
    ^-V     未下一個字符加上引號(規避該字符的特殊意義,如果有特殊意義的話)
    ^-[     同ESC(適用於大多數鍵盤)
    ^-C     將標點右邊的單字(word)換成大寫字母開頭
    ^-U     將標點右邊的單字全部換成大寫字母
    ^-L     將標點右邊的單字全部換成小寫字母
    ESC-.   在標點右邊插入上一道命令列的最後一個單字
    ESC-_   同ESC-.


    vi 編輯模式

    0. vi輸入模式的編輯命令
    del   刪除前一個字
    ^-w   刪除前一個單字
    ^-v   為下一個字符加上引號
    esc   進入控制模式

    以下所有命令都可以前置一個數字(n)做為重複次數
    1. 簡單的控制模式命令
    h   往左移一個字 (與編輯時^+H有關)
    l   往右移一個字
    w   往右移一個單字
    b   往左移一個單字
    W   移到下一個非空白單字的開頭
    B   移到前一個非空白單字的開頭
    e   移到現行單字的末端
    E   移到現行非空白單字的末端
    0(zero) 移到一列之首
    ^   移到一列之第一個非空白字符
    $   移到一列的的末端

    2.  鍵入和更改文字

    i    字前插入文字(插入)
    a    字後插入文字(添加)
    I = 0i    列首插入文字
    A = $a    列尾插入文字
    R       覆蓋既有文字(Replace in edit mode)
    (n)r    覆蓋既有文字(command)

    3. 刪除命令
    用c替代d,會在做完刪除後進入輸入模式
    dh    往左刪除一個字
    dl    往右刪除一個字
    db    往左刪除一個單字
    dw    往右刪除一個單字
    dB    往左刪除一個非空白單字
    dW    往右刪除一個非空白單字
    d$    一直刪除到行尾端
    d0    一直刪除到行開端

    D = d$
    dd = 0d$
    C = c$
    cc = 0c$
    X = dh
    x = dl

    u    undo(undelete)
    .    重複上次文字修改命令
    y(yank)    擷取
    p    文字插入在字的右邊
    P    文字插入在字的左邊

    4. 在歷史檔中遊走

    k = -    往後移動一列
    j = +    往前移動一列 (與編輯時^+J有關)
    G = gg    移動到重複次數所指定的命令列
    /string    往後搜尋字串string
    ?string    往前搜尋字串string
    n    依上次搜尋方向重複搜尋
    N    依上次搜尋反方向重複搜尋

    5. 尋找字符命令
    fn    向右移動到下一個n所在位置
    Fn    向左移動到前一個n所在位置
    tn    fn+移回一格
    Tn    Fn+往前移一格
    |

    三. 訂製你的環境
    1. 啟動檔: .bash_profile .bash_logout .bashrc
        登入系統檔案尋找順序(找不到檔案便會往下一個檔案找): .bash_profile -> .bash_login -> .profile
        離開login shell :會執行.bash_logout
        .bash_profile只會被login shell    所讀取與執行。如果在命令列上以鍵入bash的方式啟動一個新的shell(一個shushell),它會試圖從.bashrc檔中讀取命令。
    2. 別名:
        alias name='command' or command    //=兩邊不能有空格
        可以替別名取另一個別名,也就是別名可以遞迴定義。
       
        alias listfile=ls
        alias ls=listfile  //那麼別名listfile將會被略過不計。
       
        bash隱晦功能:如果一個別名的值(等號的右邊部分)是以一個空格結束的,那麼bash便會對命令列的下一個單字進行別名替換。要讓別名的值以空格結束,得要為它加上引號。

        alias name 或 alias : 會列出別名設定。
    3. 選項:
        set -o optionname (開啟option) 和 set +o optionname(關閉option)
        3.1 shopt
            -p 列出可設定的shell選項以及他們目前的設定值。
            -s 設定(set)或開啟每個選項名稱。
            -u 取消(unset)或關閉每個選項名稱。
            -q 抑制正常輸出;傳回狀態會指出一個變數是否有被設定。
            -o 允許選項名稱的值成為set命令以-o選項所定義的值。

    4. shell變數:
        varname='value' or value //=兩邊不能有空格
        變數使用$varname
        查看變數值 可使用echo $varname
        4.1 變數以及加引號
            雙引號裡的特殊字符會被shell解釋
            單引號裡的特殊字符完全不會遭到shell的解釋
        4.2 內建變數
            4.2.1 編輯模式變數

            4.2.2 郵件變數
                MAIL    郵件檔的名稱,用以檢查有無新收到的郵件
                  郵件檔 /usr/mail/yourname
                  只要設定 MAIL=/usr/mail/yourname 就行了。

                MAILCHECK    多久檢查一次新郵件,以秒為單位(預設60秒)
                MAILPATH    郵件檔名單,以冒號(:)為分隔符,用來檢查新收到的郵件。
                  ex.     MAILPATH=/usr/mail/you/martin:/usr/mail/you/geoffm:/usr/mail/you/paulr
                          MAILPARH="\
                          /usr/mail/you/martin?you have mail from Martin.:\
                          /usr/mail/you/geoffm?Mail from Geoff has arrived.:\
                          /usr/mail/you/paulr?There is new mail from Paul."
                $_ 顯示幕前的郵件檔名稱

            4.2.3 提示變數
                分別存放在PS1 PS2 PS3 PS4
                提示字串的制定命令
                \a    ASCII的響鈴(bell)字符(007)
                \A    採用24小時制,以HH:MM的格式來顯示現在的時間
                \d    以Weekday Month Day的格式來顯示日期
                \D{format}    format會被傳入strftime(3),所產生的結果會被插入提示字串;如果format為空值,則會以語系特定的格式來顯示時間
                \e    ESC(033)
                \H    主機名稱
                \h    主機名稱中第一個.之前的部分
                \j    shell目前所控管的工作數量
                \l    shell之終機端名稱的基本名稱(basename)
                \n    LR+LF
                \r    LR
                \s    shell的名稱
                \T    採用12小時制,以HH:MM:SS的格式來顯示現在的時間
                \t    以HH:MM:SS的格式來顯示現在的時間
                \@    採用12小時制,以am/pm的格式來顯示現在的時間
                \u    現行使用者的用戶名稱
                \v    bash的版本 ex.2.00
                \V    bash的發行版本;版本和修補次數 ex.2.00.0
                \w    現行工作目錄
                \W    現行工作目錄的基本名稱(basename)
                \#    現行命令的命令編號
                \!    現行命令的歷程編號
                \$    如果有效的UID是0,就顯示一個"#"符號,否則就顯示一個"$"符號
                \nnn    八進制的字符碼
                \\    顯示一個到斜線
                \[    非列印(non-printing)字符序列的開頭,像是終端機的控制序列
                \]    非列印(non-printing)字符序列的結尾。

            4.2.4 命令的搜尋路徑
                在.bash_profile加入PATH: PATH=$PATH:"/home/you/bin"

            4.2.5 將命令的路徑存入雜湊表
                hash 查看雜湊表內容
                hash -r    移除hash的一切
                hash -d    name    移除特定的命令名稱
                hash -p namepath    加入特定的命令路徑
               
                使用set 指定hashall 選項開關hash

            4.2.6 目錄的搜尋路徑和變數
                CDPATH 值如同PATH
                    空字串代表現行目錄

            4.2.7 雜項變數
                HOME    你的主機(登入)目錄名稱
                SECONDS    shell被調用之後所持續的秒數
                BASH    你所執行之shell的路徑名稱
                BASH_VERSION    你所執行之shell的版本編號
                BASH_VERSINFO    你所執行之shell的版本資訊陣列
                PWD    現行目錄
                OLDPWD    執行上一次cd命令之前所在的目錄
        5. 定製與子行程
            5.1 環境變數
                export varnames
                export or export -p 察看環境變數

                TERM=trythisone emacs filename

    四. shell 程式設計基礎

        1. shell 命令稿與函式
            執行script方法:
                        1. source scriptname
                        2. 鍵入檔案名稱+enter
            ./scriptname :相當於鍵入命令稿的絕對路徑
            透過名稱調用命令稿之前需要先賦予它執行權

            差異:
                source scriptname    shell:        ---->--script-->---->    在同一個shell上執行script

                scriptname            shell:        ---->           ---->    shell等待subshell執行完script後繼續執行
                                    subshell:         --script-->        在subshell上執行script

                scriptname&            shell:        ---->...........---->    subshell執行同時shell繼續執行(subshell在背景執行)
                                    subshell         --script-->        在背景subshell執行script

            1.1 函式
                表示方式:
                1.
                function functname
                {
                  shell commands
                }
                或
                2.
                functname ()
                {
                  shell commands
                }

                可以使用unset -f functname 刪除某個函式的定義
                declare -f :查看登入時間定義了那些函式
                declare -F :僅查看函式的名稱

                函式與script差異:    1. 函式並不會以名子來調用script那樣被執行在另一個行程中,執行一個函式的語意比較像是登入系統時,.bash_profile檔所做的事或是像以source命令來調用任何script。
                                    2. 函式與script同名時,函式具有優先執行的權利

                各種命令優先權:
                                    1. 別名
                                    2. function 以及 if 和for 之類的關鍵字
                                    3. 函式
                                    4. 內建命令,像是cd 和type
                                    5. script和可執行的程式,shell會根據PATH環境變數裡所列舉的目錄來搜尋他們
                如果名稱相同,別名會比函式和script具有更高的執行優先權。可以使用command,builtin 和 enable等內建命令來改變他們優先執行順序。
               
                type name : 解釋bash命令方式
                            type -a name 查看name所有定義
                            type -p name 查看限制為可執行檔或shell script
                            type -P name 查看限制為可執行檔或shell script 即使-t選項回傳值不是file
                            type -f name 查看只支援關鍵字、檔案以及別名。
                            type -t name 只查看描述單字alas, keyword, function, builtin 或file
                            -t可以和其他選項一起使用

        2. shell變數
            可以使用$VAR 取得值
            可以透過export讓子行程知道
            2.1 位置參數
                唯讀不能指定新值
                1 2 3等等: 他們的值$1 $2 $3表示第一個到第三個輸入參數

                0 值$0 : 代表script file name

                * 和 @ : 這2個特殊變數包含了所有的位置參數(位置參數0除外),兩者之間的差異相當微妙但很重要,而且只有當他們被雙引號刮住時,才會凸顯此一差異。
                    "$*" : 是一個包含所有位置參數的單一字串,並以環境變數IFS(internal field separator)中的第一個字符做為位置參數的分隔符;IFS的預設通常是一個空格、跳格或換行符號。
                    "$@" : 相當於"$1" "$2" ..."$N" N代表參數數目相當於N個以雙引號刮住的字串,並以空格為分隔符

                #    : 存放位置參數的數目
                2.1.1 函式中的位置參數
                -----------------------------------
                |         ascript(script)            |
                |    -------------                    |
                |    |    $var1    |                    |   
                |    -------------                    |
                |    -------------                    |
                |    |    $0        |                    |
                |    -------------                    |
                |    -------------                    |
                |    |    $1        |                    |
                |    -------------                    |
                |    -------------                    |
                |    |    $2        |                    |
                |    -------------                    |
                |    ---------------------            |
                |    |  函式 afunc        |            |
                |    |    -------------    |            |
                |    |    |    $1        |    |            |
                |    |    -------------    |            |
                |    |    -------------    |            |
                |    |    |    $2        |    |            |
                |    |    -------------    |            |
                |    ---------------------            |
                -----------------------------------
                $var1 $0 在ascript(script)和函式afunc都看的到
                script的$1 $2 只有在script看的到
                函式的$1 $2 只有在函式看的到

            2.2 函式裡的區域變數
                local var    #設定為區域變數

            2.3 具引號的$@和$*
                為什麼"$*"中的元素要以IFS的第一個字符做為分隔符號,而不使用空格就好?這是為了讓你的輸出具有彈性。
                為什麼"$@"的行為就好像是N個被加上雙引號的字串?這是為了讓你能再以分開的值來使用他們。
            2.4 變數的另一種語法
                除了$varname 外,還有${varname} 這個更一般化之語法的簡單形式。
                為什麼會有兩種語法?
                    1. 如果你的程式碼參用到九個以上的位置參數時你必須用${10}來表示第十個參數,不可以用$10
                    2. 如果想要在變數後面加一個底線之類符號
        3. 字串算符
            字串算符能讓你作以下幾件特別的事:
                確定變數存在與否(亦既有被定義而且其值不是空的)
                為變數設定預設值
                捕捉因變數未被設定所導致的錯誤
                移除變數值中與樣式相符的部份
            3.1 字串算符的語法
                字串算符的語法,基本上就是將具運算能力的特殊字符擺在變數名稱與右括號之間,而算符所需要的任何引數則會擺在算符的右邊。
                ${varname:-word} :    若varname存在而且不是空的,則傳為它的值;否則傳回word。
                                    目的:若此變數沒有被定義,則傳回一個預設值。
                                    ex.:  若count沒有定義 ${count:-0}的求值結果為0

                ${varname:=word} :    若varname存在而且不是空的,則傳回他的值;否則就把他設成word,然後傳回它的值。位置參數和特殊參數不能以這種方式指定。
                                    目的:若某個變數沒有被定義,則為他設定一個預設值。
                                    ex.: 若count沒有定義 ${count:=0} 會把count設成0。

                ${varname:?message} :    若varname存在而且不是空,則傳回它的值;否則印出varname:message,並放棄現行的命令或命令稿 (只適用於以非互動方式操作的shell)。若省略message則會產生parameter null or net set
                                    目的:捕捉因變數未定義所導致的錯誤。
                                    ex.: 若count沒有定義 ${count:?"undedined!"}會顯示 count:undefined!

                ${varname:+word}    若varname存在而且不是空的,則傳回word;否則傳回空值(null)。
                                    目的:測試某個變數存在與否。
                                    ex.: 若count有定義 ${count:+1}會傳回1(代表此事為真)。

                ${varname:offset:length} :進行子字串的擴展(substring expansion)。它會傳回$varname中開始位置為offset,字符個數為length的子字串。
                                        $varname中第一個字符的位置編號為0。若省略length,這個子字串將會從offset開始,一直延續到$varname的末端。
                                        如果offset小於0,那麼位置會從$varname的末端開始算起。
                                        如果varname是@,哪麼length就是從參數offset開始算起之位置參數的數目。
                                        目的:傳回某個字串的一部分(子子串或切片)
                                        ex.若count被設成frogfootman,則${count:4}會傳回footman。而${count:4:4}會傳回foot。

            3.2 樣式以及樣式比對
                ${variable#pattern}        如果pattern與變數值的開頭相符就刪除相符的部分(取最短者),並傳回變數其餘的部份。
                ${variable##pattern}    如果pattern與變數值的開頭相符就刪除相符的部分(取最長者),並傳回變數其餘的部份。
                ${variable%pattern}        如果pattern與變數值的結尾相符就刪除相符的部分(取最短者),並傳回變數其餘的部份。
                ${variable%%pattern}    如果pattern與變數值的結尾相符就刪除相符的部分(取最長者),並傳回變數其餘的部份。

                ${varable/pattern/string}
                ${varable//pattern/string}    在variable中符合pattern的部份(取最常者)會被取代成string。
                                            如果是第一種形式,只有的一個相符的部份會被取代。
                                            如果是第二種形式,所有相符的部份都會被取代。
                                            如果pattern是以一個#開始的,它就必須比對變數的開頭。
                                            如果pattern是以一個%開始的,它就必須和變數的結尾進行比對。
                                            如果string是空的,符合的部份會被刪除。
                                            如果variable是@或*,則會依序對每個位置參數作此操作。
            3.3 長度算符
                ${#varname}        它會傳回字符串之變數值的長度。

            3.4 經延伸的pattern比對算符
                *(patternlist)     所指定的pattern出現零或多次,代表符。
                +(patternlist)     所指定的pattern出現一或多次,代表符。
                ?(patternlist)     所指定的pattern出現零或一次,代表符。
                @(patternlist)     所指定的pattern出現一次,代表符。
                !(patternlist)     相符於任何pattern,但所指定的樣式除外。
        4. 命令替換
            $(UNIX Command)
       
    五. 流程控制
        1. if/else
            if condition
            then
                statements
            [elif condition
                ethen statements...]
            [else
                statements]
            fi

            1.1 結束狀態
                0 通常代表沒問題的結束狀態。 其他編號(1~255)通常代表錯誤。
                如果結束狀態=0, condition 會被判定為真; 若為其他值則判定為否。

                $?    最後一個執行之命令的狀態。
            1.2 return N
                return 只能用在函式和以source 執行的shell script。相對而言exit N 會結束整個shell script。
               
            1.3 合併結束命令
                statement1 && statement2 執行statement1,如果它的結束狀態是0(true),繼續執行statement2。
                statement1 || statement2 執行statement1,如果它的結束狀態不是0(true),繼續執行statement2。

            1.4 條件測試
                [...]
                [[...]] 如同[...]但不會對括符裡的單字進行單字分離和路徑名稱擴展的動作。
               
                1.4.1 字串比較
                    字串比較算符
                    算符        若...為真
                    ==================================================================               
                    str1=str2    str1與str2相符
                    str1!=str2    str1與str2不相符
                    str1<str2    str1小於str2
                    str1>str2    str2大於str2
                    -n str1        str1不是空的
                    -z str1        str1是空的
               
                1.4.2 檔案屬性檢查
                    檔案屬性算符
                    算符        若...為真
                    ==================================================================               
                    -a file        file 存在
                    -d file        file 存在,而且是一個目錄。
                    -e file        file 存在;如同-a
                    -f file        file 存在,而且是一個普通檔案(並非目錄或其他特殊檔)
                    -r file        擁有file的讀取權
                    -x file        擁有file的執行權,或目錄的搜尋權
                    -N file        自從上次讀取後file已被修改過
                    -O file        擁有file的所有權
                    -G file        file的群組識別碼符合你的群組識別碼,或是你的群組識別碼中的一個(如果你同時隸屬於多個群組)
                    file1 -nt file2    file1比file2還新
                    file1 -ot file2    file1比file2還舊

                    if [ condition ] && [ condition ]; then
                    if command && [ condition ]; then

            1.5 整數條件句
                算術測試算符
                測試            比較
                =================================================================
                -lt                小於
                -le                小於或等於
                -eq                等於
                -ge                大於或等於
                -gt                大於
                -ne                不等於

        2. for
            for 迴圈可讓已以固定的次數<重複執行某個區段的程式碼
            語法:
                for name [in list ]
                do
                    用來處理 $name 的述句
                done
            list是一份名單(如果省略 in list,這份名單會被預設為"$@")