本章概要:
型別(type)對任何程式語言來說, 都是非常重要的基礎, 尤其是C/C++這種較嚴謹的程式語言來說, 更不能忽略它. 型別定義了儲存空間(記憶體)的需求, 決定了一個資料的範圍, 以及能施行於該型別物件上的操作. 而C++所提供的基本型別如int和char, 在硬體上有各自不同的表述方式.
------------------------------------------------------------------------------------
章節2. 變數和基本型別
2.1 基本內建型別 (Primitive Built-in Types)
2.2 字面常數 (Literal Constants)
2.3 變數 (Variables)
2.4 const 飾詞 (Qualifer)
2.5 References (參照, 引照, 址參)
2.6 typedef 的名稱
2.7 列舉 (Enumerations)
2.8 Class 型別
2.9 寫出自己的標頭檔(Header Files)
------------------------------------------------------------------------------------
2.1 基本內建型別 (Primitive Built-in Types)
C++ 定義了一組基本算術型別(arithmetic types), 其中有整數, 符點數, 個別字元以及布林值(boolean, 真假值). 其中還有個特殊型別void(空型別). 通常使用在函式定義無回傳型別上.
而每個算數型別都會有自己的"尺碼", 這裡指的是記憶體上占用的資料大小, 並且決定這個型別可以表示的資料範圍. 所以選擇正確的資料型別去處理資料是很重要的基礎.
基本的算術型別尺碼
bool 布林 1bit
char 字元 8bits
wchar_t 寬字元 16bits
short 短整數 16bits
int 整數 16bits
long 長整數 32bits
float 單精度浮點數 6位有效數字
double 倍精度浮點數 10位有效數字
這部分可以參考wiki上的資料型別的說明, 而除了bool型別外, 都可以區分為Sined(帶正負號) 和 Unsigned(無正負號).
很多人一定聽過 電腦的資料是由0跟1組成, 而在程式語言中, 最小單位就是bit(位元). 是一個二進制的表示符號, 它可以表示0或1. 以int來說, 這是一個16bits的型別, 可表示的資料範圍就是2^16 (2的16次方), 而因為sign bit(正負號位元)占1bit, 所以實際的資料範圍就會是-2^15 ~ 2^15-1. (-32768 ~ 32767). 所以若是unsigned的資料型別就會多一個位元可表示(ex. uint 2^16). 而二進制的部分一樣可以參考外部資料(二進制教學).
所以從上述可得知, 程式語言中的資料範圍是有限長度, 而不是無限. 也因此在程式設計上, 若一開始使用較小的資料型別(ex. int), 但日後因為需求而導致必須更換資料型別(ex. int -> long), 就是很麻煩的事情. Y2K(千禧蟲)問題也是類似的資料設計原因造成.
而目前在程式語言中, 拜硬體快速發展的緣故, 有可能看到的極大資料單位為long long的型別(或者為Int64), 顧名思義就是2倍long的長度, 或者64位元的整數, 可表示長度就有2^64(20位數, 1千京).
在程式語言中除了整數外, 還有浮點數(float), 但float(double)的型別, 並不是真正精準的浮點數, 它只保證了一定位數的有效數字, 而會有這樣的原因, 來自於電子計算機的浮點數設計.有興趣的可參考浮點數誤差. 所以當我們在使用浮點數時, 就要特別注意誤差值, 比如float只保證6位有效數字, 所以當出現0.99999978這樣的數字時, 是不是要將之修正為1.0呢.
P.S 這邊所寫的資料型別尺碼, 是C++的原始設計, 但實際上會依據編譯器設計而有不同(ex. Visual C++中int實際上為32bits).
------------------------------------------------------------------------------------
2.2 字面常數(Literal Constants)
這邊簡單來說, 就是變數的表示資料. 比如整數(interger)可用10進制, 8進制, 16進制去表示同一個數字.
ex. 10進制的20
20 //10進制
024 //8進制
0x14 //16進制
正常使用下, 直接賦予的一般數字, 都會視為10進制來使用. 但如果在數字起首前加上0(零), 則會被計算機(電腦)視為8進制的數字. 而0x或0X則會被視為16進制.
在這邊我們利用一個泛型(template)函式去印出代入的引數大小的例子來說明, 字面常量的使用.
template <class T> //泛型使用
void testFunc(T num)
{
std::cout << "num:" << num << ", size:" << sizeof(num) << " bytes" << endl;
}
testFunc( 10 ); //沒有特別描述 則代入的引數預設型別為int
testFunc( 10LL ); //描述代入的引數值為long long型別
從上述使用中會印出的結果, 可以看出明明數字都是10但因為給予了字面常量的描述而變成不同的資料型別.
num:10, size: 4 bytes
num:10, size: 8 bytes
其餘的部分在說明上比較繁瑣且不是那麼重要, 可以直接參考C++ Gossip的字面常量.
------------------------------------------------------------------------------------
2.3 變數 (Variables)
變數, 顧名思義就是會變動的數值, 在程式語言中是用來紀錄資料的值. 並且每個變數都會有自己的資料型別.
int value = 10;
string name = "Tom";
從上例中, value跟name都是變數名稱, 前者為整數型別的變數存放10的資料, 後者為字串型別並存放"Tom"的資料.
其中我們會將' = ' 賦值運算做的左右兩方, 視為左值(Lvalues) 與右值(Rvalues).
所以上例中, value 跟name就是左值, 10跟"Tom" 就是右值.
而左值必須為變數, 右值則可是變數或者字面常數.
int a = 0, b = 1;
0 = b; //編譯時會發生錯誤 左值不可為字面常數
a = 1; //不會有問題
而變數名稱在程式語言的閱讀中, 也是非常重要的一環. 好的變數名稱可以讓程式碼更清晰易懂.
void testFunc()
{
int a, b, c, d;
//穿插n行程式碼...
cin >> a >> b >> c;
//穿插n行程式碼..
d = a *b *c;
}
從上例中, 看到了一個簡單的函式, 裡面宣告了4個int變數, 其中a,b,c透過input取得. d則是a,b,c相成後的結果. 但為什麼d = a * b * b, 可能只有撰寫程式的人當下才知道. 並且實際上, 整個程式碼可能非常攏長, 無法讓人一眼就辨識a ,b ,c ,d的由來. 這就是一個不好的變數命名習慣.
因為a,b,c,d 在字面上毫無意義可言. 若今天某人用了這樣的變數名稱寫了一個非常大的程式, 後續接手維護的人, 只會不停咒罵是哪個白癡寫的.
int computeCapacity( int length, int width, int height)
{
return length * width * height;
}
像上例我們想寫一個計算體積的函式, 包含函式名稱都已經明確指出這個函式功能, 引數的變數名稱, 也清楚指出長寬高, 這樣的命名規則才是我們希望看到的. 必須讓變數名稱是有意義的.
2.3.2 變數名稱
而變數名稱, 通常是由英文字母, 數字, 下底線( _ ) 這3個組成. 然後要注意數字不可在變數名稱的字首. (ex. int 1a), 而若一變數是由兩個英文單字組成, 通常會將後單字的字首寫為大寫, 或者透過下底線' _ ' 區隔.
ex. firstName, first_name,
ex.
strFirstName //表示這是一個字串型別的變數
m_strFirstName //表示這是一個類別的成員(member) 且為一個字串型別的變數.
2.3.4 變數初始化規則( Variable Initalization Rules)
還有在變數的使用上, 要特別記住, 當變數宣告後一定要先給予初始化(Initalization), 要避免對未初始化的變數值做操作.
ex. int a, b;
a = b - 1; //a b都未初始化 但對b做運算則會發生執行錯誤
如果可以使用direct-initalization(直接初始化)會比使用copy-initalization(拷貝初始化)好, 因為後者容易跟賦值( = ) 搞混. 尤其在類別的成員變數初始化中, 使用direct-initalization也是比較好的.
int ival1 = 1024; // copy-initalization
int ival2(1024); //direct-initalization
2.3.5 宣告(Declarations) 和定義(Definitions)
變數的定義(Definitions)跟宣告(Declarations)在使用上有些不同.
extern int i; //宣告i, 但未定義它
int i; //宣告且定義 i
關鍵字 extern 通常會使用在類別的標頭檔中, 例如在某個類別中 宣告了變數PI, 在類別實作中, 才決定PI值的內容. 而若在extern宣告時就已經定義初始值的話, 就不可重覆定義.
extern double PI = 3.14f; //宣告並定義的變數PI
double PI = 3.14f; //會發生錯誤 重覆定義的變數PI
2.3.6 名稱的作用域(Scope)
比較明確的說法, 應該是變數的作用域. 在程式語言中, 變數會有它的生命週期, 也就是作用域(scope). 一但離開了作用域, 則變數自然不存在. 而變數基本上依作用域, 區分成Local(區域)跟Global(全域).
int g_var1 = 0; //此為全域變數
void main()
{
int var2 = 0; //此為區域變數
{
int var3 = 0; //此為區域變數
}
var3 = 1; //編譯錯誤 未宣告的識別項
}
以上例來看, g_var1 的宣告位置在函式類別外, 所以必須等程式結束時才會消失. 而var2被宣告在main()這個func內, 所以作用域就只在main()內, 而var3被宣告在一個特定的scope內, 離開了scope就會不存在, 所以var3 = 1的操作, 就會發生問題.
另外要注意的是, global變數會被local變數給覆蓋, 所以要避免在程式中使用global與local同名的變數.
string s1 = "hello"; //s1為global變數
void main()
{
string s2 = "world"; // s2為local變數
cout << s1 << " " << s2 << endl; // 印出hello world
int s1 = 999; // global的s1被local的覆蓋了
cout << s1 << " " << s2 << endl; // 印出999 world
}
結果如下:
hello world
999 world
所以為了避免這樣的情況, 如果定義了一個global變數, 就明確的在名稱上給予識別會比較好.
------------------------------------------------------------------------------------
2.4 const 飾詞(Qualifier)
既然有變數(可變動)存在, 自然也會有常數(不可變動)的數值存在.比如數學上的PI為3.14..., 不希望有人誤用或修改成不是3.14...的數值時, 就可以將PI定義為一個常數.
int PI = 3.14; //可被修改
const int PI = 3.14; //不可被修改 當試圖作變更時會發生執行錯誤
------------------------------------------------------------------------------------
2.5 References (參照, 引用, 址參)
參考跟指標都是C/C++最重要的基礎之一, 而它的概念又比較抽象與複雜, 所以可能要多花點心思在這些部分上. references(ref, 參考)比較像是賦予一個變數另一個名稱, 就像暱稱那樣. 比如小叮噹跟多拉ㄟ夢, 大多數人都知道它指的是同一個東西. 又如技安跟胖虎的關係.
void main()
{
int var1 = 1;
int var2 = var1; //這是copy assignment 拷貝賦值
var1 = 2;
std::cout << "var2 = " << var2 << endl; //會印出什麼?
int &var3 = var1;
var1 = 3;
std::cout << "var3 = " << var3 << endl; //會印出什麼?
var3 = 5;
std::cout << "var1 = " << var1 << endl; //會印出什麼?
}
印出的結果如下:
var2 = 1
var3 = 3
var1 = 5
copy assignment, 顧名思義就是複製數值或者稱做Pass by Value(傳值), 所以在上述程式碼中, var2 = var1這樣的操作, 就只是把var1的數值copy賦予給var2, 當var1之後改變時, 並不會影響var2.
而參考如一開始說明, 就像個暱稱, 背後指向的對象都是同一個, 專有名詞是Pass by Reference(傳址), 所以&var3 = var1的操作, 就是宣告var3 其實是var1的另一個名字, 當var1或者var3改變時, 兩者印出的值會同時一樣.
而背後的基礎原理就是記憶體(memory). 在C/C++這種比較複雜的語言中, 記憶體是非常重要的概念. 包函指標(pointer), 參考(References), 型別(Types) 都擺脫不了.
而參考最常用的地方, 在於函式引數.
void testFunc1(int tmp1)
{
std::cout << "tmp1's memory addr: " << &tmp1 << endl;
}
void testFunc2(const int &tmp2)
{
std::cout << "tmp2's memory addr: " << &tmp2 << endl;
//tmp2 = 3; //不允許的操作 會導致編譯過程產生error
}
void main()
{
int var1 = 1;
std::cout << "var1's memory addr: " << &var1 << endl;
testFunc1(var1);
testFunc2(var1);
}
在上例中, 透過&var1去印出變數的記憶體位址, 會是一串8個16進位數字組成的. testFunc1在引數使用上, 是重新產生一個int的型別記憶體空間並且做copy assignment的數值拷貝, 因此tmp1的記憶體位址跟var1不會相同, 兩者各自獨立.
而testFunc2的引數, 透過參考取得. 所以並不會產生新的記憶體空間配置, 執行效率也會比較快,後在記憶體位址上var1會跟tmp2相同. 但為了避免testFunc2裡面的操作改變參考的對象var1的數值, 所以可以在tmp的引數宣告前加入const(常數)的修飾詞, 當程式碼中試圖對tmp2做改變的操作, 編譯器在編譯過程中就會跳錯error錯誤訊息.
------------------------------------------------------------------------------------
2.6 typedef 的名稱
型別定義, 我們在第一章1.6 C++程式裡面的例子中曾經用過. 而typedef如同前面說過的, 就是定義某個型別的同義字, 可以當成暱稱來看.
typedef的使用規則從關鍵字typedef開始, 然後是資料型別, 最後是自定義的名稱(識別符號).
而typedef之所以被廣泛使用, 有下列3個理由:
1. 隱藏某型別的實作細節, 強調該型別的使用目的.
2. 簡化複雜的型別定義, 讓它們容易被了解.
3. 允許以多個目的使用單一型別, 而且每次使用該型別的目的都很清楚.
ex: typedef *int PINT;
記住typedef是為了簡化與增強識別使用, 千萬別定義又臭又長, 或者沒意義的名稱.
ex: typedef int AAA;
------------------------------------------------------------------------------------
2.7 列舉 (Enumerations)
在程式設計中, 常常會需要為了某些功能定義有關連的數值, 比如 一個MMORPG遊戲,
有戰士, 法師, 弓箭手3個職業, 所以玩家的屬性中就會有職業(Job)這項設定值.
ex:
Job = "Fighter"
Job = "Magician"
Job = "Archer"
然後在某個情況下需要判斷玩家的職業
if(role.job == "Figher")...
else if(role.job == "Magician")...
把字串當職業的索引寫法, 在某處因為拼錯字而無作用也不會察覺到.
且因為這樣的寫法缺乏彈性. 並且這些字串值彼此沒有程式結構上的關聯性.
所以為了改善與解決這樣的問題. 可以採用 列舉 (Enumerations, 縮寫enum).
以上例來說, 可以寫成
enum JobType
{
NOVICE = 0,
FIGHTER,
MAGICIAN,
ARCHER,
JOB_COUNT
}
enum類似define的用途, 可以用來定義一組數值, 並且讓這組數值有其關連性.
以此例來說, 透過enum這個關鍵字定義了一個名叫JobType的列舉內容.
然後將第一個值視為預設值 給予了一個NOVICE(初心者)的替代字, 並且數值為0.
依序填入戰士, 法師, 弓箭手, 最後還有一個職業數統計的替代字.
在列舉中, 若無設定(=)數值的話, 數值是連續性的.
所以在上例中, NOVICE是0, FIGHTER自然就是1, ARCHER會是3, JOB_COUNT則會是4
而列舉比較特別的, 可以不用定義這個列舉群集的名稱. 所以以上也可以修改成
enum { NOVICE, FIGHTER, MAGICIAN, ARCHER, JOB_COUNT };
上面的判斷中, 就可以寫成
if(role.job == NOVICE)...
else if(role.job == FIGHTER)...
或者
switch(role.job)
{
case NOVICE:
//do something
break;
case FIGHTER:
}
透過列舉, 各個職業的替代字都是有意義的. 並且可以減少因為拼錯字而發生的錯誤.
除了職業的應用外, 封包(package)中也常常會用到.
因為封包會定義一個數字代表一個封包功能.
所以在列舉中可能就會有這樣的定義.
enum C2S_BaseMsg
{
C2S_GAME_START = 3310, //遊戲開始
C2S_ROLE_CREATE = 4000, //創角
C2S_ROLE_SELECT = 4001, //選角
C2S_ROLE_DELETE = 4002, //刪除角色
C2S_ROLE_MOVE = 5000, //角色移動
C2S_ROLE_ATTACK = 5001, //角色攻擊
}
這樣的設計模式, 在封包設計中是很常看到的.
透過列舉提供的替代字, 可以讓程式設計師很直覺得去使用這個替代字.
然後要特別注意的地方!!!
列舉元(Enumerators)是const, 也就是常數不可改的. 在列舉定義後, 是無法透過任何程式指令去修改這些值.
當然我們也可以把列舉當成類別(type)來使用.
JobType job = NOVICE; //宣告一個JobType 型別的物件, 並且賦予它 NOVICE的內容.
job = 5; //編譯錯誤, 因為job是JobType 的型別物件, 而左右值(指等號左右邊的兩值)的型別不同無法賦予
用法的範例還有RPG的角色屬性.
//類別中定義
int str, dex, int;//角色有3種屬性 力量, 敏捷, 智力
//實作中將屬性初始化
init()
{
str = 0;
dex = 0;
init = 0;
}
這樣的寫法一樣缺乏彈性, 與不夠嚴謹. 在屬性種類擴充時就很容易發生疏忽造成的bug.
若採用enum則可以寫成
//類別中
enum { STR, DEX, INT, ATTRI_COUNT };//定義屬性的enum
class xxx
{
private:
int vAttri[ATTRI_COUNT]; //建立int的矩陣 大小為 屬性種類
}
//實作中
init()
{
for(int i=0; i<ATTRI_COUNT; i++)
vAttri[i] = 0; //透過迴圈 將屬性矩陣初始化
}
------------------------------------------------------------------------------------
2.8 Class 型別
在大多數物件導向(OO)的程式中, 類別(class)是最重要的基礎. 它讓程式設計師可以量身打造所需要的資料型別(data type). 以程式中最常用到的int, string來說都是由C++提供的基本類別.
在第一章中, 我們透過一個簡單的類別去處理書籍銷售的問題. 而在本節中將介紹說明類別的設計與實作.
Class的設計從操作開始
C++的class, 可以拆解成 介面(interface)和實作(implementation). 其中介面包含了這個類別必須提供的成員(包含變數與方法)