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

2017年4月12日 星期三

Operator 隱式轉換的重載,物件的轉出與轉入

Operator 隱式轉換的重載,物件的轉出與轉入

tags: operator2
你可以把一個新的物件當作一個型態看待,就像int是一個整數型態裡面什麼都沒有就只有一個成員int,這樣的概念,我們可以自訂一個型態。
這個型態你可以決定用什麼方式初始化他
class Opcast {
public: //建構子
    Opcast(int i): i(i) {}
private:
    int num;
};
比如說妳決定創建一個屬於自己的整數型態,可以這樣建立,且你可以從主程式初始化。
int main(int argc, char const *argv[]){
    Opcast a(int(1));
    return 0;
}
你也可以選擇輸入他類型的變數,比如說 double,在輸入的時候轉換成int讓引數符合初始化參數。static_cast<int>(num) 你可以把它看成 (int)num ,但前者是更好的寫法。
int main(int argc, char const *argv[]){
    double num = 1.0;
    Opcast a(static_cast<int>(num));
    return 0;
}
這裡我提一個自己的類比概念轉入,簡而言之就知把另一個型態轉入這個型態。還有一個概念是轉出,比如說這裡的 (int)num 以int的型態轉出給main使用。
你可能已經注意的到括號的使用右邊轉入,左邊轉出。
int i=0;
cout << Opcast(i) << endl;

Opcast a;
cout << (int)a << endl;
前者是把 i 導入,後者則是把 a 以 int 型態導出,這個以int形式導出的 行別轉換函式 是一個operator,這可能比較難想像。
函式的樣貌長這個樣子
Opcast::operator int() const{
    return i;
}
看你想重載導出什麼樣貌就寫什麼型態,後面的const是為了讓它可以同時接收兩種型態的呼叫
Opcast a(0);
const Opcast b(0);
這個樣子看起來一切很很美好,但實際上轉出與轉入它潛藏著不容易被發現的危機

轉入的隱式轉換

如果你有一個副程式接收了妳的自訂類別,而且那個自訂類別參數只有一個
Opcast::Opcast(int i): num(i) {}

void fun(Opcast a){
    cout << "fun" << endl;
}
我們試著呼叫他
fun(1);
理應是找不到定義為整數的方程式
void fun(int);
而實際上,編譯器會想辦一切辦法奏和出恰當的轉換,那個呼叫會變成
fun( Opcast(1) );
產生的結果可能不是你想要的,你可以透過關鍵字 explicit 避免這種編譯器自作主張的轉換
explicit Opcast::Opcast(int i): num(i) {}

傳統的解決方案(C11之前沒有這個關鍵字),可以使用替身類別來處理
class OpcastSize{
public:
    OpcastSize(int i): len(i){}
    operator int&(){return len;}
    int len;
};

Opcast::Opcast(OpcastSize i): num(i) {}


轉出的隱式轉換

如果你加載了型別轉換的函式要注意它有可能在你不需要的時候自作主張的呼叫
    Opcast a(1);
    cout << a << endl;
看起來因該會因為你沒有加載 << 運算子而導致呼叫失敗,實際上因為你加載了型別轉換函式,編譯器將會發現經過型別轉換後就可以正常印出了,而自作主張的幫你轉換。
這裡一樣可以使用關鍵字 explicit 來避免這種狀況
explicit operator double() const{...}
傳統的方案是乾脆不寫了,另外單獨使用一個普通函式來轉出,可以觀察 string 裡面的函式 c_str() 來進行型別的轉換。
這樣的寫法比較明確不會有其他可能的誤解,誤會成我是否可以將他轉成其他的任意型態,比如說 int、double。
其次也可以減少代碼的長度,C++的正確轉型方式為 static_cast<T>(t) 非常的長呢。


範例代碼

 /*****************************************************************
Name : Operator 隱式轉換的重載
Date : 2017/04/11
By   : CharlotteHonG
Final: 2017/04/11
*****************************************************************/
#include <iostream>
using namespace std;

class Opcast {
public:
    class OpcastSize{
    public:
        OpcastSize(int i): len(i){}
        operator int&(){return len;}
        int len;
    };
public: //建構子
    // ##轉入會被隱式轉換
    Opcast(int i): num(i) {}
    // 解決方法 1
    // explicit Opcast(int i): num(i) {}
    // 解決方法 2
    // Opcast(OpcastSize i): num(i) {}
public: // 轉型函式
    operator double() const{
        cout << "Cast to double. --> ";
        return num+1;
    }
    double to_dou(){
        cout << "Cast to double. --> ";
        return num+1;
    }
private:
    int num;
};

