码迷,mamicode.com
首页 > 其他好文 > 详细

《程序设计实践》

时间:2015-05-28 00:35:33      阅读:147      评论:0      收藏:0      [点我收藏+]

标签:

第1章 风格

1.1 名字

全局变量使用具有说明性的名字,局部变量使用短名字。全局变量可以出现在整个程序中的任何地方,因此它们的名字应该足够长,具有足够的说明性。

人们常常鼓励程序员使用长的变量名,而不管用在什么地方。这种认识完全是错误的,清晰性往往是随着简洁而来的。

  1. leap_year = ((y%4 == 0) && (y%100 != 0)) || (y%400 == 0);

1.4 函数宏

避免函数宏

函数宏最常见的一个严重问题是:如果一个参数在定义中出现多次,它就可能被多次求值。下面的实现是错误的。

  1. #define isupper(c) ((c) >= ‘A‘ && (c) <= ‘Z‘)
  2. while (isupper( c = getchar()))
  3. ...

给宏的体和参数都加上括号

1.5神秘的数

神秘的数包括各种常数、数组的大小、字符位置、变换因子以及程序中出现的其它以文字形式写出的数值。

把数定义为常数,不要定义为宏

1.6注释

给函数和全局数据加注释

1.7为何如此费心

书写良好的代码更容易阅读和理解,几乎可以保证其中的错误更少。

第2章 算法与数据结构

2.9散列表

由于散列表是链接表的数组,其基本元素类型与链表相同。

  1. typedef struct Nameval Nameval;
  2. struct Nameval{
  3. char *name;
  4. int value;
  5. Nameval *next; /*in chain */
  6. }
  7. Nameval *symtab[NHASH]; /* a symbol table*/

数组到底应该取多大?

普遍的想法是要求它足够大,使每个链表至多只有几个元素,以保证查询能够是O(1)操作。

hash函数

对于字符串,最常见的散列算法之一是:逐个把字节加到已经构造的部分散列值的一个倍数上。根据经验,在对于ASCII串的散列函数中,选择3137作为乘数是很好的。(注:另有times33算法)

  1. enum { MULTIPLIER = 31};
  2. /*hash: compute hash value of string*/
  3. unsigned int hash(char* str)
  4. {
  5. unsigned int h;
  6. unsigned char *p;
  7. h = 0;
  8. for (p = (unsigned char*)str; *p != ‘\0‘; p++)
  9. h = MULTIPLTER * h += *p;
  10. return h % NHASH;
  11. }

times33经典实现算法

  1. uint nHash = 0;
  2. while (*key)
  3. nHash = (nHash << 5) + nHash + *key++;
  4. return nHash;

对于一类输入集合(例如短的变量名字)工作得非常好的散列函数,也可能对另一类输入集合(例如URL)工作得很差

第3章 设计与实现

数据结构是程序构造过程的中心环节。一旦数据结构安排好了,算法就像是瓜熟蒂落,编码也比较容易

马尔可夫链算法

算法描述

设置w1w2为文本的前两个词
输出w1w2
循环:

随机地选出w3,它是文本中w1w2的后缀中的一个

打印w3

把w1w2替换成w2w3

重复循环

下图与上述描述不符,后文的程序是严格按照上面的伪算法描述来写的,没有下图描述的智能。

技术分享

前缀中词的个数为2(NPREF)。如果将前缀缩短,产生出来的句子将趋向于无聊词语,更加缺乏内聚力;如果加长前缀,则趋向于产生原始输入的逐字拷贝。对于英语文本而言,用两个词的前缀选择第三个是一个很好的折衷方式。

  1. enum {
  2. NPREF = 2, /*number of prefix word*/
  3. NHASH = 4093, /*size of state hash table array*/
  4. MAXGEN = 10000 /*maximum words generated*/
  5. };
  6. struct State{ /* prefix + suffix list */
  7. char *pref[NPREF]; /*prefix words*/
  8. Suffix *suf; /*list of suffixes*/
  9. State *next; /*next in hash table*/
  10. };
  11. struct Suffix {/* list of suffixes*/
  12. char *word; /*suffix*/
  13. Suffix *next; /*next in list of suffixes*/
  14. };
  15. State *statetab[NHASH]; /*hash table of states*/

整个数据结构看起来是这样的:
技术分享

哈希函数对前缀字符串进行散列
技术分享
查找:

技术分享

在读入文件的同时构造散列表:

  1. /*build : read input, build prefix table*/
  2. void build( char* prefix[NPREF], FILE* f)
  3. {
  4. char buf[100], fmt[10];
  5. /* create a format string; %s could overflow buf */
  6. sprintf( fmt, "%%%ds", sizeof(buf) - 1);
  7. while ( fscanf( f, fmt, buf) != EOF)
  8. add( prefix, estrdup(buf));
  9. }

