基礎資料型態與四則運算式

基礎資料型態

C/C++提供下表的5種基礎的資料型態,用來裝載這5種數值範圍的數值,在C/C++語言當中可以下表中的保留字來宣告這些型態的變數(variable),下表並且列出了各種資料型態下每個變數會佔用的記憶體空間。在 C/C++ 語言當中有所謂像 int 或 char 這類的保留字(或稱關鍵字),如此一來編譯器在剖析原始碼的時候才知道這是在執行某些動作(例如宣告變數或是控制程式流程),而不會把它誤認為變數或函式名稱,理所當然地,你在寫程式的時候,所有的變數與函式名稱必須避開這些保留字。

  保留字 佔用記憶體 數值範圍
整數 int 4 bytes -2,147,483,648 ~ 2,147,483,64
字元 char 1 byte -128∼127
浮點數 float 4 bytes ±3.4×10-38 ~ ±3.4×1038
(小數點下有效位數約 7位)
倍精確度浮點數 double 4 bytes ±1.7×10-308 ~ ±1.7×10308
(小數點下有效位數 15位)
布林數 bool 8 bytes true 或 false

前面提到 C/C++ 是與軟硬體原理高度相關的程式語言,以這些保留字宣告的變數,它們在記憶體中就真的佔用1∼8 bytes不等的空間,因此 C/C++ 編譯器在遇到這些保留字的時候,就真的會在記憶體中保留指定長度的空間。在這些資料型態當中除了字元、浮點數、布林數固定是這些長度以外,整數可以透過 short 和 long 修飾詞來改變它的儲存長度,除此之外,還可以藉由 unsigned 修飾詞來指定要把所有的 bytes 都用來儲存正整數。

  宣告方法 記憶體 數值範圍
普通整數 int 4 bytes -2,147,483,648 ~ 2,147,483,647
短整數 short 2 bytes -32,768 ~ 32767
正短整數(word) unsigned short int 2 bytes 0 ~ 65,535
正整數(dword)
 
unsigned int 4 bytes 0 ~ 4,294,967,295

為甚麼沒有介紹 long 呢?long int 會把整數變成 8 bytes 嗎?其實 long 修飾詞是舊時代的產物,其實在很多年以前,C/C++ 語言中的 int 是 2 bytes的,那時候並沒有 short int 這種東西,反而有 long int 來將 int 由 2 bytes 擴充到 4 bytes,至少在現在 Win32 的 C/C++ 編譯器之下,int 都是 4 bytes,而 long int 和 int 則是完全相等的,換句話說 long 修飾詞等於是完全沒有效果。

除了 short 修飾詞以外,unsigned 修飾詞雖然不改變變數所佔的記憶體空間,但卻改變它的儲存方式,使得數值範圍向正數移動,其中 2 bytes 的正短整數(unsigned short int)又被稱作一個 word,而 4 bytes 的正整數(unsigned int)又被稱作是一個 double word。

除了整數以外,char 原本的數值範圍是 -128∼127 也可以藉由 unsigned 改變為 0∼255。理所當然地,short, long, 以及 unsigned 也是 C/C++ 語言的保留字,除此之外,變數還有兩個保留字修飾詞 const 和 static 可使用,這兩個保留字不改變變數的長度也不改變數值範圍,而是限制它們在程式中被使用的方式。宣告變數前加上 const 可以令它成為常數(constant),如下例所示,之後的程式碼裡如果寫 a = 50; 會出現編譯錯誤 ,因為常數是不允許重新賦予數值的。

const unsigned int a = 100;

另外一個修飾詞 static 學問太大了,以後會有專篇介紹。

宣告變數

變數(variable)從程式的角度來看,就像是數學式子中的代數,就像 a = b + c 裡面,a, b, c 都是變數。然而基於前述的知識,你應該已經知道變數代表的意義是儲存數值的記憶體空間,事實上,編譯器(compiler)在編譯程式的時候,會把所有的變數名稱替換成儲存這些數值的記憶體位址(address)給機械碼。無論如何,在你的程式裡面,變數一定要經過宣告才能使用,變數一旦宣告以後就佔有記憶體空間,變數名稱可以由英數字或底線組成,但是首字不可為數字。變數的宣告語法為:

型別 變數名稱;  或  型別 變數名稱 = 初始值;

變數在宣告的時候可以給予初始值也可以不給,請見下面的例子,修改上一篇的範例 HelloWorld.cpp,藍色字的部分都是不同型別的變數宣告。

#include <stdio.h>

void main(void)
{
    int a;
    int b = 1;
    float x = 0.1f;
    double y = 0.1;
    bool q = true;
    printf("Hello World");
}

須注意的是變數名稱是區分大小寫的,例如 int b = 1; 和 int B = 1; 會被視為兩個不同的變數,這點和 Basic 語言不一樣。變數的命名和宣告像這樣的例子就沒有問題了,另一個議題是,程式碼如果有 1, 1.0, 1.0f 三個數字,編譯器如何知道它們應該是整數、倍精確度浮點數(double)、單精確度浮點數(float)呢?在上面的例子裡面,指定初始值的時候,數值如果沒有小數點就會被當成整數,所以如果要指定數值 1 給浮點數變數,應該要寫成 1.0f,否則編譯器會以為是一個整數1要轉換成浮點數格式。浮點數的數值必有小數點,而0.1f 與 0.1 前者會被編譯器當成單精確度浮點數值(float),因為數字尾端有個 f。此外,布林變數(bool)只能有 true 和 false 兩種值。

