講義二

常用資料型態簡介與分類

  • 物件的『型態(type)』是指描述該物件的『行為』和『狀態』所成的集合。

  • C++ 是一種『強型別語言(strong typed language)』,意指:每個物件的資料型態都必須預先定義。

  • 基本型態:

    • 數值型態:

      • 整數

        • short (short int)

        • int

        • long (long int)

      • 浮點數

        • float

        • double

    • 字元型態(char

    • void空型態

    • bool型態:truefalse

  • 陣列(array)

  • 字串(string

  • 複合型態

    • 指標(pointer)

    • 參考(reference)

  • 使用者定義型態

    • 結構(struct

    • 聯合(union

    • 列舉(enum

常用資料型態佔記憶體大小

資料型態 32位元系統(位元組 byte) 64位元系統(位元組 byte)
char 1 1
short 2 2
int 4 4
float 4 4
double 8 8
long 4 8
long long(C++ 11) 8 8
  • 若無法確認某個變數或資料型態所佔用的記憶體大小,可使用sizeof運算子求得:

    int a;
    printf("a變數佔用的記憶體大小為:%d 位元組(byte)\n", sizeof(a));
    /*
     * a變數佔用的記憶體大小為: 4 位元組(byte)
     */
    
    char x = 'H';
    printf("x變數佔用的記憶體大小為:%d 位元組(byte)\n", sizeof x);      // C 寫法
    std::cout << "x變數佔用的記憶體大小為:" << sizeof(x) << " 位元組(byte)" << std::endl;   //  C++ 寫法

常用資料型態數值範圍

資料型態 最小值 最大值 所佔位元組(byte)
char -128 127 1
short -32767 32768 2
unsigned short 0 65536 2
int -2147483648 2147483647 4
unsigned int 0 4294967295 4
long -2147483648 2147483647 4
long long -9223372036854775807 9223372036854775808 8
unsigned long long 0 18446744073709551615 8
  • 同一條表達式切勿混用 signedunsigned 型態:因為 signed 型態會自動轉換為 unsigned 型態

  • 試試看下列練習:

    unsigned u = 10, u2 = 42;
    std::cout << u2 - u << std::endl;
    std::cout << u - u2 << std::endl;
    int i = 10, i2 = 42;
    std::cout << i2 - i << std::endl;
    std::cout << i - i2 << std::endl;
    std::cout << i - u << std::endl;
    std::cout << u - i << std::endl;
    int firstNumber;
    std::cout << "Please enter your number:" << std::endl;
    std::cin >> firstNumber;          // std::cin為 stream extraction operator,從鍵盤輸入獲得某值。
    std::cout << "Your number is " << firstNumber << "." << std::endl;
    int r = std::pow(2, 10);        // 計算2的10次方
    std::cout << "2^10 = " << r << std::endl;
    std::cout << "pi = " << M_PI << std::endl;
    std::cout << "e = " << M_E << std::endl;

常數

常數
  • 常數又稱常量。常數(constant)的值是無法被更改的。

  • 而 C++ 的常數多為字面值(literal)。

  • 程式中的字面值常數常被用來『初始化』變數、給變數『賦值』、參與『表達式』計算等。

常用的常數有:

  • 整數常數:如 100

    • 十進位:如123

      int abc = 100;    // 定義 abc 為型態 int 的變數並初始化,值為100。
      short def = 200;  // 定義 def 為型態 short 的變數並初始化,值為200。
      def = 300;        // 為賦值語句。此行作用為把新值300賦值給def變數。
      
      // C++ 14允許使用單引號作為數字的分隔符:
      long d = 6'546'1000;
      std::cout << d << std::endl;
    • 二進位:從 C++ 14 開始,可使用 0b0B 開頭表示二進位字面值。

      int b = 0b11111010;  // int b = 250;
    • 八進位:以0開頭的數字。

      int abc;
      abc = 012;
      printf("012的十進位數字為:%d\n", abc);
      /*
       * 012的十進位數字為:10
       */
    • 十六進位:以0x開頭的數字。

      int abc;
      abc = 0x12;
      printf("0x12的十進位數字為:%d\n", abc);
      /*
       * 0x12的十進位數字為:18
       */
    • 特殊用法:

      • 整數常數加一個字母 Uu,表示該常數以unsigned無符號整數儲存,相當於 unsigned int

      • 整數常數加一個字母 Ll,表示該常數以長整數儲存,相當於 long

      • 整數常數加一個字母 Ff,表示該常數以浮點數儲存,相當於 float

  • 浮點數(數值)常數:如 1.23

    • 十進位表示形式:0.1233.14159

    • 指數表示形式:如 168E4,等價於 168×10000。E 也可以為小寫 e

    • float

      • 一般佔 4 byte,取值範圍 1.17549e-038 ~ 3.40282e038。

      • 單精度變數:提供7為有效數字,隨機器而定。

    • double

      • 佔 8 byte,取值範圍 2.22507e-308 ~ 1.79769e308。

      • 雙精度變數:提供15 ~ 16位有效數字,隨機器而定。

        float af = 12.34567291234987654321;
        double ad = 12.34567291234987654321;
        printf("af的值是%f\n", af);
        printf("ad的值是%f\n", af);         // %f 格式符:專門用來顯示浮點數
        printf("af的值是%.20f\n", af);      // %.20f顯示小數點後20位有效數字
        std::cout.setf(std::ios_base::showpos);
            double pi = M_PI;
            std::cout << "pi is " << std::right << std::setw(20) << std::setprecision(16) << 10 * pi << std::endl;
            std::cout << "pi is " << std::setw(20) << std::right << std::setprecision(16) << 100.0 << std::endl;
            std::cout << std::boolalpha << (1 < 2) << std::endl;
  • 字元常數:用一對單引號('')包含起來,如 'A''$'

    char c1, c2, c3;   // 定義字元變數
    c1 = 'a';
    c2 = 'b';
    c3 = '\'';          // 末尾不是雙引號,是兩個單引號。\' 是跳脫字元(escape character),為單個字元。
    • 將一個字元常數放到字元變數中,實際上並非將字元本身放到字元變數所屬的記憶體當中,而是把字元對應的ASCII碼(範圍介於 0 ~ 127之間的整數數字,如字元 a 對應的數字為 97)存放到記憶體中。

      char c1, c2;
      c1 = 97;                                 // 相當於 c1 = 'a';
      c2 = 98;                                 // 相當於 c2 = 'b';
      printf("c1 = %c, c2 = %c\n", c1, c2);    // c1 = a, c2 = b
      printf("c1 = %d, c2 = %d\n", c1, c2);    // c1 = 97, c2 = 98;
      c2 = c2 + 4;
      printf("c1 = %c, c2 = %c\n", c1, c2);    // c1 = a, c2 = f
      printf("c1 = %d, c2 = %d\n", c1, c2);    // c1 = 97, c2 = 102;
  • 字串常數:

    • 字串常數是用一對雙引號("")包含起來的一串字元(可以是一個字元或多個字元)。如 "Hello World"

    • 'a' 是字元常數、"a" 是字串常數。

    • 'a' 在記憶體中佔1個byte,"a" 則在記憶體佔2個byte。而 "a" 最後一個字元為 '\0',由系統自動加上去的。

    • '\0' 是 C 和 C++ 中的『空字元』(Null字元或零字元),又稱『結束字元』

      • 代表 ASCII 碼為 0 的字元,通常用來表示字串的结束,特别是在以『空字元』结尾的 『C 字串(字元陣列)』中。
    • 『空格(space)字元』(' ')與上述『空字元』不一樣,『空格(space)字元』的 ASCII碼為 32 的字元。(又與『空字串』不同:""

  • 布林常數:

    • 布林型態資料其字面值只有兩個:truefalse。分別代表邏輯為真與邏輯為假。

    • 當其他型態的數據轉換為布林型態:

      bool b;              // 定義bool變數
      b = true;            // 對變數賦值
      bool c = false;      // 定義並初始化一個變數
      bool b = -123;       // true
      b = 0;               // false
      b = '\0';            // false
      b = 'a';             // true
      b = 0;               // false
      b = 1.2;             // true        

變數

  • 又稱『變量』,顧名思義,變數(variable)的值是可以被修改的。

  • 變數就是擁有名稱(name)的一塊記憶體儲存空間(又稱『內存』,用來儲存資料)。

  • 含下列資訊:

    • 資料型態。

    • 記憶體位址(指標值)。

    • 儲存值。

變數宣告(declaration)
  • 宣告是告訴編譯器某個名稱的存在,但不會分配記憶體或初始化變數。

  • 變數宣告可以在程式的任何位置,但必須在該變數使用前進行宣告。

  • 它僅僅是為了讓編譯器知道某個標識符(如函數名稱、變數名稱或類別名稱)的存在,以便在稍後的程式碼中使用它。

  • 變數宣告規定了變數型態與名稱。這一點與變數定義相同。

  • 除此之外,『變數定義還多要求儲存空間,也會為此變數設定初始值』。

  • 任何顯式初始化的宣告即為定義。

  • 任何想宣告一個變數而非定義它,就在變數名字前面添加關鍵字 extern

  • C++ 支援『分離編譯(separate compilation)』機制,允許將程式分割為多個原始檔(以利程式碼的組織架構整理),故每個原始檔可被獨立編譯。故:需要有再多個原始檔之間共享程式碼的機制。

  • 為了支援分離編譯,C++ 將『宣告』與『定義』區分出來。

  • 『宣告』使得『名稱』被程式所知:知道型態與名稱。

  • 『定義』則創建出與『名稱』產生關聯的物件實體,且發生初始化,故會佔用記憶體空間。

extern int i;     // 宣告i,而非定義i
int j;            // 宣告並定義j,雖然沒有顯性初始化,但發生『預設初始化』。
標識符(identifier)
  • 在程式語言中用來識別變數、函數、類別、物件、模組等各種程式實體的名稱。

  • 標識符通常是由字母、數字和下劃線組成,並且需要遵循特定的語法和命名規則,以確保它們在程式碼中能夠被正確地識別和使用。

  • 標識符的命名規則如下:

    1. 標識符可以包含字母(區分大小寫)、數字和下劃線(_)。

    2. 標識符的第一個字元必須是字母或下劃線。

    3. 標識符不能包含空格或特殊字元(如 !@#$ 等)。

    4. C++ 語言對變數名稱長度沒有限制,但名稱不應過長,以易記、意義明確為原則。

變數定義(definition)
  • 型態指定符(type specifier),後面接著一個或多個變數名所組成的列表,其中變數名以逗號(,)分隔,最後以分號(;)結束。
  • 定義不僅宣告了名稱的存在,還為它分配了記憶體並初始化變數。
  • 當定義一個變數或函數時,它會佔用實際的記憶體空間,並賦予它初值。
  • 無論開發者是否指定初始值,變數在定義後都會有一個初始值。
  • 若未指定初值,而變數之值為未定值,此值由編譯器自行決定,且多為無意義之值,容易產生錯誤。
  • 變數必須遵循『先定義後使用』的原則。
  • 變數只能被定義一次,但可被多次宣告。
  • 故在程式碼中,宣告通常出現在『標頭檔』中,而定義則出現在『原始檔』中。這種區別在 C++ 中很重要,特別用在避免重複定義的情況。
int sum = 0, value,            // sum、value 與 number都是 int  
    number = 0;                // 其中,sum 和 number 設定初值為 0
std::string book("西遊記");    // book透過 string 字面值初始化。
std::string name = "NCCU";
物件(object)
  • 物件是指一塊能儲存資料數據並具有某種型態的『記憶體空間』

  • 具有名字的物件則為變數(故物件不一定有名稱)

初始化(initialization)
  • 當物件在建立時獲得一個特定值,則稱該物件被初始化(initialized)。

  • 初始化『非』賦值。

  • 變數可使用已存在的變數進行初始化(放在初始化時等號( =)的右邊)。

  • 『賦值』的含義是指將物件當前的值給移除,而以新值來做替代。故賦值可用來修改變數值。

  • 初始化只會發生一次,即發生在變數定義時;而賦值則可以進行多次。

int a = 100;      // 初始化。
int b = a;        // 初始化:也可以拿已經存在的變數a來初始化變數b
列表初始化(list initialization)
  • C++ 語言定義初始化好幾種方式。

  • C++ 11 新標準定義了『列表初始化』:{}

  • 可避免數值發生部分值遺失的狀況

int x = 0;
int x = {0};
int x{0}
int x(0);
/*
 * 
 */
double pi = 3.1415926;
int a{pi}, b{pi};       // 錯誤,型態轉換未執行
int c(pi), d(pi);       // 正確,發生型態轉換,且遺失部分值。
預設初始化
  • 若定義變數時沒有指定初始值,則該變數發生預設初始化

  • 定義於任何函數體之外的內建型態變數被預設初始化為 0。

  • 定義於任何函數體之內的內建型態變數將不被初始化(uninitialized)

  • 而未初始化之變數的值為『未定義』。若試圖複製或存取該變數將引發錯誤。

  • 建議初始化每一個內建型態的變數。

變數與記憶體(內存)的關係

  • 變數本質上就是一塊記憶體空間(內存)。

  • 記憶體:記憶體位址(address)、記憶體大小屬性。

變數的位址(指標值)

  • 電腦的記憶體是按照位元組進行編碼。

  • 記憶體中一個位元組的編碼就是這個位元組的位址。

  • 系統可按照記憶體位址對記憶體進行存取操作(&運算子)。

  • C++ 程式定義一個變數即佔用一塊記憶體空間,而變數名為指向這塊記憶體空間的『首位址』,變數型態則是決定這塊記憶體空間的長度(大小)。

計算資料所佔位元組大小

  • sizeof 運算子回傳型態或數據的『位元組』大小。

  • sizeof 關鍵字後面可帶括號(()),也可不帶括號。

  • 括號中可以為『型態名稱』,也可以是『變數名稱』。

    #include <iostream>
    using namespace std;
    
    int main() {
        cout << "int的大小:" << sizeof(int) << " 位元組" << endl;
        cout << "double的大小:" << sizeof(double) << " 位元組" << endl;
        cout << "char的大小:" << sizeof(char) << " 位元組" << endl;
        return 0;
    }
    #include <iostream>
    using namespace std;
    
    int main() {
        int num = 42;
        double pi = 3.14159;
    
        cout << "num的大小:" << sizeof(num) << " 位元組" << endl;
        cout << "pi的大小:" << sizeof(pi) << " 位元組" << endl;
    
        return 0;
    }

const 變數

  • 使用關鍵字 -const 限定符(qualifier)-定義一個不能被修改的變數。

  • const 物件一但創建後其值就不能被修改,const 物件必須『初始化』

    const int i = 42;
    // const int k;              // 錯誤,k是一個未初始化的常數。
    const int ci = i;             // 正確,i的值被複製並賦值給ci。
    int j = ci;                  // 正確,ci的值被複製並賦值給j。
  • 上例,儘管 ci 是整數常數,但無論如何 ci 中的值還是一個整數。

  • ciconst 的特性只有在執行改變 ci 值的操作時才會產生作用。

  • 當進行編譯時,編譯器會在編譯過程中把用該變數之處替換為對應值。故需要對 const 變數進行初始化。

  • 預設情況下,const 物件只有在它所在的原始檔案有效。故可在不同原始檔案定義相同名稱的 const 變數。

  • 若想在不同原始碼檔案間共享 const 物件,必須在變數的定義之前加入 extern 關鍵字。

複合型態-參考與指標

  • 複合型態(compound type)是指『基於其他型態定義』的型態。

參考

  • 參考(reference)為替某物件取的『別名』

  • 利用 & 來定義參考。

  • 定義參考時,程式把參考和他的初始值『綁定(binding)』在一起。

  • 『參考本身並不是物件』,它只是為一個已經存在的物件所取的另一個名稱(別名)

  • 參考(左參考)只能『綁定』在物件上,無法與字面值綁定在一起。

  • 參考的型態必須與所綁定的物件型態一致(強型別)。

  • 故無法定義出『參考的參考』

    int x = 10;
    int &ref1 = x;                              // 正確。
    int &ref2 = ref1;                           // 正確,ref2綁定了x。
    // int& &ref4 = ref1;                          // 錯誤,無法定義『參考的參考』。
    int x = 100;
    int *ptr = &x;
    int **ptr2 = &ptr;                          // 因為指標變數ptr是物件,故可定義『指標的指標』:ptr2指標變數儲存ptr指標變數的位址
    
    std::cout << "x = " << x << std::endl;
    std::cout << "ptr = " << ptr << std::endl;
    std::cout << "*ptr = " << *ptr << std::endl;       
    
    std::cout << "ptr2 = " << ptr2 << std::endl;
    std::cout << "*ptr2 = " << *ptr2 << std::endl;
    std::cout << "**ptr2 = " << **ptr2 << std::endl;
    
    // x = 100
    // ptr = 0x16bb16b48
    // *ptr = 100
    // ptr2 = 0x16bb16b40
    // *ptr2 = 0x16bb16b48
    // **ptr2 = 100
  • 提供對另一個物件的『間接』存取。

    int ival = 100;
    int &refVal = ival;             // refVal指向ival(是 ival的另一個名字)
    // int &refVal2;                // 錯誤,參考必須初始化。
    int &refVal3 = refVal;          // refVal3綁定了refVal綁定的物件上(ival)。
    int i = refVal;                 // i被初始化refVal的值。
  • 允許在同一個語句中定義的多個參考,但每個參考標識符都必須以符號 & 開頭。

    int i = 1024, i2 = 2048;       // i與i2都是int 
    int &r = i, r2 = i2;           // r是一個參考,與i綁定在一起。r2是int變數。 
    int i3 = 1024, &ri = i3;       // i3是int,ri是參考,與i3綁定在一起。
    int &r3 = i3, &r4 = i2;        // r3和r4都是參考。 
  • 『參考』為『指標』更安全、更便利的替代版本。

指標

  • 變數的『指標值』為變數的『位址』

  • 指標(pointer)為 『指向』(透過變數的位址) 另外一個型態變數的複合型態。

  • 透過 * 來定義指標變數。

  • 『指標變數』本身就為『物件』,故可對指標變數進行複製與賦值。

  • 不一定要在定義時給定初始值(若未定義初始值,該指標變數具未定義之值。建議初始化為『空指標』)。

  • 對指標變數賦值,就是讓它存放一個新的位址值(指標值),從而指向一個新的物件。

    • 注意:賦值永遠改變的等號左邊的物件。
  • 指標變數與參考皆提供『間接』存取物件的管道

    int i = 7, j = 9;
    float k = 12.6f;
    int *mypointer1, *mypointer2;    // 定義兩個指標變數,這兩個指標變數都是指向整數型態,
                                     // 指標變數定義時變數名稱前面放 * 。
    float * pm3;                     // 定義指向float型態的指標 pm3。
    char* pm4;                       // 定義指向char型態的指標 pm4。

取得變數的位址(指標值)

  • 指標變數存放的是某個物件的位址,若想取得該位址,則使用『取址運算子 &』。

    int ival = 42;
    int *p = &ival;                  // p存放變數ival的位址,或是說p是指向變數ival的指標。
  • 因參考並非物件,故無實際的位址,因此無法定義指向參考的指標(只能做到指向參考所綁定的物件的位址)。

  • 指標的型態必須與指向的物件型態相匹配。

    int dval = 5;
    int *pd = &dval;              // 正確,初始值是int型態物件的位址。 
    int *pd2 = pd;                // 正確,將指標變數pd的值(指標)複製至pd2。
    int **pd3 = &pd;              // 正確,定義一個『指標的指標變數』。
    
    // double *pi = pd;                    // 錯誤:型態不匹配。

指標值

  • 指標的值(即位址)應屬下列4種狀態之一:

    • 指向一個物件。

    • 指向緊鄰物件的下一個位置。

    • 『空指標』(指標並沒有指向任何物件):nullptr

    • 無效指標

  • 試圖複製或以其他方式存取『無效指標』都將引發錯誤,而編譯器並不負責檢查此類錯誤。

透過指標取得物件

  • 若指標指向了一個物件,則可使用『解參考運算子 * 』來存取該物件。

    int ival = 42;
    int *p = &ival;
    std::cout << *p << std::endl;             // 透過解參考運算子*取得指標p所指向的物件
  • 若對解參考所得結果賦值,也就是對指標所指向的物件賦值。

    *p = 0;
    std::cout << *p << std::endl;            // 輸出0
    std::cout << ival << std::endl;          // 輸出亦為0
  • 注意:符號 *& 既能作為表達式的運算子,亦能作為變數宣告的一部分出現。

    • 故兩者會依程式的上下文決定符號的作用。
  • 在宣告語句中,*& 用於定義複合型態。

  • 在表達式中,*& 轉換為運算子。

    int i = 42;
    int &r = i;
    int *p;
    p = &i;
    *p = i;
    int &r2 = *p;

空指標

  • 空指標(null pointer)不指向任何物件。

  • 定義空指標最直接的方式就是用字面值(nullptr)來初始化指標(C++ 11)。

  • 指標具有『隱性』轉換為布林的功能。

    • 任何不是 nullptr 的值都會隱性轉換為 true

    • nullptr 則會隱性轉換為 false

  • 建議初始化所有指標。

    int *p1 = nullptr;    // 推薦使用
    int *p2 = 0;          // 也是定義空指標
    
    #include <cstdlib>
    int *p3 = NULL;      // 避免使用

void * 指標

  • void * 是一個特殊的指標型態,它可以存放任何型態物件的位址

  • 它可以做到:

    • 與其他指標做比較。

    • 作為函數的輸入或輸出。

    • 賦值給另外的 void * 指標。

  • 無法直接操作 void * 所指向的物件。

double obj = 3.14, *pd = &obj;
void *pv = &obj;                            // obj可以是任何型態。
pv = pd;                                    // pv可以存放任何型態的指標。
// std::cout << *pv << std::endl;           // 錯誤:無法解參考。
std::cout << *(double *)pv << std::endl;    // 只能透過強制轉換 ((double *) )後再來解參考。注意:強制轉換型態需與原來型態一致,否則會出現問題。

定義多個變數

int* p;
int* p1, p2;                         // 合法寫法:但容易誤導:p1是指向int的指標,p2只是一個int變數。
int *p1, p2;                         // 建議寫法。
int *p1, *p2;                        // p1和p2都是指向int的指標。

指向指標的指標

  • 像其他物件一樣,指標變數也是一個佔記憶體空間的物件,故指標變數也會有自己的位址。

    int ival = 1024;
    int *pi = &ival;                    // pi指向一個int型態的物件。 
    int ** ppi = &pi;                   // ppi指向一個int型態的指標。

參考指標的參考

  • 又稱『綁定指標的參考』。

  • 參考本身並非物件,故無法定義指向參考的指標。

  • 因指標變數是物件,故可以定義綁定指標變數的參考。

    int i = 42;
    int *p;                   // p是一個int型態的指標。
    
    int *&r = p;              // r是一個對指標p的參考。
    
    r = &i;                   // r綁定一個指標,因此給r賦值&r,就是p指向i。
    *r = 0;                   // 解參考r取得i,也就是p指向的物件,將i的值更改為0。
  • 要理解 r 的型態(或是其他較複雜型態),建議『從右至左』解讀 r 的定義:離變數名最近的符號對變數的型態有最直接的影響。

    int ival = 1024;
    int *pi = &ival;   // pi指向一個int型態的物件。
    int* *ppi = &pi;   // ppi指向一個int型態的指標。
    int* &ref = pi;    // ref綁定指向一個int型態的指標。
    int** &ref2 = ppi; // ref2綁定指向一個int型態指標的指標。 
    
    std::cout << pi << std::endl;
    std::cout << ref << std::endl;
    std::cout << ppi << std::endl;
    std::cout << ref2 << std::endl;

參考與 const

const 參考
  • 注意:參考本身非物件

  • 就像綁定到其他物件上,可將參考綁定到 const 的物件上:稱之為對 const 常數的參考(reference to const)。

  • 與『普通參考』不同的是,『const 參考』不能被拿來修改被綁定的物件。

    const int ci = 1024;
    const int &r1 = ci;                // 正確:參考及其對應的物件都是常量。
    
    // r1 = 42;                        // 錯誤:r1是對常數的參考
    // int &r2 = ci;                   // 錯誤:試圖讓一個『非』常量參考指向一個常數物件

指標與 const

  • 注意:指標為物件,佔記憶體空間與型態的定義。
指向常數的指標
  • 『指向常數的指標』(pointer to const)無法修改其所指向的物件。

  • 當想儲存 const 物件的位址時,只能使用『指向常數的指標』。

    const double pi = 3.14;             // pi是一個常數,其值無法修改。
    // double *ptr = &pi                // 錯誤: ptr是一個普通指標。
    const double *cptr = &pi;           // 正確:cptr可以指向double常數。
    // *cptr = 42;                      // 錯誤:無法給 *cptr 賦值。
const 指標
  • 因指標是物件,故可像其他物件一樣,可令指標本身const

  • const 指標必須初始化(與一般 const 物件相同):一但初始化完成,其值則無法再被修改。

    int errNum = 0;
    int *const curErr = &errNum;        // curErr將一直指向errNum。
    const double pi = 3.14159;
    const double *const pip = &pi;      // pip是一個指向const物件的const指標。
    
    // *pip = 1.23;                     // 錯誤:pip是一個指向常數的指標。
  • 指標本身是 const,並不意味無法透過指標修改其所指向的物件。能否修改需視其所指向的物件型態(是否是 const)。

  • 指標本身是不是 const 與其所指向的變數是不是 const,是兩個相互獨立的議題。

  • Top-Level const:『指標物件本身』const

  • Low-Level const:『指標所指向的物件』const

int i = 0;
int *const p1 = &i;                    // 無法改變p1的值,這是一個Top-Level const。
const int ci = 42;                     // 無法改變ci的值,這是一個Top-Level const。
const int *p2 = &ci;                   // p2的值可被改變,這是一個Low-Level const。
const int *const p3 = p2;              // 右邊的const是Top-Level const,左邊的const是Low Level const。
const int &r = ci;                     // 用於宣告的const是Low-Level const。
/*
 * 執行複製動作並不會改變被複製之物件的值。
 */
i = ci;                                // 正確:複製ci的值,ci是Top-Level const,對此操作無影響。
p2 = p3;                               // 正確:p2與p3指向的物件型態相同,p3的Top-Level const不受影響。
/*
 * 1. 當進行複製時,賦值運算子("=")左右兩邊的物件必須具有相同的Low-Level const 。
 * 2. 或是兩邊的物件型態必須能夠進行轉換。
 * 3. 『非常數』可以轉換為『常數』。
 */
// int *p = p3;     
const int *p = p3;
p2 = p3;
p2 = &i;
// int &r = ci;
const int &r2 = i;
  • p3既是 Top-Level const 也是 Low-Level const ,複製 p3可以不在乎它是 Top-Level const,但必須清楚它所指向的物件是個常數。故無法使用 p3 去初始化 p(因 p 所指向的是一個普通整數)。

  • 另外,p3值可以賦值給 p2。因兩者都為 Low-Level const

constexpr與常數表達式

  • 基於程式性能與安全性的考量,若可在編譯期執行,而非於執行期完成時,則都應優先在編譯期進行。

    • 例如:當涉及字面值的簡單數學運算,對應的表達式是可以在編譯期執行。
  • constexpr 指示編譯器在編譯期時對表達式(expression)求值(如果可以的話)。

  • 常數表達數(const expression)是指值不會改變,且在『編譯期』就能得到計算結果的表達式。

  • 字面值屬於常數表達式。

  • 某物件或表達式是否屬於常數表達式是由其資料型態初始值共同決定。

    const int max_files = 20;                           // max_files為常數表達式。
    int staff_size = 27;                                // staff_size不是常數表達式。
    
    const int sz = get_size();                          //  sz不是常數表達式:因需執行函數get_size()後才會知道其值。
  • 對於複雜的程式,實際狀況是很難分辨一個初始值是否為常數表達式。

  • C++ 11 新標準規定,可將變數宣告constexpr 型態以便編譯器驗證其是否為常數表達式。

  • 無法使用『一般函數』的執行結果作為 constexpr 變數的初始值:需使用『 constexpr 函數』。

    constexpr int mf = 20;                           // 20是常數表達式。
    constexpr int limit = mf + 1;                    // mf +1 是常數表達式。
    constexpr int sz = size();                       // 只有當size函數是一個『constexpr函數』時,這行語句才會正確。
  • 一般來說,當認定變數為常數表達式,建議宣告為 constexpr 型態。

  • 指標與參考都能被定義為 constexpr

  • 但『 constexpr 指標』的初始值必須為 nullptr0,或是儲存某個『固定位址物件』(定義在『函數體之外』或是『可見範圍(scope)』在函數體之外的物件)的位址。

  • constexpr 參考亦可以綁定『固定位址物件』上。

    const int *p = nullptr;                         // p是一個指向整數常數的指標。
    
    // 注意下列兩個例子,兩者都是 Top-Level const。
    constexpr int *q = nullptr;                     // q是一個指向整數型態的『const 指標』:Top-Level const
    int *const r = nullptr;                         // r是一個指向整數型態的『const 指標』:Top-Level const
    constexpr int *np = nullptr;                    // np是一個指向整數的const 指標。
    int j = 0;
    constexpr int i = 42;                           // i的型態為整數型態。
    /*
     * i和j都必須定義在函數體之外。
     */
    constexpr const int *p = &i;                    // p為 const指標,其指向整數型態i。
    constexpr int *p1 = &j;                         // p1為 const指標,其指向整數型態j。
  • constexpr 函數:當 constexpr 應用於函數時,表示該函數可以在編譯期執行,且只能包含適合於編譯時計算的程式碼。這對在編譯期可以確定的值或計算很有用。

    • 在編譯期時,編譯器會嘗試執行 constexpr 函数,而非執行期時。

    • 允許在編譯期時確定函数的结果,將其用作常數,以及在編譯期進行優化。

    • constexpr 函數中,只能包含可在編譯時計算的程式碼,通常包括常數表達式和其他 constexpr 函數。

    constexpr int Add(int a, int b) {
        return a + b;
    }
    
    int main() {
        const int result = Add(3, 4); // 在編譯期計算出結果。
        return 0;
    }
  • constconstexpr 密切相關:

    • constexpr 強制表達式在編譯期求值。

    • const 則強制變數在某些作用域(執行期)內不可修改。

    • 所有 constexpr 表達式都是 const,因為他們在執行期時都為固定。

右值參考

  • 之前所學習的參考,為『左值參考』:綁定左值。

  • 『右值參考』綁定到『右值』:C++ 11新標準。

  • 右值參考所綁定的物件在使用之後就無需保留(為『臨時變數』:即將銷毀的物件)。

  • 使用 && 運算子來定義。

    // int &ref = 3;                 // 錯誤:因為ref為左值參考。 
    int &&refRightValue = 3;         // 綁定至常數。可將refRightValue理解為一個int型態變數。
    refRightValue = 5;               // 亦可進行修改。
    std::string strtest = "I love Taiwan!";
    
    std::string &r1 = strtest;                     // 正確:左值參考綁定左值。
    
    // std::string &r2 = "I love Taiwan!";         // 錯誤:左值參考無法綁定右值。
    
    const std::string &r3 = "I love Taiwan!";      //  正確: const參考可以綁定至右值。
    /*
      等價於:
      std::string tempvalue = "I love Taiwan!";
      const std::string &r3 = tempvalue;
    */
    
    // std::string &&r4 = strtest;                 // 錯誤:右值參考無法綁定到左值。
    
    std::string &&r5 = "I love Taiwan!";           // 正確:右值參考可以綁定臨時的物件。
    
    int i = 10;
    int &ri = i;
    // int &&ri2 = i;
    // int &ri3 = i * 100;
    const int &ri4 = i * 100;
    int &&ri5 = i * 1000;
  • 右值參考設計的目的是要提高程式執行的效率。做法是將複製的物件變成『移動物件』從而提高執行效率。

使用者定義型態

結構(struct

  • 結構(struct)是一個包含多個資料成員(member)的集合,格式如下:

    struct 標識符
    {
      成員列表
    };
    struct SomeStruct
    {
      float a;
      int b;
    };                                 // 分號代表結構定義結束。容易忽略!
  • 結構也是一種資料型態,故也可以用來定義變數。

    struct SomeStruct s;                       
    s.a = 10;                          // 結構成員的取得
    struct student
    {
      int num;
      char name[100];
      int sex;
      int age;
      char address[100];
    };
    
    struct student s1, s2;             

    也可以直接定義變數:

    struct student
    {
      int num;
      char name[100];
      int sex;
      int age;
      char address[100];
    } s1, s2;
  • 使用結構之前必須先定義結構型態。

  • 結構可以嵌套結構。

    struct date
    {
      int month;
      int day;
      int year;
    };
    
    struct student
    {
      int num;
      char name[100];
      int sex;
      int age;
      char address[100];
      struct date birthday;                        // birthday是一個結構
    } s1, s2;
    s1.num = 1001;                                // 將1001賦值給s1變數的成員num。
    s1.birthday.month = 12;
    s1.birthday.day = 30;
    s1.birthday.year = 2018;
  • 結構內的成員變數名稱可與程式中的變數名稱一樣,並不會發生名稱衝突。

  • 結構的初始化:

    struct student s5 = {100, "王小明", 1, 18, "台北市", 10, 14, 2018};
  • 結構是『類別(class)』的基礎。

  • 結構指標:

    • 就是結構變數的指標。

    • 指向該結構變數所佔記憶體空間的『首位址』

    struct student
    {
      int num;
      char name[100];
      int sex;
      int age;
      char address[100];
    };
    
    struct student s1, s2;
    
    student *p1 = &s1;
    student *p2 = &s2;
    
    std::cout << p1 << std::endl;
    std::cout << p2 << std::endl;

聯合(union

  • 有時候需要將不同型態的變數儲存在同一段記憶體。例如將一個整數型態、一個字元型態與一個字元陣列放在同一個地址開始的記憶體單元。

  • 上述不同型態變數在記憶體所佔的空間大小不同,但他們都是從同一個位址開始。

  • 換句話說,就是這些型態的變數會互相替代。

  • 上述這種共通佔用同一段記憶體空間的資料儲存方式就是聯合(union)。

  • 聯合與結構有些類似,但是結構中的成員變數會分佔不同的記憶體空間

  • 語法:

    union 聯合名稱
    {
      成員變數
    } 變數列表;
    union myuni
    {
      int carnum;
      char cartype;
      char cname[60];
    } a, b, c;
    union myuni
    {
      int carnum;
      char cartype;
      char cname[60];
    };
    
    union myuni a, b, c;
  • 聯合變數的特點:

    • 同一段記憶體空間可以存放幾種不同型態的變數,但每一個瞬間只能存放其中一種。只有一個成員發生作用。

    • 程式中最後哪個成員被賦值,哪個成員才會起作用。

    • 聯合變數位址與其成員的位址都相同。

    • 聯合變數名代表聯合變數的首位址。

列舉(enum

  • 就是將值一一列舉出來,利用標識符來取代代號。

  • 能更直觀,更容易讓人看懂。

    enum color
    {
      Red,
      Green,
      Blue,
      Yellow
    };
    • 上述定義名稱為color的列舉型態。

    • Red, Green, Blue, Yellow 被稱為列舉常數。

    • 編譯器會按照定義時的順序賦予賦予它們的值。且值從 0 開始。

    • 故 Red 等於 0,Green 等於 1, Blue 等於 2,Yellow 等於 3。

    enum color mycolor1, mycolor2;              // 定義兩個列舉型態的變數。
    enum {Red, Green, Blue, Yellow} mycolor1, mycolor2;
  • 可直接給列舉型態變數賦值。

    mycolor = Red;
  • 定義列舉型態時,可以改變列舉常數的值。

    enum color
    {
      Red = 7
      Green,                              // 8
      Blue = 2,
      Yellow                              // 3
    };
  • 列舉常數可以理解為整數型態。但在撰寫程式碼時,無法直接將整數型態賦值給列舉常數。

    enum color mycolor1;
    // mycolor1 = 1;                        // 錯誤
    
    mycolor1 = (enum color) 1000;        // 正確:使用強制型態轉換。
  • 列舉值可以賦值給常數變數。故可將列舉常數當作整數值使用:

    enum color mycolor1, mycolor2; // 定義兩個列舉型態的變數。
    mycolor1 = Red;
    mycolor2 = (enum color)100;
    int i = Blue;
    int j = Yellow;
    
    std::cout << mycolor1 << std::endl;
    std::cout << mycolor2 << std::endl;
    std::cout << i << std::endl;
    std::cout << j << std::endl;

列舉類別(enum class)

  • 又稱『作用域列舉』。

  • 從C++ 11 開始,使用 enum class 定義『列舉類別』。

    enum class EnumName {
        Enumerator1,
        Enumerator2,
        // 更多的列舉值。
    };
  • 與一般列舉不同,enum class 具有下列特點:

    1. 強型別:enum class為強型別,這意味著列舉值不能直接轉換為整數,也不能與其他列舉類別進行比較,需要顯性地進行型態轉換。

    2. 作用域限定:列舉值的名稱在其所屬的列舉類別的作用域內,故可避免全局命名空間的污染問題。

    3. 隱性轉換:與普通的列舉不同,enum class不會發生隱性轉換到整数的情況,需要顯性地進行轉換。

    使用enum class可以提高程式碼的安全性和可讀性,並避免了許多與普通列舉相關的問題。

    #include <iostream>
    
    // 定義 enum class,名為 Season
    enum class Season {
        Spring,
        Summer,
        Autumn,
        Winter
    };
    
    int main() {
        // 宣告 Season 的變數,並初始化為 Spring 列舉值
        Season currentSeason = Season::Spring;
    
        // 使用 switch 語句處理不同的季節
        switch (currentSeason) {
            case Season::Spring:
                std::cout << "It's Spring!" << std::endl;
                break;
            case Season::Summer:
                std::cout << "It's Summer!" << std::endl;
                break;
            case Season::Autumn:
                std::cout << "It's Autumn!" << std::endl;
                break;
            case Season::Winter:
                std::cout << "It's Winter!" << std::endl;
                break;
        }
    
        // 可進行顯性的型態轉換
        int seasonValue = static_cast<int>(currentSeason);
        std::cout << "Current season value: " << seasonValue << std::endl;
    
        return 0;
    }
    • 值得注意的是,在使用enum class时,列舉值的名稱必須使用『作用域解析運算子(::)』來存取,例如Season::Spring

型態別名(type alias)

  • 隨著程式越來越複雜,程式中所定義的型態也會越來越複雜:

    • 型態難以定義。

    • 根本不知道型態是什麼。

  • 可為某種複雜的型態設定型態別名(type alias)或同義詞。

  • 可使型態名稱變得簡單且易於理解與使用。

  • 有助於開發者知道該類型的使用目的。

    • 方法1:typedef

      • typedef 是用來定義各種型態的,不是用來定義變數。

      • typedef 只能對已經存在的型態增加一個別名,並無法創造新型態。

      • typedef 是編譯時處理的。

        typedef int INTEGER;
        typefef char *PSTRING;                   // 定義 PSTRING 為字元指標。
        typedef double WAGES;                    // WAGES 是 double 的別名。
        typedef WAGES base, *p;                  // base 也是 double 的別名,p 是 double* 的別名。
        WAGES hourly, weekly;
    • 方法2:別名宣告(alias declaration)

      • using:從 C++ 11開始,以關鍵字 using 作為別名宣告的開始,其後緊接『別名』與 =

        using SI = Sales_item;                      // SI 是 Sales_item的別名。 
        SI item;
  • 指標、常數與型態別名

    typedef char *pstring;                 // 定義型態別名 pstring,被定義為指向char的指標型態。
    const pstring cstr = 0;                // cstr為指向char的const指標
    const pstring *ps;                     // ps為一個指標,其物件為指向char const指標。
    • typedef char *pstring; 此行程式碼定義了一個名為pstring的型態別名,它被定義為指向char的指標型別。

    • const pstring cstr = 0; 此行程式碼宣告了一個指向char的指標常數cstr,並將其初始化為0。由於pstring是指向char的指標型態,所以const pstring是一個指向char的指標常量,即cstr是一個指向char的指標常量。

    • const pstring *ps; 此行程式碼宣告一個指向指標常量的指標ps,而這個指標指向的是指向char的指標常量。因為pstring是指向char的指標型別,所以const pstring是一個指向char的指標常量,而const pstring *則是一個指向指標常量的指標。