c++primer 第6章 函数

第6章 函数

  • 函数定义和声明(函数传参/返回值/重载(调用匹配)/函数指针)
  • 函数 命名了的代码块 可以有0/多个参数 通常产生一个结果 可以重载

6.1 函数基础

  • 函数定义:返回类型、函数名字和0个或者多个形参组成的列表以及函数体
  • 调用运算符:调用运算符的形式是一对圆括号,它作用于一个表达式,该表达式是函数或者指向函数的指针,圆括号之内是用逗号隔开的实参列表,用实参初始化函数的形参
  • 编写函数
  • 调用函数
      1. 用实参初始化函数对应的形参
      1. 将控制权转移给被调用函数
    • 主调函数执行被中断,被调函数开始执行
    • return语句:
        1. 返回return语句中的值
        1. 将控制权从被调函数转移回主调函数
  • 形参和实参
    • 实参是形参的初始值(没有规定实参的求值顺序)
    • 实参的类型必须与对应的形参类型匹配(实参数目)
  • 函数的形参列表
    • 可以为空 但不能省略
    • 形参列表中的形参用逗号隔开 每个形参都是含有一个声明符的声明
    • 任意形参不能同名,函数最外层作用域局部变量也不能与形参同名
    • 形参名可选 但必须为它提供一个实参
  • 函数返回类型
    • 返回类型不能是数组类型或者函数类型,但可以是指向数组或者函数的指针

6.1.1 局部对象

  • 名字作用域:程序文本一部分,名字在其中可见
  • 对象生命周期:程序执行过程中对象存在的一段时间
  • 局部变量:形参和函数体内部定义的变量(会隐藏在外层作用域中同名的其他所有声明)
  • 自动对象
    • 普通局部变量如形参
  • 局部静态对象
    • static类型:令局部变量的生命周期贯穿函数调用及之后的时间
    • 若没有显式初始值将执行值初始化 内置类型局部静态变量初始化为0

6.1.2 函数声明

  • 函数声明(也称函数原型)无需函数体(无须形参名字)
  • 函数三要素:返回类型/函数名/形参类型
  • 在头文件中进行函数声明
    • 头文件声明,源文件定义,源文件包含头文件,编译器验证函数定义与声明是否匹配

6.1.3 分离式编译

  • 编写程序时按照逻辑关系将其划分开来,每个文件独立编译
  • 编译和链接多个源文件
    • $:系统提示符;CC:编译器名字;#:命令行下的注释语句
    • 若修改了其中一个源文件,只需重新编译改动了的文件,产生.o/.obj文件(该文件包含对象代码)
    • 编译器负责把对象文件链接在一起形成可执行文件

6.2 参数传递

  • 每次调用函数都会重新创建它的形参 用传入的实参对形参进行初始化(初始化机理与变量初始化一样)
  • 引用/传引用传递:形参为引用类型,将绑定到对应的实参上(引用形参是对应实参别名)
  • 值/传值传递:形参和实参是两个相互独立的对象,实参的值被拷贝给形参

6.2.1 传值参数

  • 初始化非引用类型变量 初始值被拷贝给变量,对变量的改动不会影响初始值
  • 指针形参
    • 指针行为与其他非引用类型一样:执行指针拷贝时,拷贝的是指针的值,拷贝后两个指针是不同的指针(但他们指向的地址是相同的)
    • c程序员常使用指针类型形参访问函数外部对象,c++建议使用引用类型形参代替指针

6.2.2 传引用参数

  • 对引用的操作实际上是作用在引用所引的对象上
  • 使用引用避免拷贝
    • 当某种类型不支持拷贝操作时,只能使用引用形参访问该类型对象
    • 如无需改变形参对象的内容,可将其定义为对常量的引用(当函数无需修改引用形参的值时最好将其声明为常量引用)
  • 使用引用形参返回额外信息
    • 返回多个结果可以定义一个新的数据类型,让它包含多个成员
    • 给函数传入额外的引用实参

6.2.3 const形参和实参

  • 与其他初始化一样,用实参初始化形参时会忽略掉顶层const,当形参有顶层const时,可以传给它常量或者非常量对象。(因为顶层const被忽略所以在函数重载时,有无顶层const的形参是一样的,不能进行这样的重载)
  • 指针或引用形参与const
    • 形参的初始化方式和变量的初始化方式是一样的
    • tip:
      • 不能用字面值初始化一个非常量引用(但可以初始化常量引用)
      • 不能把普通引用绑定到const对象上
      • 不能把普通引用绑定到字面值上
  • 尽量使用常量引用
    • 使用引用而非常量引用会极大限制函数所能接受的实参类型
      • 例如不能把const对象/字面值/需要类型转换的对象传递给普通引用对象

