现在的位置: 首页 > 综合 > 正文

C++ effective

2013年10月19日 ⁄ 综合 ⁄ 共 5642字 ⁄ 字号 评论关闭

第一章 从C转向C++

我应该算是从C到C++的典型了,虽然我不是什么老人,可是从我接触编程 至今,区区6、7年间,我竟是用了5年多的C,而且一直是在TC 2.0的环境下。当我决心放弃TC的时候(2006年上半年),距C++之父Bjarne Stroustrup所著的《The C++ Programming Language》问世已经过了21年了,距Scott Meyers的《Effective C++》第一版也已16年之久,想来不禁为自己的后知后觉感到惭愧。

条款01:视C++为一个语言联邦

凭个人体会,不同背景的人会将C++看作不同的语言,正应了“横看成岭侧成峰,远近高低各不同”。

C++时代,对于C程序员最大的幸事和不幸都是基于一点:C++ >= C,我可以抱残守缺,认为C就是C++。

当然,大多数C程序员并不会这么做,起码,我不会一致这么做。我首先会积极地用class替代struct,然后试着自定义constructors和destructors,用protected和virtual……

这样大概过了半年,我就从一个Structrue-Oriented coder进化成了一个Object-Oriented coder。

现在,为了工作的方便,在没有经过任何理论学习的情况下,我开始使用STL,却没有去深入探寻STL源代码。当然,我知道这是不够的,也是不好的,等我把《Effective C++》看得更好一些,我就去看Template和STL。

条款02:尽量以const、enum、inline替换#define

尽量用编译器而不用预处理,作者这样说的原因是因预处理造成的bugs如果在编译时暴露,那将是非常难揪的,尤其是带参数的宏:

#define max(a,b) ((a) > (b) ? (a) : (b))

int a = 5, b = 0;
max(
++a, b);          // a 的值增加了2次
max(++a, b+10);   // a 的值只增加了1次

原因就在于上述语句在预处理时被处理成:

((++a) > (b) ? (++a) : (b));
((
++a) > (b+10? (++a) : (b));

危险啊!
 
即使我可以凭借丰富的经验避免上述错误,但违反OO原则的帽子我是不愿戴的,所以我更愿意用inline函数来代替带参的宏。

  即使我只使用无参数的宏,也不如多考虑用const或enum。在大多数情况下,用const(如果想保证一个类只有一个对应常量,就用static const)来声明常量显然比#define来得高明,此时#define的优势在于其地址不能被获取,而const并不具备这一功能,考虑一下enum 吧,OK,问题迎刃而解。

第二版条款02:尽量用<iostream>而不用<stdio.h>

在C中,scanf和printf的最大问题是其类型安全的不确定。经常遇到有朋友提出类似的困惑:为什么scanf的数据一团糟,为什么printf的结果和预期的不一致?另一个问题是scanf和printf对于复杂的结构不能扩展使用。

至 于使用<iostream>的理由,听听Scott怎么说吧:“iostream库的类和函数所提供的类型安全和可扩展性的价值远远超过你当 初的想象,所以不要仅仅因为你用惯了<stdio.h>而舍弃它。毕竟,转换到iostream后,你也不会忘掉< stdio.h>。”

条款03:尽可能使用const

尽可能使用const可能会让你多写很多个const,而且会在你试图改变被const修饰的“变量”时报告一大堆bugs。然而,如果没有了它们,就成了掩耳盗铃了吧?后果是不是不堪设想呢?很简单,你只需要看看const修饰的是谁,就知道谁不能被改变了。

对于返回const的函数,它是为了防止你或者别人对返回的对象进行写操作(赋值)。

bitwise const成员函数保证了在函数内部任何成员(static除外)都不会被改变,logical const则会给需要改变的成员变量在声明时加上mutable关键字。

第二版条款03:尽量用new和delete而不用malloc和free

在C中,我们谨记malloc和free是我们在使用指针时的忠实朋友,在C++中,我们需要为对象、为含有对象的对象进行内存分配的时候,malloc和free就帮不了我们太多了,因为它们不知道还有构造和析构这样的事情。

为了能够把const与non-const严格区别对待,你需要重载函数,这就意味着代码重复。或许你也想到让non-const去调用const或者反过来(有点危险和莫名其妙,我们不是为了const而来的吗?)。

class TextBlock {
public:
  ...
  
const char& operator[](std::size_t position) const     // same as before
  {
    ...                                                  
// do bounds checking
    ...                                                  // log access data
    ...                                                  // verify data integrity
    return text[position];
  }


  
char& operator[](std::size_t position)                 // now just calls const op[]
  {
    
return
      const_cast
<char&>(                                          // cast away const on         这儿很好理解
        static_cast<const TextBlock&>(*this)           // add const to *this's type; 避免递归调用自身
          [position]                                                           // call const version of op[]
      );
  }

...
}
;

条款04:确定对象在使用前已先被初始化

关于语言本身对初始化的保证,从C到C++一直是个讨论的热点,Scott说:“always initialize your objects before you use them”。

class PhoneNumber { ... };

class ABEntry {                       // ABEntry = "Address Book Entry"
public:
  ABEntry(
const std::string& name, const std::string& address,
          
const std::list<PhoneNumber>& phones);
private:
  std::
string theName;
  std::
string theAddress;
  std::list
<PhoneNumber> thePhones;
  
int num TimesConsulted;
}
;

// 赋值做法,相信每一个C程序员都对这样的写法倍感亲切
ABEntry::ABEntry(const std::string& name, const std::string& address,
                 
const std::list<PhoneNumber>& phones)
{
  theName 
= name;                         // these are all assignments,
  theAddress = address;                 // not initializations
  thePhones = phones
  numTimesConsulted 
= 0;
}


// 成员初始列做法,这才是初始化
ABEntry::ABEntry(const std::string& name, const std::string& address,
                 
const std::list<PhoneNumber>& phones)
: theName(name),
  theAddress(address),                  
// these are now all initializations
  thePhones(phones),
  numTimesConsulted(
0)              // 这个built-in type无所谓,不存在效率上的问题
{}                                                         // the ctor body is now empty

不 仅仅是习惯问题,先调用default constructor再进行copy assignment显然比直接调用copy constructor要慢半拍(岂止啊)!对于const或references因为无法赋值,只能使用初始列。而且,初始化顺序也应该遵循先base 后derived,FCFS的原则。

至于例外也不是没有,为了避免涉及数据读写的大量变量在赋值前多余的初始化(前提是你正确的知道那是多余的),合理改用赋值是明智的。

“The relative order of initialization of non-local static objects defined in different translation units is undefined.”对于different translation units中定义的non-local static objects们,我们不可能也没有必要在每次使用时都对其初始化,然而我们又的确需要初始化已经实施过这样的保证,GoF提出的Singleton模 式、解决了这个问题。

说句实话,我很少使用non-local static objects,但是我也很少使用local static objects。虽然我对《Design Pattern》看过多遍,但你也体会到了,那又能说明什么呢?

第二版条款04:尽量使用c++风格的注释

为了强调要从C转向C++,Scott连C的注释风格也一起批判:

if ( a > b ) {
 
// int temp = a; // swap a and b
 
// a = b;
 
// b = temp;
}

==>

if ( a > b ) {
 
/* int temp = a;  /* swap a and b */
  a 
= b;
  b 
= temp;
 
*/
}

啊喔,因为注释不能嵌套,上面的修改导致了编译时无法通过。

看来,从C到C++,必须要转变的彻底才行。

第二章 内存管理

在写程序的时候,尤其是写大程序的时候,尤其尤其是几十个类、几百个类联合出动的时候,我们经常能从编译器得到内存泄露的反馈消息。其实,我不怕程序编译通不过,充其量这不过是个错而已,就怕内存莫名其妙的泄露,那是罪啊!

第二版条款05:对应的new和delete要采用相同的形式

new 和delete不仅仅是要成对出现的问题(当然,这是最根本最重要的问题),而更是要正确的成对出现,就像{}一样。Scott说:“如果你调用new时 用了[],调用delete时也要用[]。如果调用new时没有用[],那调用delete时也不要用[]。”如果问题仅仅这么简单,那么第二章至此应该 结束了,至少本条款应该结束了。

typedef string addresslines[4]; 

string *pal = new addresslines;  // 这个typedef使用的太有创意了!
delete pal;                                        // 错误!
delete [] pal;                                     // 正确

 

第二版条款06:析构函数里对指针成员调用delete

对指针的使用,很多人在C时代就是诚惶诚恐,到了C++时代更是临深履薄。上面我说过,对于C++程序员,内存泄露犹如犯罪,而错误的使用指针则是C++程序员最大的罪名。

Scott给了三条原则:

·在每个构造函数里对指针进行初始化。对于一些构造函数,如果没有内存要分配给指针的话,指针要被初始化为0(即空指针)。

·删除现有的内存,通过赋值操作符分配给指针新的内存。

·在析构函数里删除指针。

如 果你用了兄弟什么东西,忘了还,兄弟不怪你;如果你用了内存,千万要还,千万要还啊!哪怕是一个空指针,你最好也把它删除,因为删除空指针是安全的。还记 得当年汪精卫说过什么吗?“宁可错杀三千,不可使一人漏网”,如果你自认为不是一个英明的神(不仅能照顾自己,还能救人水火的神),就学学汪精卫的这种精 神吧!

当然,为了和汪精卫有区别,你还要记住最基本的一点:new和delete要成对出现,如果你没有new过,那你就不要delete,过于残暴的delete所有指针毕竟不能修成正果。

抱歉!评论已关闭.