1 / 97

从问题到程序

从问题到程序. 裘宗燕 北京大学数学学院 2005 年. 第五章 C 程序结构. 本章讨论与 C 程序整体结构有关的问题。 对正确理解 C 语言 / 正确书写 C 程序都很重要。是学习用 C 程序设计时应了解的“深层问题”。 有些叙述性内容,有些例子要推迟到后面章节。 建议在学习后面章节之后重读这章。. 1 )基本数值类型的全面介绍 2 )函数与 C 程序结构,函数原型 3 )变量类,作用域与存在期 4 )预处理命令 5 )字位运算符(特殊问题,只需初步了解). 5.1 数值类型. 实数类型: float , double , long double.

Download Presentation

从问题到程序

An Image/Link below is provided (as is) to download presentation Download Policy: Content on the Website is provided to you AS IS for your information and personal use and may not be sold / licensed / shared on other websites without getting consent from its author. Content is provided to you AS IS for your information and personal use only. Download presentation by click this link. While downloading, if for some reason you are not able to download a presentation, the publisher may have deleted the file from their server. During download, if you can't get a presentation, the file might be deleted by the publisher.

E N D

Presentation Transcript


  1. 从问题到程序 裘宗燕 北京大学数学学院 2005年

  2. 第五章C程序结构

  3. 本章讨论与C程序整体结构有关的问题。 对正确理解C语言/正确书写C程序都很重要。是学习用C程序设计时应了解的“深层问题”。 有些叙述性内容,有些例子要推迟到后面章节。 建议在学习后面章节之后重读这章。 1)基本数值类型的全面介绍 2)函数与C程序结构,函数原型 3)变量类,作用域与存在期 4)预处理命令 5)字位运算符(特殊问题,只需初步了解)

  4. 5.1 数值类型 实数类型:float,double,long double 实数类型外的算术类型都是整数类型。字符类型也看作整数类型,可以作为整数参加运算。 每一基本整数类型都有带符号与无符号两种类型,用限定词signed或unsigned说明。 无符号类型的值大于等于0。 同一基本类型的带符号/无符号类型用同样长度编码。 signed可省略,如signed long简写为long。

  5. 字符类型:char,signed char,unsigned char 用一个字节表示,其中存字符编码。字符作为整数参加运算时直接用编码,依赖于编码方式。 ASCII字符集里数字(字母)字符的编码连续排列。 char等同于signed char或unsigned char(不同系统可能不同)。简单程序只用char,不需要关心有无符号。只有用char参加整数运算时才需要考虑。 有符号/无符号字符不好理解。就是:参加整数运算时看作有符号整数还是无符号整数? 若字符编码在0-127内也不会有问题。 基本ASCII字符集的编码范围是0-127。

  6. 整数类型基本:int,short int,long int 总共六个: int unsigned int short unsigned short long unsigned long short int简写为short,long int简写long, unsigned int简写为unsigned short表示范围不大于int,long不小于int。无符号/有符号类型位数相同。具体表示由C语言系统确定。 基本类型是int,一般用计算机的字长表示。 16位机器的C系统中,int通常用16位表示; 32位机器的C系统中,int通常用32位表示。

  7. 微机C系统情况复杂: 老系统16位int,8086字长,32位long,16位short 许多新系统用32位int和long,short用16位。 整数字面量可用十、八或十六进制写(加后缀)。 short无字面量写法。无符号数加后缀u或U:例: 123U,2987654LU,327LU,32014U 无符号整数的算术运算按取模方式做(不会溢出),超出表示范围时取模作为计算结果。 例:若16位unsignd范围为0~65535。计算超范围时取模65536。234U+65500U的计算结果是198U。

  8. 混合类型运算前把小类型转换到大类型的值。 整数提升: 小整数(short、char等)先转为int值再运算。若转换结果超出int范围(unsigned short提升可能出问题),就提升为unsigned。 基本类型相同时,认为无符号类型是比同样的有符号类型更大的类型。例如做下面计算: 2365U + 18764 将先由18764(整型值)转换生成unsigned对应值,再用此新值参与运算。 若转换结果在相应类型里无法表示,结果没有定义。

  9. 基本数据类型的选择 C 语言提供多个浮点类型和整数类型,是供编程者选择使用。对于初学,这方面的选择不太重要。 • 如无特殊需要,浮点数总用double,因其精度和范围能满足一般要求(float精度过低,long double可能低效,一般不用它们)。 • 如无特殊需要,整数总用int。int是C语言里最基本的类型,能得到硬件基本支持,其效率不会低于任何其他整数类型。有时用long类型。 • 如无特殊需要,字符总采用char。 • 尽量少用unsigned类型,除非服务于特殊目的。

  10. 5.2 函数和标准函数库 问题复杂使程序变长。大程序难开发/难阅读理解/难修改;修改时容易破坏完整性,难保证不引进新错误。 程序中常出现许多相同/类似片段。使程序更长,增加不同部分间的联系,损害可修改性。 • 处理复杂问题的基本方法: • 将复杂事物分解为 较简单的部分,分别处理。 • 由各部分的解构造出整个问题的解。 • 复杂概念在简单的基本概念的基础上定义。 • 证明复杂数学定理,常先证明一些引理。

  11. 为支持复杂程序开发,各种程序抽象机制被引进程序语言。使人能把程序片段抽象出来作为整体对待处理。为支持复杂程序开发,各种程序抽象机制被引进程序语言。使人能把程序片段抽象出来作为整体对待处理。 借助这些机制可控制复杂的程序结构,完成复杂的程序或软件系统。抽象机制也是程序结构的组织手段。 C只提供了计算过程的抽象机制,函数。其他语言把这类抽象机制分为 “函数”和“过程”两类。C统称为函数。 后有许多发展,如模块、类型定义、面向对象等。 编写大程序时应特别注意函数分解。 没有合理函数分解不可能完成复杂工作/多花时间/程序更难理解/错误更难发现和纠正。 开始就应强调好的编程习惯(包括函数分解)。

  12. 函数的作用:把一段计算抽象出来,封装(包装)成独立实体。这种封装程序段称为函数定义。函数的作用:把一段计算抽象出来,封装(包装)成独立实体。这种封装程序段称为函数定义。 定义后通过函数名就可以用简便方式要求执行该函数所封装的计算。这种描述片段称为函数调用。 • 函数抽象机制的意义: • 重复片段可用唯一的函数定义和一些形式简单的函数调用取代,使程序更简短清晰 • 同样计算片段只描述一次,易于修改 • 函数定义和使用形成对复杂程序的分解。可独立考虑函数定义与使用,大大提高工作效率 • 具有独立逻辑意义的函数可看作高层基本操作,使人可站在合适的抽象层次上观察把握程序的意义

  13. C语言的库函数 • 基本C语言很小,ANSI C定义了标准库,其中提供最常用的与平台无关的功能。 • 每个符合标准的C系统都提供了标准库,通常还提供一些扩充库,以便使用特定硬件/特定系统的功能: • DOS 上的C系统提供与DOS有关的功能函数 • Windows上的系统提供与 Windows有关的函数 • UNIX系统上的系统提供与UNIX接口的函数 • 扩充库不标准,使用扩充库的程序依赖于具体系统。 库函数实现常用计算,可按规定方式调用,不必自己实现/不必关心怎样实现。开发一次使所有用户受益。

  14. 标准库功能包括输入输出、文件操作、存储管理,其他如数学函数、数据转换函数等。有关介绍散布在各章,后面有详细介绍。标准库功能包括输入输出、文件操作、存储管理,其他如数学函数、数据转换函数等。有关介绍散布在各章,后面有详细介绍。 对具体C系统的扩充库可查阅系统联机帮助,或查阅有关手册、参考书籍。需要学习如何阅读。 下面介绍两组常用标准函数。

  15. 字符分类(<ctype.h>) isalpha(c) c是字母字符 isdigit(c) c是数字字符 isalnum(c) c是字母或数字字符 isspace(c) c是空格、制表符、换行符 isupper(c) c是大写字母 islower(c) c是小写字母 iscntrl(c) c是控制字符 isprint(c) c是可打印字符,包括空格 isgraph(c) c是可打印字符,不包括空格 isxdigit(c) c是十六进制数字字符 ispunct(c) c是标点符号 int tolower(int c) 转为对应小写字母 int toupper(int c) 转为对应大写字母

  16. #include <stdio.h> #include <ctype.h> int main() { int c, cd = 0, cu = 0, cl = 0; while ((c = getchar()) != EOF) { if (isdigit(c)) ++cd; if (isupper(c)) ++cu; if (islower(c)) ++cl; } printf ("digits: %d\n", cd); printf ("uppers: %d\n", cu); printf ("lowers: %d\n", cl); return 0; } 用标准库函数完成判断比自己写条件更合适。

  17. 统计单词程序: #include <stdio.h> #include <ctype.h> int main (){ int count = 0, c; while ((c = getchar()) != EOF) { if (isspace(c)) continue; ++count; while ((c = getchar()) != EOF && !isspace(c) ) ; } printf("Words: %d\n", count); return 0; }

  18. 随机数生成函数应用有时需要带随机性的计算:随机数生成函数应用有时需要带随机性的计算: • 程序调试:用数据做运行试验,随机数据非常合适。 • 计算机模拟:模拟实际情况/过程,探索规律性。客观事物变化有随机性。 • 为此需要生成随机数。 • 计算机只能生成 “伪随机数”。 常用方法是定义某种递推关系,设法使序列中的数比较具有随机性。最常用的是除余定义: a0 = A an = (B × an-1 + C) mod D A,B,D,C为常数,0≤A<D。适当选择B、C可产生0 到 D-1 的较好随机数列。 可以根据这种关系定义随机数生成函数(后面讨论)。

  19. 使用标准库的随机数功能需要包含文件<stdlib.h>。使用标准库的随机数功能需要包含文件<stdlib.h>。 随机数生成函数:int rand(void) 无参数,得到0和符号常量RAND_MAX间的随机整数。 不同系统的RAND_MAX可能不同,为至少32767。 TURBO-C里的RAND_MAX是32767。 函数srand用seed值设种子值: void srand(unsigned seed) 默认初始种子值是1。rand根据当时种子值生成下一随机数并修改种子值。

  20. 下面程序先打印出由默认种子值出发的10个随机数,而后打印设定了新的种子值11后生成的10个随机数:下面程序先打印出由默认种子值出发的10个随机数,而后打印设定了新的种子值11后生成的10个随机数: #include <stdio.h> #include <stdlib.h> int main() { int i; for (i = 0; i < 10; ++i) printf ("%d ", rand()); putchar('\n'); srand(11); for (i = 0; i < 10; ++i) printf ("%d ", rand()); putchar('\n'); return 0; }

  21. 5.3 函数定义和程序的函数分解 库函数有限,编写复杂程序时需要自己定义函数。 一个C程序主要是一系列的函数定义。函数定义形式: 返回值类型 函数名 参数表 函数体 函数头部:描述函数外部与函数内部的联系。 无返回值类型表明函数返回int值(新标准不允许)。应该总写返回类型。无返回值用void说明。 允许任意多个参数。无参函数用(void)(早期C语言对ANSI C的影响),定义函数时可用()。

  22. 主函数main • main的返回值规定为int。这个返回值在程序结束时送给外部。程序外部(如操作系统)可用这个值 • 一般用返回0表示程序正常结束,其他值表示出错 • main以外的函数只有被调用时才会执行 • 不允许调用main int main () { /*一般写法*/ ... ... return 0; }

  23. 程序的函数分解 • 什么样的程序片段应该定义为函数: • 重复出现的相同/相似计算片段,可设法抽取性定义为函数。可能缩短程序,提高可读性和易修改性。 • 长计算过程中有逻辑独立性的片段,即使出现一次也可定义为函数,以分解复杂性。 经验原则:可以定义为函数的东西,就应该定义为函数;一个函数一般不超过一页。 往往存在很多可行的分解,寻找合理有效的分解是需要学习的东西。

  24. 实例研究:字符图形 写程序打印下面这类字符图形 最“简单”的方式:用一系列 putchar 调用打印星号和空格(数清字符个数,就可以写出程序)。

  25. 方法二:程序里写一系列printf调用。例: int main () { /* 打印菱形 */ printf(" * "); printf(" * * "); printf(" * * *); ... ... /* 下略 */ } 不好!每个程序需要写一组输出语句,很罗嗦,没意思 • 若要修改图形大小或形式,程序需全部重写 • 如果需要画出新图形,已做工作对新工作毫无价值 应考虑画这类图形的“基本动作”,定义为函数。

  26. 一种分析(存在许多合理分析): 需要画的图形比较规范,一行有两种情况:1)一段连续字符;2)在两个特定位置的字符。可考虑定义两个函数,其头部分别为: void line(int begin, int end) void points(int fst, int snd) 分别从begin到end输出*,或者在fst和snd输出*。 字符图形未必用星号,可推广,引进一个字符参数 void line(char c, int begin, int end) void points(char c, int fst, int snd)

  27. 虽然函数还没定义,由于其功能很清楚,已经可以用它们输出图形了。例如画菱形:虽然函数还没定义,由于其功能很清楚,已经可以用它们输出图形了。例如画菱形: for (i = 10, j = 10; i > 4; --i, ++j) points('*', i, j); for (i+=2, j-=2; i<=j; ++i,--j) points('*', i, j); /* 10和5是常量,确定菱形的位置和大小 */ 画其他空心或实心的规范图形也不难。 函数使用者只需考虑函数的使用形式和功能,如何基于它们完成工作,无需考虑函数实现(也不应该考虑)。 下面考虑函数实现。 两个函数就是输出一些空格和字符后换行:

  28. void line(char c, int begin, int end) { int i; for (i = 0; i < begin; ++i) putchar(' '); for ( ; i <= end; ++i) putchar(c); putchar('\n'); } void points(char c, int first, int second) { int i; for (i = 0; i < first; ++i) putchar(' '); putchar(c); for (++i ; i < second; ++i) putchar(' '); if (first < second) putchar(c); putchar('\n'); } 做好了一个图形后,修改其大小或形状也比较容易。

  29. 也可引入附加参数,将两个函数合而为一: void line(char c, int begin, int end, int fill) 在fill为0值时在两端点输出,非0时连续输出字符。 还可基于它们定义画各种基本几何图形的函数。设计函数时需要选定合适参数。例如画矩形的函数: void rect(char c, int begin, int len, int high) { ... ... } void rect_fill(char c, int begin, int len, int high) { ... ... } 这些函数又形成另一层抽象层次。定义好这样一组函数后,就可以在新的层次上写字符图形程序了。 上述分解有优点也有缺点。完全可以考虑其他分解

  30. 对函数的两种观点 封装把函数内外隔成两个世界。不同世界形成了对函数的两种观点。函数头规定了两个世界的交流方式。

  31. 函数是独立的逻辑实体。定义后可以调用执行。由此形成对函数的两种观察方式:函数是独立的逻辑实体。定义后可以调用执行。由此形成对函数的两种观察方式: 1)从函数外(以函数使用者的角度)看函数; 2)在函数内(以函数定义者的角度)看函数。 计划函数时,要同时从两个观点看:需要什么函数/参数/返回值?分析确定函数头部,定好公共规范。 写函数定义时应站在内部观点思考/解决问题; 使用函数时应站在外部立场上思考/解决问题。 功能描述清楚,接口定义好以后,函数定义和使用可由不同人做。要求双方遵循共同规范,对函数功能有一致理解。自己写函数时也要保证两种观点的一致性。

  32. 函数定义 解决实现函数的算法问题和语言问题。关注函数调用时应该做什么,如何描述有关的计算过程。 参数是局部变量,调用时用对应实际参数赋初值,然后开始执行函数体。参数像其他局部变量一样可以赋值。 return语句的执行导致函数结束。 return语句的两种形式: return; return 表达式; 函数体里可有多个return语句。在带不带表达式/所带表达式的类型方面必须与函数头部一致。 执行到函数体末端时函数结束(无返回值的函数)。

  33. 函数调用 调用函数时必须提供数目正确/类型合适的实参。调用无参函数时也需要写圆括号。 无返回值函数用在独立的函数调用语句里: pc_area(x + 3); 有返回值的函数通常用在表达式里。返回值也可不用。 例:printf返回int。工作正常时返回实际输出的字符数,出错时返回负值。前面例子都没用。

  34. C函数的参数是值参数。函数调用时先计算实参表达式的值,把值复制给对应形参,而后执行函数体。函数内对形参的操作与实参无关。这种参数意义清晰。C函数的参数是值参数。函数调用时先计算实参表达式的值,把值复制给对应形参,而后执行函数体。函数内对形参的操作与实参无关。这种参数意义清晰。 值传递与赋值和初始化类似,可能出现转换。要求实参值可转换到形参类型,否则是类型错。 语言没有规定调用时的实参求值顺序,对求值顺序敏感的函数调用是错误(与二元运算类似)。例: n = ...; m = gcd(n += 15, n); 另一错误调用: printf("%d, %d", n++, n);

  35. 函数原型 命名对象(变量/函数等)有一个定义点,以及可能多个使用点。基本规则是先定义后使用。 保证正确编译的基本原则:从每个对象的每个使用点向前看,能得到使用该对象的完备信息。 局部变量:变量定义在语句前,保证了先定义后使用。后面变量定义可引用前面已经定义的变量。 函数:使用点所需信息就是函数的类型特征,包括函数名/参数个数和类型/返回值类型等。 调用位置要检查参数个数和类型,是否需要以及能否转换。对返回值也有类似问题。不知道函数的类型特征就无法保证正确完成这些检查和处理。

  36. 将主函数写在最后就是为保证函数定义与使用的关系。将主函数写在最后就是为保证函数定义与使用的关系。 递归定义的函数也没有问题: long fact(long n) { return n<=1 ? 1 : n * fact(n-1); } 有些情况无法通过安排函数定义位置解决。典型例子: int h(int x) { int g(int y) { .... .... .. g(...) .. .. h(...) .. .... .... } } 这是引进函数原型说明的一个原因。更重要的是使人能自由安排函数定义位置,支持大型程序开发。

  37. 原型说明的形式与函数头部类似,加分号。参数名可省略,可与函数定义用的名字不同。原型说明的形式与函数头部类似,加分号。参数名可省略,可与函数定义用的名字不同。 原型的参数名最好用有意义的名字,有利于写注释。 任何可以写定义的地方都可以写原型说明。提倡把原型说明都放在程序文件最前面: 1)使文件里所有调用能看到同一个原型说明。 2)使函数的定义点也能看到同一个原型说明。 原型说明是保证函数定义/使用间一致性的媒介。 为保证原型能起作用,必须给出完整的类型特征 void line(char c, int begin, int end); void points(char c, int fst, int snd);

  38. 过时的函数定义形式/原型形式 C标准化时保留了一些过时东西,以保证老程序合法。这里介绍老形式只是为能读别人写的老程序。 过时函数定义形式。例: double f1(x, y, n) double x, y; int n; { /* 函数体 */ } /*早期高级程序语言中常见的形式 */ 不要使用过时函数定义形式!

  39. 过时原型形式 形式:不给出参数类型,常与变量定义写在一起。这种“简写”原型说明是许多C程序中隐藏错误的根源。 例,设有两个函数在某处定义,完整原型分别是: double f(double); int g(double, int); 程序: int main (void) { int g(), n; double f(), x; .... x = f(3); n = g(n); .... } /* 大错,编译程序不会发现! */ 危害极大,绝不要使用!

  40. 不写函数原型 为宽容老程序,C语言容许不写原型。 遇到调用 f(...),若此前无f定义或原型就做默认处理,方式是假设有一个原型说明: int f(); 出现在包含该函数调用的最内层复合语句的最前面,按此假设对函数调用进行编译和处理。 只有f确实返回int,调用的实参个数和类型都与f的要求完全相同(不需转换)时才能产生正确结果。 不写原型可能引起许多问题。是极危险的编程习惯!

  41. 错误的例子: 下例编译连接都不会出错。运行时无法保证输出什么: #include <stdio.h> int main(){ double x, y; float u = 1.57; x = sin(1); y = sin(u); printf("Strange: %f, %f\n", x, y); return 0; } 用C++系统编译这种C程序可能给出警告。

  42. 为避免函数使用的隐含错误,应坚持正确编程原则:为避免函数使用的隐含错误,应坚持正确编程原则: • 如果使用标准库,必须#include必要的库文件 • 所有使用前未给出定义的函数(无论实际上在哪里定义),都必须给出正确完整的原型说明 • 应把原型说明写在源文件最前面 • 基本原则:使函数的定义点和所有使用点都能“看到”同一个原型说明。 • 坚持这些原则,就能避免函数调用与定义不一致的错误(常常是编译程序检查不到的隐含致命错误)。 简单地说“用C语言编程序容易出错”并不合理。许多错误是由于人们没按正确地方式做事情。

  43. 5.4 程序结构与变量 • C源程序由一系列外部定义和外部说明构成。 • 外部定义/说明:写在程序表层(而不是函数或其他结构内部)的定义/说明。 • 函数只能定义在外部。 • 函数定义是外部定义,写在表层的原型说明是外部说明。外部定义/说明从出现开始起作用直至文件结束。 • #开始的行不是基本语言的东西,下节讨论。 本节讨论由定义/说明位置引起的语义问题。以变量为例。函数的情况简单(不允许在函数内部定义函数) 重要概念:作用域和存在期。

  44. 外部变量 在函数之外定义的变量称为外部变量/全局变量。 外部变量原则上可在程序中任何地方用。由于变量定义从出现处开始起作用,通常把外部变量定义写在源文件最前面,使文件里的函数都可以访问它们。 外部变量可以后定义先使用,或在一个源文件里定义在其他文件使用。为此,使用前应给出外部变量说明。形式与变量定义类似,前面加关键词extern。例: extern int n, m; 外部说明通常放在源文件最前面,供整个文件参照, 。 更重要的:保证整个程序参照同一说明,保证一致性。

  45. 书上例。后面会看到有更有意义的例子。 新情况:如果一个函数定义里用到外部变量,它就依赖这些变量,不再独立了。 • 两点注意: • 定义和说明不同。定义要求创建被定义的对象;说明只指明其存在,必须另有定义,否则该说明无效。有关变量定义与说明的差别下面还要讨论 • 外部变量可在整个程序用,在一个完整程序里不能有重名外部变量。否则连接时会出问题 • 不要与库里东西重名(标准库定义了一批外部名字)。

  46. 作用域与生存期 • 变量定义:1)定义特定类型的变量;2)为变量命名。 • 还确定了: • 变量定义起作用的范围,变量定义的作用域。由定义位置确定,在此范围可通过该名字使用该变量 • 确定变量建立和销毁时间,变量的存在期。各种变量的存在期可能不同。变量实现的基础是内存单元,存在期就是变量被分配内存存储到撤消的期间 作用域/存在期是重要概念。有联系但又不同。弄清它们,许多问题就容易理解了。

  47. 一个定义的作用域是一段源程序,是静态概念。如在函数体开始定义的变量,作用域是整个函数体。一个定义的作用域是一段源程序,是静态概念。如在函数体开始定义的变量,作用域是整个函数体。 存在期是动态概念(程序执行的一段时间)。变量在存在期中保持其存储单元,不经赋值那里的值就不变。 在作用域和存在期方面,外部变量和函数内的普通局部变量(称为自动变量)性质截然不同。 外部变量定义的作用域是整个程序(全局的),这样定义的变量可以在程序中任何地方使用。 自动变量定义的作用域是定义所在的复合语句。在该复合语句之外无效(局部的)。 形参看作函数体的局部变量,作用域是整个函数体。

  48. 从作用域角度看,main也是普通函数,其中的定义是局部定义,作用域是main函数的体。从作用域角度看,main也是普通函数,其中的定义是局部定义,作用域是main函数的体。 外部变量的存在期是程序整个执行期间。程序开始时建立所有外部变量,一直保续到程序结束。 复合语句里定义的变量,存在期是该复合语句的一次执行。复合语句开始时创建,结束时撤消。复合语句再次执行时重建,新变量与撤消的变量无关。自动变量。 for (n = 0; n < 10; n++) { int m; if (n == 0) m = 2; /* 循环第二次到这里时m的值未定 */ }

More Related