高质量软件开发之道
1. 学习"高质量编程"的目的是要在干活的时候一次性编写出高质量的程序,而不是当程序出错后再去修补
2. 十大软件质量属性包括:
正确性(Correctness): 指软件按照需求正确执行任务的能力。正确性是第一重要的软件质量属性。
健壮性(Robustness): 指在异常情况下,软件能够正常运行。健壮性包括容错能力和恢复能力。
可靠性(Reliability): 指在一定环境下,在给定的时间内,系统不发生故障的概率。
性能(Performance): 通常是指软件的时空效率。程序员可通过优化数据结构、算法和代码来提高软件的性能。
易用性(Usability): 指用户使用软件的容易程序。
清晰性(Clarity): 意味着工作成果易读、易理解。
安全性(Security): 指防止系统被非法入侵的能力,即属于技术问题又属于管理问题。
可扩展性(Extendibility): 反映软件适应“变化”的能力。“变化”指需求、设计的变化,算法的改进,程序的变化等。
兼容性(Compatibility): 指两个或以上软件交换信息的能力。新软件应与已流行的软件相兼容,否则难以被市场接受。
可移植性(Portability): 指软件运行于不同软硬件环境的能力。软件设计时应将设备相关和设备无关的程序分开,功能模
块与用户界面分开,这样可以提高软件的可移植性。
3. 软件的高生产率必须以质量合格为前提,如果质量不合格,软件产品要么卖不出去,要么卖出去了再赔偿客户的损失。这种情况下“高生产率”变得毫无意义了。从短期效益看,追求高质量可能会延长软件开发时间,一定程序上降低了生产率。从长期效益看,追求高质量将使软件开发过程更加成熟和规范化。
4. 在某个领域,当市场上只出现尚未形成竞争格局的一个或几个产品时,产品价格基本上是由厂商自己制定,这里称其为“市场价(Marketing price)”。当产品之间形成竞争时,就会出现“杀价”现象。由于各家产品的功能、质量旗鼓相当,竞争实质上是在拼成本。谁的成本低,谁就有利可图。这时的产品价格称为“成本价(Cost price)”
5. CMM(Capability Maturity Model)是用于衡量软件过程能力的标准,分为5个级别,最低为1级,最高为5级。
6. 开发高质量的软件产品必需有条理地组织技术开发活动和项目管理活动,我们把这些活动的组织形式称为过程模型。关注技术开发活动的软件开发模型有“喷泉模型”、“增量模型”、“快速原型模型”、“螺旋模型”、“迭代模型”等。SPP是一个基于CMM3的软件过程模型。
7. 复用有利于提高软件质量、提高生产率和降低成本。
8. 测试的主要目的是为了发现尽可能多的缺陷,而成功的测试在于发现了迄今尚未发现的缺陷。
9. 测试阶段: 单元测试、集成测试、系统测试、验收测试
测试方式: 白盒测试、黑盒测试
测试内容: 功能测试、健壮性测试、性能测试、用户界面测试、安全性测试、压力测试、可靠性测试、安装/反安装测试...
测试流程: 制定测试计划 -> 设计测试用例 -> 执行测试 -> 撰写测试报告 -> 修改错误(回归测试) -> 完成测试
10. 改错过程很像侦破案件,有些坏事发生了,而仅有的信息就是它的确发生了。
11. 老板给程序员发工资,并不是让他们来享受的,而是希望他们创造比工资多得多的效益。所以要求程序员“在上班时间不干与工作无关的事情”是合情合理的。
12. 如果某人在汇报工作时口齿不清、结结巴巴,虽然令听者难受,但尚可接受。倘若说话吞吞吐吐、支支吾吾,则会令人恼火。前者是表达能力差(或者生理问题)导致的,可以被原谅。而后者是工作态度差并导致的,难以被原谅。
13. 为了让会议有成效,应该先让所有与会者明白究竟要做什么。而主持人在发开会通知前,应该先问自己这样的问题:这个会议是否真的重要,即使中断很多人的工作也值得?是否没有其他不影响别人工作的方法来取代开会?这次会议的目的是什么?我该怎样做才能达到目的?如果你能清楚地回答上述问题,一般不会让会议变成漫无目的的讨论。
14. 随时随地记录你在工作中遇到的问题,以及你产生的灵感,不要等到将来再靠回忆来写总结。
15. 项目经理是企业的基层干部,是推动企业发展的中坚分子。优秀的项目经理应该有丰富的产品开发经验和较高的技术水平,懂得管事和管人,有较好的人格魅力。
16. 管理的目的是让大家一起把工作做好,并且让各人获得各自的快乐和满足。当一个团队被出色地领导时,雇员甚至不知道他们已被领导。管理者不能老惦记着自己是一个官,而应时刻意识到自己是责任的主要承担者。如果领导缺乏人格魅力,很少有人会信服你。
17. 软件开发人员的“饭碗”是技术。
18. 有一些机会是靠运气得到的,而更多的机会是自己努力创造的。其实人在任何地方、任何时候都可以学习新知识。职位低、工资少都不可怕,可怕的是丧失了进取心。
19. 不想当将军的士兵不是好士兵。同理,不想当领导的开发人员也不是优秀的开发人员。
20. 缺乏表达能力和管理能力是软件开发人员的通病,值得业界关注。
21. 在允许自由竞争的环境中,如果有人埋怨其才能被“埋没”了,通常是他自己的错。如果真有本事,你就应该自己冒出来,怎么会被“埋没”呢?难道非要等着别人来照顾你不成?
22. 导致软件项目失败的因素很多,如果不去找借口的话,就会发现错误的根源在自己身上:知识贫乏、才能低下、经验不足、骄傲自负、...
23. “迷信”通常是傻子碰到骗子的结果。傻是内因,骗是外因。傻子碰到好人未必能做出好事,傻子碰到另一个骗子就会做出另一件傻事。
技术与技巧
1. 布尔变量与零值比较: if(flag) 或者 if(!flag)
2. 整型变量与零值比较: if(value==0) 或者 if(value!=0)
3. 浮点变量与零值比较: 应将 if(x==0.0) 转化为 if((x>=-EPSINON) && (x<=EPSINON)),其中EPSINON是精度
4. 指针变量与零值比较: if(p==NULL) 或者 if(p!=NULL)
5. 在多重循环中,应当将最长的循环放在最内层,最短的循环放在最外层,以减少CPU跨切循环层的次数。如:
{
for(row=0; row<100; row++)
{
sum = sum + a[row][col];
}
}
6. 如果循环体内存在逻辑判断,并且循环次数很大,宜将逻辑判断移到循环体的外面。如:
{
for(i=0; i<N; i++)
DoSomething();
}
else
{
for(i=0; i<N; i++)
DoOtherthing();
}
7. C++程序中只使用const常量而不使用宏常量,即用const常量完全取代宏常量。
8. 需要对外公开的常量放在头文件中,不需要对外公开的常量放在定义文件的头部。
9. const数据成员的初始化只能在类构造函数的初始化列表中进行。如:
{
public:
Person(int age); //constructor
private:
const int AGE; //const member
};
Person::A(int age):AGE(age) //initialization of AGE
{
//program code
}
Person wang(30); //wang's AGE is 30
Person xiao(20); //xiao's AGE is 20
10. 如果函数参数是指针,且仅做输入用,则应在类型前加const,以防止该指针在函数体内被意外修改。例如:
11. 如果输入参数以值传递的方式传递对象,则宜改用 "const &" 方式来传递,这样可以省去临时对象的构造和析构过程,从而提高效率。
12. 不要省略返回值类型,没有返回值应声明为void类型。
13. 如果函数的返回值是一个对象,有些场合用“引用传递”能提高效率,而有些场合只能用“值传递”,否则会出错。
{
//相加函数只能用“值传递”的方式返回String对象,因为局部引用在函数返回时会销毁
friend String operate+(const String &str1, const String &str2);
public:
//赋值函数应该用“引用传递”的方式返回String对象来提高效率
String & operate=(const String &other);
private:
char *m_data;
};
String & String::operate=(const String &other)
{
if(this == &other)
return *this;
delete m_data;
m_data=new char[strlen(other.data)+1];
strcpy(m_data, other.data);
return *this; //返回的是*this的引用,无需拷贝过程
}
String String::operate+(const String &str1, const String &str2)
{
String temp;
delete temp.data; //temp.data是仅含'\0'的字符串
temp.data=new char[strlen(str1.data)+strlen(str2.data)+1];
strcpy(temp.data, str1.data);
strcat(temp.data, str2.data);
return temp;
}
14. 引用传递的性质像指针传递,而书写方式像值传递,引用的一些规则:
(1) 引用被创建的同时必须被初始化
(2) 不能有NULL引用
(3) 一旦被初始化,就不能改变引用的关系
15. 常见内存错误:
(1)内存分配未成功,却使用了它。使用安全检查解决:assert(p!=NULL); if(p==NULL); if(p!=NULL)
(2)内存分配虽然成功,但是尚未初始化就引用它
(3)内存分配成功并且已经初始化,但操作超过了内存的边界
(4)忘记了释放内存,造成内存泄漏
(5)释放了内存却继续使用它。用free或delete释放了内存后,立即将指针设置为NULL,防止产生“野指针”
16. 指针与数组的对比: 数组要么在静态存储区被创建,要么在栈上被创建。数组名对应着(而不是指向)一块内存,其地址和容量在生命期内保持不变,只有数组的内容可以改变;指针可以随时指向任意类型的内在块,它的特征是“可变”,所以我们常用指针来操作动态内存。
指针与数组的特性比较:
char a[]="hello"; //数组a的容量是6个字符,内容为hello\0
a[0]='X'; //a的内容可以改变
cout<<a<<endl;
char *p="world"; //p指向常量字符串"world",位于静态存储区,内容为world\0
p[0]='X'; //常量字符串的内容不可以修改,编译器不能发现该错误
cout<<p<<endl;
/*内容复制和比较*/
char a[]="hello";
char b[10];
strcpy(b,a); //不能用b=a;否则产生编译错误
if(strcmp(b,a)==0) //不能用if(b==a)
int len=strlen(a); //strlen字符串的长度,不包括'\0'
char *p=(char *)malloc(sizeof(char)*(len+1));
strcpy(p,a); //不要用p=a; p=a是把a的地址赋给了p,要想复制a的内容,可以先用库函数
//malloc为p申请一块容量为strlen(a)+1个字符的内存,再用strcpy进行字符串复制
if(strcmp(p,a)==0) //不能用if(b==a),它比较的是地址,不是内容
/*计算内存容量*/
char a[]="hello world";
char *p=a;
cout<<sizeof(a)<<endl; //12字节,sizeof计算数组的容量,包括'\0'
cout<<sizeof(p)<<endl; //4字节
void Func(char a[100])
{
cout<<sizeof(a)<<endl; //4字节而不是100字节,数组作函数参数传递时,自动退化为同类型的指针
}
17. 野指针
strcpy(p, "hello");
free(p); //p所指的内存被释放,但是p所指的地址仍然不变,delete同理
if(p!=NULL) //没有起到防错作用
{
strcpy(p, "world"); //出错
}
“野指针”的成因主要有三种:
(1) 指针变量没有初始化
(2) 指针p被free或者delete之后,没有置为NULL,让人误以为p是个合法的指针
(3) 指针操作超越了变量的作用范围
18. 有了malloc/free为什么还要new/delete?
malloc/free是C++/C语言的标准库函数,new/delete是C++的运算符。它们都可用于申请动态内存和释放内存
对于非内部数据类型的对象而言,光用malloc/free无法满足动态对象的要求。对象在创建的同时要自动执行构造函数,对象在消亡之前要自动执行析构函数。由于malloc/free是库函数而不是运算符,不在编译器控制权限之内,不能够把执行构造函数和析构函数的任务强加于malloc/free。对于内部数据类型的“对象”没有构造与析构过程,对它们而言,malloc/free和new/delete是等价的。为什么C++不把malloc/free淘汰出局呢?这是因为C++程序经常要调用C函数,而C程序只能用malloc/free管理动态内存。
19. 内存耗尽怎么办?
{
A *a=new A;
if(a==NULL) //判断指针是否为NULL,如果是则马上用return语句终止本函数
{
return;
}
}
void Func(void)
{
A *a=new A;
if(a==NULL) //判断指针是否为NULL,如果是则马上用exit(1)终止整个应用程序
{
cout<<"Memory Exhausted"<<endl;
exit(1);
}
}
20. malloc原型:void *malloc(size_t size);
(1) malloc返回值的类型是void *,所以在调用malloc时要显式地进行类型转换,将void *转换成所需要的指针类型
(2) malloc函数本身并不识别要申请的内存是什么类型,它只关心内存的总字节数
21. 如果p是NULL,free(P)多少次都不会出问题,如果p不是NULL,对p连续操作两次就会导致程序运行错误
22. C++程序调用已经被编译后的C函数方法:
{
vod foo(int x, int y);
//其它函数
}
extern "C"
{
#include"myheader.h"
//其它C头文件
}
23. 全局函数调用时就加"::"标志,如:::Print(...); //表示Print是全局函数而非成员函数
24. 当心隐式类型转换导致重载函数产生二义性
void output(int x); //函数声明
void output(float x); //函数声明
void output(int x)
{
cout<<"output int "<<x<<endl;
}
void output(float x)
{
cout<<"output float "<<x<<endl;
}
void main(void)
{
int x=1;
float y=1.0;
output(x); //output int 1
output(y); //output float 1
output(1); //output int 1
//output(0.5); //error!ambiguous call
output(int(0.5)); //output int 0
output(float(0.5)); //output float 0.5
}
25. 重载与覆盖
成员函数被重载的特征 | 覆盖是指派生类函数覆盖基类函数,特征是 |
相同的范围(在同一个类中) | 不同的范围(分别位于派生类和基类) |
函数名字相同 | 函数名字相同 |
参数不同 | 参数相同 |
virtual关键字可有可无 | 基类函数必须有virtual关键字 |
class Base
{
public:
void f(int x) { cout<<"Base::f(int) "<<x<<endl; }
void f(float x) { cout<<"Base::f(float) "<<x<<endl; }
virtual void g(void) { cout<<"Base::g(void)"<<endl; }
};
class Derived : public Base
{
public:
virtual void g(void) { cout<<"Derived::g(void)"<<endl; }
};
void main(void)
{
Derived d;
Base *pb=&d;
pb->f(42); //Base::f(int) 42
pb->f(3.14f); //Base::f(float) 3.14
pb->g(); //Derived::g(void)
}
26. 隐藏规则
这里的“隐藏”是指派生类的函数屏蔽了与其同名的基类函数,规则如下:
(1) 如果派生类的函数与基类的函数同名,但是参数不同,此时,无论有无virtual关键字,基类的函数将被隐藏(注意别与重载混淆)
(2) 如果派生类的函数与基类的函数同名,并且参数也相同,但是基类函数没有virtual关键字,此时,基类的函数被隐藏(注意别与覆盖混淆)
1>函数Derived::f(float)覆盖了Base::f(float)。
2>函数Derived::g(int)隐藏了Base::g(int),而不是重载。
3>函数Derived::h(float)隐藏了Base::h(float),而不是覆盖。
bp和dp指向同一地址,按理说运行结果应该是相同的,可事实并非这样。
class Base
{
public:
virtual void f(float x) { cout<<"Base::f(float) "<<x<<endl; }
void g(float x) { cout<<"Base::g(float) "<<x<<endl; }
void h(float x) { cout<<"Base::h(float) "<<x<<endl; }
};
class Derived : public Base
{
public:
virtual void f(float x) { cout<<"Derived::f(float) "<<x<<endl; }
void g(int x) { cout<<"Derived::g(int) "<<x<<endl; }
void h(float x) { cout<<"Derived::h(float) "<<x<<endl; }
};
void main(void)
{
Derived d;
Base *pb=&d;
Derived *pd=&d;
//Good: behavior depends solely on type of object
pb->f(3.14f); //Derived::f(float) 3.14
pd->f(3.14f); //Derived::f(float) 3.14
//Bad: behavior depends on type of the pointer
pb->g(3.14f); //Base::g(int) 3.14
pd->g(3.14f); //Derived::g(int) 3 (surprise!)
//Bad: behavior depends on type of the pointer
pb->h(3.14f); //Base::h(float) 3.14 (surprise!)
pd->h(3.14f); //Derived::h(float) 3.14
}
27. 摆脱隐藏
{
public:
void f(int x);
};
class Derived : public Base
{
public:
void f(char *str);
};
void Test(void)
{
Derived *pd=new Derived;
pd->f(10); //error
}
{
public:
void f(char *str);
void f(int x) { Base::f(x); }
};
28. 参数默认值只能出现在函数的声明中,而不能出现在定义体中,如:void Func(int x=0, int y=0);
29. 如果函数有多个参数,参数只能从后向前挨个儿默认,如:
正确:void Func(int x, int y=0, int z=0);
错误:void Func(int x=0, int y, int z=0);
30. 运算符重载
如果运算符被重载为全局函数,那么只有一个参数的运算符叫做一元运算符,有两个参数的运算符叫做二元运算符。
如果运算符被重载为类的成员函数,那么一元运算符没有参数,二元运算符只有一个右侧参数,对象自己成了左侧参数。
31. 默认函数
对于任意一个类A,如果不想编写上述函数,C++编译器将自动为A产生四个默认的函数,如:
A(void); //默认的无参数构造函数
A(const A &a); //默认的拷贝构造函数
~A(void); //默认的析构函数
A & operate=(const A &a); //默认的赋值函数
32. 类的继承与初始化列表
如果类存在继承关系,派生类必须在其初始化表里调用基类的构造函数。例如:
{
A(int x); //A的构造函数
};
class B : public A
{
B(int x, int y); //B的构造函数
};
B::B(int x, int y) : A(x) //在初始化表里调用A的构造函数
{
}
非内部数据类型的成员对象应该采用初始化列表的方式初始化,以获取更高的效率。例如:
{
A(void);
A(const A &a);
A & operate=(const A &other);
};
class B
{
public:
B(const A &a); //B的构造函数
private:
A m_a; //成员对象
};
类B的构造函数在其初始化表里调用了类A的拷贝构造函数,从而将成员对象m_a初始化。
:m_a(a)
{
}
类B的构造函数在函数体内用赋值的方式将成员对象m_a初始化。我们看到的只是一条赋值语句,但实际上B的构造函数干了两件事:先暗地里创建m_a对象(调用了A的无参构造函数),再调用类A的赋值函数,将参数a赋给m_a数,从而将成员对象m_a初始化。
{
m_a=a;
}
33. 构造函数与析构函数
构造从类层次的最根处开始,在每一层中,首先调用基类的构造函数,然后调用成员对象的构造函数。析构则严格按照与构造相反的次序执行,该次序是唯一的,否则编译器将无法自动执行析构过程。成员对象的初始化顺序只由成员对象在类中声明的次序决定。
类String的构造函数和析构函数
String::String(const char *str)
{
if(str==NULL)
{
m_data=new char[1];
*m_data='\0';
}
else
{
int length=strlen(str);
m_data=new char[length+1];
strcpy(m_data, str);
}
}
//String的析构函数
String::~String(void)
{
delete [] m_data;
//由于m_data是内部数据类型,也可以写成delete m_data;
}
34. 拷贝构造函数与赋值函数
由于并非所有的对象都会使用拷贝构造函数和赋值函数,程序员对这两个函数可能有些轻视。请记住以下警告:
(1) 如果不主动编写拷贝构造函数和赋值函数,编译器将以“位拷贝”的方式自动生成默认的函数。倘若类中含有指针变量,那么这两个默认的函数就隐含了错误。以类String的两个对象a, b为例,假设a.m_data的内容为"hello",b.m_data的内容为"world"。现将a赋给b,默认赋值函数的“位拷贝”意味着b.m_data=a.m_data。这将造成三个错误:一是b.m_data原有的内存没有被释放,造成内存泄漏;二是b.m_data和a.m_data指向同一块内存,a或b的任何一方变动都会影响另一方;三是对象被析构时,m_data被释放了两次。
(2) 这两种构造函数非常容易混淆,常导致错写、错用。拷贝构造函数是在对象被创建时调用的,而赋值函数只能被已经存在了的对象调用。以下程序中,第三个语句和第四个语句很相似,你分得清楚哪个调用了拷贝构造函数,哪个调用了赋值函数吗?
String b("world");
String c=a; //调用了拷贝构造函数,最好写成c(a);
c=b; //调用了赋值函数
类String的拷贝构造函数与赋值函数
String::String(const String &other)
{
//允许操作other的私有成员
int length=strlen(other.m_data);
m_data=new char[length+1];
strcpy(m_data, other.m_data);
}
//赋值函数
String & String::operate=(const String &other)
{
//(1)检查自赋值
if(this==&other)
return *this;
//(2)释放原有的内存资源
delete []m_data;
//(3)分配新的内存资源,并复制内容
int length=strlen(other.m_data);
m_data=new char[length+1];
strcpy(m_data, other.m_data);
//(4)返回本对象的引用
return *this;
}
类String的赋值函数比构造数复杂得多,分四步实现:
第一步:检查自赋值。防止出现间接的自赋值。如间接的内容自赋值:b=a; ... c=b; ... a=c; 间接的地址自赋值:b=&a; ... a=*b; 如果不检查自赋值,看看第二步的delete,自杀后还能复制自己吗?
第二步:用delete释放原有的内存资源。如果现在不释放,以后就没机会邓,将造成内存泄漏。
第三步:分配新的内存资源,并复制字符串。注意函数strlen返回的是有效字符长度,不包含结束符'\0'。函数strcpy则边'\0'一起复制。
第四步:返回本对象的引用,目的是为了实现像a=b=c这样的链式表达。能否写成return other呢?效果不是一样吗?不可以!因为我们不知道参数other的生命期。有可能other是个临时对象,在赋值结束后它马上消失,那么return other返回的将是垃圾。