● 函数可以有0个或多个参数,而且(通常)会产生一个结果。
● 我们通过调用运算符来执行函数。调用运算符的形式是一对圆括号, 它作用于一个表达式,该表达式是函数或者指向函数的指针; 调用表达式的类型就是函数的返回类型。
● return
语句也完成两项工作: 一是返回return
语句中的值(如果有的话), 二是 将控制权从被调用函数转移回主调函数。 函数的返回值用于初始化调用表达式的结果,之后继续完成调用所在的表达式的剩余部分。
注意: 尽管实参与形参存在对应关系, 但是并没有规定实参的求值顺序, 编译器能以任意可行的顺序对实参求值
注意: 实参的类型必须与对应的形参类型匹配, 即 在初始化过程中初始值的类型也必须与初始化对象的类型匹配。
fact("hello") // 实参类型不正确,因为不能将 const char* 转换成int,所以错误。
● 注意 : 任意两个形参都不能同名, 而且函数最外层作用域中的局部变量也不能使用与函数形参一样的名字
注意: 不管怎样, 是否设置未命名的形参并不影响调用时提供的实参数量。 即使某个形参不被函数使用, 也必须为它提供一个实参
注意 : 函数的返回类型不能是数组类型或函数类型, 但可以是指向数组或函数的指针
● 对象的生命周期是程序执行过程中该对象存在的一段时间。
注意 : 局部变量还会隐藏在外层作用域中同名的其他所有声明
● 局部变量的生命周期依赖于定义的方式:
我们把只存在于块执行期间的对象称为自动对象。当块的执行结束后,
块中创建的自动对象就变成未定义的了。
形参是一种自动对象。 函数开始时为形参申请存储空间, 因为形参定义在函数体作用域之内, 所以一旦函数终止, 形参也就被销毁了。
● 某些时候,有必要令局部变量的生命周期贯穿函数调用及之后的时间,可以将局部变量定义成static
类型从而获得这样的对象,称为局部静态对象
函数声明
● 函数只能定义一次,但可以声明多次。 如果一个函数永远也不会被我们用到,那么它可以只有声明没有定义。
参数传递
● 每次调用函数时都会重新创建它的形参, 并用传入的实参对形参进行初始化
形参初始化的机理与变量初始化一样
指针形参
● 指针的行为和其他非引用类型一样。 当执行指针拷贝操作时, 拷贝的是指针的值。 拷贝之后, 两个指针是不同的指针。 因为指针使我们可以间接地访问它所指的对象, 所以通过指针可以修改它所指对象的值
int n=0,i=42;
int *p=&n,*q=&i; // p 指向 n, q指向i
*p=42; //n的值改变,p不变
p=q; // p现在指向了i, 但是 i和n的值都不变
const 形参和实参
● 顶层const 表示的是: 指针本身是个常量或者 任意对象是常量(顶层const 作用于对象本身)
● 底层const 表示指针所指对象是常量
● 注意 : 当用实参初始化形参时会忽略掉顶层const。 换句话说, 形参的顶层const被忽略掉了。
当形参有顶层const 时, 传给它常量对象或者非常量对象都是可以的
● 忽略掉形参的顶层const 有时可能产生意想不到的结果:
void fcn(const int i) {/*fcn 能够读取i, 但是不能向i写值*/}
void fcn(int i){/* .... */} // 错误: 重复定义了 fcn(int )
说明: 在 C++ 语言中,允许我们定义若干具有相同名字的函数, 不过前提是不同函数的形参列表应该有明显的区别。
因为顶层const被忽略掉了, 所以在上面的代码中传入两个fun函数的参数是完全一样的。
因此第二个fcn是错误的,尽管形式上有差异,但实际上它的形参和第一个fcn的形参没什么不同。
指针或引用形参与 const
● 注意 : 我们可以使用非常量初始化一个底层 const 对象, 当是反过来不行; 同时一个普通的引用必须用同类型的对象初始化。
● void reset (int &i)
{
}
要想调用引用版本的reset , 只能使用 int 类型的对象, 而不能使用字面值、求值结果为int的表达式、需要转换的对象或者 是 const int 类型的对象。
类似的,要想调用指针版本的reset只能使用int*
注意: C++ 允许我们用字面值初始化常量引用
const int &a=42;
数组形参
● 数组的两个特殊性质对我们定义和使用作用在数组上的函数有影响, 这两个性质分别是:
不允许拷贝数组以及使用数组时通常会将其转换成指针。
因为不能拷贝数组,所以我们无法以值传递的方式使用数组参数。 因为数组会被转换成指针, 所以当我们为函数传递一个数组时,实际上传递的是指向数组首元素的指针。
注意: 和其他使用数组的代码一样,以数组作为形参的函数也必须确保使用数组时不会越界。
● 管理指针形参有三种常用的技术。
(1) 使用标记指定数组长度
● 管理数组实参的第一种方法是要求数组本身包含一个结束标记, 使用这种方法的典型示例是C 风格字符串。
C 风格字符串存储在字符数组中, 并且在最后一个字符后面跟着一个空字符。 函数在处理C风格字符串时遇到空字符停止。
void print(const char *p)
{
if(cp) //若cp不是一个空指针
while(*cp) //只要指针所指的字符不是空字符
cout<<*cp++;
}
说明: 这种方法适用于那些有明显结束标记且标记不会与普通数据混淆的情况, 但是对于像int
这样所有取值都是合法值的数据就不太有效了。
(2) 使用标准库规范
● 管理数组实参的第二种技术是传递指向数组首元素和尾后元素的指针,
#include<iostream>
using namespace std;
void print(const int *beg, const int *end)
{ //输出beg到end之间(不包含 end) 的所有元素
while (beg != end)
{
cout << *beg++ << endl;
}
}
int main()
{
int j[2] = { 0,1 };
//j 转换成指向它首元素的指针
//第二个实参是指向j 的尾后元素的指针
print(begin(j), end(j));
system("pause");
return 0;
}
(3)显式传递一个表示数组大小的形参
● 第三种管理数组实参的方法是专门定义一个表示数组大小的形参, 在C 程序和过去的C++程序中常常使用这种方法。
#include<iostream>
using namespace std;
//size 表示数组的大小, 将它显式地传给函数用于控制对 ia 元素的访问
void print(const int ia[],size_t size)
{
for (size_t i = 0; i != size; ++i)
{
cout << ia[i] << endl;
}
}
int main()
{
int j[2] = { 0,1 };
print(j, end(j) - begin(j));
system("pause");
return 0;
}
传递多维数组
● 和所有数组一样,当将多维数组传递给函数时,真正传递的是指向数组首元素的指针。 因为我们传递的是数组的数组, 所以首元素本身就是一个数组, 指针就是一个指向数组的 指针
数组第二维(以及后面所有维度)的大小都是数组类型的一部分,不能省略:
void print (int (*matrix)[10],int rowSize); // matrix 指向数组的首元素, 该数组的元素是由10个整数构成的数组
等价定义
void print(int matrix[][10],int rowSize);
上述语句将matrix 声明成指向含有10 个整数的数组的指针。
int main(int argc,char *argv[]){};
注意 : 当使用`argv` 中的实参时,一定要记得可选的实参从`argv[1]` 开始; `argv[0]` 保存程序的名字, 而非用户输入。
含有可变形参的函数
● 为了能编写处理不同数量实参的函数, C++11 新标准提供了两种主要的方法:
如果所有的实参类型相同, 可以传递一个名为 initialize_list 的标准库类型;
C++ 还有一种特殊的形参类型(即省略符),可以用它传递可变数量的实参, 不过这种功能一般只用于与C 函数 交互的接口程序
initialize_list 形参
● initialize_list
是一种标准库类型, 用于表示某种特定类型的值的数组。
注意: initialize_list 也是一种模板类型,定义 initialize_list 对象时, 必须说明列表中所含的元素类型,列如:
initialize_list <string> ls; // initialize_list 的元素类型是string
注意 : 和 vector
不一样的 是, initialize_list 对象中的元素永远都是常量值, 我们无法修改 initialize_list 对象中元素的值
注意: 如果想向` initialize_list `形参中传递一个值的序列,则必须把序列放在一对花括号内
注意 : 含有initialize_list
形参的函数也可以同时拥有其他形参
返回类型和return语句
● 返回void 的函数不要求非得有return
语句, 因为在这类函数的最后一句后面会隐式地执行 return
● 注意 : 一个返回类型是void
的函数也能使用return expression
, 不过此时 return
语句的 expression
必须是另一个返回void
的函数。 强行令void
函数返回其他类型的表达式将产生编译错误
有返回值函数
● return
语句的return expression
形式提供了函数的结果。 只要函数的返回类型不是void
, 则该函数内的每条return
语句必须返回一个值。
return 语句返回值的类型必须与函数的返回类型相同, 或者能隐式地转换成返回的返回类型。
尽管C++ 无法确保结果的正确性,但是可以保证每个 return 语句的结果类型都正确。
也许无法顾及所有情况, 但是编译器仍然尽量确保具有返回值的函数只能通过一条有效的return 语句退出
注意 : 返回局部对象的引用时错误的; 同样,返回局部对象的指针也是错误的。一旦函数完成,局部对象被释放,指针将指向一个不存在的对象。
返回类类型的函数和调用运算符
● 调用运算符的优先级与点运算符和箭头运算符相同, 并且也符合左结合律
因此,如果函数返回指针、引用或类的对象, 我们就能使用函数调用的结果来访问结果对象的成员。
const string &shorterString(const string &s1, const string &s2)
{
return s1.size() <= s2.size() ? s1 : s2;
}
int main()
{
string s1 = "huang", s2 = "tt";
//我们可以通过这样的形式得到较短的string对象的长度
auto temp = shorterString(s1, s2).size(); //调用string 对象的size成员, 该string对象是由shorterString 函数返回的
cout << "输出结果为:" << temp << endl;
system("pause");
return 0;
}
因为上面提到的运算符都满足左结合律, 所以shorterString 的结果是点运算符的左侧运算对象,
点运算符可以得到该string 对象的size 成员,size 又是第二个调用运算符的左侧运算对象
引用返回左值
● 注意 : 函数的返回类型决定函数调用是否是左值。 调用一个返回引用的函数得到左值, 其他返回类型得到右值。
可以像使用其他左值那样来使用返回引用的函数的调用
特别是, 我们能为返回类型是非常量引用的函数的结果赋值
char &get_val(string &str, string::size_type ix)
{
return str[ix]; //get_val 假定索引值是有效的
}
int main()
{
string s("a value");
cout << "输出s的结果:" << s << endl;
get_val(s, 0) = 'A'; //将s[0] 的值改为 A, 返回值是引用,因此调用是个左值,可以出现在赋值运算符的左侧
cout << "输出修改后s的值:" << s << endl;
system("pause");
return 0;
}
注意: 如果返回类型是常量引用, 我们就不能给调用的结果赋值,比如给上述函数加上const, 就不能给该函数赋新值
列表初始化返回值
● C++ 新标准规定, 函数可以返回花括号包围的值的列表。类似于其他返回结果, 此处的列表也用来对表示函数返回的临时量进行初始化。
如果列表为空, 临时量执行中值初始化; 否则, 返回的值由函数的返回类型决定。
int temp()
{
return { 6 };
}
int main()
{
int a = temp();
cout << "输出结果:" << a << endl;
system("pause");
return 0;
}
注意 : 如果函数返回的是内置类型,则花括号包围得列表最多包含一个值, 而且该值所占空间不应该大于目标类型的空间。
如果函数返回的是类类型, 由类本身定义初始值如何使用
主函数 main 的返回值
● 注意 : 如果函数的返回类型不是 void
,那么它必须返回一个值。 但是这条规定有个例外:
我们允许main 函数没有return 语句直接结束。如果控制到达了main 函数的结尾处
而且没有return 语句,编译器将隐式地插入一条返回0的return语句
● cstdlib
头文件定义了两个预处理变量,我们使用这两个变量分别表示与失败:
// 该返回值与机器无关
EXIT_FAILURE 和 EXIT_SUCCESS
因为它们是预处理变量,所以既不能在前面加上 std:: , 也不能 在 using
声明中 出现。
返回数组指针
● 注意 : 因为数组不能被拷贝,所以函数不能返回数组。 不过函数可以返回数组的指针或引用, 其中最直接的方法是使用类型别名。
typedef int arrT[10]; // arrT是一个类型别名,它表示的类型是含有10个整数的数组
using arrT=int[10]; // arrT的等价声明
arrT *func(int i) //func 返回一个指向含有10个整数的数组的指针
{
}
声明一个返回数组指针的函数
● 返回数组指针的函数的形式如:
type(*function (parameter_list))[dimension]
int(*func(int i)) [10]
注意 : (*function (parameter_list))
两端的括号必须存在, 如果没有括号, 函数的返回类型将是指针的数组。
使用尾置返回类型
● 在C++11 新标准中还有一种可以简化上述func 声明 的方法, 就是使用尾置返回类型。 任何函数的定义都能使用尾置返回,但是这种形式对于返回类型比较复杂的函数最有效, 比如返回类型是 数组的指针 或者 数组的引用。
尾置返回类型跟在形参列表后面并以一个 -> 符号开头。为了表示函数真正的返回类型跟在形参列表之后,我们在本应该出现返回类型的地方放置一个auto
//func 接受一个int 类型的实参,返回一个指针, 该指针指向含有10个整数的数组
auto func(int i) -> int(*)[10];
注意 : 还有一种情况, 如果我们知道函数返回的指针将指向哪个数组, 就可以使用 decltype
关键字声明 返回类型 。
重载和 const 形参
● 注意 : 顶层const
不影响传入函数的对象。 一个拥有 顶层 const
的形参无法和另一个没有顶层const
的形参区分开来。
● 另一方面, 如果形参是某种类型的指针或引用, 则通过区分其指向的是常量对象还是非常量对象可以实现函数重载, 此时的const
是底层的:
重载与作用域
● 重载对作用域的一般性质并没有什么改变: 如果我们在内层作用域中声明名字, 它将隐藏外层作用域中声明的同名实体。 在不同的作用域中无法重载函数名。
注意 : 在局部作用域中声明函数不是一个很好的选择
默认实参
● 调用含有默认实参的函数时,可以包含该实参,也可以省略该实参。
● 函数只声明一次,但是多次声明同一个函数也是合法的。 不过有一点需要注意, 在给定的作用域中一个形参只能被赋予一次默认实参。
换句话说, 函数的后续声明只能为之前那些没有默认值的形参添加默认实参, 而且该形参右侧的所有形参必须都有默认值。
● 通常, 应该在函数声明中指定默认实参, 并将该声明放在合适的头文件中。
● 注意 : 局部变量不能作为默认实参。 除此之外, 只要表达式的类型能转换成形参所需的类型, 该表达式就能作为默认实参。
内敛函数和constexpr 函数
● 调用函数一般比求等价表达式的值要慢一些。 在大多数机器上,一次函数调用其实包含着一系列工作:调用前要先保存在寄存器; 可能需要拷贝实参; 程序转向一个新的位置继续执行。
注意: 内联说明只是向编译器发出的一个请求, 编译器可以选择忽略这个请求。
注意 : 内联函数可以避免函数调用的开销。一般来说,内联机制用于优化规模较小、流程直接、频繁调用的函数。很多编译器都不支持内联递归函数,而且一个几十行上百行的代码也不大可能在调用点内联的展开。
constexpr 函数
● constexpr
函数 是 指用于常量表达式的函数。 不过要注意的是:
函数的返回类型及所有形参的类型都得是字面值类型
而且函数体中必须有且只有一条return语句
constexpr int new_sz()
{
return 42;
}
constexpr int foo=new_sz(); //正确:foo 是一个常量表达式
说明 : 因为编译器能在程序编译时验证 new_sz() 函数返回的是常量表达式, 所以可以用 new_sz 函数 初始化 constexpr ;类型的变量 foo
因执行该初始化任务时, 编译器把对 constexpr
函数的调用替换成其结果值。为了能在编译过程中随时展开, constexpr
函数被隐式地指定为内联函数。
注意 : constexpr 函数体内也可以包含其他语句, 只要这些语句在运行时不执行任何操作就行。
例如: constexpr
函数中可以有 空语句、类型别名 以及 using
声明
constexpr int new_sz()
{
return 42;
}
constexpr size_t scale(size_t cnt)
{
return new_sz()*cnt;
}
int main()
{
constexpr int foo = new_sz();
int arr[scale(2)] = {}; //正确: scale(2) 是常量表达式
int i = 2; //i不是常量表达式
int a2[scale(i)]; // 错误: scale(i) 不是常量表达式
system("pause");
return 0;
}
注意 : 我们允许constexpr
函数的返回值并非一个常量。 constexpr
函数不一定返回常量表达式
和 其他函数不一样, 内联函数和constexpr
函数可以在程序中多次定义。 毕竟, 编译器要想展开函数仅有函数声明是不够的, 还需要函数的定义。
不过,对于某个给定的内联函数或者constexpr
函数来说, 它的多个定义必须完全一致。
基于这个原因, 内联函数和 constexpr
函数通常定义在头文件中。
asset 预处理宏
● asset
是一种预处理宏。 所谓预处理宏其实是一个预处理变量。
● 注意 : 预处理名字由预处理器而非编译器管理, 因此我们可以直接使用预处理名字而无须提供 using
声明。
● 和预处理变量一样, 宏名字在程序内必须唯一。 含有 cassert
头文件的程序不能再定义名为 assert
的变量 、函数或者其他实体。
注意 : assert 宏 常用于检查“不能发生”的条件。
NDEBUG 预处理变量
● assert
的行为依赖于一个名为 NDEBUG
的预处理变量的状态。 如果定义了NDEBUG
, 则 asset
什么也不做。
在默认状态下没有定义 NDEBUG
, 此时 assert
将执行运行时检查。
● 我们可以使用 一个 #define
语句定义 NDEBUG
,从而关闭调试状态。
● 定义NDEBUG
能避免检查各种条件所需的运行时开销。因此,assert
应该用于验证那些确实不可能发生的事情。
● 除了用于 assert
外, 也可以使用 NDEBUG
编写自己的条件调试代码。 如果NDEBUG
未定义, 将执行 #ifndef
和 #endif
之间的代码; 如果定义了NDEBUG
,这些代码将被忽略掉。
void print(const int ia[], size_t size
{
#ifndef NDEBUG
// __func__ 编译器定义的一个局部静态变量, 用于存放函数的名字
cerr<<__func_<<": array size is "<<size<<endl;
#endif
}
说明: 在上段代码中, 我们使用变量 __func__
输出当前调试的函数的名字。 编译器为每个函数都定义了__func__
, 它是 const char
的一个静态数组, 用于存放函数的名字。
可以使用这些常量在错误消息中提供更多的信息
函数匹配
● 当几个重载函数的形参数量相等以及某些形参的类型可以由其他类型转换得来时, 就不容易确定某次调用应该选用哪个重载函数。
确定候选函数和可行函数
● 函数匹配的第一步: 首先是选定本次调用对应的重载函数集, 集合中的函数称为候选函数。
候选函数具备两个特征:(1) 与被调用的函数同名,(2) 其声明在调用点可见。
函数匹配的第二步: 考察本次调用提供的实参, 然后从候选函数中选出能被这组实参调用的函数, 这些新选出的函数称为可行函数。
可行函数也有两个特征:
(1) 其形参数量与本次调用提供的实参数量相等
(2) 每个实参的类型与对应的形参类型相同, 或者能转换成形参的类型。
函数匹配的第三步(寻找最佳匹配(如果有的话)): 从 可行函数
中选择与本次调用最匹配的函数。 在这一过程中, 逐一检查函数调用提供的实参, 寻找形参类型与实参类型最匹配的那个 可行函数。
它的基本思想是: 实参类型与形参类型越接近, 它们匹配得越好 精确匹配比需要类型转换的匹配更好。
注意: 调用重载函数时应尽量避免强制类型转换。 如果在实际应用中确实需要强制类型转换, 则说明我们设计的形参集合不合理。
函数匹配和const 实参
● 如果重载函数的区别在于它们的引用类型的形参是否引用了const
, 或者指针类型的形参是否指向const
, 则当调用发生时编译器通过实参是否是常量决定选择哪个函数
● 指针类型的形参也是类似。 如果两个函数的唯一区别是它的指针形参指向常量或非常量,则编译器能通过实参是否是常量决定选用哪个函数:
如果实参是指向常量的指针,调用形参是const * 函数
; 如果实参是指向非常量的指针, 调用形参是普通指针的函数。
使用函数指针
● 函数指针指向的是函数而非对象。 函数的类型由它的返回类型和形参类型共同决定,与函数名无关。
● 当把 函数名作为一个值使用时, 该函数自动地转换成指针。
● 我们还能直接使用指向函数的指针调用该函数, 无须提前解引用指针:
string lengthCompare(const string &s1, const string &s2)
{
return s1.size() < s2.size() ? s2 : s1;
}
int main()
{
string(*pf)(const string &s3, const string &s4); //定义函数指针pf
pf = lengthCompare; //pf指向名为lengthCompare 的函数
//pf = &lengthCompare; //等价的赋值语句: 取地址赋是可选的
cout << "请输入两个字符串:\n";
string s1, s2;
cin >> s1 >> s2;
string s3 = (*pf)(s1, s2); //调用lengthCompare函数
//string s4 = pf(s1,s2); 一个等价的调用
//string s5=lengthCompare(s1,s2); //另一个 等价的调用
cout << "输出较长的字符串长度为:" << s3 << endl;
system("pause");
return 0;
}
● 在指向不同函数类型的指针间不存在转换规则。但是,我们可以为函数指针赋一个nullptr
或者 值为0的整型常量表达式, 表示该指针没有指向任何一个函数:
string lengthCompare(const string &s1, const string &s2)
{
return s1.size() < s2.size() ? s2 : s1;
}
string::size_type sumLength(const string &, const string &);
string cstringCompare(const string*, const string*);
int main()
{
string(*pf)(const string &s3, const string &s4); //定义函数指针pf
pf = 0; //正确: pf不指向任何函数
pf = sumLength; //错误: 函数返回类型不匹配
pf = cstringCompare; //错误: 函数形参类型不匹配
pf = lengthCompare; //正确: 函数和指针的类型精确匹配
system("pause");
return 0;
}
重载函数的指针
● 当我们使用重载函数时, 上下文必须清晰地界定到底应该选用哪个函数。 如果定义了指向重载函数的指针。编译器会通过指针类型决定选用哪个函数, 函数指针的形参列表与重载函数中的某一个形参列表精确匹配。
void ff(int *); //声明两个重载函数
void ff(unsigned int);
int main()
{
void(*pf1)(unsigned int) = ff; //pf1 指向 ff(unsigned int)
void(*pf2)(int) = ff; //错误: 没有任何一个ff与该形参列表匹配
double(*pf3)(int *) = ff; //ff和pf3 的返回类型 不匹配
system("pause");
return 0;
}
函数指针形参
● 虽然不能定义函数类型的形参,但是形参可以是指向函数的指针,此时,形参看起来是函数类型,实际上却是当成指针使用:
string lengthCompare(const string &s1, const string &s2)
{
return s1.size() < s2.size() ? s2 : s1;
}
//第三个形参是函数类型,它会自动地转换成指向函数的指针
void useBigger(const string &s1, const string &s2, string pf(const string &, const string &))
{
// 一些语句
}
// 一个等价的声明,显式地将形参定义成指向函数的指针
/*void useBigger(const string &s1, const string &s2, string(*pf)(const string &, const string &))
{
// 一些语句
}*/
int main()
{
cout << "请输入两个字符串:\n";
string s1, s2;
cin >> s1 >> s2;
useBigger(s1, s2, lengthCompare); //我们可以直接把函数作为实参使用,此时它会自动转换成指针
//自动将函数lengthCompare 转换成指向该函数的指针
system("pause");
return 0;
}
● 直接使用函数指针类型显得 很长而且繁琐。 类型别名能让我们简化使用了函数指针的代码:
string lengthCompare(const string &s1, const string &s2)
{
return s1.size() < s2.size() ? s2 : s1;
}
// Func 和 Func2 是函数类型
typedef string Func(const string &s1, const string &s2);
typedef decltype(lengthCompare) Func2; //等价的类型
// FuncP 和 Funcp2 是指向函数的指针
typedef string (*FuncP)(const string &, const string &);
typedef decltype(lengthCompare) *Funcp2;//等价的类型
//useBigger 的等价说明,其中使用了类型别名
void useBigger(const string &, const string &,Func);
void useBigger(const string &, const string &,FuncP2);
注意 : decltype
返回函数类型, 此时不会将函数类型自动转换成指针类型。因为decltype
的结果是函数类型, 所以只有在结果前面加上 * 才能得到指针。