6.2.4 数组形参

  • 数组特殊性质
    • 不允许拷贝数组
    • 使用数组时通常会将其转换为指针(指向数组首元素的指针)
  • 但是可以将形参写成类似数组的形式,如(const int[])/(const int[10])但最终唯一形参都是(const int*)
  • 数组的大小对函数调用没有影响(但须确保使用数组时不会越界)。因数组以指针形式传递给数组,函数不知道数组数组尺寸,调用者须为此提供些额外信息
  • 管理指针形参的三种常用技术
    • 使用标记指定数组长度
      • 适用于有明显结束标记且该标记不会与普通数据混淆的情况,如C风格字符串末尾空字符
    • 使用标准库规范
      • 传递指向数组首元素和尾后元素的指针(begin end)
    • 显示传递一个表示数组大小的形参
      • 通过形参确定数组元素个数,如个数可为end(j)-begin(j)
  • 数组形参和const
    • 当函数不需要对数组元素执行写操作的时候,数组形参应该是指向const的指针
    • 只有当函数确实需要改变元素值的时候才把形参定义成指向非常量的指针
  • 数组引用形参
    • c++允许将变量定义成数组的引用,形参也可以是数组的引用(数组名(int (&arr)[10])两端的括号必不可少)
    • 数组的大小是构成数组类型的一部分,只要不超过维度在函数体内就可以放心的使用数组
  • 传递多维数组
    • 多维数组其实是数组的数组
    • 将多维数组传递给函数时,真正传递的是指向数组首元素的指针
    • 数组第二维(以及后面所有维度)的大小都是数组类型的一部分不能省略
void print(int (*matrix)[10],int rowSize){
    
    ...}//matrix指向含有10个整数的数组的指针
void print(int matrix[][10],int rowSize){
    
    ...}//与上式等价

6.2.5 main:处理命令行选项

//argc表示数组中字符串的数量 argv是一个数组它的元素是指向C风格字符串的指针
int main(int argc,char *argv[]){
    
    ...}
//与上式等效 其中argv指向cahr* argv的第一个元素指向程序的名字或者一个空字符串
//接下来的元素依次传递命令行提供的实参 最后一个指针之后的元素值保证为0
int main(int argc,char **argv){
    
    ...}

6.2.6 含可变形参的函数

  • 所有实参类型相同,可以传递一个名为initializer_list的标准库类型
  • 如果实参类型不同 则编写一种特殊函数 即可变参数模板
  • 特殊形参类型:省略符,可传递可变数量实参
  • initializer_list形参
    • 实参数量未知,但全部实参的类型都相同
    • 用于表示某种特定类型的值的数组
    • 是一种模板类型 但对象中的元素永远是常量值 无法修改对象元素的值
    • initializer_list包含begin和end成员,可以使用范围for循环
      | | initializer_list提供的操作 |
      |-----|-----|
      | initializer_list<T> lst; | 默认初始化;T类型元素的空列表 |
      | initializer_list<T> lst{a,b,c...}; | lst的元素数量和初始值一样多;lst的元素是对应初始值的副本;列表中的元素是const。 |
      | lst2(lst) | 拷贝或赋值一个initializer_list对象不会拷贝列表中的元素;拷贝后,原始列表和副本共享元素 |
      | lst2 = lst | - |
      | lst.size() | 列表中的元素数量 |
      | lst.begin() | 返回指向lst中首元素的指针 |
      | lst.end() | 返回指向lst中微元素下一位置的指针 |
  • 省略符形参
    • 便于c++程序访问某些特殊C代码设置的,这些代码使用名为varags的C标准库功能
    • 仅适用于c和c++通用的类型,大多数类类型对象传递省略符形参时都无法正确拷贝
    • 省略符形参只能出现在形参列表最后一个位置
    • 省略符形参无需类型检查

6.3 返回类型和return语句

  • return语句终止当前正在执行的函数并将控制权返回到调用该函数的地方
  • 两种形式return;return expression;

6.3.1 无返回值函数

  • 只能用于返回类型是void的函数,返回void的函数不要求非有return因为这类函数最后一句会隐式执行return
  • 如果在void函数中间位置提前退出可以使用return
  • 返回类型为void的函数return语句若有expression必须是另一个返回void的函数

