1、第7章内容回顾,哈希函数的构造方法有那些哈希表中处理冲突的方法有那些,高质量编程规范,第八章,预习检查,C语言要经历哪几个编译过程如何申请链表单元,及释放链表单元实现单链表插入的基本语法简述一下快速排序基本理论要点,本章目标,本章概述 阐述如何进行高质量的编程,以及注意事项本章目标 了解高质量编程注意的方方面面 从代码风格,算法,方便调试,性能等。重点 内存分配与释放,悬挂指针,本章结构,程序员的态度,高质量编程规范,微观上高质量,宏观上高质量,8-1 程序员的态度,程序员的弱点不太愿意测试自己的代码 不愿意REVIEW团队队员的代码程序员重点保证自己的代码没有 BUG 来,8-1 程序员的态
2、度,程序员自身可以在程序生成的流程 仔细设计 编写代码单元测试 功能测试 代码 REVIEW,8-1-1 编码的风格,版权和版本的申明 头文件的结构 定义文件的结构 头文件的作用 目录结构 命名规则 注释规则,/* Copyright (c) 2001,上海贝尔有限公司网络应用事业部 * All rights reserved.* * 文件名称:filename.h * 文件标识:见配置管理计划书 * 摘 要:简要描述本文件的内容* * 当前版本:1.1 * 作 者:输入作者(或修改者)名字 * 完成日期:2001年7月20日* * 取代版本:1.0 * 原作者 :输入原作者(或修改者)名字
3、* 完成日期:2001年5月10日,8-1-1-1 版权和版本的申明,版权和版本的声明位于头文件和定义文件的开头,主要内容有: (1)版权信息。 (2)文件名称,标识符,摘要。 (3)当前版本号,作者/修改者,完成日期。 (4)版本历史信息。 范例,8-1-1-2 头文件的结构,头文件由三部分内容组成: 头文件开头处的版权和版本声明。 预处理块。 函数和类结构声明等。 范例 为了防止头文件被重复引用,应当用ifndef/define/endif结构产生预处理块。 用 #include 格式来引用标准库的头文件(编译器将从标准库目录开始搜索)。 用 #include “filename.h” 格
4、式来引用非标准库的头文件(编译器将从用户的工作目录开始搜索)。 头文件中只存放“声明”而不存放“定义” 不提倡使用全局变量,尽量不要在头文件中出现象extern int value 这类声明。,8-1-1-3 定义文件的结构,定义文件有三部分内容: 定义文件开头处的版权和版本声明 对一些头文件的引用 程序的实现体(包括数据和代码) 范例,/ 版权和版本声明#include “graphics.h” / 引用头文件 / 全局函数的实现体 void Function1() ,8-1-1-4 头文件的作用,通过头文件来调用库功能头文件能加强类型安全检查,8-1-1-5 目录结构,特点: 便于维护 通
5、常应将头文件和定义文件分别保存于不同的目录 加强信息隐藏 :如果某些头文件是私有的,它不会被用户的程序直接引用,则没有必要公开其“声明” 范例:Network 工程建立三个目录source:存放工程源文件,如:server.c Client.cInclude:存放工程头文件,如:server.h Client.hLib:存放工程库文件,如:tipr.lib stdio.lib,8-1-1-6 命名规则,主要思想:在变量和函数名中加入前缀以增进人们对程序的理解具体规则:标识符应当直观且可以拼读,可望文知意,不必进行“解码”。标识符的长度应当符合“min-length & max-informat
6、ion”原则命名规则尽量与所采用的操作系统或开发工具的风格保持一致程序中不要出现仅靠大小写区分的相似的标识符程序中不要出现标识符完全相同的局部变量和全局变量变量的名字应当使用“名词”或者“形容词名词”用正确的反义词组命名具有互斥意义的变量或相反动作的函数等,8-1-2 程序的版式,空行 代码行 代码行内的空格 代码对齐 长行拆分 修饰符的位置 注释,8-1-2-1 空行,空行起着分隔程序段落的作用。空行得体(不过多也不过少)将使程序的布局更加清晰。空行不会浪费内存空行规则 每个函数定义结束之后都要加空行 在一个函数体内,逻揖上密切相关的语句之间不加空行,其它地方应加空行分隔。,8-1-2-2
7、代码行规则,一行代码只做一件事情 如只定义一个变量,或只写一条语句。这样的代码容易阅读,并且方便于写注释。if、for、while、do等语句自占一行,执行语句不得紧跟其后。不论执行语句有多少都要加。尽可能在定义变量的同时初始化该变量(就近原则),8-1-2-3 代码行内的空格,关键字之后要留空格函数名之后不要留空格,紧跟左括号(,以与关键字区别(向后紧跟,)、,、;向前紧跟,紧跟处不留空格,之后要留空格二元操作符的前后应当加空格。一元操作符前后不加空格。象“”、“.”、“-”这类操作符前后不加空格。,8-1-2-4 对齐和拆分规则,对齐规则 程序的分界符和应独占一行并且位于同一列,同时与引用
8、它们的语句左对齐 之内的代码块在右边数格处左对齐。长行拆分规则 代码行最大长度宜控制在70至80个字符以内 长表达式要在低优先级操作符处拆分成新行,操作符放在新行之首(以便突出操作符)。拆分出的新行要进行适当的缩进,使排版整齐,语句可读,8-1-2-5 长行拆分规则,代码行最大长度宜控制在70至80个字符以内 长表达式要在低优先级操作符处拆分成新行,操作符放在新行之首(以便突出操作符) 例:,if (very_longer_variable1 = very_longer_variable12),8-1-2-6 修饰符的位置,修饰符 * 和 修饰符 紧靠变量名 例如:char *name;int
9、 *x, y; / 此处y不会被误解为指针,x? y?,8-1-2-7 注释,C语言的注释符为“/*/” 行注释一般采用“/” 注释通常用于 版本、版权声明; 函数接口说明; 重要的代码行或段落提示。,8-1-2-7 注释,注释规则注释是对代码的“提示”,而不是文档如果代码本来就是清楚的,则不必加注释。边写代码边注释注释应当准确、易懂,防止注释有二义性尽量避免在注释中使用缩写,特别是不常用缩写。注释的位置应与被描述的代码相邻当代码比较长,应当在一些段落的结束处加注释,8-1-2-7 注释,注释实例,/* * 函数介绍: * 输入参数: * 输出参数: * 返回值 : */ void Funct
10、ion(float x, float y, float z) ,阶段总结,版权的申明和头文件的结构及作用 命名的规则 代码的版式规则 空行 对齐 拆分 代码的注释,8-2 微观上的高质量,程序的健壮性 防止内存泄漏 编程的优化,8-2-1 程序的健壮性,使用断言复合表达式If语句使用const提高函数的健壮性,8-2-1-1 使用断言,程序一般分为Debug版本和Release版本断言assert是仅在Debug版本起作用的宏 断言优势:跟踪程序运行,帮助调试输出错误原因可以自定义,8-2-1-1 使用断言,使用断言规则:使用断言捕捉不应该发生的非法情况在函数的入口处,使用断言检查参数的有效性
11、(合法性)。一旦确定了的假定,就要使用断言对假定进行检查。如果“不可能发生”的事情的确发生了,则要使用断言进行报警,8-2-1-2 复合表达式,复合表达式例子a = b = c = 0 优势: 书写简洁 提高编译效率,8-2-1-2 复合表达式,复合表达式使用规则 不要编写太复杂的复合表达式。 例子:i = a = b 不要把程序中的复合表达式与“真正的数学表达式”混淆 if (a b c) 与 if (ab) & (bc),8-2-1-3 If语句,布尔变量与零值比较 不可将布尔变量直接与TRUE、FALSE或者1、0进行比较。 例子:,假设布尔变量名字为flag,它与零值比较的标准if语句
12、如下: if (flag) / 表示flag为真 if (!flag) / 表示flag为假其它的用法都属于不良风格,例如:if (flag = TRUE) if (flag = 1 ) if (flag = FALSE) if (flag = 0),8-2-1-3 If语句,整型变量与零值比较 整型变量用“=”或“!=”直接与0比较 例子:,假设整型变量的名字为value,它与零值比较的标准if语句如下: if (value = 0) if (value != 0)不可模仿布尔变量的风格而写成 if (value) / 会让人误解 value是布尔变量 if (!value),8-2-1-3
13、 If语句,浮点变量与零值比较 不可将浮点变量用“=”或“!=”与任何数字比较 设法转化成“=”或“=.EPSINON) & (x=EPSINON)其中EPSINON是允许的误差(即精度)。,假设整型变量的名字为value,它与零值比较的标准if语句如下: if (value = 0) if (value != 0)不可模仿布尔变量的风格而写成 if (value) / 会让人误解 value是布尔变量 if (!value),8-2-1-3 If语句,指针变量与零值比较 指针变量用“=”或“!=”与NULL比较 例子:,与零值比较的标准if语句如下:if (p = NULL)/ p与NULL
14、显式比较,强调p是指针变量if (p != NULL) 不要写成if (p = 0) / 容易让人误解p是整型变量if (p != 0) 或者if (p) / 容易让人误解p是布尔变量if (!p),8-2-1-4 使用const提高函数的健壮性,const 用法:定义常量 修饰函数的参数 修饰函数的返回值 修饰函数的定义体,8-2-1-4-1 用const修饰函数的参数,const只能修饰输入参数 特点 如果输入参数采用“指针传递”,那么加const修饰可以防止意外地改动该指针,起到保护作用 例:void StringCopy(char *strDestination, const char
15、 *strSource); 如果输入参数采用“引用传递 ”, 可以避免修改参数值的值传递 void Func(const A &a),8-2-1-4-2 用const修饰函数的返回值,如果给以“指针传递”方式的函数返回值加const修饰,那么函数返回值(即指针)内容不能被修改 例如函数const char * GetString(void); 如下语句将出现编译错误:char *str = GetString(); 正确的用法是const char *str = GetString();如果函数返回值采用“值传递方式”,由于函数会把返回值复制到外部临时的存储单元中,加const修饰没有任何价值
16、,8-3-1 防止内存泄漏,内存分配方式 malloc/free 的使用要点 常见的内存错误及其对策 引用与指针的比较 指针与数组的对比 指针参数是如何传递内存的 动态内存自动释放 杜绝“野指针”,8-3-1-1 内存分配方式,内存分配方式有三种:从静态存储区域分配 在栈上创建 从堆上分配,亦称动态内存分配 malloc或new free或delete,8-3-1-2 malloc/free 的使用要点,malloc 语法:void * malloc(size_t size); 作用:申请一块长度为length的整数类型的内存 例子:int *p = (int *) malloc(sizeof
17、(int) * length) Free 语法:void free( void * memblock ) 作用:释放内存 例子:free(p) 如果p是NULL指针,那么free对p无论操作多少次都不会出问题 如果p不是NULL指针,那么free对p连续操作两次就会导致程序运行错误。,8-3-1-3 常见的内存错误及其对策,常见的内存错误 内存分配未成功,却使用了它 内存分配虽然成功,但是尚未初始化就引用它 内存分配成功并且已经初始化,但操作越过了内存的边界 忘记了释放内存,造成内存泄露 释放了内存却继续使用它,8-3-1-3 常见的内存错误及其对策,内存管理的规则 用malloc之后,应该立
18、即检查指针值是否为NULL。防止使用指针值为NULL的内存。不要忘记为数组和动态内存赋初值。防止将未被初始化的内存作为右值使用。避免数组或指针的下标越界,特别要当心发生“多1”或者“少1”操作。动态内存的申请与释放必须配对,防止内存泄漏。用free释放了内存之后,立即将指针设置为NULL,防止产生“野指针”。,8-3-1-4 引用与指针的比较,表示符差别 引用 & 指针 *引用的规则: 引用被创建的同时必须被初始化(指针则可以在任何时候被初始化)。不能有NULL引用,引用必须与合法的存储单元关联(指针则可以是NULL)。一旦引用被初始化,就不能改变引用的关系(指针则可以随时改变所指的对象)。,
19、void Func1(int x) x = x + 10;printf( “x= %dn”,x); int n = 0; Func1(n);printf( “n1= %dn”,n);,8-3-1-4 引用与指针的比较,函数的参数和返回值的传递方式 值传递 例:,输出结果:x = 10 n = 0,void Func2(int *x) (* x) = (* x) + 10;printf( “x= %dn”,x); int n = 0; Func1(,8-3-1-4 引用与指针的比较,函数的参数和返回值的传递方式 指针传递 例:,输出结果:x = 10 n = 10,void Func3(int
20、,8-3-1-4 引用与指针的比较,函数的参数和返回值的传递方式 引用传递,输出结果:x = 10 n = 10,8-3-1-5 指针与数组的对比,差别 数组: 要么在静态存储区被创建(如全局数组),要么在栈上被创建 数组名对应着(而不是指向)一块内存 其地址与容量在生命期内保持不变 只有数组的内容可以改变 指针: 随时指向任意类型的内存块 动态生存 在数据堆,8-3-1-5 指针与数组的对比,差别 内存的容量大小 例:1例2,char a = “hello world“;char *p = a;sizeof(a) ? / 12字节sizeof(p) ? / 4字节,void Func(cha
21、r a100) sizeof(a) ? / 4字节而不是100字节 ,8-3-1-5 指针与数组的对比,动态内存会自动释放 不会自动释放 例:指针应该注意的特性 指针消亡了,并不表示它所指的内存会被自动释放。 内存被释放了,并不表示指针会消亡或者成了NULL指针。,void Func(void) char *p = (char *) malloc(100); / 动态内存会自动释放吗? ,8-3-1-6 指针参数是如何传递内存的,*如果函数的参数是一个指针,不要指望用该指针去申请动态内存,void GetMemory(char *p, int num) p = (char *)malloc(s
22、izeof(char) * num); void Test(void) char *str = NULL;GetMemory(str, 100);/ str 仍然为 NULL strcpy(str, “hello“); / 运行错误 ,8-3-1-6 指针参数是如何传递内存的,*如果函数的参数是一个指针,不要指望用该指针去申请动态内存,void GetMemory2(char *p, int num) *p = (char *)malloc(sizeof(char) * num); void Test(void) char *str = NULL;GetMemory( / 运行错误 ,8-3-
23、1-6 指针参数是如何传递内存的,* 不要用return语句返回指向“栈内存”的指针,char *GetString(void) char p = “hello world“;return p; / 编译器将提出警告 void Test4(void) char *str = NULL;str = GetString();/ str 的内容是垃圾printf(“%sn”,str ); ,8-3-1-6 指针参数是如何传递内存的,* 不要用return语句返回指向“栈内存”的指针,char *GetString(void) char *p = “hello world“;return p; / 编
24、译器将提出警告 void Test4(void) char *str = NULL;str = GetString();/ str 的内容是垃圾printf(“%sn”,str ); ,如果换成常量该如何?,8-3-1-7 动态内存自动释放,free函数特点 释放指针所指的内存 没有销毁指针,故指针地址仍然不变(非NULL) 例:,char *p = (char *) malloc(100); strcpy(p, “hello”); free(p); / p 所指的内存被释放,但是p所指的地址仍然不变 if(p != NULL) strcpy(p, “world”); / 出错 ,没有起到防错
25、作用,8-3-1-8 杜绝“野指针”,什么是“野指针” 不是NULL指针 是指向“垃圾”内存的指针 If(p!=NULL)不能起到作用野指针的成因 指针变量没有被初始化 指针初始化: char *p = NULL; char *str = (char *) malloc(100); 指针p被free或者delete之后,没有置为NULL,8-3-2 编程的优化,为什么需要常量 Const与#define的比较 常量定义规则 循环语句的效率 For语句的循环控制变量,8-3-2-1 为什么用常量,常量是一种标识符,它的值在运行期间恒定不变。 用 #define来定义常量(称为宏常量),还可以用c
26、onst来定义常量为什么要常量 程序的可读性 在程序的很多地方输入同样的数字或字符串,难保不发生书写错误。 如果要修改数字或字符串,则会在很多地方改动,既麻烦又容易出错。尽量使用含义直观的常量来表示那些将在程序中多次出现的数字或字符串。 例如: #define MAX 100 /* C语言的宏常量 */ const int MAX = 100; / C+ 语言的const常量 const float PI = 3.14159; / C+ 语言的const常量,8-3-2-2 const 与 #define的比较,两种区别比较 const常量有数据类型,而宏常量没有数据类型。 编译器可以对前者进
27、行类型安全检查。而对后者只进行字符替换,没有类型安全检查 有些集成化的调试工具可以对const常量进行调试,但是不能对宏常量进行调试。常量定义规则 需要对外公开的常量放在头文件中,不需要对外公开的常量放在定义文件的头部 为便于管理,可以把不同模块的常量集中存放在一个公共的头文件中。 如果某一常量与其它常量密切相关,应在定义中包含这种关系,而不应给出一些孤立的值。 例如: const float RADIUS = 100; const float DIAMETER = RADIUS * 2;,8-3-2-3 循环语句的效率,在多重循环中,如果有可能,应当将最长的循环放在最内层,最短的循环放在最外
28、层,以减少CPU跨切循环层的次数。如果循环体内存在逻辑判断,并且循环次数很大,宜将逻辑判断移到循环体的外面。,for (row=0; row100; row+) for ( col=0; col5; col+ )sum = sum + arowcol; ,if (condition) for (i=0; iN; i+) DoSomething(); else for (i=0; iN; i+) DoOtherthing(); ,8-3-2-4 for语句的循环控制变量,循环控制变量规则 不可在for 循环体内修改循环变量,防止for 循环失去控制 建议for语句的循环控制变量的取值采用“半开半
29、闭区间”写法 例子:,半开半闭区间,闭区间,for (int x=0; xN; x+) ,for (int x=0; x=N.1; x+) ;,8-3-2-5 Pragma预处理,其格式一般为: #Pragma Para 其中Para 为参数,下面来看一些常用的参数。 #pragma message 编译信息输出窗口中输出相应的信息,这对于源代码信息的控制是非常重要的。其使用方法为: #Pragma message(“消息文本”) #pragma once 只要在头文件的最开始加入这条指令就能够保证头文件被编译一次,这条指令实际上在VC6中就已经有了,但是考虑到兼容性并没有太多的使用它。 pr
30、agma comment(.) 该指令将一个注释记录放入一个对象文件或可执行文件中。 常用的lib关键字,可以帮我们连入一个库文件。,8-3-2-5 Pragma预处理,#pragma pack() 对于想结构体Struct这样的类型,VC采用8字节对齐的方式,如果我们不想使用8字节对齐(在网络变成中经常需要这样),我们可以在结构体前面加上 #pragma pack(1) struct #pragma pack() #pragma warning #pragma warning( disable : 4507 34; once : 4385; error : 164 ) 等价于: #pragm
31、a warning(disable:4507 34) / 不显示4507和34号警告信息 #pragma warning(once:4385) / 4385号警告信息仅报告一次 #pragma warning(error:164) / 把164号警告信息作为一个错误。,阶段总结,数值与0或者NULL的比较操作 内存分配注意细节 野指针 如何避免内存泄露 Const与define数据的差别 为什么要const数据,本章总结,程序员的态度,高质量编程规范,微观上高质量,宏观上高质量,主要讲述程序员在编程中的态度,讲述了程序编码风格和版式中具体细节,深入讲述C语言中内存管理和程序的健壮性和对程序编码的优化。,实验1,实验内容 编写strcpy函数-已知strcpy函数的原型是char *strcpy(char *strDest, const char *strSrc);其中strDest是目的字符串,strSrc是源字符串。实验目的 编码的规范以及代码注释; const指针和内存的操作; assert函数错误检测利用; 内存的释放和数据的返回值;实验分析 函数注释说明; 参数定义和内存申请和释放; assert数据检测和野指针检测实现; 数据返回值;,