如前所述,宣告變數的時候,修飾詞直接加在型別之前即可,如下面的例子中的紅字所示:

#include <stdio.h>

void main(void)
{
    unsigned int a;
    short int b = 1;
    float x = 0.1f;
    double y = 0.1;
    bool q = true;
    printf("Hello World");
}

在C/C++語言中有一個保留字 sizeof( ) 可以用來取得型別在記憶體中所佔的 bytes,例如 int a = sizeof(float); 則 a 的值為 4,這個保留字用在這些基礎資料型態或許看不出效用,因為以上的基本資料型態我們都明確地知道它們在記憶體中所佔的長度,但是 sizeof( ) 的威力在於也可以用於 struct 或 class 這種多欄位的複合式自訂資料型態,sizeof( ) 主要的用途在於計算配置記憶體所需的空間,例如, 配置一段容納 100 個正短整數記憶體空間,所需的記憶體就是 sizeof (unsigned short int) * 100 bytes。

接下來讓我們看看下面這段程式碼和上面的程式碼有何不同?下面的程式碼將變數宣告寫在 main( ) 函式的外面(或者該說不寫在任何函式的裡面),這樣的變數就是全域變數(global variable),全域變數可以從這份程式碼的任何地方被使用,換句話說可以在任何一個函式裡面使用這些變數。反過來說,像上面例子裡在某個函式 { } 範圍裡宣告的變數則稱為區域變數(local variable),這些變數只能在 { } 範圍裡使用,如果有兩個函式各自在它們的 { } 範圍裡宣告了同名的變數也不要緊,並不會相互牴觸。

#include <stdio.h>
int a;
int b = 1;
float x = 0.1f;
double y = 0.1;
bool q = true;

void main(void)
{
    printf("Hello World");
}

C/C++ 的述句

上一篇提到的,在以上的例子裡,每一句以 ; 結尾的句子都是一句 C/C++ 語言的述句(statement),表示程式的一個動作,如上所述地,述句中所有的所有的變數, 保留字(例如 int, float…)一律區分大小寫,而且述句必定以 ; 號結尾,意思是程式碼中寫成

   a = b + c;  x = 1;

   a = b + c;
  x = 1;


是完全相同的,簡單地來介紹起來,述句大概有五種:

宣告述句

格式是「型別 變數名稱;」或「型別 變數名稱 = 初始值;」
也就是前面提到宣告變數的方法,例如 int a = 1; 就是一句宣告述句。

呼叫函數述句

