函數(function)可視為一段被命名的程式碼,代表一段可以重複的程式碼,可大幅減少程式中重複程式碼出現的次數。
每個函數用於實現一個相對獨立且短小的功能。當需要該功能時,可透過直接呼叫這些函數來實現。
可透過傳遞不同的參數來控制函數的各種執行動作。
main
函數是整個程式執行的『入口函數』,程式先從
main
函數開始執行。
一個 C\C++ 程式可視為一個主函數(main
函數,名稱是固定的,不可寫錯)與若干個其他函數所組成。
主函數可以呼叫其他函數,其他函數也可以相互呼叫,但無法呼叫
main
函數。
故程式從 main
函數開始執行(由系統來呼叫),最終也是在 main
函數結束。
一個原始檔 (source file)會包含一個至多個函數不等。
對於大型專案,一般而言並不會將所有函數放在一個檔案。故常見專案會由多個原始檔所組成。
多個函數可以分別放在多個原始檔,並可被其他原始檔的函數所呼叫。
注意:在 C++,『函數無法嵌套』,意指無法在一個函數內部嵌套另一個函數。
除了標準程式庫之內的函數外,還可以定義自己的函數。
函數可以有 0 個或多個參數,且(通常)會回傳結果。
也可以『重載(overload)』函數:一個函數名稱可對應多個不同函數。
函數有『宣告』與『定義』之分。
// 階乘函數
int fact(int val)
{
int ret = 1; // local變數,用於保存計算結果。
while (val > 1)
ret *= val--; // 把ret與val的乘積賦值給ret,接著將val減1。
return ret; // 回傳ret。
}
fact
,『參數(形式參數)』型態為
int
,且『回傳型態』為 int
。呼叫運算子(()
):透過『呼叫運算子(()
)』來執行(調用)函數。
其作用於表達式,一般常見的表達式為『函數名』或是『函數指標(指向函數的指標)』。
當函數被呼叫時,括號內放的是由『逗號
,
』隔開的『實際參數(實參)』列表。
當傳遞參數時,是指使用『實參』來初始化『形參』。
呼叫運算子(()
)的『回傳型態』就是函數的『回傳型態』。
int main()
{
int j = fact(5); // j = 120,即為fact(5)的結果。
std::cout << "5! is " << j << std::endl;
return 0;
}
故『函數呼叫』完成兩項工作:
使用『實參』來初始化函數對應的『形參』。
將控制權轉移到被『呼叫的函數(fact
函數)』。此時,『呼叫函數(main
函數)』的執行被中斷,而『被呼叫函數』開始執行。
當執行到 return
語句時,函數就會結束執行,並將控制權轉移到『呼叫函數(main
函數)』。之後繼續執行接下來的語句。
『實參』與『形參』:
實參就是形參的『初始值』。
第一個實參初始化第一個形參,第二個實參初始化第二個形參,依此類推,按照『順序』。但並沒有辦法確定初始化的順序。
實參的型態必須與對應的形參『型態』匹配。與一般初始化的觀念是一樣的。
實參的數量需與形參的『數量』一致。
// fact("hi"); // 錯誤:實參型態不正確。
// fact(); // 錯誤:實參數量不足。
// fact(42, 10, 10); // 錯誤:實參數量太多。
fact(3.14); // 正確:實參可以轉換成int型態。 fact(3);
形參列表:
回傳型態 函數名(形參列表);
形參列表可為『空』,但不能省略。
void f1() {}; // 『隱式』定義 空 形參列表。
void f2(void) {}; // 『顯示』定義 空 形參列表。
形參間以逗號(,
)隔開,其中每個形參都必須進行型態宣告,且不能同名稱。
int f3(int v1, v2) {}; // 錯誤。
int f4(int v1, int v2) {}; // 正確。
即使某個形參在函數內部不被使用,也必須為它提供實參進行初始化。
函數『回傳型態』:
大多數型態都能當作函數的回傳型態(包含自定義型態,如
struct
或是 自訂的類別型態 class
)。
void
型態代表函數『不回傳任何值』。
注意:函數『無法』回傳『陣列』或『函數』,但可以回傳指向『陣列』或『函數』的『指標(pointer)』。
void printHello()
{
std::cout << "Hello World!" << std::endl;
}
呼叫函數時,會為函數的形參分配記憶體空間,呼叫函數結束後,形參的記憶體空間會被釋放,故形參只能在函數內部使用。
呼叫函數時,實參的值會自動賦值給形參(『複製』實參值,為『值傳遞(pass by value)』)。
注意:若形參與實參為『陣列名』時(『陣列名』代表『陣列的首位址』),則傳遞『陣列首位址』。
實參可以是常數、變數、表達式:
int result = add(3, 4);
int result = add(1 + 2, 2 + 2);
int result = add(i, j);
呼叫函數之前,應先『宣告』該函數:只要被宣告過的函數,(在編譯期)即可編譯成功而不用知道該函數『定義』在哪裡(寫在不同的原始檔也可以)。
故一般專案常將『函數宣告』寫在『標頭檔(header
file)』內(函數被宣告的位置),並在每一個使用該函數的原始檔開頭引入(#include
)該標頭檔,以完成函數宣告。
函數宣告:
函數三要素:描述函數與外界的溝通的介面(interface),用來說明呼叫該函數所需的所有資訊:
函數名
形參型態
回傳型態
函數宣告又稱『函數原型(function prototype)』。
形式:回傳型態 函數名(形式參數列表);
不包含函數體。
尾端加 ;
號。
與函數宣告相比,『函數定義』包含函數體,且尾端不需要
;
號。
為了通用性與便利性,可將所有自定義的函數宣告寫在 .h
的標頭檔中。
當某原始檔欲使用某函數時,在該原始檔的開頭 #include
該函數宣告的標頭檔。
函數宣告就是讓編譯器知道將使用的『函數名』、『形參型態』、『回傳型態』等資訊。並根據上述資訊,可在編譯期進行「檢查」。
故一般在標頭檔進行函數宣告。
局部變數
在函數內部定義的變數為『局部(local)變數』,它只在該函數範圍內有效。
main
函數中定義的變數只在 main
函數內有效。
不同函數內部可以使用相同的變數名稱而互不干擾。
注意:『形參』為局部變數。
全局變數
程式編譯的單位為『原始檔(source
file)』。一個原始檔內可以包含一個(main
函數)或多個函數。
在函數外部定義的變數則為『全局(global)變數』。
全局變數的有效範圍從『該變數定義的位置』到『整個程式(或稱『程序』(program))結束為止』。
對於函數,既可使用該函數內定義的局部變數,也可使用有效的全局變數。
除非必要,盡量不要使用全局變數:因為全局變數在整個程式執行期間一直存在。
全局變數會降低程式的可讀性。
若某個函數想使用在它之後才定義的全局變數,則可以使用關鍵字
extern
作『外部變數宣告』(注意:不需初始化),用以表示該變數在『函數外部』做定義。
全局變數只能定義一次,其位置在所有函數之外,且定義時系統會為它分配記憶體空間。
extern int c1, c2; // 外部變數宣告,不需初始化。c1, c2 在其他原始檔定義。
void lookvalue()
{
c1 = 5; // 為c1, c2變數賦值。在操作賦值之前,變數必須定義。
c2 = 8;
return;
}
int main()
{
lookvalue();
std::cout << "c1 = " << c1 << std::endl;
std::cout << "c2 = " << c2 << std::endl;
return 0;
}
在同一個原始檔中,若全局變數與局部變數同名,則在該局部變數作用域內,全局變數不起作用。
int a = 4, b = 5;
void lookvalue(int a, int b)
{
a = 123;
b = 456;
}
int main()
{
int i = 2, j = 5;
lookvalue(i, j);
std::cout << "a = " << a << std::endl; // a = 4
std::cout << "b = " << b << std::endl; // b = 5
}
宣告階段:當在程式碼中宣告一個函數時,這個函數的名稱和『簽名』被引入到當前的作用域中。此時,編譯器知道這個函數的存在。
函數「簽名」(signature)是指函數的名稱以及它的參數列表的型態和數量,這些特徵可唯一識別函數。
函數簽名的具體內容包括:
函數名稱
參數的數量
每個參數的型態
即使函數具有相同的名稱,函數『簽名』可用來區分不同的函數(『函數重載(overloading)』)。
定義階段:函數的定義包括『函數體』的實際實現。函數定義確定了函數的實作細節。
編譯階段:在編譯原始程式碼時,編譯器將函數的調用點綁定到相應的函數定義。這是靜態繫結的一部分,它發生在編譯期間。
執行階段:
當程序運行時,函數可以被呼叫執行。
函數的生命週期在函數被呼叫時開始,並在函數執行完畢時結束。
函數的參數和局部變數在函數執行期間存在。
而函數的『靜態(static
)變數』在整個程式的執行期存在。
C++ 中的函數被視為可呼叫的『程式碼區塊』,而『非物件』。
因函數沒有類別或結構,其生命週期不同於物件。
故函數的生命週期包括宣告、定義、編譯和執行階段,並且在執行期根據函數的呼叫情況進行執行。函數的生命週期不同於物件的生命週期,它們是不同的概念。
局部變數與全局變數是以變數的『作用域』來做定義。
若以變數存在的時間(『生存期』)區分,則可定義『靜態儲存變數』與『動態儲存變數』。
靜態儲存(區):在程式『編譯期』分配『固定』的儲存空間的方式。
全局變數(在函數外部定義)放在靜態儲存區,且佔固定的儲存空間。
程式開始執行之前給全局變數分配儲存區,並在整個程式執行完畢後釋放該儲存區。
動態儲存(區):在程式『執行期』根據需要『動態』分配儲存空間的方式。
儲存下列數據:
函數形參。
局部變數:函數內部定義的變數。
函數呼叫時所需之數據資料與回傳的位址。
上述數據在函數呼叫時分配儲存空間,函數執行完畢後這些空間就會被釋放(回收)。
故每次呼叫函數時,分配給該函數局部變數的記憶體位址可能是不一樣的。
static
)局部變數若希望函數中的『局部變數』在函數呼叫結束後不要被系統自動釋放而保留原值,且在下次呼叫該函數時,該局部變數值即為上一次該函數呼叫結束後之值;則只需使用
static
關鍵字修飾『局部變數』為『靜態(static
)局部變數』。
void funcTest()
{
static int c = 4; // local static variable
std::cout << "c = " << c << std::endl;
++c;
return;
}
int main()
{
funcTest();
funcTest();
funcTest();
return 0;
}
『靜態局部變數』在『靜態儲存區』分配記憶體空間,程式在整個執行期都『不會』被釋放。
『靜態局部變數』是在編譯期設定初值,且只會被賦值『一次』。
定義『靜態局部變數』若不進行初始化,則編譯時會自動設定初值為
0
。若是一般的局部變數,則為不確定的值。
雖然『靜態局部變數』在函數呼叫結束後仍存在,但其他函數是無法對它進行存取。
除非必要,不要過多使用靜態局部變數。
若某函數想使用在其之後定義的全局變數,可使用關鍵字
extern
進行『外部變數宣告』,表示該變數在該函數外部做定義,否則編譯就會出錯。
int func(int a, int b)
{
extern int constValue; // 外部變數宣告
int r = a + b + constValue;
return r;
}
int constValue = 100; // 全局變數
int main(int argc, char const *argv[])
{
int x = 1, y = 2;
int ans = func(x, y);
std::cout << "ans = " << ans << std::endl;
return 0;
}
若某個原始檔想使用另外一個原始檔中定義的全局變數,也可以使用關鍵字
extern
。也就是在使用該全局變數的原始檔的『開頭』進行『外部變數說明』。
// utilities.cpp
int constValue = 100;
// test.cpp
#include <iostream>
extern int constValue; // 外部變數宣告
int func(int a, int b)
{
int r = a + b + constValue;
return r;
}
int main(int argc, char const *argv[])
{
int x = 1, y = 2;
int ans = func(x, y);
std::cout << "ans = " << ans << std::endl;
return 0;
}
表示本原始檔要使用的變數是一個已經在其他原始檔定義過的外部變數,不需為它分配記憶體空間。
一個全局變數的『作用域』是從它『所定義的位置』到『整個原始檔結束』為止。但可透過
extern
將它的作用域擴大到所有具有關鍵字 extern
的其他原始檔。
使用跨原始檔全局變數要小心,因為在某個原始檔修改該變數,也會影響到其他使用該全局變數的原始檔。
若希望某些全局變數,只在該原始檔被使用,不想被其他原始檔存取,則可在該全局變數前面加上
static
關鍵字。
每次函數呼叫時,都會重新建立『形參』,並使用『實參』對『形參』進行初始化。
若形參為『參考』,則形參將『綁定』對應的實參(傳參考:passed by reference)。否則,為將依實參值『複製』後的值賦值給形參(傳值:passed by value)。
指標與一般『非參考』型態一樣,都是複製後傳值(傳值:passed by value)。
因複製的是指標值,故存在兩個具有相同指標值的指標變數(一個是實參,一個是形參)。
因為指標變數可間接存取它所指向的物件,故可透過指標變數來修改他所指向的物件。
void reset(int *ip)
{
*ip = 0; // 改變指標變數ip所指物件的值。
ip = 0; // 只改變ip 的局部變數,實參值未被改變。
}
int i = 42;
reset(&i);
std::cout << i << std::endl; // i = 0;
對參考的操作,實際上就是操作其所『綁定』的物件。
故對參考形參的操作,即可允許函數對實參進行修改。
void reset(int &i)
{
i = 0; // 修改參考變數i所綁定的物件。
}
int j = 42;
reset(j);
std::cout << j << std::endl; // j = 0;
可使用參考來避免複製。
複製大型物件往往會造成程式的低效。
某些類別型態的物件並不支援複製(如 IO 型態)。
若函數不需修改其所綁定的物件(參考),建議可將其宣告為
const
參考。
const
參考,亦可傳遞『非
const
實參』。// 字串可能非常長,應避免直接複製他們。
//
bool isShorter(const std::string &s1, const std::string &s2)
{
return s1.size() < s2.size();
}
注意:一個函數只能回傳一個值。
當函數需要回傳多個值時,參考形參可幫助開發者完成回傳多個結果。
// 回傳字串s中,字元c第一次出現的位置。
// 參考形參用來記錄c出現的總次數。
std::string::size_type find_char(const std::string &s, char c, std::string::size_type &occurs)
{
auto ret = s.size(); //
occurs = 0;
for (decltype(ret) i = 0; i != s.size(); ++i)
{
if (s[i] == c)
{
if (ret == s.size())
ret = i;
++occurs;
}
}
return ret;
}
std::string x = "Mississippi";
std::string::size_type i = 0;
std::string::size_type location = find_char(x, 's', i);
std::cout << "字元s共出現 " << i << " 次 "<< std::endl;
std::cout << "字元s第一次出現在的位置:"<< location << std::endl;
const
形參和實參注意:Top-Level const
作用於物件本身。
const int ci = 42; // 無法改變ci,為Top-Level。
int i = ci // 正確:當複製ci時,會忽略Top-Level const。
int * const p = &i; // Top-Level const
*p = 100; // 正確:透過p改變所指向的i是被允許的,只是不能改變指標變數的指標值。
與一般初始化原理一樣,當使用實參初始化形參時會忽略
Top-Level const
。
故當形參具有 Top-Level const
時,傳給它的實參可以是『const
』或『非
const
』都可以。(注意:重載函數還會用到這個觀念。)
盡量使用『const
參考』:
可避免透過參考形參改變實參值。
避免限制函數所能接受的實參型態。
// 不良設計:
std::string::size_type find_char(std::string &s, char c, std::string::size_type &occurs);
find_char("Hello World", 'o', ctr); // 錯誤。
bool is_sentence(const std::string &s) // 編譯錯誤。
{
// 若s的末尾只有一個句號,則s是一個句子。
std::string::size_type ctr = 0;
return find_char(s, '.', ctr) == s.size() - 1 && ctr == 1;
}
陣列不能複製:故無法以值複製傳遞給形參。
使用『陣列名』時會將其轉換成『指標』:故當我們為函數傳至『陣列名』時,實際上傳的是『首元素的位址』。
將『陣列名』作為函數參數時,是將實參陣列的『首位址』傳遞給形參,代表實參與形參皆指向同一段記憶體。
故當形參陣列內的元素發生修改時,等同於實參陣列元素發生改變。
void changeValue(int a[])
{
a[3] = 30;
a[4] = 40;
return;
}
int x[] = {100, 200 ,300, 400, 500, 600};
changeValue(x);
std::cout << x[3] << std::endl;
std::cout << x[4] << std::endl;
若形參為『陣列名』,則實參也必須是『陣列名』,且型態要一樣。
形參陣列大小可以不指定,即使指定了大小也沒有用(因為編譯器不會檢查大小),因此若實參陣列大小與形參陣列大小不一樣也沒關係。
必須確保操作形參陣列時不會超過定義的範圍(這是程式開發者的責任)。
// 下列三個函數是等價的。
// 每個形參都是 const int*。
void myfun(const int p*);
void myfun(const int x[]);
void myfun(const int x[10]) // 10這個數字,只是表明開發者心中預期的陣列大小而已,編譯器不會檢查陣列大小。
int i = 0, j[2] = {100, 200};
myfun(&i); // 正確。
myfun(j); // 正確。
C++
允許將變數定義為陣列的參考,故函數的形參也可以是陣列的參考(int (&arr)[]
)。
void print(int (&arr)[5])
{
for (auto elem : arr)
{
std::cout << elem << std::endl;
}
}
int m[5]{1, 2, 3, 4, 5};
print(m);
C++ 實際上並沒有真正的多維陣列,所謂的多維陣列,即為『陣列的陣列(嵌套式陣列)』。
與一般陣列一樣,當將多維陣列傳入至函數時,真正傳遞的是陣列的首位址。
多陣陣列的首位址即是『一個陣列』,指標就是指向這個陣列。
#include <iostream>
int main() {
int a[][3] = {1, 2, 3, 4, 5, 6, 7, 8, 9}; // 宣告並初始化二維陣列
int (*ptr)[3][3] = &a; // 宣告並初始化指向陣列的指標
int (**p)[3][3] = &ptr; // 宣告並初始化指向指標的指標
// 輸出指標的值
std::cout << "Address of a: " << a << std::endl;
std::cout << "Value of ptr (should be address of a): " << ptr << std::endl;
std::cout << "Value of p (should be address of ptr): " << p << std::endl;
// 檢查是否相等
std::cout << "Address of a and value of ptr are equal: " << std::boolalpha <<(a == *ptr) << std::endl;
std::cout << "Value of ptr and value pointed by p are equal: " << std::boolalpha << (ptr == *p) << std::endl;
return 0;
}
在這段程式碼中:
a
是二維陣列的首地址(指向
a[0]
)。
ptr
是指向
a
的指標,其值是
a
的位址。
p
是指向
ptr
的指標,其值是
ptr
的位址。
void processArray(int (*arr)[3], int rows, int cols) // 多陣陣列的首位址即是『一個陣列』:int[3]
{
for (int i = 0; i < rows; i++) {
for (int j = 0; j < cols; j++) {
std::cout << arr[i][j] << " ";
}
std::cout << std::endl;
}
}
int myArray[2][3] = {{1, 2, 3}, {4, 5, 6}};
processArray(myArray, 2, 3); // 傳入二维陣列的指標及其列和行數
std::vector
作為參數的簡單函數範例:#include <iostream>
#include <vector>
// 定義一個函數,接受 std::vector<int> 作為參數,並計算向量中所有元素的總和
int sumVector(const std::vector<int>& vec) {
int sum = 0;
for(int num : vec) {
sum += num;
}
return sum;
}
int main() {
// 創建一個包含整數的向量
std::vector<int> numbers = {1, 2, 3, 4, 5};
// 呼叫 sumVector 函數並輸出結果
int result = sumVector(numbers);
std::cout << "The sum of the vector is: " << result << std::endl;
return 0;
}
#include <iostream>
#include <vector>
#include <algorithm>
#include <numeric>
// 定義一個函數,接受 std::vector<int> 作為參數,並對向量進行多種操作
void processVector(const std::vector<int>& vec) {
if (vec.empty()) {
std::cout << "The vector is empty." << std::endl;
return;
}
// 複製向量並進行排序
std::vector<int> sortedVec = vec;
std::sort(sortedVec.begin(), sortedVec.end());
// 計算總和與平均值
int sum = std::accumulate(vec.begin(), vec.end(), 0);
double average = static_cast<double>(sum) / vec.size();
// 找到最大值與最小值
int minValue = *std::min_element(vec.begin(), vec.end());
int maxValue = *std::max_element(vec.begin(), vec.end());
// 輸出結果
std::cout << "Original vector: ";
for (int num : vec) {
std::cout << num << " ";
}
std::cout << std::endl;
std::cout << "Sorted vector: ";
for (int num : sortedVec) {
std::cout << num << " ";
}
std::cout << std::endl;
std::cout << "Sum: " << sum << std::endl;
std::cout << "Average: " << average << std::endl;
std::cout << "Min value: " << minValue << std::endl;
std::cout << "Max value: " << maxValue << std::endl;
}
int main() {
// 創建一個包含整數的向量
std::vector<int> numbers = {5, 3, 8, 1, 9, 6, 2};
// 呼叫 processVector 函數
processVector(numbers);
return 0;
}
return
語句會終止目前正在執行的函數,並將控制權回傳到呼叫該函數的地方。
return
語句有兩種形式:
return;
return expression;
若令回傳型態為 void
的函數回傳其他型態的表達式時,將發生編譯錯誤。
無回傳值的 return
語句只能用在回傳型態是
void
的函數中。
回傳 void
的函數不一定要有 return
語句,因為該類函數最後一行語句後會『隱性』執行 return
語句。
一般情況下,void
函數若想在中間位置提前退出執行,可以使用 return
語句。
void swap(int &v1, int &v2)
{
// 若兩者相等,則不需要交換,並直接退出。
if (v1 == v2)
return;
int tmp = v2;
v2 = v1;
v1 = temp;
// 此處無需顯性的return語句。
}
return
語句的第二種形式提供函數回傳的結果。只要函數回傳的型態不是
void
,則該函數內的每一條 return
語句必須回傳一個值。
return
語句回傳的型態必須與函數的回傳型態一致。若不一致,編譯器會將其轉換為回傳型態。
bool str_subrange(const std::string &str1, const std::string &str2)
{
// 大小相同:
if (str1.size() == str2.size())
return str1 == str2;
auto size = (str1.size() < str2.size())? str1.size() : str2.size();
// 以較短的字串長度為限,檢查兩個string物件的對應字元是否相等。
for (decltype(size) i = 0; i != size; ++i)
{
if (str1[i] != str2[i])
return; // 錯誤1:沒有回傳值,編譯器會檢查到這個錯誤。
}
// 錯誤2:for迴圈可能尚未回傳任何值就結束函數的執行。
// 編譯器可能檢查不出這個錯誤。
}
在含有 return
語句的循環後面應該要有一條
return
語句。若沒有,則為錯。且編譯器不一定檢查的出來。
回傳值的機制與『初始化變數』或『初始化形參』一致:回傳值用來初始化函數調用點的『臨時變數』,該臨時變數就是呼叫函數後的結果。
std::string make_plural(size_t ctr, const std::string &word, const std::string &ending)
{
return (ctr > 1)? word + ending: word;
}
上述函數回傳的型態為
string
,意指回傳值將被『複製到函數調用點』。
故該函數將回傳 word
的副本或是一個未命名的臨時
string
物件(值為 word 與 ending 執行 +
的結果)。
若函數回傳『參考』,該『參考』則是它所綁定之物件的『別名』。
const std::string& shorterString(const std::string &s1, const std::string &s2)
{
return s1.size() <= s2.size() ? s1: s2;
}
int& whichmax(int& a, int& b)
{
return a >= b? a: b;
}
int main(int argc, char const *argv[])
{
int x = 100;
int y = 200;
whichmax(x, y) = 0;
std::cout << x << std::endl;
std::cout << y << std::endl;
return 0;
}
函數執行結束後,局部變數所佔用記憶體空間將被釋放回收。
故函數結束意味著局部變數所綁定(參考)或指向(指標)的物件將不再有意義。
int& createLocalObject() {
int localVariable = 42; // 局部變數
return localVariable; // 回傳其參考
}
int* createLocalObjectPointer() {
int localVariable = 42; // 局部變數
return &localVariable; // 回傳其指標
}
int main() {
int& ref = createLocalObject(); // 錯誤,ref 變成了懸掛參考
int* ptr = createLocalObjectPointer(); // 錯誤,ptr 變成了懸掛指標
// 使用這些參考或指標會導致未定義行為
std::cout << "Ref: " << ref << std::endl;
std::cout << "Ptr: " << *ptr << std::endl;
return 0;
}
執行上述代碼可能會導致以下問題:
若要確保回傳安全的回傳值,可作下列檢查:綁定或指向的物件,是否在函數使用之前就已經存在了?
()
)呼叫運算子(()
)也有優先級與結合律。
呼叫運算子(()
)的優先級與『點運算子(.
)』和『箭頭運算子(->
)』相同,並服從『左結合律』。
因此,若函數回傳指標變數、參考或類別的物件時,可使用執行函數的結果,並存取『物件的成員』。
auto sz = shorterString(s1, s2).size(); // 符合『左結合律』。順序由左至右。
函數的回傳型態決定『呼叫函數』後的結果是否為『左值』:執行一個回傳『參考』的函數其結果為『左值』。其他類型則為『右值』
。
char& get_val(std::string &str, std::string::size_type ix)
{
return str[ix]; // 假設ix值為有效。
}
int main()
{
std::string s("a value");
std::cout << s << std::endl;
get_val(s, 0) = 'A'; // 呼叫函數的結果為『左值』,故可對其近行修改。
std::cout << s << std::endl;
return 0;
}
C++ 11 新標準制定,函數可以回傳 {}
所包括的『列表』。
此『列表』可用來對函數回傳的臨時變數進行初始化。
// process函數的結果可用來對其他 std::vector<string> 型態變數進行列表初始化。
std::vector<string> process()
{
//....
// expected 與 actual 為 std::string 物件
if (expected.empty())
return {};
else if (expected == actual)
return {"functionX", "okay"};
else
return {"functionX", expected, actual};
}
因陣列不能被複製,故函數無法回傳陣列。
但函數可以回傳陣列的『指標』或是『參考』。
因型態定義比較麻煩,故定義回傳陣列指標或參考比較麻煩。
typedef int arrT[10]; // arrT是一個『型態別名』。其表示型態為含有10個整數的陣列
using arrT = int[10]; // arrT的等價宣告,與上述同。
// 函數宣告
arrT* func(int i); // 宣告一個回傳指向含有10個整數的陣列的『指標』。
其中,arrT
是含有 10 個整數的陣列的別名。
因為無法回傳陣列,故將回傳型態定義為『陣列的指標』(arrT*
)。
請熟記以下觀念:
int arr[10]; // arr是一個含有10個整數的陣列。
int* p1[10]; // p1是一個含有10個指標變數的『指標的陣列』。
int (*p2)[10] = &arr; // p2是一個指標,其指向含有10個整數的陣列。
若想定義一個回傳『陣列指標』的函數,則陣列的維度必須在『函數名稱之後』。
函數的形參列表也跟在函數名稱之後,且在陣列維度之前。
語法:
Type (*function (parameter_list)) [dimension]
Type
:元素的型態。
dimension
:陣列的大小。
(*function (parameter_list))
兩端的
()
必須存在。如果 ()
不存在,函數的回傳型態將會是『指標的陣列』。
ex: int (*func(int i)) [10]
func(int i)
:表示呼叫函數時需要 1 個
int
型態的實餐。
(*func(int i))
:表示可以對函數呼叫的結果進行『解參考』。
(*func(int i)) [10]
:表示『解參考』後將得一個長度為
10 的陣列。
int (*func(int i)) [10]
: 表示該陣列內元素的型態為
int
。
#include <iostream>
// 定義回傳指向陣列的指標的函數
int (*func(int size))[10] {
static int arr[10]; // 使用 static 確保陣列在函數結束後依然存在
// 將陣列填入一些數值,這裡僅作範例
for (int i = 0; i < 10; ++i) {
arr[i] = size + i;
}
return &arr; // 回傳指向陣列的指標
}
int main() {
// 呼叫函數並接收回傳的指向陣列的指標
int (*ptr)[10] = func(5);
// 使用指標來存取陣列的元素
for (int i = 0; i < 10; ++i) {
std::cout << (*ptr)[i] << " " << std::endl;;
}
return 0;
}
int (*func(int size))[10]
:這表示
func
是一個接受
int
型態參數
size
的函數,並回傳一個指向長度為
10
的 int
型態陣列的指標。
在函數內部,宣告了一個靜態陣列
arr
,這樣它在函數回傳後仍然存在。
將這個靜態陣列的地址回傳作為函數的回傳值。
C++ 11 提供可簡化上述 func
宣告的方法:『尾置回傳型態(traling return
type)』。
任何函數皆可使用,但對於比較複雜的函數幫助最大。
尾置回傳型態(traling return type):
在『形參列表』後面以 ->
符號開頭。
為表示真正的回傳型態在形參列表之後,在原來的函數回傳型態位置使用
auto
來替代。
auto func(int i) -> int(*) [10];
#include <iostream>
// 使用尾置回傳型態來定義函數
auto func(int size) -> int(*)[10] {
static int arr[10]; // 使用 static 確保陣列在函數結束後依然存在
// 將陣列填入一些數值,這裡僅作範例
for (int i = 0; i < 10; ++i) {
arr[i] = size + i;
}
return &arr; // 回傳指向陣列的指標
}
int main() {
// 呼叫函數並接收回傳的指向陣列的指標
int (*ptr)[10] = func(5);
// 使用指標來存取陣列的元素
for (int i = 0; i < 10; ++i) {
std::cout << (*ptr)[i] << " ";
}
return 0;
}
在 C++ 中,&arr
和 arr
的差異主要在於它們的類型和語意,這是理解陣列與指標行為的關鍵。
arr
當直接使用陣列名稱(如
arr
)時,通常會被『隱式轉換』為指向陣列首元素的指標(int*
)。
類型
arr
的類型:
如果不經特殊處理:會被隱式轉換為
int*
,表示指向陣列首元素的指標。
原始類型:int[10]
。
行為
例如,arr
可以直接用於指向陣列首元素的位置:
int arr[10];
int* ptr = arr; // 合法,arr 被隱式轉換為 int*
使用案例
可以使用 arr
來操作陣列的元素:
arr[0] = 1; // 等價於 *(arr + 0) = 1;
*(arr + 1) = 2; // 存取第二個元素
&arr
&arr
是取陣列的『整體地址』,表示整個陣列的記憶體位址,類型是「指向陣列的指標」。
類型
&arr
的類型是
int (*)[10]
,這是一個「指向大小為 10 的 int
陣列的指標」。行為
&arr
是整個陣列的地址,它和 arr
的表現不同:
int arr[10];
int (*ptr)[10] = &arr; // 合法,ptr 指向整個陣列
使用案例
如果要傳遞整個陣列的指標給函數或操作整個陣列(而不是陣列的首元素),使用
&arr
:
void func(int (*p)[10]) {
// 參數 p 是指向 int[10] 的指標
}
func(&arr); // 傳遞整個陣列的地址
表達式 | 類型 | 意義 |
---|---|---|
arr |
int* |
指向陣列首元素的指標(隱式轉換後)。 |
&arr |
int (*)[10] |
指向整個陣列的指標(包含整個陣列的地址)。 |
記憶體地址比較
arr
和 &arr
在值上看似相同(它們指向相同的地址),但在『型態』上有區別:int arr[10];
std::cout << arr << std::endl; // 地址:指向首元素(arr[0])
std::cout << &arr << std::endl; // 地址:整個陣列的地址
指標偏移比較 假設 arr
開始於記憶體地址 0x1000
:
arr + 1
偏移到 0x1004
(假設
int
是 4 bytes,移到第二個元素)。&arr + 1
偏移到 0x1028
(整個陣列大小是
40 bytes,跳過整個陣列)。視覺化圖解
假設 arr
是一個包含 10 個整數的陣列,記憶體地址從
0x1000
開始:
記憶體地址 | 內容 |
---|---|
0x1000 | arr[0] |
0x1004 | arr[1] |
0x1008 | arr[2] |
… | … |
0x1024 | arr[9] |
arr
的值為
0x1000
,表示首元素的地址。
&arr
的值也是
0x1000
,但類型不同,它是指向整個陣列的指標。
arr + 1
->
0x1004
(下一個元素)。
&arr + 1
->
0x1028
(跳過整個陣列)。
#include <iostream>
int main() {
int arr[10];
std::cout << "arr: " << arr << std::endl; // 輸出陣列首地址
std::cout << "&arr: " << &arr << std::endl; // 輸出整個陣列的地址
std::cout << "arr + 1: " << arr + 1 << std::endl; // 指向第二個元素
std::cout << "&arr + 1: " << &arr + 1 << std::endl; // 跳過整個陣列(+40 bytes)
return 0;
}
輸出示例:
arr: 0x7ffea1a67020
&arr: 0x7ffea1a67020
arr + 1: 0x7ffea1a67024
&arr + 1: 0x7ffea1a67048
arr
和 &arr
有相同的地址。
arr + 1
偏移一個元素。
&arr + 1
偏移整個陣列。
結論
arr
是指向首元素的指標(int*
)。
arr
的隱式轉換允許它被用作普通的指標,與指標算術操作結合非常方便。
&arr
是指向整個陣列的指標(int (*)[10]
)。
操作 arr
是在對陣列中的每個元素進行存取,語意是基於單一元素的操作。
&arr
的語意是把陣列當作一個單位來處理,語意是基於整體的操作。
當對 &arr
進行指標運算時,偏移量是基於整個陣列的大小,而不是單個元素的大小。
型態區別非常重要,尤其是在函數傳遞和指標運算中。
decltype
可使用 decltype
關鍵字來宣告函數回傳型態。
decltype
可用來推導一個表達式的型態,而不需要實際計算該表達式的值。
適用於無初始化值:
簡單情況下,auto
和
decltype
的行為可能一致,但在處理複雜型態時,decltype
提供更多的精確控制。
#include <iostream>
int odd[] = {1, 3, 5, 7, 9};
int even[] = {0, 2, 4, 6, 8};
// 使用 decltype 來定義函數回傳型態
decltype(odd)* arrPtr(int i) {
return (i % 2) ? &odd : &even; // 回傳指向陣列的指標
}
int main() {
// 呼叫函數並接收回傳的指向陣列的指標
int (*ptr)[5] = arrPtr(3);
// 使用指標來存取陣列的元素
for (int i = 0; i < 5; ++i) {
std::cout << (*ptr)[i] << " ";
}
return 0;
}
arrPtr
使用 decltype
代表其回傳型態是個『指標』,且該指標變數所指向的物件與 odd
物件的型態相同。
因 odd
為陣列,故 attPtr
回傳一個指向含有 5 個 int
的陣列指標。
注意:decltype
並『不會』將『陣列型態』轉換成對應的『指標』,故
decltype
的結果是一個陣列。故要加上
*
。
#include <iostream>
#include <array> // 使用 std::array 替代靜態陣列
// 定義函數,回傳型態為 std::array<int, 10>* 指標
std::array<int, 10>* func(int size) {
static std::array<int, 10> arr; // 靜態 std::array,確保函數結束後依然有效
// 初始化陣列
for (int i = 0; i < 10; ++i) {
arr[i] = size + i;
}
// 回傳指向陣列的指標
return &arr;
}
int main() {
// 呼叫函數並取得回傳的陣列指標
auto ptr = func(5);
// 輸出陣列內容
for (int i = 0; i < 10; ++i) {
std::cout << (*ptr)[i] << " ";
}
return 0;
}
靜態陣列 V.S. std::array
靜態陣列:
std::array
:
提供容器功能,支持標準程式庫容器的介面,如:
.size()
:回傳陣列大小。
.begin()
/ .end()
:回傳迭代器,支援範圍
for
循環。
.fill(value)
:將陣列填充為指定值。
.swap()
:交換兩個 std::array
的內容。
支援與 STL 演算法(如 std::sort
)配合使用。
#include <array>
#include <algorithm>
#include <iostream>
std::array<int, 5> arr = {3, 1, 4, 1, 5};
std::sort(arr.begin(), arr.end()); // 使用 STL 排序
for (auto elem : arr) {
std::cout << elem << " "; // 輸出:1 1 3 4 5
}
幾乎與靜態陣列等效的性能。
額外的功能(如 .size()
或
.at()
)只在需要時付出少量性能代價。
作為 STL 容器,使用中更安全且現代化。
適合現代 C++ 程式設計,尤其是需要安全性、可讀性和與 STL 兼容的情況。
int arr[10];
// 使用 auto 推導
auto func1() {
return arr; // 回傳型態是 int* (首元素指標)
}
// 使用 decltype 保留陣列型態
auto func2() -> decltype(arr) {
return arr; // 回傳型態是 int[10] (完整陣列型態)
}
int arr[10];
// auto 會推導為指向首元素的指標
auto a = arr; // a 的型態是 int* (指向 arr[0] 的指標)
// decltype 保留原始型態
decltype(arr) b = arr; // b 的型態是 int[10] (完整的陣列型態)
簡化複雜型態宣告
提高程式可讀性
與泛型結合
decltype
可用於推導模版參數型態。std::declval
std::declval
是用來模擬特定型態的『右值參考』,目的是讓編譯器推導型態,而不需要實例化物件。
語法:std::declval<T>()
特點:
T&&
(右值參考)。decltype
搭配使用:用於推導需要構造特定型態的操作結果型態。#include <utility>
#include <type_traits>
struct MyClass
{
int getValue() const { return 42; }
};
int main(int argc, char const *argv[])
{
// 推導 MyClass::getValue 的回傳型態
using ReturnType = decltype(std::declval<const MyClass>().getValue()); // 回傳型態
// 驗證型態
static_assert(std::is_same<ReturnType, int>::value, "Type mismatch");
return 0;
}
差異:
特性 | decltype | std::declval |
---|---|---|
主要功能 | 推導表達式的型態 | 模擬型態的右值參考,用於型態推導 |
是否執行運算 | 不執行,只獲取表達式的型態 | 不執行,僅模擬型態行為 |
回傳內容 | 表達式的型態 | T&& ,即型態 T 的右值參考 |
是否需要實例化物件 | 需要能執行表達式,但不實際執行 | 不需要實例化型態 |
典型用途 | 推導運算、函數或變數的型態 | 配合 decltype 推導無法實例化型態的型態 |
#include <iostream>
#include <utility>
#include <type_traits>
struct MyClass {
MyClass(int x) : value(x) {} // 沒有預設建構函數
double compute() const { return value * 1.5; }
private:
int value;
};
// 假設我們想知道一個 struct 的成員函數返回的型態,但這個 struct 無法直接實例化(例如,沒有預設建構函數)。
int main() {
// 使用 declval 模擬 MyClass 的右值參考,並推導 compute() 的回傳型態
using ReturnType = decltype(std::declval<const MyClass>().compute());
// 驗證回傳型態是否為 double
static_assert(std::is_same<ReturnType, double>::value, "Type mismatch");
std::cout << "Return type of compute(): " << typeid(ReturnType).name() << std::endl;
return 0;
}
decltype
:直接推導表達式的型態,無需執行。
std::declval
:模擬型態的右值參考,專為型態推導服務,常與
decltype
搭配使用。
如果需要推導一個已存在的表達式的型態,使用
decltype
。
如果需要推導無法直接實例化的型態的操作結果型態,使用
std::declval
與 decltype
配合。
錯誤範例:
#include <iostream>
// 定義函數,回傳型態為 decltype 推導
auto func(int size) -> decltype(&std::declval<int[10]>()) {
static int arr[10]; // 靜態陣列,確保函數結束後陣列依然有效
// 初始化陣列
for (int i = 0; i < 10; ++i) {
arr[i] = size + i;
}
// 回傳指向陣列的指標
return &arr;
}
int main() {
// 呼叫函數並取得回傳的陣列指標
auto ptr = func(5);
// 輸出陣列內容
for (int i = 0; i < 10; ++i) {
std::cout << (*ptr)[i] << " ";
}
return 0;
}
錯誤訊息:cannot take the address of an rvalue of type 'int[10]
。
std::declval
使用不當:
std::declval<int[10]>
不適用於靜態陣列型態(例如
int[10]
)。靜態陣列並非常見的右值參考用使用,應改用更明確的型態描述。
若要推導靜態陣列的指標型態,直接使用 int(*)[10]
即可,無需 std::declval
。
std::declval
常用於複雜型態或無法實例化的類型中,而非單純的陣列。
使用
std::declval<int(&)[10]>
int(&)[10]
表示一個大小為 10
的靜態陣列的參考。
std::declval<int(&)[10]>
用於模擬這樣的參考型態,推導函數回傳型態。
回傳陣列的參考
簡化使用
在 main
函數中,auto& ref
直接接收靜態陣列的參考,透過索引來存取元素。
#include <iostream>
#include <utility> // std::declval
// 定義函數,回傳型態為 decltype 推導
auto func(int size) -> decltype(std::declval<int(&)[10]>()) {
static int arr[10]; // 靜態陣列,確保函數結束後陣列依然有效
// 初始化陣列
for (int i = 0; i < 10; ++i) {
arr[i] = size + i;
}
// 回傳指向陣列的參考
return arr;
}
int main() {
// 呼叫函數並取得回傳的陣列參考
auto& ref = func(5);
// 輸出陣列內容
for (int i = 0; i < 10; ++i) {
std::cout << ref[i] << " ";
}
return 0;
}
原始碼編譯完成後,函數則為一組指令的集合。當呼叫函數時,系統會跳轉至這組指令集的『首位址』開始執行。而當函數回傳時,系統則跳回函數調用處之下一條指令繼續執行。
無論函數呼叫幾次,每次系統都會跳轉至同一位址。
故函數雖然節省記憶體空間,但也付出一定的代價(存在著影響程式性能的系統開銷)。
若函數短小,只有幾條指令時,則跳轉所花費的時間就會站到相當大的比重。
『內聯函數』可以避免跳轉,則程式的執行效率就會大大提高。
int add(int a, int b)
{
return a + b;
}
在 C++ 中,若在函數宣告前加入 inline
關鍵字,則稱為『內聯函數(inline
function)』。
對於內聯函數,編譯器不會創建真實的函數,而只是在函數呼叫處『展開(將函數的程式碼直接複製到調用處)』,來避免系統跳轉,規避使用函數呼叫的代價。
// 函數宣告
inline int add(int a, int b);
// 函數呼叫
int x = add(1, 2);
// 編譯後,實際程式碼如下:
int x = 1 + 2;
若函數是在標頭檔案中『定義』,則編譯器會自動將該函數視為內聯函數。
但使用 inline
關鍵字與標頭檔定義函數的方式,只是對編譯器『建議』,最後決定由編譯器來做判斷。
只能在其所定義的原始檔中被其他函數所調用。
使用 static
關鍵字。
又稱『靜態函數』。
若在不同的原始檔定義了『同名』的函數,若使用 static
修飾這些函數,則即使函數名稱相同,也不會被影響,則可編譯成功。
語法:
static 回傳型態 函數名(形參列表) {};
若定義函數時不使用關鍵字 static
修飾,則其為『外部函數』。
在呼叫該函數的其他原始檔中,只要加入『函數宣告』即可。
可以將這些外部函數的宣告放在一個標頭檔(.h
)中,任何原始檔(.cpp
)只要在開頭
#include
該標頭檔,則在這些原始檔內就可以調用其他原始檔所定義的外部函數。
若在同一個作用域內,有多個函數名稱相同但其形參列表不同,則稱之為『重載(overloaded)函數』。
重載函數接收不同型態的形參,但執行的操作非常類似。
當呼叫重載函數時,編譯器會根據傳遞的『實參』型態來推斷要執行的是哪個函數。
函數重載可以減輕程式設計師記憶函數名稱的負擔。
C++ 區分函數依靠的不只是『函數名稱』,還有函數的『形參列表』。
int func();
int func(int);
int func(double);
int func(int, double);
同一個作用域內幾個函數名稱相同但『形參列表不同』,稱之為『函數重載(overloaded)』。
void print(const char *cp);
void print(const int *begin, const int *end);
void print(const int ia[], size_t size);
上述函數的形參列表不同,但執行的操作非常類似。
當呼叫這些函數時,編譯器會根據所傳遞的『實參型態』來判斷要使用哪一個函數。
int j[2] = {0, 1};
print("Hello World"); // 呼叫void print(const char*)。
print(j, end(j) - begin(j)); // 呼叫void print(const int*, const int*)。
print(begin(j), end(j)); // 呼叫void print(const int ia[], size_t size);
對於被重載的函數而言,它們必須在『形參個數』與『形參型態』有所不同。
注意:函數的『回傳型態』並無法用來區分函數(以函數的『簽名』來區分)。
函數重載可以減輕為函數取名與記住函數名稱的負擔。
main
函數無法被重載。
『運算子』也可以像函數一樣進行『重載』:『運算子重載』。
大多數情況下,很容易確定某次函數調用應該選用哪個重載函數。
當某些重載函數的形參數量相等,但形參的型態可從其他型態轉換得來時,重載函數的選擇就會不容易。
void f();
void f(int);
void f(int, int);
void f(double, double = 3.14);
f(5.6); // 呼叫 void f(double, double);
『函數匹配』第一步:選定本次調用對的『重載函數集合』。集合中的函數稱為『候選函數(candicate function)』。
與被呼叫的函數同名。
其『宣告』在函數呼叫時可見。
『函數匹配』第二步:檢查該次調用所提供的『實參』,接著從候選函數中選擇能被這組實參調用的函數(可行函數,viable funcion)。
形參數量與實參數量相同。
每個實參的型態與對應的形參相同,或是能轉換成形參的型態。
『函數匹配』第三步:從可行函數選擇與此次調用最匹配的函數。
實參型態與形參型態越接近,匹配越好。
精確匹配比需要型態轉換的匹配更好。
有時候兩個形參列表看起來不一樣,但實際上編譯器會認定為相同。如下列:
形參名稱不重要
Record lookup(const Accont &acct);
Record lookup(const Accont &); // 省略形參的名稱。故形參名稱不重要。
typedef Phone Telno;
Record lookup(const Phone&);
Record lookup(const Telno&); // Telno與Phone是指一樣的型態。
const
形參注意:Top-Level const
對傳入函數的物件沒有影響。
因此:編譯器『無法』區分出具有
Top-Level const
的形參與另一個沒有
Top-Level const
的形參的差別。
Record lookup(Phone);
Record lookup(const Phone); // 錯誤:重複宣告了Record lookup(Phone)。
Record lookup(Phone*);
Record lookup(Phone* const); // 重複宣告了Record lookup(Phone*)。
但是,當形參(Low-Level
const
)為『指標』或『參考』(間接存取)時,則透過區分其指向或綁定的物件(實參)為
『const
』 或『非 const
』
來實現函數重載。
因為:雖然上述四個函數都能綁定『非
const
』的物件,或是指向『非
const
』物件的指標,但編譯器會優先選擇『非
const
』版本的函數。
Record lookup(Account&); // 函數接受Account的參考
Record lookup(const Account&); // 視為不同函數,接受 const 參考。
Record lookup(Account*); // 函數接受Account的指標
Record lookup(const Account*); // 視為不同函數,接受 const 指標。
Account a; lookup(a);
呼叫
lookup(Account&)
const Account a; lookup(a);
呼叫
lookup(const Account&)
Account* p; lookup(p);
呼叫
lookup(Account*)
const Account* p; lookup(p);
呼叫
lookup(const Account*)
函數也是有位址的:編譯後的函數,本質上是一組指令的集合。
當程式在執行時,會將這組指令集合存放在記憶體空間中。且其『起始位址』就是該函數的位址,也稱為函數的入口點位址。
『函數指標』就是指向函數(存放函數記憶體位址)的指標變數。
函數指標變數,可用來裝進不同的指標值(記憶體位址),近而指向不同的函數,但函數型態必須相符。
函數也是有型態:函數的型態是由『回傳型態』與『形參型態』共同決定,與函數名稱無關。
可使用『函數名稱』來代表函數的位址;也可在函數名稱之前加上『取址運算子
&
』來取得函數位址。兩者為等價方法。
範例:
int Add(int a, int b)
{
return a + b;
}
Add
以及 &Add
都代表
Add
函數的記憶體位址。範例:
// 比較兩個string物件的長的
bool lengthCompare(const std::string&, const std::string&); // 該函數型態為:bool (const std::string&, const std::string&)
該函數型態為:bool (const std::string&, const std::string&)
想要宣告一個指向該函數指標變數,只要將『函數名』替換為『指標』即可:
bool (*pf)(const std::string&, const std::string&);
pf
是一個『函數指標(變數)』。
*pf
兩端的 ()
不可省略。若省略,則
pf
為一個回傳值為
bool
指標的函數:bool *pf(const std::string&, const std::string&);
當我們直接使用『函數名』時,則將自動轉換為指標值。
int add(int x, int y)
{
return x + y;
}
int (*funPtr1)(int, int);
int (*funPtr2)(int, int);
funPtr1 = add;
funPtr2 = &add;
// 列印出函數指標變數的指標值。
std::cout << "funPtr1 函數的記憶體位址: " << reinterpret_cast<void*>(funPtr1) << std::endl;
std::cout << "funPtr2 函數的記憶體位址: " << reinterpret_cast<void*>(funPtr2) << std::endl;
// printf("funPtr1 函數的記憶體位址: %p\n", funPtr1);
// printf("funPtr2 函數的記憶體位址: %p\n", funPtr2);
也可以直接使用指向函數的指標呼叫函數,無需先『解參考』函數指標(也可以進行『解參考』)。
有時候為了明確看出所使用的物件是一個函數指標,也會進行函數指標『解參考』。
int add(int x, int y)
{
return x + y;
}
int (*pf)(int a, int b) = nullptr;
pf = add;
int x = pf(3, 4);
int y = (*pf)(3, 4)
當使用函數重載時,編譯器必須清晰地界定應該選擇哪個函數。例如:
void ff(int*);
void ff(unsigned int);
void (*pf1)(unsigned int) = ff; // pf1指向ff(unsigned)
編譯器透過『指標型態』決定選擇哪一個函數,尤其,指標型態必須與重載函數中的某一個『精確匹配』:
// void (*pf2)(int) = ff; // 錯誤:沒有任何一個ff與該形參列表匹配。
// double (*pf3)(int*) = ff; // 錯誤:ff與pf3回傳型態不匹配。
函數的參數可以是一般的變數、指向一般變數的『指標』、『陣列名』、『指向陣列的的首位址』的『指標變數』。
現在,指向函數的指標變數(函數指標)也可以作為參數,從而實現函數位址的傳遞。
int max(int x, int y)
{
if (x > y)
return x;
return y;
}
int wwmax(int x, int y, int (*midfun)(int, int))
{
int result = midfun(x, y);
return result;
}
int main(int argc, char const *argv[])
{
int c;
c = wwmax(5, 19, max); // 傳入函數名。
std::cout << "c = " << c << std::endl;
int (*p) (int, int) = nullptr; // 定義函數指標變數,並以nullptr初始化。
p = max;
c = wwmax(45, 31, p); // 傳入函數指標變數。
std::cout << "c = " << c << std::endl;
return 0;
}
與『陣列』類似,形參無法定義出函數型態,但形參可以是指向函數的指標。
形參看起來是函數型態,但實際上是指向函數的指標變數。
// 第三個參數是函數型態,他會自動地轉換為指向函數的指標
void useBigger(const sd::string &s1, const std::sting &s2,
bool pf(const std::string &, const std::string &));
// 等價的宣告:顯性地將形參定義為指向函數的指標
void useBigger(const sd::string &s1, const std::sting &s2,
bool (*pf)(const std::string &, const std::string &));
呼叫函數時,可直接把函數作為『實參』使用,此時它會被自動轉換為指標。
useBigger(s1, s2, lengthCompare);
簡化函數指標宣告:
// Func和Func2為函數型態。
typedef bool Func(const std::string &, const std::string &);
typedef decltype(lengthCompare) Func2
// FuncP和FuncP2 是指向函數的指標
typedef bool(*FuncP)(const std::string &, const std::string &);
typedef decltype(lengthCompare) *FuncP2;
decltype
回傳的是函數型態,此時不會將函數型態自動轉換成指標型態。
因 decltype
的結果是函數型態,故只有在結果上面加上
*
才能得到指標。
特性 | 函數型態 | 函數指標型態 |
---|---|---|
定義 | 描述函數簽名 | 儲存函數地址的指標 |
是否包含記憶體地址 | 不包含記憶體地址 | 包含函數的記憶體地址 |
用法 | 定義函數或描述其簽名 | 用於存取或呼叫函數 |
例子 | int(int, int) |
int (*)(int, int) |
用途 | 描述函數結構 | 動態選擇或間接呼叫函數 |
能否作為參數傳遞 | 不可以 | 可以 |
函數型態描述的是函數的簽名。
函數指標型態是指向特定函數的指標,用來存取和調用函數。
函數指標提供了靈活性,例如可以用來實現策略模式、回調(callback)函數等。
std::function
是 C++
標準庫中提供的一個通用函數包裝器,位於標頭檔案
<functional>
中。它能夠
存儲、複製和調用任何可以被調用的物件,包括:
普通函數
Lambda 表達式
函數指標
成員函數指標
函數物件(Function Object)
這使得 std::function
成為一種靈活且強大的工具,特別是在需要回調函數或泛型函數的場景中。
語法:
#include <functional> // 必須包含
std::function<回傳型態(參數列表)> 名稱;
#include <iostream>
#include <functional>
#include <string>
using namespace std;
// 定義執行策略的函數
void executeStrategy(std::function<int(int, int)> strategy, int a, int b) {
cout << "結果: " << strategy(a, b) << endl;
}
int main() {
// 定義多個策略
std::function<int(int, int)> add = [](int x, int y) { return x + y; };
std::function<int(int, int)> multiply = [](int x, int y) { return x * y; };
std::function<int(int, int)> subtract = [](int x, int y) { return x - y; };
// 動態決定策略
string operation;
cout << "請選擇操作(add, multiply, subtract): ";
cin >> operation;
if (operation == "add") {
executeStrategy(add, 3, 4); // 加法策略
} else if (operation == "multiply") {
executeStrategy(multiply, 3, 4); // 乘法策略
} else if (operation == "subtract") {
executeStrategy(subtract, 3, 4); // 減法策略
} else {
cout << "未知的操作!" << endl;
}
return 0;
}
靜態 vs 動態:
原範例中的策略是直接在程式碼中選定的,屬於「靜態地」選擇策略。
改進範例則根據使用者的輸入,動態決定選擇哪個策略執行。
執行期選擇邏輯:
當程式運行時,輸入的 operation
決定了
executeStrategy
的實際行為。
如果輸入 add
,則執行加法邏輯;如果輸入
multiply
,則執行乘法邏輯。
『動態』的意思是指
行為在執行期而非編譯期決定。這讓程式具備更高的靈活性,可以根據不同的需求或輸入執行不同的邏輯。使用
std::function
包裝不同策略後,程式不僅可以靜態配置策略,也能根據執行期的條件動態調整行為。
性能討論
與函數指標相比,std::function
會有一些額外的性能損耗,因為它可能涉及記憶體分配和多態的使用。
如果性能非常重要且不需要支持多種『可呼叫物件(callable object)』,可直接使用函數指標或模板。
專案是由一個或多個原始檔所組成。
專案可透過『預處理』、『編譯』、『連結』等過程產生一個執行檔。
編譯是以一個一個的原始檔(.cpp
)為單位進行,每個原始檔都會被編譯為『目標檔(.o
或 .obj
)』。
若原始檔有多個,則會編譯成多個『目標檔(object file)』,接著會將這些目標檔案進行連結,最後產生一個『執行檔』。
程式在編譯前會先經過『預處理』的階段:
預處理是根據程式中的 『macro(巨集)』
指令(都是以 #
開頭的指令)來補充與完善原始程式碼:
marco 定義:#define
檔案包含:#include
條件編譯
一般型式: #define 巨集名 被替換的內容
#define PI 3.1415926 // 注意:末尾沒有分號 ;
以 PI
來替代 3.1415926
。
在預處理階段,所有在 #define
語句後的程式碼中出現
PI
都會被替代為 3.1415926
。
#define
為『巨集(marco)定義』,實現以一個簡單的名稱(巨集名)來替代一個較長內容的效果。
在預處理時將『巨集名』替換為指定內容的過程稱為『巨集展開』。
『巨集名』習慣上用大寫字母表示。
巨集定義並不是 C/C++
語言語句,不需在行末加;
。
#define PI 3.1415926;
x = 2 * PI; // x = 2 * 3.1415926;;
y = PI * 2; // 錯誤:x = 3.1415926; * 2;
#define
指令編寫在程式中函數的『外面』。
『巨集名』的有效範圍為 #define
之後到『原始檔案結束』,注意:無法跨檔案使用。
因此,若要在另一個原始檔案中使用,則必須在另外的原始檔案中也做相同的定義,或是將這些
#define
定義統一放在一個公用檔案(如標頭檔)內,並使用
#include
將這些公用檔案包含到每個原始檔案中。
可使用 #undef
指令終止巨集定義的作用域。
#define PI 3.1415926
int main()
{
float x;
x = PI * 2;
print("x = %f\n", x);
return 0;
}
#undef PI
void func1()
{
float x;
x = PI * 2; // 注意:這行程式會出錯,因為PI已經被 #undef 終止。
}
使用 #define
進行巨集定義時,還可使用已定義的巨集,可以層層置換。
#define PI 3.1415926
#define DPI 2 * PI
#define DPICPI PI * DPI
int main()
{
float ftmp;
ftmp = PI * 2;
ftmp = DPI;
ftmp = DPICPI;
}
注意:字串內的字元即使與巨集名相同,並不會進行替換。
帶參數的巨集定義,不僅是簡單的內容替換,還要進行參數替換。
一般型式: #define 巨集名(參數列表) 被替換的內容
#define S(a, b) = a * b
........
int Area = S(3, 2);
系統會將3
, 2
分別替代巨集定義的形參
a
, b
,最終 3*2
替換
S(3, 2)
。
故 int Area = S(3, 2)
等價於
int Area = 3 * 2
。
#define PI 3.1415926
#define S(r) PI * r * r
int main()
{
float area;
area = S(3.6); // 等價於3.1415926*3.6*3.6
return 0;
}
若
area=S(1+5)
,替換後會變成3.1415926*1+5*1+5,這會與預期不同。為了解決此問題,要在形參外面加上()
,
如下所定義:
#define S(r) PI * (r) * (r)
巨集定義時,巨集名與帶參數的 ()
之間不能有空格。
巨集展開並不會對 1+5 求值,只單純做替換而已。
函數呼叫是在執行期階段執行到該函數才運行其中的程式碼,其中將涉及所呼叫的函數分配臨時記憶體空間等一系列工作。
但巨集展開只是在編譯期階段進行,且展開時亦不會分配記憶體空間。
巨集的參數並沒有型態的定義,其只是一個符號。
巨集展開只會佔用編譯時間,不會花費執行的時間。反之,函數呼叫會佔用執行時間,其中包含分配記憶體空間、傳遞參數、執行函數體與回傳。
『檔案包含』是指:一個檔案(原始檔 .cpp
)可以透過
#include
指令將另外一個檔案(標頭檔
.h
)的全部內容包含進來。
#include "檔案名"
#include
指令可以節省大量的重複動作:可將一些公用的內容寫成一個檔案(.h
),如『巨集定義』、函數宣告
等,然後每個原始檔都透過 #include
指令將此公用檔案包含進來。
一般而言,#include
都是包含一個 .h
檔案,很少包含 .cpp
的情況。
一旦修改 .h
檔案,相當於修改所有
#include
這個 .h
的所有原始檔。
一條 #include
指令只能包含一個檔案。
#include
可以嵌套
// base_math.h
// 這是基礎數學函數的標頭檔。
#ifndef BASE_MATH_H
#define BASE_MATH_H
// 基礎數學函數的宣告
int add(int a, int b);
int subtract(int a, int b);
#endif // BASE_MATH_H
// advanced_math.h
// 這是擴展數學函數的標頭檔。
#ifndef ADVANCED_MATH_H
#define ADVANCED_MATH_H
#include "base_math.h" // 嵌套包含基礎數學函數的頭文件
// 擴展數學函數的宣告
int multiply(int a, int b);
int divide(int a, int b);
#endif // ADVANCED_MATH_H
// math_operations.cpp
// 這是數學操作的實現文件。
#include "advanced_math.h" // 只需包含擴展頭文件
// 基礎和擴展函數的定義
int add(int a, int b) {
return a + b;
}
int subtract(int a, int b) {
return a - b;
}
int multiply(int a, int b) {
return a * b;
}
int divide(int a, int b) {
if (b != 0) {
return a / b;
} else {
return 0; // 簡單的錯誤處理
}
}
在實作的程式 math_operations.cpp
中,只需包含 advanced_math.h
。
由於 advanced_math.h
已經嵌套包含了
base_math.h
,所以這裡可以存取所有宣告的函數。
使用 <>
是在『系統目錄』尋找所包含的檔案。
使用 ""
是優先在『當下目錄』查找所包含的檔案,若找不到則會再去『系統目錄』查找。
在產生『執行檔』的過程中,原始檔中所有程式碼都會進行編譯,但有時會希望對其中的某一段程式只在滿足某些條件下才參與編譯,或是當某一條件滿足時對一段語句進行編譯等,則為『條件編譯』。例如:寫一些跨作業系統平台的程式時。
條件編譯是一種靈活而強大的工具,能夠根據不同的需求動態地選擇性編譯程式碼。掌握條件編譯可以使程式更具適應性和可移植性,特別是在開發複雜或跨平台的應用程式時。
#include <iostream>
int main() {
#ifdef _WIN32
std::cout << "This code is compiled on Windows." << std::endl;
#elif __APPLE__
std::cout << "This code is compiled on macOS." << std::endl;
#elif __linux__
std::cout << "This code is compiled on Linux." << std::endl;
#else
std::cout << "Unknown operating system." << std::endl;
#endif
return 0;
}
常見用途
跨平台程式設計: 根據作業系統或硬體平台選擇性地編譯不同的程式碼段。
除錯與測試: 在測試環境下編譯除錯資訊或測試代碼,而在正式環境中排除這些代碼。
配置選項: 根據不同的編譯選項啟用或禁用某些功能模組。
#include <iostream>
// 定義一個巨集來控制功能啟用
#define FEATURE_ENABLED 1
int main() {
#if FEATURE_ENABLED
std::cout << "Feature is enabled." << std::endl;
#else
std::cout << "Feature is disabled." << std::endl;
#endif
return 0;
}
形式1:
#ifdef 巨集名
程式碼1
#else
程式碼2
#endif
#define
定義的),則對 程式碼1
進行編譯,否則對 程式碼2 進行編譯。#define DEBUG
#ifdef DEBUG
std::cout << "輸出一些除錯的訊息" << std::endl;
#endif
形式2:
#ifndef 巨集名
程式碼1
#else
程式碼2
#endif
#define
定義),則對
程式碼1 進行編譯,否則對 程式碼2 進行編譯。形式3:
#if 表達式
程式碼1
#else
程式碼2
#endif
true
時則編譯程式碼1,否則編譯程式碼2。#if 表達式1
程式碼1
#elif 表達式2
程式碼2
#else
程式碼3
#endif
『命名空間(namespace)』:為了必免程式中『名稱』發生衝突所引入的機制。
可定義多個命名空間,每個命名空間都有自己的名字,不可同名。
命名空間就是一個『作用域(scope)』。
若一個命名空間內定義的函數與另一個命名空間的函數同名,兩者互不影響(因為不同命名空間,代表不同的作用域)。
語法:namespace 命名空間名 {}
(注意:無需;
)
命名空間定義可以不連續,可以寫在不同的位置,不同的原始檔。
存取方式:使用『作用域運算子::
』:
命名空間::物件名
。// test.cpp
namespace MyName
{
void func()
{
std::cout << "MyName::func函數被執行。" << std::endl;
}
}
// using namespace 命名空間;
int main()
{
MynName::func();
}
// MyProject2.cpp
namespace MyName2
{
void func()
{
std::cout << "MyName::func函數被執行。" << std::endl;
}
}
std::cout
:
std
:標準程式庫定義的命名空間。
cout
:iostream
相關的物件。cout
物件被稱為『標準輸出』,一般用於向電腦螢幕輸出內容。
可以使用 using
關鍵字來簡化命名空間的使用:
#include <iostream>
namespace MyNamespace {
void function() {
std::cout << "Inside MyNamespace::function" << std::endl;
}
}
int main() {
using namespace MyNamespace;
function(); // 不需使用作用域運算子
return 0;
}
匿名命名空間(anonymous namespace)
匿名命名空間是指『沒有名稱』的命名空間。
被用來限制變數和函數的作用域僅在定義它們的編譯單元(通常是單個.cpp
檔案)內。
這種命名空間的成員無法在其他編譯單元中直接存取,有效防止命名衝突。
匿名命名空間避免了全局名稱空間的污染,有效防止了名稱衝突,特別適合大型專案和多個程式庫的使用。
namespace {
// 匿名命名空間內的變數和函數
int internalVariable = 42;
void internalFunction() {
std::cout << "This function is in an anonymous namespace." << std::endl;
}
}
#include <iostream>
// 匿名命名空間
namespace {
int internalVariable = 42;
void internalFunction() {
std::cout << "This function is in an anonymous namespace." << std::endl;
}
}
int main() {
// 可以直接存取匿名命名空間中的成員
std::cout << "internalVariable: " << internalVariable << std::endl;
internalFunction();
return 0;
}
internalVariable
和
internalFunction
都定義在『匿名命名空間』中,故它們只能在這個.cpp
檔案內部使用,無法在其他.cpp
檔案中直接存取。匿名命名空間 V.S. 全局變數:
作用域(Scope):
全局變數:在整個程序的所有編譯單元中都可見。
匿名命名空間:變數和函數只在定義它們的編譯單元內可見。
連結性(Linkage):
全局變數:具有外部連結(external linkage),可以在多個編譯單元中使用和定義。
匿名命名空間:具有內部連結(internal linkage),只能在定義它們的編譯單元內使用。
名稱衝突:
全局變數:在大型專案中,容易與其他編譯單元中的變數名稱衝突。
匿名命名空間:防止名稱衝突,因為每個匿名命名空間的成員在其他編譯單元中不可見。
使用情境
全局變數:適合需要在多個編譯單元中共享的變數,但要小心命名衝突和意外修改。
匿名命名空間:適合不需要跨編譯單元共享的變數和函數,確保變數和函數只在局部範圍內可見,避免名稱衝突。
#include <iostream>
// 全局變數
int globalVariable = 100;
// 匿名命名空間
namespace {
int internalVariable = 42;
void internalFunction() {
std::cout << "This function is in an anonymous namespace." << std::endl;
}
void displayInternalVariable() {
std::cout << "internalVariable: " << internalVariable << std::endl;
}
}
// 全局函數
void displayGlobalVariable() {
std::cout << "globalVariable: " << globalVariable << std::endl;
}
int main() {
// 使用全局變數和函數
std::cout << "Accessing global variable and function:" << std::endl;
displayGlobalVariable();
globalVariable += 10;
displayGlobalVariable();
// 使用匿名命名空間內的變數和函數
std::cout << "\nAccessing internal variable and function:" << std::endl;
displayInternalVariable();
internalFunction();
return 0;
}
其他範例
#include <iostream>
int value = 100; // 全局變數
void displayValue() {
std::cout << "Global value: " << value << std::endl;
}
int main() {
int value = 50; // 局部變數
std::cout << "Local value: " << value << std::endl;
std::cout << "Accessing global value: " << ::value << std::endl; // 使用 :: 存取全局變數
displayValue();
return 0;
}
#include <iostream>
namespace NamespaceA {
void display() {
std::cout << "Inside NamespaceA" << std::endl;
}
}
namespace NamespaceB {
void display() {
std::cout << "Inside NamespaceB" << std::endl;
}
}
int main() {
NamespaceA::display(); // 使用 :: 存取 NamespaceA 中的 display 函數
NamespaceB::display(); // 使用 :: 存取 NamespaceB 中的 display 函數
return 0;
}
#include <iostream>
class MyClass {
public:
static int staticValue;
static void display() {
std::cout << "Static value: " << staticValue << std::endl;
}
};
int MyClass::staticValue = 10; // 定義靜態成員
int main() {
MyClass::staticValue = 20; // 使用 :: 設定靜態成員
MyClass::display(); // 使用 :: 存取靜態成員函數
return 0;
}
#include <iostream>
namespace OuterNamespace {
namespace InnerNamespace {
void display() {
std::cout << "Inside InnerNamespace" << std::endl;
}
}
}
class OuterClass {
public:
class InnerClass {
public:
static void display() {
std::cout << "Inside InnerClass" << std::endl;
}
};
};
int main() {
OuterNamespace::InnerNamespace::display(); // 使用 :: 存取嵌套命名空間中的成員
OuterClass::InnerClass::display(); // 使用 :: 存取嵌套類別中的成員
return 0;
}
C++ 11之後,lambda 表達式成為 C++ 的一部分。
允許在不需要單獨定義函數的情況下撰寫緊湊,短小、可在其被定義的地方立即執行的函數程式碼。
可呼叫(callable)物件:
函數
函數指標
lambda 表達式(lambda expression)
lambda
表達式與一般函數相比不需要『函數名』(匿名函數),取而代之是多了一對『方括號([ ]
)』。
lambda 表達式亦使用『尾置回傳型態(traling return type)』宣告其回傳值型態。
語法:[捕捉列表](參數列表) mutable -> 回傳型態 {語句}
[捕捉列表]
:捕捉列表。
(參數列表)
:參數列表。
mutable
:mutable
修飾符。
-> 回傳型態
:回傳值型態。
{語句}
:函數體。
其中,『參數列表
』與『回傳型態
』為『可選』。『捕捉列表
』與『函數體
』可為空。
若 lambda 表達式的函數體未指定回傳型態,則回傳
void
。
最簡略的 lambda 表達式:[]{};
int boys = 4, girls = 3;
auto totalChild = [girls, &boys]() -> int { return girls + boys; };
std::function<int(int, int)> add = [](int a, int b) { return a + b; };
// 使用 std::function 來儲存 lambda 表達式的好處是,它提供了一種標準化的方式來儲存和傳遞不同類型的可呼叫物件,包括函數指針、成員函數指針以及 lambda 表達式。這種做法在需要對不同的函數進行抽象或者在容器中存儲不同的函數時非常有用。
std::cout << totalChild() << std::endl;
auto func1 = []{};
int a = 3;
int b = 4;
auto func2 = [=] {return a + b;};
auto func3 = [=, &b](int c) -> int {return c += a + b;};
std::cout << func3(100) << std::endl;
[捕捉列表]
有下列多種形式:
[var]
:『值傳遞(pass by value)』的方式捕捉變數
var
。
[=]
:『值傳遞(pass by
value)』的方式捕捉所有『enclosing scope』的變數(包括
this
)。
[&var]
:『參考傳遞(pass by
reference)』捕捉變數 var
。
[&]
:『參考傳遞(pass by
reference))』的方式捕捉所有『enclosing scope』的變數(包括
this
)。
[this]
:『值傳遞(pass by value)』的方式捕捉當下
this
指標。
『enclosing scope』:指的是『包含』
lambda 函數的語句。例如:可能是 main
函數的作用域。
[=, &a, &b]
:表示以『參考傳遞』的方式捕捉變數
a
與
b
,以『值傳遞』的方式捕捉其他所有變數。
請注意,捕捉列表不允許變數『重複傳遞』。
int x = 1, y = 2, z = 3;
// auto func1 = [=, y]() { return x + y; }; // 錯誤:重複捕捉a變數。
// auto func2 = [&, &z]() {return x + z;}; // 錯誤:以參考傳遞捕捉所有變數,但是z重複捕捉。
關鍵字 mutable
用於 lambda
表達式允许 lambda 函数修改其所捕捉的變數,即使在 lambda
函数體内部是以值傳遞。
一般情況下,lambda 表達式以『值傳遞』的方式捕捉外部變數,意指
lambda 内部不能修改以值方式捕捉的變數。但若使用
mutable
,則可在 lambda
函数『内部』修改這些變數。
在帶有 mutable
的例子中,即使
x
仍然是以值方式捕獲,lambda
表達式現在可以修改它的捕獲『副本』。需要注意的是,這種修改只影響 lambda
內部的副本,並不會改變原始 x
變數的值。,如下例:
int x = 5;
// 以值方式捕捉 x,並使用 mutable 關鍵字允许修改 x
auto lambda = [x]() mutable
{
x += 10;
std::cout << "Inside lambda: x = " << x << std::endl;
};
lambda(); // 呼叫 lambda 函数
// 外部的 x 並没有被修改
std::cout << "Outside lambda: x = " << x << std::endl;
『值傳遞(pass by value)』加上
mutable
:適用於希望在 lambda
內部進行修改,但不影響外部變數的情況。
auto
型態推論C++ 11 標準規定關鍵字 auto
為新的型態指定符(type-specifier),代表變數的型態是由編譯器在編譯期推論得知。
auto
宣告的變數必須被初始化,以讓編譯器能夠從初始化表達式中推論其型態。
是一種『佔位符』的概念,編譯器在編譯期會將 auto
替代為變數實際的型態。
auto x = 1; // x的型態為int。
auto name = "Hello World\n.";
struct m
{
int i;
} str;
auto str1 = str; // str1的型態為struct m
// auto z; // 錯誤:無法進行推進。
auto z = x; // z的型態為int。
#include <string>
#include <vector>
void loopover(std::vector<std::string> &s)
{
std::vector<std::string>::iterator i = vs.begin(); // 定義迭代器i。
for(; i < vs.end(); ++i)
{
// 語句
}
}
上例必須寫出型態
std::vector<std::string>::iterator
來定義迭代器
i
。
若使用關鍵字 auto
可寫出簡單且可讀性高的程式碼。
#include <string>
#include <vector>
void loopover(std::vector<std::string> &s)
{
for(auto i = v.begin(); i < vs.end(); ++i)
{
// 語句
}
}
auto
的使用,可使 STL
變得更加容易使用,並讓程式碼更清晰可讀。auto
可以免除型態宣告的麻煩,或是降低發生型態宣告錯誤的頻率。
class PI
{
public:
double operator*(float v)
{
return static_cast<double>(val * v);
}
const float val = 3.1415926f;
};
int main()
{
float radius = 1.7e10;
PI pi;
// float circumference = 2 * (pi * radius); // circumference為float型態。
auto circumference = 2 * (pi * radius); // circumference為double型態。
}
對於不同平台程式碼的維護,auto
也可以帶來一些『泛型』的好處。以 strlen
函數為例,在32位元的編譯環境下,strlen
回傳的是 4 位元組的
int
型態,而在64位元的編譯環境,strlen
回傳 8
位元組的 int
型態。雖然 size_t
型態可以支援多平台程式碼共用,但使用關鍵字 auto
同樣具備相同效果。
auto var = strlen("Hello World");
unsigned int a = 4294967295; // 最大的unsigned int值
unsigned int b = 1;
auto c = a + b; // c的型態依舊是unsigned int,並不會自動擴展。『auto並不會提高精確度』。
std::cout << "a = " << a << std::endl;
std::cout << "b = " << b << std::endl;
std::cout << "a + b = " << c << std::endl; // a + b = 0,
auto
只是用來推斷表達式的結果類型,它不會改變表達式的算術行為。
auto
應用於『模板(template)』定義中,『自適應性』會得到更充分的體現。
template<typename T1, typename T2>
double Sum(T1 &t1, T2 &t2)
{
auto s = t1 + t2; // s的型態會在模板實例化時被推論出來。
return s;
}
int main(int argc, char const *argv[])
{
int a = 3;
long b = 5;
float c = 1.0f, d = 2.3f;
auto e = Sum<int, long>(a, b); // s的型態會在模板實例化時被推論為long。
auto f = Sum<float, float>(c, d); // s的型態會在模板實例化時被推論為float。
return 0;
}
Sum
函數模板接收兩個參數。型態 T1
與
型態 T2
要在模板實例化時才能確定,故在 Sum
中將變量 s
的型態宣告為 auto
。
在主函數 main
中將模板實例化時,Sum<int, long>
中的 s
變數會被推論為 long
型態,而
Sum<float, float>
中的 s 變數則會被推論為
float
。
當 auto
搭配模板一起使用時,其『自適應性』能夠強化
C++ 的『泛化』能力。
auto
也會有使用上的限制。
auto
不能作為形參的型態(即使形參有設定預設值可供推論)。
在 struct
會 class
定義中,『非靜態成員變數』不能設定
auto
。這是因為類或結構的成員變數需要在類型定義時明確其類型。
無法在實例化模板時使用 auto
作為模板參數。例如:無法使用
std::vector<auto> myvector;
。
無法宣告 auto
陣列。這是因為
auto
用於型態推論,而在陣列宣告中,需要提供一個確切的、非推論的型態,以便編譯器確定陣列的大小和佔用的記憶體空間。
decltype
在 C++ 發展中,型態推論是隨著模板與泛型(泛型編程是指編寫可以與任何數據類型一起工作的程式碼)的廣泛使用而加入。
在非泛型編程中,並不太需要進行型態推論,因為表達式中的變數型態與函數的回傳值型態都是確定的。
而在泛型編程中,型態不可知。最適當的解決方法就是讓編譯器來進行型態推論。
decltype
的型態推論並不像 auto
是從變數宣告的初始化表達式得到變數的型態,而是使用一個『表達式』作為參數,回傳該表達式的型態。
decltype
與 auto
相同的是,可作為型態指定符,並用來定義另外一個變數。
decltype
與 auto
都是在『編譯期』進行的。
decltype
常與
typedef
/using
搭配使用。
decltype
來推論某個表達式的型態,並透過 typedef
或
using
為這個型態定義一個新名稱,從而使程式碼更加清晰可讀。decltype
評估表達式的型態,而不會實際求取表達式之值。
using size_t = decltype(sizeof(0));
using ptrdiff_t = decltype((int*)0 - (int*)0);
using nullptr_t = decltype(nullptr);
decltype
可以大大增加程式的可讀性。
#include <vector>
int main()
{
std::vector<int> vec;
typedef decltype(vec.begin()) vectype;
for (vectype i = vec.begin(); i < vec.begin(); ++i)
{
//
}
for (decltype(vec)::iterator i = vec.begin(); i != vec.end(); ++i)
{
//
}
}
int i = 0;
decltype(i) a; // a: int
// decltype((i)) b; // 錯誤:int&, 無法編譯成功。
C++ 的記憶體分為五區:
『棧(Stack)』:函數內的『局部變數』一般在這裡創建,由『編譯器』自動分配與釋放。
『堆(heap)』:由程式設計師使用 malloc/new
來申請或使用 free/delete
釋放記憶體空間,來防止系統資源耗盡導致程式潰壞。若忘記使用
free/delete
釋放,程式結束時會由系統回收這些記憶體。這個區域的記憶體空間需要手動管理,如果忘記釋放可能導致記憶體洩漏(memory
leak)。
對程式執行期間的影響:即使記憶體最終會被回收,但在程式執行期間,未釋放的記憶體依然會佔用系統資源。對於長時間運行或資源敏感的應用程式來說,這可能導致性能問題或系統資源不足。
空間大小理論上只要不超過實際擁有的物理記憶體空間即可。
分配速度相對較慢。
操作靈活。
new/delete
(用於 C++ 語言) 比
malloc/free
(用於 C 語言)多做了一些工作:
new
:除了分配記憶體空間,還多做了初始化工作(執行類別的構造函數)。
delete
:除了釋放記憶體空間,還多做了清理工作(執行類別的解構函數)。
new/delete
必須成對使用。
『自由儲存區(Free Store)』:與『堆』類似,但其管理機制是透過
malloc/free
來管理,在 C
程式中經常使用。在 C++ 中,『自由儲存區(Free Store)』
通常與『堆(heap)』 互相混用,它指的是由
new
和
delete
管理的動態記憶體區域。
『全局/靜態儲存區(Global/Static
Storage)』:存放著『全局變數』和『靜態(static
)變數』的記憶體空間(由編譯器負責)。全局變數在程式開始時創建,靜態變數在它們所在的範圍(函數或類別)首次被存取時創建,並在程式結束時釋放。
『常數儲存區(Constants
Area)』:用來存放『常數』,例如由雙引號(""
)定義的字串等。這些物件是在編譯時所分配,在程式執行期間無法修改。
Constants Area |
Global/ Static Storage Area |
Heap/Free Store (Dynamic Memory) |
Stack (Local Variables) |
靜態記憶體管理:
『棧』、『全局/靜態存储區』以及『常數储存區』中的分配是由『編譯器』来進行,並在程式執行之前已經被分配,故稱之為『靜態記憶體管理』。
由編譯器負責。
動態記憶體管理:
『堆(heap)』以及『自由儲存區』的記憶體分配,是在程式的執行期間來進行,故稱之為『動態記憶體管理』。
由程式開發者負責管理。
#include <iostream>
int a; // 全局變數,儲存在『全局/靜態儲存區』。
int main()
{
int b; // 局部變數,儲存在『棧』。
int *p = new int(); // 由運算子new分配,儲存在『堆』。
static int d; // 靜態變數,儲存在『全局/靜態儲存區』。
const int e = 0; // 常數,儲存在『常數儲存區』
delete p; // 釋放『堆』中的記憶體空間。
}
語法:型態標識符 *指標名 = new 型態標識符(初始值);
int *p = new int(3);
上述語句在『堆(heap)』上分配一塊整數型態的記憶體空間,並使指標變數
p
指向這塊記憶體空間。
括號內的 3
為這塊記憶體空間提供『初始值』。
new
回傳的是該物件的『指標值』。
int *ptr = new int; // ptr指向一個int物件。
std::string *mystr = new std::string(5, 'a'); // 產生5個a的字串。
std::vector<int> *pointv = new std::vector<int>{1, 2, 3, 4, 5};
int
物件並沒有名稱,但是
new
可以回傳該『無名物件』的指標值,其並用來初始化指標變數
ptr
。因此,我們可透過該指標變數來操作這個無名物件。在『堆(heap)』上分配記憶體空間時也可以不提供初始值:int *p = new int;
new
分配『類別型態』的記憶體:
new
運算子為類別型態分配記憶體時,會自動呼叫該類別的建構函數。new MyClass()
會分配
MyClass
的一個新實例並呼叫其預設建構函數。new
分配『內建資料型態』的記憶體:
int
、float
、char
等),使用 new
分配記憶體時,分配的記憶體內容初始是未定義的。int* p = new int;
會分配一個整數的記憶體,但 *p
的初始值是『未定義的』。int* p = new int(0);
,這樣
*p
就被初始化為
0
。也可以為『陣列』分配記憶體空間:
語法:型態標識符 *指標名 = new 型態標識符[長度];
int *pArr = new int[10]; // 定義一個長度為10的陣列。
與陣列的定義不同,使用 new 運算子為陣列分配記憶體空間時,其陣列長度可以是『變數』,而不必是常數。
因為動態記憶體分配在程式執行時才執行記憶體分配,故陣列長度可以是『變數』。
int n = 10;
int *ptr = new int[n]; // 可以使用變數n作為動態生成的陣列長度。
透過 new
分配的記憶體空間必須由程式設計師手動去釋放。
若記憶體空間沒有被釋放,則會一直存在到該程式結束為止。
new
分配的記憶體,必須透過 delete
顯性地釋放記憶體空間。若不,則會造成記憶體洩漏(memory leak)。
語法:
一般記憶體空間:delete 指標名;
陣列:delete []指標名;
[]
表示釋放的是一個陣列,不需要指定陣列長度。int *p = new int;
delete p; // 釋放變數的記憶體空間。
int *pArr = new int[10];
delete []pArr; // 釋放陣列的記憶體空間。
int i; // 局部變數。
int *pi = &i;
std::string str = "abcdef"; // 字串指標變數。
double *pd = new double(33); // 透過new分配記憶體空間。
// delete str; // 錯誤。
// delete pi; // 錯誤。
delete pd; // 正確。
delete str;
:這是一個錯誤。str
是一個 std::string
物件,它是在棧上而不是在堆上分配的。delete
是用於釋放在堆上分配的記憶體,所以嘗試使用
delete
來釋放
str
是不正確的。
delete pi;
:同樣是一個錯誤。pi
是一個指向 i
的指標,i
是在棧上分配的變數,不應該使用
delete
釋放它。在這種情況下,i
的記憶體空間將會在離開其作用域時自動釋放。
在C++中,編譯器可能允許某些不符合規範的程式碼進行編譯,但這並不意味著它的行為是正確的或者是安全的。在你提到的情況下,delete pi;
雖然可能編譯通過,但這實際上是一個未定義行為(undefined
behavior)的情況。
在C++中,只有那些在動態分配記憶體後使用
new
分配的記憶體,才能使用
delete
來釋放。對於在棧上分配的變數,它們的記憶體將在離開其作用域時自動釋放,不需要手動使用
delete
。
儘管 delete pi;
可能不會產生編譯錯誤,但這種行為可能導致程式執行時的問題,例如在某些情況下可能導致程式崩潰或者記憶體洩漏(memory
leak)。因此,最好遵守規範,僅對動態分配的記憶體使用
delete
。
delete pd;
:這是正確的用法。pd
是透過 new
在堆上分配的
double
型變數的指針,因此使用
delete
來釋放相應的記憶體是正確的做法,以防止記憶體泄漏。
現實狀況下,C/C++ 程式會在執行時突然退出,或是佔用的記憶體空間越來越多,最後不得不需要重啟程式。這些問題的原因可能是來於 C/C++ 堆(stack)記憶體管理:沒有正確處理記憶體的分配與釋放造成的。
懸空指標(wild pointer):某些記憶體位址已經被釋放,但之前指向它的指標仍被使用,則會導致無法預測的錯誤。
重複釋放:程式試圖去釋放已經被釋放過的記憶體,或是釋放已經被重新分配過的記憶體,則會造成重複釋放。
記憶體洩漏(memory leak):不再需要的記憶體空間若沒有被釋放則會造成記憶體洩漏。若程式不斷地重複進行這類的操作,則會導致記憶體佔用劇增,從而降低電腦的效能。
雖然顯性的記憶體管理在程式性能上具有一定的優勢,但卻可能容易出錯。
C++ 11 提供更好更安全的機制 – 『智慧指標(smart pointer)』,更加適用實際的應用,以擺脫記憶體管理的細節。
對於 C++ 程式新手,程式碼中常發生記憶體洩漏的狀況。且當程式長期執行時,記憶體洩漏的情況則會積少成多,從而導致記憶體資源不足最後使得程式崩潰。
new
出來的記憶體空間,必須在合適的時機進行
delete
,且很容易會出現錯誤,故不易管理。
當多個指標變數指向同一個動態分配的記憶體時,必須小心處理這些指標變數:因為釋放該記憶體可能會影響其他指標的有效性。
在這種情況下,只有當最後一個指向該記憶體的指標被銷毀時(或者被重新賦值為其他記憶體地址),才能釋放該記憶體位址,以避免其他指標變數仍然試圖存取已經被釋放的記憶體,從而引發未定義行為或記憶體錯誤。
裸指標(raw pointer):
是指直接使用原始指標(不帶任何智慧指標包裝或管理的指標),它們可以指向動態分配的記憶體或者其他物件。
使用裸指標時需要謹慎,因為它們需要手動管理記憶體的生存期。意指需要開發者負責適時地釋放該記憶體,否則可能導致『記憶體洩漏』或者存取無效記憶體的問題。
為了解決裸指標可能帶來的各種使用問題,C++ 引入『智慧指標』的概念:對『裸指標』進行包裝,使得智慧指標能夠『自動釋放所指向的物件』。
建議使用智慧指標取代裸指標:可使得開發出來的程式更穩健、更安全。
裸指標可以初始化
std::shared_ptr
,但不建議使用(智慧指標不要與裸指標穿插使用,一不小心會出問題,盡量使用
make_shared
)。
C++ 標準程式庫提供 4
種智慧指標:每一種智慧指標都有適用的情境,可幫助開發者管理動態分配之物件的生存期。即便忘記
delete
,系統也會幫助開發者進行 delete
。
std::auto_ptr
:C++ 98
就有,也是當時唯一的智慧指標。目前已經被 std::unique_ptr
所取代(C++ 11 已棄用 std::auto_ptr
)。
缺點:
無法在容器內儲存 std::auto_ptr
。
函數無法回傳 std::auto_ptr
。
std::unique_ptr
:同一個時間內只有一個指標能夠指向該物件。且該物件的所有權是可以移交出去。
std::shared_ptr
:共享指標,多個指標指向同一個物件,當最後一個指標被銷毀時,該物件就會被釋放。
std::weak_ptr
:用來輔助 std::shared_ptr
指標的指標。
為『類別模板』。
若程式中需要使用多個指向同一物件的指標,則選擇
std::shared_ptr
。
若程式中『不』需要使用多個指向同一物件的指標,則選擇
std::unique_ptr
。
優先考慮使用 std::unique_ptr
,若
std::unique_ptr
無法滿足需求,再考慮std::shared_ptr
。
std::weak_ptr
輔助 std::shared_ptr
(強共享指標)的操作。
指向一個由 std::shared_ptr
管理的物件,但並不控制所指向之物件的生存期。
std::weak_ptr
綁定到 std::shared_ptr
上並不會改變 std::shared_ptr
的『參考計數』。
只能監視 std::shared_ptr
的生存期,是對
std::shared_ptr
擴充,不是獨立的智慧指標。
auto pi = std::make_shared<int>(100);
std::weak_ptr<int>piw(pi); // piw若共享pi,pi的參考計數不改變,pi與piw兩者指向相同的記憶體位址。
std::weak_ptr<int> piw;
piw = pi; // pi為std::shared_ptr,並賦值給std::weak_ptr,現在pi是一個強參考,兩個弱參考。
std::unique_ptr
std::unique_ptr
是一種獨佔式智慧指標,同一個時刻,只能有一個
std::unique_ptr
指向一個物件。
無法複製
std::unique_ptr
(防止多個指標管理同一個物件),但可以轉移所有權(通過
std::move
)。
當這個 std::unique_ptr
被銷毀時,它所指向的物件也會被釋放。
內部只儲存一個指標,與裸指標的記憶體需求幾乎相同。
一般初始化(std::unique_ptr
與 new
配合):
std::unique_ptr<int> pi;
if (pi == nullptr) // 條件成立。
{
std::cout << "pi為空指標" << std::endl;
}
std::unique_ptr<int> pi2(new int(100));
#include <memory>
int main() {
std::unique_ptr<int> ptr1 = std::make_unique<int>(42);
// std::unique_ptr<int> ptr2 = ptr1; // 錯誤!不允許複製
return 0;
}
std::make_unique
函數
std::make_unique
:std::unique_ptr<int> p3(new int(100));
std::unique_ptr<int> p1 = std::make_unique<int>(100);
auto p2 = std::make_unique<int>(200);
#include <iostream>
#include <memory>
int main() {
std::unique_ptr<int> ptr1 = std::make_unique<int>(42);
// 將所有權轉移到 ptr2
std::unique_ptr<int> ptr2 = std::move(ptr1);
if (!ptr1) {
std::cout << "ptr1 is null" << std::endl;
}
std::cout << "Value: " << *ptr2 << std::endl; // 輸出 42
return 0;
}
常用成員函數
get()
:回傳所管理物件的裸指標(不釋放所有權)。``` cpp
std::unique_ptr<int> ptr = std::make_unique<int>(42);
int* raw_ptr = ptr.get();
std::cout << *raw_ptr; // 42
```
release()
:釋放管理的指標,回傳裸指標並將
unique_ptr
設為空(不會釋放記憶體,需手動管理)。
std::unique_ptr<int> ptr = std::make_unique<int>(42);
int* raw_ptr = ptr.release(); // 釋放管理
delete raw_ptr; // 手動釋放記憶體
reset():
釋放當前指標並管理新的物件(若有)。
std::unique_ptr<int> ptr = std::make_unique<int>(42);
ptr.reset(new int(99)); // 釋放舊物件並管理新物件
swap()
:
std::unique_ptr<int> ptr1 = std::make_unique<int>(42);
std::unique_ptr<int> ptr2 = std::make_unique<int>(99);
ptr1.swap(ptr2);
std::cout << *ptr1; // 99
std::cout << *ptr2; // 42
std::weak_ptr
有可能指向一個不存在的物件,故無法使用
std::weak_ptr
來直接存取物件,必需使用 lock
成員函數來檢查其所指向的物件是否存在。
若存在,lock
回傳一個指向共享物件的
std::shared_ptr
(參考計數會 +1)。
若不存在,則回傳一個空的 std::shared_ptr
。
auto pi2 = piw.lock(); // 強參考計數會加1,現在pi是兩個強參考,兩個弱參考。
if(pi2 != nullptr) // 寫成 if(pi2) 也可以。
{
std::cout << "所指物件存在" << std::endl;
}
auto pi = std::make_shared<int>(100);
std::weak_ptr<int>piw(pi); // piw若共享pi,pi強參考計數不改變,弱參考計數會從0變成1。
pi.reset(); // 因為pi是唯一指向該物件的指標,則釋放pi指向的物件,將pi設定為空。
auto pi2 = piw.lock(); // 因為所指向的物件被釋放,故piw弱參考屬於『過期』。
if(pi2 != nullptr)
{
std::cout << "所指物件存在" << std::endl;
}
use_count
成員函數
expired
成員函數
reset
成員函數
lock
成員函數