前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >【笔记】《C++Primer》—— 第6章:函数

【笔记】《C++Primer》—— 第6章:函数

作者头像
ZifengHuang
发布2020-07-29 16:06:09
6860
发布2020-07-29 16:06:09
举报
文章被收录于专栏:未竟东方白未竟东方白

好久不见,我每天都好摸啊,继续之前的吧。

函数这一节内容又多又杂,但是相当有用,尤其是其中关于引用的应用和最后的调试部分。可能会比较长,等下一节写完就来做个小总结。

6.1 函数基础

  1. 函数由返回类型,函数名,形参和函数体组成。
  2. 当函数被调用时,调用带来的实参会被初始化给形参(类似新定义变量),原函数执行中断从被调函数开始执行,直到return
  3. 要注意赋值给形参的时候,函数没有规定实参的求值顺序
  4. 形参必定会被拷贝初始化(显式赋值或默认赋值)
  5. 建议函数的声明与定义要分开来写,因为函数可以声明多次但只能定义一次,声明建议写在头文件中
  6. 形参名是可选的,但是不写形参的名字会使得我们无法在函数中使用这个形参且降低可读性,所以建议都写上形参,即使在声明中也一样
  7. 如前所述,名字有生命周期,函数中的局部名字会隐藏外部的名字,函数执行结束那些变量就会销毁
  8. 随着函数执行自动创建和销毁的称为自动对象。
  9. 若将局部变量用static创建,则得到局部静态对象,此时它只能在此作用域中使用但生命周期直到程序终结
  10. 函数声明也叫做函数原型,含有函数声明的头文件应被包含到定义函数的源文件中

6.2 参数传递

  1. 函数形参可以是引用类型,此时传入的实参称为引用传递或传引用调用,传引用形参是实参的别名,也就是函数内修改这个形参会影响外面的对应实参
  2. 传引用要比C风格的指针形参更加有效实用,建议使用引用来代替之前需要指针的形参
  1. 由于前面说到函数初始化形参是需要进行拷贝的,这个过程比较低效,所以建议使用引用来避免拷贝。又若需要避免函数对引用参数的修改,则使用常量引用来保证安全性,这样又方便又高效(最常见的用法是操作诸如比较两字符串长度的函数)
  2. 由于引用形参可以修改原值,所以可以用此方法来变相返回多个值
  3. const形参的调用范围要大于普通形参,但不好操作,普通形参不能传入常量实参,但更好操作
  4. 当函数不会修改传入的参数时,定义为常量引用是更好的习惯
  5. 函数的参数可以写为数组形式, 与写为指针形式是等价的
  6. 数组有三种常见的传参方法:用某个不会出现的元素标定数组尾(如用\0标定字符串尾),用标准库得到的begin和end指针标定范围,C风格的写法也即显式传入数组大小
  7. 传递数组的引用时,注意由于引用必须要有实体,所以需要保证输入的数组大小与形参指定的大小相同,如同传递多维数组时一样。
  8. main函数可以带有两个参数,argc和argv,其中argc是命令行调用此程序时附带传入的参数数量,argv是各个参数的字符串形式
  9. 要注意若调用为类似:prog -d -o a b,此时argc为5,实际参数只有4个,而argv有六个元素。这是由于argv的第一个元素固定为程序调用时所输入的程序名,最后一个元素固定为0
  10. 有几种方法来传入可变数量的实参:一种是当数量未知而类型固定时,使用C11标准库的initializer_list来作为形参,其使用类似列表,可用其size(),begin(),end()函数来遍历,实参输入时将对应的内容写在花括号中传入;另一种是用到varargs的C标准库功能,常在C风格代码见到,形参列表结尾写省略号“...”,表示忽视多余的实参

6.3 返回类型和return语句

  1. return会终止当前函数的执行并返回到调用此函数的地方,除void类型的函数外,每个return都需要有返回值。
  2. void类型的函数会自动在函数尾隐含补上return,但若不是void型,则要保证每条路径都要有返回值,很多编译器无法发现越过循环的return缺失(vs可以发现这个错误并以警告方式提示)
  1. 返回值的原理实际上用结尾的return的值初始化一个临时量作为结果,也是拷贝得到的,所以要谨记不要返回不可拷贝的局部变量,也不要返回对局部变量的引用或指针,例如不要返回局部数组的指针。
  2. C11规定可以使用花括号,利用vector类型来返回列表值
  3. main函数的返回值通常是给操作系统看的,0表示执行成功,其他值表示失败,具体意义要依据机器决定
  4. 调用了自身的函数称为递归函数,main函数无法递归调用自己
  5. 返回数组指针时,要注意保持好正确的写法:先看括号,从括号内往括号外看,然后数组的中括号对应的是前面紧接着的数组名,数组的具体元素类型要看数组前面的类型名,用括号来使星号和引用号与类型名相隔离(下面的例子中若去掉括号会变为拥有十个int*的指针数组,而现在这样是指向拥有10个int的数组的指针)
  1. 上面一条可以看到这样的func的声明会变得非常复杂,C11增加了一种更加清晰的声明方法称为尾置返回类型,方法是写一个返回类型为auto的函数,然后在声明后面用箭头号->指出真正的返回类型
  1. 当然也可以使用之前提到的decltype来简写各种类型

