跳至主要內容

函数式编程

LincZero大约 16 分钟

函数式编程

自定义函数

简概

  • 使用函数要素
    • 提供函数定义 + 提供函数原型 + 调用函数
    • 函数原型可隐藏于头文件中
  • 定义和使用的位置
    • 总结就是:函数定义在前还是在后都无所谓(解释型语言必须放在前面),但函数原型声明需要放在使用前(解释型语言不需要声明函数原型)
  • 函数原型的作用
    • 作用:描述了编译器的接口
      • 编译器正确处理函数返回值(知道如何从寄存器或内存中检索多少字节以及如何解释它们)
      • 编译器检查参数数目是否正确
      • 编译器检查参数类型是否正确,如果不匹配则尝试自动类型转换
    • 补充
      • 原型的参数列表可以包括也可以不包括变量名,而且其变量名相当于占位符,可以与函数定义的变量名不同(即可用于备注说明)
      • ANSI C中,原型是可选的。但在C++中,原型是必须的
      • 在编译阶段进行的原型化被称为静态类型检查static type checking),可捕获去躲在运行阶段难以捕获的错误
  • 返回值的底层原理
    • 做法
      • 函数通过将返回值赋值到指定的CPU寄存器或内存单元(主存)中将其返回
      • 随后调用程序将查看该内存单元,并以函数原型所声明的类型返回出去
    • 原因
      • 为什么不能放在原来的内存?因为函数周期结束后函数栈会连同内部的数据一起被销毁(局部变量的生命周期的原理)
      • 为什么返回值不能是数组?因为怕数组太大?

使用

使用

  • 通用

    void functionName(parameterList)
    {
        statement(s)
        return;
    }
    // 或
    typeName  functionName(parameterList)
    {
        statement(s)
        return value;
    }
    
  • 举例

    include <iostream>
    using namespace std;
    
    int main()
    {
        void simon(int);
        simon(3);
        return 0;
    }
    void simon(int n)
    {
        cout << n << endl;
    }
    

不同的类型

  • 不接受参数
    • 显式声明:int rand(void)
    • 隐式声明:int rand()
  • 函数返回值
    • 无返回值:void functionName(parameterList)
    • 有返回值:typeName functionName(parameterList)
    • 但注意返回值类型不能是数组(可以是整型、浮点数、指针、甚至结构和对象)(故可以通过将数组作为结构或对象组成部分来返回)

递归思想

递归:C++函数可以自己调用自己,形成调用链(C++中main()不能调用自己而C可以)

递归中的一些情况:每次递归会有一个内存单元,同一个变量名在不同的内存单元中可能不同

尾调用:有的编译器/解释器还会进行一个尾调用优化,以优化栈内存,防止栈溢出

C++函数新特性(与C不同)

新特性目的
内联函数提高效率
按引用传递变量简化指针表示、提高传参效率和返回效率、适用于结构/类
默认的参数值方便
函数重载(多态)允许有多个同名函数
模板函数函数重载的更简便版本

内联函数

  • 底层原理:操作系统将指令载入到内存中,每条指令都有特定的内存地址。计算机执行这些指令,有时跳过一些指令,向前或向后跳到特定地址

    • 非内联函数:函数调用时会跳到函数的地址,并在函数结束时返回 函数调用后立即存储该指令的内存地址,并将函数参数赋值到堆栈,调到标记函数起点的内存单元,执行函数(有时还传入返回值),然后调到地址被保存的指令处 来回跳转意味着需要一定的开销
    • 内敛函数:使用响应的函数代码替换函数调用。使用的是预处理机制
  • 比较

    • 内联函数:稍微快,但占用更多内存(每调用一次函数就生成一个函数副本)
    • Q:如果是这个原因的话,默认构造函数和空析构函数写成内联函数会不会比在cpp中实现更好?(虽说直接不写最好)
  • 选择

    • 执行函数代码时间比处理函数调用机制的时间长,则节省时间短,无必要
  • 使用

    • 在函数声明和函数定义前加上关键字inline

    • 一般做法是将定义凡在原本提供原型的地方(可以是在函数头)

      inline double square(double x) {return x*x;}
      double  = square(5.0);
      
  • 注意

    • 当在类中定义时,可以不加关键字inline
    • 在类中声明并定义的函数,编译器都会视作内联函数

引用变量——按引用传递变量