呼叫某個函式,例如前面提到的 printf("Hello World”);,其實呼叫函數和運算都可以看作同類的述句,例如 float y = sin(a) * x; 就是混合了函數呼叫和四則運算的述句,像 sin( ) 這個三角函數會傳回 a 的正弦值,就可以當作是運算式的一部份。

運算 / 賦值述句

例如 a = b + c; 這類型的述句一定會有個 = 號,表示要把右邊的運算結果儲存到左邊的變數,亦或者說,要以右邊的運算結果來賦予左邊的值。在 = 號右邊的部份稱為 right value,必定是個變數以承接右邊的運算結果,在 = 號的左邊的部份則稱為 left value,可以是另一個變數、數值、或一段運算。

流程控制

例如 return; 這些都是 C/C++ 的保留字,用來控制程式的執行流程。

複合述句 (compound statement)

只要能擺得下一行述句的地方,都能夠以 { } 內擺下多行述句。

C/C++ 的註解

所謂註解(comment)就是原始碼當中完全無效用的文字,這些文字只是說明性質,方便人類閱讀這些程式碼,編譯器遇到這些文字則是會完全忽略掉,C/C++中的註解有兩種,一種是單行註解,一種是區段註解:

單行註解: // … 到行尾

例如: int a = 1;  // 把 a 設為 1

區段註解: /* …註解內容(可以多行) */

例如: int a = 1;  /* 把 a 設為 1 */

區段註解還有一種常見的另類用法如下例,可以令一整段程式碼失效,方便程式設計師在開發過程中選擇性地使用或停用一段程式碼。

   int a;
   int b = 1;
/* float x = 0.1f;
   double y = 0.1;
   bool q = true; */

C/C++ 的四則運算

其實 C/C++ 的四則運算和一般常見的數學式子一樣,有正負號、先乘除後加減,跨號內優先運算的這些性質,運算後的結果如上所述地,以 = 號儲存到左邊的變數,四則運算式用到的這些加減乘除的符號稱為運算子(operator),

一元運算子: -
把變數或數值變號(正變負, 負變正),例如 -a

二元運算子: +, -, *, /, %
放在兩個變數或數值中間,例如: a * x + b
% 是取餘數,例如 a = 19 % 2 會得到 a 等於 1。

優先順序: 先乘除後加減, 除非有 ( )
放在 ( ) 內的會優先計算,例如: (a + b) * x

賦值運算子: =
把右邊的結果存入左邊變數當中,如 y = a * x + b;

這裡舉一個修改自 HelloWorld 的例子 HelloWorld2.cpp 來說明:

#include <stdio.h>

void main(void)
{
    int a;                        // 宣告變數 a,沒有初始值
    int b = 1;                    // 宣告變數 b,初始值 1
    a = b + 2;                    // 計算 b+2 並儲存到 a
    printf("Hello World %d", a);  // 利用 printf( )顯示 a 的值
}

執行的結果會是顯示 Hello Word 3。

在前面提到,變數宣告時的型別和修飾詞決定了該變數儲存數值時所用的記憶體長度與格式,那如果述句在不同型態的變數之間指定數值,不管是在宣告另一個變數時,或是在四則運算的過程中,會發生甚麼事情?例如下列這些情況:

int a = 5;
unsigned int b = a;  // 整數賦值給正整數

int c = 5;
short int d = c;     // 整數賦值給短整數

float x = 1.2f;
int y = x;           // 浮點數賦值給正整數

其實不同型別的變數間賦值就必須做型別轉換(casting)的動作,也就是轉換儲存格式、擴充或截斷(truncate)精確度的行為,差別只是隱性型別轉換(編譯器自動做)與顯性型別轉換(程式碼中指定),其實上面的例子裡的三個賦值述句當中,編譯器都做了隱性型別轉換。

隱性型別轉換

當述句的等號(=)兩邊的變數型別(儲存格式)、編譯器判斷是它能夠處理的轉換的時候,它就會進行隱性型別轉換,遇到無法轉換的述句,就會回報編譯錯誤,而有些可以轉換但是會造成資料流失的狀況,則會回報一些警告訊息:

適用: 由精確度較低到精確度較高的型別,例如短整數賦值給長整數,或是整數賦值給浮點數。
產生警告: 由精確度較高到精確度較低的型別,例如浮點數賦值給整數,會流失小數點以下的資料。
沒有警告,但資料流失: 右邊的數值範圍超過左邊,例如長整數賦值給短整數。

顯性型別轉換

而顯性型別轉換就是不管編譯器的判斷,也稱為強制型別轉換,以程式碼要求、不顧資料儲存格式差異與資料流失也要進行的轉換,方法是在等號(=)的右邊以 (型別) 註明欲轉換的型別,例如上例中的轉換可以寫成:

float x = 1.2f;
int y = (int) x;     // 浮點數賦值給正整數

在這個例子裡面 y 的值會是 1,留失了 x 小數點以下的部份,在上例中雖然 x 是個浮點數,但是以程式碼要求要轉換成整數,在這種狀況下雖然 x 儲存到 y 會流失精確度,但由於這是程式碼中明確要求的轉換,編譯器將不會產生任何的警告訊息,接下來看一個更完整的例子 HelloWorld3.cpp:

#include <stdio.h>

void main(void)
{
    // 低精確度型別到高精確度型別,編譯器隱性型別轉換,不會有警告或錯誤:
    short int a = 1;
    int b = a; // 從2 byte 的短整數a 指定到4 byte 的整數b。

    // 同樣精確度型別在有號和無號之間轉換,編譯器隱性型別轉換,不會有警告或錯誤:
    unsigned int c = b; // 從4 byte 的整數b 指定到4 byte 的正整數b。

    // 高精確度數值到低精確度變數,編譯器發出警告!
    float x = 1.3f;
    a = x;

    // 使用顯性型別轉換,沒有警告,但是a 是1,浮點數的0.3 流失了:
    a = (short int)x;

    // 從4 byte 整數d 到2 byte 整數a,結果a 的數值是1,流失較高的2 bytes:
    int d = 65537;
    a = d;
    printf("%d", a);
}

編譯這個例子的時候,輸出視窗會在 a=x; 這一行回報警告訊息「helloword3.cpp(14) : warning C4244: '=' : 將'float' 轉換為'short',由於型別不同,可能導致資料遺失」。

最後要介紹的,C/C++ 語言的四則運算式除了寫成上述的數學式子以外,還有些更簡短的寫法,如下表:

運算子 範例 等同效果
+= a += b; a = a + b;
-= a -= b; a = a - b;
*= a *= b; a = a * b;
/= a /= b; a = a / b;
++ ++i; 或 i++; i = i + 1;
-- --i; 或 i--; i = i - 1;

為甚麼要有這一套運算子,只是為了讓程式設計師少打幾個字嗎?當然不是,本課程中一再提到的,C/C++ 語言的一舉一動都有它的軟硬體目的,如果運算式的結果是要儲存回到運算式中的變數本身,採用這一套寫法,編譯器可以編譯成更有效率的機械碼,所以請盡量地使用。以 ++i; 為例,CPU 的指令集當中本來就有累加(increase)的機械碼,所以寫成 ++i; 當然比寫成 i=i+1; 有效率的多。

本篇重點回顧