博文

高质量C++/C编程指南13(转帖)(2006-01-15 10:52:00)

摘要:第11章 其它编程经验
11.1 使用const提高函数的健壮性
 看到const关键字,C++程序员首先想到的可能是const常量。这可不是良好的条件反射。如果只知道用const定义常量,那么相当于把火药仅用于制作鞭炮。const更大的魅力是它可以修饰函数的参数、返回值,甚至函数的定义体。
 const是constant的缩写,“恒定不变”的意思。被const修饰的东西都受到强制保护,可以预防意外的变动,能提高程序的健壮性。所以很多C++程序设计书籍建议:“Use const whenever you need”。
 
11.1.1 用const修饰函数的参数
 如果参数作输出用,不论它是什么数据类型,也不论它采用“指针传递”还是“引用传递”,都不能加const修饰,否则该参数将失去输出功能。
 const只能修饰输入参数:
如果输入参数采用“指针传递”,那么加const修饰可以防止意外地改动该指针,起到保护作用。
 例如StringCopy函数:
  void StringCopy(char *strDestination, const char *strSource);
其中strSource是输入参数,strDestination是输出参数。给strSource加上const修饰后,如果函数体内的语句试图改动strSource的内容,编译器将指出错误。 如果输入参数采用“值传递”,由于函数将自动产生临时变量用于复制该参数,该输入参数本来就无需保护,所以不要加const修饰。
 例如不要将函数void Func1(int x) 写成void Func1(const int x)。同理不要将函数void Func2(A a) 写成void Func2(const A a)。其中A为用户自定义的数据类型。 对于非内部数据类型的参数而言,象void Func(A a) 这样声明的函数注定效率比较底。因为函数体内将产生A类型的临时对象用于复制参数a,而临时对象的构造、复制、析构过程都将消耗时间。
 为了提高效率,可以将函数声明改为void Func(A &a),因为“引用传递”仅......

阅读全文(2760) | 评论:0

高质量C++/C编程指南12(转帖)(2006-01-15 10:51:00)

摘要:第10章 类的继承与组合  对象(Object)是类(Class)的一个实例(Instance)。如果将对象比作房子,那么类就是房子的设计图纸。所以面向对象设计的重点是类的设计,而不是对象的设计。
 对于C++程序而言,设计孤立的类是比较容易的,难的是正确设计基类及其派生类。本章仅仅论述“继承”(Inheritance)和“组合”(Composition)的概念。
 注意,当前面向对象技术的应用热点是COM和CORBA,这些内容超出了C++教材的范畴,请阅读COM和CORBA相关论著。
10.1 继承
 如果A是基类,B是A的派生类,那么B将继承A的数据和函数。例如:
 class A
 {
  public:
  void  Func1(void);
  void  Func2(void);
};  class B : public A
 {
  public:
  void  Func3(void);
  void  Func4(void);
};  main()
 {
  B  b;   
  b.Func1();  // B从A继承了函数Func1
  b.Func2();  // B从A继承了函数Func2
  b.Func3();
  b.Func4();
 }
 
 这个简单的示例程序说明了一个事实:C++的“继承”特性可以提高程序的可复用性。正因为“继承”太有用、太容易用,才要防止乱用“继承”。我们应当给“继承”立一些使用规则。
 
【规则10-1-1】如果类A和类B毫不相关,不可以为了使B的功能更多些而让B继承A的功能和属性。不要觉得“白......

阅读全文(2361) | 评论:0

高质量C++/C编程指南11(转帖)(2006-01-15 10:51:00)

摘要:第9章 类的构造函数、析构函数与赋值函数
 构造函数、析构函数与赋值函数是每个类最基本的函数。它们太普通以致让人容易麻痹大意,其实这些貌似简单的函数就象没有顶盖的下水道那样危险。
 每个类只有一个析构函数和一个赋值函数,但可以有多个构造函数(包含一个拷贝构造函数,其它的称为普通构造函数)。对于任意一个类A,如果不想编写上述函数,C++编译器将自动为A产生四个缺省的函数,如
 A(void);     // 缺省的无参数构造函数
 A(const A &a);    // 缺省的拷贝构造函数
 ~A(void);     // 缺省的析构函数
 A & operate =(const A &a); // 缺省的赋值函数  这不禁让人疑惑,既然能自动生成函数,为什么还要程序员编写?
 原因如下:
