Effective C++笔记: 让自己习惯C++

 

Item 1: C++ 视为 federation of languages(语言联邦)

不要将 C++ 视为一个由相关语言组成的联邦,而非单一的语言。在每一个特定的 sublanguage(子语言)中,它的特性趋向于直截了当,简单易记。但你从一个 sublanguage(子语言)转到另外一个,它的规则也许会发生变化。为了感受 C++,你必须将它的主要的 sublanguages(子语言)组织到一起。幸运的是,它只有 4 个:

·    C ——归根结底,C++ 依然是基于 C 的。blocks(模块),statements(语句),preprocessor(预处理器),built-in data types(内建数据类型),arrays(数组),pointers(指针)等等,全都来自于 C。在很多方面。C++ 提出了比相应的 C 版本更高级的解决问题的方法(例如,参见 Item 2(选择 preprocessor(预处理器))和 13(使用 objects(对象)管理 resources(资源))),但是,当你发现你自己工作在 C++ C 部分时,effective programming(高效编程)的规则表现了 C 的诸多限制范围:没有 templates(模板),没有 exceptions(异常),没有 overloading(重载)等等。

·    Object-Oriented C++ —— C++ 的这部分就是 C with Classes 涉及到的全部:classes(类)(包括构造函数和析构函数),encapsulation(封装),inheritance(继承),polymorphism(多态),virtual functions (dynamic binding)(虚拟函数(动态绑定))等。C++ 的这一部分直接适用于 object-oriented design(面向对象设计)的经典规则。

·    Template C++ ——这是 C++ generic programming(泛型编程)部分,大多数程序员对此都缺乏经验。template(模板)的考虑已遍及 C++,而且好的编程规则中包含特殊的 template-only(模板专用)条款已不再不同寻常(参见 Item 46 通过调用 template functions(模板函数)简化 type conversions(类型转换))。实际上,templates(模板)极为强大,它提供了一种全新的 programming paradigm(编程范式)—— template metaprogramming (TMP) (模板元编程)。Item 48 提供了一个 TMP 的概述,但是,除非你是一个 hard-core template junkie(死心塌地的模板瘾君子),否则你不需在此费心,TMP 的规则对主流的 C++ 编程少有影响。

·    STL —— STL 是一个 template library(模板库),但它一个非常特殊的 template library(模板库)。它将 containers(容器),iterators(迭代器),algorithms(算法)和 function objects(函数对象)非常优雅地整合在一起,但是。templates(模板)和 libraries(库)也可以围绕其它的想法建立起来。STL 有很多独特的处事方法,当你和 STL 一起工作,你需要遵循它的规则。

当你从一种 sublanguage(子语言)转到另一种时,为了高效编程你需要改变你的策略!!

 

Item 2: 尽量用consts, enums inlines 取代 #defines

·    对于 simple constants(简单常量),用 const objectsconst 对象)或 enums(枚举)取代 #defines

·    对于 function-like macros(类似函数的宏),用 inline functions(内联函数)取代 #defines

以下是一个用较大的宏参数调用函数 f macro(宏):

// call f with the maximum of a and b

#define CALL_WITH_MAX(a, b) f((a) > (b) ? (a) : (b))

你可以通过一个 inline function(内联函数)的 template(模板)来获得 macro(宏)的效率,以及完全可预测的行为和常规函数的类型安全(参见 Item 30):

template<typename T>                               // because we don't

inline void callWithMax(const T& a, const T& b)    // know what T is, we

{                                  // pass by reference-to-

  f(a > b ? a : b);                // const - see Item 20

}

Item 3: 尽可能使用 const

1.用于修饰指针

对于 pointers(指针),你可以指定这个 pointer(指针)本身是 const,或者它所指向的数据是 const,或者两者都是,或者都不是:

char greeting[] = "Hello";

char *p = greeting;                    // non-const pointer,
                                       // non-const data

const char *p = greeting;              // non-const pointer,
                                       // const data

char * const p = greeting;             // const pointer,
                                       // non-const data

const char * const p = greeting;       // const pointer,
                                       // const data

STL iterators(迭代器)以 pointers(指针)为原型,所以一个 iterator 在行为上非常类似于一个 T* pointer(指针)。声明一个 iterator const 就类似于声明一个 pointer(指针)为 const(也就是说,声明一个 T* const pointer(指针)):不能将这个 iterator 指向另外一件不同的东西,但是它所指向的东西本身可以变化。如果你要一个 iterator 指向一个不能变化的东西(也就是一个 const T* pointer(指针)的 STL 对等物),你需要一个 const_iterator

std::vector<int> vec;
...
const std::vector<int>::iterator iter = // iter acts like a T* const
  vec.begin();
*iter = 10;                // OK, changes what iter points to
++iter;                    // error! iter is const

 

