C++ Primer 第四版读书笔记(六)之函数
内联函数、类成员函数和重载函数以及函数指针。
函数可以看作程序员定义的操作。与内置操作符相同的是,每个函数都会实现一系列的计算,然后(大多数时候)生成一个结算结果。但与操作符不同的是,函数有自己的函数名,而且操作数没有数量限制。与操作符一样,函数可以重载,这意味着同样的函数名可以对应对个不同的函数。
一、函数的定义
函数由函数名以及一组操作数类型唯一地表示。函数的操作数,也即形参(parameter),在一对圆括号中声明,形参与形参之间以逗号分隔。函数执行的计算在一个称为函数体(functionbody)的块语句中定义。每一个函数都有一个相关联的返回类型(return type)。
1.1 函数的调用
C++语言使用调用操作符(即一对圆括号)实现函数的调用。调用操作符的操作数是函数名和一组(有可能是空的)由逗号分隔的实参。函数调用的结果类型就是函数返回值的类型,该运算的结果本身就是函数的返回值。
函数调用做了两件事:用对应的实参初始化函数的形参,并将控制权转移给被调用函数,主调函数的执行被挂起,被调函数开始执行。函数的运行以形参的(隐式)定义和初始化。
1.2 函数体是一个作用域
函数体是一个语句块,定义了函数的具体操作。通常,这个块语句包含在一对花括号中,形成了一个新的作用域。和其他的块语句一样,在函数体中可以定义标量。在函数体内定义的变量只在该函数中才可以访问。这种变量称为局部变量,她们相对于定义它们的函数而言是“局部的”,其名字只能在该函数的作用域中可见。这种变量只在函数运行时存在。
1.3 形参和实参
类似于局部变量,函数的形参为函数提供了已命名的局部存储空间。它们之间的差别在于形参是在函数的形参表中定义的,并由调用函数时传递给函数的实参初始化。
实参则是一个表达式。它可以是标量或者是字面常量值,甚至是包含一个或几个操作符的表达式。在调用函数时,所传递的实参个数必须和函数的形参个数完全相同。与初始化式的类型必须与被初始化对象的类型匹配一样,实参的类型也必须与其对应的形参的类型完全匹配:实参必须具有与形参类型相同、或者能隐式转换为形参类型的数据类型。
1.4 函数返回类型
函数的返回类型可以是内置类型(如int或者double)、类类型或复合类型(如int&或string*),还可以是void类型,表示该函数不返回任何值。
函数不能返回另一个函数或者内置数组类型,但可以返回指向函数的指针,或指向数组元素的指针的指针。
二、参数传递
每次调用函数时,都会重新创建该函数所有的形参,此时传递的实参将会初始化对应的形参。
形参的初始化与变量的初始化一样:如果形参具有非引用类型,则复制实参的值;如果形参为引用类型,则它只是实参的别名。
2.1 非引用形参
普通的非饮用类型的参数通过复制对应的实参实现初始化。当用实参副本初始化形参时,函数并没有没有访问调用所传递的实参本身,因此不会修改实参的值。
非饮用形参表示对应的实参的局部副本。对这类形参的修改仅仅改变了局部副本的值。一旦函数执行结束,这些局部变量的值也就没有了。
2.1.1 指针形参
函数的形参可以是指针,此时将复制实参指针。与其他非引用类型的形参一样,该类形参的任何改变也仅作用于局部副本。如果函数将新指针赋值给形参,主调函数使用的实参指针的值没有改变。
事实上被复制的指针只影响对指针的赋值。如果函数形参是非const类型的指针,则函数可以通过指针实现赋值,修改指针所指向对象的值。
可以将指向const对象的指针初始化为指向非const对象,但不可让指向非const对象的指针指向const对象。
2.1.2 const形参
在调用函数时,如果该函数使用非饮用的非const形参,则既可给该函数传递const实参也可以传递非const实参。
2.1.3 复制实参的局部性
复制实参并不是在所有的情况下都适合,不适宜复制实参的情况包括:
1、当需要在函数中修改实参的值时。
2、当需要以大型对象作为实参传递时。对实际的应用而言,复制对象所付出的时间和存储空间代价往往过大。
3、当没有办法实现对象的复制时。
2.2 引用形参
应该将不需要修改的引用形参定义为const引用。普通的非const引用形参在使用时不太灵活。这样的形参既不能用const对象初始化,也不能用字面值或产生右值的表达式实参初始化。
三、 vector和其他容器类型的形参
通常,函数不应该有vector或其他标准库容器类型的形参。调用含有普通的非引用vector形参的函数将会复制vector的每一个元素。
四、数组形参
数组有两个特殊的性质,影响我们定义和使用作用在数组上的函数:一是不能复制数组;二是使用数组名字时,数组名会自动转化为指向其第一个元素的指针。因为数组不能复制,所以无法编写使用数组类型形参的函数。因为数组会被自动转化为指针,所以处理数组的函数通常通过操纵指向数组中的元素的指针来处理数组。
4.1 数组形参的定义
如果要编写一个函数,输出int型数组的内容,可用下面三种方式指定数组形参:
void printValue(int *){}
void printValue(int []){}
void printValue(int [10]){}
虽然不能直接传递数组,但是函数的形参可以写成数组的形式。
通常,将数组形参直接定义为指针要比使用数组语法定义更好。这样就明确地表示,函数操纵的是指向数组元素的指针,而不是数组本身。由于忽略了数组长度,形参定义中如果包含了数组长度则特别引起误解。
当编译器检查数组形参关联的实参时,它只会检查实参是不是指针、指针的类型和数组元素的类型是否匹配,而不会检查数组的长度。
4.2 数组实参
和其他类型一样,数组形参可定义为引用或非引用类型。大部分情况下,数组以普通的非引用类型传递,此时数组会悄悄地转换为指针。一般来说,非引用类型的形参会初始化为其相应实参的副本。而在传递数组时,实参是指向数组第一个元素的指针,形参复制的是这个指针的值,而不是数组元素本身。函数操纵的是指针的副本,因此不会修改实参指针的值。然而,函数可通过该指针改变它所指向的数组元素的值。通过指针形参做的任何改变都在修改数组元素本身。
4.3 main:处理命令行选项
传统上,主函数的实参是可选的,用来确定程序要执行的操作。
4.4 含有可变形参的函数
在无法列举出传递给函数的所有实参的类型和数目时,可以使用省略符形参。省略符暂停了类型检查机制。
五、return 语句
5.1 主函数main的返回值
返回类型不是void的函数必须返回一个值,但此规定有一个列外情况:允许主函数main没有返回值就可以结束。如果程序控制执行到主函数main的最后一个语句都还没有返回,那么编译器会隐式地插入返回0的语句。
5.2 返回非引用类型
函数的返回值用于初始化在调用函数处创建的临时对象。在求解表达式时,如果需要一个地方存储其运算结果,编译器会创建一个没有命名的对象,这就是临时对象。
用函数返回值初始化临时对戏那个与用实参初始化形参的方法是一样的。如果返回类型不是引用,在调用函数的地方会将函数返回值复制给临时对象。当函数返回非引用类型时,其返回值既可是局部对象,也可以是求解表达式的结果。
5.3 返回引用
当函数返回引用类型时,没有复制返回值。相反,返回的是对象本身。
5.4 千万不要反悔局部对象的引用
5.5 递归
直接或间接调用自己的函数称为递归函数。
六、局部对象
在C++语言中,每个名字都有作用域,而每个对象都有生命期。
在函数中定义的形参和变量的名字只位于函数的作用域中:这些名字只在函数体中可见。通常,变量名从声明或定义的地方开始到包围它的作用域结束处都是可用的。
6.1 自动对象
默认情况下,局部变量的生命期局限于所在函数的每次执行期间。只有当定义它的函数被调用时才存在的对象称为自动对象。自动对象在每次调用函数时创建和撤销。
局部变量所对应的自动对象在函数控制经过变量定义语句时创建。如果在定义时提供了初始化式,那么每次创建对象时,对象都会被赋予指定的初值。对于未初始化的内类类型局部变量,其初值不确定。当函数调用结束时,自动对象就会被撤销。
形参也是自动对象。形参所占用的存储空间在调用函数时创建,而在函数结束时撤销。
自动对象,包括形参,都在定义它们的块语句结束时撤销。形参在函数块中定义,因此当函数执行结束时撤销。当函数结束时,会释放他的局部存储空间。在函数结束后,自动对象和形参的值都不能再访问了。
6.2 静态局部对象
一个变来那个如果位于函数的作用域内,但生命期却跨越了这个函数的多次调用,这种变量往往很有用。则应该将这样的对象定义为static。
static局部对象确保不迟于在程序执行流程第一次经过该对象的定义语句时进行初始化。这种对象一旦被创建,在程序结束前都不会被撤销。当定义静态局部对象的函数结束时,静态局部对象不会撤销。在该函数被多次调用的过程中,静态局部对象会持续存在并保持它的值。
七、内联函数
7.1 内联函数避免函数调用的开销
一般来说,内联机制适用于优化晓得、只有几行的而且经常被调用的函数。大多数的编译器都不支持递归函数的内联。
7.2 把内联函数放入头文件
内联函数的定义对编译器而言必须是可见的,以便编译器能够在调用点内联展开该函数的代码。
内联函数可能要在程序中定义不止一次,只要内联函数的定义在某个源文件中只出现一次,而且在所有源文件中,其定义必须是完全相同的。把内联函数的定义放在头文件中,可以确保在调用函数时所使用的定义时相同的,并且保证在调用点该函数的定义对编译器可见。
注:在头文件中加入或修改内联函数时,使用了该头文件的所有源文件都必须重新编译。
八、类的成员函数
成员函数的定义与普通函数的定义类似。和任何函数一样,成员函数也包含下面四个部分:
1、函数返回类型。
2、函数名。
3、用逗号隔开的形参表(也可能是空的)。
4、包含在一对花括号{ }里面的函数体。
8.1 this指针的引入
每个成员函数都有一个额外的、隐含的形参this。在调用成员函数时,形参this初始化为调用函数的对象的地址。
8.2 const成员函数的引入
在成员函数声明的形参后面const所起的作用:const改变了隐含的this形参的类型。
用这种方式使用const的函数称为常量成员函数。由于this是指向const对象的指针,const成员函数不能修改调用该函数的对象。
注:const对象、指向const对象的指针或引用只能用于调用其const成员函数,如果尝试用他们来调用非const成员函数,则是错误的。
8.3 this指针的使用
九、重载函数
出现在相同作用域中的两个函数,如果具有相同的名字而形参不同,则称为重载函数。
通过省去为函数起名并记住函数名字的麻烦,函数重载简化了程序的实现,是程序更容易理解。
9.1 重载与作用域
在函数中局部声明的名字将屏蔽而不是重载在外层作用域中声明的同名函数。
一般来说,局部地声明函数是一种不明智的选择。函数的声明应放在头文件中。