- 函数是一个命名了的代码块,通过调用函数来执行相应代码。
- 函数可有0个或多个参数,可能产生0个或1个结果。
- 函数可以重载,即一个名字对应多个不同函数。
函数基础
- 实参是形参的初始值,它们一一对应,实参与形参类型需匹配或能转换。但调用运算符并未规定实参的求值顺序。
- 为与C兼容,可用void关键字表示函数没有形参。
- 每个形参是含有一个声名符的声明。但即使两个形参类型一样,也必须都写出类型,不可省略。
- 函数最外层作用域的局部变量不能与形参同名。
- 形参名是可选的,不会被用到的形参不需命名,但需提供实参。
- 函数返回类型不可以是数组或函数类型,但可以是指向数组或函数的指针。
局部对象
- 自动对象:只存在于块执行期间的对象
- 形参是一种自动对象,函数开始时创建,函数终止时销毁
- 若局部变量对应的自动对象定义时有初始值,则用初始值初始化,若无初始值则默认初始化。即,内置类型的未初始化局部变量产生未定义的值。
- 局部静态对象:将局部变量定义成static类型,在程序执行第一次经过定义语句时初始化,直到程序终止才被销毁。
- 若局部静态对象没有显式初始值,将执行值初始化。即,内置类型的局部静态变量被初始化为0
函数声明
- 函数的名字必须在使用前声明。函数只能定义一次,但可声明多次。如果一个函数不会被使用,则可只声明不定义。
- 函数声明又叫函数原型,函数声明的三要素(返回类型、函数名、形参类型)描述了函数的接口。
- 函数声明不需函数体,用分号代替函数体。由于不使用形参,故声明中形参可不命名。
- 建议函数在头文件中声明,在源文件中定义。和变量一样。
分离编译
- C++支持分离式编译,即允许将程序分割到几个文件中,每个文件单独编译。
- 每个文件单独编译时产生后缀名为
.obj(windows)
或.o(UNIX)
的文件,含义是该文件中包含对象代码 - 单独编译后
链接器
将对象文件链接起来成为可执行文件
参数传递
- 每次调用函数都会重新创建形参并用实参初始化
- 如果形参是引用类型,将被绑定到对应的实参,否则将实参的值拷贝给形参。
- 两种传参:
引用传递
:形参是引用类型,引用形参绑定到对应实参的对象,是实参的别名。说这样的实参被引用传递,或者说函数被传引用调用。值传递
:实参的值被拷贝给形参,形参和实参是两个独立的对象。说这样的实参被值传递,或者说函数被传值调用
- C程序员常用指针类型形参修改函数外的对象,但在C++中,建议用引用类型形参代替指针
引用参数传递
- 使用引用传参的情形:
- 拷贝大的类类型对象或容器对象效率较低,最好用引用形参访问
- 有的类型不支持拷贝(如IO类),只能用引用传参访问
- 引用传参为函数
一次返回多个结果
提供了途径,只需将多个对象作为引用传进函数即可修改。 - 无需修改引用形参时,最好用
常量引用
数组形参
- 由于不能拷贝数组,故
不能以值传递方式
传入数组 - 由于数组会被转为指针,故向函数传入数组时,实际上是
传指针
。(所以函数内不能用sizeof
等得到数组大小) - 形参数组的大小对函数没有影响,函数也
不知道
实参数组的尺寸
//以下3个函数等价,形参类型都是const int *
void print(const int *);
void print(const int []);
void print(const int [10]); //数组大小传入时被丢弃
复制代码
- 3种传入数组时界定范围的方法:
- 要求数组本身有结束标记,如C风格字符串
- 传入一对迭代器(首指针和尾后指针),可由begin和end函数得到,类似标准库操作
- 定义一个表示数组大小的形参
void print(const char *cp){} //C风格字符串
void print(const int *beg, const int *end){} //一对迭代器
void print(const int ia[], size_t size){} //传入大小
复制代码
- 若函数不需要写数组元素,则数组形参应是指向const的指针
- 形参可以是
数组的引用
,此时引用形参绑定到对应数组,此时避免数组被转为指针,给实参时大小也需与形参大小相等。 - 例子:形参是数组的引用,数组不转指针
void print(int (&arr)[10]){ //形参是数组的引用,数组不转指针,实参大小也必须符合
for(auto elem:arr)
cout<<elem<<endl;
}
复制代码
- 传递多维数组时,多维数组的首元素本身就是数组,因此首元素指针就是指向数组的指针。传入函数时,首元素指针指向的对象仍是数组,因此数组的第二维以及其后所有维度的大小都是数组类型的一部分,不能省略。
返回类型和return语句
- return语句终止当前正在执行的函数,并将控制权返回到调用该函数的地方
有返回值与无返回值函数
- 只要函数返回类型不是void,则函数内的每个return都必须返回值。return返回的类型必须与函数返回类型相同,或能隐式转换为函数返回类型
- 有返回值的函数必须通过显式的return返回,最后不存在隐式的return。未通过显式return返回是未定义行为。
- 函数返回值的方式和初始化变量、形参一样:返回的值用于初始化调用点的一个临时量,该临时量是调用表达式的结果
- 若返回值非引用,则被拷贝到调用点。若返回值是引用,则它仅是它所引对象的别名。
不可返回局部对象的引用或指针
:函数终止意味着局部变量的引用将指向不再有效的内存区域。- 调用运算符
()
的优先级与点运算符.
和箭头运算符->
相同,且满足左结合律。因此,如果函数返回类类型对象或其引用,就可在调用表达式后直接取成员
返回数组指针
- 数组不能拷贝,故函数不能返回数组,但可返回数组的指针或引用
- 若不使用类型别名,就必须记得将数组维度带上。要定义一个返回数组指针的函数,则返回数组的维度必须放在形参列表之后。形如:
type (*function(parameter_list))[dimension]
。读法:function是形参列表为parameter_list的函数,它返回一个指针,该指针指向大小为dimension的数组,元素类型是type - 尾置返回类型:为简化复杂返回类型的定义,任何函数的定义都可用尾置返回。尾置返回类型放在形参列表后,并以->开头,代替原来返回类型的地方使用auto。尾置返回常用于返回数组指针或引用的函数
- 如果知道函数返回的指针指向哪个数组,就可用
decltype
声明返回类型。decltype作用于数组时不会转指针,故指针符号需手动加上
typedef int arrT[10]; //arrT等价于长度为10的整型数组,不会转指针
using arrT=int[10]; //等价于上一行,arrT等价于长度为10的整型数组,不会转指针
arrT *func(int i); //函数返回指向arrT的指针
int odd[]={1,3,5,7,9};
int even[]={0,2,4,6,8};
decltype(odd) *arrPtr(int i){ //推导返回类型为数组指针
return (i%2)?(&odd):(&even);
}
复制代码
函数重载
- 重载函数:同一作用域内几个函数名字相同但形参列表不同。即应该在形参的数量或类型上有所区分(仅返回类型不同不叫重载)。
- 调用重载函数时,编译器根据实参类型推断出想要的是哪个函数
- main函数不可重载
- 顶层const不影响传入函数的对象,
顶层const的形参无法和顶层非const的形参区分,因此顶层const不可重载
- 若形参是指针或引用,则通过区分指向对象是否是const可实现重载,即
底层const可以重载
Record lookup(Phone);
Record lookup(const Phone); //顶层const,不可重载,重复声明
Record lookup(Phone *);
Record lookup(Phone * const); //顶层const,不可重载,重复声明
Record lookup(Account &);
Record lookup(const Account &); //底层const,可以重载
Record lookup(Account *);
Record lookup(const Account *); //底层const,可以重载
复制代码
- 编译器可通过实参底层是否是const判断该调用哪个函数。
- const对象/指向const的指针只能传递给底层const形参
- 非const对象/指向非const的指针可以传递给底层const形参或底层非const形参,但编译器优先选择底层非const版本
- const_cast在重载函数的情形中很有用,它可以修改指针或引用的底层const权限。若一个引用/指针指向了非常量对象,而该引用/指针却被声明为常量引用/指向常量的指针,则不可通过此引用/指针来修改对象,除非用const_cast将此引用/指针的底层const资格去掉。
- const_cast只可在底层本来为非常量对象时,才能去掉引用/指针的底层const
- 例子:const_cast修改引用/指针的底层const
//返回两字符串的较短者,输入输出都是常量引用
const string &shorterString(const string &s1, const string &s2){
return (s1.size()<=s2.size()>)?(s1):(s2);
}
//上一个函数的重载,输入输出都是非常量引用的版本
string &shorterString(string &s1, string &s2){ //输入对象本来是非常量
auto &r=shorterString( const_cast<const string &>(s1),
const_cast<const string &>(s2)); //修改引用权限,使引用为底层const
return const_cast<string &>(r); //底层对象实际仍为非常量,只是引用是底层const,此时可用const_cast修改引用权限
}
复制代码
- 调用重载函数时可能有3种结果:
- 找到最佳匹配的函数
- 找不到任何函数与实参匹配,无匹配错误
- 匹配到多于一个函数,但每一个都不是最佳,二义性调用错误
特殊用途语言特性
默认实参
- 默认实参:函数调用时可以不指定对应的形参,此时该形参被初始化为默认实参。
- 带有默认实参的形参,既可接受默认实参,也可在调用时被赋予指定值
- 默认实参只能在形参列表最后:函数调用时实参按照位置解析,默认实参负责填补函数调用缺少的尾部实参
- 设计含有默认实参的函数时,需要合理安排形参顺序
string screen(sz, sz, char=' '); //第一条声明
string screen(sz, sz, char='*'); //错,重复声明
string screen(sz=24, sz=80, char); //对,添加默认实参
复制代码
- 函数内的局部变量不可作为默认实参
- 用作默认实参的名字在函数声明所在的作用域中解析,但求值过程发生在函数调用时
- 例子:默认实参的名字在声明作用域中解析,但求值发生在调用时
sz wd=80;
char def=' ';
sz ht();
string screen(sz=ht(), sz=wd, char=def); //声明函数,带有默认实参
string window=screen(); //screen(ht(),80,' ')
void f2(){
def='*'; //def变量仍是外部的
sz wd=100; //内部重新定义了wd变量·
window=screen(); //默认实参的名字在外部解析,故为screen(ht(),80,'*')
}
复制代码
内联函数和constexpr
- 调用函数比求等价的表达式更慢,因为运行函数有开销
内联函数
:将函数在每个调用点处内联地展开(类似宏定义),避免运行时开销。形式是在函数返回类型前加inline
关键字- 内联说明只是向编译器发出的请求,编译器可将其忽略
- 内联适合用
规模小、流程直接、调用频繁
的函数。很多编译器不支持内联递归函数 constexpr函数
:能用于常量表达式的函数,其返回类型及所有形参类型都是字面值类型,且函数体有且仅有一条return语句
- constexpr函数应在编译期就可确定结果,其返回值是
常量表达式
,故可给constexpr类型变量赋值。 - 为在编译期随时展开,
constexpr函数一定是内联
的 - 允许
constexpr的返回值是非常量
,只要保证输入常量表达式
时能输出常量表达式
(即编译期求值
)即可。
constexpr int new_sz() {return 42;} //返回常量表达式
constexpr size_t scale(size_t cnt) {return new_sz()*cnt;} //形参非常量,返回值非常量
int arr[scale(2)]; //对,scale输入常量表达式时输出即是常量表达式
int i=2;
int a2[scale(i)]; //错,scale输入不是常量表达式,输出也不是常量表达式
复制代码
- 普通函数只可多次声明不可多次定义(不要把普通函数的定义放在头文件中),但内联函数和constexpr函数可多次定义,只需保证多个定义相同。因为调用这些函数只是内联展开
- 内联函数和constexpr函数应定义于头文件中,因为内联函数在编译时(链接前)就要展开,故它的定义必须对编译器可见
调试帮助
- assert宏用表达式作为条件,形如assert(expr)。对expr求值,若为true则什么也不做,若为false则输出信息并终止程序
- 预处理名字由预处理器而非编译器管理,不在std命名空间中。它们在程序内必须唯一。
- assert宏的行为依赖于预处理变量NDEBUG,若定义了NDEBUG,则assert宏什么也不做。
- 默认状态下NDEBUG未被定义,可用#define NDEBUG来定义它,从而关闭调试状态,不执行assert检查
- 亦可在编译选项中指定预处理变量
__func__
:编译器为每个函数定义了该变量,它是const char的静态数组,存放函数的名字- 预处理器定义了另外几个对调试有用的名字:
__FILE__
存放文件名的字符串字面值__LINE__
存放当前行号的整型字面值__TIME__
存放文件编译时间的字符串字面值__DATE__
存放文件编译日期的字符串字面值
函数匹配
- 当几个重载的函数形参数量相等且某些形参类型可转化时,函数匹配比较困难
- 第一步:按名字筛选。要求:1、与被调用函数同名;2、在调用点可见。这些函数称为候选函数
- 第二步:按实参筛选。要求:1、形参与实参数量匹配;2、形参与实参类型匹配,或实参能转换为形参。这些函数称为可行函数,如果没有可行函数,编译器报错:无匹配函数
- 第三步:选择最匹配。实参与形参类型越接近则匹配得越好,精确匹配比需要类型转换的匹配更好。
- 选择最匹配时,如果有且仅有一个函数满足以下条件,则匹配成功,否则编译器报错:二义性调用:
- 该函数每个实参的匹配都不劣于其他可行函数
- 该函数至少有一个实参的匹配优于其他可行函数
- 调用重载函数时尽量避免强制转换。设计良好的系统中不应对实参做强制转换
实参类型匹配转换
- 为确定最佳匹配,编译器将实参到形参的类型转换分为几个等级:
- 精确匹配,包括:类型相同、数组转指针、函数转指针、改变顶层const
- 通过const转换实现的匹配
- 通过类型提升实现的匹配
- 通过算术转换或指针转换实现的匹配
- 通过类类型转换实现的匹配
- 即使是很小的整型也会直接被提升为int
- 所有算术转换的级别一样,double转float并不比double转int更好。
- 非常量对象初始化常量引用需要类型转换
- 例子:实参类型转换
//整型提升
void ff(int);
void ff(short);
ff('a'); //char提升为int,调用(int)
//算术转换
void manip(long);
void manip(float);
manip(3.14); //浮点字面值是double,转换到long和float代价一样,二义性调用
//const转换
Record lookup(Account &);
Record lookup(const Account &);
const Account a;
Account b;
lookup(a); //底层const只能与底层const匹配,调用(const Account &)
lookup(b); //底层非const优先与底层非const匹配,调用(Account &)
复制代码
函数指针
- 函数指针指向的是
函数
而非对象。函数不是对象
- 函数的类型由其
返回值类型
和形参类型
共同决定,与函数名和形参名无关。 - 把函数名作为值使用时,函数自动转为指针
- 可使用函数指针调用函数,此时写不写解引用
*
无所谓 - 例子:函数类型和函数指针
bool lengthCompare(const string &, const string &); //类型是:bool(const string &, const string &)
bool (*pf)(const string &, const string &); //声明对应类型的函数指针,未初始化
pf=&lengthCompare; //初始化函数指针
pf=lengthCompare; //等价于上一句。因为函数名转为指针,故取地址符是可选的
//下面3条语句互相等价
bool b1=pf("hello","goodbye"); //解引用符是可选的
bool b2=(*pf)("hello","goodbye");
bool b3=lengthCompare("hello","goodbye");
复制代码
- 指向不同函数类型的指针间不存在转换规则
- 可以将函数指针赋值为nullptr或0的常量表达式,表示不指向任何函数
- 若定义了指向重载函数的指针,则指针类型必须与某一函数精确匹配
- 例子:指向重载函数的指针必须精确匹配
void ff(int *);
void ff(unsigned int);
void (*pf1)(unsigned int)=ff; //精确匹配到ff(unsigned int)
void (*pf2)(int)=ff; //错,无精确匹配
void (*pf3)(int *)=ff; //错,无精确匹配
复制代码
- 不能定义函数类型的形参,但形参可以是指向函数的指针。此时看起来像函数,实际上是指针。形参中的函数会被自动转为指针
- 可用类型别名和decltype简化函数/函数指针作为形参的函数原型,对函数做decltype时不会被转指针
- 例子:形参是函数指针
//类型是:bool(const string &, const string &)
bool lengthCompare(const string &, const string &);
//以下两行等价,形参中的函数类型会自动转为指针
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 &));
//以下两行等价,Func是函数类型
typedef bool Func(const string &, const string &);
typedef decltype(lengthCompare) Func;
//以下两行等价,FuncP是函数指针类型
typedef bool (*FuncP)(const string &, const string &);
typedef decltype(lengthCompare) *FuncP;
//以下两行等价,形参中的函数类型会自动转为指针
void useBigger(const string &s1, const string &s2, Func);
void useBigger(const string &s1, const string &s2, FuncP);
复制代码
- 不能将函数作为返回值,但能将函数指针作为返回值
- 函数指针作为返回值时,必须明确写成指针。返回值中的函数类型不会被自动转为指针
- 可用类型别名和尾置返回类型简化函数指针作为返回值的函数原型
- 例子:返回类型是函数指针
using F=int(int*,int); //是函数类型
using PF=int(*)(int*,int); //是函数指针类型
int (*f1(int))(int*,int); //直接声明
PF f1(int); //返回指向函数的指针,等价于上一行
F *f1(int); //返回指向函数的指针,等价于上一行
F f1(int); //错,不能返回函数
auto f1(int) -> int(*)(int*,int); //等价于原声明
复制代码
- auto和decltype也可用于返回函数指针的原型
- 例子:decltype用于返回函数指针
string::size_type sumLength(const string &, const string &);
string::size_type largerLength(const string &, const string &);
decltype(sumLength) *getFcn(const string &); //返回指向上面两函数之一的指针
复制代码
© 版权声明
文章版权归作者所有,未经允许请勿转载。
THE END