Programming Language Pragmatics 习题解答七(类型系统)、八(子程序和控制抽象)

第七章介绍了类型系统,包括内部类型(record,array,recurision,file)、可自定义类型、类型等价性/类型相容/类型推理的规则,类型的转换/强制/非变换类型转换,类型等价(基于名字和基于结构),存储流失,悬空引用,废料回收。这些机制大多数都是Pascal、Modula、Algol、ML、Ada这些语言中提出的.
有关于静态/动态,强类型/弱类型的区别也是很重要的(见下图)。
"计算机科学中的大多数问题都可以通过增加一层间接性来解决。"第七章也介绍了很有意思的tombstones和key/lock用来解决悬空引用问题,对于引用计数、标记清除和分代算法也进行了比较深入的讨论。
第八章则有关子程序和控制抽象,包括调用序列(静态链指针、动态链指针、区头向量)、参数传递模型、换名调用、Thunk、异常处理和协程。
可以看到这两章也出现了很多面试中常见的问题,比如GC、Routine、Inline/#define区别、volatile等等,属于是比较重要的两章。

这个图是好的

文章目录

  1. 类型在程序设计中起着什么作用?
    类型为许多操作提供隐藏的上下文环境,使程序员不必显式地描述这种环境。类型限制合法程序中可以执行地操作集合。

  2. 什么是类型系统?
    (1)包括一种定义类型并将它们与特定的语言结构相关联的机制以及(2)一集有关类型等价、类型相容和类型推理的规则。

  3. 类型等价和类型相容之间的不同在哪里?
    类型等价规定两个值的类型何时相同,类型相容确定特定类型的值是否可以用在特定的上下文环境中。类型推理基于表达式的各组分类型或者外围环境,确定表达式的类型。

  4. 什么是类型推理?
    类型推理基于表达式的各组分类型或者外围环境,确定表达式的类型。

  5. 什么是强类型?什么是静态类型?
    文中提到:如果一种语言可以贯彻一种规则,禁止把任何操作应用到不该应用的对象上,这种类型就是强类型的。如果一种语言是强类型,并且所有的类型检查都能在编译时执行,那它就称为静态类型。
    这里的定义不太严谨,下面更加形式化一点:

作者:rainoftime
链接:https://www.zhihu.com/question/19918532/answer/21647195
来源:知乎
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
1.先定义一些基础概念
Program Errorstrapped errors。
导致程序终止执行,如除0,Java中数组越界访问
untrapped errors。
出错后继续执行,但可能出现任意行为。如C里的缓冲区溢出、Jump到错误地址
Forbidden Behaviours
语言设计时,可以定义一组forbidden behaviors. 它必须包括所有untrapped errors, 但可能包含trapped errors.
Well behaved、ill behavedwell behaved:
如果程序执行不可能出现forbidden behaviors, 则为well behaved。
ill behaved:
否则为ill behaved…
2.有了上面的概念,再讨论强、弱类型,静态、动态类型
强、弱类型强类型
strongly typed: 如果一种语言的所有程序都是well behaved——即不可能出现forbidden behaviors,则该语言为strongly typed。
弱类型
weakly typed:
否则为weakly typed。
比如C语言的缓冲区溢出,属于trapped errors,即属于forbidden behaviors…故C是弱类型
前面的人也说了,弱类型语言,类型检查更不严格,如偏向于容忍隐式类型转换。譬如说C语言的int可以变成double。 这样的结果是:容易产生forbidden behaviours,所以是弱类型的动态、静态类型
静态类型
statically: 如果在编译时拒绝ill behaved程序,则是statically typed;
动态类型
dynamiclly: 如果在运行时拒绝ill behaviors, 则是dynamiclly typed。

  1. 请举出两种语言,是强类型并且是动态类型的
    pytho、scheme

  2. 什么是类型冲突?

  3. 声明和定义的区别?
    声明引入了一个名字,并且指明了其作用域,定义则描述了名字所约束的类型或者对象。

  4. 请讨论有关于类型的指称式观点、构造式观点和基于抽象的观点之间的差异。
    从指称的观点看一个类型就是一个集值。从构造的观点来看,类型就是一组内置类型本身或者其复合。从抽象的观点看,一个类型就是一个界面,由一组定义良好并且有着相互协调的语义的操作组成。从指称语义学来看,一集值也称为一个论域,类型就是论域,在指称语义学里表达式的意义就是表达式的类型所对应的论域里的一个值。

  5. 离散类型和标量类型的不同在哪?
    整数、布尔和符号属于离散类型。其论域是可数的。
    离散类型、有理数类型、实数和复数类型统称为标量类型,标量类型有时候被称为简单类型。

  6. 请给出两个缺少布尔类型的语言的例子,它们用什么替代?
    C和Icon

  7. 枚举类型在哪些地方优于一组命名常量?子类型在哪些方面优于其基类型?字符串在哪些地方优于字符的数组?
    枚举类型是排序的,可以合法的做比较操作,存在机制确定前驱和后继,并且与整数类型不同,语义更清晰。
    子界类型和枚举类型都由Pascal引入,子类型可以作为程序的文档,可以用比任意整数更少的二进制位去表示子界。
    字符串可以定义一些数组不能使用的操作。

  8. 语言特性是正交的意味着什么?
    Pascal类型系统比Fortran更加正交,它允许从任何离散类型和成员类型出发构造数组,Fortran只允许用整数作为下标,以标量为元素。类型正交性意味着有能力描述任意复合类型的文字量值。(就是print anytype)

  9. 类型的名字等价和结构等价两者各自的比较优势?请举出两种不同方式的各三种语言。请说明严格的和宽松的名字等价之间的不同。
    结构等价是比较低级的,由实现决定的方式,问题在于区分名字不同但是结构相同的方式。名字等价能够区分这种不同。
    名字等价:Go(Go实际上是严格的)
    结构等价:Modula-3
    严格的名字等价:认为别名类型互不相同。
    宽松的名字等价:认为别名类型都相同。

  10. 类型变换、类型强制和非变换类型转换的不同?
    类型变换的三种情况:
    1.结构等价,名字不等价,不需要执行代码,只需要逻辑转换
    2.不同的值集,具有相同的表示方式,需要执行代码进行检查和转换
    3.具有不同的底层表示方式,但是可以定义某种对应关系,处理器完成变换
    非变换的类型转换:改变类型但是并不修改二进制位
    类型强制:无论在什么地方,只要语言允许将一个类型的值用到期望另一个类型的上下文中,语言实现都必须执行一个到所期望类型的自动隐式变换。

  11. 请解释ML的类型推理如何很自然的导致了多态?
    参考以前的ML相关文章。简单来说,类型推理不需要确定所有使用的类型,剩下的不确定部分就是多态的。

  12. 什么是合一,在ML中扮演了什么角色?
    合一是ML为类型提供的约束,所有约束都可以看作合一