void fun(Opcast a){
    cout << "fun" << endl;
}
/*==============================================================*/
int main(int argc, char const *argv[]){
    Opcast a(1);
    // 轉入的隱式轉換
    cout << a << endl;
    // 轉出的隱式轉換
    fun(10);
    return 0;
}
/*==============================================================*/

2017年4月11日 星期二

Win10 中文輸入法消失不見了 無法輸入中文

Win10 中文輸入法消失不見了 無法輸入中文

先按一下CTRL+空白看看,按到會關閉輸入法打不出中文。
也不曉得怎麼觸發的,就忽然不見了,可以嘗試重這裡重啟輸入法,重啟輸入法之後要重開機,然後就正常了

位置

先按一下開始,然後直接貼上 工作排程 會自動搜索
進來之後左邊資料開始開依序是
工作排程器程式集 -> Microsoft-Windows -> TextServicesFramework ->
然後看右邊有個 MsCtfMonitor 對他點右鍵執行
然後重新開機吧~之後就一切正常了

地區及語言

也有可能是輸入法被砍了,一樣從開始搜索一下 地區及語言
要確定有 中文(台灣) 沒有的話按新增語言,搜索找一下自己新增回來

2017年4月9日 星期日

C語言 字元char與字串 差異與詳解

C語言 字元char與字串 差異與詳解

tags: C++ Concept2
初學C語言的時候遇到字串總是感到莫名其妙與不知道該怎麼用,比如說為什麼字串不能直接接上,或直接相等置換等等問題,大致列舉了一些常見的錯誤與解法。


常見的困惑

容易誤會的點,把字串當作型態,實際上字串是字元的陣列
int arr[3], arr2[3];
arr1 = arr2;
陣列的指派是不可行的,卻很容易把字串的指派,當作是可行的踩了坑
char* s="123\0";
char s2[4];

s2=s;
字串的指派是不可行的,不過反過來 s=s2 倒是可以。
還有一個很有趣的題型
char s[]="123\0";
printf("%s\n", s);
printf("%s\n", &s);
他們居然打印出一樣的結果!待會還是會提到,下面就開始提提有哪些該注意的地方,這都是指針與陣列的誤區。


指標與陣列的關係與差異

隱式轉換

陣列是陣列、指標是指標
只不過在大多數的時候陣列會自動轉換成指標,操作起來就像個指標一樣。具體差異在哪裡,並不太容易敘述清楚,下面會簡單的舉個例子說明不可互相替代。

指標宣告與陣列宣告

一般來說常數是存在唯讀記憶體內,比如說一個副程式的呼叫
“ABC\0” 返回的型態是 char* 一個指向 "ABC\0"的A 的地址
fun("ABC\0");
那個”ABC”不能夠被更動,是帶const屬性的,宣告的時候也一樣
這裡可以視為把 char* 賦值給 char* 是可執行的
char* str="ABC\0";
這樣子的方式會建立暫存,並返回指標,讓str指向那個暫存的唯讀空間
所以當你嘗試修改他時,是非法的。
str[0]='0';
如果我們使用字串的話則會不同的情況發生
char str[] = "123\0";
創建一個陣列,並將其內容初始化,這種情況下我們才可以正常的存取,從這裡可以看出來,除非你很確定我不會動到他,否則還是盡量使用
char str[];
避免自己採坑了,設定成指標之後不小心修改到發生非法存取
指標與陣列的差異也可以從sizeof上看出區別
int* num;
int arr[3];
printf("%d\n", (int)sizeof(num)); // 64bit size 8
printf("%d\n", (int)sizeof(arr)); // size 4*3=12
一個陣列可以獲取實際長度,轉為指標後則不可獲取
sizeof(arr) / sizeof(arr[0])

陣列的地址

