关于函数形参的一些讨论
形參的初始化與變量的初始化一樣:如果形參具有非引用類型,則復制實參的值,如果形參為引用類型,則它只是實參的別名。
- 非引用形參:
普通的非引用類型的參數通過復制對應的實參實現初始化。當用實參副本初始化形參時,函數并沒有訪問調用所傳遞的實參本身,因此不會修改實參的值。
while 循環體雖然修改了 v1 與 v2 的值,但這些變化僅限于局部參數,而對調用 gcd 函數使用的實參沒有任何影響。于是,如果有函數調用gcd(i, j),i 與 j 的值不受 gcd 內執行的賦值操作的影響。
#include <iostream> #include <string>using namespace std;int gcd(int v1, int v2) {while (v2) {int temp = v2;v2 = v1 % v2;v1 = temp;}cout << "v1:" << v1 << endl;cout << "v2:" << v2 << endl;return v1; }int main() {int i = 100;int j = 3;gcd(i, j);cout << "i:" << i << endl;cout << "j:" << j << endl;system("pause");return 0; }輸出結果:
所以,非引用形參表示對應實參的局部副本。對這類形參的修改僅僅改變了局部副本的值。一旦函數執行結束,這些局部變量的值也就沒有了。
- 指針形參:
函數的形參可以是指針,此時將復制實參指針。與其他非引用類型的形參一樣,該類形參的任何改變也僅作用于局部副本。如果函數將新指針賦給形參,主調函數使用的實參指針的值沒有改變。但如果對局部副本的指針所指向的對象的值進行修改,則實參指針所指對象的值也會相應的被修改。
如果函數形參是非 const 類型的指針,則函數可通過指針實現賦值,修改指針所指向對象的值:
void reset(int *ip) {*ip = 0; // changes the value of the object to which ip pointsip = 0; // changes only the local value of ip; the argument is unchangedcout << "local ip: " << ip << endl; }int main() {int i = 42;int *p = &i;cout << "i: " << *p << '\n'; // prints i: 42cout << "p: " << p << endl;reset(p); // changes *p but not pcout << "i: " << *p << endl; // ok: prints i: 0cout << "p: " << p << endl;//并沒有改動 system("pause");return 0; }如果保護指針指向的值,則形參需定義為指向 const 對象的指針:
?
指針形參是指向 const 類型還是非 const 類型,將影響函數調用所使用的實參。我們既可以用 int* 也可以用 const int* 類型的實參調用 use_ptr 函數;但僅能將 int* 類型的實參傳遞給 reset 函數。這個差別來源于指針的初始化規則——可以將指向 const 對象的指針初始化為指向非 const 對象的指針,但不可以讓指向非 const 對象的指針初始化為指向 const 對象的指針。
?
- const形參:
在調用函數時,如果該函數使用非引用的非 const 形參,則既可給該函數傳遞 const 實參也可傳遞非 const 的實參。例如,可以傳遞兩個 int 型 const 對象調用 gcd:
int gcd(int v1, int v2) {while (v2) {int temp = v2;v2 = v1 % v2;v1 = temp;}return v1; }int main() {const int i = 3, j = 6;int k = gcd(3, 6); system("pause");return 0; }這種行為源于 const 對象的標準初始化規則。因為初始化復制了初始化時的對象,僅僅是要用那個值,即便修改也是對拷貝后的對象進行修改,而不對原對象進行修改,所以可用 const 對象初始化非 const 對象,反之亦然。
如果將形參定義為非引用的 const 類型:
void fcn(const int i) {}則在函數中,不可以改變實參的局部副本。由于實參仍然是以副本的形式傳遞,因此傳遞給 fcn 的既可以是 const 對象也可以是非 const 對象。
盡管函數的形參是 const,但是編譯器卻將 fcn 的定義視為其形碼被聲明為普通的 int 型:
void fcn(const int i) { } void fcn(int i) { }?
?
這種用法是為了支持對 C 語言的兼容,因為在 C 語言中,具有 const 形參或非 const 形參的函數并無區別。
- 復制實參的局限性:
當需要在函數中修改實參的值時。
當需要以大型對象作為實參傳遞時。對實際的應用而言,復制對象所付出的時間和存儲空間代價往往過大。
當沒有辦法實現對象的復制時。
- 引用形參
考慮下面不適宜復制實參的例子,該函數希望交換兩個實參的值:
void swap(int v1, int v2){int tmp = v2;v2 = v1; v1 = tmp;}這個例子期望改變實參本身的值。但對于上述的函數定義,swap 無法影響實參本身。執行 swap 時,只交換了其實參的局部副本,而傳遞 swap 的實參并沒有修改:
int main() {int i = 10;int j = 20;cout << "Before swap():\ti: "<< i << "\tj: " << j << endl;swap(i, j);cout << "After swap():\ti: "<< i << "\tj: " << j << endl;system("pause");return 0; }輸出結果:
為了使 swap 函數以期望的方式工作,交換實參的值,需要將形參定義為引用類型:
void swap(int &v1, int &v2) {int tmp = v2;v2 = v1;v1 = tmp; }int main() {int i = 10;int j = 20;cout << "Before swap():\ti: "<< i << "\tj: " << j << endl;swap(i, j);cout << "After swap():\ti: "<< i << "\tj: " << j << endl;system("pause");return 0; }輸出結果:
與所有引用一樣,引用形參直接關聯到其所綁定的實參,而并非這些對象的副本。定義引用時,必須用與該引用綁定的對象初始化該引用。引用形參完全以相同的方式工作。每次調用函數,引用形參被創建并與相應實參關聯。此時,當調用swap(i, j); 形參 v1 只是對象 i 的另一個名字,而 v2 則是對象 j 的另一個名字。對 v1 的任何修改實際上也是對 i 的修改。同樣地,v2 上的任何修改實際上也是對 j 的修改。
從 C 語言背景轉到 C++ 的程序員習慣通過傳遞指針來實現對實參的訪問。在 C++ 中,使用引用形參則更安全和更自然。
- 使用引用形參返回額外的信息
引用形參的另一種用法是向主調函數返回額外的結果。函數只能返回單個值,但有些時候,函數有不止一個的內容需要返回。
如何定義既返回一個迭代器又返回出現次數的函數?我們可以定義一種包含一個迭代器和一個計數器的新類型。而更簡便的解決方案是給 find_val 傳遞一個額外的引用實參,用于返回出現次數的統計結果:
vector<int>::const_iterator find_val(vector<int>::const_iterator beg, vector<int>::const_iterator end, int value, vector<int>::size_type &occurs) {vector<int>::const_iterator res_iter = end;occurs = 0; for (; beg != end; ++beg)if (*beg == value) {if (res_iter == end)res_iter = beg;++occurs; }return res_iter; }int main() {vector<int> ivec = { 1, 42, 3, 42, 5, 6, 7, 8 };vector<int>::size_type ctr = 0;vector<int>::const_iterator it = find_val(ivec.begin(), ivec.end(), 42, ctr);cout << ctr << endl;system("pause");return 0; }輸出結果:
- 利用const引用避免復制
在向函數傳遞大型對象時,需要使用引用形參,這是引用形參適用的另一種情況。雖然復制實參對于內置數據類型的對象或者規模較小的類類型對象來說沒有什么問題,但是對于大部分的類類型或者大型數組,它的效率(通常)太低了;此外,某些類類型是無法復制的。使用引用形參,函數可以直接訪問實參對象,而無須復制它。編寫一個比較兩個 string 對象長度的函數作為例子。這個函數需要訪問每個 string 對象的 size,但不必修改這些對象。由于 string 對象可能相當長,所以我們希望避免復制操作。使用 const 引用就可避免復制:
bool isShorter(const string &s1, const string &s2){return s1.size() < s2.size();}其每一個形參都是 const string 類型的引用。因為形參是引用,所以不復制實參。又因為形參是 const 引用,所以 isShorter 函數不能使用該引用來修改實參。如果使用引用形參的唯一目的是避免復制實參,則應將形參定義為 const 引用。
- 更靈活的指向const的引用
如果函數具有普通的非 const 引用形參,則顯然不能通過 const 對象進行調用。畢竟,此時函數可以修改傳遞進來的對象,這樣就違背了實參的 const 特性。
int incr(int &val) {return ++val; }int main() {const int v2 = 42;int v3 = incr(v2); // error: v1 is not an int }會報錯:
但比較容易忽略的是,調用這樣的函數時,傳遞一個右值或具有需要轉換的類型的對象同樣是不允許的:
int main() {short v1 = 0;const int v2 = 42;int v3 = incr(v1); // error: v1 is not an intv3 = incr(0); // error: literals are not lvaluesv3 = incr(v1 + v2); // error: addition doesn't yield an lvalueint v4 = incr(v3); // ok: v3 is a non const object type intsystem("pause");return 0; }會提示錯誤:
問題的關鍵是非 const 引用形參只能與完全同類型的非 const 對象關聯。
應該將不修改相應實參的形參定義為 const 引用。如果將這樣的形參定義為非 const 引用,則毫無必要地限制了該函數的使用。例如,可編寫下面的程序在一個 string 對象中查找一個指定的字符:
string::size_type find_char(string &s, char c) {string::size_type i = 0;while (i != s.size() && s[i] != c)++i; // not found, look at next characterreturn i; }這個函數將其 string 類型的實參當作普通(非 const)的引用,盡管函數并沒有修改這個形參的值。這樣的定義帶來的問題是不能通過字符串字面值來調用這個函數:
if (find_char("Hello World", 'o'))雖然字符串字面值可以轉換為 string 對象,但上述調用仍然會導致編譯失敗:
繼續將這個問題延伸下去會發現,即使程序本身沒有 const 對象,而且只使用 string 對象(而并非字符串字面值或產生 string 對象的表達式)調用 find_char 函數,編譯階段的問題依然會出現。例如,可能有另一個函數 is_sentence 調用 find_char 來判斷一個 string 對象是否是句子:
bool is_sentence (const string &s){return (find_char(s, '.') == s.size() - 1);}如上代碼,函數 is_sentence 中 find_char 的調用是一個編譯錯誤。傳遞進 is_sentence 的形參是指向 const string 對象的引用,不能將這種類型的參數傳遞給 find_char,因為后者期待得到一個指向非 const string 對象的引用。
應該將不需要修改的引用形參定義為 const 引用。普通的非 const 引用形參在使用時不太靈活。這樣的形參既不能用 const 對象初始化,也不能用字面值或產生右值的表達式實參初始化。
- 傳遞指向指針的引用:
假設我們想編寫一個與前面交換兩個整數的 swap 類似的函數,實現兩個指針的交換。已知需用 * 定義指針,用 & 定義引用。現在,問題在于如何將這兩個操作符結合起來以獲得指向指針的引用。這里給出一個例子:
void ptrswap(int *&v1, int *&v2) {int *tmp = v2;v2 = v1;v1 = tmp; }形參int *&v1的定義應從右至左理解:v1 是一個引用,與指向 int 型對象的指針相關聯。也就是說,v1 只是傳遞進 ptrswap 函數的任意指針的別名。調用 ptrswap 交換分別指向值 10 和 20 的指針:
int main() {int i = 10;int j = 20;int *pi = &i; // pi points to iint *pj = &j; // pj points to jcout << "Before ptrswap():\t*pi: "<< *pi << "\t*pj: " << *pj << endl;ptrswap(pi, pj); // now pi points to j; pj points to icout << "After ptrswap():\t*pi: "<< *pi << "\t*pj: " << *pj << endl;system("pause");return 0; }輸出結果:
即指針的值被交換了。在調用 ptrswap 時,pi 指向 i,而 pj 則指向 j。在 ptrswap 函數中,指針被交換,使得調用 ptrswap 結束后,pi 指向了原來 pj 所指向的對象。換句話說,現在 pi 指向 j,而 pj 則指向了 i。
- vector和其它類型容器的形參
通常,函數不應該有 vector 或其他標準庫容器類型的形參。調用含有普通的非引用 vector 形參的函數將會復制 vector 的每一個元素。從避免復制 vector 的角度出發,應考慮將形參聲明為引用類型。然而,事實上,C++ 程序員傾向于通過傳遞指向容器中需要處理的元素的迭代器來傳遞容器:
void print(vector<int>::const_iterator beg,vector<int>::const_iterator end){while (beg != end) {cout << *beg++;if (beg != end) cout << " "; // no space after last element }cout << endl;}這個函數將輸出從 beg 指向的元素開始到 end 指向的元素(不含)為止的范圍內所有的元素。除了最后一個元素外,每個元素后面都輸出一個空格。
- 數組形參
數組有兩個特殊的性質,影響我們定義和使用作用在數組上的函數:一是不能復制數組;二是使用數組名字時,數組名會自動轉化為指向其第一個元素的指針。因為數組不能復制,所以無法編寫使用數組類型形參的函數。因為數組會被自動轉化為指針,所以處理數組的函數通常通過操縱指向數組指向數組中的元素的指針來處理數組。
如果要編寫一個函數,輸出 int 型數組的內容,可用下面三種方式指定數組形參:
void printValues(int*) { /* ... */ } void printValues(int[]) { /* ... */ } void printValues(int[10]) { /* ... */ }雖然不能直接傳遞數組,但是函數的形參可以寫成數組的形式。雖然形參表示方式不同,但可將使用數組語法定義的形參看作指向數組元素類型的指針。上面的三種定義是等價的,形參類型都是 int*。
形參的長度會引起誤會。編譯器忽略為任何數組形參指定的長度。根據數組長度(權且這樣說),可將函數 printValues 編寫為:
void printValues(const int ia[10]){for (size_t i = 0; i != 10; ++i){cout << ia[i] << endl;}}盡管上述代碼假定所傳遞的數組至少含有 10 個元素,但 C++ 語言沒有任何機制強制實現這個假設。下面的調用都是合法的:
int main(){int i = 0, j[2] = {0, 1};printValues(&i); // ok: &i is int*; probable run-time errorprintValues(j); // ok: j is converted to pointer to 0th// element; argument has type int*;// probable run-time errorreturn 0;}雖然編譯沒有問題,但是這兩個調用都是錯誤的,可能導致運行失敗。在這兩個調用中,由于函數 printValues 假設傳遞進來的數組至少含有 10 個元素,因此造成數組內在的越界訪問。程序的執行可能產生錯誤的輸出,也可能崩潰,這取決于越界訪問的內存中恰好存儲的數值是什么。
當編譯器檢查數組形參關聯的實參時,它只會檢查實參是不是指針、指針的類型和數組元素的類型時是否匹配,而不會檢查數組的長度。
和其他類型一樣,數組形參可定義為引用或非引用類型。大部分情況下,數組以普通的非引用類型傳遞,此時數組會悄悄地轉換為指針。一般來說,非引用類型的形參會初始化為其相應實參的副本。而在傳遞數組時,實參是指向數組第一個元素的指針,形參復制的是這個指針的值,而不是數組元素本身。函數操縱的是指針的副本,因此不會修改實參指針的值。然而,函數可通過該指針改變它所指向的數組元素的值。通過指針形參做的任何改變都在修改數組元素本身。不需要修改數組形參的元素時,函數應該將形參定義為指向 const 對象的指針:
void f(const int*) { /* ... */ }- 通過引用傳遞數組
和其他類型一樣,數組形參可聲明為數組的引用。如果形參是數組的引用,編譯器不會將數組實參轉化為指針,而是傳遞數組的引用本身。在這種情況下,數組大小成為形參和實參類型的一部分。編譯器檢查數組的實參的大小與形參的大小是否匹配:
void printValues(int(&arr)[10]) {} int main() {int i = 0, j[2] = { 0, 1 };int k[10] = { 0, 1, 2, 3, 4, 5, 6, 7, 8, 9 };printValues(&i); // error: argument is not an array of 10 intsprintValues(j); // error: argument is not an array of 10 intsprintValues(k); // ok: argument is an array of 10 intssystem("pause");return 0; }報錯:
這個版本的 printValues 函數只嚴格地接受含有 10 個 int 型數值的數組,這限制了哪些數組可以傳遞。
- 多維數組的傳遞
所謂多維數組實際是指數組的數組。和其他數組一樣,多維數組以指向 0 號元素的指針方式傳遞。多維數組的元素本身就是數組。除了第一維以外的所有維的長度都是元素類型的一部分,必須明確指定:
void printValues(int (matrix*)[10], int rowSize);上面的語句將 matrix 聲明為指向含有 10 個 int 型元素的數組的指針。//如果傳遞的數組時int a[10],那么形參應該是void printValues(int locala[10], int rowSize);,locala就是指向int類型的指針。傳遞數組,形參類型就是數組元素的指針類型。因為int a[10]是整型數組,所以形參是整形指針,因為這里要傳遞的是數組的數組,所以形參應該是數組的指針。
我們也可以用數組語法定義多維數組。與一維數組一樣,編譯器忽略第一維的長度,所以最好不要把它包括在形參表內:
void printValues(int matrix[][10], int rowSize);這條語句把 matrix 聲明為二維數組的形式。實際上,形參是一個指針,指向數組的數組中的元素。數組中的每個元素本身就是含有 10 個 int 型對象的數組。
舉例:
int matrix[3][10] = { 1, 2, 3, 4, 5, 6, 7, 8, 9, 0,11, 12, 13, 14, 15, 16, 17, 18, 18, 20,21, 22, 23, 24, 25, 26, 27, 28, 29, 30 };其實二維數組matrix中的數據是現行排放的,而matrix則是首元素的地址:
非引用數組形參的類型檢查只是確保實參是和數組元素具有同樣類型的指針,而不會檢查實參實際上是否指向指定大小的數組。任何處理數組的程序都要確保程序停留在數組的邊界內。
有三種常見的編程技巧確保函數的操作不超出數組實參的邊界。第一種方法是在數組本身放置一個標記來檢測數組的結束。C 風格字符串就是采用這種方法的一個例子,它是一種字符數組,并且以空字符 null 作為結束的標記。處理 C 風格字符串的程序就是使用這個標記停止數組元素的處理。
第二種方法是傳遞指向數組第一個和最后一個元素的下一個位置的指針。如:
void printValues(const int *beg, const int *end){while (beg != end) {cout << *beg++ << endl;}}int main(){int j[2] = {0, 1};// ok: j is converted to pointer to 0th element in j// j + 2 refers one past the end of jprintValues(j, j + 2);return 0;}第三種方法是將第二個形參定義為表示數組的大小,這種用法在 C 程序和標準化之前的 C++ 程序中十分普遍。
- main處理命令行選項
傳統上,主函數的實參是可選的,用來確定程序要執行的操作。比如,假設我們的主函數 main 位于名為 prog 的可執行文件中,可如下將實參選項傳遞給程序:
prog -d -o ofile data0這種用法的處理方法實際上是在主函數 main 中定義了兩個形參:
int main(int argc, char *argv[]) { ... }第二個形參 argv 是一個 C 風格字符串數組。第一個形參 argc 則用于傳遞該數組中字符串的個數。由于第二個參數是一個數組,主函數 main 也可以這樣定義:
int main(int argc, char **argv) { ... }表示 argv 是指向 char* 的指針。//形參是char*,其原型可以是char類型的數組,argv是char類型數組的指針,那么原型可以是char類型數組的數組。簡化思考,可以發現,參數中有幾個“*”,傳遞的就可以是幾維數組。
以前面的命令行為例,argc 應設為 5,argv 會保存下面幾個 C 風格字符串:
argv[0] = "prog";argv[1] = "-d";argv[2] = "-o";argv[3] = "ofile";argv[4] = "data0";- 含有可變形參的函數
在無法列舉出傳遞給函數的所有實參的類型和數目時,可以使用省略符形參。省略符暫停了類型檢查機制。它們的出現告知編譯器,當調用函數時,可以有 0 或多個實參,而實參的類型未知。省略符形參有下列兩種形式:
void foo(parm_list, ...);void foo(...);第一種形式為特定數目的形參提供了聲明。在這種情況下,當函數被調用時,對于與顯示聲明的形參相對應的實參進行類型檢查,而對于與省略符對應的實參則暫停類型檢查。在第一種形式中,形參聲明后面的逗號是可選的。大部分帶有省略符形參的函數都利用顯式聲明的參數中的一些信息,來獲取函數調用中提供的其他可選實參的類型和數目。因此帶有省略符的第一種形式的函數聲明是最常用的。
轉載于:https://www.cnblogs.com/predator-wang/p/5195830.html
總結
以上是生活随笔為你收集整理的关于函数形参的一些讨论的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: C# 地磅串口编程
- 下一篇: vivado编译出错 [Synth 8-