第6章:控制流——《实践之路》笔记
聊聊这章都讲了啥
有很多有趣的问题
1.表达式的递归定义:为什么很多概念都是以递归的形式给出的?
递归有一个终结条件,定义最终会递归到一个不可分元素,最小表达式就是一个常量或变量
通常我们是用结构(迭代)的角度考虑问题的,把表达式看做一个个运算对象拼起来的,因为程序就是这么敲出来的哈哈
而迭代的定义通常会趋向于无限,我们可以指出最小的表达式,但无法指出一个表达式最大会是什么样
所以,用递归定义简洁描述了一个表达式是如何逐步分解至不可分的
2.一行代码中发生了什么?为什么分别定义表达式与语句?
求值(表达式完成)
副作用(赋值语句完成)
区分表达式与赋值,就区分了求值与副作用
3.表达式与语句的关系
赋值需要一个新值
求表达式,得到值,赋值保存值
4.把运算符归结到函数
运算符是内部实现的函数,运算对象自然就是实参
这么看来运算符像是自举的结果,定义上更加简洁了
内置的运算应该经过了优化,因为出现频繁,所以决定了算法的效率,应该并不只是自举
5.函数定义与调用发生了什么?
函数签名里说明了运算需要啥数据,巧妇难为无米之炊
调用时,把数据传入函数,需要有效区分传入的实参:位置参数,关键字参数
6.计算的顺序
先计算哪个运算符?优先级
都是同样的运算符?结合律
运算对象的值先求哪个?应用求值,正则求值
7.什么是变量的值模型,引用模型?
名字——地址——值
选两个打包起来
值模型:名字与地址打包
引用模型:地址与值打包
9.要求表达式的上下文,要求语句的上下文?
需要一个值
需要执行的步骤
10.为什么要关注类型?
类型指定操作的域
不同类型互操作:可能引起精度损失,或发生错误
11.goto的废弃?
goto+地址很难表达整体意图
12.goto的替代方案
异常处理也是一种语句跳转
13.函数能修改啥?不该修改啥?
参数是否修改?替代修改参数的方案?
函数作用域与其他作用域的相互可见性
14.迭代与递归的作用
程序规模不再局限于程序正文的线性长度
15.迭代器在各种语言中怎么实现?
编程约定,模仿缺失的特征,获得等价的功能
引言
顺序执行是命令式语言的核心
函数式语言强调表达式的求值
逻辑式语言藏起控制流
6.1表达式求值
表达式递归定义:
1.简单对象(内置类型)、自定义类型实例(支持运算符)
2.运算符与函数应用于运算对象或参数
运算对象或参数也是表达式
运算符:可看做简单形式的内部函数
运算对象:运算符的参数
就此把运算符与运算对象归结到了函数的讨论范畴
函数调用
函数名(参数列表)
前缀,中缀,后缀
三元中缀运算符
if then else
优先级与结合性
决定了求值顺序(还有之后提到的参数求值顺序)
c语言优先级丰富
类型强制,函数调用,数组下标,记录域选取
6.1.1优先级与结合性
优先级
括号 算数 比较 逻辑
结合律
左至右
赋值 右至左
6.1.2赋值
命令式语言的计算:通过修改内存中的变量值
修改的基本操作:赋值
赋值的参数:值,变量引用
通过修改变量值,影响后续计算
所以赋值是最基本的副作用
命令式语言严格区分 表达式与语句
表达式:产生值
语句:产生副作用
纯函数式:没有副作用
表达式是引用透明的
引用透明:幂等,无副作用
没有副作用,意味着耦合度低,不易出错
变量值模型:变量是值的容器,只能改变值:内存中数据
变量引用模型:变量是值的命名引用,改变引用没有改变值
左表达式:计算出地址
右表达式:计算出值
引用模型中需要对变量做解引用取得值
通常实现了隐式解引用来取得值
两种模型的区别
被引用的值在原位改变
引用相同值的不同对象
引用不同对象的值相同
效率
访问时的间接运算
内存效率:不变对象的多个 副本(本书中用副本表示具体的值)
名字——地址——值
Java内部类型:值模型
用户类型:引用模型
要求传递内部类型:自动装箱拆箱
语言正交性
各个特征正交,任意组合使用,组合中保持意义
要求表达式与要求语句的上下文
c允许表达式中出现赋值
赋值语句返回值(赋值成功?)
增加了正交性:表达式中运算对象的副作用——赋值,返回值——成功标志
c与c++提供了到布尔类型的自动强制:类型安全弱化
组合赋值:减少冗余的地址计算
!注意表达式地址计算的副作用
增量减量运算符
重载后用于下标与指针的地址运算
前缀:+=的语法糖——赋值的优先级高
后缀:减少临时变量
多路赋值:元组解包
消除了函数的非正交性
6.1.3初始化
赋值语句:修改变量值
初始化:指定初始值
初始值的用处:
子程序中:局部静态变量需要初始值,然后使用
静态分配的变量:在声明上下文中指定初始值,由编译器放入全局内存,避免了赋值开销
避免使用未初始化的值造成错误
对内置类型:系统提供相应字面量
更正交——复合类型,自定义类型:需要聚集值——结构化的值
js:对象字面量
初始化的时期
初始化只对静态分配的变量节约时间
堆栈中变量只能运行时初始化
未初始化问题对应另一种情况:破坏了原有值而之后未分配有效值
如:悬空引用
语言可在声明时赋予默认值:内存填零
动态检查未初始化:动态语义错误
语义检查的优势:区分未初始化值与有效值(默认初始化值为有效值)
效率:需要维护更新每个变量的使用情况
java:定义性赋值
所有之前可达路径中都完成了变量的赋值(初始化)
自定义类型初始化与赋值:
默认值初始化:可能在之后改变对象大小
有效值初始化:避免释放默认值分配的空间
6.1.4表达式中的顺序问题=运算符运算顺序+运算对象求值顺序
优先级与结合性定义了运算符的顺序
未定义运算对象的求值顺序(参数列表的求值顺序)
求值顺序的重要性:
副作用:引入歧义
代码改进:公共子表达式
java:
更多运行时开销(编译器试图减少)
更清晰的语义
更高的可靠性
对方法调用使用动态约束,参数检查?
数组下标越界
类型崩溃?
其他语义错误的运行时检查
基于运算定律的表达式整理
精度变化,溢出,运算符副作用等不安全因素引入
6.1.5短路求值
表达式
不完全求值
新的求值顺序
代码改进与提高可读写(读到逻辑确定即可)
短路求值改变了布尔表达式的语义(表达式:求出所有运算对象,按优先级执行运算符)
按位运算——非短路求值
用于避免下标越界,避免除0(放在布尔表达式前部)
注意布尔表达式产生的副作用
延迟,被动求值?
在用于确定控制流时,布尔表达式转化为跳转表
6.2结构化与非结构化的流程
6.2.1goto的结构化替代品
结构化程序设计:自顶向下设计(逐步精化),代码模块化(可见性控制:导入导出), 结构化类型(集合,指针数组),描述性的变量常量名
##函数参数与返回值:一种可见性控制?其他作用域内变量(全局,外层函数的局部变量:python显示导入)。
##其他可见性控制?各种导入导出动作的联系与差别
return:跳至子程序末尾
break:跳出单次循环
异常:特定外围上下文处
回卷:修复运行栈上的子程序调用信息
包括:释放并跳出帧栈,信息管理(恢复寄存器)
错误与异常
多层返回假定了被调用方知道传递给调用方的信息,即返回合适的值
组件未能完成所执行功能的规范——退出至能恢复执行的地方。退出条件:异常
结构化的异常处理机制:在可能出现异常的地方放置处理器(一个代码块)
处理器完成修复工作
多层返回与异常的工作有很多相似性:
控制转移:内层嵌套上下文——>特定外层上下文
运行栈的回卷
区别:异常处理内层上下文无法完成工作的情况
简单异常处理:辅助变量
6.2.2继续
代码地址+引用环境
进入代码地址——恢复环境
一种抽象:可以继续的执行上下文
6.3顺序执行
语句顺序执行:也是一种优先级
复合语句:加了分隔符的语句列表
块:声明+复合语句
无副作用函数:幂等:透明:同参数重复调用 返回相同值
Ada折中:函数修改全局变量与静态变量,不允许修改参数
6.4选择
6.4.1短路条件
else歧义消除:最近匹配
elif:保持平行结构
计算布尔表达式不是为了保存值,而是生成转跳码
未显式存入寄存器,而是隐式用于流程控制
机器提供条件分支指令
6.4.2case/ switch
整数表达式与编译时常数对比
生成转跳表
转跳表搜索:顺序,散列,折半
查找策略:取决于标号的数目,范围
处理未出现的标号 default
落入方式:连续落入,或直接中断
6.5迭代
迭代与递归使程序规模不局限于程序正文的线性长度
迭代:用循环实现,执行为了副作用——修改变量值
迭代次数的确定方式:枚举控制,逻辑控制:更明确的结构语义
6.5.1枚举控制
循环下标:初始化,范围,步进量
各分量是否要求编译时确定
是否可以在循环中修改
预先得到迭代次数,存于寄存器
下标的作用域
##为何要求编译时确定?效率?
引入循环头部——进一步结构化,集中循环控制代码,解耦合
是否限定循环体中修改循环变量
在集合中迭代
枚举类型:要求枚举(作为特定类型),允许枚举(支持枚举的类型)
6.5.2组合循环:循环嵌套
各层的作用域与可见性?
语言结构是否引入作用域,以及作用域间的可见性(特别的,全局变量)
6.5.3迭代器
实现迭代器对象接口
生成器:迭代器的更一般形式,将枚举与回溯 组合
Python生成器——真迭代器
真迭代器:容器抽象提供 枚举自己元素的迭代器
#真迭代器:枚举值的独立上下文?
迭代器在前面结束的地方继续
像是控制进程:有自己的程序计数器,降低枚举元素的代码 与 使用元素代码的耦合
Python中range迭代器是预定义的?
命令式语言中
实现迭代器设计for循环的特殊形式
与一种在循环中枚举值的机制
这两个概念可以分开
C++,java提供了类似python的枚举控制循环
没有yield语句,没有用于枚举值的独立的上下文(类似线程)
迭代器只是个对象,提供方法:用于初始化,生成下个下标值,检测结束
在不同的调用之间,保持迭代器状态(作为数据成员)
真迭代器:显式的数据结构来维护中间状态
迭代器对象:中间状态维护在程序计数器与局部变量(构成可重新唤醒的执行上下文)?具体?
c++将迭代器看做指针,自增,间接引用,end:特殊迭代器
运算符重载,变量值模型,显式废料收集,容器声明为泛型,针对特定元素实例化
scheme:循环体写成函数,循环下标作为参数,函数作为参数传递为迭代器
6.5.5逻辑控制循环
何处检查结束条件
先检测,后检测,中间检测
6.6递归
允许函数调用自身
迭代与递归算法可以相互转换
迭代修改值
递归不修改值(值作为参数继续传递?)
6.6.1迭代与递归
尾递归:调用后没有其他计算函数,不需要动态分配栈空间,重复使用当前计算空间
非尾递归——>转化为尾递归,图解?
构造尾递归树?
6.6.2应用序求值与正则序求值
应用序求值:调用前求值
正则序求值:实际需要值的时候求值(函数执行,计算参数)
参数在传递给调用函数时未完成求值
传递未求值实参——>实际需要值——>求值,运行函数
正则序求值:出现在短路求值,按名调用参数中
应用序求值:清晰高效
正则序求值:产生更快的代码,参数值不需要
惰性求值:惰性数据结构,用于组合搜索问题
函数声明,传参,可调用对象,调用操作
6.7非确定性
非确定性:随意选择顺序依然保持正确
形式化意义上的公平
编程约定,模仿缺失的特征,获得等价的功能