基本使用

  • 引用变量

    • 是已定义变量的别名。可以交替使用原名和别名来表示变量
  • 使用

    • 举例:int rats; int & rodents = rats;
  • 初始化注意

    • 注意在调用时必须进行初始化
    • 而且对引用变量来说,赋值相当于给原变量赋值,这也是为什么引用变量更像是const的指针
  • 本质

    • 看上去是伪装的const指针,或者const指针的语法糖,而并非指针 int & rodents = rats;int * const pr = &rats;等价
  • 使用原因

    • 修改调用函数中的数据对象
    • 提高运行速度
  • 何时使用

    • 不作修改时
      • 数组:const指针(指针是唯一选择)
      • 数据对象:较小时使用按值传递。较大时使用const指针const引用
      • 类对象:const引用(而不是指针,类设计的语义常常要求使用引用)
    • 需作修改时
      • 内置数据类型:指针
      • 数组:只能使用指针
      • 结构:引用或指针
      • 类对象:引用(而不是指针,类设计的语义常常要求使用引用)

左值引用(rvalue reference

  • 使用
    • 举例:double && rref = std::sqrt(36.00)
  • 特点
    • 这种引用可以指向右值,而普通引用只能指向左值
    • 右值引用可以用来实现移动语义

引用 x 函数

  • 用处(作函数引用值)
    • 可以用来传递函数参数和作为返回值(本质是传递指针参数)
    • 举例:swapr(&arg1, &arg2){}函数原型,在被调用时,看上去与普通调用一样==(只能通过原型或函数定义才能知道是按引用传递)==
      • 普通调用:void swapr(int a, int b){};swapr(arg1, arg2);
      • 按引用传递:void swapr(int & a, int & b){};swapr(arg1, arg2);
  • 临时变量
    • 描述
      • 如果实参与引用参数不匹配,C++将生成临时变量
    • 生成临时变量的条件
      • 旧版C++条件
        • 实参类型正确,但不是左值
        • 实参类型不正确,但可以转换为正确的类型
      • 新版C++附加条件
        • 参数为const引用
    • 举例
      • void swapr(int & a, int & b){}; swapr(3L, 5L);
    • 什么时候用
      • 意图是修改作为参数传递的变量,则不用,创建临时变量会阻止这种意图的实现
  • 函数参数应该尽可能地使用const的理由
    • 避免无意中修改数据的编程错误
    • 能处理const和非const实参,否则只能接受非const数据
    • 使用const引用,使函数能够正确生成并使用临时变量
  • Lambda补充
    • Lambda特点是根据传入参数不同自动生成不同的函数原型
      • [=,&a]时相当于函数原型为fn(int &a);
      • [=,a]时相当于函数原型为fn(int a);
    • 此时&被赋予了新的意义,被用于指定函数原型,传入参数为&a时不表示传入a的取地址

引用 x 结构

  • 结构作参
    • 写法:typeName fnName(const 结构名 & 结构变量);
    • 好处:本质是指针传值,性能高
  • 结构作返回值
    • 写法:结构名 & fnName(argument);
    • 好处1:可以写成fn1(fn2(结构变量)),等价于fn2(结构变量); fn1(结构变量),更简便
    • 好处2:可以写成fn(结构变量1) = 结构变量2,等价于fn(结构变量1); 结构变量1 = 结构变量2
  • 注意要点
    • 应该避免返回函数终止时不再存在的内存单元引用

引用 x 对象、继承

  • 引用类的特性
    • 基类引用可以指向派生类对象,而无需进行强制类型转换。但非引用则不行
    • 举例:ofstream类继承了ostream类,前者是派生类,而后者是基类。参数类型为ostream &的函数还可以接受ofstream对象
    • ostream &参数可接受其派生类

捋一下 &*

  • 区别
    • 左侧的&在这里不是地址运算符,而是类型标识符的一部分,就像char*是表示指向char的指针一样
    • 即等号左侧的符号和右侧的符号的性质是不一样的!!!必须要捋清这一点
  • 引用
    • int rats; int & rodents = rats;看作(int &) (rodents = rats) 且 (*rodents = *rats)
    • rodents = rats = 值*rodents = *rats = 地址
  • 指针
    • int * pi_e = &i_e看作(int * 类型) (pi_e = &i_e)而非int (*pi_e) = (&i_e)
    • pi_e = &i_e = 地址*pi_e = *&i_e = i_e = 值
  • 传参
    • ......

默认的参数值

  • 使用:通过函数原型
    • 举例:char * left(const char*str, int n = 1);

函数重载(多态)

术语多态指是有多种形式,函数多态(函数重载)可以使用多个同名函数。他们使用参数列表(也叫函数特征标function signature))区分

  • 使用:编写多个原型与多个定义
  • 使用场景:不要滥用,仅当函数基本执行相同任务但使用不同形式的数据时才应该使用
  • 底层原理——名称修饰
    • C++如何跟踪每一个重载函数?
      • C++编译器对函数进行名称修饰name decoration)或名称校正name mangling
      • 它根据函数原型中指定的形参类型对每个函数名进行加密
      • 比如long MyFunctionFoo(int, float);的函数名可能被修饰为?MyFunctionFoo@@YAXH
    • 名称修饰所带来的一些影响
      • 链接程序可能无法链接不同编译器所编译的库
        • 解决方案:见模块系统一章
      • C++使用C的库文件中预编译的函数时,可能找不到(C语言不允许函数重载,并没有名称修饰)
        • 解决方案:用函数原型来指出要使用的约定:
        • extern "C" void spiff(int);,使用C语言链接性 查找函数名
        • extern void spoff(int);,默认使用C++语言链接性 查找函数名
        • extern "C++" void spoff(int);,显示使用C++语言链接性 查找函数名
  • 其他注意项
    • 一些看起来不同的特征标不能共存,比如(double x)(double &x),编译器无法确定究竟使用哪个原型
    • 若有两个原型:const指针与常规指针,则编译器根据实参是否为const决定使用哪个原型(const变量作为非const的参数)

