C++ 学习笔记之(6)-函数、重载和指针
函数基础
函数定义包括以下几个部分
返回类型、函数名字、由0个或多个形参组成的列表以及函数体
局部对象
C++语言中,名字有作用域,对象有生命周期
- 名字的作用域是程序文本的一部分,名字在其中可见
- 对象的生命周期是程序执行过程中该对象存在的一段时间
自动对象
存在于块执行期间的对象成为自动对象,即执行到变量定义时创建对象,快末尾销毁它。比如形参等。
局部静态对象
在程序的执行路径第一次经过对象定义语句时初始化,并且知道程序终止才被销毁,即使所在的函数执行结束。
size_t count_calls()
{
static size_t ctr = 0; // 调用结束后,这个值仍然有效
return ++ctr;
}
int main()
{
for(size_t i = 0; i != 10; ++i)
cout << count_calls() << endl;
return 0;
}
函数声明
函数原型:即函数声明,包含返回类型,函数名和形参类型。
函数的声明和函数的定义类似,区别是声明无需函数体,用一个分号替代即可
定义函数的源文件应该把含有函数声明的头文件包含进来,编译器负责验证函数的定义和声明是否匹配
参数传递
形参的初始化方式和变量的初始化方式一样
引用传递:形参是引用类型,也被叫做传引用调用,即引用形参是对应实参的别名
- 值传递:当实参的值拷贝给形参是,形参和实参是两个独立对象,也被称为传值调用
传值参数
指针形参:指针与非引用类型一样,执行拷贝操作时,拷贝的是指针的值。两个指针是不同的指针,但是由于指针可以间接访问所指对象,所以指向的是统一内容
void reset(int *ip) { *ip = 0; // 改变指针 ip 所指对象的值 ip = 0; // 只改变了 ip 的局部拷贝,实参未被改变 }
传引用参数
对于引用的操作实际上是作用在引用所引的对象上
void reset(int &i) { i = 0; // 改变了 i 所引对象的值 }
使用引用避免拷贝
如果函数无需改变引用形参的值,最好将其声明为常量引用
const 形参和实参
实参初始化形参时会忽略掉顶层const,即形参的顶层const被忽略
void fcn(const int i) { /* fcn 能够读取 i, 但是不能向 i 写值 */ } void fcn(int i) {/* ... */} // 错误:重复定义 fcn(int)
C++语言允许函数名字相同,但前提是形参列表应该有明显区别,由于顶层const被忽略,故上述代码中传入两个函数的参数可以完全一样,故为重定义
非常量能够初始化底层const对象,反之不行
字面值能够初始化常量引用
函数形参尽量使用常量引用
- 常量:避免函数内修改值,同时可以传入普通参数和常量参数
- 引用:避免函数对参数进行拷贝,提高效率
知识点
数组形参
数组的两个性质
- 不允许拷贝数组, 故无法值传递
- 使用数组时通常会将其转换成指针,故实际上传递给函数的是指向数组首元素的指针
// 下面三个函数等价,每个函数形参都是 const int*
void print(const int*);
void print(const int[]); // 可以看出,函数的意图是作用于一个数组
void print(const int[10]); // 维度表示期望数组含有多少元素,但实际不一定
含有可变形参的函数
C++11新标准提供了两个方法处理不同数量实参的函数
- 若所有实参类型相同,可以传递一个名为
initializer_list
的标准库类型 - 若实参不同,则可以拜年可变参数模板(16.4节介绍)
initializer_list
若函数的实参数量未知但全部实参类型相同,则可以使用initializer_list
类型的形参, initializer_list
是一种标准库类型,用于表示某种特定类型的值的数组。其定义在同名头文件中
和
vector
类似,initializer_list
也是模板类型,定义时,需要说明类型initializer_list
对象中元素必须是常量值,无法改变, 并且放在花括号中void error_msg(initializer_list<string> il) { for(auto beg = il.begin(); beg != il.end(); ++beg) cout << *beg << " "; cout << endl; } // 调用语句 // expected, actual 是 string 对象 if (expected != actual) error_msg({"functionX", expected, actual}); else error_msg({"functionX", "okay"});
省略符形参
省略符形参是为了便于C++程序访问某些特殊的C代码设置的,这些代码使用了varargs
的C标准库功能。
省略符形参只能出现在形参列表的最后一个位置,形式有两种
// 第一种指定了部分形参的类型,对于这部分参数将进行正常的类型检查,省略符形参所对应的实参无需类型检查 void foo(parm_list, ...); void foo(...);
进阶
返回类型和return语句
return
语句终止当前正在执行的函数并将控制权返回到调用该函数的地方,return
语句有两种形式
return;
return expression;
无返回值函数
无返回值的return
语句用在返回类型是void
的函数中,此类函数最后会隐式执行return
。
有返回值函数
return
语句返回值的类型必须与函数的返回类型相同,或能隐式转换成函数的额返回类型
返回值的方式和初始化一个变量或形参的方式完全一样
不要反悔局部对象的引用或指针:因为函数完成后,其存储空间被释放,局部变量的引用指向的内存无效
调用返回引用的函数得到左值,其他返回类型得到右值
列表初始化返回值
vector<string> process() { return {"return", "example"}};
返回数组指针
函数不能返回数组,所以可以返回数组指针或引用
Type (*function (parameter_list)) [dimension]
// 例子,使用类型别名 typedef int arrT[10]; // arrT为类型别名,表示类型是含有 10 个整数的数组 using arrT = int[10]; // arrT的等价声明 arrT* func(int i); // func 返回一个指向含有 10 个整数的数组的指针 // 例子,没有使用类型别名 int (*func(int i))[10]; // 准曾理解该声明含义 func(int i); // 表示调用func函数时需要一个int类型的实参 (*func(int i)); // 意味着我们可以对函数调用的结果执行解引用操作 (*func(int i))[10]; // 表示解引用func的调用将得到一个大小是10的数组 int (*func(int i))[10]; // 表示数组中的元素是int 类型
使用尾置返回类型
// func 接受一个 int 类型的实参,返回一个指针,该指针指向含有 10 个整数的数组 auto func(int i) -> int(*)[10];
使用
decltype
int odd[] = {1, 3, 5, 7, 9}; decltype(odd) *arrPtr(int i); /* 返回一个指向含有 5 个整数的数组的指针,注意 decltype 的结果是数组,不会把数组类型转换成对应的指针,故需要加 * 符号 */
函数重载
若同一作用域内的几个函数名字相同但形参列表不同,即为重载函数
顶层
const
形参等价于无顶层const
形参int lookup(int i); int lookup(const int i); // 重复声明 int lookup(int); 顶层const int lookup(int *pi); int lookup(int * const pi); // 重复声明 int lookup(int *); 顶层const
底层
const
可以实现函数重载int lookup(int &); // 函数作用域 int 引用 int lookup(const int&); // 新函数,作用域常量引用 int lookup(int *); // 新函数,作用于指向 int 的指针 int lookup(const int *); // 新函数,作用于指向常量的指针
const_cast
和重载// 参数和返回类型都是 const string 的引用 const string &shorterString(const string &s1, const string &s2) { return s1.size() <= s2.size() ? s1 : s2; } // 返回类型是普通引用, 参数为普通变量 string &shorterString(string &s1, string &s2) { // 将普通引用转换成对对const的引用,然后调用const版本,返回对const的引用,如果不转换成const,则会根据非常量参数调用非const版本,直至报错 auto &r = shorterString(const_cast<const string&>(s1), const_cast<const string&>(s2)); // 将const引用转成普通引用返回 return const_cast<string&>(r); }
特殊用途语言特性
默认实参
某个形参被赋予默认值,则其后面的所有形参都必须有默认值
在给定作用域中,一个形参只能被赋予一次默认实参
typedef string::size_type sz; string screen(sz, sz, char = ' '); string screen(sz, sz, char = '*'); // 错误:重复声明 string screen(sz = 24, sz = 80, char); // 正确:添加默认实参
通常,应该在函数声明中指定默认实参,并将该声明放在合适的头文件中。
局部变量不能作为默认实参,除此之外,只要表达式的类型能转换成形参类型,该表达式就能作为默认实参
内联函数就和constexpr
函数
内联函数
inline
, 即函数在调用点 内联地展开内联说明只是向编译器发出的请求,编译器可以选择忽略这个请求
很多编译器不支持内联递归函数
constexpr
函数:能用于常量表达式的函数。 函数的返回类型及所有形参的类型都是字面值类型,函数体中必须有且只有一条return
语句constexpr int new_sz() { return 42; } // new_sz() 为无参数的 constexpr 函数
constexpr
函数被隐式地指定为内联函数constexpr
函数体内可以包含其他语句,只要这些语句在运行时不执行任何操作,比如空语句,类型别名以及using
声明constexpr
函数不一定返回常量表达式// 若参数 arg 为常量表达式,则 scale(arg) 也是常量表达式 constexpr size_t scale(size_t cnt) { return new_sz() * cnt; }
内联函数和
constexpr
函数可以在程序中多次定义,为了多个定义保持完全一致,通常定义在头文件内
调试帮助
assert
预处理宏:一个预处理变量,类似于内联函数,由预处理器而非编译器管理assert(expr); // 对 exp r求值,若表达式为假(即 0 ),assert输出信息并终止运行;若为真,则什么也不做
NDEBUG
:预处理变量,assert
的行为依赖于次变量的状态, 若定义了NDEBUG
, 则assert
什么也不错,相当于关闭调试。默认没有定义,此时assert
会执行运行时检查void print(const int ia[], size_t size) { # ifndef NDEBUG cerr << __func__ << endl // 当前调试函数名 << __FILE__ << endl // 存放文件名的字符串字面值 << __LINE__ << endl // 存放当前行号的整型字面值 << __TIME__ << endl // 存放文件编译时间的字符串字面值 << __DATE__ << endl; // 存放文件编译时期的字符串字面值 }
函数匹配
- 候选函数:调用对应的重载函数集,有两个特征
- 与被调用函数同名
- 其声明在调用点可见
- 可行函数:通过考察调用提供的实参,从候选函数中选出能被这组实参调用的函数,也有两个特征
- 其形参数量与本次调用提供的实参数量相等
- 每个实参类型与对应形参类型相同,或能转换成形参类型
- 实参类型与形参类型能够越接近,匹配的越好
- 若有多个形参匹配,编译器会一次检查每个实参以确定最佳匹配函数,若未找到,则会报告二义性错误
- 该函数每个实参的匹配都不劣于其他可行函数需要的匹配
- 至少有一个实参的匹配由于其他可行函数提供的匹配
实参类型转换
为了确定最佳匹配,编译器将实参类型到形参类型的转换分成等级,如下所示
需要类型提升和算数类型转换的匹配
小整形一般会提升到
int
类型或更大的整数类型void ff(int); void ff(short); ff('a'); // char 提升成 int; 调用 f(int)
所有算数类型转换的级别都一样
void manip(long); void manip(float); manip(3.14); // 错误:二义性调用,因为double可以转换为long 和 float
函数匹配和const
形参
若重载函数的区别在于用用类型的形参是否引用了const
,或指针类型的形参是否执行const
,则编译器通过实参是否为常量决定函数选择
int lookup(int &); // 参数为 int 引用
int lookup(const int &); // 参数为常量引用
const int a;
int b;
lookup(a); // 调用 lookup(const int &)
lookup(b); // 调用 lookup(int &)
函数指针
函数指针即指向函数的指针,函数指针也是指向某种特定类型。函数类型由其返回类型和形参类型决定,与函数名无关。
// 函数类型是 bool (const string&, const string &)
bool lengthCompare(const string &, const string &);
// 声明指向该函数的指针,只需要用指针替换函数名即可
// pf 指向一个函数,该函数的参数为两个 const string引用,返回值是 bool 类型
bool (*pf)(const string &, const string &); // 未初始化
函数名作为值使用时,自动转换为指针
pf = lengthCompare; // 等价于 pf = &lengthCompare; 取地址符可选
可直接使用函数指针调用函数,无需解引用
bool b1 = pf("hello", "goodbye"); // 调用 lengthCompare 函数 bool b2 = (*pf)("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, book (*pf)(const string &, const string &)); // 自动将函数 lengthCompare 转换成指向该函数的指针 useBigger(s1, s2, lengthCompare);
函数能返回函数指针,与形参不同,编译器不会自动将函数返回类型转换成对应的指针类型。
using F = int(int *, int); // F 是函数类型,不是指针 using PF = int(*)(int *, int); // PF 是指针类型 PF f1(int); // 正确:PF为函数指针,f1 返回函数指针 F f1(int); // 错误:F是函数类型,f1不能返回一个函数 int (*f1(int))(int *, int); // 等价于PF f1(int); auto f1(int) -> int(*)(int *, int); // 使用尾置返回类型方式声明返回函数指针的函数
结语
- 函数是命名了的计算单元,每个函数都包括返回类型、名字、(可能为空的)形参列表以及函数体
- C++中,函数可以被重载:同一个敏子可用于定义多个函数,只要形参数量或形参类型不同即可。
函数相关基础知识已经了解,以后要深入学习函数相关知识。