經過以上大致敘述了陣列與指針的差異的,其中還有一個就是陣列也是可以取址的!
回頭來看看剛剛那一題,或許你已經想到是怎麼回事了
char s[]="123\0";
printf("%s\n", s);
printf("%s\n", &s);
這裡第一個 s 返回的是陣列起始的位址,型態為char*,這裡會發生一次隱式轉型,編譯器自作主張的將陣列char(*)[4]轉為指針char*型態;
第二個 &s 指的是對一個陣列 char(*)[7] 取址,而它的地址恰好與陣列的起始位置相等,型態一樣是char(*)[4]
printf 接收到兩個相等的位置,自然會打印出兩個一樣的結果。
另外要記住對一個位址取址是非法的。


為什麼不能直接相等傳遞

char str1[]="ABC\0";
char str2[]="DEF\0";
str1 = str2; //Error
就像整數陣列一樣沒辦法直接等號過去,很容易誤把字串當成是一種變數叫字串,實際上他是字元的陣列;換個方式寫你可能就有感覺了
int arr1[]={1, 2, 3};
int arr2[]={3, 2, 1};

arr1 = arr2; // Error

另外這裡也可以看出陣列與指標的不同之處
int  arr1[]={1, 2, 3};
int* p1 = NULL;
p1 = arr1; // is ok

只能利用for迴圈一個一個搬移過去
char* str1="ABC\0";
char  str2[4];

for (int i=0; i<4 ; ++i){
    str1[i]=str2[i];
}

printf("%s\n", str2);
你也可以使用內建的函式搬移
#include <stdio.h>
#include <string.h>

int main (){
  char str1[]="Sample string";
  char str2[40];
  char str3[40];
  strcpy (str2,str1);
  strcpy (str3,"copy successful");
  printf ("str1: %s\nstr2: %s\nstr3: %s\n",str1,str2,str3);
  return 0;
}


為什麼不能用+的

同上原因,陣列也沒辦法直接用加的,除此之外還要注意一個問題,長度是否足夠容下相加後
char str1[]="ABC";
char str2[]="ABC";
上述的作法長度會是3+1,可是相加後他們會變成6+1,你必須有一個足夠長的陣列,比如說將第二個長度拉長兩倍;這裡的+1是結束符。
char str1[]="ABC";
char str2[7]="ABC";

str2[3] = str1[0]
str2[5] = str1[1]
str2[4] = str1[2]
字串的相接也有函式可以使用
strcat (str2, str1);


字串殘留上一次的字元、清空與初始化字串

比如說這樣的範例,試圖讓副函式操作字串
/*****************************************************************
Name : 
Date : 2017/04/08
By   : CharlotteHonG
Final: 2017/04/08
*****************************************************************/
#include <stdlib.h>
#include <stdio.h>
#include <string.h>

void fun(char* str, char* str2){
    for(int i = 0; i < strlen(str2); ++i) {
        str[i]=str2[i];
    }
    printf("%s\n", str);
}
/*==============================================================*/
int main(int argc, char const *argv[]){
    char s[]="ABCDEF", s2[]="abc";
    fun(s, s2);
    return 0;
}
/*==============================================================*/
印出的結果是
abcDEF
通常這時候就會有人告訴你,你沒有將字串歸零!而我必須告你這只是表象,歸零可以解決這個問題,但是不是主因。
主因是不正確的操作字串
我把它定義為不正確操作字串而不是錯誤,需要理解一下這句話的意思以及差別這非常重要。

必須要知道的是自串一個很特別的地方在於,判斷他的長度或結束點實際上是以 '/0' 做判別的,每個字串的結尾都會有這個符號,可以通過簡單的方法檢測
char* str="ABC";

if(str[3] == '\0')
    printf("End\n");
補上正確的結束符號
void fun(char* str, char* str2){
    int len=strlen(str2);
    for(int i = 0; i < len; ++i) {
        str[i]=str2[i];
    } str[len]='\0';
    printf("%s\n", str);
}
是的單單只是補上 str[len]='\0'; 即可解決這個問題;
這才是解決問題的根本,歸零可能會造成太多效能的浪費。歸零我認為是好習慣,但是更重要的是正確的操作字串,所以這裡應該使用正確的操作字串而不是歸零。
由此也可以推斷有一點必須要小心,字串的長度必須是實際長度+1
char str[4]="ABC";


不容易發現的坑

宣告的缺失的結束符

