【C++基礎總結】函數

C++基礎總結:變量和基本類型

一、函數基礎

  • 函數調用過程:
    • 將控制權轉移給調用函數,函數隱式地定義並初始化它的形參;
    • 當執行到return語句或者執行完函數內全部語句後返回結果。
  • 實參和形參:實參是形參的初始值
  • 函數的返回類型
    • 大多數類型都可以作爲函數的返回類型。void也可以,它表示不返回任何值。
    • 函數的返回類型不能是數組類型或函數類型,但可以是指向數組或函數的指針。
  • 局部靜態對象
    • 局部靜態對象在程序的執行路徑第一次經過對象定義語句時初始化,並且直到程序最終才被銷燬。
    • 局部靜態對象只能被初始化一次,不會被多次初始化!如果沒有顯示初始值,它將執行值默認初始化,內置類型的局部靜態變量初始化爲0
    • 示例:
    int count_calls()
    {
    	static int count = 0;
    	return ++count;
    }
    
    int main()
    {
    	for(int i = 0; i < 6; ++i)
    		std::cout <<count_calls() << ", ";
    }
    
    輸出結果:
    1, 2, 3, 4, 5, 
    

二、參數傳遞

  • 形參初始化的原理和變量初始化一樣。
  • 實參可以通過引用傳遞或者值傳遞給形參初始化。

1. 傳值參數

  • 通過值傳遞的形參是實參的拷貝,兩者獨立,不會相互影響
  • 指針形參
    • 當執行指針拷貝操作時,拷貝的是指針的值。拷貝後,兩個指針是不同的指針。所以,我們可以通過指針形參訪問或修改實參指向的值,如果修改形參的指向,實參指向並不會因此改變。

2. 傳引用參數

  • 拷貝大的類類型對象或者容器對象時效率低,甚至有的類類型根本不支持拷貝操作,此時我們可以使用引用形參訪問該類型的對象。
  • 如果函數無須改變引用形參的值,最好將其聲明爲常量引用。
  • 內置類型初始化一般會比引用效率高,所以內置類型無須使用常量引用,可以使用引用或傳值參數。
  • 使用引用形參是方便返回多個結果的有效途徑。

3. const形參和實參

  • 某變量或字面值是否可以用作參數傳遞,可以考慮其是否可以用於變量初始化的值。
  • 我們要儘可能的使用常量引用代替普通引用。因爲普通引用會給使用不需要修改參數函數的使用者一種誤導,並且普通引用會限制可傳入參數類型。我們不能把const對象、字面值或者需要類型轉換的對象傳遞給普通的引用形參。

4. 數組形參

  • 數組有連個特性,分別是:不允許拷貝數組以及使用數組時(通常)會將其轉換成指針。
  • 因爲不能拷貝數組,所以無法使用值傳遞的方式使用數組參數。
  • 因爲數組會被轉換成指針,所以當我們爲函數傳遞一個數組時,實際上傳遞的是指向數組首元素的指針。
  • 儘管不能以值傳遞的方式傳遞數組,但我們可以把形參寫成類似數組的形式:
    //儘管形式不同,但是下面三個函數是等價的
    void print(const int*);
    void print(const int[]);		//函數的用途是作用於一個數組
    void print(const int[10]);		//這裏的維度表示我們期望數組含有多少元素,實際並不一定
    
    儘管表現形式不同,但三個函數等價:每個函數的唯一形參都是const int*類型。
    int i =0, j[2] = {0, 1};
    print(&i);  //正確:&i的類型是int*
    print(j);  //正確:j轉換成int*並指向j[0]
    
  • 爲防止數組指針越界,管理指針形參有三種常用技術:
    • 使用標記指定數組長度
      這種方法是在數組結尾有一個結束標誌,典型就是C風格字符數組。字符數組最後跟着一個空字符,函數在處理C風格字符串時遇到空字符停止:
      	void print(const char* cp)
      	{
      		if(cp)								//若cp不是空指針
      			while(*cp)					//若指針所指字符不是空字符
      				cout << *cp++;			//輸出當前字符並指向下一個字符
      	}
      
      這種方式適用於有明顯結束標記的數組,但對於int這種數組就不太有效了。
    • 使用標準庫規範
      第二種方法來自標準庫技術,是傳入數組首元素和尾後元素的指針。
      	void print(const int* beg, const int* end)
      	{
      		//輸出beg和end之間(不含end)的所有元素
      		while(beg != end)
      			cout << *beg++ << endl;  //輸出當前元素並將指針向前移動一個位置
      	}
      
      可以通過調用標準庫的begin和end函數,代碼安全地獲取數組首指針和尾後指針
      	int j[2] = {0, 1};
      	print(std::begin(j), std::end(j));
      
    • 顯式傳遞一個表示數組大小的形參
      第三種方法是專門定義一個表示數組大小的形參,在C程序和舊的C++程序中常常使用這個方法。
      	//const int ia[]等價於const int* 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));
      
  • 數組引用形參
    C++語言允許將變量定義成數組的引用,所以,形參也可以是數組的引用。引用形參綁定到對應的實參上,也就是綁定到數組上:
    void print(int (&arr)[10])		//&arr兩端的括號必不可少,後邊的10與傳入數組指針形參不同,這個具有實際意義,即數組長度必須爲10
    {
    	for(auto elem : arr)
    		cout << elem << endl;
    }
    
  • 傳遞多維數組
    • 和所有數組一樣,當將多爲數組傳遞給函數時,真正傳遞的是指向數組首元素的指針。
    //matrix指向數組的首元素,該數組的元素是由10個整數構成的數組
    void print(int (*matrix)[10], int rowSize){/* ... */}
    等價於
    void print(int matrix[][10], int rowSize){/* ... */}
    matrix看起來是一個二維數組,實際上形參是指向含有10個整數的數組的指針
    
    int *matrix[10];		//10個指針構成的數組
    int (*matrix)[10];		//指向含有10個整數的數組的指針
    

