📔 C++ Primer 0x06 学习笔记

​更好的阅读体验(实时更新和修正)​


6.1 函数基础

  • 一个典型的函数定义包括以下部分:
  • 返回类型
  • 函数名字
  • 由0个或多个形参组成的列表
  • 函数体
  • 函数调用完成两项工作:隐式得用实参初始化形参;将控制权交给被调用函数,主函数的执行暂时中断
  • 实参是形参的初始值,尽管实参和形参存在对应关系,但没有规定实参的求值顺序
  • 形参名可选,但一般都要有。是否设置未命名的形参并不影响调用时提供的实参数量,即使某个形参不被函数使用,也必须提供一个实参

6.1.1 局部对象

  • 名字有作用域,是程序文本的一部分,名字在其中可见
  • 对象有生命周期,是程序执行过程中对象存在的一段时间
  • 函数是个有名字的代码块,构成一个新的作用域,可以在其中定义变量。形参和函数体内部定义的变量统称为局部变量,会隐藏外层作用域中同名的其他所有声明
  • 函数体之外定义的对象存在于整个程序执行过程中。此类对象在程序启动时被创建,直到程序结束才销毁
  • 局部变量的声明周期依赖于定义方式
  • 自动对象:普通局部变量,函数控制路径经过定义语句时创建,到达定义所在块尾销毁
  • 局部静态对象:普通变量定义成​​static​​类型,函数控制路径第一次经过定义语句时创建,程序结束时销毁

6.1.2 函数声明

  • 函数声明(函数原型)可以多次,函数声明不需要函数体,用一个分号代替即可,也无需形参名字
  • 函数三要素:返回类型、函数名、形参类型描述了函数的接口
  • 含有函数声明的头文件应该被包含到定义函数的源文件中

6.1.3 分离式编译

  • 分离式编译好处是如果我们修改了其中一个源文件,你们只需要重新编译那个改动了的文件即可
  • 分离式编译通常会产生一个后缀为​​.obj(Windows)​​​或​​.o(UNIX)​​的文件,表示对象代码
  • 编译器会把对象文件链接在一起形成可执行文件

6.2 参数传递

  • 形参初始化的机理与变量初始化一样
  • 当形参是引用类型时,实参被引用传递,引用形参是它对应实参的别名
  • 当实参的值被拷贝给形参时,形参和实参是两个独立的对象,实参被值传递

6.2.1 传值参数

  • 当初始化一个非引用类型的变量时,初始值拷贝给变量,变量的改动不会影响初始值
  • 指针形参:可以通过拷贝指针的值,间接访问和修改所指对象的值。C++ 中建议用引用类型的形参替代指针

6.2.2 传引用参数

  • 拷贝大的类类型或容器对象比较低效,甚至有的类型不支持拷贝(包括IO类型)
  • 传引用可以避免拷贝,如果函数无需改变引用形参的值,最好声明为常量引用
  • 我们可以利用引用形参返回额外信息

6.2.3 const 形参和实参

  • 形参是 ​​const​​​ 时注意顶层​​const​​​,实参初始化形参的时候会忽略掉顶层​​const​​,传常量对象或者非常量对象都可以
  • 因为顶层​​const​​​会被忽略掉,所以同名函数不能被形参是否为​​const​​区分
  • 一个普通引用必须采用同类型对象初始化,不能使用字面值,表达式,需要转换的对象或者​​const T​​类型的对象
  • 尽量使用常量引用:一方面避免意外修改;另一方面常量引用除了绑定常量对象还可以绑定非常量对象、字面值、一般表达式,可以处理更多实参

6.2.4 数组传参

  • 数组不允许拷贝数组,因此我们无法以值传递的方式使用数组参数,数组会被自动转换成指针
  • 因为数组以指针形式传递给函数,所以函数并不知道数组确切尺寸,所以要提供额外信息。一般管理指针形参有三种技术
  • 使用标记指定数组长度:例如字符串最后的空字符
  • 使用标准库规范:传递首元素和尾后元素指针
  • 现实传递一个表示数组大小的形参
  • 数组引用形参注意加括号,注意数组引用的维度被限定了,不可以将不同维度的数组作为实参了
  • ​int (*matrix)[10]​​​和​​int matrix[][10]​​是等价的,注意一下后面那个形式

6.2.5 main: 处理命令行选项

