第6章 函数
6.1 函数基础
函数的调用完成两项工作:一是用实参初始化函数对应的形参,二是将控制权转移给被调函数。
被调函数的return语句也完成两项工作:一是返回return语句中的值(如果有的话),二是将控制权从被调函数转移到主调函数。
定义空形参列表
void f1(){} //隐式的定义形参列表 void f2(void){} //显式的定义形参列表
函数的返回类型不可以是数组类型或函数类型,但可以是指向数组或函数的指针。
形参:函数定义时的参数
实参:函数调用时的参数,函数实际操作对象
局部对象
对于普通局部变量对应的对象来说,当函数的控制路径经过变量定义语句时创建该对象,当到达定义所在的块末尾时销毁它。这种对象叫自动对象
形参是一种自动对象。函数开始时为形参申请存储空间,因为形参定义在函数体作用域之内,所以一旦函数终止,形参也就被销毁。
在所有函数体之外的对象存在于程序的整个执行过程中。此类对象在程序启动时被创建,直到程序结束才会销毁。
局部静态对象在程序执行的路径第一次经过对象定义语句时初始化,并且直到程序终止才会被销毁,在此期间即使对象所在的函数结束执行也不会对它有影响。用static
修饰。
函数声明
函数只能定义一次,但是可以声明多次
如果一个函数永远不会被我们用到,那么可以只有声明没有定义
因为函数的声明不包含函数体,所以声明可以不加上形参的名字
6.2 参数传递
传值参数
即值拷贝,
值形参改变不会影响实参
指针形参改变会影响实参
当执行指针拷贝时,拷贝的是指针的值。
传引用参数
形参会修改实参
如果函数无需改变引用形参的值,最好将其声明为常量引用,加const
一个函数只能返回一个值
const形参和实参
当形参有顶层const时,实参初始化形参时会忽略掉顶层const,传给它常量对象或者非常量对象都是可以的
void func(const int i){} void func(int i){} //错误,重复定义
形参的初始化和变量的初始化是一样的
数组形参
int *matrix[10]; //10个指针构成的数组 int (*matrix)[10]; //指向含有10个整数的数组的指针
//三个等价 void print(const int*); void print(const int[]); void print(const int[10]);
数组的引用
void print(int (&arr)[10]){ for(auto elem : arr){ cout << elem << endl; } }
含有可变形参的函数
为了编写能处理不同数量实参的函数,C++11新标准提供了两种主要的方法:如果所有实参类型相同,可以传递一个名为initializer_list
的标准库类型;如果实参的类型不同,可以编写一种特殊的函数,也就是可变参数模板。
如果函数的形参数量未知但是类型都相同,可以使用initializer_list
,其定义在同名的头文件中
initializer<string> ls; //元素类型是string initializer<int> li; //元素类型是int
和vector不一样的是,initializer_list
对象中的元素永远是常量值,我们无法改变initializer_list
对象中的元素。
6.3 返回类型和return语句
return语句终止当前正在执行的函数并将控制权返回到调用该函数的地方。
return; return expression;
void函数不要求非得写return语句
不要返回局部对象的引用或指针;调用的函数终止局部变量会释放
引用返回左值
char &get_val(string &str, string::size_type ix){ return str[ix]; //假定索引值是有效的 } int main(){ string s("a value"); cout << s <<endl; // a value get_val(s,0) = 'A'; //s[0]的值改为A cout << s << endl; // A value return 0; }
shorterString("hi", "bye") = "X"; //错误:返回值是一个常量
列表初始化返回值
C++11新标准规定,函数可以返回花括号包围的值的列表。
主函数main可以没有返回值,编译器会隐式地插入一条返回0的return语句。
main函数不能调用自己
返回数组指针
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]; //p2是一个指针,指向含有10个整数的数组
尾置返回类型
C++11新标准
//func接受一个int类型的实参,返回一个指针,该指针指向含有10个整数的数组 auto func(int i) -> int(*)[10];
6.4 函数重载
如果同一作用域内的几个函数名字相同但形参列表不同,我们称之为重载函数
main函数不能重载
Record lookup(Phone); Record lookup(const Phone); //重复声明
Record lookup(Account&); //函数作用于Account的引用 Record lookup(const Account&); //新函数,作用于常量引用 Record lookup(Account*); //新函数,作用于指向Account的指针 Record lookup(const Account*); //新函数,作用于指向常量的指针
函数匹配,即重载确定,有三种情况
- 编译器找到一个与实参最佳匹配的函数,并生成调用该函数的代码
- 一个都找不到,编译器发出无匹配的错误
- 找到了好几个,但都不是明显的最佳选择,此时也将发生错误,称为二义性错误
重载与作用域
如果我们在内层作用域中声明名字,它将隐藏外层作用域中声明的同名实体。
6.5 特殊用途语言特性
默认实参
一旦某个形参被赋予了默认值,它后面的所有形参都必须有默认值
如果想使用默认实参,调用的时候省略该参数就可以
window = screen(, , '?'); //错误:只能省略尾部的实参
在设计含有默认实参的参数时,尽量让不怎么使用默认值的形参出现在前面,而让那些经常使用默认值的形参出现在后面。
默认实参声明
定义只能有一次,声明可以有多次
string screen(sz, sz, char=' '); string screen(sz, sz, char='*'); //错误:重复声明,试图修改一个已经存在的默认值 string screen(sz=24, sz=80, char=' '); //正确:添加默认实参
局部变量不能作为默认实参
表达式的类型能转换成形参所需的类型,该表达式就能作为默认实参
内联函数
封装成函数的优点很多,这里就不讲
函数调用的缺点,一次函数调用其实包含着一系列工作:调用前要先保存寄存器,并在返回时恢复;可能需要拷贝实参;程序转向一个新的位置继续执行。
内联函数可以避免函数调用的开销
将函数指定为内联函数,通常就是将它在每个调用点上“内联地”展开。
一般来说,内联机制用于优化规模较小、流程直接、频繁调用的函数
在编写函数时,在返回类型前面加上关键字inline
,就声明成了内联函数
其实很多编译器不支持内联函数,而且一个很长的函数怎么内联展开
constexpr函数
第2章讲的是constexpr变量,这里是函数
constexpr函数指能用于常量表达式的函数
定义constexpr函数,函数的返回值类型及所有形参的类型都得是字面值类型
constexpr int new_sz(){return 42;} //new_sz()是constexpr函数 constexpr int foo = new_sz(); //正确,foo是一个常量表达式
constexpr函数不一定返回常量表达式,那时候报错而已
内联函数和constexpr函数通常定义在头文件中
6.6 函数匹配
函数匹配有三个步骤:
- 确定重载函数集。候选函数两个特征:一是与被调用的函数同名,二是其声明在调用点可见
- 确定可行函数。可行函数有两个特征:一是其形参数量与本次调用提供的实参数量相等,二是每个实参的类型与对应的形参类型相同,或者能转换成形参的类型
- 确定最佳匹配
如果没找到可行函数,编译器将报告无匹配函数的错误
如果找不到最佳匹配。编译器最终因二义性拒绝调用请求
实参类型转换
void ff(int); void ff(short); ff('a'); //char提升成int,调用ff(int)
void manip(long); void manip(float); manip(3.14); //二义性 double存在两种算术类型转换
Record lookup(Account&); //函数的参数是Account的引用 Record lookup(const Account&); //函数的参数是一个常量引用 const Account a; Account b; lookup(a); //调用lookup(const Account&) lookup(b); //调用lookup(Account&)
6.7 函数指针
函数指针指向的是函数而非对象
//比较两个string对象的长度 bool lengthCompare(const string &, const string &);
要想声明一个可以指向该函数的指针,只需要用指针替换函数名即可
//pf指向一个函数,该函数返回值bool类型,两个参数 bool (*pf)(const string &, const string &); //未初始化
使用函数指针
pf = lengthCompare; //pf指向名为lengthCompare的函数 pf = &lengthCompare; //等价 &是可选的
调用函数
bool b1 = pf("hello", "goodbye"); bool b1 = (*pf)("hello", "goodbye"); //等价 bool b1 = lengthCompare("hello", "goodbye"); //等价
同样可以为函数指针赋一个nullptr或值为0的整型常量表达式,表示该指针不指向任何一个函数
函数指针形参
函数指针可以作为形参
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; //等价
void useBigger(const string&, const string&, Func); void useBigger(const string&, const string&, FuncP2); //等价
在第一条语句中,编译器自动地将Func表示的函数类型指针转换成指针
返回指向函数的指针
实例:编写两个int加减乘除,返回值为int,vector对象保存指向这些函数的指针,并输出打印每个函数
int add(int a, int b) { return a + b; } int sub(int a, int b) { return a - b; } int mul(int a, int b) { return a * b; } int main() { //typedef int(*p)(int a, int b); using p = int(*)(int, int); //等价 vector<p> v = { add,sub,mul }; for (auto f : v) { cout << f(2, 2) << endl; } system("pause"); return 0; }