std::vector<int>::const_iterator cIter =  // cIter acts like a const T*
  vec.begin();
*cIter = 10;                                // error! *cIter is const
++cIter;                                    // fine, changes cIter

 

2. 用于修饰函数:在一个 function declaration(函数声明)中,const 既可以用在函数的 return value(返回值)上,也可以用在个别的 parameters(参数)上,对于 member functions(成员函数),还可以用于整个函数。

member functions(成员函数)被声明为 const 的目的是标明这个 member functions(成员函数)可能会被 const objects(对象)调用。因为两个原因,这样的 member functions(成员函数)非常重要。首先,它使一个 class(类)的 interface(接口)更容易被理解。知道哪个函数可以改变 object(对象)而哪个不可以是很重要的。第二,它们可以和 const objects(对象)一起工作。因为,书写高效代码有一个很重要的方面,就像 Item 20 所解释的,提升一个 C++ 程序的性能的基本方法就是 pass objects by reference-to-const(以传引用给 const 的方式传递一个对象)。这个技术只有在 const member functions(成员函数)和作为操作结果的 const-qualified objects(被 const 修饰的对象)存在时才是可行的。

两个成员函数如果只是常量性不同,可以被重载。

class TextBlock {
public:
  ...
  const char&
operator[](std::size_t position) const   // operator[] for
  { return text[position]; }                           // const objects

  char& operator[](std::size_t position)           // operator[] for
  { return text[position]; }                        // non-const objects

private:
   std::string text;
};

TextBlock operator[]s 可能会这样使用:

TextBlock tb("Hello");
std::cout << tb[0];                    // calls non-const
                                       // TextBlock::operator[]
const TextBlock ctb("World");
std::cout << ctb[0];                   // calls const TextBlock::operator[]

 

3.bitwise constness logical constness

