预习检查
谈谈什么是好的编程风格?
什么是内存泄露,如何防范内存泄露?
什么是野指针,如何杜绝?
课程目标
本章概述
重点
阐述如何进行高质量的编程,以及注意事项
本章目标
了解高质量编程注意的方方面面
从代码风格,算法,方便调试,性能等。
内存分配与释放,悬挂指针
本章结构
高质量编程规范
高质量编程规范
宏观上高质量
宏观上高质量
程序员的态度
程序员的态度
微观上高质量
微观上高质量
7.1 程序员的态度 程序员的弱点
不太愿意测试自己的代码 不愿意REVIEW团队队员的代码
程序员重点
保证自己的代码没有 BUG 来
7.1 程序员的态度 程序员自身该做的工作:
仔细设计
编写代码 单元测试 功能测试 代码 REVIEW
7.2.1 编码的风格
版权和版本的申明
头文件的结构
定义文件的结构
头文件的作用
目录结构
命名规则
注释规则
7.2.1.1 版权和版本的申明 版权和版本的声明位于头文件和定义文件的开头,主
要内容有:
(1)版权信息。 (2)文件名称,标识符,摘要。 (3)当前版本号,作者/修改者,完成日期。
(4)版本历史信息。 范例
/* Copyright (c) 2001,上海贝尔有限公司网络应用事业部 /* Copyright (c) 2001,上海贝尔有限公司网络应用事业部
* All rights reserved.*
* All rights reserved.*
* 文件名称:filename.h * 文件名称:filename.h
* 文件标识:见配置管理计划书 * 文件标识:见配置管理计划书
*摘 要:简要描述本文件的内容* *摘 要:简要描述本文件的内容*
* 当前版本:1.1 * 当前版本:1.1
*作 者:输入作者(或修改者)名字 *作 者:输入作者(或修改者)名字
* 完成日期:2001年7月20日* * 完成日期:2001年7月20日*
* 取代版本:1.0 * 取代版本:1.0
* 原作者 :输入原作者(或修改者)名字
* 原作嵌者入:式输家入原园作w者w(w或.e修m改be者dc)lu名b.字com
* 完成日期:2001年5月10日 * 完成日期:2001年5月10日
上海嵌入式家园-开发板商城 http://embedclub.taobao.com/
7.2.1.2头文件的结构
头文件由三部分内容组成:
范例
头文件开头处的版权和版本声明。
预处理块。
函数和类结构声明等。
为了防止头文件被重复引用,应当用ifndef/define/endif结构产生预处 理块。
用 #include <filename.h> 格式来引用标准库的头文件(编译器将从 标准库目录开始搜索)。
用 #include “filename.h” 格式来引用非标准库的头文件(编译器 将从用户的工作目录开始搜索)。
头文件中只存放“声明”而不存放“定义” 不提倡使用全局变量,尽量不要在头文件中出现象extern int value
这类声明。
7.2.1.3定义文件的结构
定义文件有三部分内容:
范例
定义文件开头处的版权和版本声明
对一些头文件的引用
程序的实现体(包括数据和代码)
// 版权和版本声明
#include “graphics.h” ...
// 全局函数的实现体 void function1(...)
// 引用头文件
{
...
}
7.2.1.4 头文件的作用 通过头文件来调用库功能
头文件能加强类型安全检查
7.2.1.5 目录结构 特点:
便于维护 通常应将头文件和定义文件分别保存于不同的目录
加强信息隐藏 :如果某些头文件是私有的,它不会被用户的程序直接引用,则没有必要
公开其“声明”
范例:
Network 工程建立三个目录
source:存放工程源文件,如:server.c client.c Include:存放工程头文件,如:server.h client.h Lib:存放工程库文件,如:tipr.lib stdio.lib
7.2.1.6 命名规则 匈牙利命名规则主要思想:在变量和函数名中加入前缀以增进人们对程序的
理解 具体规则:
标识符应当直观且可以拼读,可望文知意,不必进行“解码”。
标识符的长度应当符合“min-length && max-information”原则
命名规则尽量与所采用的操作系统或开发工具的风格保持一致
程序中不要出现仅靠大小写区分的相似的标识符
程序中不要出现标识符完全相同的局部变量和全局变量
变量的名字应当使用“名词”或者“形容词+名词”
用正确的反义词组命名具有互斥意义的变量或相反动作的函数等
7.2.2 程序的版式
空行
代码行
代码行内的空格
代码对齐
长行拆分
修饰符的位置
注释
7.2.2.1 空行 空行起着分隔程序段落的作用。空行得体(不过多也不过少)将使
程序的布局更加清晰。
空行不会浪费内存
空行规则
每个函数定义结束之后都要加空行
在一个函数体内,逻揖上密切相关的语句之间不加空行,其它地方应加
空行分隔。
7.2.2.2 代码行规则 一行代码只做一件事情
如只定义一个变量,或只写一条语句。这样的代码容易阅读,并且方便于写
注释。
if、for、while、do等语句自占一行,执行语句不得紧跟其后。不 论执行语句有多少都要加{}。
尽可能在定义变量的同时初始化该变量(就近原则)
7.2.2.3 代码行内的空格
关键字之后要留空格 函数名之后不要留空格,紧跟左括号‘(’,以与关键字区别 ‘(’向后紧跟,‘)’、‘,’、‘;’向前紧跟,紧跟处不留空格 ‘,’之后要留空格
二元操作符的前后应当加空格。 一元操作符前后不加空格。 象“[]”、“.”、“->”这类操作符前后不加空格。
7.2.2.4 对齐和拆分规则 对齐规则
程序的分界符‘{’和‘}’应独占一行并且位于同一列,同时与引用它 们的语句左对齐
{ }之内的代码块在‘{’右边数格处左对齐。
长行拆分规则
代码行最大长度宜控制在70至80个字符以内
长表达式要在低优先级操作符处拆分成新行,操作符放在新行之首(以
便突出操作符)。拆分出的新行要进行适当的缩进,使排版整齐,语句
可读
7.2.2.5 长行拆分规则 代码行最大长度宜控制在70至80个字符以内
长表达式要在低优先级操作符处拆分成新行,操作符放
在新行之首(以便突出操作符)
例:
if ((very_longer_variable1 >= very_longer_variable12)
&& (very_longer_variable3 <= very_longer_variable14) && (very_longer_variable5 <= very_longer_variable16))
{
dosomething();
}
7.2.2.6 修饰符的位置 修饰符 * 和 &
修饰符 紧靠变量名
x? y?
例如:
char *name;
int *x, y; // 此处y不会被误解为指针
7.2.2.7 注释
C语言的注释符为“/*...*/”
行注释一般采用“//...”
注释通常用于
版本、版权声明;
函数接口说明;
重要的代码行或段落提示。
7.2.2.7 注释 注释规则
注释是对代码的“提示”,而不是文档
如果代码本来就是清楚的,则不必加注释。
边写代码边注释
注释应当准确、易懂,防止注释有二义性
尽量避免在注释中使用缩写,特别是不常用缩写。
注释的位置应与被描述的代码相邻
当代码比较长,应当在一些段落的结束处加注释
7.2.2.7 注释 注释实例
/*
* 函数介绍:
* 输入参数:
* 输出参数:
* 返回值 :
*/
void Function(float x, float y, float z) {
...
}
阶段小节
版权的申明和头文件的结构及作用
命名的规则
代码的版式规则
空行 对齐 拆分
代码的注释
7.3 微观上的高质量
程序的健壮性
防止内存泄漏
编程的优化
7.3.1程序的健壮性
使用断言 复合表达式 If语句
使用const提高函数的健壮性
7.3.1.1使用断言
程序一般分为Debug版本和Release版本
断言assert是仅在Debug版本起作用的宏
断言优势:
跟踪程序运行,帮助调试
输出错误原因
可以自定义
7.3.1.1使用断言 使用断言规则:
使用断言捕捉原本不应该发生的非法情况 在函数的入口处,使用断言检查参数的有效性(合法性)。 一旦确定了的假定,就要使用断言对假定进行检查。 如果“不可能发生”的事情的确发生了,则要使用断言进行报警
7.3.1.2 复合表达式 复合表达式例子
优势:
a=b=c=0
书写简洁 提高编译效率
7.3.1.2 复合表达式 复合表达式使用规则
不要编写太复杂的复合表达式。
例子:i = a >= b && c < d && c + f <= g + h ;
不要有多用途的复合表达式
d = (a = b + c) + r ;
不要把程序中的复合表达式与“真正的数学表达式”混淆 if (a < b < c) 与 if ((a<b) && (b<c))
7.3.1.3 if语句
布尔变量与零值比较
不可将布尔变量直接与TRUE、FALSE或者1、0进行比较。 例子:
假设布尔变量名字为flag,它与零值比较的标准if语句如下: if (flag) // 表示flag为真
if (!flag) // 表示flag为假
其它的用法都属于不良风格,例如:
if (flag == TRUE) if (flag == 1 )
if (flag == FALSE) if (flag == 0)
7.3.1.3 if语句
整型变量与零值比较
整型变量用“==”或“!=”直接与0比较 例子:
假设整型变量的名字为value,它与零值比较的标准if语句如下: if (value == 0)
if (value != 0)
不可模仿布尔变量的风格而写成
if (value) // 会让人误解 value是布尔变量 if (!value)
7.3.1.3 if语句
浮点变量与零值比较
不可将浮点变量用“==”或“!=”与任何数字比较
设法转化成“>=”或“<=”形式
例子:
假设浮点变量的名字为x,应当将
if (x == 0.0) // 隐含错误的比较 转化为
if ((x>=.EPSINON) && (x<=EPSINON))
其中EPSINON是允许的误差(即精度)。
7.3.1.3 If语句
指针变量与零值比较
指针变量用“==”或“!=”与NULL比较 例子:
与零值比较的标准if语句如下:
if (p == NULL)// p与NULL显式比较,强调p是指针变量 if (p != NULL)
不要写成
if (p == 0) // 容易让人误解p是整型变量 if (p != 0)
或者
if (p) if (!p)
// 容易让人误解p是布尔变量
7.3.1.4 使用const提高函数的健壮性 const 用法:
定义常量
修饰函数的参数
修饰函数的返回值
7.3.1.4.1 用const定义常量 const类型定义:指明变量或对象的值是不能被更新,引
入目的是为了取代预编译指令。
可以保护被修饰的东西,防止意外的修改,增强程序的
健壮性。
编译器通常不为普通const常量分配存储空间,而是将它 们保存在符号表中,这使得它成为一个编译期间的常量 ,没有了存储与读内存的操作,使得它的效率也很高。
可以节省空间,避免不必要的内存分配。
7.3.1.4.2 用const修饰函数的参数 const只能修饰输入参数
特点
如果输入参数采用“指针传递”,那么加const修饰可以防止意外地 改动该指针,起到保护作用
例:void StringCopy(char *strDestination, const char *strSource); 如果输入参数采用“引用传递 ”, 可以避免修改参数值的值传递
void func(const A &a)
7.3.1.4.3 用const修饰函数的返回值 如果给以“指针传递”方式的函数返回值加const修饰,
那么函数返回值(即指针)内容不能被修改
例如函数
const char * GetString(void); 如下语句将出现编译错误:
char *str = GetString(); 正确的用法是
const char *str = GetString();
如果函数返回值采用“值传递方式”,由于函数会把返回 值复制到外部临时的存储单元中,加const修饰没有任 何价值
7.3.2 防止内存泄漏
内存分配方式
malloc/free 的使用要点 常见的内存错误及其对策 引用与指针的比较 指针与数组的对比 指针参数是如何传递内存的 动态内存自动释放 杜绝“野指针”
7.3.2.1内存分配方式
内存分配方式有三种:
从静态存储区域分配
在栈上创建
从堆上分配,亦称动态内存分配 malloc或new
free或delete
7.3.2.2 malloc/free 的使用要点
malloc
free
语法:void * malloc(size_t size); 作用:申请一块长度为length的整数类型的内存 例子:int *p = (int *) malloc(sizeof(int) * length)
语法:void free( void * memblock )
作用:释放内存
例子:free(p) 如果p是NULL指针,那么free对p无论操作多少次都不会出问题 如果p不是NULL指针,那么free对p连续操作两次就会导致程序运行错误。
7.3.2.3 常见的内存错误及其对策 常见的内存错误 :
内存分配未成功,却使用了它
内存分配虽然成功,但是尚未初始化就引用它
内存分配成功并且已经初始化,但操作越过了内存的边界
忘记了释放内存,造成内存泄漏
释放了内存却继续使用它
7.3.2.3 常见的内存错误及其对策 内存管理的规则
用malloc之后,应该立即检查指针值是否为NULL。防止使用指针值为NULL的内 存。
不要忘记为数组和动态内存赋初值。防止将未被初始化的内存作为右值使用。 避免数组或指针的下标越界,特别要当心发生“多1”或者“少1”操作。 动态内存的申请与释放必须配对,防止内存泄漏。 用free释放了内存之后,立即将指针设置为NULL,防止产生“野指针”。
7.3.2.4 引用与指针的比较
表示符差别
引用 -- & 指针-- *
引用的规则:
引用被创建的同时必须被初始化(指针则可以在任何时候被初始化
)。
不能有NULL引用,引用必须与合法的存储单元关联(指针则可以是 NULL)。
一旦引用被初始化,就不能改变引用的关系(指针则可以随时改变
所指的对象)。
7.3.2.4 引用与指针的比较 函数的参数和返回值的传递方式
值传递 例:
void func1(int x) {
x = x + 10;
printf( “x= %d\n”,x);
输出结果:
}
...
int n = 0; func1(n);
x = 10 n=0
printf( “n1= %d\n”,n);
7.3.2.4 引用与指针的比较 函数的参数和返回值的传递方式
指针传递 例:
void func2(int *x) {
(* x) = (* x) + 10;
输出结果:
printf( “x= %d\n”,x);
}... intn=0; func2(&n);
x = 10 n =10
printf( “n = %d\n”,n);
7.3.2.4 引用与指针的比较 函数的参数和返回值的传递方式
引用传递
void func3(int &x) {
x = x+10;
printf( “x= %d\n”,x);
输出结果:
}... intn=0; func3(n);
x = 10 n =10
printf( “n1 = %d\n”,n);
7.3.2.5 指针与数组的对比 差别
数组: 要么在静态存储区被创建(如全局数组),要么在栈上被创建
数组名对应着(而不是指向)一块内存
其地址与容量在生命期内保持不变
只有数组的内容可以改变
指针: 随时指向任意类型的内存块 通用使用指针操作动态内存
7.3.2.5 指针与数组的对比 差别
内存的容量大小 例:1
例2
char a[] = "hello world"; char*p =a;
sizeof(a) ?
// sizeof(a)= 12字节 sizeof(p) ?
}
// sizeof(a)= 4字节
void func(char a[100]) {
sizeof(a) ?
// sizeof(a)==sizeof(char *) //4字节而不是100字节
注意 :当数组作为函数的参数进行传递时,
嵌入式家园 www.embedclub.com该数组自动退化为同类型的指针
上海嵌入式家园-开发板商城 http://embedclub.taobao.com/
7.3.2.5 指针与数组的对比
分析下面代码:
char *p = (char *) malloc(100); strcpy(p, “hello”);
free(p);
...
if( p != NULL) {
}
strcpy(p, “world”);
free函数只是把指针所指的内存给释放掉 ,但并没有把指针本身干掉。
p为野指针
嵌入式家园 www.embedclub.com
上海嵌入式家园-开发板商城 http://embedclub.taobao.com/
7.3.2.5 指针与数组的对比 动态内存不会自动释放
例:
void Func(void)
{
}
char *p = (char *) malloc(100);
// 动态内存会自动释放吗?
指针应该注意的特性
指针消亡了,并不表示它所指的内存会被自动释放。 内存被释放了,并不表示指针会消亡或者成了NULL指针。
7.3.2.6 指针参数是如何传递内存的 ***如果函数的参数是一个指针,不要指望用该指针去申请动态内存
void GetMemory(char *p, int num) {
p = (char *)malloc(sizeof(char) * num); }
void Test(void) {
}
char *str = NULL; GetMemory(str, 100);
// str 仍然为 NULL strcpy(str, "hello");
// 运行错误
7.3.2.6 指针参数是如何传递内存的 ***如果函数的参数是一个指针,不要指望用该指针去申请动态内存
void GetMemory(char **p, int num) {
*p = (char *)malloc(sizeof(char) * num); }
void Test(void) {
char *str = NULL; GetMemory(&str, 100);
strcpy(str, "hello"); }
7.3.2.6 指针参数是如何传递内存的
*** 用return语句返回指向“堆内存”的指针
char *GetMemory( int num) {
char *p = (char *)malloc(sizeof(char) * num);
return p; }
void Test(void) {
}
char *str = NULL;
str = GetMemory(100); strcpy(str, "hello"); printf(“%s\n”, str); free(str);
7.3.2.6 指针参数是如何传递内存的
*** 不要用return语句返回指向“栈内存”的指针
char *GetString(void) {
char p[] = "hello world";
returnp; //编译器将提出警告 }
void Test4(void) {
}
char *str = NULL;
str = GetString();
// str 的内容是垃圾 printf(“%s\n”,str );
7.3.2.6 指针参数是如何传递内存的
*** 不要用return语句返回指向“栈内存”的指针
char *GetString(void) {
char *p = "hello world";
returnp; //编译器将提出警告 }
void Test4(void) {
}
char *str = NULL;
str = GetString(); printf(“%s\n”,str );
7.3.2.7动态内存自动释放 free函数特点
没有起到 防错作用
char *p = (char *) malloc(100);
strcpy(p, “hello”);
free(p); // p 所指的内存被释放,但是p所指的地址仍然不变 ...
if(p != NULL)
{
释放指针所指的内存 没有销毁指针,故指针地址仍然不变(非NULL)
}
例:
strcpy(p, “world”); // 出错
7.3.2.8 杜绝“野指针”
什么是“野指针” 不是NULL指针
是指向“垃圾”内存的指针 if(p!=NULL)不能起到作用
野指针的成因
指针变量没有被初始化 指针初始化:
char *p = NULL;
char *str = (char *) malloc(100);
指针p被free或者delete之后,并没有置为NULL
7.3.3 编程的优化 引入常量
const与#define的比较 常量定义规则 循环语句的效率 for语句的循环控制变量
7.3.3.1 为什么用常量 常量是一种标识符,它的值在运行期间恒定不变。
用 #define来定义常量(称为宏常量),还可以用const来定义常量
为什么要常量
增加程序的可读性
在程序的很多地方输入同样的数字或字符串,难保不发生书写错误。
如果要修改数字或字符串,则会在很多地方改动,既麻烦又容易出错。
尽量使用含义直观的常量来表示那些将在程序中多次出现的数字或字符串。
例如:
#define MAX 100 /* C语言的宏常量 */
const int MAX = 100; // C++ 语言的const常量
const float PI = 3.14159; // C++ 语言的const常量
7.3.3.2 const 与 #define的比较
两种区别比较
const常量有数据类型,而宏常量没有数据类型。 编译器可以对前者进行类型安全检查。而对后者只进行字符替换,没有类型安全检
查 有些集成化的调试工具可以对const常量进行调试,但是不能对宏常量进行调试。
常量定义规则
需要对外公开的常量放在头文件中,不需要对外公开的常量放在定义文件的头部
为便于管理,可以把不同模块的常量集中存放在一个公共的头文件中。
如果某一常量与其它常量密切相关,应在定义中包含这种关系,而不应给出一些孤
立的值。
例如:
const float RADIUS = 100;
const float DIAMETER = RADIUS * 2;
7.3.3.4循环语句的效率 在多重循环中,如果有可能,应当将最长的循环放在最内层,最短的循环放
在最外层,以减少CPU跨切循环层的次数。 for (row=0; row<100; row++)
{
{
} }
for ( col=0; col<5; col++ )
sum = sum + a[row][col];
如果循环体内存在逻辑判断,并且循环次数很大,宜将逻辑判断移到循环体
的外面。
if (condition) {
} else {
嵌入式家园 www.embedclub.comfor (i=0; i<N; i++) DoOtherthing();
}
for (i=0; i<N; i++)
DoSomething();
上海嵌入式家园-开发板商城 http://embedclub.taobao.com/
7.3.3.5 for语句的循环控制变量
循环控制变量规则
不可在for 循环体内修改循环变量,防止for 循环失去控制 建议for语句的循环控制变量的取值采用“半开半闭区间”写法
例子:
半开半闭区间
闭区间
for (int x=0; x<N; x++) for (int x=0; x<=N-1; x++) {{
...... }};
7.3.3.4 pragma预处理 其格式一般为: #pragma para
其中para 为参数,下面来看一些常用的参数。 #pragma message
编译期间,在编译信息输出窗口中输出相应的信息,这对于源代码信息
的控制是非常重要的。其使用方法为:
#pragma message(“消息文本”) #pragma once
只要在头文件的最开始加入这条指令就能够保证头文件被编译一次。
pragma comment(...)
该指令将一个注释记录放入一个对象文件或可执行文件中。
7.3.3.4 pragma预处理 #pragma pack()
改变程序代码段的对齐方式。比如对于结构体struct这样的类型,我们可 以在结构体前面加上
#pragma pack(4) //设定为4字节对齐 struct { ...... }
#pragma pack()
#pragma warning
#pragma warning( disable : 4507 34; once : 4385; error : 164 ) 等价于:
#pragma 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数据检测和野指针检测实现; 数据返回值;
git@github.com:Kevin-Dfg/Data-Structures-and-Algorithm-Analysis-in-C.git