6.4 函数重载

  1. 函数名相同而形参不同的一系列函数称为重载函数,但形参不同是有一系列条件的
  2. 首先是形参的不同指的是类型的不同,形参名字是无意义的
  3. 然后若参数是指针或引用,则const可以区分出不同的函数,其余情况下顶层const无法和底层const区分开来
  1. const和非const直接存在一次类型转换,当参数出现多个匹配时,会按照匹配优先级进行选择
  2. 前面出现过的const_cast类型转换在重载中非常有效,主要用于先将函数主干用const写完,然后重载一个普通版本的函数,其中传入的参数都利用const_cast转换为const带给主干函数,运算完再cast后传出。这样既保证了安全性又满足了灵活性
  3. 小作用域中的同名函数会对大作用域中的函数进行隐藏而不是重载,所以需要重载时一定要将函数们写在同个作用域中
  4. 尽量不要在局部作用域中声明函数

6.5 特殊用途语言特性

  1. 默认实参只能是最末尾的一系列形参,且调用时也只能省略末尾的参数,不允许间断
  2. 默认实参声明后不能再修改,但是函数可以多次添加默认实参
  3. 尽管局部变量不能成为函数的默认实参,但是常量表达式可以,函数名字的计算会在函数调用时才进行
  4. 通过inline关键字可指定某函数为内联函数,这也编译器会将函数在调用点展开,节省函数调用时切换寄存器等等开销,使得效率变高一些
  5. 但是要注意:一,内联只是对编译器的一种请求,编译器是可以无视的;二,内联适合那些频繁调用且规模较小的函数,否则会适得其反;三,尽量不要在内联函数中使用递归,很多编译器不支持这样的操作(很高兴vs是支持递归内联函数的)
  1. 前面说到的constexpr函数是常量表达式,限制了返回类型和形参必须是字面值且函数体中只能有一个return。但实际上constexpr会隐式展开为内联函数,而且形参也可以不是常量,只是此时返回值也将不会是常量了
  2. 要注意内联函数和constexpr由于需要在调用处随时展开,所以需要多次定义,最方便的方法就是将他们的定义写在头文件中

6.6 函数匹配

  1. 第四节中说到的重载函数的判定问题,具体来说就是函数匹配问题,分为候选函数算则,可行函数选择和寻找最佳匹配三步
  2. 首先在所有函数中找到同名且可调用的函数,称为候选函数
  3. 然后在候选函数中选出符合目前提供的实参的函数,成为可行函数 可行函数需形参数量与实参相等(可利用默认实参)且类型符合(可转换来适应)
  4. 最后若有多个可行函数,则需要进行最佳匹配寻找,若找不到最佳匹配则报错“存在二义性”
  5. 最佳匹配实际上就是要找出有唯一一个函数,其至少一个实参匹配等级高与其余所有函数,且其他实参的匹配等级不低于其余的函数,也就是存在唯一函数匹配程度完全优于其他函数
  6. 其中匹配等级则分为五级:一,精确匹配,即完全相同或仅仅是数组变为指针类型和增减顶层const转换;二,进行了const转换;三,进行类型提升;四,进行类型匹配(如int转double);五,进行类类型转换
  7. 类型提升通常都是提升到int及以上类型,且所有类型匹配的级别都是一样的,int转unsigned int并不比int转double高级

6.7 函数指针

  1. 函数指针的写法比较简单,声明一个函数,然后将函数名改写为(*name)即可,要注意此处括号不可省略因为这会影响星号是与返回类型匹配还是与名称匹配
  2. 使用函数指针时,可以直接用名称使用,可以不需要解引用符,赋值函数指针也可以不需要对目标函数用取地址符
  3. 使用重载函数指针必须保证函数指针与目标重载函数精确匹配
  4. 函数指针的好处就是可以将函数作为形参来传入也可以作为返回值返回了,大大增加了灵活性
  5. 同样可以用auto和decltype来简写其类型

6.8 预处理器与调试

  1. 这部分是将第二章的预处理器部分和这一章6.5的调试部分合起来的
  2. 前面说到我们需要确保函数的定义相同且只发生一次,而inline函数等常常定义在头文件中,又头文件常常要被许多函数引用,那么我们就需要使用预处理器语句来防止头文件的重复引用造成数据的重复定义了
  3. 具体来说使用头文件保护符来解决,有些编译器(如vs)支持#progma once语句,但是更通用的方法是使用#ifndef NAME配合#endif预处理符,这对符号只看字面意思就能明白其作用机理了
  1. 然后assert预处理宏是需要依赖与assert.h这个C头文件的,适用于对一个我们可以明确预知的关键表达式进行求值检验,当检验结果为假时,程序输出信息并终止。这个宏应用于检验“绝对不能发生”的条件,写在将要运行的代码前
  1. assert宏依赖与一个叫NDEBUG的预处理变量的状态,这个变量默认没有定义,则assert会执行,若定义了这个变量则assert不会执行
  2. NDEBUG变量可以在命令行更改,配合这个变量和#ifdef等命令,我们可以更轻松地进行调试,将只想在调试阶段执行的代码写在def对中
  3. 为了方便调试,预处理器还定义了几个非常使用的常量,分别是:存放当前调试的函数名的_ _func_ _,存放调用的文件名的_ _FILE_ _,存放报错时当前行号的_ _LINE_ _,存放文件编译时间的_ _TIME_ _,存放文件编译日期的_ _DATE_ _
本文参与 腾讯云自媒体分享计划,分享自微信公众号。
原始发表:2020-01-08,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 未竟东方白 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档