第6章 函数
函数的定义与声明(如何传入参数 以及 如何返回结果)
重载函数&函数匹配
函数指针
函数是一个命了名的代码块
6.1 函数基础
函数组成:返回值类型 函数名 形参列表 函数体
调用运算符:形式是一对圆括号
函数调用:一是实参初始化形参,二是控制权转移给被调用函数
return语句:一是返回return语句中的值,二是将控制权归还给主调函数
规定实参数量应与形参数量一致,形参一定会被初始化
实参的类型必须与对应的形参类型匹配,不一定非要完全一致,能够类型转换也可以
函数的形参列表可以为空,但不能省略。
定义一个不带形参的函数,可以书写一个空的形参列表,也可以使用关键字void表示函数没有形参(与C语言兼容)
任意两个形参都不能同名,而且函数最外层作用域的局部变量也不能与函数形参同名
形参名是可选的,是否为形参命名取决于是否在函数体内使用该形参。不管形参命名与否,都需要为其提供实参,即形参一定会被初始化
函数的返回值类型不能是数组类型或者函数类型,但可以是指向数组或者函数的指针
6.1.1 局部对象
c++中,名字有作用域,变量有生命周期
形参和函数体内部定义的变量统称为局部变量
局部变量的生命周期依赖于定义的方式:自动对象、局部静态对象。自动对象始于变量定义语句,终于块末尾;局部静态变量始于变量定义语句,终于程序终止。
内置类型的未初始化的局部变量会产生未定义的值(默认初始化),内置类型的未初始化的局部静态变量初始化为0(值初始化)。
补充:值初始化:如果是内置类型,初始化为0;如果是类类型,执行类默认初始化
6.1.2 函数声明
函数只能定义一次,但可以声明多次。
函数声明与函数定义的区别:函数声明无需函数体,在后面加分号即可。因为声明中无函数体,所以无需写形参的名字,在函数声明中经常省略形参的名字,但加上形参名有助于更好的理解函数的功能。
函数声明又名函数原型,函数三要素(返回值类型、函数名、形参列表)描述了函数的接口,说明该函数所需的全部信息。
变量在头文件中声明,在源文件中定义;
函数在头文件中声明,在源文件中定义。
定义函数的源文件应该把含有函数声明的头文件包含进来,编译器负责验证函数声明和函数定义是否匹配
回顾:
①为了支持分离式编译,在多文件中共享代码,c++将声明与定义区分开。声明使得名字为程序所知,定义负责创建与名字关联的实体。定义只能有一次,但声明可以有多次。
②变量声明与变量定义的区别:变量声明规定了变量的类型与名字。定义除了规定了变量的类型与名字,还为变量申请了内存空间,可能会为变量赋初值。
如果想要声明一个变量而非定义它,可以在前面加extern,不要显式初始化。
6.1.3 分离式编译
分离式编译:允许将程序分割到几个文件中去,每个文件独立编译。只需要重新编译修改了的源文件。
经过分离式编译,把源文件变为对象代码,编译器把对象文件链接在一起形成可执行文件。
6.2 参数传递
每次调用函数都会重新创建它的形参,并用传入的实参对形参进行初始化
形参的类型决定了形参与实参的交互方式:
①形参是引用类型,形参绑定到对应的实参上; ——引用传递
②形参不是引用类型,将实参的值拷贝后赋值给形参。 ——值传递
6.2.1 传值参数
函数对形参做的所有操作不会影响实参。
指针形参:执行指针拷贝操作,两个指针是不同的指针,指向同一个对象。因为指针可以间接访问它所指的对象,所以通过指针可以修改它所指对象的值。
c++中建议用引用类型的形参代替指针。
6.2.2 传引用参数
使用引用形参, 允许函数改变一个或多个实参的值。
使用引用避免拷贝,也支持某些不支持拷贝的类类型的参数传递。
如果无需修改引用形参的值,最好将其设置为对常量的引用
使用引用形参返回额外信息,给函数传入额外的引用实参,使函数实际返回的值不只一次
6.2.3 const形参和实参
当实参初始化形参时会忽略掉顶层const,当形参是顶层const时,传给它常量或者非常量对象都可以。
void fcn(const int i); //fcn能够读取i,但是不能向i写值
void fcn(int i); //由于上一行函数忽略了顶层const,所以两个函数传入的fcn函数参数可以完全一样,因此第二个fcn是错误的
指针或引用形参与const
可以使用一个非常量初始化一个底层const对象,不能用一个底层const初始化一个非常量
回顾:
①关于引用:
引用必须被初始化
大多数情况下, 引用类型要与与之绑定的对象类型严格匹配
引用只能绑定在对象上,不能绑定在字面值或者表达式的计算结果上,所以引用类型的初始值必须是一个对象,但是初始化常量引用时允许以任何值作为初始值。
把函数不会改变的形参定义成普通引用是错误的,同时使用普通引用也会极大地限制函数所能接受的实参类型。
6.2.4 数组形参
数组的特殊性质:不允许拷贝数组;使用数组时通常会转换成指针
因为不能拷贝数组,所以不能以值传递的形式使用数组参数。
因为数组会被转换成指针,当为函数传递一个数组时,实际传递的是指向数组首元素的指针
void print(const int*);
void print(const int[]);
void print(const int[10]);
以上三种形式等价,并且数组的大小对函数的调用没有影响
由于数组是以指针的形式传递的,所以函数并不知道数组的尺寸。可以显式传递一个表示数组大小的形参:
void print(const int[],size_t size);
数组引用形参:
void print(int (&arr)[10]);
&arr两端括号必不可少,如果没有括号,arr是包含10个整型引用的数组
传递多维数组:本质传递指向数组首元素的指针,多维数组首元素也是一个数组。数组第二维的大小(以及后面所有维)都是数组类型的一部分,不能省略。
void print(int (*matrix)[10],int rowSize); //matrix指向含有10个整数的数组的指针
void print(int matrix[][10],int rowSize); //等价形式,以数组的语法传入参数
同样的,*matrix两端的括号不能少,如果去掉,matrix是包含10个指向整型的指针的数组
以数组的语法传入参数,由于转换成指向首元素的指针,所以只关心首元素的类型,不关心数组中元素的个数,即会忽略数组的第一个维度
6.2.5 main:处理命令行选项
情景:需要给main函数传递实参
命令行通过两个形参传递给main函数:
int main(int argc,char * argv[ ]) {...}
argv是一个数组,数组的每一个元素是指针,指针的类型是指向c风格字符串的指针,指针指向的字符串长度并不一定相等;argc表示数组中字符串的数量
由于
void print(const int*);
void print(const int[]);
等价,所以
int main(int argc,char* argv[]);
int main(int argc,char** argv);
与等价
6.2.6 含有可变形参的函数
如果所有实参的类型相同,数量不固定,可以传递一个名为initializer_list的标准库类型。initializer_list也是模板类型,其所有元素永远是常量值。使用列表初始化传值给initializer_list。含有initializer_list的函数也可以同时拥有其他形参。
省略符形参:传递可变数量的实参,只用于与c函数交互的接口程序,仅用于c和c++的通用类型,类对象不要通过省略符形参传递,只出现在形参列表的最后一个位置
void foo(param list,...); //形参声明后的逗号可省略,指定了类型的形参执行正常的类型检查,省略符形参对应的实参无须类型检查
void foo(...);
6.3 返回类型和return语句
6.3.1 无返回值函数
6.3.2 有返回值函数
6.3.3 返回数组指针
数组不能被拷贝,所以数组不能作为实参传入,也不能作为返回值传出。但是数组可以被转化成指向首元素的指针,所以函数可以把数组的指针或者引用作为实参传入,作为返回值传出。
定义一个返回数组的指针或者引用的函数比较麻烦,可以借助类型别名
typedef int arrT[10]; //arrT是含有10个整数的数组
using arrT=int[10]; //等价形式,arrT是含有10个整数的数组
arrT * func(int i); //func返回一个指向含有10个整数的数组的指针
不借助类型别名,直接声明一个返回数组指针的函数:
Type (* function(parameter_list))[dimension]
还可以使用尾置返回类型(c++11新特性)
将返回值类型跟在形参列表后面并以->开头,在本应该出现返回值类型的地方放置auto
auto func(int i)->int (*)[10]
如果知道函数返回的指针将指向哪个数组,可以使用decltype关键字。decltype不负责把数组类型转换成对应的指针,所以decltype(数组名)的结果是个数组,还需要在函数名前加*
int odd[]={1,2,3,4,5};
int even[]={2,3,4,5,6};
decltype(odd) *arrayPtr(int i)
{
return (i%2)? &odd:&even;
}
6.4 函数重载
重载函数:函数名称相同,但形参列表不同,对返回值不做要求
回归:重写有什么要求吗?函数名相同,形参列表相同,返回值类型相同
main函数不能重载
由重载函数的要求,可以推断不允许两个函数除了返回类型外其他所有的要素都相同。
(反证:如果存在仅返回值类型不同的函数,那么编译器在决定调用哪个函数时根本无法做出判断)
顶层const不影响参数传递(本质是顶层const不影响拷贝),形参有没有顶层const不影响。
void func(int i);
void func(const int i); //以上两种形式无法区分开
底层const会影响参数的传递,形参是否有底层const是有区别的。
void func(int *i);
void func(const int *i); //以上两种形式不同
补充:允许将指向非常量的指针或引用转换成指向常量的指针或引用
不允许将指向常量的指针或引用转换成指向非常量的指针或引用,不能试图删除底层const
const_cast在重载函数的情景中最有用
//比较两个string对象的长度,返回较短的那个引用
const string & shorterString(const string &s1,const string &s2)
{
return s1.size() <=s2.size() ?s1:s2;
}
如果对两个非常量的string实参调用此函数,希望返回值是一个指向非常量的引用,但调用此函数结果仍是指向常量的引用,该如何改进呢?
const string & shorterString(string &s1,string &s2)
{
auto &r=shorterString(const_cast<const string &>(s1),const_cast<const string &>(s2));
return const_cast<const string &>(r);
}
上面的函数先将实参强制转换成了对const的引用,然后调用了shorterString的const版本,const版本返回对常量的引用,再通过强制转换变成指向非常量的string。
误区:之前以为const_cast函数只能去掉底层const。
正确:const_cast既可以为指向非常量的指针或引用添加底层const,也可以去掉底层const
调用重载函数时可能有三种结果:
①找到与实参最佳匹配的函数,生成调用该函数的代码
②找不到任何一个函数与调用的实参匹配,编译器发出无匹配的错误信息
③有多于一个函数可以匹配,但每一个都不是最佳选择,会发生二义性调用错误
6.4.1 重载与作用域
如果我们在内层作用域中声明名字,它将隐藏外层作用域中声明的同名实体。
在不同作用域无法实现函数重载。
string read;
void print(const string &);
void print(double); //重载print函数
void fooBar(int val)
{
bool read=false; //新作用域:隐藏了外层的read
string s=read(); //错误,read是布尔值,而非函数
void print(int); //新作用域:隐藏了之前的print
print("value"); //错误:print(const string &)被隐藏掉了
print(ival); //正确:当前print(int)可见
print(3.14); //正确:调用print(int); print(double)被隐藏掉了
}
6.5 特殊用途语言特性
6.5.1 默认实参
一旦某个形参被赋予了默认值,它后面的所有形参都必须有默认值。
默认实参负责填补函数调用缺少的尾部实参(靠右侧位置)
6.5.3 调试帮助
功能:类似头文件保护机制,有选择地执行调试代码
assert预处理宏:
形式:assert(expr);
对expr求值,如果表达式为false,assert输出错误信息并终止程序;如果是true,什么也不做
一般expr是大多数情况都符合的条件
assert在头文件cassert中,把assert当做关键字,不要定义同名实体
NDEBUG预处理变量:assert的行为依赖NDEBUG的状态。如果用#define定义了NDEBUG,表明关闭调试状态,则assert什么也不做;如果没有定义NDEBUG,表示在调试状态,assert将执行运行时检查
除了用于assert,NDEBUG也可以编写自己的条件调试代码。如果NDEBUG未定义,将执行#ifndef和#endif之间的代码,如果定义了,这些代码会被忽略掉
#ifndef NDEBUG
//__func__是c++编译器定义的局部静态变量,用于存放函数名
cerr<<__func__<<":array size is"<<size<<endl;
#endif
预处理器定义了另4个名字:
//__FILE__ 存放文件名的字符串字面值
//__LINE__存放当前行号的整型字面值
//__TIME__存放文件编译时间的字符串字面值
//__DATE__存放文件编译日期的字符串字面值
#ifndef NDEBUG
cerr<<“Error:”<<__FILE__
<<"in function:"<<__func__
<<"at line"<<__LINE__
<< Compiled on:<<__DATE__
<<"at"<<__TIME__<<endl;
#endif
6.6 函数匹配
6.7 函数指针
指向函数的指针
用处:当需要函数作为形参或者返回值时。和数组一样,函数不能直接作为形参或返回值,只能用指向函数的指针作为形参或者返回值。
指向函数的指针的类型取决于函数的返回值和形参列表,声明指向函数的指针时用指针代替函数名
bool lengthCompare(const string &,const string &); //函数
bool (*ptr)(const string &,const string &); //指向函数的指针,括号不能省
给函数指针赋值:
ptr=lengthCompare; //直接把函数名赋值给指向函数的指针
//加上取地址符也可以
ptr=&lengthCompare;
使用指向函数的指针调用该函数:
bool b1=ptr("hello","goodbye"); //直接使用指向函数的指针调用函数,需要传入形参
bool b2=(*ptr)("hello","goodbye"); //也可以将指向函数的指针解引用,等价形式的调用
bool b3=lengthCompare("hello","goodbye"); //以上两种形式和使用函数名调用是等价的
和指向变量的指针一样:
①指向不同函数类型的指针不存在转换规则(只要返回值类型和形参列表不同,就不能进行相互转换)
②指向函数的指针可以被赋值为nullptr
函数指针作为形参
实参和形参都可以直接是函数类型,会被自动转换成指向函数的指针
void useBigger(const string &s1,const string &s2,bool func((const string &,const string &)); //直接把函数作为形参,其会被自动转化成指向函数的指针
void useBigger(const string &s1,const string &s2,bool (*ptr)((const string &,const string &)); //直接传入指向函数的指针作为形参
//调用函数时,可以直接把函数名作为实参传递,其会被自动转换成指向函数的指针
useBigger(s1,s2,lengthCompare);
借助类型别名简化函数指针的书写
bool lengthCompare(const string &s1,const string &s2);
typedef bool Func(const string &s1,const string &s2); //Func是函数类型
typedef decltype(lengthCompare) Func2; //Func2是函数类型,decltype作用于某个函数时,返回函数类型而不是指针类型
typedef bool (*Funpc)(const string &s1,const string &s2); //Funpc是指向函数的指针类型
typedef decltype(lengthCompare) *Funcp2; //Funcp2是指向函数的指针类型
函数指针作为返回值
和形参不同,编译器会把函数转换成指向函数的指针,编译器不会自动将函数返回类型当成对应指针类型处理
,必须把返回值类型写成指针类型。
使用类型别名:
using F=int (int*,int); //F是函数类型
using Fp=int* (int*,int); //Fp是指针类型
//必须显式地将返回类型定义成指针
Fp f1(int);
F *f1(int);
//尾置返回值类型的方法
auto f1(int)->int* (int*,int);