17.请解释tuple和record的不同,ML的record和Algol中的记录有什么不同?
tuple和record区别在于类型相同与否
在于记录的顺序和内存布局是否一致
record 关键字定义一个引用类型,用来提供用于封装数据的内置功能。

  1. 空洞是什么?为什么会出现空洞?
    空洞就是内存布局引起的内存空间浪费。

  2. with语句的作用是什么?为什么C不需要这种结构?
    with引入一个嵌套的作用域,在其中record的一部分是打开的。
    Pascal中的with只能打开两个同样类型的record中的一个record

  3. 为什么将变体union和结构联合到一起很有意义?为什么不把它们当作独立的机制?
    union和struct联合到一起可以方便的使用同样的域名访问,

  4. 变体引发了什么类型安全问题?如何解决?
    union(或者说equivalence)分为带区分的联合和无区分的联合,可以用来解决类型安全问题。

  5. 什么是数组切片?数组切片在哪些地方有用?
    slice python、Go、C#都提供了slice的机制

  6. 数组什么时候可以在栈上分配?什么时候必须在堆中分配?
    1.全局生存期,静态维数约束
    2.局部生存期,静态约束
    3.局部生存期,加工时约束
    4.任意生存期,加工时约束
    5.任意生存期,动态约束

  7. 数组元素位置的计算哪些可以在编译时完成,哪些可以在运行时完成?
    当数组是一个全局变量,静态已知,那么A的地址也是静态已知的。
    如果数组是一个局部变量,那么其地址就可以分解为一个静态的偏移量加上运行时帧指针的内容。

  8. 什么是内情向量?
    内情向量是保存数组的维下届和各维大小的运行时描述字。

  9. 对于指针和采用变量的引用模型的语言里自然出现的递归类型做一些比较。
    像Lisp、ML、Clu、Java中的引用模型,每个变量都是引用的,所以可以用同名来定义递归类型;C、Pascal、Ada中必须使用引用来定义递归类型。

  10. 指针和地址有什么不同?
    指针是一个高级概念,就是对于对象的引用,地址是一个低级概念,表示内存单元的位置。
    指针通常使用地址实现,但是在具有分段存储器的体系结构的机器上,指针由一个段标识和一个端内偏移量组成。

  11. 解决悬空引用的tombstones、锁和钥匙
    tombstones引入一层间接,存储指针指向的对象,如果对象死亡,修改指向一个合法地址 一般是0
    锁和钥匙 只能用于堆对象,为悬空引用提供了具有概率性的保护,避免了需要永远维持所有的tombstones,每一个指针中都有一个地址和一个钥匙,堆中每一个对象的开头是一个锁,只有在匹配的时候才能够使用这个对象。

  12. 引用计数和标记清除方法的优点和缺点?
    比较常见的面试题2333

  13. 为什么废料收集在命令式语言中采纳的过程如此缓慢?
    弱类型语言中,无法通过指针找到所有指针指向的对象。

  14. 为什么表被广泛用在函数式语言中?
    表本身就是递归定义的,因为fp中需要递归或者高阶函数操作,所以表就显得特别有用。

  15. 将IO构筑在语言本身中而非外部库中有什么好处?
    可定制参数,可定制关键字

  1. 函数和过程的区别是什么?
    有返回值的称为function,无返回值的是procedure

  2. 什么是子程序的调用序列?这种序列做什么事情?子程序的前序代码和后序代码是什么意思?
    调用序列是由调用者紧接着子程序调用之前和之后执行的代码。前序代码和后序代码就是调用前和调用后所使用的代码。

  3. 为CISC和RISC服务的编译器在调用序列方面的不同是什么?
    CISC机器在压入弹出实参时,采用自动增量和自动减量的寻址模式,副作用是更新sp,经常修改sp使得它作为基址去访问局部变量变得很困难。
    CISC编译器把所有参数放入堆栈而非寄存器中,CISC必须有一个帧指针,依赖于相当复杂的特殊指令。
    RISC(MIPS)sp很稳定。
    注意此处RISC是精简指令集计算机,CISC是复杂指令集计算机

  4. 说明如何在子程序调用中维护静态链
    静态链用于找到外围子程序中的对象(在允许嵌套子程序并且是静态作用域的语言中)。
    维护两个引用,第一个引用是静态链指针,用来包含对于外围帧的引用;第二个是动态链指针,用来帮助子程序维护返回值。

  5. 什么是区头向量?它与静态链有什么不同?在子程序调用过程中如何维护它?
    区头向量就是把静态栈放入一个数组,向量的第j个元素包含着一个引用,它引用语法上位于外面第j层的子程序的最近活动栈。
    区头向量一般比静态链的成本更高。区头变量实现闭包时需要被闭包所包裹。

  6. 堆栈指针和栈指针寄存器的作用?
    堆栈指针寄存器包含着堆栈内第一个未使用位置的地址,栈指针寄存器始终指向栈顶帧的一个地址,用来对对象的访问寻址。

  7. 在叶子程序的情况下,可以对子程序调用序列做哪些优化?请列出这些优化。
    叶子程序可能不需要堆栈存放数据,直接在寄存器中完成操作。

  8. 为什么编译器需要在堆栈上为参数分配空间?
    因为要根据栈帧指针对参数寻址,同时充分利用寄存器

  9. 实际参数和形式参数

  10. 请描述四种常见的参数传递模型
    值传递,引用传递
    in、out、in out 模式
    在这里插入图片描述

  11. 请解释Modula-3中提供的READONLY参数的合理性
    READONLY就相当于C中的const

  12. 什么是换名调用?什么语言最先提供了它?为什么该语言的后继者没有提供它?
    这个东西很神奇,是一种类似于宏的实参传递。在每次使用名字调用参数时,都需要在被调用者的引用环境中对它进行重新求值,这样做的效果就相当于被调用子程序在调用点展开,展开时用实际参数代入形式参数的每个出现。
    很容易发现,在这种情况下procedure的semantics就完全不同了
    在这里插入图片描述

  13. 什么是形实转换程序Thunk?
    为了实现名字调用,必须传递一个形实转换例程,在调用者的引用环境中对它重新求值,这样做的效果相当于子程序在调用点展开。缺点是成本比较高。

  14. inline和宏的区别?
    1、内联函数在编译时展开,而宏在预编译时展开 2、在编译的时候,内联函数直接被嵌入到目标代码中去,而宏只是一个简单的文本替换。 3、内联函数可以进行诸如类型安全检查、语句是否正确等编译功能,宏不具有这样的功能。

  15. 通用型子程序和宏的区别?

  16. 什么是命名参数?作用是什么?
    标号参数:将标号(label)作为参数传递
    命名参数:具名的参数,在一些语言(Ada、Modula、Fortran、Common Lisp)中,形参具名可以打乱顺序传递

  17. 什么是相似数组?
    在某个需要编译时确定数组形状的语言中,可以推迟到运行时再确定形状的形式数组称为相似数组或者开发数组参数。

  18. 为什么说返回函数引用是有用的?

  19. 请描述一个算法,可用于在Ada或者C++中引发异常时辨识适当的处理器
    异常处理是PL/I发明的,异常处理机制的一种方式就是转移到适当的处理器中

  20. 请解释为什么将异常定义为类是很有用的
    1.能够提供参数 2.能够匹配性的捕获

  21. 请说明如何实现异常可以没有额外代价
    异常的第一种实现方式:维护一个处理器的链接表堆栈,表的头部记录作用于这个块的所有处理器,当某个异常引发时,运行系统弹出最内层的处理器并且调用它。如果不是该处理器引发的异常,执行后序代码并且重新引发异常。
    第二种实现方式:编译时维护一个表格,每个表项包括两个域:代码块的起始地址和与之对应的处理器地址。当异常发生时,根据程序计数器在表中折半查找。

    第一种方法的开销在于每个被保护块开始的时候,需要执行把处理器压入处理器表的代码,第二种方法则在出现异常时开销更大。

  22. 请总结C库例程setjmp和longjmp的缺点
    setjmp以一个缓冲区作为参数,能够把程序当前状态的某种表示存入其中,随后可以将这个缓冲区传入longjmp要求恢复保存的状态。
    问题在于这样会恢复寄存器的值,无法反映已经改变的内存的内容。可以使用volatile将改变立刻保存到内存中。

  23. C语言里的volatile变量作用是什么?
    常见面试题
    volatile关键字表示可能会自发改变,C语言在该变量改变时会保存到内存中,在每次读取的时候装入。
    防止CPU缓存热数据,强制直接访问数据原始地址,防止缓存与实际数值不符合。

  24. 哪个语言首先提供了协程?
    Modula-2

  25. 协程和线程的不同是什么?
    典中典面试题

  26. 什么是仙人掌堆栈?
    仙人掌堆栈中,堆栈中每一个分支中包含着一个协程的所有堆栈帧

  27. 为什么转移库例程在协程切换时需要修改程序计数器?
    PC、堆栈和处理器寄存器的改变用来实现coroutine的transfer

  28. 如何解释不使用协程实现迭代器
    这里的意思其实是不使用Modula中协程的堆栈方式实现yield去切换调度。也就是无栈协程,也就是将调用的栈帧分配在迭代器帧上。

  29. 什么是离散事件模拟?
    离散事件模拟是一类模拟,其中的模型可以很自然的用在某些特定时刻发生的事件来表示。

*最近删了以下以前的博客,以前写的垃圾还真不少。删完之后感觉博客终于像那么回事了,多少能给别人看了。写博客是个好习惯,但是CV或者写没什么营养的东西是没什么意义的。以后别人看见我的博客不要在下面骂电子垃圾,这应该算最低要求了。CSDN的各个部分虽然都想让人骂几句,但是我还是起码先把自己的博客维护好吧…

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值