【功能扩展】函数

【功能扩展】函数参 x 指针和指针(作参)

  • 写法(数组作参和指针作参)
    • 数组作参:例如int sum_arr(int arr[], int n){}
    • 指针作参:例如int sum_arr(int *arr, int n){}
    • 字符串作参:例如int sum_arr(char * str, char ch)
    • 多维数组作参:例如int sum(int ar2[][4], int size)(表示只接受4列的数据,不然4可省略)
    • 多维指针作参:例如int sum(int (*ar2)[4], int size)(表示只接受4列的数据,不然4可省略)
    • 字符串返回值:例如char * buildstr(char c, int n)
  • 函数原型中(可省略函数名),这些写法是等价的:
    • const double * f1(const double ar[], int n)
    • const double * f2(const double [], int n)
    • const double * f3(const double *, int n)
  • 其他数组代替品
    • string对象:与结构更相似
    • array对象:例如void show(std::array<double, 整型常量> da)
  • 区别(数组作参和指针作参)
    • 注意:int arr[]int *arr完全等价,前者并没有拷贝整个数组(开销大),两者都是赋值地址
    • 注意:当且仅当用于函数头或函数原型中,int *arrint arr[]的含义才是相同的(或者说后者的含义被改为了前者) *(arr+i) == arr[i],但int * pn != int arr[]

注意点

  • 通常数组作参时还要传递第二个参数获知数组的元素数量

  • 或者也可以指定元素区间,分别传递数组头和数组尾这两个指针

  • 数组/指针与普通参数的区别:

    • 普通参数按值传递数据,函数使用数据的副本。但接受数组名的函数将使用原始数据
    • 数据保护:但有时应防止函数无意中修改数组内容,可在声明形参时使用关键字const 如:void show_array(const double ar[], int n);

【功能扩展】函数参 x 结构体(作参)

比数组更简单,对象被视作一个整体,可以按值传递

  • 结构传值
    • 可以按值传递(但需要内存大,速度慢,适用于结构较小时使用,使用句点成员运算符)
      • struct structName{...}; structName val={...}; typeName fn(structName val) {}; fn(val)
    • 可以传递结构的地址(C程序员常用的方法,使用间接成员运算符->使用成员)
      • struct structName{...}; structName val={...}; typeName fn(structName *val) {}; fn(&val)
    • 可以按引用传递==(C++新增方法)==

【功能扩展】函数 x 指针(函数指针)

  • 函数地址:函数名表示函数地址
  • 声明函数指针:类似于声明原型,比如:
    • 声明函数指针:double (*pf)(int);
    • 声明函数原型:double pam(int);
  • 使用函数指针调用函数
    • functionName改为*functionPoint即可(或使用functionPoint也可以)
    • pf(*pf)等价,正如函数名(地址)与函数本身等价一样

【功能扩展】函数 x 模板 = 模板函数