5. main:處理命令行選項

  • 通常,我們使用空參主函數,但我們也可以給主函數傳入參數
int main(int argc, char* argv[]){...}

第一個形參是數組長度,第二個形參是C風格字符串數組,argv指向char*,所以也可以寫成:

int main(int argc, char** argv){...}

當我們使用argv中的參數時,一定要記得可選的實參從argv[1]開始,argv[0]保存程序的名字,而非用戶輸入

6. 含有可變形參的函數

C++11標準提供了兩種主要方法:如果所有實參類型相同,可以傳遞一個名爲initializer_list的標準庫類型;如果實參的類型不同,我們可以使用可變參數模板,此方法在後續的模板章節中總結。除此之外,C++還提供了一種特殊的形參類型,即省略符形參。

initializer_list形參

  • initializer_list是一種標準庫類型,用於表示某種特定類型的值的數組。initializer_list類型定義在同名的頭文件中。

  • 和vector一樣,initializer_list也是一種模板類型,定義對象時,必須說明列表中所含元素的類型。

  • 和vector不同的是,initializer_list對象中的元素永遠是常量值,無法改變initializer_list對象中元素的值

  • 示例:

    	void error_msg(initializer_list<string> il)
    	{
    		for(auto beg = il.begin(); beg != il.end(); ++beg)
    			cout << *beg << " ";
    	}
    
    	error_msg({"functionx", "okay"});		//通過一對花括號傳入序列
    
  • initializer_list提供的操作

    操作 說明
    initializer_list lst; 默認初始化;T類型元素的空列表
    initializer_list lst{a, b, c…}; lst的元素數量和初始值一樣多;lst的元素是對應初始值的副本;列表中的元素是const
    lst2(lst) 拷貝或賦值一個initializer_list對象不會拷貝列表中的元素;拷貝後,原始列表和副本共享元素
    lst2 = lst 拷貝或賦值一個initializer_list對象不會拷貝列表中的元素;拷貝後,原始列表和副本共享元素
    lst.size() 類表中的元素數量
    lst.begin() 返回指向lst中首元素的指針
    lst.end() 返回指向lst中尾後元素的指針

省略符形參
省略符形參是爲C++便於訪問某些特殊的C代碼而設置的,這些代碼使用了名爲varargs的C標準庫功能。通常,省略符形參不應用於其他目的。

省略符形參應該僅僅用於C和C++通用的類型。特別注意的是,大多數類類型的對象在傳遞給省略符形參時都無法正確拷貝。

省略符形參只能出現在形參列表的最後一個位置

void foo(parm_list, ...);
void foo(...);

我們可以添加cstdarg文件,使用其中定義的函數方法訪問省略符形參

#include <cstdarg>