6.3.2 有返回值函数

  • 只要函数返回值不是void,return就必须返回一个值,返回值类型必须相同或者能隐式地转换为函数的返回类型
  • 在含有return语句的循环后面应该也有一条return语句
  • 值是如何被返回的
    • 返回一个值的方式和初始化一个变量或形参的方式完全一样
  • 不要返回局部对象的引用或者指针
    • 函数终止意味着局部变量的引用将指向不再有效的内存区域
  • 返回类类型的函数和调用运算符
    • 调用运算符的优先级与点运算符和箭头运算符相同,且也符合左结合律
  • 引用返回左值
    • 调用一个返回引用的函数得到左值,其他返回类型得到右值
    • 可以像使用其他左值那样来使用返回引用的函数的调用,并且能够为返回类型是非常量引用的函数的结果赋值,如果返回类型是常量引用则不能给调用的结果赋值
  • 列表初始化返回值
    • 函数可以返回花括号包围的值的列表,用来对表示函数返回的临时量进行初始化
    • 如果列表为空,临时量执行值初始化,否则返回的值由返回类型决定
    • 如果函数返回内置类型,则花括号包围列表最多包含一个值,而且该值所占空间不应该大于目标类型的空间;如果返回的是类类型,由类本身定义初始值如何使用
  • 主函数main的返回值
    • 允许main函数没有return语句直接结束,控制到达结尾也没return语句编译器将隐式地插入一条返回0的return语句
    • cstdlib头文件定义了两个预处理变量,表示成功与失败,因为是预处理变量所以不能在前面加std::,也不能在using声明中出现
      • EXIT_FAILURE
      • EXIT_SUCCESS
  • 递归
    • 一个函数调用它自身,一定有某条路径是不包含递归调用的
    • 递归循环
      • 函数不断调用它自身直到程序栈空间耗尽为止
    • main函数不能调用它自己

6.3.3 返回数组指针

  • 数组不能被拷贝,所以函数不能返回数组,不过可以返回数组的指针或引用
  • 可以使用类型别名简化返回数组的指针或者引用
  • 声明一个返回数组指针的函数
    • 想定义一个返回数组指针的函数,数组维度必须跟在函数名字之后
    • 函数的形参列表也跟在函数名字后面并且形参列表应该先于数组的维度
Type (*function(parameter_list))[dimension]
  • 使用尾置返回类型
    • 跟在形参列表后面并且以一个->符号开头
    • auto func(int i)->int(*)[10]
  • 使用decltype
    • 已知函数返回的指针将指向哪个数组,就可以使用decltype关键字声明返回类型
    • decltype并不负责把数组类型转换成对应的指针,所以decltype的结果是一个数组,要想表示返回指针还需在函数声明时加一个*

6.4 函数重载

  • 同一作用域内几个函数名字相同但形参列表不同
  • 编译器会根据传递的实参类型推断想要的函数
  • main函数不能重载
  • 定义重载函数
    • 重载函数 形参数量或者形参类型上有所不同
    • 不允许两个函数除了返回类型外其他所有的要素都相同
  • 判断两个形参的类型是否相异
    • 形参名字仅起到助记的作用 有没有它并不影响形参列表的内容
    • 使用类型别名本质上形参列表也没什么不同
  • 重载和const形参
    • 顶层const不影响传入函数的对象(一个有顶层const的形参无法和另一个没有顶层const的形参区分开来)
    • 若形参是某种类型的指针或引用,则通过区分其指向的是常量对象还是非常量对象可以实现函数重载,此时const是底层的。编译器可以通过实参是否是常量来推断应该调用哪个函数
    • const不能转换为其他类型,但非常量可以转换为const。当传递一个非常量对象或者指向非常量对象的指针时,编译器会优先选用非常量版本的函数
  • 是否重载函数要看哪个更容易理解
  • const_cast和重载
    • 当实参不是常量时首先将实参强制转换为对const的引用,然后调用函数的const版本,const版本返回对const type的引用,这个引用实际绑定在了某个初始的非常量实参上,因此可以将其转换回一个普通的type& 这是安全的
  • 调用重载的函数
    • 函数匹配/重载确定:把函数调用与一组重载函数中的某一个关联起来(参数数量不同/参数类型无关)
    • 重载函数三种可能:
      • 最佳匹配
      • 无匹配:找不到任何一个函数与调用的实参匹配
      • 二义性调用:有多余一个函数可以匹配,但每一个都不是明显的最佳选择

6.4.1 重载与作用域

  • 如果在内层作用域中声明名字,它将隐藏外层作用域中声明的同名实体。在不同的作用域中无法重载函数名
  • 一旦在作用域中找到了所需的名字,编译器就会忽略掉外层作用域中的同名实体,然后检测函数调用是否有效
  • c++名字查找发生在类型检查之前

