參考(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)(或稱『指標變數』)為 『指向』(透過變數的位址) 另外一個類型變數的複合型態。
透過 *
來定義指標變數。
『指標變數』本身就是物件,故可以對指標變數進行複製與賦值。
不一定在定義時給定初始值(若未定義初始值,該指標具未定義之值,此時建議初始化為『空指標』:nullptr
)。
對指標變數賦值,就是令它存放一個新的位址值,從而指向一個新的物件。
『指標變數』與『參考』都能提供『間接』存取物件的管道。
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 = π // 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 = π // 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 = π // 正確: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 = π // 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
指標』的初始值必須為
nullptr
或
0
,或是儲存某個『固定位址物件』(定義在『函數體之外』或是『可見範圍(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 int Add(int a, int b) {
return a + b;
}
int main() {
const int result = Add(3, 4); // 在編譯期計算出結果。
return 0;
}
const
與 constexpr
密切相關:
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;
又稱『作用域列舉』。
從C++ 11 開始,使用 enum class
定義『列舉類別』。
enum class EnumName {
Enumerator1,
Enumerator2,
// 更多的列舉值。
};
與普通列舉不同,enum class
具有下列特點:
強型別:enum class
為強型別,這意味著列舉值不能直接轉換為整數,也不能與其他列舉類別進行比較,需要顯性地進行型態轉換。
作用域限定:列舉值的名稱在其所屬的列舉類別的作用域內,故可避免全局命名空間的污染問題。
隱性轉換:與普通的列舉不同,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)或同義詞。
可使型態名稱變得簡單且易於理解與使用。
有助於開發者知道該類型的使用目的。
方法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 *
則是一個指向指標常量的指標。