Chinaunix首页 | 论坛 | 博客
  • 博客访问: 1887484
  • 博文数量: 496
  • 博客积分: 12043
  • 博客等级: 上将
  • 技术积分: 4778
  • 用 户 组: 普通用户
  • 注册时间: 2010-11-27 14:26
文章分类

全部博文(496)

文章存档

2014年(8)

2013年(4)

2012年(181)

2011年(303)

2010年(3)

分类: C/C++

2011-01-26 09:04:59

  1. 下面的这些要点是对所有的C++程序员都适用的。我之所以说它们是最重要
  2. 的,是因为这些要点中提到的是你通常在C++书中或网站上无法找到的。如:指向
  3. 成员的指针,这是许多资料中都不愿提到的地方,也是经常出错的地方,甚至是对
  4. 一些高级的C++程序员也是如此。
  5.   这里的要点不仅仅是解释怎样写出更好的代码,更多的是展现出语言规则里面
  6. 的东西。很显然,它们对C++程序员来说是永久的好资料。我相信这一篇文章会使
  7. 你收获不小。

  8.   首先,我把一些由不同层次的C++程序员经常问的问题归到一起。我惊奇的发
  9. 现有很多是有经验的程序员都还没意识到 .h 符号是否还应该出现在标准头文件
  10. 中。


  11. 要点1: 还是 ?

  12.   很多C++程序员还在使用而不是用更新的标准的库。
  13. 这两者都有什么不同呢?首先,5年前我们就开始反对把.h符号继续用在标准的头
  14. 文件中。继续使用过时的规则可不是个好的方法。从功能性的角度来讲,
  15. 包含了一系列模板化的I/O类,相反地只仅仅是支持字符
  16. 流。另外,输入输出流的C++标准规范接口在一些微妙的细节上都已改进,因此,
  17. 在接口和执行上都是不同的。最后,的各组
  18. 成都是以STL的形式声明的,然而的各组成都是声明成全局型的。

  19.   因为这些实质上的不同,你不能在一个程序中混淆使用这两个库。做为一种习
  20. 惯,在新的代码中一般使用,但如果你处理的是过去编写的代码,为了
  21. 继承可以用继续用旧保持代码的一致性。  


  22. 要点2:用引用传递参数时应注意的地方  

  23.   在用引用传递参数时,最好把引用声明为const类型。这样做的好处是:告诉
  24. 程序不能修改这个参数。在下面的这个例子中函数f()就是传递的引用:
  25.   
  26. void f(const int & i);
  27. int main()
  28. {
  29.  f(2); /* OK */
  30. }
  31.   
  32.   这个程序传递一个参数2给f()。在运行时,C++创建一个值为2的int类型的临
  33. 时变量,并传递它的引用给f().这个临时变量和它的引用从f()被调用开始被创建
  34. 并存在直到函数返回。返回时,就被马上删除。注意,如果我们不在引用前加上
  35. const限定词,则函数f()可能会更改它参数的值,更可能会使程序产生意想不到的
  36. 行为。所以,别忘了const。

  37.   这个要点也适用于用户定义的对象。你可以给临时对象也加上引用如果是
  38. const类型:
  39.   
  40. struct A{};
  41. void f(const A& a);
  42. int main()
  43. {
  44.  f(A()); // OK,传递的是一个临时A的const引用
  45. }  
  46.   

  47. 要点3:“逗号分离”表达形式

  48.  “逗号分离”表达形式是从C继承来的,使用在for-和while-循环中。当然,这
  49. 条语法规则被认为是不直观的。首先,我们来看看什么是“逗号分离”表达形式。

  50.   一个表达式由一个或多个其它表达式构成,由逗号分开,如:
  51.   
  52.  if(++x, --y, cin.good()) //三个表达式  
  53.   这个if条件包含了三个由逗号分离的表达式。C++会计算每个表达式,但完整
  54. 的“逗号分离”表达式的结果是最右边表达式的值。因此,仅当cin.good()返回
  55. true时,if条件的值才是true。下面是另一个例子:  
  56. int j=10;  
  57. int i=0;
  58. while( ++i, --j)
  59. {
  60.  //直到j=0时,循环结束,在循环时,i不断自加
  61. }  

  62. 要点4,使用全局对象的构造函数在程序启动前调用函数

  63.   有一些应用程序需要在主程序启动前调用其它函数。如:转态过程函数、登记
  64. 功能函数都是必须在实际程序运行前被调用的。最简单的办法是通过一个全局对象
  65. 的构造函数来调用这些函数。因为全局对象都是在主程序开始前被构造,这些函数
  66. 都将会在main()之前返回结果。如:  
  67. class Logger
  68. {

  69.  public:
  70.  Logger()  
  71.   {  
  72.    activate_log();//译者注:在构造函数中调用你需要先运行的函数
  73.   }
  74. };
  75. Logger log; //一个全局实例

  76. int main()
  77. {
  78.  record * prec=read_log();//译者注:读取log文件数据
  79.  //.. 程序代码
  80. }

  81.   
  82.   全局对象log在main()运行之前被构造,log调用了函数activate_log()。从
  83. 而,当main()开始执行时,它就可以从log文件中读取数据。


  84.   毫无疑问地,在C++编程中内存管理是最复杂和最容易出现bug的地方。直接访
  85. 问原始内存、动态分配存储和最大限度的发挥C++指令效率,都使你必须尽力避免

  86. 有关内存的bug。
  87.   
  88. 要点5:避免使用复杂构造的指向函数的指针

  89.   指向函数的指针是C++中可读性最差的语法之一。你能告诉我下面语句的意思
  90. 吗?  
  91.   
  92. void (*p[10]) (void (*)());  
  93.   P是一个“由10个指针构成的指向一个返回void类型且指向另一个无返回和无
  94. 运算的函数的数组”。这个麻烦的语法真是让人难以辨认,不是吗?你其实可以简
  95. 单的通过typedef来声明相当于上面语句的函数。首先,使用typedef声明“指向一
  96. 个无返回和无运算的函数的指针”:  
  97. typedef void (*pfv)();  
  98.   接着,声明“另一个指向无返回且使用pfv的函数指针”:  
  99. typedef void (*pf_taking_pfv) (pfv);  
  100.   现在,声明一个由10个上面这样的指针构成的数组:  
  101. pf_taking_pfv p[10];  
  102.   与void (*p[10]) (void (*)())达到同样效果。但这样是不是更具有可读性
  103. 了!  

  104. 要点6:指向成员的指针

  105.   一个类有两种基本的成员:函数成员和数据成员。同样的,指向成员的指针也
  106. 有两种:指向函数成员的指针和指向数据成员的指针。后则其实并不常用,因为类
  107. 一般是不含有公共数据成员的,仅当用在继承用C写的代码时协调结构(struct)和
  108. 类(class)时才会用到。

  109.   指向成员的指针是C++语法中最难以理解的构造之一,但是这也是一个C++最强
  110. 大的特性。它可以让你调用一个类的函数成员而不必知道这个函数的名字。这一个
  111. 非常敏捷的调用工具。同样的,你也可以通过使用指向数据成员的指针来检查并改
  112. 变这个数据而不必知道它的成员名字。

  113.   指向数据成员的指针

  114.   尽管刚开始时,指向成员的指针的语法会使你有一点点的迷惑,但你不久会发
  115. 现它其实同普通的指针差不多,只不过是*号的前面多了::符号和类的名字,例:
  116. 定义一个指向int型的指针:

  117.   
  118. int * pi;   
  119.   定义一个指向为int型的类的数据成员:  
  120. int A::*pmi; //pmi是指向类A的一个int型的成员  
  121.   你可以这样初始化它:  
  122. class A
  123. {
  124.  public:
  125.  int num;
  126.  int x;
  127. };
  128. int A::*pmi = & A::num;   
  129.   上面的代码是声明一个指向类A的一个int型的num成员并将它初始化为这个num
  130. 成员的地址.通过在pmi前面加上*你就可以使用和更改类A的num成员的值:  
  131. A a1, a2;
  132. int n=a1.*pmi; //把a1.num赋值给n
  133. a1.*pmi=5; // 把5赋值给a1.num  
  134. a2.*pmi=6; // 把6赋值给6a2.num  
  135.   
  136.   如果你定义了一个指向类A的指针,那么上面的操作你必须用 ->*操作符代
  137. 替:  
  138. A * pa=new A;
  139. int n=pa->*pmi;  
  140. pa->*pmi=5;   

  141.   指向函数成员的指针

  142.   它由函数成员所返回的数据类型构成,类名后跟上::符号、指针名和函数的参
  143. 数列表。举个例子:一个指向类A的函数成员(该函数返回int类型)的指针:
  144.   
  145. class A  
  146. {
  147.  public:
  148.  int func ();  
  149. };  
  150. int (A::*pmf) ();  
  151.   
  152.   上面的定义也就是说pmf是一个指向类A的函数成员func()的指针.实际上,这
  153. 个指针和一个普通的指向函数的指针没什么不同,只是它包含了类的名字和::符
  154. 号。你可以在在任何使用*pmf的地方调用这个函数  
  155. func():
  156. pmf=&A::func;
  157. A a;
  158. (a.*pmf)(); //调用a.func()  
  159.   如果你先定义了一个指向对象的指针,那么上面的操作要用->*代替:  
  160. A *pa=&a;
  161. (pa->*pmf)(); //调用pa->func()  
  162.   指向函数成员的指针要考虑多态性。所以,当你通过指针调用一个虚函数成员
  163. 时,这个调用将会被动态回收。另一个需要注意的地方,你不能取一个类的构造函
  164. 数和析构函数的地址。

  165. 要点7、避免产生内存碎片


  166.   经常会有这样的情况:你的应用程序每运行一次时就因为程序自身缺陷而产生
  167. 内存漏洞而泄漏内存,而你又在周期性地重复着你的程序,结果可想而知,它也会
  168. 使系统崩溃。但怎样做才能预防呢?首先,尽量少使用动态内存。在大多数情况
  169. 下,你可能使用静态或自动存储或者是STL容器。第二,尽量分配大块的内存而不
  170. 是一次只分配少量内存。举个例子:一次分配一个数组实例所需的内存,而不是一
  171. 次只分配一个数组元素的内存。

  172. 要点8、是delete还是delete[]

  173.   在程序员中有个荒诞的说法:使用delete来代替delete[]删除数组类型时是可
  174. 以的!
  175.   举个例子吧:
  176.   
  177.  int *p=new int[10];
  178.  delete p; //错误,应该是:delete[] p  
  179.   上面的程序是完全错误的。事实上,在一个平台上使用delete代替delete[]的
  180. 应用程序也许不会造成系统崩溃,但那纯粹是运气。你不能保证你的应用程序是不
  181. 是会在另一个编译器上编译,在另一个平台上运行,所以还是请使用delete[]。

  182. 要点9、优化成员的排列

  183.   一个类的大小可以被下面的方式改变:
  184.   
  185. struct A

  186. {
  187.  bool a;
  188.  int b;
  189.  bool c;
  190. }; //sizeof (A) == 12
  191.   
  192.   在我的电脑上sizeof (A) 等于12。这个结果可能会让你吃惊,因为A的成员总
  193. 数是6个字节:1+4+1个字节。那另6字节是哪儿来的?编译器在每个bool成员后面
  194. 都插入了3个填充字节以保证每个成员都是按4字节排列,以便分界。你可以减少A
  195. 的大小,通过以下方式:
  196.   
  197. struct B
  198. {
  199.  bool a;
  200.  bool c;
  201.  int b;
  202. }; // sizeof (B) == 8
  203.   
  204.   这一次,编译器只在成员c后插入了2个字节。因为b占了4个字节,所以就很自
  205. 然地把它当作一个字的形式排列,而a和c的大小1+1=2,再加上2个字节就刚好按两
  206. 个字的形式排列B。   

  207. 要点10、为什么继承一个没有虚析构函数的类是危险的?

  208.   一个没有虚析构函数的类意味着不能做为一个基类。如std::string,  
  209. std::complex, 和 std::vector 都是这样的。为什么继承一个没有虚析构函数的
  210. 类是危险的?当你公有继承创建一个从基类继承的相关类时,指向新类对象中的指
  211. 针和引用实际上都指向了起源的对象。因为析构函数不是虚函数,所以当你delete
  212. 一个这样的类时,C++就不会调用析构函数链。举个例子说明:
  213.   
  214. class A
  215. {
  216.  public:
  217.  ~A() // 不是虚函数
  218.  {
  219.  // ...
  220.  }
  221. };  
  222. class B: public A //错; A没有虚析构函数
  223. {
  224.  public:
  225.  ~B()
  226.  {
  227.  // ...
  228.  }
  229. };

  230. int main()
  231. {
  232.  A * p = new B; //看上去是对的
  233.  delete p; //错,B的析构函没有被调用
  234. }


  235.   

  236. 要点11、以友元类声明嵌套的类

  237.   当你以友元类声明一个嵌套的类时,把友元声明放在嵌套类声明的后面,而不
  238. 前面。
  239.   
  240. class A  
  241. {
  242.  private:
  243.  int i;
  244.  public:
  245.  class B //嵌套类声明在前
  246.  {
  247.   public:
  248.   B(A & a) { a.i=0;};  
  249.  };
  250.  friend class B;//友元类声明
  251. };
  252.   
  253.   如果你把友元类声明放在声明嵌套类的前面,编译器将抛弃友元类后的其它声
  254. 明。  
阅读(584) | 评论(0) | 转发(0) |
给主人留下些什么吧!~~