6.5 特殊用途语言特性

  • 函数相关语言特性:默认实参/内联函数/constexpr函数

6.5.1 默认实参

  • 默认实参:在函数的很多次调用中都被赋予一个相同的值,在调用含默认实参的函数时,可以包含该实参也可以省略该实参。
  • 可以定义一个或多个形参定义默认值,一旦某个形参被赋予了默认值,它后面的所有形参都必须有默认值
  • 使用默认实参调用函数
    • 在调用函数时省略该实参就行
    • 函数调用实参按位置解析,默认实参负责填补函数调用缺少的尾部实参
    • 合理设置形参的顺序,让不怎么使用默认值的形参出现在前面,让经常使用默认值的形参出现在后面
  • 默认实参声明
    • 在给定作用域中一个形参只能被赋予一次默认实参,函数后续声明只能为之前没有默认值的形参添加默认实参,且该形参右侧所有形参都必须有默认值
    • 通常应该在函数声明中指定默认实参,并将声明放在合适的头文件中
  • 默认实参初始值
    • 局部变量不能作为默认实参,除此之外,只要表达式类型能够转换成形参所需的类型,该表达式就能作为默认形参
    • 用作默认实参的名字在函数声明所在作用域内解析,求值过程发生在函数调用时

6.5.2 内联函数和constexpr函数

  • 定义为函数的好处:
    • 容易阅读理解
    • 确保行为的统一
    • 修改函数比等价表达式容易
    • 可以被重复利用
  • 缺点:
    • 函数比求等价表达式的值要慢一些
    • 函数调用包括:
      • 调用前保存寄存器,返回时恢复
      • 可能需要拷贝实参
      • 程序转向一个新的位置继续执行
  • 内联函数可避免函数调用的开销
    • inline 让函数在调用点内联地展开可消除函数运行时的开销
    • 内联函数只是向编译器发出的一个请求,编译器可以选择忽略
    • 内联机制用于优化规模较小、流程直接、频繁调用的函数
    • 很多编译器都不支持内联递归函数
  • constexpr函数
    • 用于常量表达式的函数,返回类型以及所有形参的类型都得是字面值类型,函数体中有且只有一条return语句
    • constexpr函数被隐式地指定为内联函数
    • constexpr函数不一定返回常量表达式
  • 把内联函数和constexpr函数放在头文件内
    • 内联函数和constexpr函数可以在程序中多次定义,但多个定义必须完全一致,通常定义于头文件中

6.5.3 调试帮助

  • 头文件保护等,程序可包含一些用于调试的代码,但只在开发时使用,发布时屏蔽掉。两项预处理功能:assert和NDEBUG
  • assert预处理宏
    • 预处理名字由预处理器而非编译器管理,可以直接使用预处理名字而无须提供using声明。与预处理变量一样,宏名字在程序内必须唯一。
    • assert(expr)如果表达式为假,assert输出信息并终止程序的执行,如果表达式为真则什么也不做
    • assert宏常用于检查 不能发生 的条件
  • NDEBUG预处理变量
    • 如果定义了NDEBUG,assert什么也不做;如果没定义NDEBUG则assert将执行运行时检查
    • 使用#define NDEBUG关闭调试状态
    • 定义自己的调试代码,若未定义NDEBUG则执行#ifndef和#endif之间的代码;若定义了NDEBUG则这些代码被忽略掉
    • c++编译器定义
      • _ _func_ _函数名字
    • 预处理器定义程序调试时有用的名字
      • _ _FILE_ _文件名字符串字面值
      • _ _LINE_ _当前行号整型字面值
      • _ _TIME_ _编译时间字符串字面值
      • _ _DATE_ _编译日期字符串字面值

6.6 函数匹配

  • 当函数形参数量相等以及某些形参的类型可以由其他类型转换得来时如何确定重载函数
  • 确定候选函数和可行函数
    • 候选函数:与被调用函数同名;声明在调用点可见
    • 可行函数:形参数量与本次提供的实参数量相等;每个实参类型与形参类型相同或能够转换为形参的类型
    • 如果没找到可行函数,编译器将报告无匹配函数的错误
  • 寻找最佳匹配
    • 逐一检查函数提供的实参,寻找形参类型与实参类型最匹配的那个可行函数
    • 实参与形参类型越接近则匹配的越好;精确匹配比需要类型转换的匹配更好
  • 含有多个形参的函数匹配
    • 编译器选择形参数量满足要求且实参类型和形参类型能够相匹配的函数,依次检查每个实参以确定哪个函数是最佳匹配,如果有且只有一个函数满足下列要求,则匹配成功
      • 该函数每个实参的匹配都不劣于其他可行函数需要的匹配
      • 至少有一个实参的匹配优于其他可行函数提供的匹配
  • 调用重载时应该尽量避免强制类型转换,形参设计的合理性

