改善程序设计技术的50个有效做法

1,421 views
1,323 views

Published on

Published in: Design, Technology, Business
0 Comments
5 Likes
Statistics
Notes
  • Be the first to comment

No Downloads
Views
Total views
1,421
On SlideShare
0
From Embeds
0
Number of Embeds
3
Actions
Shares
0
Downloads
22
Comments
0
Likes
5
Embeds 0
No embeds

No notes for slide

改善程序设计技术的50个有效做法

  1. 1. 改善程序设计技术的 50 个有效做法 第二版 2002.3 Scott Meyers 侯 捷 译
  2. 2. <ul><li>如何完成较好的设计 </li></ul><ul><li>如何避免常见的问题 </li></ul><ul><li>如何提高效率的一些准则 </li></ul><ul><li>不是放之四海而皆准的唯一真理 </li></ul>
  3. 3. C++ 新标准新的类型 bool <ul><li>有两个值 true, false. </li></ul><ul><li>typedef int bool; </li></ul><ul><li>const bool false=0; </li></ul><ul><li>const bool true=1; </li></ul>
  4. 4. 新的转型动作 <ul><li>static_cast<type>(expression) </li></ul><ul><li>// 将表达式 expression 转为 type 类型 </li></ul><ul><li>const_cast<type>(expression) </li></ul><ul><li>// 将常数类型 expression 转为非常数类型 </li></ul><ul><li>dynamic_cast<type>(expression) </li></ul><ul><li>// 安全向下转型 见 39 </li></ul><ul><li>reinterpret_cast<type>(expression) </li></ul><ul><li>// 函数指针类型转换 不常用 </li></ul>
  5. 5. 1. 尽量以 const 和 inline 取代 #define <ul><li>#define 是一个宏,只能被预处理, </li></ul><ul><li>而不被编译,用它定义的常量甚至不被编 </li></ul><ul><li>译器看见,因此不能发现使用中的错误。 </li></ul><ul><li>用 #define 定义一个简单函数,必须为每一 </li></ul><ul><li>个参数加上一个括号,容易造成错误。用 </li></ul><ul><li>内联函数高效准确。 </li></ul>
  6. 6. # define ratio 1.653 // 编译器看不见 ratio ,只看见 1.653 // 一旦出错,不会报告 <ul><li>const double ratio=1.653; </li></ul><ul><li>const char* const name=“Scott Meyers”; </li></ul><ul><li>// 字符串常量 </li></ul>
  7. 7. In Class 常量 <ul><li>用静态变量 </li></ul><ul><li>类内声明,类外定义。 </li></ul><ul><li>class EngineerConstants{ </li></ul><ul><li>private: </li></ul><ul><li>static const double Factor; </li></ul><ul><li>…… </li></ul><ul><li>} ; </li></ul><ul><li>const double EngineerConstants::Factor=1.35; </li></ul>
  8. 8. 2. 尽量以 <iostream> 取代 <stdio.h> <ul><li>scanf printf 函数 </li></ul><ul><li>不能扩充用来输入输出 </li></ul><ul><li>自定义类型的变量。 </li></ul><ul><li>cin>>i>>x; </li></ul><ul><li>cout<<i<<x ; </li></ul><ul><li>可以扩展,方便得多 </li></ul><ul><li>改变旧有的 C 习惯( shifting from C to C++ ) </li></ul><ul><li>尽量以 const 和 inline 取代 #define </li></ul><ul><li>#define 是一个宏,只能被预处理,而不被编译,用它定义的常量甚至不被编译器看见,因此不能发现使用中的错误。 </li></ul><ul><li>用 #define 定义一个简单函数,必须为每一个参数加上一个括号,容易造成错误。用内联函数高效准确。 </li></ul>
  9. 9. 3. 尽量以 new 和 delete 取代 malloc 和 free <ul><li>malloc 和 free 不能调用构造函数,析构函数 </li></ul><ul><li>new 和 delete 则可。 </li></ul><ul><li>不能混用 new delete malloc free </li></ul><ul><li>必要用 C 库函数时检查是否用到 malloc </li></ul><ul><li>重新用 new 和 delete 改过。 </li></ul>
  10. 10. 4. 尽量使用 C++ 风格的注释形式 <ul><li>/* …… */ 要保证成对出现,不小心错一大片。 </li></ul><ul><li>// 好看好读 </li></ul><ul><li>可以混合使用 </li></ul><ul><li>当心! </li></ul><ul><li># define light_speed 3e8 //m/sec(in a vacum) </li></ul>
  11. 11. 内存管理 (memory management) <ul><li>new 隐式调用构造函数, </li></ul><ul><li>delete 隐式调用析构函数, </li></ul><ul><li>可以重载 operator new 和 operator delete. </li></ul><ul><li>不小心运用 new 和 delete 会导致各种错误。 </li></ul>
  12. 12. 5. 使用相同形式的 new 和 delete <ul><li>string *a=new string[10]; </li></ul><ul><li>…… </li></ul><ul><li>delete a;// 出错 </li></ul><ul><li>delete[ ] a;// 正确 </li></ul><ul><li>string *b=new string; </li></ul><ul><li>…… </li></ul><ul><li>delete[ ] b;// 出错 </li></ul><ul><li>delete b;// 正确 </li></ul>
  13. 13. typedef string addresslines[4]; <ul><li>string *a=new addresslines; </li></ul><ul><li>…… </li></ul><ul><li>delete a;// 出错 </li></ul><ul><li>delete[ ] a;// 正确 </li></ul><ul><li>不要对数组类型用 typedef, </li></ul><ul><li>不容易记住用哪一种 delete </li></ul>
  14. 14. 6. 记得在析构函数中以 delete 对付指针成员 <ul><li>如果类中有指针数据成员 , </li></ul><ul><li>在每个构造函数中为指针成员配置内存,否则将它初始化为 0 ( NULL 指针)。 </li></ul><ul><li>若构造函数中用 new 配置了内存,一定要 </li></ul><ul><li>在析构函数中用 delete 释放 </li></ul><ul><li>在赋值运算重载时要将原有指针的内存删除,重新分配内存。 </li></ul><ul><li>要在析构函数中删除这个指针。 </li></ul>
  15. 15. <ul><li>不要用 delete 删除一个未完成初始化的指针, </li></ul><ul><li>不要删除一个未分配内存的指针。 </li></ul><ul><li>不要 delete 从一个类外面传来的指针。 </li></ul>
  16. 16. 7. 为内存不足的状况预作准备 <ul><li>不能认为“检查内存是否分配成功” 是多此一 </li></ul><ul><li>举。否则会出现严重后果。 </li></ul><ul><li>必须建立一个错误处理策略。当 operator new 无 </li></ul><ul><li>法满足需求时,在抛出异常之前,会调用一个内 </li></ul><ul><li>存不足处理函数 new handler ,这个函数由头文件 <new> 提供。 </li></ul>
  17. 17. typedef void (* new_handle )( ); <ul><li>new_handler set_new_handler( new_handler p)throw( ); </li></ul><ul><li>new_handler 是一个函数指针,无参 , 无返回值。 </li></ul><ul><li>函数 set_new_handler 用来配置 new_handler , </li></ul><ul><li>参数和返回值都是函数指针, new_handler 类型。 </li></ul><ul><li>它确定新的 new_handler 函数 ( 参数 ) ,保留旧的 new_handler 函数(返回)。 </li></ul>
  18. 18. 可以自定义新的 new_handler 函数, 用 set_new_handler 确认。 <ul><li>void nomoreMemory ( ) </li></ul><ul><li>{ cerr<< “Unable to satisfy for memeory ” </li></ul><ul><li>abort( ); //exit </li></ul><ul><li>} </li></ul><ul><li>int main( ) </li></ul><ul><li>{ set_new_handler( nomoreMemory ); </li></ul><ul><li>int *pBigDataArray = new int[100000000]; </li></ul><ul><li>…… } </li></ul><ul><li>当 operator new 无法配置 10000000 个整数空间 </li></ul><ul><li>时,系统调用 nomoreMemory ,然后结束。 </li></ul>
  19. 19. 设计 new_handler 函数,令其完成如下任务: <ul><li>-- 让更多的内存可用。预留一块内存, </li></ul><ul><li>new_handler 第一次被调用时释放, </li></ul><ul><li>同时发出警告。 </li></ul><ul><ul><li>安装新的 new_handler 以取代自己。 new_handler 中调用 C++ 标准库函数 set_new_handler 即可。 </li></ul></ul><ul><ul><li>卸载这个 new_handler ,返回 NULL 指针,并抛出 bad_alloc (或其继承)类型的异常。 </li></ul></ul><ul><ul><li>直接调用 abort 或 exit 终止程序。 </li></ul></ul>
  20. 20. C++ 不支持 class 中 专用的 new_handler ,但仍可以在一个 class 中重载 operator new, 和 set_new_handler 函数,让它调用特定的 new_handler 函数,而不用系统给出的全局 new_handler 。 <ul><li>class X </li></ul><ul><li>{ public: </li></ul><ul><li>static new_handler set_new_handler(new_handler p); </li></ul><ul><li>static void* operator new(size_t siz); </li></ul><ul><li>private: </li></ul><ul><li>static new_handler currentHandler; </li></ul><ul><li>}; </li></ul>
  21. 21. <ul><li>new_handler X::currentHandler;// 初始化为 0 </li></ul><ul><li>new_handler X:: set_new_handler (new_handler p) </li></ul><ul><li>{ new_handler oldHandler = currentHandler; </li></ul><ul><li>// 保留当前 new_handler </li></ul><ul><li>currentHandler = p ; // 再设置当前 new_handler </li></ul><ul><li>return oldHandler; </li></ul><ul><li>} </li></ul>
  22. 22. void *X:: operator new (size_t size) <ul><li>{ newHandler globalHandler= </li></ul><ul><li>std::set_new_handler(currentHandler); </li></ul><ul><li>// 配置新 new_handler 保存 globalHandler </li></ul><ul><li>void *memory; </li></ul><ul><li>try{ memory = ::oprator new(size); // 试分配内存 } </li></ul><ul><li>catch(std::bad_alloc&){ </li></ul><ul><li>std::set_new_handler(globalHandler); </li></ul><ul><li>// 恢复原有处理方法 </li></ul><ul><li>throw; // 传播异常 } </li></ul><ul><li>std::set_new_handler(globalHandler): </li></ul><ul><li>// 恢复原有处理方法 </li></ul><ul><li>return memory; </li></ul><ul><li>} // 调用一次特定处理方法,用毕恢复 </li></ul>
  23. 23. // 应用 <ul><li>void nomoreMemory( ); </li></ul><ul><li>X::set_new_handler(nomoreMemory); </li></ul><ul><li>X *px1 = new X; </li></ul><ul><li>// 如果内存分配失败,调用 nomoreMemory( ) </li></ul><ul><li>string *ps = new string; </li></ul><ul><li>// 如果内存分配失败,调用 globalHandler </li></ul><ul><li>X:: set_new_handler(0); </li></ul><ul><li>X *px2 = new X; </li></ul><ul><li>// 如果内存分配失败,立即抛出异常 </li></ul>
  24. 24. 可以做一个混合风格基类 允许 “设定 class 专属 new_handler” <ul><li>template<class T> </li></ul><ul><li>class NewHandlerSupport </li></ul><ul><li>{ </li></ul><ul><li>public: </li></ul><ul><li>static new_handler set_ new_handler(new_handler p); </li></ul><ul><li>static void* operator new(size_t siz); </li></ul><ul><li>private: </li></ul><ul><li>static new_handler currentHandler; </li></ul><ul><li>}; </li></ul>
  25. 25. template<class T> <ul><li>new_handler NewHandlerSupport<T>:: </li></ul><ul><li>set_new_handler(new_handler p) </li></ul><ul><li>{ new_handler oldHandler = currentHandler; // 保留当前 new_handler </li></ul><ul><li>currentHandler = p; // 再设置当前 new_handler </li></ul><ul><li>return oldHandler; </li></ul><ul><li>} </li></ul>
  26. 26. template<class T> <ul><li>void * NewHandlerSupport<T>::operator new(size_t size) </li></ul><ul><li>{ newHandler globalHandler= </li></ul><ul><li>std::set_new_handler(currentHandler); </li></ul><ul><li>// 配置新 new_handler 保存 globalHandler </li></ul><ul><li>void *memory; </li></ul><ul><li>try{ memory = ::oprator new(size); // 试分配内存 } </li></ul><ul><li>catch(std::bad_alloc&){ </li></ul><ul><li>std::set_new_handler(globalHandler); </li></ul><ul><li>// 恢复原有处理方法 </li></ul><ul><li>throw; // 传播异常 } </li></ul><ul><li>std::set_new_handler(globalHandler): </li></ul><ul><li>// 恢复原有处理方法 </li></ul><ul><li>return memory; } </li></ul>
  27. 27. new_handler NewHandlerSupport<T> ::currentHandler; // 初始化为 0 <ul><li>class X : public NewHandlerSupport<X> </li></ul><ul><li>{ </li></ul><ul><li>…… </li></ul><ul><li>// 不必声明 set_new_handler 和 operator new </li></ul><ul><li>} </li></ul><ul><li>类 X 不必改动原有的程序代码,就可以继续运作。 </li></ul>
  28. 28. <ul><li>1993 年前 C++ 要求 operator new 在无法满足内存需 </li></ul><ul><li>求时返回 0 ,新标准则是抛出一个 bad_alloc 类型 </li></ul><ul><li>异常。 </li></ul><ul><li>失败便转为 0 的传统被保留为 “ nothrow” 不抛出异 </li></ul><ul><li>常。 <new> 头文件中定义了一个 nothrow 对象 </li></ul><ul><li>class Widget {……}; </li></ul><ul><li>Widget *pw1 = new Widget; </li></ul><ul><li>// 如果失败抛出 std::bad_alloc 异常 </li></ul><ul><li>if(pw1==0)……// 无效 </li></ul><ul><li>widget *wp2 = new(nothrow) Widget; </li></ul><ul><li>// 如果失败,返回 0 </li></ul><ul><li>if(wp2==0)……// 有效 </li></ul>
  29. 29. 8. 撰写 operator new 和 operator delete 时 应遵守的公约 <ul><li>当你有必要重载 operator new 时,你的 new 函数的行为应该与系统原有的 new 函数的行为保持一致。 </li></ul><ul><li>应该有正确的返回值:返回一个指针指向分配的内存。如果内存不足,抛出一个 bad_alloc 类型的异常。 </li></ul><ul><li>不能覆盖系统原有的 new 函数。 </li></ul>
  30. 30. //new 函数的伪码 <ul><li>void * operator new(size_t size) </li></ul><ul><li>{ if(size==0){size=1;} </li></ul><ul><li>// 将 0 内存需求,看成 1 内存需求,避免与无内存混淆 </li></ul><ul><li>while(true )// 无穷循环 直到内存被分配 或抛出异常 </li></ul><ul><li>{ attempt to allocate size bytes; </li></ul><ul><li>if(the allocation was successful) </li></ul><ul><li>return (a pointer to the memory); </li></ul><ul><li>new_handle globalHandle = set_new_handler(0) </li></ul><ul><li>// 利用 NULL ,找出目前的错误处理函数 </li></ul><ul><li>set_new_handler(globalHandler); </li></ul><ul><li>// 重新设定为原本的函数 </li></ul><ul><li>if(globalHandler) (*globalHandler)( ) </li></ul><ul><li>else throw std::bad_alloc( ); } </li></ul><ul><li>} </li></ul>
  31. 31. 无穷循环可以让更多的内存可用, 或安装一个不同的 new_handler , 或卸载 new_handler , 或抛出一个异常, 或直接结束程序。
  32. 32. operator new 可以被继承 , 但要小心 , 否则会导致问题 <ul><li>class Base </li></ul><ul><li>{ public: </li></ul><ul><li>static void*oprator new(size_t size); </li></ul><ul><li>…… </li></ul><ul><li>} ; </li></ul><ul><li>class Derived : public Base </li></ul><ul><li>{……} ; // 导出类中没有 operator new 函数 </li></ul><ul><li>Derived *p = new Derived; </li></ul><ul><li>// 调用 Base 类中的 operator new 出错 </li></ul><ul><li>这里导出类内存比基类要大。 </li></ul>
  33. 33. 改进的办法: <ul><li>void *operator new(size_t size) </li></ul><ul><li>{…… </li></ul><ul><li>if(size != sizeof (Base) return :: oprator new(size); </li></ul><ul><li>// 回到标准 operator new 函数 </li></ul><ul><li>…… </li></ul><ul><li>} </li></ul>
  34. 34. 重写 operator delete <ul><li>void operator delete(void*rawMemory) </li></ul><ul><li>{ </li></ul><ul><li>if(rawMemory == 0)return; </li></ul><ul><li>// 与 C++ 标准 delete 保持一致 </li></ul><ul><li>Deallocate the memory pointed to </li></ul><ul><li>by rawMemory; </li></ul><ul><li>return; </li></ul><ul><li>} </li></ul>
  35. 35. //member 版 <ul><li>class Base </li></ul><ul><li>{ public: </li></ul><ul><li>static void *operator new(size_t size); </li></ul><ul><li>static void operator delete(void* rawMemory,size_t size); </li></ul><ul><li>…… </li></ul><ul><li>} </li></ul>
  36. 36. <ul><li>void Base::operator delete( </li></ul><ul><li>void* rawMemory,size_t size); </li></ul><ul><li>{ if(rawMemory ==0)return; </li></ul><ul><li>if(size!=sizeof(Base) // 如果大小错误 </li></ul><ul><li>{ :: operator delete(rawMemory); </li></ul><ul><li>// 用标准版 delete 处理 </li></ul><ul><li>return; </li></ul><ul><li>} </li></ul><ul><li>deallocate the memory pointed to </li></ul><ul><li>by rawmeMemory; </li></ul><ul><li>return; </li></ul><ul><li>} </li></ul>
  37. 37. 9. 避免覆盖 new 的正规形式 <ul><li>解决办法 </li></ul><ul><li>(1) 再写的一个专用的 operator new 函数,让它支 </li></ul><ul><li>持正规的 new </li></ul><ul><li>class X{ </li></ul><ul><li>public: </li></ul><ul><li>void f( ); </li></ul><ul><li>static void* operator new(size_t size, </li></ul><ul><li>new_handler p); </li></ul><ul><li>static void* operator new(sise_t size) </li></ul><ul><li>{ return ::operator new(size); } </li></ul><ul><li>}; </li></ul>
  38. 38. <ul><li>X *p1=new (specialErrorHandler) X; </li></ul><ul><li>// 调用 X:: operator new(size_t size, new_handler p); </li></ul><ul><li>X *p2=new X; </li></ul><ul><li>// 调用 X:: operator new(size_t size ); </li></ul><ul><li>(2) 为 operator new 的每一个参数提供默认值 (缺省值) </li></ul>
  39. 39. 10. 如果写了一个 operator new 不要忘记写一个 operator delete <ul><li>需要动态分配大量小额内存空间的应用程序, </li></ul><ul><li>有时需要重载 operator new 。 </li></ul><ul><li>class AirplaneRep {……}; </li></ul><ul><li>class Airplane { </li></ul><ul><li>public: </li></ul><ul><li>…… </li></ul><ul><li>private: </li></ul><ul><li>AirplaneRep *rep; // 唯一数据成员是指针 </li></ul><ul><li>}; </li></ul>
  40. 40. Airplane *p=new Airplane; // 要求内存不大 <ul><li>分配的内存比实际所需要的内存要大, </li></ul><ul><li>这是为了 delete 这块内存时,系统能知道其大小。 </li></ul>pa <ul><li>为了节省内存需要定制内存管理。 </li></ul>Airplane 对象所需的 内存 纪录内存大小的 数据
  41. 41. 定制内存管理。 <ul><li>class Airplane{ </li></ul><ul><li>public: </li></ul><ul><li>static void* operator new( size_t size); </li></ul><ul><li>static void operator delete(void*deadObject, size_t size); </li></ul><ul><li>…… </li></ul><ul><li>private: </li></ul><ul><li>union{ AirplaneRep *rep; </li></ul><ul><li>Airplane *next; </li></ul><ul><li>}; // 两个指针公用一个内存 </li></ul><ul><li>static const int BLOCK_SIZE; </li></ul><ul><li>static Airplane * headOfFreeList; </li></ul><ul><li>// 用链表配置一片内存,整个类只须一个链 </li></ul><ul><li>}; </li></ul>
  42. 42. <ul><li>void* Airplane :: operator new( size_t size); </li></ul><ul><li>{ if(size != sizeof(Airplane)) return </li></ul><ul><li>::operator new(size); </li></ul><ul><li>Airplane *p= headOfFreeList; //p 指向链表头 </li></ul><ul><li>if(p) headOfFreeList = p->next; // 表头后移 , p 可用 </li></ul><ul><li>else { Airplane *newBlock = </li></ul><ul><li>static_cast<Airplane*>(::operator </li></ul><ul><li>new(BLOCK_SIZE*sizeof(Airplane))); </li></ul><ul><li>for(int i=1; i<BLOCKSIZE-1;++i) // 保留第一块 </li></ul><ul><li>newBlock[i].next = &newBlock[i+1]; </li></ul><ul><li>newBlock[BLOCK_SIZE-1].next = 0; // 置表尾 </li></ul><ul><li>p = newBlok; // p 可用 </li></ul><ul><li>headOfFreeList =&newBlock[1]; } </li></ul><ul><li>return p; } </li></ul>
  43. 43. <ul><li>只有当 ::operator new 失败时,这里的 operator new </li></ul><ul><li>才失败。这时 ::operator new 会调用 new_handler 直 </li></ul><ul><li>到抛出异常,因此我们不需要再写一次 new_handler 处理 </li></ul><ul><li>具体实现文件中要先对静态成员初始化 , </li></ul><ul><li>Airplane * Airplane::headOfFreeList; </li></ul><ul><li>// headOfFreeList 置 0 </li></ul><ul><li>const int Airplane::BLOCK_SIZE = 512; </li></ul><ul><li>这个版本的 operator new 可以运作良好,速度快过两个数量级。 </li></ul>
  44. 44. <ul><li>还要在 Airplane 类中写一个 operator delete </li></ul><ul><li>void Airplane::operator </li></ul><ul><li>delete(void*deadObject, size_t size) </li></ul><ul><li>{ if(deadObject==0) return; </li></ul><ul><li>if(size !=sizeof(Airplane)){ </li></ul><ul><li>::operator delete(deadObject); </li></ul><ul><li>// 与 operator new 处理保持一致 </li></ul><ul><li>return; } </li></ul><ul><li>Airplane *carcass = </li></ul><ul><li>static_cast<Airplane>(deadObject); </li></ul><ul><li>carcass->next =headOfFreeList; </li></ul><ul><li>HeadOfFreeList = carcass; </li></ul><ul><li>} </li></ul>
  45. 45. <ul><li>如果没有定义相应的 delete 函数,而使用了原有的 delete, 结果会出现意想不到的错误,有时是严重的错误。 </li></ul><ul><li>如果用 member 版本不要忘记定义 virtual 析构函数。 </li></ul><ul><li>这里的 delete 函数没有 memory leak 问题。 </li></ul><ul><li>这是因为用了 memory pool 一次分配一块内存,逐步使用逐步释放, </li></ul><ul><li>不必再专门释放 memory pool. </li></ul>
  46. 46. 定义一个 memory pool 类,使每一个 pool 对象都是一个内存配置器。 <ul><li>class Pool </li></ul><ul><li>{ public: </li></ul><ul><li>Pool(size_t n); </li></ul><ul><li>void* alloc( size_t n );// 为一个对象配置足够 </li></ul><ul><li>// 的内存遵循 operator new 的规矩 </li></ul><ul><li>void free(void* p, size_t n );// 将 p 的内存送回 </li></ul><ul><li>//pool 遵循 operator delete 的规矩 </li></ul><ul><li>~pool( ); // 释放 pool 中所有内存 </li></ul><ul><li>}; </li></ul>
  47. 47. <ul><li>用 Pool 对象来配置内存,当被销毁时,配置的内存自动被释放。 </li></ul><ul><li>于是 memory leak 就可以避免。 </li></ul>
  48. 48. <ul><li>class Airplane </li></ul><ul><li>{ public: </li></ul><ul><li>static void* operator new( size_t size); </li></ul><ul><li>static void operator delete(void*p, size_t size); </li></ul><ul><li>…… </li></ul><ul><li>private: </li></ul><ul><li>AirplaneRep *rep; </li></ul><ul><li>static Pool memPool; </li></ul><ul><li>//Airplane 的 memory pool </li></ul><ul><li>}; </li></ul>
  49. 49. <ul><li>inline void Airline::operator new(size_t size) </li></ul><ul><li>{ return memPool.alloc(size); } </li></ul><ul><li>inline void Airline::operator delete(void* p, size_t size) </li></ul><ul><li>{memPool.free(p, size); } </li></ul><ul><li>为 Airplane 的 memPool 初始化, 要放在 Airplane 类实现的文件里 </li></ul><ul><li>Pool Airplane::memPool(sizeof(Airplane)); </li></ul>
  50. 50. 构造函数、析构函数和赋值运算符 <ul><li>构造函数、析构函数和赋值运算用来产生一个新对象并初始化,撤销一个对象并收回占有的内存,为已有的对象赋一个新值。 </li></ul><ul><li>不能有错,必须将他们彻底搞清楚。 </li></ul>
  51. 51. 11. class 内有成员指针并动态配置内存时,一定要有拷贝构造函数,赋值运算符重载
  52. 52. <ul><li>class String </li></ul><ul><li>{ </li></ul><ul><li>public: </li></ul><ul><li>String(const char*value); </li></ul><ul><li>~String( ); </li></ul><ul><li>…… // 没有拷贝构造函数, </li></ul><ul><li>// 也没有赋值运算符重载 </li></ul><ul><li>private: </li></ul><ul><li>char*data; </li></ul><ul><li>}; </li></ul>
  53. 53. <ul><li>String::String(const char *value) </li></ul><ul><li>{ if(value) </li></ul><ul><li>{ data = new char[strlen(value)+1]; </li></ul><ul><li>strcopy(data, value); </li></ul><ul><li>} </li></ul><ul><li>else </li></ul><ul><li>{ data = new char[1]; </li></ul><ul><li>*data = “”; </li></ul><ul><li>} </li></ul><ul><li>} </li></ul><ul><li>inline String::~String( ){delete[ ] data;} </li></ul>
  54. 54. <ul><li>String a( “Hello” ); </li></ul><ul><li>String b(“World” ); </li></ul><ul><li>b = a; </li></ul>Hello World a b data data
  55. 55. <ul><li>由于没有自定义的赋值函数,只能用 C++ 产生的默认赋值函数, </li></ul><ul><li>它简单地将 b 的成员指针 data 指向 a.data, 引起 字符串“ World” 占有的内存遗失。 </li></ul><ul><li>而且 a.data 与 b.data 指向同一个内存,其中一个被析构时另一个就丢失了。 </li></ul>
  56. 56. <ul><li>拷贝构造函数用来传值, </li></ul><ul><li>void donothing(String la){ } </li></ul><ul><li>String s= “the truth is out of there”; </li></ul><ul><li>donothing(s); </li></ul><ul><li>当函数 donothing 完成任务后,参数 s 所含的指针被析构, la 被删除。 </li></ul><ul><li>即便 la 不再使用,将来又一次析构 la 会造成问题。 </li></ul>
  57. 57. <ul><li>解决的办法就是自己定义拷贝构造函数,赋值函数重载。 </li></ul><ul><li>如果确信永不使用这些函数,把他们定义为私有函数,而且不实现。 </li></ul><ul><li>一旦出错,编译器会给出错误提示。 </li></ul>
  58. 58. 12 构造函数中尽量以初始化代替赋值 <ul><li>一个类中的 const 成员数据和 reference 引用数据只能被初始化,不能被赋值。 </li></ul><ul><li>即便没有 const 成员数据和 reference 引用数据,初始化也比赋值效率高。 </li></ul><ul><li>构造函数分两个阶段实现: </li></ul><ul><li>1. 数据成员初始化。 </li></ul><ul><li>2. 调用构造函数。 </li></ul><ul><li>数据成员赋值要调用构造函数,再调用赋值函数,做两次调用影响效率。 </li></ul><ul><li>初始化也容易维护,修改。 </li></ul>
  59. 59. 有一种例外: 一个类内有大量数据成员时,赋值比初始化效率高。 <ul><li>class ManyDataMbs </li></ul><ul><li>{ public: </li></ul><ul><li>ManyDataMbs () </li></ul><ul><li>ManyDataMbs ( const ManyDataMbs& x ) ; </li></ul><ul><li>private: </li></ul><ul><li>int a, b, c, d, e, f, g, h; </li></ul><ul><li>double i, j, k, l, m; </li></ul><ul><li>void init ( );// 用来将数据成员初始化,不做他用 </li></ul><ul><li>}; </li></ul>
  60. 60. <ul><li>void ManyDataMbs::init( ) </li></ul><ul><li>{ a=b=c=d=e=f=g=h=1; </li></ul><ul><li>i=j=k=l=m=0; </li></ul><ul><li>} </li></ul><ul><li>ManyDataMbs::ManyDataMbs( ) </li></ul><ul><li>{ init( ); </li></ul><ul><li>…… </li></ul><ul><li>} </li></ul><ul><li>ManyDataMbs :: ManyDataMbs(const ManyDataMbs& x) </li></ul><ul><li>{ init( ); </li></ul><ul><li>…… </li></ul><ul><li>} </li></ul>
  61. 61. <ul><li>静态数据成员 static class member 不应该在构造函数中初始化。 </li></ul><ul><li>静态数据成员只能初始化一次,不能初始化多次。 </li></ul>
  62. 62. 12. 数据成员初始化的次序应该和类内声明的次序相同 <ul><li>template<class T> </li></ul><ul><li>class Array // 有上下界的数组 </li></ul><ul><li>{ public: </li></ul><ul><li>Array(int lowBound, int highBound); </li></ul><ul><li>…… </li></ul><ul><li>private: </li></ul><ul><li>vector<T> data; </li></ul><ul><li>// 数组数据存储于一个 vector 对象 data 中 </li></ul><ul><li>size_t size; // 数组中元素的个数 </li></ul><ul><li>int lBound, hBound; // 上下界 </li></ul><ul><li>} ; </li></ul>
  63. 63. <ul><li>template<class T> </li></ul><ul><li>Array<T>::Array(int lowBound, int highBound) </li></ul><ul><li>: size(highBound-lowBound+1), </li></ul><ul><li>lBound(lowBound), hBound(highBound), data(size) </li></ul><ul><li>{ } </li></ul><ul><li>实际初始化中, data 先被初始化,然后 size, lBound, hBound. </li></ul><ul><li>这样数组中,究竟有多少个元素无法确定。 </li></ul><ul><li>基类成员总是比导出类先初始化。多重继承时初始化的先后次序要十分小心。 </li></ul>
  64. 64. 14. 总是让基类拥有虚析构函数 <ul><li>一个军事应用软件 </li></ul><ul><li>class EnemyTarget </li></ul><ul><li>{ public: </li></ul><ul><li>EnemyTarget( ){++ numTargets;} </li></ul><ul><li>EnemyTarget(const EnemyTarget&) </li></ul><ul><li>{++ numTargets;} </li></ul><ul><li>~ EnemyTarget( ){-- numTargets;} </li></ul><ul><li>static size_t numberOfTargets( ) </li></ul><ul><li>{return numTargets;} </li></ul><ul><li>virtual bool destroy( ); // 摧毁敌方目标是否成功 </li></ul><ul><li>private: </li></ul><ul><li>static size_t numTargets; // 对象计数器 </li></ul><ul><li>} ; </li></ul><ul><li>size_t EnemyTarget::numTargets; </li></ul><ul><li>// 静态成员初始化为 0 ,放在类外 </li></ul>
  65. 65. <ul><li>class EnemyTank : public EnemyTarget </li></ul><ul><li>{ public: </li></ul><ul><li>EnemyTank ( ){++ numTanks;} </li></ul><ul><li>EnemyTank (const EnemyTank&){++ numTanks;} </li></ul><ul><li>~ EnemyTank ( ){-- numTanks;} </li></ul><ul><li>static size_t numberOfTanks( ){return numTanks;} </li></ul><ul><li>virtual bool destroy( ); // 摧毁敌方坦克是否成功 </li></ul><ul><li>private: </li></ul><ul><li>static size_t numTanks; // 敌方坦克计数器 </li></ul><ul><li>} ; </li></ul>
  66. 66. <ul><li>EnemyTarget *targetPtr = new EnemyTank; </li></ul><ul><li>…… </li></ul><ul><li>delete targetPtr; // 未定义,计数出错,影响战斗胜败 </li></ul><ul><li>解决办法,把 EnemyTarget 类中的析构函数定义为 virtual 即可。 </li></ul><ul><li>几乎所有的基类都有虚函数,只要有一个虚函数,就要把析构函数定义为虚函数。 </li></ul><ul><li>没有虚函数的类,有继承派生类对象析构,也要定义虚析构函数。 </li></ul><ul><li>但虚函数会增加内存开销。完全不必要时不要用虚析构函数。 </li></ul><ul><li>声明一个抽象类,可以加一个纯虚析构函数。 </li></ul>
  67. 67. 15. 让 operator= 返回 *this 的引用 reference <ul><li>C 语言中 operator= 的原型 </li></ul><ul><li>C& C::operator=(const C&); </li></ul><ul><li>char x, y, z; </li></ul><ul><li>x=y=z= ‘a’; </li></ul><ul><li>x,operator=(y.operator=(z.operator= ‘a’)); </li></ul><ul><li>z.operator= 的返回值是 y.operator= 的实参。他们应该有相同的类型。 </li></ul><ul><li>但不要让 operator= 返回 void 类型, const 类型 </li></ul>
  68. 68. <ul><li>Strin& String::operator=(const String& rhs) </li></ul><ul><li>{ …… </li></ul><ul><li>return *this; // 返回一个引用指向左侧对象 } </li></ul><ul><li>Strin& String::operator=(const String& rhs) </li></ul><ul><li>{ …… </li></ul><ul><li>return rhs; // 返回一个引用指向右侧对象,错误 } </li></ul><ul><li>后一个返回值,编译器无法编译,无法返回 const 类型 . 如果参数中去掉 const 变成: </li></ul><ul><li>Strin& String::operator=( String& rhs) ; </li></ul><ul><li>X= ‘a’; // 无法编译 rhs 应该是一个变量。 </li></ul><ul><li>结论:必须返回 *this; </li></ul>
  69. 69. 16. 在 operator= 中为所有的数据成员赋值 <ul><li>基类中这不成问题,在派生类中要小心。 </li></ul><ul><li>正确的赋值运算 </li></ul><ul><li>Derived& Derived::operator=(const Drived& rhs) </li></ul><ul><li>{ if(this = = &rhs) return *this; </li></ul><ul><li>Base::operator=(rhs); // 调用基类的赋值运算 </li></ul><ul><li>data = rhs.data; </li></ul><ul><li>return *this; </li></ul><ul><li>} </li></ul>
  70. 70. <ul><li>Derived& Derived::operator=(const Drived& rhs) </li></ul><ul><li>{ </li></ul><ul><li>if(this = = &rhs) return *this; </li></ul><ul><li>static_cast<Base&>(*this) = rhs; </li></ul><ul><li>//*this 强制转换成基类的引用赋值基类成员 </li></ul><ul><li>data = rhs.data; </li></ul><ul><li>return *this; </li></ul><ul><li>} </li></ul><ul><li>拷贝构造函数中要调用基类构造函数。 </li></ul><ul><li>用第一种方法 </li></ul>
  71. 71. 在 operator= 中检查是否“自己赋值给自己” <ul><li>class X{……}; </li></ul><ul><li>X a; X&b = a;//b 是 a 的别名 (aliasing) </li></ul><ul><li>a = b; // 自己赋值给自己 合法 </li></ul><ul><li>在赋值函数中要特别谨慎的处理自己的别名赋值给自己的问题。 </li></ul>
  72. 72. <ul><li>提高效率 </li></ul><ul><li>先做检查,一发现自己赋值给自己立即返回。导出类的赋值运算重载中一定要先检查,可以节省许多工作 </li></ul><ul><li>确保正确性 </li></ul><ul><li>赋值运算通常要先将左边对象的资源释放,再行赋值。如果有自己赋值给自己的现象,这个资源可能丢失,不可挽回了。 </li></ul>
  73. 73. <ul><li>如何判断两个对象是同一个对象? </li></ul><ul><li>不是对象的内容相同,而是看他们的地址是否相同。 </li></ul><ul><li>X& X::operator=(const X& rhs) </li></ul><ul><li>{ if(this==&rhs) return *this; </li></ul><ul><li>…… </li></ul><ul><li>} </li></ul><ul><li>aliasing 问题不限于赋值运算内,只要用到指针或引用,就可能出现。这时我们就要当心,不要误删了有用的资源。 </li></ul>
  74. 74. 类和函数的设计和申明 <ul><li>设计一个高效率的类型( class 型别), </li></ul><ul><li>必须先回答下列问题 </li></ul><ul><li>对象如何产生和销毁? </li></ul><ul><li>确定构造函数和析构函数的设计。 </li></ul><ul><li>对象的初始化和赋值有什么不同? </li></ul><ul><li>决定构造函数和赋值函数的设计。 </li></ul><ul><li>对象如何传值 </li></ul><ul><li>决定拷贝构造函数的设计 </li></ul>
  75. 75. <ul><li>确定合法的范围 成员数据的定义域 </li></ul><ul><li>确定做什么检查,何时抛出异常 </li></ul><ul><li>判断是否能从已有的类继承 </li></ul><ul><li>如果能继承,注意受基类哪些约束,哪些要用虚函数。 </li></ul><ul><li>允许那种类型转换 </li></ul><ul><li>构造函数可以用作隐式类型转换,显式类型转换要自定义。 </li></ul>
  76. 76. <ul><li>新类型需要哪些运算和函数 </li></ul><ul><li>确定 class 的接口。 </li></ul><ul><li>哪些运算和函数必须禁用 </li></ul><ul><li>放到 private 成员中。 </li></ul><ul><li>新类型的对象可调用哪些函数 </li></ul><ul><li>确定公有成员函数,保护成员函数, </li></ul><ul><li>私有成员函数。 </li></ul><ul><li>是否通用类型 </li></ul><ul><li>确定是否要用类模板 </li></ul>
  77. 77. 18 .努力让接口完满( complete )且最小化 <ul><li>客户端接口 ( client interface )指公有成员,一般只有公有函数,不要有公有数据。 </li></ul><ul><li>完满接口 允许客户做合理要求的任意事情。 </li></ul><ul><li>最小化接口 尽量让函数个数最少。不能有功能重叠的函数。太多函数不容易被理解,不易维护,浪费资源。 </li></ul>
  78. 78. <ul><li>如果增加一个函数,使新类型更方便使用,就可以增加。 </li></ul><ul><li>T& operator[ ](int index); </li></ul><ul><li>// 传回数组的一个元素,可读,可写 </li></ul><ul><li>const T& operator[ ](int index)const; </li></ul><ul><li>// 传回数组的一个元素,可读,不可写 </li></ul>
  79. 79. 19 .区分成员函数、非成员函数 和友元函数 <ul><li>成员函数可以动态绑定,可以用 virtual </li></ul><ul><li>非成员函数不能用 virtual , </li></ul><ul><li>非成员函数能不做友元尽量不做友元函数。 </li></ul><ul><li>非成员函数要调用类中私有数据成员或私有函数,则一定要声明为友元。 </li></ul><ul><li>不要让 operaor<< 和 operator>> 成为类的成员函数 , 必要时作友元。 </li></ul><ul><li>要让函数式左边对象做类型转换,就不能做成员函数。 </li></ul>
  80. 80. <ul><li>例子 </li></ul><ul><li>class complex </li></ul><ul><li>{…… </li></ul><ul><li>complex operator*(complex rhs)const; </li></ul><ul><li>private: </li></ul><ul><li>float x, y; </li></ul><ul><li>}; </li></ul><ul><li>complex a(1,2),b(1.5,4); </li></ul><ul><li>a=a*b;// 正确 </li></ul><ul><li>a=a*2;// 可以 </li></ul><ul><li>a=2*a;// 出错 </li></ul><ul><li>只能声明为非成员函数 </li></ul><ul><li>const complex operator*(const complex& lhs , const complex& rhs); </li></ul>
  81. 81. 20 .避免将数据成员设置为公有数据 <ul><li>让公有成员都是函数,可以 保持一致性 。 </li></ul><ul><li>将数据成员声明为私有成员或保护成员,可以 确保数据的安全 。 </li></ul>
  82. 82. 21 .尽可能使用 const <ul><li>使用 const 可以让编译器知道某值不能改变,编译器会确保这个条件不会被改变。 </li></ul><ul><li>const char* p; // 指针,指向常值字符串 </li></ul><ul><li>char* const p;// 常指针,指向固定地址,地址内字符串不一定是常量 </li></ul><ul><li>const char* const p;// 常指针,指向固定地址,内置常字符串 </li></ul><ul><li>const chr *p; char const *p; // 意义相同 </li></ul>
  83. 83. 函数中 const 可以修饰传回值,参数,成员函数时甚至可以修饰整个函数。 <ul><li>函数返回值用 const , 可以改善函数的安全性,和效率。 </li></ul><ul><li>T& A<T>::operator[ ](int index); </li></ul><ul><li>// 传回数组的一个元素,可读,可写 </li></ul><ul><li>A a(8); </li></ul><ul><li>cout<<a[2]; </li></ul><ul><li>a[2]=’b’ ; // 正确 </li></ul>
  84. 84. <ul><li>const T& A<T>::operator[ ](int index); </li></ul><ul><li>// 传回数组的一个元素,可读,不可写 </li></ul><ul><li>A a(8); </li></ul><ul><li>cout<<a[2]; // 正确 </li></ul><ul><li>a[2]=’b’ ; // 错误 </li></ul>
  85. 85. <ul><li>const complex operator*(const complex& lhs , const complex& rhs); </li></ul><ul><li>complex a,b,c; </li></ul><ul><li>(a*b)=c; // 不允许 </li></ul>
  86. 86. <ul><li>参数用 const 可以保证参数值不变,让编译器作检查。 </li></ul><ul><li>const 成员函数保证 this 指针不变。 </li></ul><ul><li>class A </li></ul><ul><li>{ public: </li></ul><ul><li>…… </li></ul><ul><li>int length( )const; </li></ul><ul><li>private: </li></ul><ul><li>int size; </li></ul><ul><li>} ; </li></ul><ul><li>int A::length( )const </li></ul><ul><li>{ if(size<0)return 0; // 错误 不能改变任何数据成员 </li></ul><ul><li>return size; } </li></ul>
  87. 87. <ul><li>新 C++ 标准 新增保留字 mutable </li></ul><ul><li>class A </li></ul><ul><li>{ public: </li></ul><ul><li>…… </li></ul><ul><li>int length( )const; </li></ul><ul><li>private: </li></ul><ul><li>mutable int size;// 可以在任何地点被改动, </li></ul><ul><li>// 即使在 const 成员函数中 </li></ul><ul><li>} ; </li></ul><ul><li>int A::length( )const </li></ul><ul><li>{ if(size<0)return 0; // 正确 </li></ul><ul><li>return size; } </li></ul>
  88. 88. 22 .尽量使用引用参数传址 pass by reference <ul><li>拷贝构造函数用来传值 pass by value , </li></ul><ul><li>为函数的参数传值, </li></ul><ul><li>为函数的返回值传值。 </li></ul><ul><li>传值要占用许多资源。 </li></ul>
  89. 89. <ul><li>class Person </li></ul><ul><li>{ </li></ul><ul><li>public: </li></ul><ul><li>Person( ); </li></ul><ul><li>~Person( ); </li></ul><ul><li>…… </li></ul><ul><li>private: </li></ul><ul><li>string name, address; </li></ul><ul><li>};  </li></ul>
  90. 90. <ul><li>class student : public Person </li></ul><ul><li>{ public: </li></ul><ul><li>student( ); </li></ul><ul><li>~student( ); </li></ul><ul><li>private: </li></ul><ul><li>string schoolname,schoolAddress; </li></ul><ul><li>}; </li></ul><ul><li>  student returnstudent(student s) </li></ul><ul><li>{return s;} </li></ul><ul><li>  student plato; </li></ul><ul><li>returnstudent(plato); </li></ul>
  91. 91. <ul><li>函数调用中 copy 构造函数被调用两次,将 plato 传给参数 s, 再将函数值返回,析构函数调用两次,析构 s, 析构函数返回值。 </li></ul><ul><li>更有甚者,基类 Person 的构造函数也要调用两次。 student 对象中两个 string 数据对象要构造,基类 Person 中两个 string 数据对象也要构造, plato 给 s 构造四次 , 返回传值构造四次 </li></ul><ul><li>总共调用 12 次 构造函数,当然还有 12 次析构函数要调用。 </li></ul>
  92. 92. <ul><li>免除这些不当成本,改用引用参数传址 by reference </li></ul><ul><li>  </li></ul><ul><li>const student& returnstudent(const student& s) </li></ul><ul><li>{return s;} </li></ul><ul><li>引用参数传址 by reference 不调用任何构造函数析构函数。 </li></ul><ul><li>虚函数的引用参数是基类时,实际传入派生类对象时可以调用派生类的函数。 </li></ul><ul><li>传值参数没有这样的功能。 </li></ul><ul><li>  </li></ul><ul><li>引用参数要注意别名( aliasing )问题。 </li></ul>
  93. 93. 23 .当你必须传回 objebct( 传值 ) 时不要传址 ( 引用) <ul><li>尽可能让事情简单,但不要过于简单。 </li></ul><ul><li>A.Einstein </li></ul><ul><li>尽可能让事情有效率,但不要过于有效率。 </li></ul><ul><li>C++ </li></ul><ul><li>函数必须传回一个对象,就不要传址不要返回引用。 </li></ul><ul><li>不能传回一个不存在的地址,不能传回函数中产生的局部对象的地址。 </li></ul>
  94. 94. <ul><li>const complex operator*(const complex& lhs , const complex& rhs) </li></ul><ul><li>{complex temp(lhs.x*rhs.x-lhs.y*rhs.y, </li></ul><ul><li>lhs.x*rhs.y+lhs.y*rhs.x); </li></ul><ul><li>return temp; } </li></ul><ul><li>  </li></ul>& 错误 。返回值地址指向局部对象,与局部对象同名,运算执行完毕,局部对象被析构,返回值指向一个不存在的地址。
  95. 95. <ul><li>const complex& operator*(const complex& lhs , const complex& rhs) </li></ul><ul><li>{complex *temp(lhs.x*rhs.x-lhs.y*rhs.y, </li></ul><ul><li>lhs.x*rhs.y+lhs.y*rhs.x); </li></ul><ul><li>return *temp; } </li></ul><ul><li>指针 temp 被析构,内存已丢失。。 </li></ul>
  96. 96. <ul><li>const complex& operator*(const complex& lhs , const complex& rhs) </li></ul><ul><li>{complex *temp=new complex(lhs.x*rhs.x-lhs.y*rhs.y, lhs.x*rhs.y+lhs.y*rhs.x); </li></ul><ul><li>return *temp; } </li></ul><ul><li>指针 temp 没有析构,将来谁来析构呢。内存可能丢失。 </li></ul><ul><li>complex one(1), two(2), three(3), four(4); </li></ul><ul><li>complex product; </li></ul><ul><li>product=one*two*three*four; </li></ul><ul><li>如何析构这几个 operator* 中间产生的 temp 指针呢? </li></ul>
  97. 97. 24 .函数重载和参数缺省之间,谨慎抉择 <ul><li>函数重载和参数缺省之间容易引起混淆。 </li></ul><ul><li>如果可以选择一个 合理的默认值 ,并且只需要 一种算法 ,最好使用缺省参数。 </li></ul><ul><li>否则使用重载函数。 </li></ul>
  98. 98. <ul><li>例:求五个整数的最大值 </li></ul><ul><li>#include<limits> </li></ul><ul><li>int max(int a, </li></ul><ul><li>int b=std::numeric_limits<int>::min( ), </li></ul><ul><li>int c=std::numeric_limits<int>::min( ), </li></ul><ul><li>int d=std::numeric_limits<int>::min( ), </li></ul><ul><li>int e=std::numeric_limits<int>::min( ),) </li></ul><ul><li>{ int temp=a>b?a:b; </li></ul><ul><li>int temp=temp>c? temp:c; </li></ul><ul><li>int temp= temp >d? temp:d </li></ul><ul><li>int temp= temp >e? temp:e; </li></ul><ul><li>} </li></ul>
  99. 99. <ul><li>使用 max 函数对两个参数,三个参数,直至五个参数都有效。 </li></ul><ul><li>但是,计算平均数就找不到合适的默认值,只好重载。 </li></ul><ul><li>一般,构造函数和拷贝构造函数的算法不同,需要重载。 </li></ul>
  100. 100. 25 .避免对指针类型和数值类型进行重载 <ul><li>void f(int x); </li></ul><ul><li>void f(string *ps); </li></ul><ul><li>f(0); // 调用那一个?调用 f(int) </li></ul><ul><li>void * const NULL=0;// 无类型指针 </li></ul><ul><li>f(NULL);// 错误 类型不符 </li></ul><ul><li>#define NULL 0 </li></ul><ul><li>f(NULL);// 调用 f(int) </li></ul><ul><li>#define NULL((void*) 0)) </li></ul><ul><li>f(NULL);// 错误 类型不符 </li></ul>
  101. 101. <ul><li>class NULLClass // 类型名可以隐去 </li></ul><ul><li>{ public: </li></ul><ul><li>template<class T> </li></ul><ul><li>operator T*( ) {return 0;}// 为任意类型 T 传回一个 NULL 指针 </li></ul><ul><li>}NULL; </li></ul><ul><li>f(string*ps); </li></ul><ul><li>f(NULL);//NULL 被转换为 string* 调用 f(string*ps) </li></ul><ul><li>尽可能避免对指针类型和数值类型进行重载 </li></ul>
  102. 102. 26 .防备隐性二义性状态 <ul><li>class B; </li></ul><ul><li>classA </li></ul><ul><li>{public: </li></ul><ul><li>A(const B&); // 由 B 可以造出 A 来 </li></ul><ul><li>} ; </li></ul><ul><li>class B </li></ul><ul><li>{public: </li></ul><ul><li>operator A( )const; //B 可以转换成 A </li></ul><ul><li>}; </li></ul><ul><li>void f(const A&); </li></ul><ul><li>B b; </li></ul><ul><li>f(b);// 错误 模棱两可 两种方法哪种更好? </li></ul>
  103. 103. <ul><li>void f(int); </li></ul><ul><li>void f(char); </li></ul><ul><li>double d=6.02; </li></ul><ul><li>f(d);// 模棱两可 </li></ul><ul><li>模棱两可可以潜伏很久,直到爆发。 </li></ul>
  104. 104. <ul><li>多继承最容易引发模棱两可。 </li></ul><ul><li>class B </li></ul><ul><li>{public: </li></ul><ul><li>B doit( ); }; </li></ul><ul><li>class C </li></ul><ul><li>{public: </li></ul><ul><li>C doit( ); // 放在私有成员中同样不行 }; </li></ul><ul><li>class Derived :public B, public C </li></ul><ul><li>{……} ; </li></ul><ul><li>Derived d; </li></ul><ul><li>d.doit( );// 模棱两可 </li></ul><ul><li>d.B::doit( );// 正确 </li></ul><ul><li>d.C;;doit( );// 正确 </li></ul>
  105. 105. 27 .如果不想使用编译器暗自产生的成员函数,明确地拒绝 <ul><li>不允许一个函数存在,只要不把它放进 class 中。但赋值函数,拷贝构造函数例外,系统会自行产生一个这种函数。 </li></ul><ul><li>不许对象调用某个函数,把它放在私有成员中。但公有函数,友元可以调用。 </li></ul><ul><li>声明一个函数,而不定义它,调用它编译器会指出错误。 </li></ul>
  106. 106. 28 .尝试切割 global namespace ( 全局命名空间 ) <ul><li>标识符重名会引起混乱。同类名词冠以同一 </li></ul><ul><li>词头会使名字太长。 </li></ul><ul><li>建议使用 namespace 名字空间 </li></ul><ul><li>namespace sdm </li></ul><ul><li>{ </li></ul><ul><li>const int BOOK_VERSION = 2.0; </li></ul><ul><li>class Handle{……}; </li></ul><ul><li>Handle getHandle( ); </li></ul><ul><li>} </li></ul>
  107. 107. 有三种方法取用 namespace 内的名字。 <ul><li>void f1( ) </li></ul><ul><li>{ </li></ul><ul><li>using namespace sdm;// 汇入所有名字 </li></ul><ul><li>cout<<BOOK_VERSION; </li></ul><ul><li>Handle h=getHandle( ); </li></ul><ul><li>…… </li></ul><ul><li>} </li></ul>
  108. 108. <ul><li>void f2( ) </li></ul><ul><li>{ </li></ul><ul><li>using sdm:: BOOK_VERSION;// 汇入单个名字 </li></ul><ul><li>cout<<BOOK_VERSION;// 正确 </li></ul><ul><li>Handle h=getHandle( );// 错误 </li></ul><ul><li>…… </li></ul><ul><li>} </li></ul><ul><li>void f3( ) </li></ul><ul><li>{ </li></ul><ul><li>cout<< sdm:: BOOK_VERSION; // 没问题只用一次 </li></ul><ul><li>double d=BOOK_VERSION;// 错误 </li></ul><ul><li>Handle h=getHandle( );// 错误 </li></ul><ul><li>…… </li></ul><ul><li>} </li></ul>
  109. 109. <ul><li>两个 namespace 中有相同的名字,只要标明 namespace 域名即可区分。 </li></ul><ul><li>using namespace stm ; </li></ul><ul><li>using namespace sdm ; </li></ul><ul><li>stm :;BOOK_VERSION; </li></ul><ul><li>sdm ::BOOK_VERSION; </li></ul>
  110. 110. 类与函数的实现 29 .避免传回内部数据的 handles <ul><li>class String{public: </li></ul><ul><li>String(const char *value); </li></ul><ul><li>~String( ); </li></ul><ul><li>operator char *( ) const; </li></ul><ul><li>private: </li></ul><ul><li>char * data;}; </li></ul><ul><li>inline String::operator char *( )const </li></ul><ul><li>{ return data; } // 潜伏着危险 </li></ul><ul><li>const String B(“I love you!”); </li></ul><ul><li>char * str = B;// str 与 B.data 指向同一个地址 </li></ul><ul><li>strcpy(str, “I love you ?”);/ / 改变 str, 也就改变了 B.data </li></ul><ul><li>字符串常量 B 被改变。 </li></ul>
  111. 111. <ul><li>去掉 operator char*( )const; 中 const , </li></ul><ul><li>可以令常量 B 不能调用 operator char *( ); </li></ul><ul><li>但 String 便不能转换成 char*. </li></ul><ul><li>安全的做法是: </li></ul><ul><li>inline String::operator char *( )const </li></ul><ul><li>{ char* copy = new char[strlen(data)+1]; </li></ul><ul><li>strcpy(copy, data); </li></ul><ul><li>return copy; </li></ul><ul><li>} </li></ul><ul><li>这个函数比较慢,可能产生内存丢失。 </li></ul>
  112. 112. <ul><li>class String{ </li></ul><ul><li>public: </li></ul><ul><li>String(const char *value); </li></ul><ul><li>~String( ); </li></ul><ul><li>operator const char *( ) const; </li></ul><ul><li>private: </li></ul><ul><li>char * data; </li></ul><ul><li>}; </li></ul><ul><li>inline String::operator const char *( )const </li></ul><ul><li>{ return data; }// 又快又安全 </li></ul><ul><li>传回一个常量指针,不可改变。 </li></ul>
  113. 113. <ul><li>引用也可能发生传回内部数据的 handle 问题。 </li></ul><ul><li>class String{ </li></ul><ul><li>public: </li></ul><ul><li>…… </li></ul><ul><li>char& operator[ ](int index)const </li></ul><ul><li>{return data[index];} </li></ul><ul><li>private: </li></ul><ul><li>char *data; </li></ul><ul><li>}; </li></ul><ul><li>String s= “I am not const”; </li></ul><ul><li>s[0]= ‘x’; </li></ul><ul><li>const String cs = “I am const”; </li></ul><ul><li>cs[0] = ‘x’; // 改变了常字符串,编译器没发现 </li></ul><ul><li>问题出在 const 函数返回值是引用。 </li></ul>
  114. 114. <ul><li>即使不是 const 函数,“传回 handles” 也是有问题的。 </li></ul><ul><li>// 随机选择一个作家的名字 </li></ul><ul><li>String someFamousAuthor( ) </li></ul><ul><li>{ switch( rand( )%3){ // 随机函数 <stdlib.h> </li></ul><ul><li>case 0: </li></ul><ul><li>return “Margaret Mitchell”; // 《飘》的作者 </li></ul><ul><li>case 1: </li></ul><ul><li>return “Stephen King”; // 著名通俗小说家 </li></ul><ul><li>case 3: </li></ul><ul><li>return “Scott Meyers” // 本书作者 </li></ul><ul><li>} </li></ul><ul><li>return “ ”; </li></ul><ul><li>} </li></ul>
  115. 115. <ul><li>这个函数的返回值是一个局部指针 handle ,函数用毕被销毁 </li></ul><ul><li>因此返回值将是无定义的内存 (dangling pointer) 。 </li></ul><ul><li>尽可能避免让一个函数传回 dangling handles ,可以提高程序的可靠性。 </li></ul>
  116. 116. <ul><li>避免写出成员函数返回一个 non-const pointer 或 reference( 引用 ) 并以之指向较低存取层级的成员 </li></ul><ul><li>class Address{……} ; </li></ul><ul><li>class Person{ </li></ul><ul><li>public: </li></ul><ul><li>address& personAddress( ){return address;} </li></ul><ul><li>…… </li></ul><ul><li>private: </li></ul><ul><li>Address address; // 私有数据成员 </li></ul><ul><li>…… </li></ul><ul><li>} ; </li></ul>
  117. 117. <ul><li>Person scott(……); </li></ul><ul><li>Address& addr=scott.personAddress( ); </li></ul><ul><li>// 全局对象 addr 与 scott.address 同一地址 </li></ul><ul><li>私有成员公开化,可以从外部改变私有数据的值。 </li></ul><ul><li>变成指针也有同样的问题。 </li></ul>
  118. 118. 31 .千万不要返回“函数内局部对象的 reference” 或“函数内以 new 获得的指针所指的对象” <ul><li>“ 传回一个引用( reference ),指向局部对象”,函数用毕返回时,局部对象被析构,变成引用一个不存在的对象。 </li></ul><ul><li>const complex& operator*(const complex& lhs , const complex& rhs) </li></ul><ul><li>{complex temp(lhs.x*rhs.x-lhs.y*rhs.y, lhs.x*rhs.y+lhs.y*rhs.x); </li></ul><ul><li>return temp; } </li></ul><ul><li>错误。返回值地址指向局部对象,与局部对象同名,运算执行完毕,局部对象被析构,返回值指向一个不存在的地址。 </li></ul>
  119. 119. <ul><li>class Address{……} ; </li></ul><ul><li>class Person{ </li></ul><ul><li>public: </li></ul><ul><li>address& personAddress( ){return address;} </li></ul><ul><li>…… </li></ul><ul><li>private: </li></ul><ul><li>Address address; // 私有数据成员 </li></ul><ul><li>…… </li></ul><ul><li>} ; </li></ul><ul><li>Person scott(……); </li></ul><ul><li>Address& addrPtr=scott.personAddress( ); </li></ul><ul><li>// 指针 addr 指向 scott.address 同一地址 </li></ul><ul><li>只要把函数返回值改成 const 类型即可避免外部修改。 </li></ul>
  120. 120. <ul><li>class Address{……} ; </li></ul><ul><li>class Person{ </li></ul><ul><li>public: </li></ul><ul><li>address* personAddress( ){return address;} </li></ul><ul><li>…… </li></ul><ul><li>private: </li></ul><ul><li>Address address; // 私有数据成员 </li></ul><ul><li>…… </li></ul><ul><li>} ; </li></ul><ul><li>Person scott(……); </li></ul><ul><li>Address* addrPtr=scott.personAddress( ); </li></ul><ul><li>// 指针 addr 指向 scott.address 同一地址 </li></ul><ul><li>同样问题 </li></ul><ul><li>只要把函数返回值改成 const 类型即可避免外部修改。 </li></ul>
  121. 121. 32. 尽可能延缓变量定义式的出现 <ul><li>// 这个函数太早定义变量 encrypted </li></ul><ul><li>string encryptPassward(const string& passward) </li></ul><ul><li>{ string encrypted; </li></ul><ul><li>if ( passward.length( ) < MINIMUM_PASSWARD_LENGTH) </li></ul><ul><li>{throw logic_error(“Passward is too short”); </li></ul><ul><li>…… </li></ul><ul><li>return encrypted; </li></ul><ul><li>} </li></ul><ul><li>一旦有异常抛出, encrypted 定义就是多余的。 </li></ul><ul><li>尽量在变量定义时初始化。 </li></ul>
  122. 122. 33 . 明智地运用 inline <ul><li>inline 内联函数提高效率,编译时实现最佳化。但是增加 object 目标代码,加大内存开销。 </li></ul><ul><li>太多的 inline 函数,会减低取出指令的速度( instruction fetch ) </li></ul><ul><li>编译器会因某些理由自动拒绝 inline 函数,将它当成非 inline 函数。这时系统发出一个警告。比如太长的函数, virtual 函数,不适合 inline 的函数。 </li></ul><ul><li>构造函数看起来不长,有时继承派生类中的构造函数,比看起来的要长。 </li></ul>
  123. 123. <ul><li>比如基类的构造函数, new 的使用,都使 inline 失效。 </li></ul><ul><li>inline 函数不会自动升级,程序一旦被改动就要重新编译。 </li></ul><ul><li>inline 函数应限制用于一些非常平凡的,占有重要效率地位的函数。 </li></ul><ul><li>慎重使用 inline 函数,便于日后除错。 </li></ul>
  124. 124. 34 . 将文件之间的编译关系降到最低 <ul><li>一个 class 在文件 file 中定义并实现。程序用到这个类的对象就要连接文件 file </li></ul><ul><li>#include<file.h> </li></ul><ul><li>file 文件就与程序发生依赖关系。修改 file 文件,会引起全程序重编译。 </li></ul><ul><li>C++ 一个 class B 中,用到另一 class A 的对象, A 必须先完全定义并实现。 </li></ul><ul><li>如果 A 写在文件 file 里, A 的任何改变都会引起整个程序重新编译。 </li></ul><ul><li>改进 的方法是 A 中尽量使用指向 B 类对象的指针或引用 。这样, B 只要先声明, A 的编译不依赖于 B. file 改变时, A 不需要重新编译。 </li></ul><ul><li>A 称为 Handle class </li></ul>
  125. 125. <ul><li>另一种方法是用抽象类做基类,成为 Protocol class 。 Protocol class 没有任何实现。其作用只是一个接口。 </li></ul><ul><li>如果引用或指针能够完成任务,就不用对象。 </li></ul><ul><li>如果可能, 以 class 的声明,代替 class 的实现。 </li></ul><ul><li>尽可能只把 class 的声明放在头文件里,其余实现放在由客户完成的文件里,不参与编译。 </li></ul><ul><li>头文件尽量不 include 别的头文件,除非不联不行。 </li></ul>
  126. 126. 继承关系与面向对象设计 <ul><li>继承体系是 C++ 与 C 的更本区别。 </li></ul><ul><li>如果需要一群 class ,拥有许多共享性质,那就要考虑用基类还是模板。 </li></ul><ul><li>如果 class A 是根据 class B 实现,考虑 A 中应该有一个 B 类对象还是 A 继承 B. </li></ul><ul><li>如果需要设计一个安全类型,通用,而 C++ 标准库未定义,那么应该使用模板还是以泛型 void* 指针来实现? </li></ul><ul><li>“ 说出你的意思,并了解你所说的每一句话。” </li></ul>
  127. 127. 35 .公有继承, “ isa” 的关系 <ul><li>请牢记: 公有继承 public inheritance 是一种 isa 的关系。 </li></ul><ul><li>如果 class D 公有继承 class B, 则 D 是 B 的 subclass 子类。 D 的对象是 B 的对象 , 反之不成立。 B 比 D 更一般化, D 比 B 更特殊化。可以用 B 对象的地方, D 的对象也可以用。要用 D 对象的地方, B 对象无法效劳。 </li></ul><ul><li>每一匹白马都是马,每一匹马不一定是白马。公孙策“白马非马”,白马是马的真子集,而不相等。马是基类,白马是派生类。 </li></ul>
  128. 128. <ul><li>C++ 的继承关系,不同于日常生活,也不同于数学。 </li></ul><ul><li>鸟 bird 会飞。 </li></ul><ul><li>企鹅 penguin 是鸟,可企鹅不会飞。如果鸟 class 中有 fly ,则企鹅不是鸟的派生类。 </li></ul><ul><li>长方形  正方形?还是 正方形  长方形?两者都不对。 </li></ul><ul><li>另外定义一个基类, </li></ul><ul><li>长方形和正方形都是它的派生类。 </li></ul>
  129. 129. 36 .区分接口继承( interface inheritance )和实现继承( implementation inheritance ) <ul><li>成员函数的接口总是会被继承 </li></ul><ul><li>声明纯虚函数是为了让导出类只继承其接口。 </li></ul><ul><li>纯虚函数也可以定义,即可以提供其实现代码。只有一种方法调用就是写明 class 域名 . </li></ul><ul><li>声明一般虚函数(非纯)是为了让导出类只继承其接口和缺省行为。 </li></ul><ul><li>声明非虚拟函数是为了让导出类只继其接口及其实现。 </li></ul><ul><li>继承类中行为不变的函数不应声名为虚函数。 </li></ul>
  130. 130. 37. 绝对不要重新定义继承而来的非虚函数 <ul><li>如果基类中定义了一个非虚函数,派生类同时继承其接口和实现。派生类的对象可直接调用基类的这个函数,行为不变。 </li></ul><ul><li>如果派生类中重新定义继承而来的非虚函数,基类中的函数将被覆盖。函数调用将发生变化。这就是说本应该定以为虚函数。 </li></ul><ul><li>如果不希望函数行为有变化,则不应该重定义非虚函数。 </li></ul>
  131. 131. 38 .绝对不要重新定义继承而来的缺省参数值 <ul><li>虚函数的调用由实际引入的参数对象确定。动态绑定,后联编。 </li></ul><ul><li>但如果函数有缺省值,缺省值则是静态绑定。调用一个有缺省值的虚函数, </li></ul><ul><li>非缺省参数的派生类型实参确定调用哪一个函数,但缺省参数依然用基类的值。 </li></ul><ul><li>重新定义的缺省参数并不起任何作用,反而引起混淆。 </li></ul><ul><li>要么不定义虚函数参数的缺省值,有了缺省值就不可以重定义。 </li></ul>
  132. 132. 39 .避免在继承中做向下转型( castdown )动作 <ul><li>class Person{……} ; </li></ul><ul><li>class BankAccount </li></ul><ul><li>{ BankAccount(const Person* primaryOwner, const Person *jointOwner); </li></ul><ul><li>virtual ~BankAccount( ); </li></ul><ul><li>virtual void makeDepoisit(double amount)=0; </li></ul><ul><li>// 存款 </li></ul><ul><li>virtual void makeWithdrawal(double amount)=0; </li></ul><ul><li>// 取款 </li></ul><ul><li>virtual void balance( )const =0;// 余额 </li></ul><ul><li>…… </li></ul><ul><li>} ; </li></ul>
  133. 133. <ul><li>class SavingAccount : public BankAccout </li></ul><ul><li>{ public: </li></ul><ul><li>SavingAccount(const Person *primaryOwner, const Person *jointOwner); </li></ul><ul><li>~SavingAccount( ); </li></ul><ul><li>void creditInterest( ); // 利息加入账户 </li></ul><ul><li>…… } ; </li></ul><ul><li>list<BankAccount*>allAccount;// 所有账户列表, </li></ul><ul><li>for(list<BankAccount*>::iterator p=allAccount.begin( ); </li></ul><ul><li>p!=allAccount.end( ); ++p) </li></ul><ul><li>//p 相当于 BankAccount** 类型 </li></ul><ul><li>{ </li></ul><ul><li>(*p)->creditInterest( ); // 错误! </li></ul><ul><li>} </li></ul>
  134. 134. <ul><li>*p 是 BankAccount* 指针, BankAccount 类中没有 creditInterest( ) 函数。尽管 BankAccount 是纯虚基类,没有实现, SavingAccount 是唯一可实现的类。编译器仍然读不懂 (*p)->creditInterest( ); </li></ul>
  135. 135. <ul><li>可以改为: </li></ul><ul><li>for(list<BankAccount*>::iterator p=allAccount.begin( ); </li></ul><ul><li>p!=allAccount.end( ); ++p) </li></ul><ul><li>{ </li></ul><ul><li>static_cast<SavingAccount*>(*p)->creditInterest( ); </li></ul><ul><li>// 强制转型,向下转型! </li></ul><ul><li>} </li></ul><ul><li>从基类向下到派生类的转型 downcast 会导致维护上的梦魇。 </li></ul><ul><li>// 增加支票账户类 </li></ul><ul><li>class CheckingAccount : public BankAccount </li></ul><ul><li>{ public: </li></ul><ul><li>void CreditInterest( ); // 将利息存入账户 </li></ul><ul><li>…… } ; </li></ul>
  136. 136. <ul><li>for(list<BankAccount*>::iterator p=allAccount.begin( ); </li></ul><ul><li>p!=allAccount.end( ); ++p) </li></ul><ul><li>{ if(*p points to a SavingAccount) </li></ul><ul><li>static_cast<SavingAccount*>(*p)->creditInterest( ); </li></ul><ul><li>// 强制向下转型! </li></ul><ul><li>if(*p points to a checkingAcount) </li></ul><ul><li>static_cast<checkingAccount*>(*p)->creditInterest( ); </li></ul><ul><li>// 强制向下转型 } </li></ul><ul><li>这是不符合 C++ 精神的做法。 C++ 中以对象类型决定不同行为,应该使用虚函数。 </li></ul>
  137. 137. <ul><li>class BankAccount{……};// 如前 </li></ul><ul><li>class interestBearingAccount : public BankAccount </li></ul><ul><li>{ public: </li></ul><ul><li>virtual void creditInterest( )=0; </li></ul><ul><li>…… } ; </li></ul><ul><li>class SavingAccount : </li></ul><ul><li>public InteresttBearingAccount </li></ul><ul><li>{……} ; // 如前 </li></ul><ul><li>class checkingAccount : </li></ul><ul><li>public InteresttBearingAccount </li></ul><ul><li>{……} ; // 如前 </li></ul><ul><li>list<InterestBearingAccount*> allIBAccount; </li></ul><ul><li>// 银行中所有“须付利息的账户” </li></ul>
  138. 138. <ul><li>for(list<InterestBearingAccount*>:: </li></ul><ul><li>iterator p=allIBAccount.begin( ); </li></ul><ul><li>p!=allIBAccount.end( ); ++p) </li></ul><ul><li>{ </li></ul><ul><li>(*p)->creditInterest( ); // 正确,也适用于将来 </li></ul><ul><li>} </li></ul><ul><li>downcast 向下转型,可以用虚函数的方法消除。 </li></ul><ul><li>向下转型容易出错,难以理解,难以维护。 </li></ul><ul><li>如果非向下转型不可,可以采用“安全向下转型动作”( safedowncasting ) </li></ul>
  139. 139. <ul><li>dynamic_cast 将一个指针指向的动态对象转变成要求的类型,如果失败,传回 null 指针。 </li></ul><ul><li>class BankAccount{……};// 如前 </li></ul><ul><li>class SavingAccount : public BAnkAccount </li></ul><ul><li>{……} ; // 如前 </li></ul><ul><li>class checkingAccount : public BankAccount </li></ul><ul><li>{……} ; // 如前 </li></ul><ul><li>list<BankAccount*> allAccount;// 银行中所有账户 </li></ul><ul><li>void error(const string& msg); </li></ul>
  140. 140. <ul><li>for(list<BankAccount*>:: </li></ul><ul><li>iterator p=allAccount.begin( ); </li></ul><ul><li>p!=allAccount.end( ); ++p) </li></ul><ul><li>{ if( SavingAccount *psa = dynamic_cast<SavingAccount*>(*p)) </li></ul><ul><li>psa->creditInterest( ); // 安全转型,向下转型! </li></ul><ul><li>else if( checkingAcount </li></ul><ul><li>*pca=dynamic_cast<checkingAccount*>(*p)) </li></ul><ul><li>pca->creditInterest( ); // 安全向下转型! </li></ul><ul><li>else </li></ul><ul><li>error( “unknown account type!” ); </li></ul><ul><li>} </li></ul>
  141. 141. <ul><li>向下转型失败时可以侦察得到。但是,如果有人新加入一个账户类型,又忘记更新以上代码,上面这个程序中所有 downcast 都会失败。 </li></ul><ul><li>downcast 会导致程序难以维护。比起虚函数相差太远。非万不得已,不要出此下策。 </li></ul>
  142. 142. 40 .通过 layering 技术来模塑 has-a 或 is-implemented-in-terms-of 的关系 <ul><li>layering 技术: 用另一个类的对象做本类数据成员。 </li></ul><ul><li>称 layering class (外层), layered class (内层) </li></ul><ul><li>class Address{……} ; </li></ul><ul><li>class PhoneNumber{……} ; </li></ul><ul><li>class Person </li></ul><ul><li>{ public: …… </li></ul><ul><li>private: </li></ul><ul><li>string name; //layered object </li></ul><ul><li>Address address; //layered object </li></ul><ul><li>PhoneNumber voicNumber; //layered object </li></ul><ul><li>PhoneNumber faxNumber; //layered object </li></ul><ul><li>}; </li></ul>
  143. 143. <ul><li>称 Person 类铺陈( layered )在 string, Address, PhoneNumber 之上。 </li></ul><ul><li>layering == composition, containment, embedding; </li></ul><ul><li>layering 意味着有一个 has-a 或根据某物实现( is-implemented-in-terms of ) . </li></ul><ul><li>如何区分 isa 和 has-a ? </li></ul>
  144. 144. <ul><li>C++ 标准库有一个 set<T> 模板类,要求 set 的元素类型 T 是整型,可以编号排序。如果需要做一个 set 其元素类型 T 不能排序,怎么办? </li></ul><ul><li>可以用 list<T> 来实现 set? list<T> 是 T 的链表类型。 </li></ul><ul><li>template<class T> </li></ul><ul><li>class Set : public list<T>{……} ; </li></ul><ul><li>实际上完全错误。因为链表中元素可以重复出现,而集合不允许。 </li></ul><ul><li>正确的方法是,用链表做成员,重新设计一个集合类型即可。 </li></ul>
  145. 145. <ul><li>template<class T> </li></ul><ul><li>class Set </li></ul><ul><li>{ public: </li></ul><ul><li>bool member(const T& item)const; </li></ul><ul><li>void insert(const T& item); </li></ul><ul><li>void remove(const T& item); </li></ul><ul><li>int cardinality ( ) const; // 集合中元素个数 </li></ul><ul><li>private: </li></ul><ul><li>list<T> rep;// 链表作成员 </li></ul><ul><li>}; </li></ul>
  146. 146. <ul><li>template<class T> </li></ul><ul><li>bool set<T>:: member (const T& item)const </li></ul><ul><li>{ return find(rep.begin( ), rep.end( ), item)!= </li></ul><ul><li>rep.end;) </li></ul><ul><li>} </li></ul><ul><li>template<class T> </li></ul><ul><li>void set<T>:: insert (const T& item) </li></ul><ul><li>{ </li></ul><ul><li>if(!member(item))rep.push_back(item); </li></ul><ul><li>} </li></ul>
  147. 147. <ul><li>template<class T> </li></ul><ul><li>void set<T>:: remove (const T& item) </li></ul><ul><li>{list<T>::iterator it = find(rep.begin( ), rep.end( ), item); </li></ul><ul><li>if(it != rep.end( ))rep.erase(it); </li></ul><ul><li>} </li></ul><ul><li>template<class T> </li></ul><ul><li>int set<T>:: cardinality ( )const </li></ul><ul><li>{ return rep.size( ); </li></ul><ul><li>} </li></ul><ul><li>注意:采用 layering 技术使两个 class 有了编译依赖关系,最好采用 34 条方法加以改进。 </li></ul>
  148. 148. 41 .区分继承和模板 <ul><li>两个例子: </li></ul><ul><li>信息系学生学习 C++ ,想自行设计 stack , int stack, string stack 等等。 </li></ul><ul><li>一个猫迷想设计一些 cat classes 表现不同品钟的猫。 </li></ul><ul><li>必须先问自己一个问题,不同的类型 T 会不会影响 class 的行为。如有影响,必须用虚函数,从而要用继承机制。如不影响可以用模板。 </li></ul><ul><li>不同类型的 stack 都有相同的行为。 push, pop, peek, empty 等等,用 template 再简单不过了。 </li></ul><ul><li>而不同的猫有不同的特性,不同的吃睡本领。用继承最好。 </li></ul>
  149. 149. 42 .明智地运用私有继承 <ul><li>class Person{……} ; </li></ul><ul><li>class Student : private Person // 私有继承 </li></ul><ul><li>{……} ; </li></ul><ul><li>void dance(const Person& p); // 任何人都会跳舞 </li></ul><ul><li>void study(const Student& s); // 只有学生才在学校学习 </li></ul><ul><li>Person p; </li></ul><ul><li>Student s; </li></ul><ul><li>dance(p); </li></ul><ul><li>dance(s);// 错误! student 不是 Person 对象 </li></ul>
  150. 150. <ul><li>私有继承,基类中的公有,和保护成员在派生类中都变成私有成员。 </li></ul><ul><li>采用私有继承,并不希望 isa, 即导出类对象不再是基类对象。 </li></ul><ul><li>基类中私有数据成员在导出类中不可见。 </li></ul><ul><li>公有和保护成员函数的实现,可以被导出类成员函数采用。 </li></ul><ul><li>尽量不用私有继承,除非必要 。 </li></ul>
  151. 151. <ul><li>模板类的每一个具体类型化,都会产生一份代码。十个类型便得到十份完整代码。 </li></ul><ul><li>“ 因 template 导致程序代码膨胀”。 </li></ul><ul><li>可以用泛型指针来避免代码膨胀。 </li></ul><ul><li>把模板 stack 改为非模板 stack class. </li></ul>
  152. 152. <ul><li>class GenericStack </li></ul><ul><li>{ protected: </li></ul><ul><li>GenericStack( ); </li></ul><ul><li>~GenericStack( ); </li></ul><ul><li>void push(void * object); </li></ul><ul><li>void * pop( ); </li></ul><ul><li>bool empty( )const; </li></ul>
  153. 153. <ul><li>private: </li></ul><ul><li>struct StackNode </li></ul><ul><li>{ void * data; </li></ul><ul><li>StackNode *next; </li></ul><ul><li>StackNode(void *newData, StackNode, </li></ul><ul><li>*NextNode) </li></ul><ul><li>: data(newData), next(nextNode){ } </li></ul><ul><li>}; </li></ul><ul><li>StackNode *top; </li></ul><ul><li>GenericStack(const GenericStack& rhs); </li></ul><ul><li>GenericStack& operator= </li></ul><ul><li>(const GenericStack& rhs); </li></ul><ul><li>}; </li></ul>
  154. 154. <ul><li>GenericStack 类不能使用,把 protected 改成 public 可以 </li></ul><ul><li>用,但太容易出错了。泛型指针对任意指针都不加区别。 </li></ul><ul><li>可以用私有继承完成代码: </li></ul><ul><li>class IntStack : private GenericStack </li></ul><ul><li>{ </li></ul><ul><li>public: </li></ul><ul><li>void push (int *intptr) </li></ul><ul><li>{ GenerickStack::push(intptr);} </li></ul><ul><li>int * pop( ) </li></ul><ul><li>{ return static_cast<int*>(GenerickStack::pop( ));} </li></ul><ul><li>bool empty( )const </li></ul><ul><li>{ return GenericStack::empty( );} </li></ul><ul><li>} ; </li></ul>
  155. 155. <ul><li>class CatStack : private GenericStack </li></ul><ul><li>{ </li></ul><ul><li>public: </li></ul><ul><li>void push (Cat *icatptr) </li></ul><ul><li>{ GenerickStack::push(icatptr);} </li></ul><ul><li>Cat * pop( ) </li></ul><ul><li>{ return </li></ul><ul><li>static_cast<Cat*>(GenerickStack::pop( ));} </li></ul><ul><li>bool empty( )const </li></ul><ul><li>{ return GenericStack::empty( );} </li></ul><ul><li>} ; </li></ul><ul><li>IntStack ints;// 没问题 </li></ul><ul><li>CatStack cs;// 没问题 </li></ul>
  156. 156. <ul><li>与 layering 技术一样,私有继承实现的代码,可以避免重复。还可以用模板私有继承改进,进一步避免太多的书写。 </li></ul><ul><li>template<class T> </li></ul><ul><li>class Stack : private GenericStack </li></ul><ul><li>{ </li></ul><ul><li>public: </li></ul><ul><li>void push (T *objectptr) </li></ul><ul><li>{ GenerickStack::push(objectptr);} </li></ul><ul><li>int * pop( ) </li></ul><ul><li>{ return static_cast<T*>(GenerickStack::pop( ));} </li></ul><ul><li>bool empty( )const </li></ul><ul><li>{ return GenericStack::empty( );} </li></ul><ul><li>} ; </li></ul>
  157. 157. <ul><li>这是一个通用 Stack , 可以具体实现任意类的 Stack. 如果你写错字,编译器会自动指出错误。由于 GenericStack 使用泛型指针,这段代码只付出一份拷贝的代价。 </li></ul><ul><li>因此,这个程序既安全又高效,很难做得更好。 </li></ul>
  158. 158. 43 .明智地运用多继承 <ul><li>多继承产生歧义( ambiguity ) , 两个基类中有同名成员,派生类中必须指明其基类。 </li></ul><ul><li>由一个类派 A 生出两个 B,C, 再由 B,C 多继承派生出 D, 所谓钻石形,出现模棱两可歧义。 </li></ul><ul><li>用虚基类可以避免数据成员的模棱两可。 </li></ul><ul><li>但要避免将构造函数的参数传给虚基类。最好的办法是虚基类中不要有任何数据成员。 Java 就有这样的规定。 </li></ul><ul><li>但怎样处理虚函数呢?虚函数经由不同的路径产生模棱两可。因此要避免钻石形继承。 </li></ul><ul><li>但非钻石形多继承是有意义的。 </li></ul>
  159. 159. <ul><li>两个基类一个是抽象类继承其接口,另一个继承其实现,有时有奇妙的作用。 </li></ul><ul><li>一个公有继承,一个私有继承。 </li></ul><ul><li>class Person </li></ul><ul><li>{ public: </li></ul><ul><li>virtual ~Person( ); </li></ul><ul><li>virtual string name( )const=0; </li></ul><ul><li>virtual string birthDate( )const=0; </li></ul><ul><li>virtual string address( )const=0; </li></ul><ul><li>virtual string nationality( )const=0; </li></ul><ul><li>}; </li></ul>
  160. 160. <ul><li>class DatabaseID{……} ; </li></ul><ul><li>class PersonInfo </li></ul><ul><li>{ public: </li></ul><ul><li>PersonInfo(DatabaseID pid); </li></ul><ul><li>virtual ~PersonInfo( ); </li></ul><ul><li>virtual const char * thename( )const; </li></ul><ul><li>virtual const char * theBirthDate( )const; </li></ul><ul><li>virtual const char * theAddress( )const; </li></ul><ul><li>virtual const char * theNationality( )const; </li></ul><ul><li>virtual const char * valumeDelimOpen( )const; </li></ul><ul><li>virtual const char * valumeDelimClose( )const; </li></ul><ul><li>…… </li></ul><ul><li>} ; </li></ul>
  161. 161. <ul><li>class MyPerson : public Person, private PersonInfo </li></ul><ul><li>{ public: </li></ul><ul><li>MyPerson(DatabaseID pid): PersonInfo(pid){ } </li></ul><ul><li>const char *valumeDelimOpen( )const{return “ ” ; } </li></ul><ul><li>const char *valumeDelimClose( )const{return “ ” ; } </li></ul><ul><li>string name( )const </li></ul><ul><li>{return PersonInfo::theName( );} </li></ul><ul><li>string birthDate( )const </li></ul><ul><li>{return PersonInfo::theBirthDate( );} </li></ul><ul><li>string address( )const </li></ul><ul><li>{return PersonInfo::address( );} </li></ul><ul><li>string nationality( )const </li></ul><ul><li>{return PersonInfo::theNationality( );} </li></ul><ul><li>}; </li></ul>
  162. 162. <ul><li>43 .说出你的意思并了解你所说的每一句话 </li></ul><ul><li>杂项讨论 </li></ul><ul><li>45 .清楚知道 C++ 编译器为我们完成和调用哪些函数 </li></ul><ul><li>46 .宁愿编译和连接时出错,也不要执行时出错 </li></ul><ul><li>47 .使用 non-local static objects 之前确定它已有 </li></ul><ul><li>初值 </li></ul><ul><li>48 .不要对编译器的警告信息视而不见 </li></ul><ul><li>49 .尽量让自己熟息 C++ 标准程序库 </li></ul><ul><li>50 .加强自己对 C++ 的了解 </li></ul>
  163. 163. <ul><li>class CartoonCharacter{……}; // 卡通形象 </li></ul><ul><li>class Insert : public CartoonCharacter // 昆虫 </li></ul><ul><li>{ public: </li></ul><ul><li>virtual void dance( ); </li></ul><ul><li>virtual void sing( ); </li></ul><ul><li>protected: </li></ul><ul><li>virtual void danceCustomization1( )=0; </li></ul><ul><li>virtual void danceCustomization2( )=0; </li></ul><ul><li>virtual void singCustomization ( )=0; </li></ul><ul><li>}; </li></ul>
  164. 164. <ul><li>class Grasshoper : public Insert // 蚱蜢 </li></ul><ul><li>{ protected: </li></ul><ul><li>virtual void danceCustomization1( )=0; </li></ul><ul><li>virtual void danceCustomization2( )=0; </li></ul><ul><li>virtual void singCustomization ( )=0; </li></ul><ul><li>}; </li></ul><ul><li>class Cricket : public Insert // 蟋蟀 </li></ul><ul><li>{ protected: </li></ul><ul><li>virtual void danceCustomization1( )=0; </li></ul><ul><li>virtual void danceCustomization2( )=0; </li></ul><ul><li>virtual void singCustomization ( )=0; </li></ul><ul><li>}; </li></ul>
  165. 165. 44 .说出你的意思并了解你所说的每一句话 <ul><li>共同的基类意味着共同的特性公有继承是一种 isa </li></ul><ul><li>私有继承意味着根据某物实现 </li></ul><ul><li>( is-implemented-in-terms-of ) </li></ul><ul><li>layering 意味着 has-a 或 </li></ul><ul><li>is-implemented-in-terms-of </li></ul><ul><li>公有继承中 </li></ul><ul><li>纯虚函数意味着只继承函数的接口 </li></ul><ul><li>非纯虚函数意味着函数接口和缺省参数会被继承 </li></ul><ul><li>非虚函数意味着函数的接口和实现都被继承 </li></ul>
  166. 166. 杂项讨论 45 .清楚知道 C++ 编译器为我们完成和调用哪些函数 <ul><li>一个空类,编译器处理过后,会为你声明缺省构造函数,拷贝构造函数,析构函数,赋值运算,取址运算 </li></ul><ul><li>如果要禁止某个函数,要明确说明,采取措施。 </li></ul>
  167. 167. 46 .宁愿编译和连接时出错,也不要执行时出错 <ul><li>尽可能将检查错误的工作交给编译器来做。 </li></ul><ul><li>数组边界检查,数据有效性检查。 </li></ul><ul><li>连接期检验检查一个用到的函数只被定义一次。 </li></ul><ul><li>执行期错误要靠自己运行检验,设各种可能的计边界条件,出界条件来检验。 </li></ul>
  168. 168. 47 .使用 non-local static objects 之前确定它已有初值 <ul><li>non-local static objects 指 </li></ul><ul><li>全局或 namespace 中的静态对象 </li></ul><ul><li>某个类内声明的对象 </li></ul><ul><li>某个文件内定义的对象 </li></ul><ul><li>Singleton patten 技术:将一个 non-local static objects 放到一个函数里,让函数返回一个引用,指向这个 non-local static objects 。 这时编译器会检查是否初始化。 </li></ul>
  169. 169. <ul><li>48 .不要对编译器的警告信息视而不见 </li></ul><ul><li>弄清每一个警告的含义避免出错。 </li></ul>
  170. 170. 49 .尽量让自己熟息 C++ 标准程序库 <ul><li>旧 C++ 头文件,如 <iostream.h> 继续存在,但不在 namespace std 内 </li></ul><ul><li>新的 C++ 头文件,如 <iostream> 对应旧的头文件,放在 namespace std 内 </li></ul><ul><li>C 头文件,如 <stdio.h> 继续存在,但不在 namespace std 内 </li></ul><ul><li>新的 C++ 头文件,如 <cstdio> 对 C 的头文件,放在 namespace std 内 </li></ul><ul><li>标准程序库都被 template 化 </li></ul><ul><li>不要自己声明标准程序库中已有的任何东西,只要包含适当头文件就可以。 </li></ul>
  171. 171. <ul><li>iostreams 内容增加,原有都有效 </li></ul><ul><li>string 效率更高 </li></ul><ul><li>STL Standard Template Library 可扩充 </li></ul><ul><li>containers 包括 </li></ul><ul><li>vectors( 动态扩充数组 ) 、 lists( 双向串行 ) 、 queues 、 stacks 、 deques 、 maps 、 sets 、 bitsets </li></ul><ul><li>iterator </li></ul><ul><li>Algorithms( 算法 ) function templates 大部分适用于所有 containers </li></ul><ul><li>国际化支持 ( facets locales )国际字符对应映射,时间,日期,数值,货币表示法。 </li></ul><ul><li>数值处理 </li></ul><ul><li>诊断功能 出错处理 </li></ul>
  172. 172. 50 .加强自己对 C++ 的了解

×