一、函數基礎
一個函數(function)定義包括:返回類型、函數名字、0或若干個形參組成的列表以及函數體。
實參是形參的初始值,並且實參的類型必須與對應的形參類型匹配,函數的形參列表可以爲空,但是不能省略,一般是書寫一個空的形參列表,也可以使用關鍵字void表示函數沒有形參
void f1(){/*...*/} //隱式地定義空形參列表
void f2(void){/*...*/} //顯示地定義空形參列表
大多數類型都能作爲函數的返回類型,void類型表示不返回任何值,函數的返回類型不能是數組類型或函數類型,但可以是指向數組或函數的指針。
1.2 局部對象
C++中,名字有作用域,即程序文本的一部分;對象有聲明週期,是程序執行過程中該對象存在的一段時間。
形參和函數體內部定義的變量統稱爲局部變量(local variable),它們僅在函數的作用域內可見,並隱藏在外層作用域中同名的其他所有生命之中。
自動對象(automatic object)指只存在於執行期間的對象,即當函數控制路徑經過變量定義語句時創建該對象,當達到定義所在塊末尾時銷燬它,這時自動對象的值就變成未定義的了,形參是一種自動對象。
有時候需要令局部變量的生命週期貫穿函數調用及之後的時間,這時可以將局部變量定義爲static類型,即局部靜態對象(local static object),其在程序執行路徑第一次經過對象定義語句時初始化,並直到程序終止才被銷燬。
1.2 函數聲明
函數的名字必須在使用之前聲明,與變量類似,函數只能定義一次,但可以聲明多次,函數的聲明與定義的唯一區別是函數聲明無須函數體,用一個分好替代即可,函數聲明也稱作函數原型(function prototype)。
一般地,與變量相似,函數在頭文件中聲明,在源文件中定義,定義函數的源文件應該含有函數聲明的頭文件包含進來,編譯器負責驗證函數的定義和聲明是否匹配。
1.3 分離式編譯
隨着程序越來越複雜,一般吧程序的各個部分分別存儲在不同的文件中,爲了允許編寫程序時按照邏輯關係將其劃分開來,C++語言支持分離式編譯(separate compilation)。
二、參數傳遞
形參的類型決定了形參與實參交互的方式,如果形參是引用類型,它將綁定到對應的實參上,否則將實參的值拷貝後賦給形參。
當形參是引用類型時,稱它對應的實參被引用傳遞,引用形參也是它綁定對象的別名,即形參是其對應實參的別名。當實參的值被拷貝給形參時,形參和實參是兩個獨立的對象,我們稱實參被值傳遞。
2.1 傳值參數
初始化一個非引用類型的變量時,初始值被拷貝給變量,此時對變量的改動不會影響初始值。
指針的行爲和其他非引用類型一樣,當執行指針拷貝操作時,拷貝的是指針的值,拷貝之後兩個指針是不同的指針。
int n = 0,i = 42;
int *p = &n,*q = &i; //p指向n,q指向i
*p = 42; //n的值改變,p不變
p=q; //p指向i,但i和n的值都不變
指針形參的行爲與上述相似:
void reset(int *ip)
{
*ip = 0; //改變指針ip所指對象的值
ip = 0; //只改變了ip的局部拷貝,實參未被改變
}
調用reset函數之後,實參所指的對象被置爲0,但是實參本身並沒有改變:
int i = 42;
reset(&i); //改變i的值而非i的地址
cout << "i = " << i << endl; //輸出i = 0
同時C++中建議使用引用類型的形參替代指針。
2.2 傳引用參數
引用形參的行爲與對引用的操作類似,通過使用引用形參,允許函數改變一個或多個實參的值。
//該函數接收一個int對象的引用,然後將對象的值置爲0
void reset(int &i) //i是傳給reset函數的對象的另外一個名字
{
i = 0; //改變了i所引對象的值
}
int j = 42;
reset(j); //j採用傳引用方式,它的值被改變
①使用引用避免拷貝
拷貝大的類類型對象或者對象比較抵消,甚至有的類類型根本就不支持拷貝操作,這時函數只能通過引用形參訪問該類型的對象,比如編寫一個函數比較兩個string對象的長度,爲了避免直接拷貝過長的string對象,可以使用引用形參,且因爲無需改變string內容,所以最好把形參定義成對常量的引用:
bool isShorter(const string &s1,const string &s2)
{
return s1.size() < s2.size();
}
②使用引用形參返回額外信息
一個函數只能返回一個值,但是引用形參可以爲我們一次返回多個結果。比如定義一個函數能夠返回string對象中某個指定字符第一次出現的位置,並且也返回該字符出現的總次數。一種方法是定義一個新的數據類型,讓它包含位置和數量兩個成員,而除此之外,我們還可以傳入一個額外的引用實參,令其保存字符出現的次數。
string::size_type find_char(const string &s,char c,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; //出現次數通過occurs隱式地返回
}
auto index = find_char(s,'o',ctr);
2.3 const形參和實參
當形參是const時,與之前討論的頂層const作用於對象本身是類似的,即當用實參初始化形參時會忽略頂層const,傳給它常量對象或者非常量對象都可以:
const int ci = 42; //不能改變ci,const是頂層的
int j = ci; //正確,拷貝ci時,忽略其頂層const
int * const p = &j; //const是頂層的,不能給p賦值
*p = 0; //正確,通過p改變對象的內容是允許的,i變成0
void fcn(const int i){/*fcn能夠讀取i,但是不能向i寫值*/}
void fcn(int i){/*...*/} //錯誤,重複定義了fcn(int)
C++中允許有相同名字函數,但是函數的形參列表應有明顯區別,上述函數因爲頂層const被忽略了,所以兩個函數的參數可以是一樣的,所以第二個函數時錯誤的。
①指針或引用形參與const
形參的初始化方式和變量的初始化方式是一樣,可以使用非常量初始化一個低層const對象,但是反過來不行,同時一個普通的引用必須用同類型的對象初始化。
int j = 42;
const int *cp = &j; //正確,但是cp不能改變j
const int &r = j; //正確,但是r不能改變j
const int &r2 = 42; //正確
int *p = cp; //錯誤,p的類型和cp的類型不匹配
int &r3 = r; //錯誤,r3的類型和r不匹配
int &r4 = 42; //錯誤,不能用字面值初始化一個非常量引用
const int cj = j;
string::size_type ctr = 0;
reset(&j); //調用形參類型是int*的reset函數
reset(&cj); //錯誤,不能用指向const int對象的指針初始化int*
reset(j); //調用形參類型是int&的reset函數
reset(cj); //錯誤,不能把普通引用綁定到const對象cj上
reset(42); //錯誤,不能把普通應用綁定到字面值上
reset(ctr); //錯誤,類型不匹配,ctr是無符號類型
//正確,find_char的第一個形參是對常量的引用
find_char("hello world!", 'o',ctr);
②儘量使用常量引用
把函數不會改變的形參定義成普通的引用會帶給函數的調用者一種誤導,即函數可以修改它的實參值,並且使用非常量引用也會極大限制函數所能接收的實參類型,如上面的不能把const對象、字面值或需要類型轉換的對象傳遞給普通的引用形參。
2.4 數組形參
數組有兩個非常重要的性質:①不允許拷貝數組,所以無法以值傳遞的方式使用數組參數;②使用數組時通常會將其轉換成指針,所以當我們爲函數傳遞一個數組時,實際傳遞的是指向數組首元素的指針。
雖然不能以值的方式傳遞數組,但是看可以把形參寫成類似數組的形式,當給傳給函數的是一個數組,實參會自動地轉換成指向數組首元素的指針,數組大小對函數的調用沒有影響:
//儘管形式不同,但這三個print函數時等價的,每個函數都有一個const int*類型的形參
void print(const int*);
void print(const int[]); //可以看出函數的意圖是作用域一個數組
void print(const int[10]); //這裏維度表示我們期望數組有多少個元素,實際不一定
①使用標記指定數組長度
管理數組實參的第一種方法要求數組本身包含一個借宿標記,這種方法典型示例是C風格字符串,其儲存在字符數組中,並且在最後一個字符後面跟着一個空字符,函數處理C風格字符串時遇到空字符停止:
void print(const char *cp)
{
if (cp) //若cp不是一個空指針
while (*cp) //只要指針所指的字符不是空字符
cout << *cp++; //輸出當前字符並將指針向前移動;一個位置
}
②使用標準庫規範
管理數組實參的第二種技術是傳遞指向數組首元素和尾元素的指針,使用該方法可以按照如下形式輸出元素內容:
void print(const int *beg,const int *end)
{
//輸出beg到end之間(不含end)的所有元素
while (beg !=end)
cout << *beg++ << endl; //輸出當前元素並將指針向前移動一個位置
③顯示傳遞一個表示數組大小的形參
第三種管理數組實參的方法是專門定義一個表述數組大小的形參:
//const int ia[]等價於const in* ia
//size表示數組的大小,將它顯示地傳給函數用於控制對ia元素的訪問
void print(const int ia[],size_t size)
{
for (size_t i = 0;i !=size; ++i){
cout << ia[i] << endl;
}
}
int j[] = {0,1};
print(j,end(j) - begin(j));
需要注意的是,關於引用的討論同樣適用於指針,即函數不需要對數組元素執行寫操作的時候,數組形參應該是指向const的指針。
④數組引形參
C++語言允許將變量定義數組的引用,同樣形參也可以是數組的引用,引用形參綁定到對應的實參上,即綁定到數組上:
//形參是數組的引用,維度是類型的一部分
void print(int (&arr)[10])
{
for (auto elem :arr)
cout << elem << endl;
}
2.5 main:處理命令行選項
如果我們需要給main函數傳遞實參,一種常見的情況是用戶通過設置一組選項來確定所要執行的操作,假定main函數位於可執行文件prog之內,我們可以向程序傳遞下面的選項:
prog -d -o ofile data0
這些命令選項通過兩個形參傳遞給main函數:
int main(int argc,char *argv[]) { ... }
//or
int main(int argc,char **argv) { ... }
第一個形參argc表示數組中字符串的數量;第二個形參argv是一個數組,它的元素是指向C風格字符串的指針,所以main函數也可以定義成第二種形式,其中argv指向char*,當實參傳給main函數之後,argv的第一個元素指向程序的名字或者第一個空字符串,接下來的元素一次傳遞命令提供的實參,最後一個指針知乎的元素值保證爲0。需要注意的是當使用argv中的實參時,可選實參從argv[1]開始,argv[0]保存程序的名字,而非用戶輸入。
argv[0] = "prog";
argv[1] = "-d";
argv[2] = "-o";
agrv[3] = "ofile";
argv[4] = "data0";
argv[5] = 0;
2.6 含有可變形參的函數
爲了可以編寫可以處理不同數量實參的函數,C++11提供了兩種主要的方法;如果所有的實參類型相同,可以傳遞一個名爲initializer_list的標準庫類型;如果實參的類型不同,那麼可以編寫一種特殊的函數,即可變參數模板。
①initializer_list形參
initializer_list用於某種特定類型數值的數組,其類型定義在同名的頭文件中。與vector一樣initializer_list也是一種模板類型,定義initializer_list對象時,必須說明列表中所含元素的類型:
initializer_list<string> ls; //元素類型是string
initializer_list<int> li; //元素類型是int
與vector不一樣的是,initializer_list對象中的元素永遠是常量值,我們無法改變其對象中的值;我們使用如下形式編寫輸出錯誤信息的函數,使其可以作用域可變數量的實參:
void error_msg(initializer_list<string> li)
{
for (auto beg = li.begin();beg != li.end(); ++beg)
cout << *beg << " " ;
cout << endl;
}
參考文獻:
①C++ primer 第五版。