int main(int argc,char *argv[]){...}
int main(int argc,char* *argv){...}//等价
  • ​argc​​ 表示数组中字符串个数
  • ​argv[]​​是一个数组,元素是指向C风格字符串的指针
  • ​argv[0]​​​为程序名字,可选实参从​​argv[1]​​开始
  • ​argv[argc]​​也就是最后一个指针之后的元素保证为 0

6.2.6 含有可变形参的函数

  • 为了编写能处理不同数量实参的函数,​​C++11​​ 新标准提供了两种主要的方案:
  • 如果所有实参类型相同,可以传递一个名为​​initializer_list​​的标准库类型
  • 如果实参类型不同,我们可以编写特殊函数:可变参数模板

​initializer_list​

  • ​initializer_list​​​和​​vector​​​一样是一种模板类型,定义​​initializer_list​​必须说明所含元素的类型
  • 如果想向​​initializer_list​​形参中传递一个值的序列,则必须把序列放在一对花括号内
  • 范围​​for​​​循环中使用​​initializer_list​​​对象时应该常量引用类型,​​initializer_list​​对象的元素都是常量,无法修改

省略符形参

  • 省略符形参应该仅仅用于​​C​​​和​​C++​​通用的类型。特别应该注意的是,大多数类类型的对象在传递给省略符形参时都无法正确拷贝
  • 省略符形参只能出现在形参列表的最后一个位置

6.3 返回类型和 return 语句

6.3.1 无返回值函数

  • 返回类型 ​​void​
  • 可以没有​​return​​​,函数会隐式地执行​​return​
  • 强行令 ​​void​​函数非那会其他类型的表达式将产生编译错误

6.3.2 有返回值函数

  • ​return​​ 语句返回值类型必须和函数返回值类型相同或可以隐式转换
  • ​C++​​​ 无法保证返回值结果正确性,只能保证每一个​​return​​类型正确
  • 在含有​​return​​​语句的循环后面应该也有一条​​return​​语句,如果没有的话该程序就是错误的,很多编译器也检查不出来
  • 返回一个值的方式和初始化变量凡是完全相同,返回的值用于初始化调用点的一个临时量,该临时量就是函数调用结果。返回值时会发生拷贝
  • 如果返回引用,返回的引用对象的别名,不会真正发生拷贝
  • 不要返回局部对象的引用或指针,要想保证返回值安全,我们不妨提问:引用所引的是在函数之前已经存在的哪个对象
  • 调用一个返回引用的函数得到左值,其他返回值得到右值,我们可以为返回类型是非常量引用的函数的结果赋值
  • 函数可以返回花括号包围的值的列表
  • 我们允许​​main​​​函数没有​​return​​​语句直接结束,编译器会隐式地插入一个​​return 0​
  • ​main​​函数不能调用自己

6.3.3 返回数组指针

  • 因为数组不能被拷贝所以不能返回数组,但是可以返回数组的引用或指针
  • 可以用类型别名简化返回数组指针或引用
  • 可以使用尾置返回类型简化一个返回数组指针的函数的声明
  • 如果知道返回的指针指向那个数组,可以使用​​decltype关键字​​​声明返回类型,注意​​decltype​​​不会将数组转为指针,​​decltype​​​结果是个数组,所以还要在函数声明前加一个​​*​​符号

6.4 函数重载

  • 函数重载可以减轻命名和记名字的负担,但最好只重载功能确实非常相近的函数,要表现出功能特点
  • ​main​​函数不能重载
  • 编译器根据函数的实参类型推断想要哪个函数
  • 不允许两个函数除了返回类型外其他所有要素都相同
  • 形参有没有名字或使用别名,并能区分两个同名函数
  • 一个拥有顶层​​const​​​的形参无法和另一个没有顶层​​const​​的形参区分开来
  • 可以利用​​const_cast​​帮助重载
  • 调用重载函数可能三种结果:
  • 找到一个与实参的最佳匹配,调用该函数
  • 无匹配,报错
  • 多余一个函数可以匹配,但每个都不是最佳选择,报错。二义性调用

6.4.1 重载与作用域

  • 将函数声明置于局部作用域不是一个明智的选择
  • ​C++​​,名字查找发生在类型检查之前
  • 内层作用域中声明名字会隐藏外层作用域中声明的同名实体
  • 在不同作用域中无法重载函数名

