C++ 程式設計/程式碼/語句/函式
一個函式,也可以稱為子程式、過程、子程式,甚至方法,執行由稱為語句塊的一系列語句定義的任務,這些語句只需要編寫一次,並且可以根據需要被程式呼叫任意次來執行相同任務。
函式可能依賴於傳遞給它們的值,稱為實參,並且可以將任務的結果傳遞給函式的呼叫者,這稱為返回值。
需要注意的是,存在於全域性作用域中的函式也可以稱為全域性函式,在類內部定義的函式稱為成員函式。(術語方法通常在其他程式語言中用於指代類似於成員函式的內容,但這會導致在處理支援成員函式的虛擬和非虛擬分派的 C++ 時出現混淆。)
函式必須在使用之前宣告,宣告中包含一個名稱來標識它,函式返回的值的型別,以及傳遞給它的任何引數的型別。引數必須命名並宣告它接受的值的型別。如果引數沒有被修改,引數應該始終作為const傳遞。通常函式執行操作,所以名稱應該清楚地表明它做了什麼。透過在函式名稱中使用動詞並遵循其他命名約定,可以使程式更自然地閱讀。
在下面的示例中,我們定義了一個名為main的函式,它返回一個整數型別int的值,並且不接受任何引數。函式的內容稱為函式的主體。int這個詞是關鍵字。C++ 關鍵字是保留字,即不能用於任何與它們含義不同的目的。另一方面,main不是關鍵字,你可以在許多不能使用關鍵字的地方使用它(儘管不建議這樣做,因為會導致混淆)。
int main()
{
// code
return 0;
}
inline關鍵字宣告行內函數,該宣告是對編譯器的一個(非繫結)請求,要求對特定函式進行內聯擴充套件;也就是說,它建議編譯器在使用該函式的每個上下文中插入函式的完整主體,因此它用於避免在簡單實現子程式時進行 CPU 從程式碼中一個位置跳轉到另一個位置,然後再返回以執行子程式所帶來的開銷。
inline swap( int& a, int& b) { int const tmp(b); b=a; a=tmp; }
當函式定義包含在類/結構體定義中時,它將是一個隱式內聯,編譯器將嘗試自動內聯該函式。在這種情況下,不需要使用inline關鍵字;在該上下文中新增inline關鍵字是合法的,但冗餘,並且良好的風格是省略它。
示例
struct length
{
explicit length(int metres) : m_metres(metres) {}
operator int&() { return m_metres; }
private:
int m_metres;
};
內聯可以是最佳化,也可以是悲觀最佳化。它可以增加程式碼大小(透過在多個呼叫點重複函式的程式碼),也可以減少程式碼大小(如果函式的程式碼在最佳化後小於呼叫非行內函數所需的程式碼大小)。它可以提高速度(透過允許進行更多最佳化並避免跳轉),也可以降低速度(透過增加程式碼大小,從而導致快取未命中)。
內聯的一個重要副作用是,更多程式碼可以被最佳化器訪問。
將函式標記為內聯還會影響連結:允許多個行內函數定義(只要每個定義都在不同的翻譯單元中)只要它們是相同的。這允許行內函數定義出現在標頭檔案中;在標頭檔案中定義非行內函數幾乎總是錯誤(儘管函式模板也可以在標頭檔案中定義,並且通常是在標頭檔案中定義的)。
主流 C++ 編譯器,例如Microsoft Visual C++和GCC,支援一個選項,可以讓編譯器自動內聯任何合適的函式,即使是那些沒有被標記為行內函數的函式。編譯器通常比人類更能判斷特定函式是否應該內聯;特別是,編譯器可能不願意或不能內聯人類要求內聯的許多函式。
過度使用行內函數會大大增加耦合/依賴關係和編譯時間,並使標頭檔案作為介面文件的用途降低。
通常在呼叫函式時,程式會評估並存儲引數,然後呼叫(或跳轉到)函式的程式碼,然後函式稍後返回到呼叫者。雖然函式呼叫速度很快(在現代處理器上通常不到一微秒),但開銷有時可能很大,尤其是在函式很簡單並且被多次呼叫時。
在某些情況下,可以使用所謂的內聯函式作為效能最佳化。將函式標記為內聯是對編譯器的請求(有時稱為提示),建議編譯器考慮用該函式的程式碼副本替換對該函式的呼叫。
結果在某些方面類似於使用#define 宏,但是如之前提到,宏會導致問題,因為它們不是由預處理器評估的。內聯函式不會遇到相同的問題。
如果行內函數很大,這種替換過程(由於明顯的原因被稱為“內聯”)會導致“程式碼膨脹”,導致程式碼變大(因此通常變慢)。但是,對於小型函式,它甚至可以減少程式碼大小,特別是在編譯器的最佳化器執行後。
請注意,內聯過程需要編譯器能夠訪問函式的定義(包括程式碼)。特別是,從多個原始檔使用的內聯標頭檔案必須完全定義在標頭檔案中(而在普通函式中,這將是一個錯誤)。
指定函式為內聯的最常見方法是使用inline關鍵字。要記住,編譯器可以配置為忽略關鍵字並使用自己的最佳化。
在處理內聯成員函式時,會進行進一步的考慮,這將在面向物件程式設計章節中介紹。
引數和實參
[edit | edit source]函式宣告定義了它的引數。引數是一個變數,它在函式呼叫中接受傳遞給它的對應實參的含義。
實參代表你在呼叫函式時提供給函式引數的值。呼叫程式碼在呼叫函式時提供實參。
函式宣告中宣告預期引數的部分稱為引數列表,函式呼叫中指定實參的部分稱為實參列表。
//Global functions declaration
int subtraction_function( int parameter1, int parameter2 ) { return ( parameter1 - parameter2 ); }
//Call to the above function using 2 extra variables so the relation becomes more evident
int argument1 = 4;
int argument2 = 3;
int result = subtraction_function( argument1, argument2 );
// will have the same result as
int result = subtraction_function( 4, 3 );
許多程式設計師根據上下文使用引數和實參,以區分它們的含義。在實踐中,區分這兩個術語通常對於正確使用它們或將它們的使用傳達給其他程式設計師來說是不必要的。或者,可以使用等效術語形式引數和實際引數代替引數和實參。
引數
[edit | edit source]你可以定義一個沒有引數、一個引數或多個引數的函式,但要使用帶有實參的函式呼叫,你必須考慮定義的內容。
空引數列表
[edit | edit source]//Global functions with no parameters
void function() { /*...*/ }
//empty parameter declaration equivalent the use of void
void function( void ) { /*...*/ }
多個引數
[edit | edit source]宣告和呼叫具有多個引數的函式的語法可能是錯誤的來源。當你編寫函式定義時,你必須宣告每個引數的型別。
// Example - function using two int parameters by value
void printTime (int hour, int minute) {
std::cout << hour;
std::cout << ":";
std::cout << minute;
}
你可能很想寫(int hour, minute),但這種格式只適用於變數宣告,不適用於引數宣告。
但是,你不需要在呼叫函式時宣告實參的型別。(事實上,嘗試這樣做會出錯)。
示例
int main ( void ) {
int hour = 11;
int minute = 59;
printTime( int hour, int minute ); // WRONG!
printTime( hour, minute ); // Right!
}
在這種情況下,編譯器可以透過檢視它們的宣告來判斷hour和minute的型別。在將它們作為實參傳遞時,包含型別是多餘的,也是非法的。
透過指標
[edit | edit source]當指向的物件可能不存在時,函式可以使用傳值指標,也就是說,當你給出一個真實物件的地址或NULL時。傳遞指標與傳遞任何其他內容沒有區別。它與其他任何引數一樣是一個引數。指標型別的特性使其值得區分。
將指標傳遞給函式非常類似於將其作為引用傳遞。它用於避免複製開銷,以及在按值傳遞基類物件時可能發生的切片問題(因為子類比父類具有更大的記憶體佔用空間)。這也是 C 中的首選方法(出於歷史原因),其中傳值指標表示想要修改原始變數。在 C++ 中,首選使用指向指標的引用,並確保函式在對其解引用之前驗證指標的有效性。
#include <iostream>
void MyFunc( int *x )
{
std::cout << *x << std::endl; // See next section for explanation
}
int main()
{
int i;
MyFunc( &i );
return 0;
}
由於引用只是一個別名,它與它所引用的內容具有完全相同的地址,如下面的示例所示
#include <iostream>
void ComparePointers (int * a, int * b)
{
if (a == b)
std::cout<<"Pointers are the same!"<<std::endl;
else
std::cout<<"Pointers are different!"<<std::endl;
}
int main()
{
int i, j;
int& r = i;
ComparePointers(&i, &i);
ComparePointers(&i, &j);
ComparePointers(&i, &r);
ComparePointers(&j, &r);
return 0;
}
該程式會告訴你指標是相同的,然後是不同的,然後是相同的,然後是不同的。
- 陣列類似於指標,還記得嗎?
現在可能是重新閱讀關於陣列部分的好時機。但是,如果你不想翻回那麼遠,這裡有一個簡要回顧:陣列是記憶體空間的塊。
int my_array[5];
在上面的語句中,my_array是記憶體中足夠大以容納五個int的區域。要使用陣列的元素,它必須被解引用。陣列中的第三個元素(記住它們是零索引的)是my_array[2]。當你寫my_array[2]時,你實際上是在說“給我陣列中的第三個整數my_array”。因此,my_array是一個數組,但是my_array[2]是一個int。
- 傳遞單個數組元素
所以假設你想將陣列中的一個整數傳遞給一個函式。你該怎麼做?只需傳遞解引用的元素,你就可以了。
示例
#include <iostream>
void printInt(int printable){
std::cout << "The int you passed in has value " << printable << std::endl;
}
int main(){
int my_array[5];
// Reminder: always initialize your array values!
for(int i = 0; i < 5; i++)
my_array[i] = i * 2;
for(int i = 0; i < 5; i++)
printInt(my_array[i]); // <-- We pass in a dereferenced array element
}
此程式輸出以下內容
The int you passed in has value 0 The int you passed in has value 2 The int you passed in has value 4 The int you passed in has value 6 The int you passed in has value 8
這就像普通的整數一樣傳遞陣列元素,因為像my_array[2]這樣的陣列元素是整數。
- 傳遞整個陣列
好吧,我們可以將單個數組元素傳遞給函式。但如果我們想傳遞整個陣列呢?我們不能直接這樣做,但你可以將陣列視為一個指標。
示例
#include <iostream>
void printIntArr(int *array_arg, int array_len){
std::cout << "The length of the array is " << array_len << std::endl;
for(int i = 0; i < array_len; i++)
std::cout << "Array[" << i << "] = " << array_arg[i] << std::endl;
}
int main(){
int my_array[5];
// Reminder: always initialize your array values!
for(int i = 0; i < 5; i++)
my_array[i] = i * 2;
printIntArr(my_array, 5);
}
這將輸出以下內容
The length of the array is 5 Array[0] = 0 Array[1] = 2 Array[2] = 4 Array[3] = 6 Array[4] = 8
如你所見,main 中的陣列由一個指標訪問。現在這裡有一些重要的要點需要注意
- 一旦將陣列傳遞給函式,它就會被轉換為指標,因此函式無法知道如何猜測陣列的長度。除非你始終使用大小相同的陣列,否則你應該始終將陣列長度與陣列一起傳遞。
- 你傳遞了一個指標。my_array是一個數組,而不是指標。如果你在函式內部更改array_arg,my_array不會改變(即,如果你設定array_arg指向一個新陣列)。但是,如果你更改任何元素array_arg,你正在更改array_arg指向的記憶體空間,即陣列my_array.
按引用傳遞
[edit | edit source]傳遞變數時,使用了相同的引用概念。
示例
void foo( int &i )
{
++i;
}
int main()
{
int bar = 5; // bar == 5
foo( bar ); // increments bar
std::cout << bar << std::endl // 6
return 0;
}
示例 2:要交換兩個整數的值,我們可以寫
void swap (int& x, int& y)
{
int temp = x;
x = y;
y = temp;
}
在這個函式的呼叫中,我們給出了兩個型別為int的變數
int i = 7;
int j = 9;
swap (i, j);
cout << i << ' ' << j << endl;
此輸出為“9 7”。為該程式繪製一個堆疊圖,以說服自己這是真的。如果引數 x 和 y 被宣告為普通引數(沒有“&”),swap 將不起作用。它只會修改函式swap中的 x 和 y,並且不會影響 i 和 j。
當人們開始以引用方式傳遞諸如整數之類的內容時,他們通常會嘗試使用表示式作為引用引數。例如
int i = 7;
int j = 9;
swap (i, j+1); // WRONG!!
這是不合法的,因為表示式j+1不是一個變數 - 它不佔用引用可以引用的位置。弄清楚哪些表示式可以以引用方式傳遞有點棘手。現在,一個好的經驗法則是引用引數必須是變數。
這裡展示了引用在函式引數中的兩種常見用法之一 - 它們允許我們使用按值傳遞引數的傳統語法,但可以在呼叫者中操作該值。
但是,引用在函式引數中有一個更常見的用法 - 它們也可以用來傳遞指向大型資料結構的控制代碼,而不會在過程中建立它的多個副本。考慮以下情況
void foo( const std::string & s ) // const reference, explained below
{
std::cout << s << std::endl;
}
void bar( std::string s )
{
std::cout << s << std::endl;
}
int main()
{
std::string const text = "This is a test.";
foo( text ); // doesn't make a copy of "text"
bar( text ); // makes a copy of "text"
return 0;
}
在這個簡單的例子中,我們能夠看到按值傳遞和按引用傳遞的區別。在這種情況下,按值傳遞只擴充套件了幾個額外的位元組,但想象一下,例如如果text包含了一整本書的文字。
我們使用常量引用而不是引用原因是,該函式的使用者可以確保傳遞的變數的值不會在函式內部發生變化。從技術上講,我們稱之為“常量引用”。
能夠以引用方式傳遞它可以避免我們建立字串的副本,並避免使用指標的醜陋方式。
- 使用引用傳遞固定長度的陣列
在某些情況下,函式需要特定長度的陣列才能工作
void func(int(¶meter)[4]);
與陣列轉換為指標不同,parameter 不是可以轉換為指標的普通陣列,而是對包含 4 個 int 的陣列的引用。因此,只能傳遞包含 4 個 int 的陣列,不能傳遞其他長度的陣列或指向 int 的指標。這有助於防止緩衝區溢位錯誤,因為陣列物件始終會被分配,除非你透過強制型別轉換繞過型別系統。
它可以用於傳遞陣列,而無需手動指定元素數量
template<int n>void func(int(¶)[n]);
編譯器在編譯時生成長度值,在函式內部,n 儲存元素數量。但是,使用模板會生成程式碼膨脹。
在 C++ 中,多維陣列不能轉換為多級指標,因此,以下程式碼無效
// WRONG
void foo(int**matrix,int n,int m);
int main(){
int data[10][5];
// do something on data
foo(data,10,5);
}
雖然 int[10][5] 可以轉換為 (*int)[5],但不能轉換為 int**。因此,你可能需要在函式宣告中硬編碼陣列邊界
// BAD
void foo(int(*matrix)[5],int n,int m);
int main(){
int data[10][5];
// do something on data
foo(data,10,5);
}
為了使函式更通用,應該使用模板和函式過載
// GOOD
template<int junk,int rubbish>void foo(int(&matrix)[junk][rubbish],int n,int m);
void foo(int**matrix,int n,int m);
int main(){
int data[10][5];
// do something on data
foo(data,10,5);
}
在第一個版本中使用 n 和 m 的原因主要是為了保持一致性,並處理未完全使用分配的陣列的情況。它還可以用於透過比較 n/m 與垃圾/垃圾來檢查緩衝區溢位。
按值傳遞
[edit | edit source]當我們要編寫一個函式,其中引數的值獨立於傳遞的變數時,我們使用按值傳遞方法。
int add(int num1, int num2)
{
num1 += num2; // change of value of "num1"
return num1;
}
int main()
{
int a = 10, b = 20, ans;
ans = add(a, b);
std::cout << a << " + " << b << " = " << ans << std::endl;
}
輸出
10 + 20 = 30
上面的例子展示了按值傳遞的一個特性,引數是傳遞變數的副本,並且只存在於對應函式的 作用域中。這意味著我們必須承擔複製的成本。然而,這種成本通常只針對更大更復雜的變數。
在這種情況下,"a" 和 "b" 的值被複制到 "num1" 和 "num2" 上的函式 "add()" 中。我們可以看到 "num1" 的值在第 3 行改變了。但是,我們也可以觀察到 "a" 的值在傳遞到這個函式後保持不變。
常量引數
[edit | edit source]關鍵字const也可以用作保證函式不會修改傳入值的保證。這實際上只對引用和指標有用(而不是按值傳遞的東西),儘管在語法上沒有阻止使用const用於按值傳遞的引數。
例如以下函式
void foo(const std::string &s)
{
s.append("blah"); // ERROR -- we can't modify the string
std::cout << s.length() << std::endl; // fine
}
void bar(const Widget *w)
{
w->rotate(); // ERROR - rotate wouldn't be const
std::cout << w->name() << std::endl; // fine
}
在第一個例子中,我們試圖呼叫一個非 const 方法 --append()-- 在作為const引用傳遞的引數上,從而違反了我們與呼叫者達成的協議,即不修改它,編譯器會給我們一個錯誤。
對於rotate()也是一樣,但是使用const指標在第二個例子中。
預設值
[edit | edit source]C++ 函式中的引數(包括成員函式和建構函式)可以宣告為具有預設值,例如
int foo (int a, int b = 5, int c = 3);
然後,如果函式被呼叫時引數較少(但足以指定沒有預設值的引數),編譯器將為末尾缺少的引數假設預設值。例如,如果我呼叫
foo(6, 1)
這將等效於呼叫
foo(6, 1, 3)
在許多情況下,這可以讓你不必定義兩個分別接受不同數量引數的函式,這兩個函式幾乎完全相同,除了一個預設值。
"值" 作為預設值通常是一個常量,但可以是任何有效的表示式,包括執行任意計算的函式呼叫。
預設值只能用於最後一個引數;也就是說,你不能為一個引數指定預設值,而該引數後面跟著一個沒有預設值的引數,因為它永遠不會被使用。
一旦你在函式宣告中定義了引數的預設值,你就不能在後面的宣告中重新定義同一個引數的預設值,即使是相同的值。
省略號 (...) 作為引數
[edit | edit source]如果引數列表以省略號結尾,則意味著引數數量必須等於或大於指定引數的數量。它實際上會建立一個變參函式,即一個可變元數的函式;也就是說,一個可以接受不同數量引數的函式。
返回值
[edit | edit source]在宣告函式時,必須根據函式將返回的型別宣告它,這分三步完成,在函式宣告中,函式實現(如果不同)以及在同一個函式的主體中使用 return 關鍵字。
- 有結果的函式
你可能已經注意到,有些函式會產生結果。其他函式執行操作,但不會 return 值。
從函式獲取值的另一種方法是使用指標或引用作為引數,或使用全域性變數
- 從函式獲取多個值
返回值型別決定了容量,任何型別都可以使用,從陣列或 std::vector、結構體或類,它只受你選擇的返回值型別的限制。
- 這就引出了一些問題
- 如果你呼叫一個函式,但對結果不進行任何操作(即你不將其賦值給變數或不將其用作更大表達式的部分),會發生什麼?
- 如果你使用沒有結果的函式作為表示式的部分,比如 newLine() + 7,會發生什麼?
- 我們可以編寫產生結果的函式嗎?或者我們只能使用 newLine 和 printTwice 之類的函式?
第三個問題的答案是 "是的,你可以編寫返回值的函式"。現在,我會讓你自己嘗試回答其他兩個問題。當你對 C++ 中合法或非法操作有任何疑問時,第一步是詢問編譯器。但是,你應該注意兩個問題,我們在介紹編譯器時已經提到過:首先,編譯器可能會像任何其他軟體一樣存在錯誤,因此,並非所有在 C++ 中被禁止的原始碼都被編譯器正確拒絕,反之亦然。另一個問題更為危險:你可以在 C++ 中編寫程式,C++ 實現不需要拒絕,但其行為不受語言定義。不用說,執行這樣的程式可能會(而且偶爾會)對執行它的系統造成有害影響或產生錯誤的輸出!
例如
int MyFunc(); // returns an int
SOMETYPE MyFunc(); // returns a SOMETYPE
int* MyFunc(); // returns a pointer to an int
SOMETYPE *MyFunc(); // returns a pointer to a SOMETYPE
SOMETYPE &MyFunc(); // returns a reference to a SOMETYPE
如果你理解了指標宣告的語法,那麼返回指標或引用的函式的宣告應該是合乎邏輯的。上面的程式碼片段展示瞭如何宣告一個返回引用或指標的函式;下面概述了這類函式的定義(實現)的樣子
SOMETYPE *MyFunc(int *p)
{
//...
return p;
}
SOMETYPE &MyFunc(int &r)
{
//...
return r;
}
return 語句會導致執行從當前函式跳到呼叫當前函式的函式。可以返回可選的結果(返回值)。函式可能有多個 return 語句(但返回相同的型別)。
- 語法
return;
return value;
在函式體內,return 語句不應 return 指標或引用,該指標或引用在記憶體中具有在函式內宣告的區域性變數的地址,因為函式退出後,所有區域性變數都會被銷燬,你的指標或引用將指向你不再擁有的記憶體中的某個位置,因此你無法保證其內容。如果指標所指向的物件被銷燬,則該指標被稱為懸空指標,直到它被賦予一個新的值;對這種指標的值的任何使用都是無效的。擁有這樣的懸空指標是危險的;指向區域性變數的指標或引用必須不被允許從宣告這些區域性(也稱為自動)變數的函式中逃逸。
但是,在您的函式主體中,如果您的指標或引用具有動態分配記憶體的結構體或類的記憶體地址,則使用 new 運算子,那麼返回該指標或引用是合理的。
SOMETYPE *MyFunc() //returning a pointer that has a dynamically
{ //allocated memory address is valid code
int *p = new int[5];
//...
return p;
}
在大多數情況下,更好的方法是返回一個物件,例如智慧指標,它可以管理記憶體;使用廣泛分佈的 new 和 delete(或 malloc 和 free)進行顯式記憶體管理既繁瑣又容易出錯。至少,返回動態分配資源的函式應該仔細記錄。有關更多詳細資訊,請參見本書關於記憶體管理的部分。
const SOMETYPE *MyFunc(int *p)
{
//...
return p;
}
在這種情況下,返回的指標指向的 SOMETYPE 物件可能不會被修改,如果 SOMETYPE 是一個類,那麼只能對 SOMETYPE 物件呼叫 const 成員函式。
如果這樣的 const 返回值是指向類的指標或引用,那麼我們不能對該指標或引用呼叫非 const 方法,因為這會破壞我們不更改它的約定。
靜態返回
[edit | edit source]當函式返回一個靜態分配的變數(或指向它的指標)時,必須牢記每次呼叫使用它的函式時都可以覆蓋它的內容。如果您想儲存此函式的返回值,您應該手動將其儲存到其他地方。大多數這樣的靜態返回使用全域性變數。
當然,當您將其儲存到其他地方時,您應該確保實際將該變數的值複製到另一個位置。如果返回值是結構體,您應該建立一個新的結構體,然後將結構體的成員複製過來。
此類函式的一個例子是標準 C 庫函式 localtime。
返回“程式碼”(最佳實踐)
[edit | edit source]有兩種行為
正數表示成功
[edit | edit source]這是“邏輯”的思考方式,因此幾乎所有初學者都使用它。在 C++ 中,這採取布林真/假測試的形式,其中“真”(也為 1 或任何非零數)表示成功,“假”(也為 0)表示失敗。
這種構造的主要問題是所有錯誤都返回相同的值(false),因此您必須有一些外部可見的錯誤程式碼才能確定錯誤發生的位置。例如
bool bOK;
if (my_function1())
{
// block of instruction 1
if (my_function2())
{
// block of instruction 2
if (my_function3())
{
// block of instruction 3
// Everything worked
error_code = NO_ERROR;
bOK = true;
}
else
{
//error handler for function 3 errors
error_code = FUNCTION_3_FAILED;
bOK = false;
}
}
else
{
//error handler for function 2 errors
error_code = FUNCTION_2_FAILED;
bOK = false;
}
}
else
{
//error handler for function 1 errors
error_code = FUNCTION_1_FAILED;
bOK = false;
}
return bOK;
如您所見,my_function1 的 else 塊(通常是錯誤處理)可能與測試本身相距甚遠;這是第一個問題。當您的函式開始增長時,通常很難同時看到測試和錯誤處理。
這個問題可以透過原始碼編輯器的功能(例如摺疊)或測試函式返回“false”而不是 true 來彌補。
if (!my_function1()) // or if (my_function1() == false)
{
//error handler for function 1 errors
//...
這也可能使程式碼看起來更像“0 表示成功”的正規化,但可讀性略差。
這種構造的第二個問題是它往往會破壞邏輯測試(my_function2 縮進了一級,my_function3 縮排兩級),這會導致可讀性問題。
這裡的一個優點是您遵循結構化程式設計的原則,即函式具有單一入口和單一齣口。
Microsoft Foundation Class Library (MFC) 是使用此正規化的標準庫的一個例子。
0 表示成功
[edit | edit source]這意味著如果函式返回 0,則函式已成功完成。任何其他值都表示發生了錯誤,返回的值可能是對發生錯誤的指示。
這種正規化的優點是錯誤處理更接近測試本身。例如,前面的程式碼變為
if (0 != my_function1())
{
//error handler for function 1 errors
return FUNCTION_1_FAILED;
}
// block of instruction 1
if (0 != my_function2())
{
//error handler for function 2 errors
return FUNCTION_2_FAILED;
}
// block of instruction 2
if (0 != my_function3())
{
//error handler for function 3 errors
return FUNCTION_3_FAILED;
}
// block of instruction 3
// Everything worked
return 0; // NO_ERROR
在此示例中,此程式碼更具可讀性(這並不總是如此)。但是,此函式現在有多個出口點,違反了結構化程式設計的原則。
C 標準庫 (libc) 是使用此正規化的標準庫的一個例子。
組合
[edit | edit source]就像數學函式一樣,C++ 函式可以組合,這意味著您可以將一個表示式用作另一個表示式的一部分。例如,您可以將任何表示式用作函式的引數
double x = cos (angle + pi/2);
此語句將 pi 的值除以 2,並將結果加到角度的值。然後將總和作為引數傳遞給 cos 函式。
您還可以取一個函式的結果,並將其作為引數傳遞給另一個函式
double x = exp (log (10.0));
此語句求 e 的 10 的對數,然後將 e 提高到該冪。結果被分配給 x;我希望您知道它是多少。
遞迴
[edit | edit source]在程式語言中,遞迴最初是在 Lisp 中實現的,其基礎是早期的數學概念,它是一個概念,它允許我們將問題分解成一個或多個子問題,這些子問題在形式上類似於原始問題,在這種情況下,是讓函式在某些情況下呼叫自身。它通常與迭代器或迴圈區別開來。
遞迴函式的一個簡單示例是
void func(){
func();
}
需要注意的是,如上所示的非終止遞迴函式幾乎從未在程式中使用(實際上,遞迴的一些定義會排除這些非終止定義)。終止條件用於防止無限遞迴。
- 示例
double power(double x, int n)
{
if (n < 0)
{
std::cout << std::endl
<< "Negative index, program terminated.";
exit(1);
}
if (n)
return x * power(x, n-1);
else
return 1.0;
}
上述函式可以像這樣呼叫
x = power(x, static_cast<int>(power(2.0, 2)));
為什麼遞迴有用?雖然理論上,遞迴可以實現的任何事情都可以透過迭代實現(即 while),但有時使用遞迴更方便。遞迴程式碼碰巧更容易理解,如下面的示例所示。遞迴程式碼的問題是它佔用太多記憶體。由於函式被多次呼叫,而沒有刪除來自呼叫函式的資料,記憶體需求會顯著增加。但通常,遞迴程式碼的簡潔性和優雅性會超過記憶體需求。
遞迴的經典例子是階乘:,其中 按慣例。在遞迴中,此函式可以簡潔地定義為
unsigned factorial(unsigned n)
{
if (n != 0)
{
return n * factorial(n-1);
}
else
{
return 1;
}
}
使用迭代,邏輯更難理解
unsigned factorial2(unsigned n)
{
int a = 1;
while(n > 0)
{
a = a*n;
n = n-1;
}
return a;
}
雖然遞迴比迭代稍微慢一些,但在迭代會產生冗長且難以理解的程式碼的情況下,應該使用遞迴。此外,請記住遞迴函式在每個級別上都會佔用額外的記憶體(在堆疊上)。因此,在迭代方法可能只使用常量記憶體的情況下,它們可能會耗盡記憶體。
每個遞迴函式都需要有一個基本情況。基本情況是遞迴函式停止呼叫自身並返回一個值的地方。返回的值(希望)是期望的值。
對於前面的示例,
unsigned factorial(unsigned n)
{
if(n != 0)
{
return n * factorial(n-1);
}
else
{
return 1;
}
}
基本情況是在 時達到。在這個例子中,基本情況是 else 語句中包含的所有內容(它恰好返回數字 1)。返回的總值是從 到 的所有值相乘。所以,假設我們呼叫該函式並傳遞給它值 。然後函式進行計算 並返回 6 作為呼叫 factorial(3) 的結果。
另一個經典的遞迴示例是斐波那契數列
0 1 1 2 3 5 8 13 21 34 ...
該序列的第零個元素為 0。下一個元素為 1。該序列中的任何其他數字都是它前面兩個元素的總和。作為練習,編寫一個使用遞迴返回第n個斐波那契數的函式。
main
[edit | edit source]函式main也恰好是任何(符合標準的)C++ 程式的入口點,並且必須定義。編譯器安排在程式開始執行時呼叫該main函式。main可以呼叫其他函式,這些函式可以再呼叫其他函式。
該main函式返回一個整數值。在某些系統中,此值被解釋為成功/失敗程式碼。返回值為零表示程式成功完成。任何非零值都被認為是失敗。與其他函式不同,如果控制到達main()的末尾,則會自動新增用於成功的隱式 return 0;。為了使來自main的返回值更易讀,標頭檔案cstdlib定義了常量EXIT_SUCCESS和EXIT_FAILURE(分別表示成功/不成功完成)。
main 函式也可以這樣宣告
int main(int argc, char **argv){
// code
}
它將該main函式定義為返回整數值 int 並接受兩個引數。該main函式的第一個引數 argc 是一個整數值 int,它指定傳遞給程式的引數數量,而第二個引數 argv 是一個包含實際引數的字串陣列。程式幾乎總是至少傳遞一個引數;程式本身的名稱是第一個引數,argv[0]。其他引數可以從系統傳遞。
示例
#include <iostream>
int main(int argc, char **argv){
std::cout << "Number of arguments: " << argc << std::endl;
for(size_t i = 0; i < argc; i++)
std::cout << " Argument " << i << " = '" << argv[i] << "'" << std::endl;
}
如果上面的程式編譯成可執行檔案arguments並像這樣在 *nix 中從命令列執行
$ ./arguments I love chocolate cake
或在 Windows 或 MS-DOS 中的命令提示符下
C:\>arguments I love chocolate cake
它將輸出以下內容(但請注意,引數 0 可能與這不太一樣——它可能包含完整路徑,或者只包含程式名稱,或者包含相對路徑,或者甚至可能為空)
Number of arguments: 5 Argument 0 = './arguments' Argument 1 = 'I' Argument 2 = 'love' Argument 3 = 'chocolate' Argument 4 = 'cake'
您可以看到程式的命令列引數被儲存到argv陣列中,並且argc包含該陣列的長度。這使您可以根據傳遞給它的命令列引數更改程式的行為。
指向函式的指標
[edit | edit source]我們到目前為止檢視過的指標都是資料指標,指向函式的指標(更常稱為函式指標)非常相似,它們具有其他指標的相同特徵,但它們指向函式而不是指向變數。建立了一個額外的間接級別,作為在 C++ 中使用函數語言程式設計正規化的一種方式,因為它方便了呼叫從同一程式碼段在執行時確定的函式。它們允許將函式作為引數或返回值傳遞給另一個函式。
使用函式指標與任何其他函式呼叫的開銷完全相同,再加上額外的指標間接定址,並且由於要呼叫的函式僅在執行時確定,編譯器通常不會像在其他地方那樣行內函數呼叫。由於具有此特徵,使用函式指標可能比使用常規函式呼叫慢得多,並且應避免作為提高效能的一種方式。
要簡單地宣告指向函式的指標,必須將指標的名稱括起來,否則將宣告一個返回指標的函式。你還必須宣告函式的返回型別及其引數。這些必須完全相同!
考慮
int (*ptof)(int arg);
要引用的函式必須具有與指向函式的指標相同的返回型別和相同的引數型別。函式的地址可以透過使用其名稱來分配,也可以選擇性地加上地址運算子 &。呼叫函式可以使用 ptof(<value>) 或 (*ptof)(<value>) 來完成。
所以
int (*ptof)(int arg);
int func(int arg){
//function body
}
ptof = &func; // get a pointer to func
ptof = func; // same effect as ptof = &func
(*ptof)(5); // calls func
ptof(5); // same thing.
返回 a 的函式float不能被返回 a 的指標指向double。如果兩個名稱相同(例如int和signed,或 atypedef名稱),則允許轉換。否則,它們必須完全相同。你可以透過將*與變數名組合來定義指標,就像你處理任何其他指標一樣。問題是它可能被解釋為返回型別而不是指標。
通常使用 typedef 來定義函式指標型別更清晰;這也提供了一個地方來為函式指標的型別指定一個有意義的名稱
typedef int (*int_to_int_function)(int);
int_to_int_function ptof;
int *func (int); // WRONG: Declares a function taking an int returning pointer-to-int.
int (*func) (int); // RIGHT: Defines a pointer to a function taking an int returning int.
為了減少混淆,通常會typedef函式型別或指標型別
typedef int ifunc (int); // now "ifunc" means "function taking an int returning int"
typedef int (*pfunc) (int); // now "pfunc" means "pointer to function taking an int returning int"
如果你typedef函式型別,你可以宣告,但不能定義具有該型別的函式。如果你typdef指標型別,你不能宣告或定義具有該型別的函式。使用哪種方式是一個風格問題(儘管指標更流行)。
要將指標分配給函式,你只需將其分配給函式名稱。該&運算子是可選的(它並不模稜兩可)。如果存在,編譯器會自動選擇適合指標的過載函式版本。
int f (int, int);
int f (int, double);
int g (int, int = 4);
double h (int);
int i (int);
int (*p) (int) = &g; // ERROR: The default parameter needs to be included in the pointer type.
p = &h; // ERROR: The return type needs to match exactly.
p = &i; // Correct.
p = i; // Also correct.
int (*p2) (int, double);
p2 = f; // Correct: The compiler automatically picks "int f (int, double)".
使用指向函式的指標更簡單 - 你只需像呼叫函式一樣呼叫它。你被允許使用*運算子來解除引用它,但你不必
#include <iostream>
int f (int i) { return 2 * i; }
int main ()
{
int (*g) (int) = f;
std::cout<<"g(4) is "<<g(4)<<std::endl; // Will output "g(4) is 8"
std::cout<<"(*g)(5) is "<<g(5)<<std::endl; // Will output "g(5) is 10"
return 0;
}
回撥
[edit | edit source]在計算機程式設計中,回撥是作為引數傳遞給其他程式碼的可執行程式碼。它允許較低階的抽象層呼叫在較高層定義的函式。回撥通常返回到原始呼叫者的級別。
通常,較高層程式碼首先呼叫較低層程式碼中的函式,並將指向另一個函式的指標或控制代碼傳遞給它。在較低層函式執行期間,它可能會多次呼叫傳遞的函式來執行某些子任務。在另一種情況下,較低層函式將傳遞的函式註冊為一個處理程式,該處理程式將在稍後由較低層非同步呼叫,以響應某些事件。
回撥可以用作多型和泛型程式設計的更簡單替代方案,因為函式的確切行為可以透過將不同的(但相容的)函式指標或控制代碼傳遞給較低層函式來動態確定。這對於程式碼重用來說可能是一種非常強大的技術。在另一種常見情況下,回撥首先註冊,然後非同步呼叫。
過載
[edit | edit source]函式過載是在相同作用域內使用單個名稱來表示多個不同函式。共享相同名稱的多個函式必須使用不同的引數集來區分。這些函式可以在它們期望的引數數量上有所不同,或者它們的型別可以有所不同。透過這種方式,編譯器可以透過檢視呼叫者提供的引數來確定要呼叫的確切函式。這被稱為過載解析,非常複雜。
// Overloading Example
// (1)
double geometric_mean( int, int );
// (2)
double geometric_mean( double, double );
// (3)
double geometric_mean( double, double, double );
// ...
// Will call (1):
geometric_mean( 10, 25 );
// Will call (2):
geometric_mean( 22.1, 421.77 );
// Will call (3):
geometric_mean( 11.1, 0.4, 2.224 );
在某些情況下,呼叫可能是模稜兩可的,因為兩個或多個函式與提供的引數同樣匹配。
例如,假設上面定義了 geometric_mean
// This is an error, because (1) could be called and the second // argument casted to an int, and (2) could be called with the first // argument casted to a double. None of the two functions is // unambiguously a better match. geometric_mean(7, 13.21); // This will call (3) too, despite its last argument being an int, // Because (3) is the only function which can be called with 3 // arguments geometric_mean(1.1, 2.2, 3);
模板和非模板可以過載。如果兩種形式的函式與提供的引數同樣匹配,則非模板函式優先於模板函式。
注意,你也可以在 C++ 中過載許多運算子。
過載解析
[edit | edit source]請注意,C++ 中的過載解析是該語言中最複雜的部分之一。這在任何情況下可能都是不可避免的,因為存在自動模板例項化、使用者定義的隱式轉換、內建的隱式轉換以及更多語言特性。所以,如果你一開始不理解這一點,不要絕望。一旦你掌握了這些概念,它就變得非常自然,但是寫下來看起來非常複雜。
理解過載的最簡單方法是想象編譯器首先找到所有可能被呼叫的函式,使用所有合法的轉換和模板例項化。然後,編譯器從這個集合中選擇最佳匹配(如果有的話)。具體來說,這個集合的構造方式如下
- 所有具有匹配名稱的函式(包括函式模板)都放入集合中。不考慮返回型別和可見性。模板以儘可能匹配的引數新增。成員函式被視為第一個引數是指向類型別的指標的函式。
- 轉換函式被新增為所謂的代理函式,具有兩個引數,第一個是類型別,第二個是返回型別。
- 所有不匹配引數數量的函式(即使在考慮預設引數和省略號之後),都從集合中刪除。
- 對於每個函式,將考慮每個引數,以檢視是否存在合法的轉換序列來將呼叫者的引數轉換為函式的引數。如果找不到這樣的轉換序列,則從集合中刪除該函式。
合法的轉換在下面詳細說明,但簡而言之,合法轉換是任何數量的內建(如 int 到 float)轉換,再加上最多一個使用者定義的轉換。最後一部分對於理解如果你正在編寫內建型別的替換(例如智慧指標)至關重要。使用者定義的轉換在上面描述過,但總結一下,就是
- 隱式轉換運算子,如operator short toShort();
- 單引數建構函式(如果建構函式的所有引數除了一個之外都已預設,則認為它是單引數的)
過載解析透過嘗試建立最佳匹配函式來工作。
- 優先考慮簡單的轉換
檢視一個引數,優先的轉換大致基於轉換的範圍。具體來說,轉換按以下順序優先考慮,其中最優先的最高
- 無轉換,新增一個或多個 const,新增引用,將陣列轉換為指向第一個成員的指標
- const 優先用於右值(大致為常量),而非 const 優先用於左值(大致為可賦值的)
- 從短整型(bool、char、short)轉換為 int,以及 float 到 double 的轉換。
- 內建轉換,例如在 int 和 double 之間的轉換以及指標型別轉換。指標轉換的排名為
- 基類到派生類(指標)或派生類到基類(對於指向成員的指標),其中最派生的優先
- 轉換為void*
- 轉換為 bool
- 使用者定義的轉換,見上文。
- 與省略號匹配。(順便說一下,這是模板超程式設計相當有用的知識)
最佳匹配現在根據以下規則確定
- 一個函式只有在所有引數至少匹配得一樣好的情況下才是更好的匹配
簡而言之,該函式必須在各個方面都更好——如果一個引數匹配得更好,而另一個引數匹配得更差,則兩個函式都不被認為是更好的匹配。如果集合中沒有一個函式比這兩個函式都更好,則呼叫是模稜兩可的(即它失敗)示例
void foo(void*, bool);
void foo(int*, int);
int main() {
int a;
foo(&a, true); // ambiguous
}
- 非模板應該優先於模板
如果兩個函式在其他方面都相等,但一個是模板,另一個不是,則優先考慮非模板函式。這很少會造成意外。
- 最專業的模板優先
當兩個模板函式在其他方面都相等,但一個是比另一個更專業的,則優先考慮最專業的版本。示例
template<typename T> void foo(T); //1
template<typename T> void foo(T*); //2
int main() {
int a;
foo(&a); // Calls 2, since 2 is more specialized.
}
哪個模板更專業是一個完整的章節。
- 忽略返回型別
上面提到了這條規則,但它值得重複:返回型別從不是過載解析的一部分,即使所選函式的返回型別會導致編譯失敗。示例
void foo(int);
int foo(float);
int main() {
// This will fail since foo(int) is best match, and void cannot be converted to int.
return foo(5);
}
- 所選函式可能不可訪問
如果所選的最佳函式不可訪問(例如,它是一個私有函式,並且呼叫不是來自它的類的成員或友元),則呼叫失敗。