说明:以%s作为格式符调用fscanf,那就是要求把文件里的下一个由空白界定的词读入缓冲区。但是,假如在这种情况下没有长度限制,特别长的词就可能导致输入缓冲区溢出,从而酿成大祸。假设 缓冲区的大小 为100个字节(这远远超出正常文本中可能出现的词的长度),我们可以用%99s(留一个字符给串的结束符‘\0‘),这是告诉fscanf读到99个字符就结束。这样做有可能把长的词分成段,虽然是不幸的,但却是安全的。

  1. /*add: add word to suffix list, update prefix */
  2. void add(char* prefix[NPREF], char* suffix)
  3. {
  4. State* sp;
  5. sp = lookup(prefix, 1); /*create if not found*/
  6. addsuffix(sp, suffix);
  7. /* move the words down the prefix */
  8. memmove(prefix, prefix+1, (NPREF-1)*sizeof(prefix[0]));
  9. prefix[NPREF-1] = suffix;
  10. }

memmove用于从src拷贝count个字符到dest,如果目标区域和源区域有重叠的话,memmove能够保证源串在被覆盖之前将重叠区域的字节拷贝到目标区域中。但复制后src内容会被更改。但是当目标区域与源区域没有重叠则和memcpy函数功能相同。原型:void memmove( void dest, const void* src, size_t count );

  1. /* addsuffix: add to state, suffix must not change later*/
  2. void addsuffix(State* sp, char* suffix)
  3. {
  4. Suffix* suf;
  5. suf = (Suffix*)emalloc(sizeof(Suffix));
  6. suf->word = suffix;
  7. suf->next = sp->suf;
  8. sp->suf = suf;
  9. }

产生输出

  1. char NONWORD[] = "\n"; /*cannot appear as real word*/
  2. /* generate: produce output, one word per line */
  3. void generate(int nwords)
  4. {
  5. State* sp;
  6. Suffix* suf;
  7. char* prefix[NPREF],
  8. *w;
  9. int i, nmatch;
  10. for (i = 0; i < NPREF; i++)/* reset initial prefix*/
  11. prefix[i] = NONWORD;
  12. for (i = 0; i < nwords; i++) {
  13. sp = lookup(prefix, 0);
  14. nmatch = 0;
  15. for ( suf = sp->suf; suf != NULL; suf = suf->next)
  16. if (rand() % ++nmatch == 0) /*prob = 1/nmatch */
  17. w = suf->word;
  18. if (strcmmp(w, NONWORD) == 0)
  19. break;
  20. printf("%s\n",w);
  21. memmove(prefix, prefix+1, (NPREF-1)*sizeof(prefix[0]));
  22. prefix[NPREF-1] = w;
  23. }
  24. }

变量nmatch用于在扫描后缀表的过程中记录匹配的个数。表达式:rand()% ++nmatch == 0增加nmatch的值,而且使它为真的概率总是1/nmatch。在扫描过中,每一个项被选中的概率为1/nmatch。

  1. /* markov main: markov-chain random text generation*/
  2. int main(void)
  3. {
  4. int i, nwords = MAXGEN;
  5. char* prefix[NPREF]; /*current input prefix*/
  6. for (i = 0; i < NPREF; i++) /*set up initial prefix*/
  7. prefix[i] = NONWORD;
  8. build(prefix, stdin);
  9. add(prefix, NONWORD);
  10. generate(nwords);
  11. return 0;
  12. }

C++版

  1. typedef deque<string> Prefix;
  2. map<Prefix, vector<string>> statetab; //prefix->suffixes
  3. //markov main: markov-chain random text generation
  4. int main(void)
  5. {
  6. int nwords = MAXGEN;
  7. Prefix prefix; //current input prefix
  8. for ( int i = 0; i < NPREF; i++) //set up initial prefix
  9. add(prefix, NONWORD);
  10. build(prefix, cin);
  11. add(prefix, NONWORD);
  12. generate(nwords);
  13. return 0;
  14. }
  15. //build: read input words , build state table
  16. void build(Prefix& prefix, istram& in)
  17. {
  18. string buf;
  19. while (in >> buf)
  20. add(prefix, buf);
  21. }
  22. //add: add word to suffix list, update prefix
  23. void add(Prefix& prefix, const string& s)
  24. {
  25. if (prefix.size() == NPREF) {
  26. statetab[prefix].push_back(s);
  27. prefix.pop_front();
  28. }
  29. prefix.push_back(s);
  30. }
  31. //generate: produce output, one word per line
  32. void generate(int nwords)
  33. {
  34. Prefix prefix;
  35. int i;
  36. for ( i = 0; i <NPREF; i++) //reset initial prefix
  37. add(prefix, NONWORD);
  38. for ( i = 0; i < nwords; i++) {
  39. vector<string>& suf = statetab[prefix];
  40. const string& w = suf[rand() % suf.size()];
  41. if ( w == NONWORD)
  42. break;
  43. cout << w << endl;
  44. prefix.pop_front(); //advance
  45. prefix.push_back(w);
  46. }
  47. }

