函数式编程
大约 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指针
或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引用
- 旧版C++条件
- 举例
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的取地址
- Lambda特点是根据传入参数不同自动生成不同的函数原型
引用 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的库文件中预编译的函数时,可能找不到(C语言不允许函数重载,并没有名称修饰)
- 解决方案:用函数原型来指出要使用的约定:
extern "C" void spiff(int);
,使用C语言链接性 查找函数名extern void spoff(int);
,默认使用C++语言链接性 查找函数名extern "C++" void spoff(int);
,显示使用C++语言链接性 查找函数名
- 链接程序可能无法链接不同编译器所编译的库
- 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 *arr
和int 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为数组、指针、结构,则
*
运算符可能会出错
- 如果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),错误
- 有最佳可可行函数
- 多个参数的情况
- 情况非常复杂。编译器必须考虑所有参数的匹配情况
- 若一个函数要比其他函数都何时,其所有参数的匹配程度都必须不比其他函数差,同时至少有一个参数的匹配程度比其他函数都高
- 首先
完全匹配允许的无关紧要转换表
从实参 | 到形参 |
---|---|
Type | Type & |
Type & | Type |
Type [] | * Type |
Type (argument-list) | Type (*) (argument-list) |
Type | const Type |
Type | volatile Type |
Type * | const Type |
Type * | volatile Type * |