在期末复习的时候将上课课件以及课本内容进行了整理。
对于计算过程可以有多种不同看法,由此产生了不同的计算模型(范型),基于不同计算范型产生了不同的语言类(语言范型)
主要范型有:
计算看成一系列操作的执行 ,基本计算元素是一组基本操作。计算在一个环境里进行,操作的效果就是改变环境的状态。
语言提供一组描述组合操作的手段,提供的抽象手段是定义新操作(定义过程)
写程序是描述操作执行的顺序过程,描述状态和状态的变化
计算看成是一批独立对象相互作用的效果 。
面向对象语言提供:
比较纯的面向对象语言:
计算看成对数据的函数变换。
基本计算元素是一组基本函数,提供各种函数组合机制(复合,函数选择),抽象手段是定义新函数(允许递归定义)
基本思想是:只描述需要做什么,不描述怎么做
包含:
机器语言可以看做是计算机硬件的“抽象”
高级语言可看作是一种“抽象计算机” 的机器语言
常用的实现方式:编译和解释
定义:上下文无关文法G是一个四元组(V,T,P,S)
BNF提供了两类符号:终结符和非终结符
终结符:被定义的语言的符号,最终可以出现在程序中
非终结符:是为了定义语言的语法而引入的辅助符号
一个非终结符表示语言中的一个语法类:
< stmt >表示语句
一个语言的BNF语法定义由一组产生式(或规则)组成,其本质是一个上下文无关文法
产生式的形式是:
左部 → → 右部
一个简单的文法:
< program > ->< stmts >
< stmts >->< stmt >|< stmt >;< stmts >
< stmt >->< var >=< expr >
< var >->a|b|c|d
< expr>->< term>+< term>|< term>-< term>
< term>->< var>|const
推导是一个或一系列产生式的应用
句型:由开始符号经过推导得到的字符串 例如:a=b+< var >
句子:只有终结符的句型 例如:a=b+const
最左推导:每次总是替换句型最左边的非终结符
最右推导:每次总是替换句型最右边的非终结符
推导树:推导的树形结构表示,例如a=b+const
的语法树:
最右推导就是树的最右遍历
关于推导的练习 PPT
考虑下列BNF: < expr >-> < expr >+< expr >| < var >; < var >->a|b|c ;
写出其最左推导:
< expr >
=>< expr>+< expr>
=>< expr>+< expr>+< expr>
=>< var>+< expr>+< expr>
=>a+< expr>+< expr>
=>a+< var>+< expr>
=>a+b+< expr>
=>a+b+< var>
=>a+b+c
写出其最右推导:
< expr>
=>< expr>+< expr>
=>< expr>+< expr>+< expr>
=>< expr>+< expr>+< var>
=>< expr>+< expr>+c
=>< expr>+< var>+c
=>< expr>+b+c
=>< var>+b+c
=>a+b+c
二义性:文法生成的句型有两个或多个语法树,例子为上图,在这里是结合性造成的。
优先级
结合性
二义性:< expr> -> < expr> + < expr> | const
非二义性:< expr> -> < expr> + const | const
++与+:a+++b
if-then-else:
if < cond> then if < cond> then < stmt> else < stmt>
最后的else与谁匹配
//例子
//二义性:
< expr>->< expr>< op>< expr>|const
< op>->+|-|*|/
//非二义性:
< expr>->< expr>< op_L>< term>|< term>
< term>->< term>< op_H>const|const
< op_L>->+|-
< op_H>->*|/
//非二义性(允许括号):
< expr>->< expr>< op_L>< term>|< term>
< term>->< term>< op_H>< factor>|< factor>
< factor>->const|’(‘< expr>’)’
< op_L>->+|-
< op_H>->*|/
==设计无二义性的BNF的练习(PPT)==
1)左结合+(or,and,not)优先级依次变高
< expr>->< expr> or < term> | < term>
< term>->< term> and < factor> | < factor>
< factor>-> not < factor> | true | false
2)?
< stmt>->< matched>|< unmatched>
< matched>->if< logic_expr> then < matched> else < matched>|any non-if statement
< unmatched>->if then | if then < matched> else < unmatched>
! EBNF只是增加了可读性和可写性,并没有增加表达能力
1)BNF 和 对应的EBNF
//BNF
< expr> -> < expr> + < term>
| < expr> - < term>
| < term>
< term> -> < term> * < factor>
| < term> / < factor>
| < factor>
//EBNF (左结合)
< expr> -> < term> {(+ | -) < term>}
< term> -> < factor> {(* | /) < factor>}
2)将 EBNF 转为 BNF
//EBNF
S->A{bA}
A->a[b]B
B->(c|d)e
//BNF
S->SbA|A
A->aB|abB
B->ce|de
==设计描述布尔逻辑表达表达式的EBNF==
//二义性
< bExpr>->< bExpr>(and|or)< bExpr>
< bFactor>-> not< bFactor>| true | false
//非二义性
< bExpr>->[< bExpr> or]< bTerm>
< bTerm>->[< bTerm> and]< bFactor>
< bFactor>->(not< Factor>|true|false)
例子:形如id = id+id的赋值语句
BNF:
< ass_stmt>->< var>=< expr>
< expr> ->< var> +< var>
< var> -> id
为了满足上述要求,需要:
这些需求解的信息成为语义属性,简称属性
属性文法是具有以下附加特性的文法
每个文法符号X都有一个属性集A(X):
每条产生式有一个==计算属性==的语义函数集
例子:形如id = id+id的赋值语句
BNF:
< ass_stmt>->< var>=< expr>
< expr> ->< var> + < var>
< var> -> id
属性
==实际类型==:与< var>、< expr>相关的综合属性;id的本质属性
==期望类型==:与< expr>相关的继承属性
属性文法
< ass_stmt>->< var>=< expr>
函数:==< expr>.expected_type = < var>.actual_type==
函数是从左到右继承属性==(赋值= 是继承属性)==
因为< var>在声明时已经指定了类型,所以< expr>的期望类型要符合那个指定的类型
< expr> -> < var>[1] + < var>[2]
函数:==< expr>. actual_type = < var>[1].actual_type==
函数是有底向上的综合属性==(-> 是综合属性)==
谓词: < var>[1]. actual_type == < var>[2].actual_type
< expr>. expected_type == < expr>.actual_type
< var> -> id
id.actual_type = lookup(符号表,id)
< var>.actual_type是自底向上的综合属性(->)
id.actual_type 是叶子节点的本质属性
还是最初的例子id=id+id,其BNF为:
< ass_stmt>->< var>=< expr>
< expr> ->< var> +< var>
< var> -> id
对应的语法树为:
动态语义:编程语言中表达式、语句和程序单元的意义
描述机器状态最简单的形式化模型:从合法名字集合到“值”的映射
例如:当x取值3,y取值5,z取值0时的状态为 {x->3, y->5, z->0}
示例语言的BNF:
< stmt> -> SKIP
| < var> = val
| < stmt>;< stmt>
| IF < cond> THEN < stmt> ELSE < stmt>
| WHILE < cond> DO < stmt> END
SKIP
SKIP的语义规则:SKIP语句不做任何操作
即: SKIP,S=>S (表示状态无改变)
< var> = val
赋值语句语义规则:语句var=val将变量var的值改为val,其他变量不变
即:var = val, S => S[var->val]。
例: x=2, {x->3, y->5, z->0} => {x->2, y->5, z->0}
< stmt>;< stmt>
复合语句语义规则:先执行语句1,再执行语句2
即:stmt1, S => S1 stmt2, S1 => S2 ——-> stmt1;stmt2, S => S2
例:x=2;y=2, {x->3, y->5, z->0} => {x->2, y->2, z->0} (一步到位)
IF < cond> THEN < stmt> ELSE < stmt>
IF语句语义规则:如果条件为真,则执行语句1,否则执行语句2
WHILE < cond> DO < stmt> END
WHILE语句语义规则:如果条件cond为真,则执行语句stmt,直到cond为假
cond=true stmt; S => S1
WHILE cond DO stmt END; S1 => S2
即上两句的作用为:WHILE cond DO stmt END;S => S2
cond=false
即无作用:WHILE cond DO stmt END; S => S
练习题2道
1.考虑下列程序
count =n;
sum=0;
WHILE count > 0 DO
sum=sum + count;
count=count - 1;
END
在状态{n->3}和{n->-1}下的操作语义
答案:
记整个程序为P。
count = n, {n->3} => {n->3, count->3}
sum=0, {n->3, count->3} => {n->3, count->3, sum->0}
{n=3, count->3, sum->0}下,count>0成立,且 sum=sum+count; count=count-1, {n->3, count->3, sum->0} => {n->3, count->2, sum->3}
{n=3, count->2, sum->3}下,count>0成立,且 sum=sum+count; count=count-1, {n->3, count->2, sum->3} => {n->3, count->1, sum->5}
{n=3, count->1, sum->5}下,count>0成立,且 sum=sum+count; count=count-1, {n->3, count->1, sum->5} => {n->3, count->0, sum->6}
{n=3, count->0, sum->6}下,count>0不成立,那么WHILE … END, {n->3, count->0, sum->6} => {n->3, count->0, sum->6}
因此,WHILE … END, {n->3, count->3, sum->0} => {n->3, count->0, sum->6}
故有P, {n->3} => {n->3, count->0, sum->6}
2.请使用此语言设计FOR语句并定义其操作语义
思路是将For循环转换成While循环。
FOR(stmt1; cond; stmt2)
stmt;
等价于:
stmt1;
WHILE (cond) DO stmt;
stmt2 END
所以:
如果stmt1; S => S1。
且WHILE (cond) DO stmt; stmt2 END; S1 => S2。
那么FOR (stmt1; cond; stmt2) stmt, S => S2
基础:递归函数理论
思想:定义一个语义函数(指称函数),把程序的意义映射到某种意义清晰的数学对象 (就像用中文解释英文)
步骤:
简单例子——二进制数的语义:
BNF:< binary> -> ‘0’ | ‘1’ | < binary> ‘0’ | < binary> ‘1’
语义域:自然数集合
语义函数 Mbin M b i n :
示例语言的BNF:
< stmt> -> SKIP
| < var> = val
| < stmt>;< stmt>
| IF < cond> THEN < stmt> ELSE < stmt>
| WHILE < cond> DO < stmt> END
其操作语义为:< stmt>,S=>S
其中,< stmt>是输入,S=>S是输出,=>是状态到状态的函数
所以对于指称语义来说,==语义域==是状态到状态的函数空间
==语义函数==:M
SKIP
SKIP语句: M(SKIP)(S) = S
一个(恒等)状态函数,对任意输入状态返回状态本身
< var> = val
赋值语句: M(var = val)(S) = S[var -> val]
该语义函数对任意输入状态S都返回S[var -> val]
例:M(x=2)({x->3, y->5, z->0})= {x->2, y->5, z->0}
< stmt>;< stmt>
复合语句: M(stmt1;stmt2)(S) = (M(stmt2)ºM(stmt1))(S)
= M(stmt2)(M(stmt1)(S))
stmt1的语义函数和stmt2的语义函数的复合
例:M(x=2;y=2)({x->3, y->5, z->0})
= (M(y=2) º M(x=2)) ({x->3, y->5, z->0})
= M(y=2) ( M(x=2) ({x->3, y->5, z->0}) )
= M(y=2) ({x->2, y->5, z->0})
= {x->2, y->2, z->0}
IF < cond> THEN < stmt> ELSE < stmt>
条件语句:
M(IF cond THEN stmt1 ELSE stmt2)(S) =
对任意输入状态S,如果cond在S状态下为真,返回stmt1的语义函数作用于S的结果M(stmt1)(S) ,否则返回M(stmt2)(S)
例:P: IF x > 0 THEN x = x+1 ELSE x = x-1
M(P)({x->1}) = M(x=x+1)({x->1}) = {x->2}
M(P)({x->-1}) = M(x=x-1)({x->-1}) = {x->-2}
WHILE < cond> DO < stmt> END
条件语句:
M(WHILE cond DO stmt END)(S) =
对任意输入状态S,如果cond在S状态下为真,返回stmt的语义函数与WHILE语句的语义函数(递归)的复合作用, 否则返回S (终止)
相同点:操作语义用==机器的状态==来描述意义,指称语义用==程序的状态==来描述意义,在抽象层面上,这两者是一致的
关键区别:操作语义用某种编程语言编码的算法来定义状态变化,而指称语义用数学函数来定义最终效果,并不关心执行过程。
公理语义是以语言对断言的影响来定义的,一般形式:{P} stmt {Q}
*例:{x>10} sum=2*x+1 {sum>0}
最弱前置条件:保证相关后置条件有效的==限制最小==的前置条件
例子:{x>0} sum=2*x+1 {sum>0}
{x>10} sum=2*x+1 {sum>0}
{x>20} sum=2*x+1 {sum>0}
{x>50} sum=2*x+1 {sum>0}
其中x>0是最弱前置条件:实际上sum>0[sum=2x+1]=>x>-0.5,这个才是最弱前置条件。
示例语言的BNF:
< stmt> -> SKIP
| < var> = val
| < stmt>;< stmt>
| IF < cond> THEN < stmt> ELSE < stmt>
| WHILE < cond> DO < stmt> END
以最弱前置条件的计算为例
SKIP
SKIP语句:{P} SKIP {P}
< var> = val
赋值语句:{Q[var->val]} var = val {Q}
根据后置条件来计算(最弱)前置条件(逆推 )
其中Q[var->val]表示将Q中所有出现的var替换成val
例子:{ ? } x=2*y-3 {x > 25}
? 可以是:(x > 25)[x -> 2*y-3] => 2*y-3 > 25 => y > 14
< stmt>;< stmt>
复合语句:{P1} stmt1 {P2} , {P2} stmt2 {P3} 等价于 {P1} stmt1;stmt2 {P3}
例子:{?} y=3*x+1;x=y+3;{x<10}
1)先计算第二条语句的前置条件:
由{?} x=y+3 {x<10}可得 y<7
2)再计算的一条语句的前置条件:
由{?} y=3*x+1 {y<7}可得 x<2
因此,有{x<2} y=3*x+1;x=y+3;{x<10}
IF < cond> THEN < stmt> ELSE < stmt>
条件语句:
{P} IF cond THEN stmt1 ELSE stmt2 {Q}
例:{?} IF x>0 THEN y=y-1 ELSE y=y+1 {y>0}
1)假设执行THEN语句,其最弱前置条件为:
由{?} y=y-1 {y>0}可得 y>1
2)假设执行ELSE语句,其最弱前置条件:
由{?} y=y+1 {y>0}可得 y>-1
合并得到y>1,即有{y>1} IF x>0 THEN y=y-1 ELSE y=y+1 {y>0}
WHILE < cond> DO < stmt> END
循环语句:{cond and I} stmt {I}
{I} WHILE cond DO stmt END {I and (not cond)}
其中的 I 是循环不变式,难点在于==如何设计循环不变式==。
所以一般题目会给出循环不定式。。。自己验证一下就好
例子:{?} WHILE y<>x DO y=y+1 END {y=x}
设循环不变式I为y<=x
{y<>x and y<=x} y=y+1 {y<=x} ?
y<=x[y=y+1] =>y+1<=x ∴ y
操作语义
为语言使用者和实现者提供一种有效的语义描述方法
非形式化的使用;一旦形式化,可能变得非常复杂
指称语义
以高度严谨的方式看待程序
可辅助语言设计
描述较复杂,对使用者用处较少
公理语义
定义公理和推导规则是项困难的任务
研究程序正确性的有力工具,程序推理的优秀框架
定义:绑定是属性与实体之间的关联,例如变量与其类型或者值之间的关联
静态绑定:绑定在运行之前第一次出现,且在整个程序的执行过程中保持不变
类型可能通过显式声明或隐式声明来说明 (声明是说明变量的数据类型,不为其分配空间)
动态绑定:绑定在运行期间第一次出现,或者能在程序执行期间改变
优点:灵活性大,如,能写处理任何类型的数据的通用程序
缺点:
C语言的static变量是静态绑定的,非static的局部变量时动态绑定的
变量在使用前,必须绑定一个类型。
分配:从可用的存储池中取得一个存储单元
解除分配:将存储单元放回存储池
生存期:变量被绑定到某个存储单元的时间段,变量的分配与回收的全过程。
生命周期(不确定):变量存在的时间,有些语言中变量的生命周期从声明开始,但生存期不是。==变量的属性是生存期而不是生命周期==
根据生存期,变量可分为
数据区(编译时分配)、栈区(由系统自动分配)、堆区(需程序员申请与释放(或垃圾回收机制))
静态变量:在程序运行前就绑定到存储单元,且在程序结束运行前一直绑定在相同的存储单元上
例,C中全局变量、用static修饰的局部变量
例,Java中类变量、用static修饰的局部变量
例,Python中的类变量
优点:
缺点:
栈动态变量:在声明语句执行确立后才产生存储绑定
例,C函数中局部变量
例,Java函数中基本类型的变量、对象的引用变量
优点:
缺点:
显式堆动态变量:通过显式指令来分配和解除分配
例,C中malloc和free函数
例,Java的对象都是显式堆动态变量,Java的new和垃圾回收机制
优点:
缺点:
隐式堆动态变量:赋值时才绑定到堆存储空间
例,python和JavaScript的赋值语句:Highs = [74, 84, 86, 90, 71]
优点:高度灵活性
缺点:
int a = 0; //全局初始化区
char *p1; //全局未初始化区
void main() {
int b; //栈
char *p2; //栈
char *p3 = "123456"; //123456\0在常量区,p3在栈上
static int c =0; //全局(静态)初始化区
p1 = (char *)malloc(10); //堆区显式分配10个字节
p2 = (char *)malloc(20); //堆区显式分配20字节
strcpy(p1, "123456"); //123456\0放在常量区,
//编译器可能会将它与p3所指向的"123456"优化成一个地方
}
int x = 1;
int z = 4;//两个全局变量,在所有作用域都可以访问的变量
//println(x);//println(y); //println(z);
void main() {
int x = 2;
int y = 3;//两个局部变量,在此特定函数块内可访问的变量
//println(x);//println(y); //println(z);
}
静态作用域:编译时(执行之前)就决定了变量的作用域
块:程序中创建静态作用域的方法
例:函数局部变量
例:for语句:for(…) { int index; …}
动态作用域:作用域在运行期间确定
作用域与生存期看似相关联,实际上不是同一件事情:一个是空间、一个是时间
静态作用域语言中,语句的引用环境是在它的局部作用域中声明的变量,和在它的祖先作用域中声明的所有可见变量的集合
活跃的子程序:当前已经开始执行但还没终止
命名常量:和值仅绑定一次的变量
增加可读性和可靠性
例:用PI表示圆周率3.14159256
例:用len表示数据长度,修改长度只需改len
既可以静态绑定也可以是动态绑定
例如:C#的const(静态)和readonly(动态)
数据类型定义了一组数据值,以及在这些数据值上的一组操作。
类型系统的主要用途 (考点,选择题):
描述符是变量的属性的集合:
数据类型的基本设计问题:应该为各种类型的变量提供哪些操作,以及如何说明这些操作
大部分编程语言都提供了基本数据类型:
描述的值:由字符组成的序列
设计问题:
长度选择:
字符串操作:赋值、比较、串联、子串、长度、模式匹配。。。
字符串实现:
数组类型: (同质)数据的一种聚合形式
索引: 下标到数组元素的(有限)映射
数组引用:< array_name> (< index_list>) 或 < array_name> [< index_list>]
设计问题:
根据下标范围的绑定、存储空间的绑定和空间的分配地方,数组可分为五类:
记录类型:可能异构的数据的一种聚合形式
设计问题:
补充内容:
数组类型
记录类型
指针类型:
设计问题:
指针相关问题:
//C++
int * p1 = new int[100];
//创建一个堆动态变量,令p1指向它
int * p2 = p1;
//p1赋给p
delete [] p1;
//释放p1,并没有改变p2
//p1和p2都是悬空指针
堆动态变量的丢失
//C++
int * p1 = new int[100];
//创建一个堆动态变量,令p1指向它
p1 = new int[100];
//创建一个堆动态变量,令p1指向它
//这使得第一次创建的变量无法访问
C和C++的指针
用于动态存储管理和寻址
可指向任何地址的任意变量(危险)
显式的解引用(*)和取地址操作(&)
允许某些限制形式的地址计算
void * :指向任意类型,但不能解引用(无类型检测问题)
float score[100];
float * p = score;
*(p+5) –– score[5] –– p[5]
引用类型:操作有限的指针类型
C++的引用类型
补充内容:c++中的引用与指针的区别
★ 相同点:
1. 都是地址的概念;
指针指向一块内存,它的内容是所指内存的地址;引用是某块内存的别名。
★ 区别:
1. 指针是一个实体,而引用仅是个别名;
2. 引用使用时无需解引用(*),指针需要解引用;
3. 引用只能在定义时被初始化一次,之后不可变;指针可变;
引用“从一而终” ^_^
4. 引用没有 const,指针有 const,const 的指针不可变;
5. 引用不能为空,指针可以为空;
6. “sizeof 引用”得到的是所指向的变量(对象)的大小,而“sizeof 指针”得到的是指针本身(所指向的变量或对象的地址)的大小;
typeid(T) == typeid(T&) 恒为真,sizeof(T) == sizeof(T&) 恒为真,但是当引用作为成员时,其占用空间与指针相同(没找到标准的规定)。
7. 指针和引用的自增(++)运算意义不一样;
★ 联系
1. 引用在语言内部用指针实现(如何实现?)。
2. 对一般应用而言,把引用理解为指针,不会犯严重语义错误。引用是操作受限了的指针(仅容许取内容操作)。
不同语言中的指针和引用:
抽象数据类型:一个封装
//一个例子
#ifndef _stack_h
#define _stack_h
typedef struct stackCDT *stackADT;
typedef int stackElement;
stackADT NewStack(void);
void FreeStack(stackADT stack);
void Push(stackADT stack ,stackElement element );
stackElement Pop(stackADT stack);
int StackTop(stackADT stack);
#endif
算术表达式的计算是最初设计编程语言的动机之一
算术表达式由运算符、操作数、括号和函数调用组成
算术表达式的基本设计问题
1.运算符的优先级规则
2.运算符的结合性规则
3.操作数的运算顺序
4.对操作数求值的副作用是否存在约束
5.是否允许运算符重载
6.是否允许类型混合
按操作数个数,运算符可分为:
优先级规则:定义了优先级不同的相邻运算符的运算顺序 :
结合性规则:定义了优先级相同的相邻运算符的运算顺序(左结合和右结合)
括号(具有更高优先级) :可改变优先级规则和结合性规则,从而调整计算顺序。
操作数的运算顺序:
小练习:
(a – b) / c & (d * e / a – 3)的运算顺序: (((a−b)1/c)2and(((d∗e)3/a)4)−3)5)6 ( ( ( a − b ) 1 / c ) 2 a n d ( ( ( d ∗ e ) 3 / a ) 4 ) − 3 ) 5 ) 6
(从左到右,自底向上)
副作用:当函数改变其(双向)参数或者非局部变量时,则称该函数是有副作用的
//例子1
int a = 5;
int fun1() {
a = 17;
return 3;
} /* end of fun1 */
void main() {
a = a + fun1();
//a的值是多少呢?
} /* end of main */
双向参数:指针参数,引用参数
注意,数学里的函数是没有副作用的,因为数学中没有变量的概念。函数式编程语言也没有这个问题
引用透明: 如果任意两个值相同的表达式能够在该程序的任何地方互换,并且不影响程序的运行,这个程序就是引用透明的。
解决副作用的方法:
操作符重载:运算符的多种用法
常见的操作符重载:整数+、浮点数+
有些重载存在潜在危险:例如C语言的&
用户自定义的操作符重载:C++允许
收缩转换:新类型不能存下原类型的所有值
例如,double转为float,double的取值范围比float大
通常是不安全的
扩展转换:新类型能够存下原类型的所有值
例如, float转为double
通常是安全的,但可能降低精度
两种形式:
隐式类型转换
由编译器执行的强制转换:
混合模式表达式:有不同类型操作数的表达式
支持混合模式表达式的语言必须定义用于操作数类型隐式转换的协议,如整数可转换为浮点数
显式类型转换
由程序员显式执行的强制转换:
绝大多数语言提供一些功能来执行显示转换,包括收缩转换和扩展转换
如果显示的收缩转换使原来的值发生巨大变化,程序会发布警告
关系表达式:使用关系运算符和各种类型的操作数的表达式,计算结果是布尔型
布尔表达式:由布尔型变量、布尔型常量、关系表达式和布尔运算符
布尔运算符:与(AND)、或(OR) 、 非(NOT ), 可能有异或(XOR)和等价
由于算数表达式可能是关系表达式的操作数,关系表达式可能是布尔表达式的操作数,所以要求三种运算符的优先级不同
短路求值:表达式的结果不是在计算了所有的操作数之后得到的
例如 : if false && 1==1
并不会计算1==1,因为已经知道结果为false。
if(n!=0 && sum/n>50)
exam=pass;
while(p!=NULL && p->val!=key)
p=p->next;
赋值:与数据有关的最基本操作
形式:
赋值表达式:赋值语句产生的返回值作为表达式赋值
例(C语言):while((ch=getchar())!= EOF) {…}
混合模式赋值:类似混合表达式,赋值表达式可能含有不同类型
函数语言的赋值:
所有标识符只是数值的名字,值从不改变
...
val cost =...
...
val cost = quantity * price
第二个cost声明创建一个新的cost变量,将第一个cost隐藏了
设计者想让程序更加灵活、强大:
程序员关注的是程序的可写性和可读性
控制结构:包括控制语句和被它控制执行的语句集合两部分
选择语句:
双路选择语句
设计问题:
控制表达式:
子句的形式:单个语句、程序块
嵌套选择器:
if( sum == 0)
if( count == 0)
result =0;
else
result = 1;
方法1:通过静态语义规则规定,即就近原则。如 C和Java:跟第二个if匹配
方法2:通过缩进确定。如Python: 跟第一个if匹配
方法3:利用复合语句实现不同的语义
方法4:使用特殊的保留字实现不同的语义。如Ruby
多重选择结构
设计问题:
多重选择结构switch语法形式:
switch (控制表达式){
case 常量表达式1: 语句1;
……
case 常量表达式n: 语句n;
[ default : 语句n+1;]
}
switch利用break语句构成显示分支(限制控制流)
switch (index){
case 1:
case 3: odd +=1;
sumodd += index;
break;
case 2:
case 4: even += 1;
sumeven += index;
break;
default : printf(“Error in switch, index = %d\n”,index);
}
迭代语句:能使一条语句或一块语句执行0次、1次或者多次,一般称为循环。
设计问题
控制方法:
控制机制:
计数控制语句
代表:for循环
break、continue。
用户自定义的循环退出机制:满足从严格受限制的分支语句跳转到其他语句的需要
与goto语句的差别:
例如:
过程抽象:抽象的对象是实现某种==功能可能复杂的操作==
数据抽象:抽象的对象是被操作的==数据概念和相关功能==
子程序是最主要的计算过程的抽象机制
一般特性(选择题有考):
名字(类似变量的名字):大多数子程序都有名字,少数匿名函数
实参与形参的对应
默认参数:最好是不可变参数
可变参数:
//过程,新操作语句
void sort(int n, int[] array)
//函数,新操作符
float power(float base, float exp)
局部变量:子程序内部定义的变量,其作用域是子程序体内
大多数当代语言默认是栈动态变量
嵌套子程序:某个子程序只在另外一个子程序用到,则将其定义放在使用的子程序中(高度结构化)
例子:
参数传递:参数传入传出被调用的子程序的方法
形参的三种语义模型
数据传输的概念模型:
实现模型-==按值传递==(输入模型)
//C按值传递实例
void swap1(int a, int b) {
int temp = a;
a = b;
b = temp;
}
swap1(c, d);
//结果不变,过程如下
//a = c
//b = d
//temp = a
//a= b
//b = temp
实现模型-==按结果传递==(输出模型)
实现模型-==按值-结果传递==(输入输出模型)
例子:
实现模型-==按引用传递==(输入输出模型)
通过访问路径获取数据的地址,并传递给被调用的子程序
实际上,实参是由调用子程序和被调用子程序共享
优点:传递快,空间效率高
不足:访问慢,别名问题
别名问题原因:调用子程序提供了对非局部数据更大的访问
按值-结果传递则没有别名问题(但存在其他问题)
按指针传递:若把地址当作值,按指针传递就是按值传递,只不过传的值是地址而已
按引用传递:需先提取参数的地址,再传给被调用子程序
别名问题的例子1:
别名问题的例子2:
上图展示了四种不同的参数传递方式对应运行时的栈变化。
实现模型-按名字传递(多种模型)
例子
procedure P(X,Y,Z) {
Y:=Y+X;
Z:=Z+X;
}
所以
P(A+B,A,B)等价于
A:=A+A+B;
B:=B+A+B;
参数传递方法的选择
这两个原则是冲突的
良好的编程艺术:限制变量的访问,意味着尽可能单向传输
高效:按引用传递是最快的方法,意味着双向
语言例子:
泛型(或多态)子程序:能作用于不同类型的数据且执行不同的操作的子程序
提高软件重用率