6.6.1 实参类型转换

  • 实参到形参的类型转换等级
      1. 精确匹配
      • 实参类型和形参类型相同
      • 实参从数组或函数类型转换为对应指针类型
      • 向实参添加顶层const或者从实参中删除顶层const
      1. const转换实现的匹配
      1. 类型提升实现的匹配
      1. 算术类型转换/指针转换
      1. 类类型转换
  • 需要类型提升和算术类型转换的匹配
    • 小整形提升到int类型或者更大的整数类型
    • 存在两种可能的算术类型转换时调用具有二义性
  • 函数匹配和const实参
    • 调用时编译器通过实参是否是常量来决定选择哪个函数
    • 不能将普通引用绑定到const对象上
    • 非常量对象可用于初始化常量引用和非常量引用,但非常量对象初始化常量引用需要类型转换,出于精确匹配的目的选用非常量版本的函数

6.7 函数指针

  • 函数指针指向特定类型,函数类型由它的返回值类型和形参类型共同决定,与函数名无关
  • 想声明一个可以指向该函数的指针,只需用指针替换函数名,如
bool (*pf)(const string&,const string&);
//pf前有一个*,故pf为指针,右侧为形参列表,表示pf指向的是函数,左侧函数返回值是布尔值
//pf指向一个形参是两个const string的引用,返回类型是bool的函数
//*pf两端的括号不可少,不加括号的话则pf是一个返回值为bool指针的函数
  • 使用函数指针
    • 将函数名作为值使用时,函数自动转换为指针
    • 直接使用只爱那个函数的指针调用函数可无须提前解引用
    • 指向不同函数类型的指针间不存在转换规则,但可以为函数指针赋值nulllptr或0,表示该指针没有指向任何一个函数
  • 重载函数的指针
    • 编译器通过指针类型决定选用哪个函数,指针类型必须与重载函数中的某一个精确匹配
  • 函数指针形参
    • 虽然不能定义数组和函数类型的形参。但形参可以是指向函数的指针
    • 形参看起来像是函数类型,实际上是当成指针使用
    • 可以直接把函数当作实参使用,它自动转换成指针
    • 类型别名可用于简化使用函数的指针,如
//Func和Func2是函数类型
//调用Func作为参数时编译器自动将Func表示的函数类型转换为指针
typedef bool Func(const string&,const string&);
typedef decltype(lengthCompare) Func2;//与上式等价
//FuncP和FuncP2是指向函数的指针
typedef bool (*FuncP)(const string&,const string&);
typedef decltype(lengthCompare) *FuncP2;//与上式等价
//decltype返回函数类型,不会自动将函数类型转换为指针类型,只有在结果前面加*才能得到指针
  • 返回指向函数的指针
    • 和数组类似,虽然不能返回一个函数,但能够返回指向函数类型的指针,必须把返回值类型写成指针形式
    • 通过类型别名可以某符号定义成函数或者指向函数的指针类型
    • 和函数类型的形参不一样,返回类型不会自动地转换成指针,必须显示地将返回类型指定为指针
    • 可以使用尾置返回类型的方式声明一个返回函数指针的函数
  • 将auto和decltype用于函数指针指针类型
    • 明确知道返回函数是哪一个就能使用decltype简化函数指针返回类型
    • 将decltype作用于某个函数时,它返回的是函数类型而非指针类型,需要显示地加上*以表明我们需要返回的指针而非函数本身。

小结

  • 函数:命名了的计算单元,对程序结构化至关重要
  • 函数包含:返回类型/名字/形参列表/函数体
  • 函数可以被重载:同一个名字可用于定义多个函数,只要形参数量或类型不同就行,编译器自动选定被调用函数
  • 函数匹配:从一组重载函数中选取最佳函数

术语表

  • 二义性调用:函数在匹配时两个或多个函数提供的匹配一样好,编译器找不到唯一的最佳匹配
  • 自动对象:仅存在于函数执行过程中的对象。当程序控制流经过此类对象定义语句时创建该对象,当到达定义所在块末尾时销毁该对象
  • 函数原型:函数声明包含返回类型/名字/形参类型

猜你喜欢

转载自blog.csdn.net/m0_68312479/article/details/128603726