第5章 DEBUG

检查最近的改动仔细检查最近的改动能帮助问题定位。

第6章 测试

产生无错代码的一个途径是用程序来生成代码如果所用的生成器或者翻译器是正确的,给出的描述完全正确,那么作为结果的程序必然也是正确的。

测试代码的边界情况

测试前条件和后条件验证在某段代码执行前所期望的或必须满足的性质(前条件)、执行的性质(后条件)是否成立。

例如,链表节点为空不能执行->next操作,函数执行后的返回值。

使用断言

如果一个程序有逆计算,那么就检查通过该逆计算能否重新得到输入

比较相互独立的实现,或许你可以写出一个程序的某种简单版本,作为一个慢的但却又是独立的参照物。

度量测试的覆盖面测试的一个目标是保证程序里的每个语句在一系列测试过程中都执行过,如果不能保证程序的每一行都在测试中至少经过了一次执行,那么这个测试就不能说是完全的。

花点时间写一个脚本程序或者一个简单程序,用它包装起来所有的测试是非常值得做的,这能使一个完整的测试集可以通过(文字或者图形)一个按键而得到执行。

自动回归测试回归测试实际上有一个隐含假定,假定程序以前的版本产生的输出是正确的。**

第7章 性能

第8章 可移植性

char的符号问题char数据类型到底是有符号的还是无符号的,C和C++并不没有对此给出明确规定。在结合了char和int的代码里,这个问题就有可能造成麻烦,例如getchar()函数得到int值,调用它的代码就会出现问题。

  1. char c; /*should be int*/
  2. c = getchar();

如果char是无符号类型,条件s[i] == EOF总为false,EOF在stdio.h里通常定义为-1。如果char是有符号类型,那么代表正常字符的0xFF将会被被视为结束符。

  1. int i;
  2. char s[MAX];
  3. for (i = 0; i < MAX -1 ; i++)
  4. if (( s[i] = getchar()) == ‘\n‘ || s[i] == EOF)
  5. break;
  6. s[i] = ‘\0‘;

无论char的符号情况如何,你都必须把getchar的返回值存入一个int以便与EOF做比较

  1. int c, i;
  2. char s[MAX];
  3. for (i = 0; i < MAX -1 ; i++) {
  4. if (( c = getchar()) == ‘\n‘ || c == EOF)
  5. break;
  6. s[i] = c;
  7. }
  8. s[i] = ‘\0‘;

位域对机器的依赖太强,无论如何都不应该用它。

不要用char与EOF做比较

程序组织达到可移植性的方式,最重要的有两种,我们将它们称为联合的方式和取交集的方式。

联合方式使用各个特殊途径的最佳特征,采用条件式编译和安装,根据各个具体环境的特殊情况进行处理。优点是可以利用各系统在能力方面的优点。缺点是安装过程的规模和复杂性,由代码中大量费解的编译条件造成的复杂性等。

取交集的方式,即:只体表和那些在所有目标系统里都存在的特性,绝不使用那些并不是到处都能用的特征。强求使用普遍可用特性也有危险,这可能限制了目标系统的范围,或者限制了程序的功能。此外,也可能在某些系统里导致性能方面的损失。

把系统依赖性隐藏在界面后面抽象是一种强有力的技术,应该通过它划清程序的可移植性部分与不可移植部分之间的界限。(congnima注:例如java中的File类)

数据交换文本数据很容易从一个系统搬到另一个系统去,这是在不同系统间交换任意信息的最简单方式。

字节序

书中提供了一种比较笨的方法来判断系统的字节序

  1. /* byteorder: display bytes of a long */
  2. int main(void)
  3. {
  4. unsigned long x;
  5. unsigned char* p;
  6. int i;
  7. /* 11 22 33 44 => big-endian*/
  8. /* 44 33 22 11 => little-endian*/
  9. /* x = 0x1122334455667788UL; for 64-bit long*/
  10. x = 0x11223344UL;
  11. p = (unsigned char* )&x;
  12. for ( i = 0; i < sizeof(long); i++)
  13. printf("%x ",*p++);
  14. printf("\n");
  15. return 0;
  16. }

