創作內容

0 GP

[程式設計]C++ Primer 第2章(上) - 變數和基本型別

作者:夜下月│2013-01-31 20:43:31│巴幣:0│人氣:2408
本章概要:
型別(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)

資料型別 by wiki
資料型別 by C++ Gossip
字面常量 by C++ Gossip

------------------------------------------------------------------------------------

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;

額外參考: typedef用法小結

------------------------------------------------------------------------------------

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:
                //do something
                break;
}

透過列舉, 各個職業的替代字都是有意義的. 並且可以減少因為拼錯字而發生的錯誤.

除了職業的應用外,  封包(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). 其中介面包含了這個類別必須提供的成員(包含變數與方法)
引用網址:https://home.gamer.com.tw/TrackBack.php?sn=1885129
All rights reserved. 版權所有,保留一切權利

相關創作

同標籤作品搜尋:程式設計|C++|C語言|c/c++

留言共 0 篇留言

我要留言提醒:您尚未登入,請先登入再留言

喜歡★ponderzs 可決定是否刪除您的留言,請勿發表違反站規文字。

前一篇:[程式設計]C++ Pr... 後一篇:Kamigami Eng...

追蹤私訊切換新版閱覽

作品資料夾

tyu15826大家
數碼獸守護者 12 空島區域的冒險(五)更新看更多我要大聲說昨天16:06


face基於日前微軟官方表示 Internet Explorer 不再支援新的網路標準,可能無法使用新的應用程式來呈現網站內容,在瀏覽器支援度及網站安全性的雙重考量下,為了讓巴友們有更好的使用體驗,巴哈姆特即將於 2019年9月2日 停止支援 Internet Explorer 瀏覽器的頁面呈現和功能。
屆時建議您使用下述瀏覽器來瀏覽巴哈姆特:
。Google Chrome(推薦)
。Mozilla Firefox
。Microsoft Edge(Windows10以上的作業系統版本才可使用)

face我們了解您不想看到廣告的心情⋯ 若您願意支持巴哈姆特永續經營,請將 gamer.com.tw 加入廣告阻擋工具的白名單中,謝謝 !【教學】