一、函数基础
1、局部对象
对象有生命周期(lifetime)。理解这两个概念非常重要。
名字的作用域是程序文本的一部分,名字在其中可见。
对象的生命周期是程序执行过程中该对象存在的一段时间。
如我们所知,函数体是一个语句块。块构成一个新的作用域,我们可以在其中定义变量。形参和函数体内部定义的变量统称为局部变量(local variable)。它们对函数而言是“局部”的,仅在函数的作用域内可见,同时局部变量还会隐藏(hide)在外层作用域中同名的其他所有声明中。
自动对象
对于普通局部变量对应的对象来说,当函数的控制路径经过变量定义语句时创建该对象,当到达定义所在的块末尾时销毁它。我们把只存在于块执行期间的对象称为自动对象( automatic obiect)。当块的执行结束后,块中创建的自动对象的值就变成未定义的了。
局部静态对象
某些时候,有必要令局部变量的生命周期贯穿函数调用及之后的时间。可以将局部变量定义成static类型从而获得这样的对象。局部静态对象(local static object)在程序的执行路径第一次经过对象定义语句时初始化,并且直到程序终止才被销毁,在此期间即使对象所在的函数结束执行也不会对它有影响。
2、函数声明
和其他名字一样,函数的名字也必须在使用之前声明。函数只能定义一次,但可以声明多次。唯一的例外是如将要介绍,如果一个函数永远也不会被我们用到,那么它可以只有声明没有定义。
在头文件中进行函数声明
3、分离式编译
随着程序越来越复杂,我们希望把程序的各个部分分别存储在不同文件中。函数存在一个文件里,把使用这些函数的代码存在其他源文件中。为了允许编写程序时按照逻辑关系将其划分开来,C++语言支持所谓的分离式编译(separate compilation)。分离式编译允许我们把程序分割到几个文件中去,每个文件独立编译。
编译和链接多个源文件
二、参数传递
如前所述,每次调用函数时都会重新创建它的形参,并用传入的实参对形参进行初始化。
形参初始化的机理与变量初始化一样。
和其他变量一样,形参的类型决定了形参和实参交互的方式。如果形参是引用类型,它将绑定到对应的实参上;否则,将实参的值拷贝后赋给形参。
当形参是引用类型时,我们说它对应的实参被引用传递(passed by reference)或者函数被传引用调用(called by reference)。和其他引用一样,引用形参也是它绑定的对象的别名;也就是说,引用形参是它对应的实参的别名。
当实参的值被拷贝给形参时,形参和实参是两个相互独立的对象。我们说这样的实参被值传递(passed by value)或者函数被传值调用(called by value)
1、传值参数
当初始化一个非引用类型的变量时,初始值被拷贝给变量。此时,对变量的改动不会
影响初始值。
指针形参
指针的行为和其他非引用类型一样。当执行指针拷贝操作时,拷贝的是指针的值。拷贝之后,两个指针是不同的指针。因为指针使我们可以间接地访问它所指的对象,所以通过指针可以修改它所指对象的值:
2、传引用参数
我们知道对于引用的操作实际上是作用在引用所引的对象上
引用形参的行为与之类似。通过使用引用形参,允许函数改变一个或多个实参的值。
使用引用避免拷贝
拷贝大的类类型对象或者容器对象比较低效,甚至有的类类型(包括IO类型在内)根本就不支持拷贝操作。当某种类型不支持拷贝操作时,函数只能通过引用形参访问该类型的对象。
使用引用形参返回额外信息。
一个函数只能返回一个值,然而有时函数需要同时返回多个值,引用形参为我们一次返回多个结果提供了有效的途径。
3、const 形参和实参
指针或引用形参与const
尽量使用常量引用
4、数组形参
数组的两个特殊性质对我们定义和使用作用在数组上的函数有影响,这两个性质分别是:不允许拷贝数组以及使用数组时(通常)会将其转换成指针。因为不能拷贝数组,所以我们无法以值传递的方式使用数组参数。因为数组会被转换成指针,所以当我们为函数传递一个数组时,实际上传递的是指向数组首元素的指针。
使用标记指定数组长度
使用标准库规范
显式传递一个表示数组大小的形参
5、main:处理命令行选项
6、含有可变形参的函数
initializer_list形参
如果函数的实参数量未知但是全部实参的类型都相同,我们可以使用initializer list类型的形参。initializer list是一种标准库类型,用于表示某种特定类型的值的数组。
省略符形参
省略符形参是为了便于C++程序访问某些特殊的C代码而设置的,这些代码使用」名为varargs 的C标准库功能。通常,省略符形参不应用于其他目的。
省略符形参应该仅仅用于C和C++通用的类型。特别应该注意的是,大多数类类型的对象在传递给省略符形参时都无法正确拷贝。
省略符形参只能出现在形参列表的最后一个位置,它的形式无外乎以下两种:
void foo(parm_list, ...);
void foo(.. .);
三、返回类型和return语句
return语句终止当前正在执行的函数并将控制权返回到调用该函数的地方。return语句有两种形式:
return;
return expression ;
1、无返回值函数
没有返回值的return语句只能用在返回类型是void的函数中。返回void 的函数不要求非得有return语句,因为在这类函数的最后一句后面会隐式地执行return。
一个返回类型是void的函数也能使用return语句的第二种形式,不过此时return语句的 expression必须是另一个返回void 的函数。强行令void函数返回其他类型的表达式将产生编译错误。
2、有返回值函数
return语句的第二种形式提供了函数的结果。只要函数的返回类型不是void,则该函数内的每条return语句必须返回一个值。return语句返回值的类型必须与函数的返回类型相同,或者能隐式地转换成函数的返回类型。
尽管C++无法确保结果的正确性,但是可以保证每个return语句的结果类型正确。也许无法顾及所有情况,但是编译器仍然尽量确保具有返回值的函数只能通过一条有效的return 语句退出。
值是如何被返回的
返回一个值的方式和初始化一个变量或形参的方式完全一样:返回的值用于初始化调用点的一个临时量,该临时量就是函数调用的结果。
不要返回局部对象的引用或指针
函数完成后,它所占用的存储空间也随之被释放掉。因此,函数终止意味着局部变量的引用将指向不再有效的内存区域:
返回类类型的函数和调用运算符
3、返回数组指针
func(int i)表示调用func函数时需要一个int类型的实参。
(*func(int i))意味着我们可以对函数调用的结果执行解引用操作。
(*func(int i)) [10]表示解引用func的调用将得到一个大小是10的数组。
int (*func (int i))[10]表示数组中的元素是int类型。
使用尾置返回类型
在C++11新标准中还有一种可以简化上述func声明的方法,就是使用尾置返回类型(trailing return type)。任何函数的定义都能使用尾置返回,但是这种形式对于返回类型比较复杂的函数最有效,比如返回类型是数组的指针或者数组的引用。尾置返回类型跟在形参列表后面并以一个->符号开头。
typename decltype(表达式) 函数名(参数列表) -> 返回类型 {
// 函数体
}
OR
auto 函数名(参数列表) -> 返回类型 {
// 函数体
}
//举个例子
假设我们有一个简单的模板函数,该函数返回两个参数的乘积
template<typename T>
auto multiply(T a, T b) -> T {
return a * b;
}
在某些情况下,返回类型可能是一个复杂的表达式或模板元编程的结果
template<typename T>
auto make_pair(T first, T second) -> std::pair<T, T> {
return {first, second};
}
更复杂的例子,使用模板元编程
template<typename T1, typename T2>
auto add(T1&& t1, T2&& t2) -> decltype(t1 + t2) {
return t1 + t2;
}
使用decltype
还有一种情况,如果我们知道函数返回的指针将指向哪个数组,就可以使用decltype关键字声明返回类型。
四、函数重载
如果同一作用域内的几个函数名字相同但形参列表不同,我们称之为重载(overloaded)函数。
main函数不能重载。
顶层const不影响传入函数的对象。一个拥有顶层const的形参无法和另一个没有顶层const的形参区分开来
传递一个非常量对象或者指向非常量对象的指针时,编译器会优先选用非常量版本的函数。
const_cast和重载
从常量到非常量的转换:这是最常见的用法,通常是为了能够修改原本被声明为常量的数据。
从非常量到常量的转换:虽然不常见,但有时可能需要增加const限定符。
去除volatile限定符:const_cast
也可以用于去除volatile限定符。
添加volatile限定符:与上面相反,可以在指针或引用上添加volatile限定符。
调用重载函数
定义了一组重载函数后,我们需要以合理的实参调用它们。函数匹配(functionmatching)是指一个过程,在这个过程中我们把函数调用与一组重载函数中的某一个关联起来,函数匹配也叫做重载确定(overload resolution)。编译器首先将调用的实参与重载集合中每一个函数的形参进行比较,然后根据比较的结果决定到底调用哪个函数。
编译器找到一个与实参最佳匹配(best match)的函数,并生成调用该函数的代码。找不到任何一个函数与调用的实参匹配,此时编译器发出无匹配(no match)的错误信息。
有多于一个函数可以匹配,但是每一个都不是明显的最佳选择。此时也将发生错误,称为二义性调用( ambiguous call)。
1、重载与作用域
对于刚接触C++的程序员来说,不太容易理清作用域和重载的关系。其实,重载对作用域的一般性质并没有什么改变:如果我们在内层作用域中声明名字,它将隐藏外层作用域中声明的同名实体。在不同的作用域中无法重载函数名
在C+语言中,名字查找发生在类型检查之前。
五、特殊用途语言特性
默认实参、内联函数和 constexpr函数
1、默认实参
从右至左:默认参数值必须从函数参数列表的右侧开始。也就是说,一旦你为一个参数提供了默认值,所有在其右侧的参数也必须有默认值,除非在调用中显式给出。
可选性:调用函数时,你可以省略具有默认值的参数,这使得函数更加灵活,可以接受不同数量的参数
重载与默认参数:函数重载与默认参数可以结合使用。但是,当使用默认参数时,你必须确保函数签名的差异不仅仅在于哪些参数有默认值。换句话说,即使提供了默认参数,函数签名(参数类型和个数)也必须有所不同,否则编译器无法区分重载的函数。
模板与默认参数:在模板函数中,你也可以使用默认参数。但是,模板参数不能有默认值,只有函数参数可以有默认值
默认参数的计算:默认参数的值可以在编译时计算,也可以在运行时计算。如果默认值依赖于其他参数的值,则必须确保在调用中提供了必要的参数,或者前面的参数也有默认值
2、内联函数和constexpr函数
内联函数可避免函数调用的开销
将函数指定为内联函数(inline),通常就是将它在每个调用点上“内联地”展开。
constexpr函数
constexpr函数( constexpr function)是指能用于常量表达式的函数。定义constexpr函数的方法与其他函数类似,不过要遵循几项约定:函数的返回类型及所有形参的类型都得是字面值类型,而且函数体中必须有且只有一条return语句:constexpr
是C++11引入的一个关键字,用于声明一个函数或变量可以在编译时被求值。这意味着constexpr
函数可以在编译阶段执行,而不是在运行时执行,从而允许其结果被直接嵌入到编译后的二进制代码中,这在很多情况下可以提高效率和性能。
内联:constexpr
函数隐式地被声明为inline
,以避免链接问题,并且多次调用constexpr
函数在编译时会被优化为单次计算的结果。
和其他函数不一样,内联函数和 constexpr函数可以在程序中多次定义。毕竟,编
译器要想展开函数仅有函数声明是不够的,还需要函数的定义。不过,对于某个给定的内联函数或者constexpr函数来说,它的多个定义必须完全一致。基于这个原因,内联函
数和 constexpr函数通常定义在头文件中。
3、调试帮助
assert 预处理宏
assert是一种预处理宏( preprocessor marco)。所谓预处理宏其实是一个预处理变量,它的行为有点类似于内联函数。assert宏使用一个表达式作为它的条件:
assert ( expr) ;
NDEBUG预处理变量
assert 的行为依赖于一个名为NDEBUG的预处理变量的状态。如果定义了NDEBUG,则assert什么也不做。默认状态下没有定义NDEBUG,此时assert将执行运行时检查。
六、函数匹配
确定候选函数和可行函数
函数匹配的第一步是选定本次调用对应的重载函数集,集合中的函数称为候选函数( candidate function)。候选函数具备两个特征:一是与被调用的函数同名,二是其声明在调用点可见。
第二步考察本次调用提供的实参,然后从候选函数中选出能被这组实参调用的函数,这些新选出的函数称为可行函数(viable function)。可行函数也有两个特征:一是其形参数量与本次调用提供的实参数量相等,二是每个实参的类型与对应的形参类型相同,或者能转换成形参的类型。
寻找最佳匹配(如果有的话)
函数匹配的第三步是从可行函数中选择与本次调用最匹配的函数。在这一过程中,逐一检查函数调用提供的实参,寻找形参类型与实参类型最匹配的那个可行函数。下一节将介绍“最匹配”的细节,它的基本思想是,实参类型与形参类型越接近,它们匹配得越好。
该函数每个实参的匹配都不劣于其他可行函数需要的匹配。至少有一个实参的匹配优于其他可行函数提供的匹配。
1、实参类型转换
精确匹配,包括以下情况:
实参类型和形参类型相同。
实参从数组类型或函数类型转换成对应的指针类型。
向实参添加顶层const或者从实参中删除顶层const。
通过const转换实现的匹配。
通过类型提升实现的匹配。
通过算术类型转换实现的匹配。
通过类类型转换实现的匹配。
七、函数指针
函数指针指向的是函数而非对象。和其他指针一样,函数指针指向某种特定类型。函数的类型由它的返回类型和形参类型共同决定,与函数名无关。
要想声明一个可以指向该函数的指针,只需要用指针替换函数名即可:
bool (*pf)(const string &, const string &);/未初始化
*pf 两端的括号必不可少。如果不写这对括号,则pf是一个返回值为
指针的函数:
重载函数的针
函数类型的定义由返回类型和参数列表组成,不包括函数名称。因此,即使两个函数具有相同的名称,只要它们的参数列表不同,它们的函数类型就不同。这意味着你不能用一个函数指针直接指向另一个重载的函数,除非它们的类型完全相同。
和数组类似虽然不能定义函数类型的形参,但是形参可以是指向函数的指针。此时,形参看起来是函数类型,实际上却是当成指针使用
//第三个形参是函数类型,它会自动地转换成指向函数的指针
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 &));
和数组类似虽然不能返回一个函数,但是能返回指向函数类型的指针。然而,我们必须把返回类型写成指针形式,编译器不会自动地将函数返回类型当成对应的指针类型处理。
将auto和decltype用于函数指针类型