C++Primer学习笔记(六)函数
但与操作符不同的是,函数有自己的函数名,而且操作数没有数量限制。
与操作符一样,函数可以重载,这意味着同样的函数名可以对应多个不同的函数。
7.1函数的定义
函数由函数名以及一组操作数类型唯一地表示。函数的操作数,也即形参,在一对圆括号中声明,形参与形参之间以逗号分隔。函数执行的运算在一个称为函数体的块语句中定义。每一个函数都有一个相关联的返回类型。
函数的调用
⑴ 使用调用操作符()实现函数调用
① 操作数是函数名和一组(可能为空的)用逗号分开的实参(argument)
② 结果类型为函数返回值的类型,结果为函数的返回值
⑵ 函数调用做两件事:
① 首先(隐式)定义形参,并用对应的实参进行初始化
· 形参的初始化与变量一样:
如果形参为非引用类型则复制实参的值,如果形参为引用类型则它只是实参的别名
② 主调函数(calling function)的执行被挂起,被调函数(called function)开始执行
形参和实参
形参是一个函数的局部变量
实参则是一个表达式,在调用函数时,所传递的实参个数必须与函数的形参个数完全相同。
函数返回类型
函数的返回类型可以是内置类型(如 int 或者 double)、类类型或复合类型(如int& 或 tring*),还可以是 void 类型,表示该函数不返回任何值。
函数不能返回另一个函数或者内置数组类型,但可以返回指向函数的指针,或指向数组元素的指针的指针。
函数形参表
函数形参表可以为空,但不能省略。没有任何形参的函数可以用空形参表或含有单个关键字 void 的形参表来表示。
形参表由一系列用逗号分隔的参数类型和(可选的)参数名组成。如果两个参数具有相同的类型,则其类型必须重复声明
调用函数时传递过多的实参、忽略某个实参或者传递错误类型的实参,几乎肯定会导致严重的运行时错误!对于大程序,在编译时检查出这些所谓的接口错误(interface error),将会大大地缩短“编译-调试-测试”的周期。
7.2参数传递
非引用形参
普通的非引用类型的参数通过复制对应的实参实现初始化。
非引用形参表示对应实参的局部副本。对这类形参的修改仅仅变了局部副本的值。一旦函数执行结束,这些局部变量的值也就没有了。
⑴ 指针形参
① 可以通过传入指针来间接访问指针所指对象
② 若需要保护指针指向的对象,可指定形参为指向 const 对象的指针
⑵ const 形参
① 用来初始化 const 形参的实参无论是不是 const 都可以
在函数中不能改变该实参的局部副本
② 非引用形参是否为 const 不影响编译器对其所属函数类型的判别
·注:对于指针,注意区分 const 是修饰指针本身的性质还是所指对象的性质
⑶ 不适合复制实参的情况
① 需要修改实参值时
② 需要将大型对象作为实参传递时
③ 无法复制对象时
以上情况可通过将形参定义为指针或引用类型解决
引用形参
⑴ 可以使用引用形参返回额外信息
⑵ 可以利用 const 引用形参避免复制实参
⑶ 使用引用形参时,将不需要修改的定义为 const 会更灵活
非 const 引用形参不能用 const 左值或右值初始化,而定义为 const 则无此问题
vector 和其他容器类型的形参
通常,函数不应该有 vector 或其他标准库容器类型的形参。调用含有普通的非引用 vector 形参的函数将会复制 vector 的每一个元素。
数组形参
处理数组的函数通常通过操纵指向数组指向数组中的元素的指针来处理数组。
由于忽略了数组长度,形参定义中如果包含了数组长度则特别容易引起误解。
当编译器检查数组形参关联的实参时,它只会检查实参是不是指针、指针的类型和数组元素的类型时是否匹配,而不会检查数组的长度。
⑴ 当把数组直接作为实参传给函数时:
① 若形参不是数组的引用,则自动转换为指向首元素的指针
例如以下三种定义完全等价:
void f(int*); //推荐,明确指出操作对象为指针
void f(int[]);
void f(int[10]);//维数被编译器忽略,写法容易引起误解
② 若形参是数组的引用则不发生转换,函数得到的是数组本身
此时编译器会检查形参和实参数组的维数是否匹配
⑶ 可以通过传递指向数组的指针来传递多维数组
与一维数组一样,编译器忽略第一维的长度,所以最好不要把它包括在形参表内
传递给函数的数组的处理
任何处理数组的程序都要确保程序停留在数组的边界内。
1在数组本身放置一个标记来检测数组的结束
2使用标准库规范,传递指向数组第一个和最后一个元素的下一个位置的指针。
3显式传递表示数组大小的形参,将第二个形参定义为表示数组的大小
main: 处理命令行选项
使用主函数形参处理命令行选项
int main(int argc, char *argv[])
· argv为一个C风格字符串数组,储存了从命令行调用程序时输入的字符串(包括程序名和参数)
· argc表示argv中字符串的个数
⒍ 含有可变形参的函数
为兼容C语言而保留的特性,只能传入简单数据类型,大多数类类型对象都不能正确复制
7.3return 语句
用于结束当前函数,将控制权返回给该函数的主调函数
⑴ 没有返回值的 return 语句
① 只能用于返回类型为 void 的函数
② 非必需,隐式 return 发生在函数最后一个语句完成时
⑵ 具有返回值的 return 语句
① 返回类型不是 void 的函数必须返回一个值(主函数 main 除外)
注:应在函数中的每条执行路径末尾都提供 return 语句;若不提供编译器可能也无法发现
② 用函数返回值初始化在调用函数处创建的临时对象,与用实参初始化形参的方法一样
即若返回非引用类型则得到副本,若返回引用类型则得到对象本身
③ 切勿返回局部对象的引用或指针
主函数 main 的返回值
⑴ 主函数返回类型为 int, 返回值在大多数系统中是一个状态指示器
返回0表示程序运行成功,其它大部分值表示失败
⑵ 不需要显式使用 return 语句,编译器将隐式插入返回0的语句
⑶ 为使返回值独立于机器,cstdlib 头文件定义了两个预处理变量
EXIT_FAILURE(运行失败) EXIT_SUCCESS(运行成功)
理解返回引用至关重要的是:千万不能返回局部变量的引用。
7.4函数声明
⑴ 函数必须在调用前声明
⑵ 函数声明可与定义分离
⑶ 一个函数只能定义一次但可声明多次
⒉ 函数声明由函数返回类型、函数返回类型和形参列表组成
三者描述了函数的接口,称为函数原型(function prototype)
⒊ 函数一般在头文件中声明,在源文件中定义
此时应使后者包含前者,以便编译器检查定义和声明是否一致
默认实参
⑴ 通过给形参表中的形参提供明确的初值可指定默认实参,如
void f( int, int = 1, int = 2 );
① 默认实参可以是任何适当类型的表达式
② 如果一个形参有默认实参,则其后的所有形参都必须有默认实参
③ 在一个文件中,只能为一个形参指定默认实参一次
通常应在函数声明中指定默认实参,并将该声明放在合适的头文件中
⑵ 调用函数时可省略有默认值的实参:若省略则使用默认实参,否则使用用户提供的实参
· 默认实参只能用来替换函数调用缺少的靠后的实参
因此设计带有默认实参的函数时,应将最少使用默认实参的形参排在最前,最多使用默认实参的形参排在最后
7.5局部对象
名字的作用域指的是知道该名字的程序文本区。对象的生命期则是在程序执行过程中对象存在的时间。
自动对象(automatic objects)是局部于函数的对象,会在每次函数调用时重新创建,并在函数结束时撤销
非静态局部变量和形参都是自动对象
一个变量如果位于函数的作用域内,但生命期跨越了这个函数的多次调用,这种变量往往很有用。则应该将这样的对象定义为 static(静态的)
静态(static) 局部对象确保不迟于在程序执行流程第一次经过该对象的定义语句时初始化,一旦创建在程序结束前都不会撤销
在所在函数被多次调用的过程中,static 局部对象会持续存在并保持它的值
7.6内联函数
目的:提高效率,加快速度。代价:增加代码行空间
• 阅读和理解函数 shorterString (内联函数)的调用,要比读一条用等价的条件表达
式取代函数调用表达式并解释它的含义要容易得多。
• 如果需要做任何修改,修改函数要比找出并修改每一处等价表达式容易得
多。
• 使用函数可以确保统一的行为,每个测试都保证以相同的方式实现。
• 函数可以重用,不必为其他应用重写代码。
将一个较小的、常被调用的函数指定为 inline 可使函数在调用点展开,以避免调用函数的额外开销
但内联说明对编译器来说只是一个建议,编译器也可能选择忽略
把inline 函数的定义放在头文件中,可以确保在调用函数时所使用的定义是相同的,并且保证在调用点该函数的定义对编译器可见。
类的成员函数
定义在函数体内部的,自动编译为内联函数。
每一个类的成员函数都有一个隐式的参数this指针。指的是调用这个函数的对象的地址。(除static之外)
常成员函数,就是在函数声明的某位加上const,表示调用该函数的对象不可以被修改。
构造函数
与类同名、 无返回值、不同的构造函数应该有不同的类型或数目的形参。
没有参数的构造函数叫做默认构造函数。
如果没有定义构造函数,编译器会自动生成一个 “合成默认构造函数”,类内部的成员变量按照自己的初始化方式进行初始化。(次初始化的方式取决于该对象的定义的位置。全局/局部)。
重载函数
⒈ 出现在相同作用域中的两个函数,如果具有相同的名字而形参表不同,则为重载函数(overloaded function)
仅仅是返回类型不同、默认实参不同、非引用形参的 const 性质不同不能实现重载
如果函数带有指向 const 类型的指针形参,则与带有指向相同类型的非 const 对象的指针形参的函数不相同。
在一些情况下,使用不同的函数名能提供较多的信息,使程序易于理解。
⒉ 若局部地声明函数,则该函数屏蔽而非重载在外层作用域中声明的同名函数
函数的声明应放在头文件中。
⒊ 重载确定的三个步骤
函数重载确定,即函数匹配是将函数调用与重载函数集合中的一个函数相关联的过程。
⑴ 确定候选函数集合
候选函数(candidate function)是与被调函数同名的函数,且在调用点上声明可见
⑵ 从候选函数集合中选择可行函数(找不到则此调用错误)
可行函数(viable function)满足两个条件:
① 函数形参与该调用的实参个数匹配(考虑默认实参)
② 每个实参的类型须与对应形参匹配(类型相同或可隐式转换)
⑶ 寻找最佳匹配(有多个匹配程度相同的则调用有二义性)
原则是实参类型与形参类型越接近则匹配越佳。具体匹配程度从高到低为:
① 精确匹配:实参与形参类型相同
② 通过类型提升实现的匹配
③ 通过标准转换实现的匹配
④ 通过类类型转换实现的匹配
为了确定最佳匹配,编译器将实参类型到相应形参类型转换划分等级。转换等级以降序排列如下:
1. 精确匹配。实参与形参类型相同。
2. 通过类型提升实现的匹配(第 5.12.2 节)。
3. 通过标准转换实现的匹配(第 5.12.3 节)。
4. 通过类类型转换实现的匹配(第 14.9 节将介绍这类转换)。
指向函数的指针
⒈ 定义形式
返回类型 (*标识符)(形参表)
函数类型由其返回类型以及形参表确定,而与函数名无关
⒉ 可使用 typedef 简化定义
typedef 返回类型 (*自定义类型名)(形参表)
⒊ 对函数指针进行初始化和赋值,可使用:
⑴ 同类型的函数
引用函数名但不调用该函数时,函数名被自动解释为指向函数的指针
因此可以直接使用函数名对函数指针初始化或赋值,不需要使用取地址操作符&
·注:指针类型须与函数完全匹配
⑵ 同类型的函数指针
⑶ 0值常量表达式
⒋ 通过函数指针调用函数
可以不使用解引用操作符,直接使用函数调用操作符()调用所指函数
⒌ 函数指针作形参时,星号*可写可不写
⒍ 函数的指针作函数返回值
函数指针返回类型 (*函数名(函数形参表))(函数指针形参表)
7指向重载函数的指针
指针的类型必须与重载函数的一个版本精确匹配。如果没有精确匹配的函数,则对该指针的初始化或赋值都将导致编译错误