函數-Functions
說到函數,很多人覺得很簡單,但如果問你重載函數的判別原理,函數返回指針的注意事項,指針函數的定義等等,很多人就頭大了。函數是編程的基礎,這塊一定要打紮實。
函數基本知識
實參(Arguments)。形參(Parameters)。
形參初始化的順序
儘管我們知道哪個實參初始化了哪個形參,但我們並不知道這個初始化的順序!編譯器可以以任意順序初始化各個形參!因此,實參中不用包括改變自身的運算(如:++)。
初始化時的轉換
如果我們定義了一個函數
int fact(int);
那麼,在調用該函數時,實參一定要能轉變成形參的type。
fact("hello");//error,string不能轉成int
fact(3.14);//ok,float可以轉成int
形參列表
在函數的最外層定義的變量,不能和形參重複。即:
int fact(int a)
{
int a;//error!
{
int a;//ok!
}
}
因爲要使用的形參必須是有名字的,所以形參一般都會有名字。然而,如果我們在不斷更新代碼後,有的形參不再使用,那麼我們將該形參設爲沒有名字的,以示區分。注意的是,即便這個形參沒有名字,我們還是要通過實參給其賦值!
函數返回值
一個函數的返回值不能是數組或是函數,但是我們可以返回一個指針指向數組,或是指針指向函數。
局部變量
局部變量主要有兩種,一種是automatic objects,另一種是local static objects。
automatic objects
automatic objects在函數調用時生成,在函數結束時便會消亡(每一次重新調用該函數,這個object的值不會被保留)。所有傳遞來的參數都是automatic objects。
local static objects
而local static objects則不同,它在函數第一次執行時,還未執行到定義該object的語句之前就被初始化,它的值在每一次調用函數時都會被保留。其定義形式爲:
static type name;//ex: static int b;可以只聲明不初始化,也可以直接初始化
參數傳遞
參數傳遞有兩種:值傳遞(passing argument by value)和引用傳遞(passing argument by reference)。
如果函數定義時,形參是一個reference,那麼參數傳遞便是引用傳遞;否則,是值傳遞。
簡單地說,引用傳遞下,函數內對該參數的改變,在函數返回時依然有效,因爲函數內改變的是一個對原參數的引用,它和原參數指向同一個內存地址;而值傳遞下,函數內對參數的改變,在函數返回後不會影響原參數,因爲值傳遞是copy了一個一模一樣的備份,然後對該備份進行運算,這個備份和原來的參數的內存地址是不同的。
必須使用值傳遞的情況
一些類(包括IO 類),不能被複制(cannot be copied)。這種情況下,函數必須使用引用傳遞!例如:
void ioNotBeCopied(ifstream fin);
void ioNotBeCopied2(ifstream &fin);
void main()
{
ifstream fin;
ioNotBeCopied(fin);//error!
ioNotBeCopied2(fin);//ok!
}
事實上IO類的三個頭文件:iostream,fstream,sstream定義的類都是不能複製的,不能直接作爲函數參數傳遞,也不能存儲在vector中。Const 參數
top-level const
當我們初始化一個形參時,top-level const會被忽略(回顧top-level const:object本身不能被改變)。
對於一個top-level const的形參,我們可以用nonconst或top-level const object來初始化它。
對於一個nonconst形參,我們也可以用nonconst或top-level const object來初始化它。
注意:這裏的傳遞都是值傳遞,而非引用傳遞。
Pointer or Reference Parameters and const
回顧 low-level const:object指向或引用的對象不能被改變。
在pointer和reference中涉及const時,記住兩個原則:
1.我們能用nonconst初始化一個low-level const,但反之不行。
2.對於plain reference(即nonconst reference,別忘了reference只存在low-level const,不存在top-level const),必須使用同樣的類型(nonconst)來初始化。(這也解釋了上一部分top-level const的原則只適用於值傳遞)。
一些例子:
void reset(int &a);
void creset(const int &a);
int c =0;
const int cc = 0;
reset(&c);//error!非常量引用的初始值必須爲左值
creset(&cc);//error! const int*和const int&不兼容
reset(42);//error! 不能用plain reference綁定literal
creset(42);//ok!
這裏對“非常量引用的初始值必須爲左值”做一點說明,這句話的意思是int &a是一個非常量引用,所以它的初始值一定要是一個左值(lvalue),畢竟nonconst reference只能綁定object。而&c其實是c 的地址,是一個右值(rvalue),你無法對&c再次取地址。因此出現了error。
儘可能使用const reference(Use Reference to Const When Possible)
這樣做有兩個好處:
1.明確告訴使用函數的人,哪些變量可能會被改變,哪些不會。
2.plain reference比const reference有更多的限制,如不能綁定數字(literal),不能綁定一個const object。
數組參數(Array Parameters)
我們不能將數組copy給一個函數(數組沒有拷貝構造函數),當我們使用數組時,它經常會被轉換成指針。如果我們向函數傳遞一個數組,實際上我們傳遞的是指向數組第一個元素的指針。
下面的三個聲明都是完全一樣的:
void print (const int*);
void print (const int[]);
void print (const int[10]);
而當編譯器檢查時,它只會檢查實參是否是一個指針:
int i=0;
print(&i);//ok!
確保數組傳遞的正確性
正是因爲編譯器只會檢查是否是指針,因此傳遞數組很容易出錯,例如我們希望得到一個int[10],而事實上傳遞的是一個int[5],那麼在遍歷數組時就會越界!C++ Primer提供了三種方法,來避免這種錯誤。
1.用一個標記(marker)來表示數組的結束
在C-style string中,我們知道string的結尾是一個'\0‘,當函數讀到'\0'時,我們就知道數組結束了。這種方法對於有明顯的結尾標記(end marker)的情況很實用。但是對於int類型往往就不那麼好用了,因爲任何值都是有可能的。
2.傳遞頭元素(first element)和結尾元素(one past the last element)
藉助begin()和end()函數,我們可以輕鬆地獲得數組的起始和截止指針,將這兩個指針傳遞到函數內,則遍歷就不會出錯。例如:
void print(const int*beg,const int *end)
{
while(beg!=end)
cout<< *beg++ <<endl;
}
int j[2] = {0,1};
print(begin(j),end(j));
3.傳遞數組同時傳遞數組大小
這個思想非常容易想到。值得注意的是,數組的大小一般可以用size_t來表示。
void print(const int ia[],size_t size);
Array Parameters and const
和之前提到的reference儘量聲明成const一樣,array和pointer都應該儘量聲明成const,除非需要改變其中的值。
Array Reference(對array的引用)
我們可以定義一個reference,其指向一個array,作爲參數。這樣做的好處是,我們可以用range for輕鬆地遍歷數組:
void print(int (&arr)[10])
{
for(auto elem:arr)
cout<<elem<<endl;
}
注意:這裏的(&arr)兩側的括號是很有必要的!如果沒有括號,則是一個由10個reference構成的array。
但是,這樣做的缺點是,我們在初始化形參時,必須明確傳遞一個int[10]:
int j[2] = {0,1};
int k[10];
print(j);//error!
print(k);//ok!
事實上,我們也有辦法傳遞任意大小的數組,這將在很以後學到。
Passing a Multidimensional Array
傳遞多維數組(本質是有數組構成的數組)時,第二維即以後的數組大小必須被明確定義。
void print(int (*matrix) [10]);//()不能省略!
void print(matrix[][10]);//equivalent defination
命令行參數處理
我們知道,main函數其實可以接受命令行的參數,基本形式如下:
int main(int argc,char *argv[]);
int main(int argc,char **argv);//equivalent
argc表示包括函數名和參數在內的總個數,argv則是包括了函數名和參數在內的具體內容,最後以0結尾。舉例說明:
program -d
argc = 2;
argv[0]="program";
argv[1] = "-d";
argv[2] = 0;
不知道參數個數的函數(Functions with Varying Parameters)
如果我們事先不確定具體的函數參數個數,那麼有兩種主要方法來解決。
1.如果參數的類型都相同,那麼可以使用initializer_list類來完成。
2.如果參數的類型也不同,那麼我們用一種叫做variadic模板的特殊函數來完成(將在很以後介紹)。
另外,有一種參數類型叫做ellipsis(省略)也能完成這一功能,但只有當我們的程序需要和C語言兼容時我們才應該使用它,即在C++中不推薦使用ellipsis參數!
initializer_list參數
initializer_list<T> lst;//empty list of elements of type T
initializer_list<T> lst{a,b,c...};//elements are copies of the corresponding initializer!!!elements are const!!!
lst2(lst);//copy
lst2 = lst;//assign. Warning:copy or assign an initializer_list does not copy the elements in the list! The original and the copy share the elements!
lst.size();
lst.begin();
lst.end();
注意的是,initializer_list內的元素都是const,無法改變。void error_msg(initializer_list<string>il);
//expected,actual are strings
error_msg({"functionX",expected,actual});//ok!
其實,我沒有完全想明白initializer_list和vector的差別。initializer_list全是const,這一點vector可以很容易做到。唯一的差別是,initializer_list的copy和assign都是相當於別名(alias),這樣的好處是什麼呢?疑惑。
我想,可能的解釋是:initializer_list的構造只能通過{},這就限制了它的使用範圍。正如它的名字的意思,這就是用來初始化一個函數的特殊類,用vector也能實現,但是用initializer_list實現感覺分工更明確。
ellipsis parameters
void foo(parm_list, ...);
具體如何使用我並不清楚,大家可以自己上網學習,但如果是C++程序員,ellipsis parameter不那麼重要。Function Return Types
Never Return a Reference or Pointer to a Local Object
const string& manip()
{
string ret;
if(!ret.empty())
return ret;//WRONG!ret是一個local object
else
return "EMPTY";//WRONG!"EMPTY"也是一個local object。當然,如果函數返回的是const string,而非const string&,那麼這個是可以的
}
Reference Returns Are Lvalues
函數的返回類型,只有當是reference時是lvalue,其它情況都是rvalue。
如果函數的返回參數是一個reference(當然該reference不指向local object),那麼我們可以把這個返回參數當做一般的lvalue使用。
char& getValue(string &str,int ix)
{
return str[ix];
}
int main()
{
string s("a value");
get_val(s,0) = 'A';
}
List Initializing the Return Value(使用list initializer來定義返回變量)
這個很好理解,當我們的return type是vector等類型時,我們可以使用list initializer的形式定義返回變量。
vector<int> process()
{
return {1,2,3,4};
}
注意的是:如果返回的是一個built-int type(如int,float等),那麼{}中只能有一個值,且這個值的類型和返回類型必須完全一樣,不能有任何轉換。而如果返回一個類,那麼{}內的內容由該類決定。返回數組的指針(Return a Pointer to an Array)
因爲數組不能被複制,因此我們無法直接返回一個數組。然而,我們可以返回一個指向數組的指針。
要返回這樣的一個指針,有四種辦法:
1.用type alias
typedef int arrT[10];
using arrT = int[10];//equivalent
arrT* func();//func()返回一個指向int[10]的指針
2.直接定義
基本格式是:
Type (*function(parameter_list))[dimension]
一個例子:
int (*func())[10];//()不能省略!和上面的func等價
3.使用Trailing Return Type(拖尾返回類型)
C++ 11新特性,trailing return type可以定義任何函數,但是在函數的返回類型很複雜時尤其有用。
基本格式是:
auto function(parameter_list) -> return_type
一個例子:
auto func() -> int(*)[10];//和上面的func等價
4.使用decltype
int odd[] = {1,3,5,7,9};
decltype(odd) *func();//和上面的func不同,返回一個int(*)[5]。decltype(odd)得到的是一個int[5]
返回數組的引用(Return a Reference to an Array)
和上一節完全一樣,四種方法可以分別使用到數組的引用上。
函數重載
重載函數必須在參數的個數,或者類型上有所區別!如果兩個函數僅僅是返回類型不同,那麼重載這樣兩個函數將是錯誤的。
Overloading and const Parameters
void lookup(phone);
void lookup(const phone);//error!redeclaration of lookup(phone)
void lookup(phone*);
void lookup(phone* const);//error!redeclaration of lookup(phone*);
另一方面,如果兩個函數是low-level const的差別,那麼就可以重載。void lookup(account&);
void lookup(const account&);//new function
void lookup(account*);
void lookup(const account*);//new function
const_cast and Overloading
const string& shorterString(const string &s1,const string &s2)
{
return s1.size()<=s2.size()?s1:s2;
}
這時,如果我們向函數傳遞兩個nonconst string,那麼得到的結果依舊是const string。現在,我們希望在這種情況下,能得到一個nonconst string,除了重新寫一個新的函數外,我們可以這麼做:string& shorterString(string&s1,string&s2)
{
auto &r = shorterString(const_cast<const string&>(s1),
const_cast<const string&>(s2));
return const_cast<string&>(r);
}
這裏auto &r中的&的作用是保證返回的內容就是s1或s2中的一個,而不是重新創造的string。Calling an Overloaded Function
Overloading and Scope
string read();
void print(const string &);
void print(double);
void foo(int ival)
{
string s = read();//ok!
bool read = false;//hides the outer declaration of read
string s = read();//error!read is a bool variable,not a function
void print(int);//hides previous instances of print
print("Value: ");//error!
print(ival);//ok!print(int) is visible
print(3.14);//ok!calls print(int);print(double) is hidden
}
缺省參數(Default Arguments)
Default Argument Declaration
<pre name="code" class="cpp">string screen(int,int,char=' ');//ok
string screen (int,int,char='*');//error! redeclaration
string screen (int = 24,int =80,char);//ok! adds default arguments,現在兩個screen的缺省值是一樣的!都是int=24,int=80,char=' '
string screen(int =24,int=80,char=' ');//error! 最後一個char不能再次定義!
注意:我在VS2013測試時,輸入如下:string screen(int, int=20);
string screen(int = 10, int);
在編輯器內,第二行的screen下面會有紅色波浪線,顯示“錯誤:默認實參不在形參列表的結尾”,但是編譯過程沒有報錯,也可以正常運行。詭異。。Default Argument Initializers
int a=1;
int b();
string screen(int =a,int =b());//現在的默認調用是screen(1,b());
void func()
{
a=2;
screen();//調用screen(2,b());
}
Inline和constexpr函數
inline Functions
const string& shorterString(const string &s1,const string &s2)
{
return s1.size()<=s2.size()?s1:s2;
}
定義這種短小的函數有如下四個好處:inline Functions Avoid Function Call Overhead
inline const string& shorterString(const string &s1,const string &s2)
{
return s1.size()<=s2.size()?s1:s2;
}
應該注意的是:inline函數只應該用來定義那些簡短小巧的代碼。inline只是一個request(申請),編譯器可以選擇忽略這個申請。大部分編譯器對遞歸函數的inline將忽略,對於超過75行的函數將幾乎肯定被忽略。constexpr Functions
constexpr int scale(int cnt){return 3*cnt;}//只要cnt是一個const,那麼scale返回的便是一個常量
再看個列子:int arr[scale(2)];//ok!scale(2)是常表達式
int i=2; //i is not a constant expression
int a2[scale(i)];//error!
最後要說的是!constexpr目前還沒有被Visual Studio所接受!暈,具體原因涉及到編譯器的實現細節,和類模板似乎有一定關係。我也不是很懂,大家可以參考這篇文章:Put inline and constexpr Functions in Header Files
Aids for Debugging
the assert Preprocessor Macro
assert(expression);
如果語句運算的結果爲真,那麼assert不做任何動作;如果結果爲假,那麼將中斷程序,在終端上會顯示debug結果(在哪個具體位置遇到了assert失敗的情況)。the NDEBUG Preprocessor Variable
$ CC -D NDEBUG main.C #linux
$ CC /D NDEBUG main.C #microsoft
配合NDEBUG,我們還可以實現比assert更復雜的debug功能。例如:void print()
{
#ifndef NDEBUG
cerr<<__FILE__<<endl;
#endif
}
即通過宏定義來控制cerr<<__FILE__<<endl這段代碼是否執行。__FILE__:string, name of file
__LINE__:int, current line number
__TIME__:string,time the file was compiled
__DATE__:string,date the file was compiled
Function Matching
void f();
void f(int);
void f(int,int);
void f(double,double=3.14);
f(5.6);//calls void f(double,double)
Candidate and Viable Functions
尋找最佳匹配函數
Argument Type Conversions
Matches Requiring Promotion or Arithmetic Conversion
void ff(int);
void ff(long);
void ff(short);
void ff(long long);
ff('a');//調用ff(int)
另一方面,如果沒有int類型,可能就無法判斷。void ff(long);
void ff(short);
void ff(long long);
ff('a');//報錯!無法判斷
所有的算數轉換都是一樣的!從int到unsigned int不比從int到double來的優先級更高。另外,literal中,整數一般默認是int,浮點數默認是double。
void manip(int);
void manip(float);
manip(3.14);//error!無法判斷
一個特殊的例子:void ff(int);
void ff(long long);
ff(999999999999999);//調用ff(long long)
Function Matching and Const Arguments
void lookup(account&);
void lookup(const account&);
const account a;
account b;
lookup(a);//調用lookup(const account&)
lookup(b);//調用lookup(account&);
函數指針(Pointers to Functions)
Define a Function Pointer
bool lengthCompare(const string&,const string&);//該函數的類型是bool(const string&,const string&)
bool (*pf)(const string&,const string&);//定義的函數指針
注意定義函數指針的()是非常必要的!否則你就定義了一個函數!Using a Function Pointer
pf = lengthCompare;
pf = &lengthCompare;//equivalent!
我們也可以直接用函數指針來調用該函數,具體的:bool b1 = pf("hello","goodbye");
bool b2 = (*pf)("hello","goodbye");//equivalent!
bool b3 = lengthCompare("hello","goodbye");//equivalent!
任意兩個不同的function type之間不存在轉換方法(廢話麼。。。),當然我們可以對任何function pointer賦值nullptr或0,來表示他們沒有指向任何函數。Function Pointer Parameter
void f(int,bool pf(int));//ok!第二個參數被認爲是函數指針
void f(int,bool (*pf)(int));//equivalent!顯式表明第二個參數是函數指針
使用type aliases和decltype能幫我們簡化代碼。bool lengthCompare(int);
//Func,Func2是函數類型
typedef bool Func(int);
type decltype(lengthCompare) Func2;//equivalent
//FuncP,FuncP2是函數指針類型
type bool (*FuncP)(int);
type decltype(lengthCompare) *FuncP2;//equivalent
void f(int,Func);//這四個定義和之前的f()完全一樣
void f(int,Func2);
void f(int,FuncP);
void f(int,FuncP2);
Returning a Pointer to Function
using F = int(int*,int);//F是函數類型,不是指針
using PF = int(*)(int*,int);//PF函數指針類型
記住和參數列表中不同,參數列表中一個函數類型會被自動轉換成函數指針類型,但是返回類型卻不會!我們必須使用一個顯式的函數指針類型來定義函數的返回類型。PF f(int);//ok
F f(int);//error!
F* f(int);//ok!
當然,我們也可以顯式地定義這樣的函數。int (*f(int)) (int*,int);
另外,也可以用拖尾返回類型(trailing return)來定義。auto f(int) ->int(*)(int*,int);
Using auto or decltype for Function Pointer Types
int sumLength(string,string);
decltype(sumLength) *f(int);
需要注意的是,decltype返回的是函數類型,所以我們需要在函數名前加一個*來表示返回函數指針,上面例子中如果缺少了*,則會報錯。auto f(int) -> decltype(sumLength)*;
同樣,這裏最後的*也不能省略。