6.1 函数基础
- 函数是一个命名的代码块,通过调用函数执行相应的代码。可以有0个或多个参数,可以重载。
- 函数的调用:
用实参初始化函数对应的形参(类型、个数需要匹配)。
主函数暂时中断,被调函数开始执行。
6.1.1 局部对象
- 名字的作用域是程序文本的一部分,名字在其中可见。
- 对象的生命周期是程序执行过程中该对象存在的一段时间。
- 自动对象:生命周期从变量声明开始,到函数块末尾结束。
- 局部静态对象:生命周期从变量声明开始,直到程序结束才销毁。
6.1.2 函数声明
- 如果一个函数永远也不会被我们用到,那么它可以只有声明没有定义。
- 函数声明也称作函数原型。
6.2 参数传递
6.2.1 传值参数
- 形参不改变实参。
- 在C++中,建议使用引用类型的形参替代指针。
//该函数接受一个指针,然后将指针所指的值置为0
void reset (int *ip) { //指针形参
*ip = 0; //改变指针ip所指对象的值
ip = 0; //只改变了ip的局部拷贝,实参未被改变
}
int main(){
int i=42;
reset(&i);//改变i的值而非i的地址
cout<<"i="<<i<<endl; //输出i = 0
return 0;
}
6.2.2 传引用参数
- 当某种类型不支持拷贝操作时,函数只能通过引用参数访问该类型的对象。
//该函数接受一个int对象的引用,然后将对象指的值置为0
void reset (int &i)//i是传给reset函数的对象的另一个名字{
i = 0; //改变了i所引用对象的值
}
int main(){
int j=42;
reset(j);//j采用传引用方式,它的值被改变
cout<<“j="<<j<<endl; //输出j = 0
return 0;
}
- 如果函数无需改变引用形参的值,最好将其声明为常量引用。
//比较两个string对象的长度
bool isShroter(const string &s1, const string &s2)
{
return s1.size() <s2.size();
}
- 使用引用形参返回额外信息,可以为函数一次返回多个结果提供有效途径。
//返回s中c第一次出现的位置索引,引用参数occurs统计c出现的总次数
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; //将出现的次数加1
}
}
return ret;
}
6.2.3 const形参和实参
- 形参的顶层const会被忽略掉,传给它常量对象或非常量对象都是可以的。
void fcn(const int i){/*fcn能够读取i,但不能向i写值*/}
void fcn(int i){ } //错误:重复定义
- 尽量使用常量引用,因为把函数不会改变的形参定义成普通引用,不仅会给函数调用者带来误导(即函数可以修改它的实参的值),此外普通引用会极大地限制函数所能接受的实参类型。
void reset (int *ip) { *ip = 0; ip = 0; }
//int reset(const int * a){return 0;}
void reset (int &ip) { ip = 0; }
//int reset(const int & a){return 0;}
string::size_type find_char(const string&s,char c,string::size_type &occurs){ …略... }
int main(){
int i = 0;
const int ci = i;
string::size_type ctr = 0;
reset(&i); //调用形参类型是int*的reset函数
reset(&ci); //错误:不能用指向const int对象的指针初始化int*
reset(i); //调用形参类型是int&的reset函数
reset(ci); //错误:不能把普通引用绑定到const对象ci上
reset(42); //错误:不能把普通引用绑定到字面值上
reset(ctr); //错误:类型不匹配,ctr是无符号类型
//正确:find_char的第一个形参是对常量的引用
find_char( "Hello World!“ ,'o',ctr);
}
6.2.4 数组形参
- 和其他使用数组的代码一样,以数组作为形参的函数也必须确保使用数组时不会越界。
//尽管形式不同,但这三个print函数是等价的
//每个函数都有一个const int*类型的形参
void print(const int*);
void print(const int[]);
void print(const int[10]);//这里的10只是一个期望
- 管理数组实参的方法:
要求数组本身包含一个结束标记,使用这种方法的典型示例是C风格字符串。适用于那些有明显结束标记且该标记不会与普通数据混淆的情况。
void print(const char *cp){
if(cp) //若cp不是一个空指针
while(*cp) //取出来的不是空字符
cout<<*cp++;//输出当前字符,并将指针移动到下一个位置
}
使用标准库规范。传递指向数组首元素和尾后元素的指针。使用标准库begin和end函数提供所需的指针。
void print(const int *beg, const int *end){
//输出beg到end之间(不含end)的所有元素
while(beg!=end)
cout<<*beg++<<endl;
}
int j[2] = {0,1};
print(begin(j),end(j));
专门定义一个表示数组大小的形参。
//const int ia[]等价于const int* ia
//size 表示数组的大小
void print(const int ia[],size_t size){
for(size_t i = 0; i!=size; ++i)
cout<<ia[i]<<endl;
} //可以使用print(j,end(j)-begin(j));进行调用
- C++允许将变量定义成数组的引用。
//正确:形参是数组的引用,维度是类型的一部分
void print(int (&arr)[10]){//()不能少
for(auto elem:arr)
cout<<elem<<endl;
}
int i=0,j[2]={0,1},k[10]={0,1,2,3,4,5,6,7,8,9,};
print(&i);//错误:实参不是含有10个整数的数组
print(j);//错误:实参不是含有10个整数的数组
print(k); //正确
- 传递多维数组:在C++中没有实际的多维数组。
void print(int (*matrix)[10],int rowSize){ };
void print(int matrix[][10], int rowSize){ };//实际上是指向10个整数的数组的指针
6.2.5 main:处理命令行选项
- 当使用argv中的实参时,一定要记得可选的实参从argv[1]开始;argv[0]保存程序的名字,而非用户输入。
int main(int argc,char *argv[]){ }
int main(int argc,char **argv){ }
//如果运行prog -d -o ofile data0
//argc = 5或空字符串
//argv[0] = "prog"
//...
//argv[4] = "data0"
//argvp[5] = 0 //最后一个指针之后的元素值保证为0
6.2.6 含有可变形参的函数
- 处理不同数量实参的函数(C++11)
如果所有的实参类型相同,可以传递一个名为initializer_list的标准库类型。
如果实参的类型不同,可编写可变函数参数模板。
- initializer_list
initializer_list是一种模板类型,用于表示某种特定类型的值的数组。
initializer_list ls; //元素类型是string
initializer_list li; //元素类型是int
initializer_list对象中的元素永远是常量值。
如果想向initializer_list形参中传递一个值的序列,则必须把序列放在一对花括号内。
含有initializer_list形参的函数也可以同时拥有其他形参。
initializer_list<string> ls; //元素类型是string
initializer_list<int> li; //元素类型是int
- 省略符形参
省略符形参只能出现在形参的最后,是为了便于C++程序访问某些特殊的C代码而设置的,这些代码使用了名为varargs的C标准库功能。
void foo(parm_lsit, …);
void foo(…);
注意:大多数类类型的对象在传递给省略符形参时都无法正确拷贝。
省略符形参所对应的实参无须类型检查。
6.3 返回类型和return语句
6.3.1 无返回值函数
- 返回void的函数不要求非得有return语句,因为在这类函数的最后一句后面会隐式地执行return。
6.3.2 有返回值函数
- 在含有return语句的循环后面应该也有一条return语句,如果没有的话该程序就是错误的。很多编译器都无法发现此类错误。
- 不要返回局部对象的引用或指针,因为函数终止意味着局部变量的引用将指向不再有效的内存区域。
//严重错误:这个函数试图返回局部对象的引用
const string &manip(){
string ret;
//以某种方式改变一下ret
if(!ret.empty())
return ret;//错误:返回局部对象的引用!
else
return "Empty"; //错误:“Empty”是一个局部临时变量
}
- 函数的返回类型决定函数调用是否是左值。调用一个返回引用的函数得到左值,其他返回类型得到右值。如果返回类型是常量引用,则不能给调用的结果赋值。
- 函数可以返回花括号包围的值的列表。**(C++11)**此处的列表用来对表示函数返回的临时量进行初始化。如果列表为空,临时量执行值初始化,否则,返回的值由函数的返回类型决定。如果函数返回的是内置类型,则花括号包围的列表最多包含一个值,而且该值所占空间不应该大于目标类型的空间。如果函数返回的是类类型,由类本身定义初始值如何使用。
vector<string> process(){
//...
//expected和actual是string对象
if(expected.empty())
return{};//返回一个空vector对象
else if (expected == actual)
return{"functionX","okay"}; //返回列表初始化的vector对象
else
return{"functionX",expected,actual};
}
6.3.3 返回数组指针
- 数组不能拷贝,所以函数不能直接返回数组。
- 使用类型别名定义返回数组的指针或引用的函数。
typedef int arrT[10]; //arrT是一个类型别名,using arrT = int[10]; (容量在变量名后)
arrT* func(int i); //func返回一个指向含有10个整数的数组的指针
func(int i) //表示调用func函数时需要一个int型的实参
(*func(int i)) //意味着我们可以对函数调用的结果执行解引用操作
(*func(int i))[10] //表示解引用func将得到一个大小为10的数组
int (*func(int i))[10] //表示数组的大小为10
- 使用尾置返回类型(C++11)。尾置返回类型跟在形参列表后面并以一个->符号开头。为了表示函数真正的返回类型跟在形参列表之后,在本应该出现返回类型的地方放置一个auto。
auto func(int i) -> int (*)[10] //func接受一个int类型的实参,并返回一个指向含有10个int类型数组的指针。
//使用decltype:当我们知道函数返回的数组将指向哪个数组
int odd[] = {1,3,5,7,9};
int even[] = {0,2,4,6,8};
//返回一个指针,该指针指向含有5个整数的数组
decltype(odd) *arrPtr(int i)
{
return (i%2)?&odd:&even;//返回一个指向数组的指针
}
6.4 函数重载
- 函数重载:同一作用域,函数名字相同但形参列表不同(数量、类型、顺序)。
- 返回值类型不能作为函数重载的依据。
Record lookup(const Account&);
bool lookup(const Account&);//错误
- 顶层const不影响传入函数的对象。一个拥有顶层const的形参无法和另一个没有顶层const的形参区分开来。
- 如果形参是某种类型的指针或引用,则通过区分其指向的是常量对象还是非常量对象可以实现函数重载,此时const是底层的。
- 最好只重载那些确实非常相似的操作。
- 调用重载函数时的三种可能结果:
最佳匹配
无匹配
二义性调用
6.4.1 重载与作用域
- 在C++语言中,名字查找发生在类型检查之前。一旦在当前作用域中找到了所需的名字,编译器就会忽略掉外层作用域中的同名实体。
string read();
void print(const string&);
void print(double);//重载print函数
void fooBar(int ival) {
bool read = false;//隐藏了外层的read
string s = read();//错误:read是一个布尔值
//不好的习惯
void print(int);//隐藏了外层的print
print("Value:");//错误
print(ival);//正确
print(3.14);//正确:等价于print(3)
}
6.5 特殊用途语言特性
6.5.1 默认实参
- 一旦某个形参被赋予了默认值,它后面的所有形参都必须有默认值。
typedef string::size_type sz;
string screen(sz ht = 24, sz wid = 80, char backgrnd = ' ' );
string window;
window = screen(); //等价于screen(24,80,' ')
window = screen(66); //等价于screen(66,80,' ')
window = screen(66, 256); //screen(66,256,' ')
window = screen(66,256, '#'); //screen(66,256, '#')
window = screen( , , '?'); //错误:只能省略尾部的实参
window = screen( '?'); //调用screen('?',80,' ')
//多次声明同一个函数也是合法的
string screen2( sz, sz, char = ' ' );
string screen2( sz, sz, char = '*' );//错误:重复声明
string screen2(sz = 24, sz = 80, char );//正确
- 设计含有默认实参的函数时,应尽量让不怎么使用默认值的形参出现在前面,而让那些经常使用默认值的形参出现在后面。
- 在给定的作用域中一个形参只能被赋予一次默认实参。
- 函数的后续声明只能为之前那些没有默认值的形参添加默认实参,而且该形参右侧的所有形参必须都有默认值。
- 通常,应该在函数声明中指定默认实参,并将该声明放在合适的头文件中。
- 局部变量不能作为默认实参。
//除此之外,只要表达式的类型能转换成形参所需的类型,就能作为默认实参
//wd,def和ht的声明必须出现在函数之外
sz wd = 80;
char def = ' ';
sz ht();
string screen(sz = ht(), sz = wd, char = def);
string window = screen(); //调用screen(ht(),80,' ')
void f2()
{
def = '*'; //改变默认实参的值
sz wd = 100; //隐藏了那个外层定义的wd,但是没有改变默认值
window = screen(); //调用screen(ht(), 80, '*')
}
6.5.2 内联函数和constexpr函数
- 内联函数在每个调用点上“内联地”展开,可以避免函数调用的开销。
- 内联说明只是向编译器发出一个请求,编译器可以选择忽略这个请求。
- 一般来说,内联机制用于优化规模较小、流程直接、频繁调用的函数。
//比较两个string对象的长度,返回较短的那个引用
inline const string& shorterString(const string& s1, const string& s2){
return s1.size() <= s2.size() ? s1 : s2;
}
- constexpr函数:能用于常量表达式的函数。
- 定义constexpr函数:函数的返回类型及所有形参的类型都得是字面值类型,而且函数体中必须有且只有一条return语句。
- constexpr函数体内也可以包含其他语句,只要这些语句在运行时不执行任何操作就行。例如,constexpr函数中可以有空语句、类型别名以及using声明。
- constexpr函数不一定返回常量表达式。
- 把内联函数和constexpr函数放在头文件内。
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)不是常量表达式
6.5.3 调试帮助
- 调试帮助:只在开发过程中使用的代码,发布时屏蔽掉。
- assert宏定义在cassert头文件中,常用于检查“不能发生”的条件。assert宏使用一个表达式作为它的条件:assert(expr);首先对expr求值,如果表达式为假(即0),assert输出信息并终止程序的执行。如果表达式为真(即非0),assert什么也不做。
- assert的行为依赖于一个名为NDEBUG的预处理变量的状态。如果定义了NDEBUG,则assert什么也不做。
#define NDEBUG//关闭调试状态,必须在cassert头文件上面
#include <cassert>
- 定义NDEBUG能避免检查各种条件所需的运行时开销。
- 可以把assert当成调试程序的一种辅助手段,但是不能用它替代真正的运行时逻辑检查,也不能替代程序本身应该包含的错误检查。
- 使用NDEBUG编写自己的条件调试代码:
func:编译器定义的一个局部静态变量,用于存放函数的名字。
FILE:存放文件名的字符串字面值。
LINE:存放当前行号的整型字面值。
TIME:存放文件编译时间的字符串字面值。
DATE:存放文件编译日期的字符串字面值。
6.6 函数匹配
- 函数匹配:寻找最佳匹配、不能具有二义性。
- 函数匹配步骤:
a. 选定本次调用对应的重载函数集,集合中的函数称为候选函数。候选函数的两个特征:
与被调用的函数同名。
其声明在调用点可见。
b. 考察本次调用提供的实参,然后从候选函数中选出能被这组实参调用的函数,这些新选出的函数称为可行函数。可行函数的两个特征:
其形参数量与本次调用提供的实参数量相等。
每个实参的类型与对应的形参类型相同,或者能转换成形参的类型。
c. 从可行函数中选择与本次调用最匹配的函数。基本思想:实参类型与形参类型越接近,它们匹配得越好。 - 如果没找到可行函数,编译器将报告无匹配函数的错误。
- 含有多个形参的函数匹配规则:
该函数每个实参的匹配都不劣于其他可行函数需要的匹配。
至少有一个实参的匹配优于其他可行函数提供的匹配。 - 调用重载函数时应尽量避免强制类型转换。如果在实际应用中确实需要强制类型转换,则说明设计的形参集合不合理。
void f();
void f(int);
void f(int, int);
void f(double, double=3.14);
f(5.6); //调用 void f(double, double)
f(42,2.56); //错误,具有二义性
6.6.1 实参类型转换
- 为了确定最佳匹配,编译器将实参类型到形参类型的转换划分为几个等级,具体排序如下:
a. 精确匹配,包括以下情况:
实参类型和形参类型相同。
实参从数组类型或函数类型转换成对应的指针类型。
向实参添加顶层const或者从实参中删除顶层const。
b. 通过const转换实现的匹配。
c. 通过类型提升实现的匹配。
d. 通过算术类型转换或指针转换实现的匹配。
e. 通过类类型转换实现的匹配。 - 所有算术类型转换的级别都一样。例如从int向unsigned int的转换并不比从int向double的转换级别高。
void ff(int);
void ff(short);
ff('a'); //char 提升成int;调用f(int)
void manip(long);
void manip(float);
manip(3.14); //错误:二义性
- 如果重载函数的区别在于它们的引用类型的形参是否引用了const,或者指针类型的形参是否指向const,则当调用发生时编译器通过实参是否是常量来决定选择哪个函数。
Record lookup(Account&);
Record lookup(const Account&);
const Account a;
Account b;
lookup(a); //调用 lookup(const Account&);
lookup(b); //调用 lookup(Account&);
6.7 函数指针
- 把函数名作为一个值使用时,该函数自动地转换成指针。
- 在指向不同函数类型的指针间不存在转换规则。
bool lengthCompare(const string &,const string &);
//该函数的类型是bool (const string &,const string &)
//声明一个可以指向该类型函数的指针,只要用指针替换函数名即可
bool (*pf)(const string &,const string &); //括号不能少
pf = lengthCompare;
pf = &lengthCompare; //等价的赋值语句:取地址符是可选的
//可以直接使用指针调用该函数,无需提前解引用
bool b1 = pf("hello","goodbye");
bool b2 = (*pf)("hello","goodbye"); //等价的调用
bool b3 = lengthCompare("hello","goodbye");
string::size_type sumLength(const string&,const string&);
bool cstringCompare(const char*,const char*);
pf = 0; //正确:pf不指向任何函数
pf = sumLength; //错误:返回类型不匹配
pf = cstringCompare; //错误:形参类型不匹配
pf = lengthCompare; //正确:函数和指针的类型精确匹配
- 当使用重载函数时,上下文必须清晰地界定到底应该选用哪个函数。如果定义了指向重载函数的指针,编译器通过指针类型决定选用哪个函数,指针类型必须与重载函数中的某一个精确匹配。
void ff(int *);
void ff(unsigned int);
void (*pf1)(unsigned int) = ff; //pf1指向ff(unsigned)
void (*pf2)(int) = ff; //错误:没有任何一个ff与该形参列表匹配
double (*pf3)(int *) = ff; //错误:ff和pf3的返回类型不匹配
//第三个形参是函数类型,它会自动地转换成指向函数的指针
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);
//通过使用类型别名,简化使用函数指针
//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&,FuncP2);
- 不能返回函数,但可以返回指向函数的指针(和函数类型的形参不一样,返回类型不会自动地转换成指针,必须显式地将返回类型指定为指针)。
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); //正确:显示地指定返回类型是指向函数的指针
- 可以使用尾置返回类型的方式声明一个返回函数指针的函数。
int (*f1(int))(int*, int);
//使用尾置返回的方式
auto f1(int) -> int (*)(int*,int);
- 将decltype作用于某个函数时,它返回函数类型而非指针类型。因此,需要显式地加上*以表明我们需要返回指针,而非函数本身。
string::size_type sumLength(const string&, const string&);
decltype(sumLength) *getFnc(const string &);