(1)如果使用“缺省的无参数构造函数”和“缺省的析构函数”,等于放弃了自主“初始化”和“清除”的机会,C++发明人Stroustrup的好心好意白费了。
(2)“缺省的拷贝构造函数”和“缺省的赋值函数”均采用“位拷贝”而非“值拷贝”的方式来实现,倘若类中含有指针变量,这两个函数注定将出错。
 
 对于那些没有吃够苦头的C++程序员,如果他说编写构造函数、析构函数与赋值函数很容易,可以不用动脑筋,表明他的认识还比较肤浅,水平有待于提高。
 本章以类String的设计与实现为例,深入阐述被很多教科书忽视了的道理。String的结构如下:
 class String
 {
   public:
  String(const char *str = NULL); // 普通构造函数
  String(const String &other); // 拷贝构造函数
  ~ S......

阅读全文(2364) | 评论:0

高质量C++/C编程指南10(转帖)(2006-01-14 10:16:00)

摘要:第8章 C++函数的高级特性
 对比于C语言的函数,C++增加了重载(overloaded)、内联(inline)、const和virtual四种新机制。其中重载和内联机制既可用于全局函数也可用于类的成员函数,const与virtual机制仅用于类的成员函数。
 重载和内联肯定有其好处才会被C++语言采纳,但是不可以当成免费的午餐而滥用。本章将探究重载和内联的优点与局限性,说明什么情况下应该采用、不该采用以及要警惕错用。
8.1 函数重载的概念
8.1.1 重载的起源
 自然语言中,一个词可以有许多不同的含义,即该词被重载了。人们可以通过上下文来判断该词到底是哪种含义。“词的重载”可以使语言更加简练。例如“吃饭”的含义十分广泛,人们没有必要每次非得说清楚具体吃什么不可。别迂腐得象孔已己,说茴香豆的茴字有四种写法。
 在C++程序中,可以将语义、功能相似的几个函数用同一个名字表示,即函数重载。这样便于记忆,提高了函数的易用性,这是C++语言采用重载机制的一个理由。例如示例8-1-1中的函数EatBeef,EatFish,EatChicken可以用同一个函数名Eat表示,用不同类型的参数加以区别。  
 void EatBeef(…);       // 可以改为     void Eat(Beef …);
 void EatFish(…);       // 可以改为     void Eat(Fish …);
 void EatChicken(…);    // 可以改为     void Eat(Chicken …);
  
示例8-1-1 重载函数Eat  C++语言采用重载机制的另一个理由是:类的构造函数需要重载机制。因为C++规定构造函数与类同名(请参见第9章),构造函数只能有一个名字。如果想用几种不同的方法创建对象......

阅读全文(2016) | 评论:0

高质量C++/C编程指南09(转帖)(2006-01-14 10:15:00)

摘要:第7章 内存管理
 欢迎进入内存这片雷区。伟大的Bill Gates 曾经失言:
 640K ought to be enough for everybody
 — Bill Gates 1981
 程序员们经常编写内存管理程序,往往提心吊胆。如果不想触雷,唯一的解决办法就是发现所有潜伏的地雷并且排除它们,躲是躲不了的。本章的内容比一般教科书的要深入得多,读者需细心阅读,做到真正地通晓内存管理。
7.1内存分配方式
 内存分配方式有三种:
从静态存储区域分配。内存在程序编译的时候就已经分配好,这块内存在程序的整个运行期间都存在。例如全局变量,static变量。
在栈上创建。在执行函数时,函数内局部变量的存储单元都可以在栈上创建,函数执行结束时这些存储单元自动被释放。栈内存分配运算内置于处理器的指令集中,效率很高,但是分配的内存容量有限。
从堆上分配,亦称动态内存分配。程序在运行的时候用malloc或new申请任意多少的内存,程序员自己负责在何时用free或delete释放内存。动态内存的生存期由我们决定,使用非常灵活,但问题也最多。
7.2常见的内存错误及其对策
 发生内存错误是件非常麻烦的事情。编译器不能自动发现这些错误,通常是在程序运行时才能捕捉到。而这些错误大多没有明显的症状,时隐时现,增加了改错的难度。有时用户怒气冲冲地把你找来,程序却没有发生任何问题,你一走,错误又发作了。
 常见的内存错误及其对策如下:
内存分配未成功,却使用了它。
 编程新手常犯这种错误,因为他们没有意识到内存分配会不成功。常用解决办法是,在使用内存之前检查指针是否为NULL。如果指针p是函数的参数,那么在函数的入口处用assert(p!=NULL)进行检查。如果是用malloc或new来申请内存,应该用if(p==NULL) 或if(p!=NULL)进行防错处理。
 
内存分配虽然成功,但是尚未初始化就引用它。
 犯这种错误主要有两个起因:一是没有初始化的观念;二是误以为内存的缺省初值全为零,导致引用初值错误(例如数组)。
 内存的缺省初值究竟是什么并没有统一的标准,......

阅读全文(1829) | 评论:0

高质量C++/C编程指南08(转帖)(2006-01-14 10:14:00)

摘要:第6章 函数设计
 函数是C++/C程序的基本功能单元,其重要性不言而喻。函数设计的细微缺点很容易导致该函数被错用,所以光使函数的功能正确是不够的。本章重点论述函数的接口设计和内部实现的一些规则。
 函数接口的两个要素是参数和返回值。C语言中,函数的参数和返回值的传递方式有两种:值传递(pass by value)和指针传递(pass by pointer)。C++ 语言中多了引用传递(pass by reference)。由于引用传递的性质象指针传递,而使用方式却象值传递,初学者常常迷惑不解,容易引起混乱,请先阅读6.6节“引用与指针的比较”。
6.1 参数的规则
【规则6-1-1】参数的书写要完整,不要贪图省事只写参数的类型而省略参数名字。如果函数没有参数,则用void填充。
 例如:
void SetValue(int width, int height); // 良好的风格
void SetValue(int, int);    // 不良的风格
float GetValue(void);  // 良好的风格
float GetValue();   // 不良的风格 【规则6-1-2】参数命名要恰当,顺序要合理。
 例如编写字符串拷贝函数StringCopy,它有两个参数。如果把参数名字起为str1和str2,例如
 void StringCopy(char *str1, char *str2);
 那么我们很难搞清楚究竟是把str1拷贝到str2中,还是刚好倒过来。
 可以把参数名字起得更有意义,如叫strSource和strDestination。这样从名字上就可以看出应该把strSource拷贝到strDestination。
 还有一个问题,这两个参数那一个该在前那一个该在后?参数的顺序要遵循程序员的习惯。一般地,应将目的参数放在前面,源参数放在后面。
 如果将函数声明为:
 void StringCopy(char *strSource, char *st......

阅读全文(1789) | 评论:0

高质量C++/C编程指南07(转帖)(2006-01-14 10:13:00)

摘要:第5章 常量
 常量是一种标识符,它的值在运行期间恒定不变。C语言用 #define来定义常量(称为宏常量)。C++ 语言除了 #define外还可以用const来定义常量(称为const常量)。
5.1 为什么需要常量
 如果不使用常量,直接在程序中填写数字或字符串,将会有什么麻烦?
程序的可读性(可理解性)变差。程序员自己会忘记那些数字或字符串是什么意思,用户则更加不知它们从何处来、表示什么。
在程序的很多地方输入同样的数字或字符串,难保不发生书写错误。
如果要修改数字或字符串,则会在很多地方改动,既麻烦又容易出错。 【规则5-1-1】 尽量使用含义直观的常量来表示那些将在程序中多次出现的数字或字符串。
 例如:
 #define      MAX   100  /*  C语言的宏常量  */
const int    MAX = 100;  //  C++ 语言的const常量
const float  PI = 3.14159; //  C++ 语言的const常量
5.2 const 与 #define的比较
 C++ 语言可以用const来定义常量,也可以用 #define来定义常量。但是前者比后者有更多的优点:
const常量有数据类型,而宏常量没有数据类型。编译器可以对前者进行类型安全检查。而对后者只进行字符替换,没有类型安全检查,并且在字符替换可能会产生意料不到的错误(边际效应)。
有些集成化的调试工具可以对const常量进行调试,但是不能对宏常量进行调试。 【规则5-2-1】在C++ 程序中只使用const常量而不使用宏常量,即const常量完全取代宏常量。
5.3 常量定义规则
【规则5-3-1】需要对外公开的常量放在头文件中,不需要对外公开的常量放在定义文件的头部。为便于管理,可以把不同模块的常量集中存放在一个公共的头文件中。
【规则5-3-2】如果某一常量与其它常量密切相关,应在定义中包......

阅读全文(1878) | 评论:0

高质量C++/C编程指南06(转帖)(2006-01-13 13:23:00)

摘要:第4章 表达式和基本语句
 读者可能怀疑:连if、for、while、goto、switch这样简单的东西也要探讨编程风格,是不是小题大做?
 我真的发觉很多程序员用隐含错误的方式写表达式和基本语句,我自己也犯过类似的错误。
 表达式和语句都属于C++/C的短语结构语法。它们看似简单,但使用时隐患比较多。本章归纳了正确使用表达式和语句的一些规则与建议。
4.1 运算符的优先级
 C++/C语言的运算符有数十个,运算符的优先级与结合律如表4-1所示。注意一元运算符 +  -  * 的优先级高于对应的二元运算符。 优先级 运算符 结合律 
从 高 到 低 排 列 ( )  [ ]  ->  . 从左至右 
 !  ~  ++  --  (类型) sizeof
+  -  *  & 从右至左
 
 *  /  % 从左至右 
 +  - 从左至右 
 <<  >> 从左至右 
 <   <=   >  >= 从左至右 
 ==  != 从左至右 
 & 从左至右 
 ^ 从左至右 
 | 从左至右 
 && 从左至右 
 || 从右至左 
 ?: 从右至左 
 =  +=  -=  *=  /=  %=  &am......

阅读全文(2337) | 评论:0

高质量C++/C编程指南05(转帖)(2006-01-13 13:02:00)

摘要:第3章 命名规则
 比较著名的命名规则当推Microsoft公司的“匈牙利”法,该命名规则的主要思想是“在变量和函数名中加入前缀以增进人们对程序的理解”。例如所有的字符变量均以ch为前缀,若是指针变量则追加前缀p。如果一个变量由ppch开头,则表明它是指向字符指针的指针。
 “匈牙利”法最大的缺点是烦琐,例如
 int    i,  j,  k; 
 float  x,  y,  z;
倘若采用“匈牙利”命名规则,则应当写成
 int    iI,  iJ,  ik;  // 前缀 i表示int类型
 float  fX,  fY,  fZ;  // 前缀 f表示float类型
如此烦琐的程序会让绝大多数程序员无法忍受。
 据考察,没有一种命名规则可以让所有的程序员赞同,程序设计教科书一般都不指定命名规则。命名规则对软件产品而言并不是“成败悠关”的事,我们不要化太多精力试图发明世界上最好的命名规则,而应当制定一种令大多数项目成员满意的命名规则,并在项目中贯彻实施。
3.1 共性规则
 本节论述的共性规则是被大多数程序员采纳的,我们应当在遵循这些共性规则的前提下,再扩充特定的规则,如3.2节。 【规则3-1-1】标识符应当直观且可以拼读,可望文知意,不必进行“解码”。
 标识符最好采用英文单词或其组合,便于记忆和阅读。切忌使用汉语拼音来命名。程序中的英文单词一般不会太复杂,用词应当准确。例如不要把CurrentValue写成NowValue。
 
【规则3-1-2】标识符的长度应当符合“min-length && max-information”原则。
 几十年前老ANSI C规定名字不准超过6个字符,现今的C++/C不再有此限制。一般来说,长名字能更好地表达含义,所以函数名、变量名、类名长达十几个字符不足为怪。那么名字是否越长约好?不见得! 例如变量名maxval......

阅读全文(1923) | 评论:0

高质量C++/C编程指南04(转帖)(2006-01-13 13:01:00)

摘要:第2章 程序的版式
 版式虽然不会影响程序的功能,但会影响可读性。程序的版式追求清晰、美观,是程序风格的重要构成因素。
 可以把程序的版式比喻为“书法”。好的“书法”可让人对程序一目了然,看得兴致勃勃。差的程序“书法”如螃蟹爬行,让人看得索然无味,更令维护者烦恼有加。请程序员们学习程序的“书法”,弥补大学计算机教育的漏洞,实在很有必要。
2.1 空行
 空行起着分隔程序段落的作用。空行得体(不过多也不过少)将使程序的布局更加清晰。空行不会浪费内存,虽然打印含有空行的程序是会多消耗一些纸张,但是值得。所以不要舍不得用空行。
 
【规则2-1-1】在每个类声明之后、每个函数定义结束之后都要加空行。参见示例2-1(a)
【规则2-1-2】在一个函数体内,逻揖上密切相关的语句之间不加空行,其它地方应加空行分隔。参见示例2-1(b )
 
// 空行
void Function1(…)
{
  …
}
// 空行
void Function2(…)
{
  …
}
// 空行
void Function3(…)
{
  …
}
 // 空行
while (condition)
{
 statement1;
 // 空行
 if (condition)
 {
  statement2;
 }
 else
 {
  statement3;
 }
 // 空行
 statement4;
}   
 示例2-1(a) 函数之间的空行                   示例2-1(b) 函数内部的空行 2.2 代......

阅读全文(1629) | 评论:0