有些編譯器這樣寫不會幫你補上 '\0' 以下寫法可能會導致各種問題,找不到結束字元,讀到一堆垃圾值
char str[]="ABC";
手動補上結束字元可以處理這個問題
char str[4]="ABC\0";
這個問題大概只會出現在古老的編譯器上,知道就好,遇到的時候再乖乖補上。

複製時缺失的結束符

此外 string.h 內的複製函式 strncpy() 存在一樣問題,複製的長度如果不足到底,沒有複製完會導致結束符號沒有被複製到
char s[]="ABCDEF\0";
strncpy(s, "123", 3);
printf("%s\n", s); // s is "123DEF"
輸出的結果s會是 "123DEF" 這可能不是你要的結果,這是個不容易發現的坑;然後一個看似沒問的長度計算也是一個小坑。
char str[4]="ABC\0";
printf("len is %d", (int)strlen(str)); // len is 3
仍然會缺少束字元需自行補上+1的長度
char s[]="ABCDEF\0", s2[]="abc\0";
strncpy(s, s2, strlen(s2)+1);
printf("%s\n", s); // s is "abc"

越界存取

上面的+1又延伸出一個問題,小心加錯了會導致非法存取,看一下面的例子
char s[]="ABCDEF\0", s2[3]="abc";
strncpy(s, s2, strlen(s2)+1);
printf("%s\n", s); // s is "abc"
看起來好像都一樣,實際上那個
s2[3]="abc"
不存在結束符號之外,s2他的合法操作空間只有3。
strlen(s2)+1
返還的長度為3,再加上1為4,strncpy不會幫你檢查越界存取
把s與s2反過來也是,s2會被塞超過自己的長度
char s[]="ABCDEF\0", s2[3]="abc";
strncpy(s2, s, strlen(s)+1);
編譯不一定會出錯,但是已經是非法存取了,只是運氣好沒炸。

把一個字元轉為指針輸入 string.h 的函式

string.h 的函式多數都是輸入字串指針的,這裡必須區別字串的指針與字元的指針他們是不一樣的,字串的長度的是依靠'/0' 做判別,而一個字元裡面不存在結束符號。
編譯器可能會好心的在你的常數後面補上補上結束符號,即便你忘記輸入
char* str="ABC";
實際上可能為
char* str="ABC/0";
宣告陣列時
char str[3]="ABC";
實際上可能為
char str[4]="ABC/0";
可當你是一個字元時,他就是一個字元,你不能期待他能夠取出長度
char s='1';
char* p = &s;
strlen(p);
多數的string.h函式都是利用結束符號偵測長度,並會一起把結束符號複製進去
char str[10]="ABC/0";
char* str2="CBA/0";

strcat(str, str2);
利用結束符號找到 str 結尾,利用結束符號找到 str2 有多長複製幾次
如果你輸入的是字元,將可能會發生未定義行為,因為沒有結束符號
char str[10]="ABC/0";
char s='D';
char* p=&s;

strcat(str, &s); // 可能會發生非法存取
這一點不只在這個函式會出問題,其他函式也是一樣的。這裡的一個大坑就是 字串字元的指針 都是用同一個型態表示 char* ;但是他們的規格不一樣,字串要求要有結尾符號,字元的指標並不要求。很容易造成混亂。
可以使用 typedef 幫你區分兩者,不過這不是標準作法只是一個方式
typedef char* Str
接下來如果你需要表態為字串則使用 Str 需要表示字元指針則維持原方案。這部分要自己管理好代碼,因為 Str 也是兩者通用的,只是換個名字方便你區別。
char str[] = "ABC\0";
Str str2 = str;

char a='A';
char* p = a;
// Str p2 = a; // 可以編譯但這樣就沒意義,自己要管好
舉個例子
void fun(Str p){..}
當看到 Str 就應該自己想起那個規則,噢!不可以在這裡放入字元指針,要放入帶有結尾符號的字串;並且要知道這件事情只有你自己知道,編譯器不知道並不會發出提醒。

非必要不要自己造輪子

最後想提醒的一點是 char 的 sizeof() 是 1,比如說範例中有一個函式是複製字串,那種複製方法,一次最就複製1個,而你的CPU多數是是64位元,理論上最多可以一次複製64,該交給 strcpy() 還是要交給 strcpy(),要譯器廠商一定會很好的解決這些問題。
你應該要自己練習如何實作這些已有的函式
但是非萬不得以,否則不要在真正的專案上使用自己造的同功能函式。