主要内容
- 函数基础
- 参数传递
- 返回类型和return语句
- 函数重载
- 特殊用途语言特性
- 函数匹配
- 函数指针
本章主要介绍了C++中函数的相关特性,关键是参数传递中关于const引用的使用,以及函数重载部分关于重载的函数匹配的介绍,以及函数指针部分关于复杂的函数指针的定义和使用等。
难点主要是函数重载的匹配,以及函数指针的定义形式。
需要掌握的内容
调用运算符是一对云括号,作用于一个表达式,表达式是函数或者指向函数的指针。
实参的求值顺序是没有明确规定的,编译器能以任意可行的顺序对实参求值。
在定义函数的时候形参名也是可选的,但是由于我们无法使用未命名的形参,所以形参一般都应该有个名字。但是这种有什么用?虽然可以编译通过。
int funcWithAnyoPara(int, int) {
cout << "hehe" << endl;
return 0;
}
函数返回类型
函数的返回类型不能是数组或函数类型,但是可以返回数组的指针或引用,也可以返回指向函数的指针,但是不能返回指针类型。
局部对象
名字有作用域,对象有生命周期,不是一个概念:
1. 名字的作用域是程序文本的一部分,名字在其中可见。
2. 对象的声明周期是程序执行过程中该对象存在的一段时间。
作用域是相对静态的,对象声明周期是动态的。
参数传递
参数传递分为:
1. 引用传递:引用形式
2. 值传递:指针形式也是值传递
虽然可以传递指针来修改实参,但是建议使用引用类型代替指针类型。
引用
使用引用避免拷贝,例如使用string参数时,如果拷贝的话会很消耗时间,直接使用引用即可,如果不需要修改形参的值,则将其声明为常量引用。
使用引用形参返回额外参数
修改这个引用形参来返回额外参数。
const形参和实参
顶层const作用于对象本身,所以const int i = 0; 是顶层const。当实参初始化形参时会忽略掉顶层的const,形参的顶层const被忽略掉了。当形参有顶层const时,传给它常量对象或非常量对象都是可以的。
但是这种忽略掉形参的顶层const会有意想不到的结果:
void fcn(const int i) {}
void fcn(int i) {}
这样的话两个函数是重复定义,因为const直接被忽略掉了,所以两个是一样的。
忽略的是形参的const而不是实参的const。
指针或引用形参与const
形参的初始化方式和变量的初始化方式是一样的,可以使用非常亮初始化一个底层const对象,但是反过来不行。同时一个普通引用必须用同类型的对象初始化。
int i = 42;
const int *cp = &i;
const int &r = i;
const int &r2 = 42;
int *p = cp;
int &r3 = r;
int &r4 = 42;
// 后面三个都是错误的
int i = 0;
const int ci = i;
string::size_type ctr = 0;
// void reset(int &i);
// void reset(int *i);
reset(&i); // 正确
reset(&ci); //错误
reset(i); //正确
reset(ci); //错误
reset(42); //错误
reset(ctr); //错误,类型不匹配。
常量引用实际上限制了实参的传入,因为非常量引用只能传入严格匹配的类型,而常量引用可以使用字面值,求值表达式或者需要转换的对象。所以常量引用更灵活一些。而非常量引用必须要类型完全匹配,double和int的类型转换都不行。
数组形参
数组的性质有不允许拷贝数组,使用数组时一般会将其转换成指针(哪些情况不会转换?decltype,sizeof还有什么?)
由于数组不能拷贝,所以无法以值传递的方式使用数组参数,而数组会被转换成指针,所以为函数传递数组时,实际上传递的时首元素的指针。
void print(const int*);
void print(const int[]);
void print(const int[10]);
这三种形式时等价的,形式都是const int*的。编译器都将其视为const int *。
使用数组时可以使用类似标准库的规范来控制数组范围。
void print(const int*beg, const int *end);
数组引用形参
C++允许将变量定义成数组的引用,但是这种情况下必须说明数组的大小。唯独时数组类型的一部分。但是这种用法限制了函数的可用性,既需要将数组的大小固定。只能给函数传入确定维度的函数。
传递多维数组也是一样,还是传递的指针,同样的,除了第一维,后面的所有维度都不能省略。第二维度及后面的所有维度的大小都是数组类型的一部分,不能省略。
含有可变形参的函数
initializer_list形参
如果函数的实参数量未知,但是全部实参的类型都相同,可以使用initializer_list类型的形参。这是一种标准库类型,用于表示某种特定类型的值的数组。所以这实际上还是可以看成一种特殊的数组。
有如下几个典型的操作:
//赋值操作
lst2 = lst;
lst.size();
lst.begin();
lst.end();
也是一种模板类型,定义对象时必须说明列表中所含元素的类型:
initializer_list<string> ls;
void err_msg(initializer_list<string> il)
{
for (auto beg = il.begin(); beg != il.end(); beg++) {
cout << *beg << endl;
}
cout << endl;
}
如果想想该行惨重传递一个值的序列,必须把序列放在一对花括号中:
err_msg("functionX", expected, actual);
返回类型和return 语句
返回数组指针
数组别名的使用,using, decltype, typedef, 或者使用新标准中的尾置返回值声明
int arr[10];
typedef int arrT[10]; // arrT是类型别名;
using arrT = int [10];
arrT *func(int i);
//decltype(arr) *; 是指针类型
声明一个返回数组指针的函数
同样还是需要包含函数的维度,如果不使用类型别名则:
int (*func(int i))[10];
使用尾置返回类型
auto func(int i)->int ()[10]; //括号不可少,也不可少
使用decltype
这种情况不会将数组视为指针,还是数组类型,所以decltype(odd) *后面要加一个指针才能说明是数组指针类型。
函数重载
同一作用域内的几个函数名字相同但是形参列表不同,称之为重载函数。
重载和const形参
顶层const不影响传入函数的对象,一个拥有顶层const的形参无法和另一个没有顶层const的形参分开来。但是如果形参是某种类型的指针或引用,则通过区分其指向的是常量对象还是非常亮对象可以实现函数重载,此时const是底层的。
//无法分开, 顶层const
int lookup(int);
int lookup(const int);
int lookup(int *);
int lookup(int *const);
//可以分开, 底层const
int lookup(int &);
int lookup(const int &);
int lookup(int *);
int lookup(const int *);
其中常量版本可接受常量和非常量的实参,编译器会优先选用非常量版本的函数。
const_cast和重载
这个地方有点意思,const_cast在此处是最有用的。比如,一个函数的形参和返回值都是const的,但是要传入一个非const的话,返回的还是const的,这种情况可以使用const_cast来实现。
const string &shorterString(const string &s1, const string &s2) {
return s1.size() <= s2.size() ? s1 : s2;
}
如果需要一种新函数,其形参和返回值都是非常量的,可以利用上面的函数实现。
string &shorterString(string &s1, string &s2) {
auto &s = shorterString(const_cast<const string &>(s1), const_cast<const string &>(s2));
return const_cast<string &>(s);
}
注意auto后面也要加上&才行,同时不要忘记加上括号,尖括号里面也要加上引用符号。如果auto后面不加上引用。则返回的就是一个局部变量的引用,那为什么尖括号里面也要加上&呢?
const_cast尖括号中的东西必须是pointer,reference或者pointer-to-data-memer类型,不能是普通的基础类型。否则会报如下错误。因为const cast一个普通类型没有意义
E:\ClionProjects\CppPrimer\chapter6\example_1.cpp:92:61: error: invalid use of const_cast with type ‘const string {aka const std::__cxx11::basic_string}’, which is not a pointer, reference, nor a pointer-to-data-member type
auto &result = shorterString(const_cast(s1), const_cast(s2));
调用重载的函数
函数匹配/重载确定
1. 最佳匹配
2. 无匹配
3. 二义性调用
重载与作用域
重载对作用域的一般性质没有什么改变:如果我们在内层作用域中声明名字,它将隐藏外层作用域中声明的同名实体。
void print(int);
void print(int, int);
void foo() {
void print(int, int, int);
//从这以后,外面声明的都没用了,之可见上面这一个三参数的。外层的名字被隐藏了。
}
编译器首先寻找对该函数名的声明,一旦在当前作用域中找到了所需的名字,编译器就会忽略掉外层作用域中的同名实体,剩下的工作就是检查函数调用是否有效了。
特殊用途语言特性
- 默认实参
- 内联函数
- constexpr函数
默认实参
要注意的是,函数可以只有一个定义,但是声明可以有多个,可以在声明中定义不同的默认实参。注意的是,在给定的作用域中一个形参只能被赋予一次默认实参。函数的后续声明只能为之前那些没有默认值的形参添加默认实参,而且该形参右侧的所有形参必须都有默认值。
默认实参可以是未命名的声明。
string screen(sz, sz, char = ' '); //sz是类型,char类型的没有命名
string screen(sz, sz, char = '*'); //不能修改默认实参
string screen(sz = 24, sz = 24, char); //合法
局部变量不能作为默认实参,除此之外,只要表达式能转换成形参类型即可作为默认实参
sz wd = 80;
char def = ' ';
sz ht();
string screen(sz = ht(), sz = wd, char = def);
string window = screen();
上面默认实参需要的函数在求值过程中发生调用,注意的是求值顺序是不明确的。
内联函数和constexpr函数
内联函数可避免函数调用的开销
内联说明只是想编译器发出的一个请求,编译器可以选择忽略这个请求。
一般来说,内联机制用于优化规模较小、流程直接、频繁调用的函数。很多编译器不支持内联递归函数,而且一个75行的函数也不大可能在调用点内联地展开。
constexpr函数
constexpr函数是指能用于常量表达式地函数。
要求:
1. 函数的返回类型及所有形参地类型都得是字面值类型,字面值类型有哪些?string不是字面值类型
2. 函数体中必须有且只有一条return语句
编译器能把constexpr函数的调用替换成其结果值,所以constexpr函数被隐式地指定为内联函数。
constexpr可以返回值是一个非常量,这个时候就不能用在一定需要常量地地方了。
要注意的是constexpr函数不一定返回常量表达式。
一般来说,由于内敛和constexpr函数可能直接展开,他们的定义必须完全一致,因此内联函数和constexpr函数通常定义在头文件中。
调试帮助
- assert预处理宏
- NDEBUG预处理变量
- func、FILE、LINE、TIME、DATE。
函数匹配
大多数情况下,很容易确定某次调用应该选择哪个重载函数,然而,当几个重载函数的形参数量相等以及某些形参的类型可以由其他类型转换来时就比较复杂了。
确定候选函数和可行函数
候选函数:本次调用对应的函数集,也就是,与被调用的函数同名,二是其声明在调用点可见。
可行函数:形参与本次调用提供的实参数量相等。二是每个实参的类型与对应的形参类型相同,或者能转换成形参的类型。
可行函数和候选函数可能有多个,接下来要寻找最佳匹配。
含有多个形参的函数匹配
如果有且只有一个函数满足下列要求,则匹配成功:
1. 该函数每个实参的匹配都不劣于其他可行函数需要的匹配。
2. 至少有一个实参的匹配由于其他可行函数提供的匹配。
void f(int, int);
void f(double, double);
f(42,2.56);
上面的f调用,任意匹配都不满足上面的要求,因为都需要转换类型。具有二义性。
实参类型转换
编译器将是残类型到形参类型的转换分成几个等级:
1. 精确匹配,包括
- 实参类型和形参类型相同。
- 实参从数组类型或函数类型转换成对应的指针类型
- 想实参添加顶层const或从是惨重删除顶层const
2. 通过const转换实现的匹配
3. 通过类型提升实现的匹配
4. 通过算术类型转换或指针转换实现的匹配。
5. 通过类类型转换实现的匹配。
需要类型提升和算术类型转换的匹配。所有的算术类型的级别都一样,从int想unsigned int的转换并不比从intxiangdouble的转换级别高:
void manip(long);
void manip(float);
manip(3.14);
上面的调用都具有算数类型转换,所以具有二义性。但是类型提升会比这种算术类型更高。int转到long,或者short转到int是比算术类型转换高的。
函数匹配和const实参
如果重载函数的区别在于他们的引用类型的形参是否引用了const,或者指针类型的形参是否只想const,则当调用发生时编译器通过实参是否时常量来决定选择哪个函数,因为这种类型的形参可以用const或者非const来初始化。但是要注意,const转换类型实现的匹配不是精确匹配。如果非常量对象要到常量对象上,则需要转换,如果时常量到常量则不需要const转换。
函数指针
函数类型和函数指针类型时不一样的,但是函数作为参数传入时会自动将其转换成函数指针。
使用函数指针
可以直接使用函数指针调用函数,而无需提前解引用指针。指向不同函数类型的指针间不存在转换规则。下面两种都是可以的。
pf(1,1);
(*pf)(1,1);
重载函数的指针
通过指针类型来判断该函数指针指向重载函数中的哪一个,该类型必须完全和其中一个重载函数一致,返回值类型也需要完全一致。
void ff(int *);
void ff(unsigned int);
void (*pf1)(unsigned int) = ff;
void (*pf2)(int) = ff; //不匹配任何形参列表
double (*pf3)(int*) = ff; //返回类型不匹配。
函数指针形参
虽然我们不能定义函数类型的形参,但是形参可以时指向函数的指针。
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&));
上面两种形式都可以的,第一种形式虽然用的是函数类型,但是他会自动转换成指向函数的指针。也可以直接把韩树明作为形参使用,此时会自动转换成函数指针。
可以使用类型别名来简化函数指针类型。
/* 函数类型 */
typedef bool Func(const string&, const string&);
typedef decltype(lengthCompare) Func2;
/* 函数指针类型 */
typedef bool (*FuncP)(const string&, const string&)
typedef decltype(lengthCompare) * FuncP2;
要注意的时decltype返回函数类型,而不会自动转换成指针类型。
此时可以用该类型别名重新声明useBigger的声明。
void useBigger(const string&, const string&, Func);
void useBigger(const string&, const string&, FuncP2);
返回指向函数的指针
和数组类似,虽然不能返回一个函数,但是能返回指向函数类型的指针。
不像指针参数一样,我们必须把返回类型携程指针形式,编译器不会自动将函数返回类型当成对应的指针类型处理。
using F = int(int *, int);
using Fp = int(*)(int *, int);
PF f1(int); //正确
F f1(int); //不能返回函数类型
F *f1(int); //正确
还可以使用尾置返回类型方式声明:
auto f1(int)-> int(*)(int, int);
将auto和decltype用于函数指针类型
需要记住,将decltype用于函数时,它返回函数类型而非指针类型。