bitwise constness:  不改变对象的每一个bit。(问题在于如果你的const成员函数返回一个指向class内部数据成员的句柄,则其他代码可以通过该句柄在外部修改该数据

Logical constness 一个 const member function(成员函数)可能会改变调用它的 object(对象)中的一些 bits(二进制位),但是只能用客户无法察觉的方法。(使用mutable来修饰可在const成员函数内被修改的变量

 

4.constnon-const成员函数中避免重复

假设 TextBlock(包括 CTextBlock)中的 operator[] 不仅要返回一个适当的字符的 reference(引用),它还要进行 bounds checking(边界检查),logged access information(记录访问信息),甚至 data integrity validation(数据完整性确认),将这些功能都加入到 const non-const operator[] 函数中,使它们变成如下这样的庞然大物:

class TextBlock {
public:

  ...

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

  char& operator[](std::size_t position)
  {
    ...                                 // do bounds checking
    ...                                 // log access data
    ...                                 // verify data integrity
    return text[position];
  }

private:
   std::string text;
};

重复代码带来更长的编译时间,维护,代码膨胀等问题。解决办法一是 bounds checking(边界检查)等全部代码转移到一个单独的 member function(成员函数)(自然是 private(私有)的)中,并让两个版本的 operator[] 来调用它,但是,你还是要重复写出调用那个函数和 return 语句的代码。

最简单的办法是只实现一次 operator[] 的功能,而使用两次。换句话说,你可以用一个版本的 operator[] 去调用另一个版本。并可以为我们 casting away(通过强制转型脱掉)constness(常量性)。

作为一个通用规则,casting(强制转型)是一个非常坏的主意,我会投入整个一个 Item 的篇幅来告诉你不要使用它(Item 27),但是 code duplication(重复代码)也不是什么好事。在当前情况下,const 版本的 operator[] 所做的事也正是 non-const 版本所做的,仅有的不同是它有一个 const-qualified return type(被 const 修饰的返回类型)。在这种情况下,casting away(通过强制转型脱掉)return value(返回类型)的 const 是安全的,因为,无论谁调用 non-const operator[],首先要有一个 non-const object(对象)。否则,它不能调用一个 non-const 函数。所以,即使需要一个 cast(强制转型),让 non-const operator[] 调用 const 版本也是避免重复代码的安全方法。代码如下,你读了后面的解释后对它的理解可能会更加清晰:

class TextBlock {
public:

  ...

  const char& operator[](std::size_t position) const  // same as before
  {
    ...
    ...
    ...
    return text[position];
  }

  char& operator[](std::size_t position)    // now just calls const op[]
  {
    return
     
const_cast<char&>(                         // cast away const on
                                                 // op[]'s return type;
    
static_cast<const TextBlock&>(*this)  // add const to *this's type;
          [position]                     // call const version of op[]
      );
  }

...

};

正如你看到的,代码中有两处 casts(强制转型),而不是一处。我们让 non-const operator[] 调用 const 版本,但是,如果在 non-const operator[] 的内部,我们仅仅是调用 operator[],那我们将递归调用我们自己。它会进行一百万次甚至更多。为了避免 infinite recursion(无限递归),我们必须明确指出我们要调用 const operator[],但是没有直接的办法能做到这一点,于是我们将 *this TextBlock& 的自然类型强制转型到 const TextBlock&。是的,我们使用 cast(强制转型)为它加上了 const!所以我们有两次 casts(强制转型):第一次是为 *this 加上 const(以便在我们调用 operator[] 时调用它的 const 版本),第二次是从 const operator[] return value(返回值)之中去掉 const

注意,去掉 const 只能经由 const_cast 来完成,所以在这里我们没有别的选择。(在技术上,我们有。一个 C-style castC 风格的强制转型)也能工作,但是,就像我在 Item 27 中解释的,这样的 casts(强制转型)很少是一个正确的选择。

另外,反向做法——通过用 const 版本调用 non-const 版本来避免重复——是你不能做的。记住,一个 const member function(成员函数)承诺绝不会改变它的 object(对象)的逻辑状态,但是一个 non-const member function(成员函数)不会做这样的承诺。如果你从一个 const member function(成员函数)调用一个 non-const member function(成员函数),你将面临你承诺不会变化的 object(对象)被改变的风险。

 

总结:

将某些东西声明为 const 有助于编译器发现使用错误。const 能被用于任何作用域中的 object(对象),用于 function parameters(函数参数)和 return types(返回类型),用于整个 member functions(成员函数)。

编译器坚持 bitwise constness(二进制位常量性),但是你应该用 conceptual constness(概念上的常量性)来编程。

const non-const member functions(成员函数)具有本质上相同的实现的时候,使用 non-const 版本调用 const 版本可以避免 code duplication(代码重复)。

 

Item 4: 确保 objects(对象)在使用前被初始化

总是在使用之前初始化你的对象。

对于 built-in types(内建类型)的 non-member objects(非成员对象),需要你手动做这件事。

对于自定义对象,确保 all constructors(所有的构造函数)都初始化了 object(对象)中的每一样东西。

这个规则很容易遵守,但重要的是不要把 assignment(赋值)和 initialization(初始化)搞混。考虑下面这个表现一个通讯录条目的 class(类)的 constructor(构造函数):

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;

};

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 objects(对象)具有了你所期待的值,但还不是最好的做法。C++ 的规则规定一个 object(对象)的 data members(数据成员)在进入 constructor(构造函数)的函数体之前被初始化。在 ABEntry constructor(构造函数)内,theNametheAddress thePhones 不是 being initialized(被初始化),而是 being assigned(被赋值)。initialization(初始化)发生得更早——在进入 ABEntry constructor(构造函数)的函数体之前,它们的 default constructors(缺省的构造函数)已经被自动调用。但不包括 numTimesConsulted,因为它是一个 built-in type(内建类型)。不能保证它在被赋值之前被初始化。

一个更好的写 ABEntry constructor(构造函数)的方法是用 member initialization list(成员初始化列表)来代替 assignments(赋值):

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)

{}                           // the ctor body is now empty

这个 constructor(构造函数)的最终结果和前面那个相同,但是通常它有更高的效率。assignment-based(基于赋值)的版本会首先调用 default constructors(缺省构造函数)初始化 theNametheAddress thePhones,然而很快又在 default-constructed(缺省构造)的值之上赋予新值。那些 default constructions(缺省构造函数)所做的工作被浪费了。而 member initialization list(成员初始化列表)的方法避免了这个问题,因为 initialization list(初始化列表)中的 arguments(参数)就可以作为各种 data members(数据成员)的 constructor(构造函数)所使用的 arguments(参数)。在这种情况下,theName name copy-constructed(拷贝构造),theAddress address copy-constructed(拷贝构造),thePhones phones copy-constructed(拷贝构造)。对于大多数类型来说,只调用一次 copy constructor(拷贝构造函数)的效率比先调用一次 default constructor(缺省构造函数)再调用一次 copy assignment operator(拷贝赋值运算符)的效率要高(有时会高很多)。

 

non-local static 的初始化顺序问题:

local static对象替换non-local static 对象。

FileSystem& tfs()          // this replaces the tfs object; it could be
{                          // static in the FileSystem class
 
static FileSystem fs;     // define and initialize a local static object
 
return fs;               // return a reference to it
}

 

避免在初始化之前使用 objects(对象),你只需要做三件事。首先,手动初始化 built-in types(内建类型)的 non-member objects(非成员对象)。第二,使用 member initialization lists(成员初始化列表)初始化一个 object(对象)的所有部分。最后,为免除“跨编译单元之初始化次序”问题,以local static对象来代替non-local static对象。

 

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值