引自《C语言深度剖析》

  1. bool checkSystem()
  2. {
  3. union chech
  4. {
  5. int i;
  6. char ch;
  7. }c;
  8. c.i = 1;
  9. //return true means little-endian
  10. return (c.ch == 1);

第9章 记法

采用正确的语言有可能使某个程序的书写变得容易许多。

思考printf及cout各自的优缺点

语法分析树
技术分享
一旦这种树构造好了,我们就有很多可能的方法来处理它。最直接的方法是在树中运动,并在这个过程中求出各个结点的值。

版本1:

  1. typedef struct Symbol Symbol;
  2. typedef struct Tree Tree;
  3. struct Symbol {
  4. int value;
  5. char *name;
  6. };
  7. struct Tree {
  8. int op;/*operation code*/
  9. int value;/*value if number*/
  10. Symbol *symbol;/*Symbol entry if variable*/
  11. Tree *left;
  12. Tree *right;
  13. };
  14. /*eval: version 1: evaluate tree expression*/
  15. int eval(Tree* t)
  16. {
  17. int left, right;
  18. switch (t->op) {
  19. case NUMBER:
  20. return t->value;
  21. case VARIABLE:
  22. return t->symbol->value;
  23. case ADD:
  24. return eval(t->left) + eval(t->right);
  25. case DIVIDE:
  26. left = eval(t->left);
  27. right = eval(t->right_;
  28. if (right == 0)
  29. eprintf("divide %d by zero",left);
  30. return left / right;
  31. case MAX:
  32. left = eval(t->left);
  33. right = eval(t->right);
  34. return left>right ? left : right;
  35. case ASSIGN:
  36. t->left->symbol->value = eval(t->right);
  37. return t->left->symbol->value;
  38. /*...*/
  39. }
  40. }

版本2:

  1. /* addop: return sum of two tree expressions */
  2. int addop( Tree* t)
  3. {
  4. return eval(t->left) + eval(t->right);
  5. }
  6. /*...*/
  7. enum { /*operation codes, Tree.op*/
  8. NUMBER,
  9. VARIABLE,
  10. ADD,
  11. DIVIDE,
  12. /*...*/
  13. };
  14. /* optab: operator function table */
  15. int (*optab[])(Tree *) = {
  16. pushop, /*NUMBER*/
  17. pushsymop, /*VARIABLE*/
  18. addop, /*ADD*/
  19. divop, /*DIVIDE*/
  20. /*..*/
  21. };
  22. /* eval: version 2: evaluate tree from operator table */
  23. int eval(Tree *t)
  24. {
  25. return (*optab[t->op])(t);
  26. }

上面这个递归函数已经计算了整个语法树

版本3:

  1. typedef union Code Code;
  2. union Code {
  3. void (*op)(void); /*function if operator*/
  4. int value; /*value if number*/
  5. Symbol *symbol; /*Symbol entry if variable*/
  6. };
  7. /*generate: generate instructions by walking tree*/
  8. int generate(int codep, Tree* t)
  9. {
  10. switch (t->op) {
  11. case NUMBER:
  12. code[codep++].op = pushop;
  13. code[codep++].value = t->value;
  14. return codep;
  15. case VARIABLE:
  16. code[codep++].op = pushsymop;
  17. code[codep++].symbol = t->symbol;
  18. return codep;
  19. case ADD:
  20. codep = generate(codep,t->left);
  21. codep = generate(codep,t->right);
  22. code[codep++].op = addop;
  23. return codep;
  24. case DIVIDE:
  25. codep = generate(codep,t->left);
  26. codep = generate(codep,t->right);
  27. code[codep++].op = divop;
  28. return codep;
  29. case MAX:
  30. /*...*/
  31. }
  32. }
  33. Code code[NCODE];
  34. int stack[NSTACK];
  35. int stackp;
  36. int pc; /*program counter*/
  37. /*eval: version 3: evaluate expression from generated code*/
  38. int eval(Tree* t)
  39. {
  40. pc = generate(0,t);
  41. code[pc].op = NULL;
  42. stackp = 0;
  43. pc = 0;
  44. while ( code[pc].op != NULL)
  45. (*code[pc++].op)();
  46. return stack[0];
  47. }
  48. /*pushop: push number; value is next word in code stream*/
  49. void pushop(void)
  50. {
  51. stack[stackp++] = code[pc++].value;
  52. };
  53. /*divop: compute ratio of two expressions*/
  54. void divop(void)
  55. {
  56. int left, right;
  57. right = stack[--stackp];
  58. left = stack[--stackp];
  59. if (0 == right)
  60. eprintf("divide %d by zero\n", left);
  61. stack[stackp++] = left / right;
  62. }




《程序设计实践》

标签:

原文地址:http://www.cnblogs.com/codetravel/p/4534607.html

(0)
(0)
   
举报
评论 一句话评论(0
登录后才能评论!
© 2014 mamicode.com 版权所有  联系我们:gaon5@hotmail.com
迷上了代码!