void VarArgFunc(int i, ...)
{
	va_list pArg = nullptr;		//創建參數指針
	va_start(pArg, i)			//初始化參數指針
	int a = va_arg(pArg, int);	//獲取第一個參數
	char b = va_arg(pArg, char);	//獲取第二個參數
	va_end(pArg);		//清空參數指針
}

三、返回類型和return語句

1. 有返回值函數

  • 返回一個值的方式和初始化一個變量和形參的方式完全一樣:返回的值用於初始化調用點的一個臨時變量,該臨時量就是函數調用的結果。
  • 不要返回局部對象的引用或指針!所以,我們返回的引用或指針需要是函數調用之前就已經存在的變量。
  • 調用一個返回引用的函數會得到左值,其它返回類型得到右值。
  • C++11新標準規定,函數可以返回花括號包圍的值的列表。
  • main函數可以沒有顯式返回值,main函數的返回值可以看作是狀態指示器。返回0表示執行成功,返回其他值表示執行失敗。爲了使返回值與機器無關,cstdlib頭文件定義了兩個預處理變量,我們可以使用這兩個變量表示成功與失敗:EXIT_FAILURE和EXIT_SUCCESS
  • 如果一個函數調用了自身,無論是直接還是間接,都稱該函數爲遞歸函數。如果函數出現不斷調用自身直到程序棧空間耗盡爲止,我們稱其爲循環遞歸。另外,main函數不能調用自己。

2. 返回數組指針

  • 因爲數組不能拷貝,所以函數不能返回數組。但是,函數可以返回數組的指針或引用。
    使用類型別名
typedef int arrT[10];		//arrT是一個類型別名,表示的類型是含有10個整數的數組
using arrT = int[10];		//與上條等價
arrT* func(int i);			//func返回一個指向含有10個整數的數組的指針

聲明一個返回數組指針的函數

int arr[10];		//arr是一個含有10個整數的數組
int* p1[10];		//p1是一個含有10個指針的數組
int (*p2)[10] = &arr;		//p2是一個指針,它指向含有10個整數的數組

下面的函數聲明:
int (*func(int i))[10];

  • func(int i)表示調用func函數時需要一個int類型的實參
  • (*func(int i))意味着我們可以對函數調用的結果執行解引用操作
  • (*func(int i))[10]表示解引用func的調用將得到一個大小是10的數組
  • int (*func(int i))[10]表示數組中的元素是int類型

使用尾置返回類型
尾置返回類型是C++11的新標準,常用語比較複雜的返回類型,比如數組的指針或數組的引用。

下例爲函數的聲明:

auto func(int i) -> int(*)[10];	//返回類型放到形參之後,原來放置返回類型的地方使用auto

四、函數重載

同一作用域內,函數名相同,形參列表不同的函數稱之爲重載函數。

main函數不能重載。

重載和const形參
擁有頂層const的形參無法與另一個沒有頂層const的形參區分:

Record lookup(Phone);
Record lookup(const Phone);		//重複聲明瞭Record lookup(Phone)

Record lookup(Phone*);
Record lookup(Phone* const);	//重複申明瞭Record lookup(Phone*)

底層const可以重載:

Record lookup(Account&);		//函數作用於Account引用
Record lookup(const Account&);	//新函數,作用於常量引用

Record lookup(Account*);		//新函數,作用於指向Account的指針
Record lookup(const Account*);	//新函數,作用於指向常量的指針

const_cast和重載

string& shorterString(string& s1, string& s2)
{
	auto& r = shorterString(const_cast<const string&>(s1),
							const_cast<const string&>(s2));
	return const_cast<string&>(r);
}

五、特殊用途語言特性

1. 默認實參

  • 局部變量不能作爲默認實參

2. 內聯函數和constexpr函數

調用函數一般比求等價表達式的值慢一些。大多數機器上,函數調用過程爲:調用前要先保存寄存器,並在返回時恢復;可能需要拷貝實參;程序轉向一個新的位置繼續執行。

內聯函數可避免函數調用的開銷
在函數聲明前加inline,可以將其聲明成內聯函數。內聯函數是向編譯器發送請求,將其在編譯時直接展開到函數調用處,當然,編譯器也可以忽略這個請求。

很多編譯器都不支持內聯遞歸調用。

constexpr函數
constexpr函數是指能用於常量表達式的函數。定義constexpr函數需要遵循約定:

  • 函數的返回類型及所有形參的類型都得是字面值類型
  • 函數體中必須有且只有一條return語句:
constexpr int new_sz() {return 42;}
constexpr int foo = new_sz();		//正確:foo是一個常量表達式

如果arg是常量表達式,則scale(arg)也是常量表達式:

constexpr size_t scale(size_t cnt){return new_sz() * cnt;}

int arr[scale(2)];		//正確:scale(2)是常量表達式
int i = 2;
int a2[scale(i)];		//錯誤:scale(i)不是常量表達式

constexpr函數不一定返回常量表達式。

內聯函數和constexpr函數通常定義在頭文件中。

3. 調試幫助

程序可以包含一些用於調試的代碼,但是這些代碼只在開發程序時使用,當應用程序開發完成後,要先屏蔽調試代碼。這種方法用到兩項預處理功能:assert和NDEBUG。

assert預處理宏

  • assert是一種預處理宏,預處理宏是一個預處理變量,行爲類似於內聯函數。
  • assert宏定義在cassert頭文件中。
  • 可以像下面代碼一樣,檢查表達式輸出結果,如果表達式爲假(0),assert輸出信息並中止程序執行;如果爲真(非0),程序繼續執行:assert(expr);

NDEBUG預處理變量
assert的行爲依賴於一個名爲NDEBUG的預處理變量的狀態。如果定義了NDEBUG,則assert什麼也不做。默認狀態下,沒有定義NDEBUG,此時assert將檢查運行時檢查。

六、函數指針

函數指針指向的是函數,而非對象。函數的類型由他的返回類型和形參類型共同決定,與函數名無關。要想聲明一個可以指向函數的指針,只需要用指針替代函數名即可:

bool (*pf)(const string&, const string&);		//未初始化

*pf兩端的括號必不可少。如果不寫括號,則pf是一個返回值爲bool*的函數

使用函數指針
當函數名作爲值使用時,該函數自動地轉換成指針。

pf = lengthCompare;		//pf指向名爲lengthCompare的函數
pf = &lengthCompare;	//等價的賦值語句:取地址符可選

我們還可以直接使用指向函數的指針調用該函數,無須提前解引用指針:

bool b1 = pf("hello", "goodbye");			//調用lengthCompare函數
bool b2 = (*pf)("hello", "goodbye");		//一個等價的調用
bool b3 = lengthCompare("hello", "goodbye");//另一個等價的調用

函數指針形參
和數組類似,雖然不能定義函數類型的形參,但是形參可以是指向函數的指針。此時,形參看起來是函數類型,實際卻是指針:

void useBigger(const string& s1, const string& s2, bool pf(const string&, const string&));
等價於
void useBigger(const string& s1, const string& s2, bool(*pf)(const string&, const string&));
也可以直接把函數作爲實參使用,此時他會自動轉換成指針
useBigger(s1, s2, lengthCompare);

類型別名和decltype可以簡化函數指針的代碼:

//Func和Func2是函數類型
typedef bool Func(const string&, const string&);
typedef decltype(lengthCompare) Func2;			//等價的類型
//FuncP和FuncP2是指向函數的指針
typedef bool(*FuncP)(const string&, const string&);
typedef decltype(lengthCompare) *FuncP2;		//等價的類型

可以使用以下形式聲明useBigger:

void useBigger(const string&, const string&, Func);
void useBigger(const string&, const string&, Func2);

返回指向函數的指針
和數組類似,雖然不能返回一個函數,但是可以返回指向函數類型的指針。
要想聲明一個返回函數指針的函數,最簡單的方法是使用類型別名:

using F = int(int*, int);		//F是函數類型,不是指針
using PF = int(*)(int*, int);	//PF是指針類型

我們必須顯式地將返回類型指定爲指針:

PF f1(int);		//正確:PF是指向函數的指針,f1返回指向函數的指針
F f1(int);		//錯誤:F是函數類型,f1不能返回一個函數
F* f1(int);		//正確:顯式地指定返回類型時指向函數的指針

我們也能直接聲明f1:

int (*f1(int))(int*, int);

也可以使用尾置返回類型的方式:

auto f1(int) -> int (*)(int*, int);

將auto和decltype用於函數指針類型

string::size_type sumLength(const string&, const string&);
string::size_type largerLength(const string&, const string&);
//getFcn函數返回指向sumLength或者largerLength的指針
decltype(sumLength)* getFcn(const string&);

當我們將decltype作用於某個函數時,它返回函數類型而非指針類型。因此,我們顯式地加上*以表明我們需要返回指針,而非函數本身。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章