第6章 函数
函数基础
局部静态对象(local static object)
在程序的执行路径第一次经过对象定义语句时初始化,并且直到程序终止才被销毁,在此期间即使对象所在的函数结束执行也不会对它有影响。
如果局部静态变量未被显式初始化,它将执行值初始化(内置类型的局部静态变量初始化为0)。
函数声明
函数声明也称作函数原型(function prototype)
函数声明没有函数体,用一个分号表示声明语句结束。
函数声明无须形参的名字(也可以写上形参的名字)。
int f(int n); //ok
int f(int); //ok
在头文件中进行函数声明
函数应该在头文件中声明,而在源文件中定义;
含有函数声明的头文件应该被包含到定义函数的源文件中。
参数传递
const形参和实参
当形参有顶层const时,传给它常量对象或非常量对象都是可以的。换句话说,形参的顶层const被忽略掉了。
void f(const int i) { /*f能读取i不能向i写值*/}
void f(int i) { /*可以向i写值*/}
以上两个函数定义似乎是有差异的,实际上若二者同时存在属于重复定义。因为顶层const被忽略掉了,传入这两个函数的参数可以完全一样。
补充:
int calc(char* a, char* b) { /**/ }
int calc(char* const a, char* const b) { /**/ }
以上函数属于重复定义,因为这里的const是顶层const,形参的顶层const被忽略了,两个函数的所有参数类型都是char*。
补充:
int calc(char* a, char* b) { /**/ }
int calc(const char* a, const char* b) { /**/ }
以上函数可以同时定义,因为这里的const是底层const。
以上函数在函数匹配时不会发生二义性调用,如果形参是指向常量的指针,调用后者;如果实参是指向非常量的指针,虽然调用二者均可行,编译器会调用前者以达到精确匹配。
扩展:
void f(...) const {...}
void f(...) {...}
它们被认为是两个不同的函数,因为此处的const是函数签名的一部分。
数组形参
回顾数组的两个性质:
不允许拷贝数组;
使用数组时(通常)会将其转换成指针。
尽管不能以值传递的方式传递整个数组,但是可以把形参写成类似数组的形式。
void print(const int*);
void print(const int[]);
void print(const in[10]); //这里的维度表示我们期望数组含有多少元素,实际不一定
以上三者完全等价,每个函数的唯一形参都是const int*类型。编译器处理对print函数的调用时,只检查传入的参数是否const int*类型。
例如:
int i = 0, a[2] = {0,1};
print(&i); //合法;&i的类型是int*
print(a); //合法;a转换成int*并指向a[0]
数组引用形参
形参也可以是数组的引用,引用形参绑定到对应的实参上,也就是绑定到数组上。
void print(int (&arr)[10]) {/**/}
但是,这一用法也无形中限制了print函数的可用性:我们只能将函数作用于大小为10的数组。
(通过函数模板,可以实现给引用类型的形参传递任意大小的数组,后面将会讨论)
PS:&arr两端的括号不能少
void print(int &arr[10]) //非法;试图将arr声明成“引用的数组”;但是不存在引用的数组,数组元素应该是对象
void print(int (&arr)[10]) //合法;arr是一个“含有10个整数的整型数组”的引用
传递多维数组
回顾:多维数组其实是数组的数组。将多维数组传递给函数,真正传递的是指向数组首元素的指针,而首元素本身就是一个数组,所以这个指针就是一个指向数组的指针。
数组第二维(以及后面的所有维度)的大小都是数组类型的一部分 ,不能省略。
void print(int (*matrix)[10], int rowSize) { /**/ }
//matrix是指向“含有10个整数的数组”的指针
PS:*matrix两端的括号必不可少
void print(int *matrix[10], int rowSize) { /**/ }
//合法(但不是我们所要的);matrix是“10个指向整数的指针”构成的数组
另一种等价定义方式(以二维数组为例):
void print(int matrix[][10], int rowSize) { /**/ }
//matrix的声明看起来是一个二维数组,实际上形参是一个指向“含有10个整数的数组”的指针(编译器会忽略掉第一个维度)。
main:处理命令行选项
int main(int argc, char *argv[]) { ... }
int main(int argc, char **argv) { ... } //上一条语句的等价写法;argv是一个指针,指向char*类型的对象
第二个形参是一个数组,它的元素是指向C风格字符串的指针;
第一个形参argc表示数组中字符串的数量;
当实参传给main函数之后,argv[0]指向程序的名字或一个空字符串(argv[0]的值由系统设定的,而非用户输入);
从argv[1]开始,保存用户输入的可选实参;
最后一个指针之后的元素值保证为0(这也是由系统设定的);
例如:
假定main函数位于可执行文件prog内,并且我们向程序传递了下面的选项:
prog -d -o ofile data0
则结果是:
argc = 5;
argv[0] = "prog";
argv[1] = "-d";
argv[2] = "-o";
argv[3] = "ofile";
argv[4] = "data0";
argv[5] = 0;
含有可变形参的函数
(1)initializer_list形参
适用情况:函数的实参数量未知,但是全部实参的类型都相同。
initializer_list是一种标准库类型,它定义在同名的头文件中;initializer_list是一种模板类型。
initializer_list对象中的元素永远是常量值。
如果要向initializer_list形参中传递一个值的序列,则必须把序列放在一对花括号内。
void f(initializer_list lst) { ... }
f({"string1", s2, s3}); //PS:s2和s3是string对象
initializer_list提供的操作:
initializer_list<T> lst; 默认初始化;T类型元素的空列表
initializer_list<T> lst{a,b,c...}; lst中的元素是对应初始值的副本,列表中的元素是const
lst2(lst) lst2 = lst 拷贝构造、拷贝赋值;(实际上,不会拷贝列表中的元素,拷贝后,原始列表和副本共享元素)
lst.size()
lst.begin()
lst.end()
(2)省略符形参
省略符形参仅仅用于C和C++通用的类型。特别要注意:大多数类类型的对象在传递给省略符形参时都无法正确拷贝。
省略符形参只能出现在形参列表的最后一个位置。
void f(parm_list, ...); //此处的逗号是可选的
void f(...);
省略符形参对应的实参无需类型检查。
(3)函数模板:可变参数模板
这里先不讨论。
返回类型和return语句
对于有返回值的函数,如果函数体内不包含return语句,有的编译器可能会检测到这个错误,有的也许不会;
如果编译器没有发现这个错误,则运行时的行为是未定义的。(返回值不可预知)
不要返回局部对象的引用或指针
函数完成后。它所占用的存储空间也随之被释放掉。函数终止意味着局部变量的引用或指针将指向不再有效的内存区域。
注意:返回局部静态对象的引用或指针是安全的。
列表初始化返回值
C++11允许函数返回花括号包围的值的列表。
vector<string> f(string s)
{
if(s.empty())
return {}; //合法;返回一个空vector对象
else
return {"string1", "string2"}; //合法;返回列表初始化的vector对象
}
注意:如果函数是基本内置类型,也同样允许使用列表初始化返回值;但是,花括号包围的列表最多包含一个值,而且所占空间不应大于目标类型的空间。
int f() { return {} }; //合法;返回0
int f() { return {1} }; //合法
int f() { return {1.1} }; //非法;返回1;编译器给出warning(但程序还是能运行,并执行了隐式类型转换)
返回数组指针
(1)使用类型别名简化返回数组的指针或引用:
typedef int arrT[10]; //arrT是一个类型别名,它表示的类型是“含有10个整数的数组”
using arrT = int[10]; //上一条语句的等价写法
arrT* func(int i); //func返回一个“指向含有10个整数的数组”的指针
(2)如果不使用类型别名,则返回数组指针的函数形式如下:
Type (*function(parameter_list)) [dimension]
注:(*function(parameter_list))两端的括号必须存在,否则,函数的返回类型将是“元素是指针的数组”。
以具体例子来理解该声明的含义:
int (*func(int i)) [10];
可按以下顺序来理解:
func(int i) 表示调用函数时需要一个int类型的实参;
(*func(int i)) 意味着我们可以对函数的调用结果执行解引用操作(或者说:意味着函数的调用结果是一个某种类型的指针);
(*func(int i)) [10] 表示解引用func的调用将得到一个大小是10的数组;
int (*func(int i)) [10] 表示数组中的元素是int类型
(3)使用尾置返回类型
C++支持尾置返回类型(trailing return type)。
任何函数都能使用尾置返回,但是尾置返回对于较复杂的函数返回类型最有效。
上例子:
auto func(int i) -> int (*) [10]; //func接受一个int类型的实参,返回一个指针,该指针指向含有10个整数的数组。
注:尾置返回类型跟在形参列表后,并以一个->符号开头;而在本应出现返回类型的地方放置一个auto。
(4)使用decltype
当我们知道函数返回的指针将指向哪个数组,就可以使用decltype关键字声明返回类型。
例如,下面的函数返回一个指针,该指针根据参数i的不同指向两个已知数组中的某一个。
int odd[] = {1,3,5};
int even[] = {2,4,6};
decltype(odd) *arrPtr(int i)
{
return (i%2) ? &odd : &even; //返回一个指向数组的指针
}
arrPtr返回一个“指向含有3个整数的数组”的指针。
注意:之前讨论过,decltype并不会把数组类型转换成对应的指针,所以decltype(odd)的结果表示的是数组;要想表示arrPtr返回的是指针,则还必须在函数声明时加一个*。
函数重载
函数重载:同一作用域内;函数名相同;形参列表不同。
不允许两个函数除了返回类型外其它所有要素都相同。
重载和const形参
顶层const不影响传入函数的对象;一个拥有顶层const的形参无法和另一个没有顶层const的形参区分开来。
Record lookup(Phone);
Record lookup(const Phone); //重复定义
如果形参是某种类型的指针或引用,则通过区分其指向的是常量对象还是非常量对象,可以实现函数重载;此时const是底层的。
Record lookup(Account&); //函数作用于Account的引用
Record lookup(const Account&); //新函数;作用于常量引用
Record lookup(Account*); //新函数;作用于指向Account的指针
Record lookup(const Account*); //新函数;作用于指向常量的指针
因为const不能转换成非常量,所以只能把const对象(或指向const的指针)传递给const形参;
因为非常量可以转换成const,所以以上4个函数都能作用于非常量对象(或指向非常量对象的指针)。(不过,编译器会优先选择非常量版本的函数)
特殊用途语言特性
默认实参
一旦某个形参被赋予了默认值,它后面的所有形参都必须指定默认值。
默认实参负责填补函数调用缺少的尾部实参。
用作默认实参的名字在函数声明所在的作用域内解析,而这些名字的求值过程发生在函数调用时。
1 int i = 10; //A 2 char c = '.'; //B 3 4 void f(int sz = i, char ch = c) 5 { 6 cout<< sz << " " << ch <<endl; 7 } 8 9 int main() 10 { 11 c = '*'; //C 12 int i = 20; //D 13 f(); //使用默认实参 14 return 0; 15 }
输出结果是: 10 *
注解:调用f时,使用的i是函数声明所在的作用域内的那个i(A处);使用的c的值是函数调用时c的最新值(B处的变量c,C处的最新值'*')。
虽然我们意图声明一个局部变量i(D处)用于隐藏外层的i(A处),但是该局部变量与传递给f的默认实参没有任何联系。
内联函数和constexpr函数
内联函数:函数将在每个调用点上“内联地”展开,从而避免了函数调用的开销。(函数调用开销:包括保存和恢复寄存器、拷贝实参等。)
注:在函数的返回类型前加上inline关键字,以建议编译器将其做成内联函数,编译器可以忽略这个请求。
constexpr函数:能用于常量表达式的函数。
定义constexpr函数的约定:函数的返回类型及所有形参的类型都得是字面值类型(参见第2章Tips);函数体中有且仅有一条return语句。
constexpr函数体内也可以包含其它语句,但这些语句应该在运行时不执行任何操作。(例如:空语句、类型别名、using声明等)
constexpr函数并不一定返回常量表达式(我们允许其返回值并非一个常量):
注解:对func(arg),当实参是常量表达式时,它的返回结果也是常量表达式,反之不然。
constexpr int f(int cnt) { return f2() * cnt; }
f(10); //10是字面值,是常量表达式,故返回值也是常量表达式
int i = 10;
f(i); //i是一个非常量表达式,则返回值是一个非常量表达式
PS:编译器会把对constexpr函数的调用替换成其结果值,为了能在编译过程中随时展开,constexpr函数被隐式地指定为内联函数。
PS:与其它函数不同,内联函数和constexpr函数可在程序中多次定义;不过,对某个给定的内联函数或constexpr函数来说,它的多个定义必须完全一致。
基于此原因,内联函数和constexpr函数通常定义在头文件中。
预处理时的调试:assert和NDEBUG
assert:是一种预处理宏,定义在cassert头文件中。
assert(expr);
assert以一个表达式作为它的条件,如果表达式为假,assert输出信息并终止程序,如果表达式为真,assert什么也不做。
NDEBUG预处理变量:assert的行为依赖于一个名为NDEBUG的预处理变量的状态。如果定义了NDEBUG,则assert什么也不做。
默认情况下没有定义NDEBUG,此时assert将执行运行时检查。(#define NEBUG则关闭调试状态)
函数指针
函数指针指向的是函数,而非对象。
函数指针指向某种特定类型。
函数的类型由它的返回类型和形参类型共同决定,而与函数名无关。
例如函数:
bool compare(const string &, const string &);
该函数的类型是:
bool (const string &, const string &)
要想声明一个指向该函数的指针,只需要用指针替换函数名:
bool (*pf)(const string &, const string &); //注意:指针未初始化
//pf指向一个函数,该函数的参数是两个const string的引用,该函数的返回值是bool类型
可以这样理解指针pf的声明语句:
pf前面有个*,说明pf是指针;
右侧是形参列表(参数是两个const string的引用),表示pf指向的是函数;
再观察左侧,发现函数的返回类型是bool;
综上,pf是一个指向函数的指针,该函数的参数是两个const string的引用,该函数数的返回值是bool类型。
PS:*pf两端的括号必不可少。假若不写这对括号:
bool *pf(const string &, const string &); //声明一个名为pf的函数,该函数返回bool*
使用函数指针
当我们把函数名作为一个值使用时,该函数自动地转换成指针。
pf = compare; //pf指向名为compare的函数
pf = &compare; //与上一条语句等价:取地址符是可选的
扩展:对数组名,没有取地址符的写法。
例如:
int a[] = {1,2};
int *p = a; //ok
int *p = &a; //非法
int *p = &a[0]; //ok
允许直接使用指向函数的指针调用该函数,无须解引用指针。
bool b1 = pf("hi", "bye"); //ok
bool b2 = (*pf)("hi", "bye"); //ok
bool b3 = compare("hi", "bye"); //ok
//以上三者等价
指向不同函数类型的指针不存在转换规则。
int sumLength(const string &, const string &);
bool cstringCompare(const char*, const char*);
pf = sumLength; //错误;返回类型不匹配
pf = cstringCompare; //错误;形参类型不匹配
pf = compare; //ok
pf = 0; //ok;表示指针没有指向任何一个函数
函数指针形参
和数组类似,虽然不能定义函数类型的形参,但是形参可以是指向函数的指针。此时,形参看起来是函数类型,实际上却是当成指针使用。
void useBigger(const string &s1, const string &s2, bool pf(const string &, cosnt string &));
//第三个形参是函数类型,它会自动地转换成指向函数的指针
void useBigger(const string &s1, const string &s2, bool (*pf)(const string &, cosnt string &));
//等价的声明;显式地将形参定义成指向函数的指针
可以直接把函数作为形参使用,此时它会自动转换成指针:
useBigger(s1, s2, compare);
使用类型别名和decltype简化使用了函数指针的代码
//Func和Func2是某种函数类型
typedef bool Func(const string &, const string &);
typedef decltype(compare) Func2; //等价的类型
//FuncP和FuncP2是指向函数的指针
typedef bool (*FuncP)(const string &, const string &);
typedef decltype(compare) *FuncP2; //等价的类型
注:与decltype使用数组类似,decltype使用函数也不会将函数转换成指针。因为decltype返回函数类型,所以再加上一个*才能得到指向函数的指针。
此时前述useBigger可声明如下:
void useBigger(const string &s1, const string &s2, Func); //或Func2;编译器会自动地将Func表示的函数类型转换成指针
void useBigger(const string &s1, const string &s2, FuncP); //或FuncP2
返回指向函数的指针
和数组类似,虽然不能返回一个函数,但是可以返回指向函数类型的指针。
(1)使用类型别名:
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); //正确;显式地指定返回类型是指向函数的指针
(2)使用一般的方法直接声明:
int (*f1(int)) (int*, int);
注解:由内向外,看到f1有形参列表,所以f1是个函数;
f1前面有*,所以f1返回一个指针;
观察右边发现,指针的类型本身也包含形参列表,因此指针指向函数,再看最左边,知道该函数的返回类型是int。
综上,f1是一个形参类型为一个int,返回类型为“指向int(int*, int)类型的函数的指针”的函数。
或者说,f1是一个形参类型为一个int,返回类型为“int (*) (int*, int)”的函数。
(3)使用尾置返回类型:
auto f1(int) -> int (*) (int*, int);