6.5 特殊用途语言特性

6.5.1 默认实参

  • 调用含有默认实参的函数可以包含该实参,也可以甚略该实参(只能甚略尾部的)
  • 一旦某个形参被赋予了默认值,它后面的所有形参都必须有默认值
  • 设计含有默认实参的函数时,尽量让不怎么使用默认值的形参出现在前面,而让那些经常使用默认值的形参出现在后面
  • 给定的作用域中一个形参只能被赋予一次默认实参。函数可以声明多次,但要注意函数的后续声明只能为之前没有默认值的形参添加默认实参

6.5.2 内联函数和 constexpr 函数

内联函数

  • 内联函数可以避免函数调用的开销
  • 内联说明只是向编译器发出的一个请求,编译器可以选择忽略这个请求
  • 一般来说内联用于优化规模较小,流程直接,频繁调用的函数,很多编译器不支持内联递归函数或大段代码

​constexpr​​ 函数

  • 函数的返回类型以及所有形参的类型都得是字面值
  • 函数体中必须有且只有一条​​return​​语句
  • ​constexpr​​函数会被隐式地指定为内联函数
  • ​constexpr​​函数内也可以有其他语句,只要这些语句运行时不执行任何操作就行
  • 我们允许​​constexpr​​函数的返回值并非一个常量,​​constexpr​​函数不一定返回常量表达式
  • 内联函数和​​constexpr​​函数可以在程序多次定义,一般的我们通常将内联函数和cosntexpr函数定义在头文件中

6.5.3 调试帮助

  • ​assert​​​ 预处理宏,将表达式作为条件。如果表达式为假,那么​​assert​​输出信息并终止程序的执行
  • ​assert​​​的以内依赖于名为​​NDEBUG​​的预处理变量状态。如果定义了​​NDEBUG​​则​​assert​​什么也不做,默认没定义,此时​​assert​​将执行运行时检查
  • 定义​​NDEBUG​​能避免检查各种条件所需的运行时开销,既可以直接在代码中​​define​​也可以通过命令行选项​​-D NDEBUG​​定义
  • ​assert​​​应该仅用于验证那些确实不可能发生的事情。可以把​​assert​​当作调试程序的一种辅助手段,但不能替代真正的运行时逻辑检查,也不能替代程序本身应该包含的错误检查

6.6 函数匹配

  1. 函数匹配第一步是选定本次调用对应的重载函数集,这个集合中的函数为候选函数
  • 候选函数特征:与被调用函数同名,声明在调用点可见
  1. 函数匹配第二步考察调用提供的实参,选出可行函数
  • 可行函数特征:形参数量与实参数量相等,每个实参类型都和对应形参类型相同或者可以转换
  • 如果函数有默认实参,那么传入实参数量可能少于实际使用实参数量
  • 如果没有找到可行函数,编译器将报告无匹配函数错误
  1. 函数匹配第三步是寻找最佳匹配
  • 实参类型与形参类型越接近越好
  • 多个形参则要满足:该函数每个实参匹配都不劣于其他可行函数需要的匹配;至少有一个实参的匹配优于其他可行函数提供的匹配
  • 如果没有任何一个函数脱颖而出,那么会报二义性调用错误
  • 调用重载函数时应尽量避免强制类型转换。如果确实需要,说明设计的形参集不合理

6.6.1 实参类型转换

为了确定最佳匹配,编译器将实参类型到形参类型划分为一下几个等级

  1. 精确匹配
  • 实参类型和形参类型相同
  • 实参从数据或函数类型转为对应指针类型
  • 向实参添加顶层​​const​​​或从实参中删除顶层​​const​
  1. 通过​​const​​转换实现的匹配
  2. 通过类型提升实现的匹配
  3. 通过算术类型转换或指针转换实现的匹配:所有算术类型转换级别都一样
  4. 通过类类型转换实现的匹配

6.7 函数指针

  • 函数指针指向某种特定类型,函数的类型由它的返回类型和形参类型共同决定与函数名无关
  • 当我们把一个函数名作为一个值使用时,函数自动转换成指针
  • 使用重载函数的指针,指针类型必须与重载函数中的某一个精确匹配
  • 返回函数指针可以使用类型别名或尾置返回类型简化
  • 使用​​decltype​​作用于某个函数时,返回的函数本身而非指针类型,我们要显式地加上*以表明返回指针