國立屏東大學 資訊工程學系 C程式設計入門教材 ====== - #4 變數、常數與資料型態 ====== ---- C++語言使用變數(Variable)來保存程式相關的資料內容,例如使用者所輸入的資料或是執行過程中暫時或最終的運算結果。一個變數的內容可於程式執行時視需要加以改變,但其所屬型態不能變更。C++語言也提供常數(Constant)來存放特定的資料內容,但常數的內容一經定義就不可以被改變。我們已經在前一章中,使用變數來進行了幾個簡單的程式開發,例如在BMI計算程式中的weight與height,以及在門號違約金計算程式中的contractDays與subsidy等。本章將進一步為讀者詳細說明變數與常數的意義,以及它們在C++語言中的使用方式,具體的內容包含資料型態、變數與常數的宣告、初始值的設定、以及輸入與輸出等主題。 ===== - 變數 ===== 變數(Variable)指是在程式執行時,用於儲存特定資料型態(Data Type)的「值(Value)」的記憶體空間。此處的「值」是指資料內容,可以在執行過程中視需要改變其內容,但其所屬的「資料型態」不可改變。在我們正式開始介紹變數前,讓我們先回顧一下,上一章[[cppbook:ch-ipo 語言規定所有變數在初次使用前,都必須進行變數宣告(Variable Declaration)— 包含了其變數名稱以及所屬資料型態的宣告。
本節後續將進一步提供更完整的介紹,包含變數宣告、變數的命名規則與記憶體空間等相關主題。
0.0.1 變數宣告(Variable Declaration)
程式設計小技巧:為變數宣告提供註解 另外,我們還在這三個宣告敘述以分號結束後,在後面又加上了以「//」開頭的註解,來補充說明這些變數的意義。關於註解的部份並不屬於變數宣告的語法規範,但卻是筆者建議您可以維持這樣的習慣 — 儘可能在宣告變數時,以註解提供變數的補充說明。如此一來,您所撰寫的程式將比較容易維護。
=== 分隔語法單元的「白色空白(Whitespace)」=== 還有一點要特別注意的是,在C語言的語法規則中,我們必須使用一個或多個「空白鍵」、「Tab鍵」、「Enter鍵」或混合使用它們,來將各個語法單元加以分隔。我們將這種分隔方式稱為「白色空白(Whitespace)」,又將這些用以分隔的鍵稱為「白色空白字元(Whitespace Character)」。例如在上述的例子中,我們使用了一個「空白鍵」將「float」與「weight」加以分隔1)。依照這樣的規定,以下的寫法都是正確的宣告:
float weight; // 使用一個空白鍵分隔 float height; // 使用一個Tab鍵分隔 float BMI; // 使用一個Enter鍵分隔
當然,如果您想要使用更多個分隔字元也是可以的,請參考以下的例子:
float weight; // 使用多個空白鍵分隔 float height; // 使用兩個Tab鍵分隔 float BMI; // 混合使用Enter鍵與空白鍵分隔
縮排(Indentation):程式碼排版風格
白色空白不但可以用在語法單元間的分隔,也常常應用在敘述中的第一個語法單元之前。例如以下的例子:
float weight; // 在float前使用多個空白鍵 float height; // 在float前使用一個Tab鍵
相信讀者一定會有一個疑問:「為何要在前面在白色空白呢」?其實這就是所謂的「縮排(Indentation)」,又稱為「程式碼排版風格(Coding Style)」。透過在每行程式敘述前加入適當的白色空白,將有助於程式碼的閱讀,並可反映出程式碼的結構,是專業程式設計師必備的技能之一2)。以下的兩個例子都是在第2章中的BMI計算程式,但使用不同的縮排方法:
全部切齊 | 適當地縮排 |
---|---|
#include <iostream> using namespace std; int main() { float weight; float height; float BMI; cout << "請輸入您的體重(公斤): "; cin >> weight; cout << "請輸入您的身高(公尺): "; cin >> height; BMI = weight / ( height * height ); cout << "BMI值為" << BMI << endl; return 0; } | #include <iostream> using namespace std; int main() { float weight; float height; float BMI; cout << "請輸入您的體重(公斤): "; cin >> weight; cout << "請輸入您的身高(公尺): "; cin >> height; BMI = weight / ( height * height ); cout << " BMI值為" << BMI << endl; return 0; } |
其中左側的程式碼完全沒有在每行敘述前加入白色空白,右側的部份則適當地使用白色空白將程式碼對齊,不但在閱讀上比較清爽,同時在main()函式內的程式碼透過內縮對齊也反映了它們是屬於函式內部的程式結構。
and | and_eq | asm | auto | bitand | bitor | bool | break | case | catch |
char | class | compl | const | const-cast | continue | default | delete | do | double |
dynamic_cast | else | enum | explicit | export | extern | false | float | for | friend |
goto | if | inline | int | long | mutable | namespace | new | not | not_eq |
operator | or | or_eq | private | protected | public | register | reinterpret-cast | return | short |
signed | sizeof | static | static_cast | struct | switch | template | this | throw | true |
try | typedef | typeid | typename | union | unsigned | using | virtual | void | volatile |
wchar_t | while | xor | xor_eq |
</tabcaption> </WRAP>
正確與錯誤的變數命名範例
依據前述變數命名的三項規則,以下的變數名稱使用了不在規則中允許的字元或符號(僅能使用英文大小寫字母、數字與底線),當然都是錯誤的命名:
int cert#, someone@taipei; //使用了不允許的特殊字元 int average-score; //使用了不允許的連字號(也就是減號) int miles/per-second; //使用了不允許的斜線字元與連字號 float ratio!, question? //使用了不允許的驚嘆號與問號
至於數字在使用上也必須注意(不可以用於變數名稱的開頭),以下是錯誤的例子:
float 386PC, 486PC; //使用了數字開頭 int 1stScore, 2ndScore; //使用了數字開頭
除此之外,還要注意不要使用C的關鍵字做為變數名稱,例如以下的例子都是錯誤的命名: <sxh cpp; gutter:false> int namespace; //使用了C++的關鍵字namespace float auto; //使用了C++的關鍵字auto </sxh> 看完了上述的錯誤命名的例子之後,讓我們看看一些正確的變數命名範例。以下的變數名稱僅使用了大小寫英文字母、數字與底線所組成,同時也都沒有以數字開頭,或是使用了C++的關鍵字,所以當然都是正確的: <sxh cpp; gutter:false> int A, B, C; // 使用大寫英文字母所組成的變數名稱 int x, y, z; // 使用小寫英文字母所組成的變數名稱 int Wig, xYz; // 混合使用大小寫英文字母所組成的變數名稱 int H224, i386; // 混合使用英文字母與數字所組成的變數名稱 float w_1, w_2; // 混合英文、數字與底線所組成的變數名稱 </sxh> 雖然依據變數的命名規則,這些變數名稱都是正確的,但都不是很好的命名選擇,因為這些變數名稱並不具備任何意義,無法從中得到關於變數的用途或意義的提示。雖然變數的命名必須要滿足其命名規則(因為一定要滿足,不然不能通過編譯),但是更重要是為變數賦與「有意義」的名稱;換句話說,變數名稱的做用不單是用以識別不同的變數,更重要的是要能夠傳達該變數在程式中的用途或意義。為了做到這一點,讀者們應該適當地使用大小寫的英文字母、數字與底線(underscore)為變數命名,以增進程式碼的可讀性。 === 可讀性(Readability)=== 所謂的可讀性(Readability)係指程式碼容易被理解的程度,可讀性高的程式碼,閱讀起來就像行雲流水,頃刻之間就可以完全瞭解程式的內容;相反地,可讀性低的程式碼,不但讓人難以理解,有時候甚至是原始的程式創作者自己也無法理解程式的內容。為了增進程式的可讀性,我們建議讀者視變數在程式中的用途,選擇具有意義的英文詞彙為變數命名。為了讓程式碼的可讀性提升,有時我們甚至會使用一個以上的英文單字為變數命名,此時可以適當地調整大小寫或加上底線,例如下面是正確且具有意義的變數名稱: <sxh cpp; gutter:false> float weight, height, BMI; // 分別代表體重、身高與BMI值的變數 // 我們可以使用底線連接多個英文單字,以形成更具意義的變數名稱 int student_id; // 使用底線連接student與id代表學生的學號 float student_score; // 使用底線連接student與score代表學生的成績 // 我們也可以適當的使用大小寫英文字母,取代使用底線來連接多個單字 int studentID; // 使用大小寫來區分連接起來的student與ID float studentScore; // 使用大小寫來區分連接起來的student與Score </sxh> 除此之外,也可以適當地使用數字的諧音,來精簡表示特定的英文或其它含義。請參考下面的例子: <sxh cpp; gutter:false> // 使用數字4來代替英文的for float interest4loan; // 此變數名稱表示interest for loan,意即貸款的利率 float interest4saving; // 此變數名稱表示interest for saving,意即存款的利率 float discount4group; // 此變數名稱表示discount 4 group,意即團體的折扣 float discount4kids; // 此變數名稱表示discount 4 kids,意即孩童的折扣 // 使用數字2來代替英文的to int time2destination; // 此變數名稱表示time to destination, // 意即到達目的地的時間 int amount2pay; // 此變數名稱表示amount to pay,意即應支付的金額 </sxh> === 大小寫敏感(Case Sensitive)=== 除了前述的三項變數命名的規則之外,還必須特別注意的是「C++語言是對大小寫敏感(Case Sensitive)」的程式語言,意即大小寫會被視為不同的字元,因此以下的程式碼所宣告的8個變數名稱都是正確的,而且會被C++語言視為是「不相同」的變數: <sxh cpp; gutter:false> int day, Day, dAy, daY, DAy, DaY, dAY, DAY; </sxh> 儘管您應該不會像這樣宣告一些非常容易混淆的變數名稱,但仍有可能在程式中發生一些錯誤,請參考以下的例子: <sxh cpp; gutter:false> float Weight; // 宣告一個float型態的變數Weight cin >> weight; // 使用cin取得使用者輸入時,誤將變數名稱寫為「w」eight </sxh> 在上面的程式碼片段中,出現了「寫錯變數名稱」的錯誤!不小心將「Weight」寫成了「weight」與!由於C++語言對於大小寫敏感的關係,在程式碼中的「Weight」與「weight」將會被視為是兩個不同的變數。對於編譯器而言,「Weight」是正確的變數名稱(有事先使用變數宣告敘述宣告),但是用以儲存使用者輸入的「Weight」變數,因為沒有事先的宣告,所以是不正確的使用(無法通過編譯)。像這樣的錯誤其實很容易發生,例如在程式中不小心將「x」寫成「X」、或是把「v」寫成「V」等。 要避免這樣的問題,除了小心謹慎地使用變數名稱外,最好的方式是維持一定的變數命名習慣。舉例來說,如果您固定全部使用小寫字母為變數命名,那麼在使用變數時您自然也會全部使用小寫字母,那麼將「weight」誤寫成「Weight」的機會就會大幅地降地。目前業界已經有一些通用的變數命名方式,我們將其稱之為「命名慣例(Naming Convention)」,依據這些慣例來為變數命名除了有助於提升程式的可讀性外,也能夠減少犯錯機會。 === 命名慣例(Naming Convention)=== 目前有一些用於變數命名的規則可參考,例如著名的「駝峰式命名法(CamelCase)」[2]與「匈牙利命名法(Hungarian notation)」[3]等方法。駝峰式命名法是當前主流的變數命名方法,其命名方法是直接使用英文為變數命名,其名稱即為變數的意義或用途,因此具備良好的可讀性。 採用駝峰式命名法命名方法時,直接使用英文為變數命名;若使用到兩個或兩個以上的英文單字時,每個英文單字除首字母外一律以小寫表示,且單字與單字間直接連接(不須空白),但從第二個單字開始,每個單字的首字母必須使用大寫。至於第一個單字的首字母,則依其使用大寫或小寫字母,可將駝峰式命名法再細分為「大寫式駝峰式命名法(UpperCamelCase)」與「小寫式駝峰式命名法(lowerCamelCase)」兩類。例如以下的幾個名稱皆屬於大寫式駝峰式命名法: <sxh cpp;gutter:false> Number UserInputNumber MaxNumber StudentIdentifier FulltimeStudent BestScore CourseTime CamelCase UpperCamelCase </sxh> 至於小寫式駝峰式命名法,請參考下列的例子: <sxh cpp;gutter:false> amy userName happyStory setData getUserInput lowerCamelCase </sxh> 目前大部份的C++語言程式設計師,都是採用小寫式駝峰式命名法為變數命名(意即使用有意義的英文單字來為變數命名,原則上所有單字皆使用小寫英文字母組成,但從第二個單字開始,每個單字的第一個字母必須使用大寫),本書後續的程式範例,也將繼續使用小寫式駝峰式命名法為變數命名。 === 標準識別字(Standard Identifier)=== 本節最後為讀者介紹標準識別字(Standard Identifier),雖然它們完全符合變數命名的各項規定,但仍不建議讀者使用。所謂的標準識別字是一組在特定標頭檔(Header Files)或命名空間(Namespace)中預先定義好的常數、變數或函式名稱,例如我們已經在程式中使用過的cin、cout與endl等,事實上它們是定義在std這個Namespace中的名稱,您可以試著宣告以下的變數: int cin=5; int cout=10; int endl=20; 相信我,這樣的程式碼是正確的!但如此一來,以後在您的程式碼中的cin、cout與endl,倒底是代表整數值或是其原本定義的輸入、輸出與換行的功能?像這樣的例子就充份說明了什麼是標準識別字 — 雖然不是C++語言的關鍵字,但仍不鼓勵您使用這些名稱來做為您所宣告的變數名稱,因為可能會和其原本的意義不同,對程式碼的意義帶來不必要的錯誤解讀。關於標準識別字還有一些其它的例子,包含NULL、EOF、min、max、open、close、sin、cos、pow與log等,請儘量別使用這些名稱為您的變數命名。 ==== - 變數初始值宣告 ==== 變數就是在程式執行時,用以暫時儲存特定型態的值的一塊記憶體空間;在程式執行的過程中,一個變數所儲存的值可以被改變,這也正是「變數(Variable)」一詞的由來 — vary(變化)加上able(具有…能力)。我們在宣告一個變數時,除了要指定其變數名稱以及型態外,還可以給定一個「初始值(Initial Value)」— 變數一開始所儲存的值。請先回顧以下的變數宣告敘述語法: <WRAP center round box caution 60%> ** 變數宣告敘述(Variable Declaration Statement)語法定義 ** 資料型態 變數名稱 [=初始值]?[,變數名稱 [=初始值]?]*; </WRAP> 在上述的語法規定中,每個變數除了宣告其所屬的型態以及其變數名稱外,還可以選擇性地接上「=初始值」;不論是在一個變數宣告敘述中的第一個變數宣告,或是後續其它的變數宣告都可以擁有這個選項 — 給定初始值,請參考以下的例子: <sxh cpp; gutter:false> float weight=65.5, height=1.72; // 宣告兩個變數並且都給予初始數值。 int contractDays=100; // 宣告一個變數並給予初始值。 int productID, price=0, amount=12; // 宣告三個變數並給予其中兩個變數初始值 </sxh> ==== - 變數記憶體配置 ==== 在程式碼中使用變數宣告敘述所宣告的變數,在執行時會依所宣告的資料型態在記憶體中配置適當的位置。截至目前為止,本書已經介紹了兩種資料型態,分別是浮點數float與整數int,而它們都是使用連續的4個位元組(Byte)的記憶體空間。關於資料型態的更多細節,請參考本章<fc #ff0000>3-2</fc>節。請考慮以下的變數宣告: <sxh cpp; gutter:false> float weight, height, BMI; </sxh> 此行變數宣告敘述,在程式執行時,會依據其宣告內容在記憶體中配置三個浮點數型態的計憶體空間,並將其分別命名為weight、height與BMI。後續在程式中使用這些變數時(不論是讀取或改變其值),就是對這些記憶體位置內的值進行操作。<imgref Fig_VarMemoryLayout>顯示了這三個變數可能的記憶體配置圖: <WRAP center 450px> <imgcaption Fig_VarMemoryLayout center 語言為float(浮點數)型態的變數,所配置的是連續的4個位元組的記憶體空間4),因此以figure ##左上角的weight變數為例,其所配置的記憶體空間是從「0x7ffff69a3232」開始(標示於上方)至「0x7ffff69a3235」的連續4個位元組。由於變數所配置到的記憶體位置,必須等到程式執行時才能得知,如果我們想要存取變數weight的值時,並不是在程式中指定要存取「0x7ffff69a3232至0x7ffff69a3235」這連續4個位元組的記憶體空間(因為在撰寫程式時,我們還無法得知其記憶體位址),而是以該變數的名稱,也就是weight,來進行存取。可是程式執行時,又要如何才能得知weight變數所在的記憶體位址呢?關於此點,其實是透過「符號表(Symbol Table)」來查詢的。
符號表(Symbol Table)
符號表是在程式編譯或執行時所建立的一個表格5),是為了管理在程式中用以識別的變數名稱與其所分配到的記憶體空間,其內容包含有變數的名稱、型態以及其所分配到的記憶體空間位址等資訊。以我們前述的weight、height與BMI這3個變數為例,在執行時其符號表可能會有如table 1的內容6) :
其中在記憶體位址的部份,符號表僅保存其起始位址,至於其結束的位址可以由同樣保存在符號表中的資料型態來決定。例如weight在符號表中記載了其型態為float,因此其所配置到的空間就是從符號表中所記載的0x7ffff69a3232開始,一直到0x7ffff69a3235的連續4個位元組(由於float資料型態佔用了連續4個位元組的記憶體空間)。請注意,此處所使用的記憶體位址僅供參考,其真實的位址必須在程式執行時才能得知。
Address-Of運算子
如果讀者對於某個變數在執行時,其所配置到的記憶體位址有興趣,則可以使用「位址(Address-Of)運算子,&符號」來取得變數的記憶體位址(更具體來說,是變數所配置到的起始位址)。只要在變數名稱前加上位址運算子,就可以取得其所配置的記憶體位址。請參考Example 1的addressOf.cpp程式,其中宣告了兩個變數,並使用&(Address-Of運算子)取得它們所配置到的記憶體位址:
#include <iostream> using namespace std; int main() { float weight, height; // 此處使用了一行敘述來宣告兩個變數 cout << "變數weight所配置到的記憶體位址為: " << &weight << endl; cout << "變數height所配置到的記憶體位址為: " << &height << endl; return 0; }
Example 1的addressOf.cpp之執行結果如下(注意,此結果僅供參考,實際輸出的記憶體位址將會有所差異):
變數weight所配置到的記憶體位址為: 0x7fff5104cafc 變數height所配置到的記憶體位址為: 0x7fff5104caf8
0.1 常數
除了變數之外,在C++語言的程式碼中,還可以宣告常數(Constant)。常數就如同變數一樣,都擁有名稱、型態與內容值,不過一旦給定初始值後,就不允許變更其數值內容。
0.1.1 常數宣告
C++語言的常數宣告(Constant Declaration)的語法如下:
常數宣告敘述(Constant Declaration Statement)語法定義
const 資料型態 常數名稱 = 數值 [, 常數名稱 = 數值]*;
從上面的語法可以得知,其實constant declaration(常數宣告)的語法與變數宣告非常相似,不過必須在最前面加上const這個關鍵字,並且所有常數的宣告都必須給定數值(Value)7)。在常數名稱方面,其命名規則與變數的命名規則一致,請自行參考3-1-2的說明。接下來,請參考下面的程式碼片段:
const int a=100; const int b=3,c=5; ... a=200; // 改變一個常數的值 ...
上面的程式碼正確地宣告了三個整數常數a、b與c,但在後續的程式碼中卻又改變了其中一個常數的數值!這樣會導致編譯時的錯誤,您會得到「error: read-only variable is not assignable(錯誤:唯讀的變數不可以指定數值)」的錯誤訊息,因為一個常數一但被宣告後,其值是不允許被改變的。
0.1.2 常數定義
除了使用前述的常數宣告方法外,我們還可以使用「#define」這個前置處理器指令(Preprocessor Directive)8)來定義常數,其語法如下:
常數定義(Constant Definition)語法
#define 常數名稱 數值
與多個常數可以在一個宣告中同時宣告不同,常數定義一次僅能定義一個常數,且在定義時不需要使用等號(=),也不需要在結尾處的分號(;)。依照上面的語法,我們可以定義一個常數 PI,其值為3.1415926:
#define PI 3.1415926
或是定義一個名為size的常數,其值為10:
#define size 10
其實常數定義並不是幫我們產生一個常數,而是幫我們以代換的方式,將程式碼中所出現的特定文字串組合改以指定的內容代替。因為在C++語言中,所有以井字號(#)開頭的指令,都是在編譯時由編譯器先啟動一個前置處理器(Preprocessor)來負責加以處理的。包含了我們已經使用過的「#include<標頭檔>」指令,其實是在編譯前,先將指定的標頭檔案內容載入到程式碼中。至於此處所介紹的「#define」也是一個前置處理器指令(Preprocessor Directive),同樣是由編譯器內的前置處理器負責處理,以下面的程式碼為例:
#define PI 3.1415926 int main() { int radius=5; float area; area = PI * radius * radius; … }
在編譯時,編譯器會先啟動前置處理器,依據其中第一行所定義的「#define PI 3.1425926」,掃描尋找所有在程式碼中出現PI的地方,並將其改以3.1415926進行代換;完成這個代換後,編譯器才會展開真正的編譯工作。因此,上述的程式碼經過前置處理器處理後,會將以下的程式內容送交給編譯器進行編譯的工作:
int main() { int radius=5; float area; area = 3.1415926 * radius * radius; … }
關於前置處理指令更詳細的說明,可參閱本書第X章。
0.2 資料型態
一個資料型態包含了一組特定的資料內容的集合以及一組可以對其進行的操作。例如在數學領域裡,「整數」就是一種資料型態,它是一個包含了在序列{…, -3, -2, -1, 0, 1, 2, 3, …}中的所有正整數、零以及負整數的無窮集合 ,我們可以對整數的資料內容進行包含加、減、乘、除在內的相關操作。不過要注意的是,因為受限於有限的記憶體空間,在電腦系統裡的資料型態只能是有限的集合。C++語言提供多種資料型態,包含基本內建資料型態(Primitive Built-In Data Type)與使用者自定資料型態(User-Defined Data Type)兩類。本章僅就基本內建資料型態進行說明,使用者自定資料型態請參閱本書第X、Y、X與Z章。
C++語言所提供的基本內建型態,可分為以下幾種:
- 整數型態(Integer)
- 浮點數型態(Floating Point)
- 字元型態(Character)
- 布林型態(Boolean)
- 無值型態(Valueless or Void)
我們可以視需求在程式中宣告這些型態的變數來加以使用,本節後續將針對這些型態逐一加以介紹。
0.2.1 整數型態
顧名思義,整數型態(Integer type)就是用以表示整數的資料型態。在C++語言中的整數型態,是以Integer的前三個字母「int」表示。事實上,我們已經在本書中使用過這個int整數型態,在這個小節中我們將提供更完整的說明。請先回顧我們在第2章所介紹過的Example 5(請參考2-X頁)的compensaton-1.cpp程式,以下的程式碼宣告了一些整數型態的變數:
int contractDays; // 合約總日數 int contractRemainingDays; // 合約剩餘日數 int monthlyFeeDiscount; // 每月月租費優惠金額 int subsidy; // 手機補貼款 int compensation; // 違約金
這些使用int整數型態所宣告的變數,在程式執行的過程中會佔用連續的4個位元組(Byte)的記憶體空間,也就是32個位元(Bit) 。雖然int變數的值(Value)在程式執行的過程中可以有所變化,但其值必須符合int整數型態的範圍(Range)。
對於一個資料型態而言,其所謂的範圍係指一個該型態變數之數值的上限與下限,換句話說就是該型態的變數所能表達的最大值與最小值,可由該型態所佔用的記憶體大小加以計算。以4個位元組的int整數為例,其範圍是由在記憶體中連續的32個位元的排列組合所決定的,其中最左邊的位元代表正負號,稱為符號位元(Sign Bit),以0代表非負(Non-Negative)的整數,也就是正整數或0; 1則代表負整數(Negative)。此外,讀者必須特別注意的是,不論是正整數或負整數,其值一律以2補數(2’s Complement)表示 。依據2補數的運算方式,32位元的int整數其可表達的最大正整數在記憶體中的內容為:
0111 1111 1111 1111 1111 1111 1111 1111
其值為+2,147,483,647,至於最小的整數則為:
1000 0000 0000 0000 0000 0000 0000 0000
其值-2,147,483,648。因此一個int整數型態的變數,其值是介於其最大值2,147,483,647與最小值-2,147,483,648之間。