基本使用

  • 简概

    • 函数模板是通用的函数描述,即使用泛型来定义函数 模板允许以泛型(而不是具体类型)的方式类编写程序,因此有时也被称为通用编程 由于类型是用参数表示的,因此有时也被称为参数化类型parameterized types
  • 底层原理

    • 函数模板之所以是函数模板,因为它本身并不产生函数定义,只是一个生成函数定义的方案
    • 只有在使用时才生成(隐式或显式实例化)函数定义的模板实例instantiation
    • 这也是为什么函数模板的定义可以放在头文件中,而普通的函数定义不行 (普通函数在头文件会导致多个文件定义同一个函数(预处理机制),不符合单一定义原则)
  • 使用

    • 举例

      template <typename T>
      vpid Swap(T &a, T &b);
      // ...
      template <typename AnyType> // 指出建立一个模板,并将类型命名为AnyType
      void Swap (AnyType &a, AnyType &b)
      {/**/}
      
    • 使用补充:typename关键字是C++98添加的,在此之前使用关键字class来创建模板(两者等价,只是后者的单词不直观) 为书写方便,通常将T而不是AnyType用作类型参数

  • 好处

    • 使生成多个函数定义更简单、更可靠
  • 使用场景

    • 需要对多个不同类型使用同一种算法的函数时,可使用模板
  • 重载的模板

  • 即模板函数可以像普通函数一样,也定义多个模板(使用不同的参数数量来作为函数特征标

  • 模板的局限性

    • 局限性:无法兼顾所有类型,比如
      • 如果T为结构,不能进行>运算符
      • 如果T为数组名,则>运算符比较的是数组的地址,这可能不是函数设计的本意
      • 如果T为数组、指针、结构,则*运算符可能会出错
    • 解决方案(两种)
      • 重载运算符,比如重载+,以便能够将其用于特定的结构或类
      • 为特定类型提供具体化的模板定义

具体化

第三代具体化

  • 使用:
    • template <> void Swap<job>(job &, job &);template <> void Swap(job &, job &);<job>可有可无)

显式实例化和隐式实例化

  • 底层原理:详见模板函数的底层原理
  • 使用:
    • template void Swap<int>(int &, int&)
    • Swap<job>(job1, job2)(也可以在调用时显式实例化)

【专题】普通函数、函数重载、模板函数、具体化(优先级)

  • 各种函数(使用总结)

    • 种类:使用对于给定的函数名,可以有非模板函数模板函数、和显式具体化模板、以及它们的重载版本
    • 写法区别:
      • 非模板函数:void Swap(job &, job&);
      • 显式具体化模板:template <> void Swap<job>(job &, job &);template <> void Swap(job &, job &);
      • 显式实例化模板:template void Swap<job>(job &, job&);Swap<job>(job1, job2);(调用时)
      • 模板函数(隐式实例化):template <typename T> \n void Swap (T &, T &)(由模板自动生成函数定义)
  • 比较(比较总结)

    • 具体化本质
      • 两个问题
        • 实例化和具体化区别
        • 既然普通函数的优先级>模板函数,那为什么还要具体化,而不用普通函数来表示
      • 回答
        • 具体化应该依然是模板,而只是比较具体的模板而已,而并非实例
        • 所以具体化的定义可能是可以放在头文件的,而普通函数定义可能无法放在头文件
  • 优先级(优先级总结)——编译器如何选择使用哪个函数版本

    • 重载解析(底层原理):编译器选择函数版本的过程称为重载解析overloading resolution
  • 重载解析过程

    • 首先
      • 创建候选函数列表
      • 使用候选函数列表创建可行函数列表(数目正确、参数可转换匹配)
      • 确定是否有最佳的可行函数,如果没有则报错,如果有则选用
    • 第三部中判断最佳可行函数(优先级如下)
      • 完全匹配(有些无关紧要的匹配也视为完全匹配,参见下表)
      • 提升转换
      • 标准转换
      • 用户定义的转换(如类声明中定义的转换)
    • 如果有多个完全匹配的
      • 有最佳可可行函数
        • 非const指针和引用优先与非const指针和引用参数参数匹配
        • 非模板函数 > 显式具体化的模板函数 > 普通模板函数
      • 没有最佳可行函数
        • 若以上两点均不符合,则产生二义性(ambiguous),错误
    • 多个参数的情况
      • 情况非常复杂。编译器必须考虑所有参数的匹配情况
      • 若一个函数要比其他函数都何时,其所有参数的匹配程度都必须不比其他函数差,同时至少有一个参数的匹配程度比其他函数都高

完全匹配允许的无关紧要转换表

从实参到形参
TypeType &
Type &Type
Type []* Type
Type (argument-list)Type (*) (argument-list)
Typeconst Type
Typevolatile Type
Type *const Type
Type *volatile Type *