More Effective C++ 笔记(一)

来源:互联网 发布:sql注入直接写入一句话 编辑:程序博客网 时间:2024/04/30 14:11

More Effective C++ 读书笔记

条款1 指针与引用的区别
1、使用指针前总是要判断它是否为NULL。
2、当可能不指向任何对象或需要在不同时刻指向不同对象时应该用指针,如果总是指向一个对象且一旦指向后就不会改变就应该用引用。
3、重载某些操作符时必须用引用,如:[]。


条款2 尽量使用C++风格的类型转换
1、在大多数情况下,对于这些操作符你只需要知道原来你习惯于这样写,(type)expression而现在你总应该这样写:static_cast<type>(expression)。
2、onst_cast最普通的用途就是转换掉对象的const属性。
3、dynamic_casts不能被用于缺乏虚函数的类型上。


条款3 不要使用多态性数组
1、通过一个基类指针来删除一个含有派生类对象的数组,结果将是不确定的。
条款中的代码示例:
  class BST { ... };
  class BalancedBST: public BST { ... };
  我们假设BST和BalancedBST只包含int类型数据。
  有这样一个函数,它能打印出BST类数组中每一个BST对象的内容:
  void printBSTArray(ostream& s,
  const BST array[],
  int numElements)
  {
   for (int i = 0; i < numElements; ) {
    s << array[i]; //假设BST类
   } //重载了操作符<<
  }
  当你传递给该函数一个含有BST对象的数组变量时,它能够正常运行:
  BST BSTArray[10];
  ...
  printBSTArray(cout, BSTArray, 10); // 运行正常
  然而,请考虑一下,当你把含有BalancedBST对象的数组变量传递给printBSTArray函数时,会产生什么样的后果:
  BalancedBST bBSTArray[10];
  ...
  printBSTArray(cout, bBSTArray, 10); // 还会运行正常么?
  你的编译器将会毫无警告地编译这个函数,但是再看一下这个函数的循环代码:
  for (int i = 0; i < numElements; ) {
   s << array[i];
  }
  这里的array[I]只是一个指针算法的缩写:它所代表的是*(array)。我们知道array是一个指向数组起始地址的指针,但是array中各元素内存地址与数组的起始地址的间隔究竟有多大呢?它们的间隔是i*sizeof(一个在数组里的对象),因为在array数组[0]到[I]间有I个对象。编译器为了建立正确遍历数组的执行代码,它必须能够确定数组中对象的大小,这对编译器来说是很容易做到的。参数array被声明为BST类型,所以array数组中每一个元素都是BST类型,因此每个元素与数组起始地址的间隔是be i*sizeof(BST)。

感觉这里的描述有点问题,多态是需要对象指针或引用才会产生的效果,而上面示例中描述的是一个对象数组,而非
指针数组,这样的数组与多态并没有关系,那么标题中的多态性数组又从何而来?

上述问题的解决方法是:将对象数组改为对象指针数组,由于不论是基类还是派生类,其指针都是4个字节,故不会产生上述问题。


条款4 避免无用的默认构造函数
1、有些对象不借助外部信息来初始化就没有意义。
2、没有默认构造函数的缺陷:
  1、不能建立对象数组。
  2、某些模板容器需要类的默认构造函数。
  3、没有默认构造函数的虚基类不方便使用。
3、有默认构造函数的缺陷:
  1、成员函数变得更加复杂且低效,容易出错。因为成员函数需要判断变量是否已经正确初始化。
  
4、对于没有默认构造函数的类创建对象数组的解决方案:
请考虑一下有这样一个类,它表示公司的设备,这个类包含一个公司的ID代码,这个ID代码被强制做为构造函数的参数:

  class EquipmentPiece {
  public:
   EquipmentPiece(int IDNumber);
   ...
  };

  因为EquipmentPiece类没有一个缺省构造函数,所以在三种情况下使用它,就会遇到问题。第一中情况是建立数组时。一般来说,没有一种办法能在建立对象数组时给构造函数传递参数。所以在通常情况下,不可能建立EquipmentPiece对象数组:

  EquipmentPiece bestPieces[10]; // 错误!没有正确调
                // EquipmentPiece 构造函数

  EquipmentPiece *bestPieces =
  new EquipmentPiece[10]; // 错误!与上面的问题一样

  不过还是有三种方法能回避开这个限制。对于使用非堆数组(non-heap arrays)(即不在堆中给数组分配内存。译者注)的一种解决方法是在数组定义时提供必要的参数:

  int ID1, ID2, ID3, ..., ID10; // 存储设备ID号的
                // 变量
  ...

  EquipmentPiece bestPieces[] = { // 正确, 提供了构造
  EquipmentPiece(ID1), // 函数的参数
  EquipmentPiece(ID2),
  EquipmentPiece(ID3),
  ...,
  EquipmentPiece(ID10),
  };

  不过很遗憾,这种方法不能用在堆数组(heap arrays)的定义上。

  一个更通用的解决方法是利用指针数组来代替一个对象数组:

  typedef EquipmentPiece* PEP; // PEP 指针指向
               //一个EquipmentPiece对象

  PEP bestPieces[10]; // 正确, 没有调用构造函数
  PEP *bestPieces = new PEP[10]; // 也正确

  在指针数组里的每一个指针被重新赋值,以指向一个不同的EquipmentPiece对象:

  for (int i = 0; i < 10; ++i)
  bestPieces[i] = new EquipmentPiece( ID Number );

  不过这中方法有两个缺点,第一你必须删除数组里每个指针所指向的对象。如果你忘了,就会发生内存泄漏。第二增加了内存分配量,因为正如你需要空间来容纳EquipmentPiece对象一样,你也需要空间来容纳指针。

  如果你为数组分配raw memory,你就可以避免浪费内存。使用placement new方法(参见条款8)在内存中构造EquipmentPiece对象:

  // 为大小为10的数组 分配足够的内存
  // EquipmentPiece 对象; 详细情况请参见条款8
  // operator new[] 函数
  void *rawMemory =
  operator new[](10*sizeof(EquipmentPiece));
  // make bestPieces point to it so it can be treated as an
  // EquipmentPiece array
  EquipmentPiece *bestPieces =
  static_cast<EquipmentPiece*>(rawMemory);
  // construct the EquipmentPiece objects in the memory
  // 使用"placement new" (参见条款8)
  for (int i = 0; i < 10; ++i)
  new (&bestPieces[i]) EquipmentPiece( ID Number );

  注意你仍旧得为每一个EquipmentPiece对象提供构造函数参数。这个技术(也称为数组到指针的思想array-of-pointers)允许你在没有缺省构造函数的情况下建立一个对象数组。它没有绕过对构造函数参数的需求,实际上也做不到。如果能做到的话,就不能保证对象被正确初始化。
使用placement new的缺点除了是大多数程序员对它不熟悉外(能使用它就更难了),还有就是当你不想让它继续存在使用时,必须手动调用数组对象的析构函数,调用操作符delete[]来释放 raw memory(请再参见条款8):

  // 以与构造bestPieces对象相反的顺序
  // 解构它。
  for (int i = 9; i >= 0; --i)
  bestPieces[i].~EquipmentPiece();

  // deallocate the raw memory
  operator delete[](rawMemory);

  如果你忘记了这个要求或没有用这个数组删除方法,那么你程序的运行将是不可预测的。这是因为直接删除一个不是用new操作符来分配的内存指针,其结果没有被定义的。

  delete [] bestPieces; // 没有定义! bestPieces
            //不是用new操作符分配的。


条款5 谨慎定义类型转换函数
1、有两种函数允许编译器进行这些的转换:单参数构造函数(single-argument constructors)和隐式类型转换运算符。
2、隐式转换的问题可以用形如:AsString、AsDouble这样的成员函数来代替,以避免二义性以及其他问题。
3、单参数构造函数的问题可以用explicit关键字、或用一个proxy classes来解决


条款6 自增(increment)、自减(decrement)操作符前缀形式与后缀形式的区别
1、使用前缀形式可以提高效率
2、后缀形式用前缀形式来实现


条款7 不要重载&&, ||, or ,
1、重载后的&&和||不再具有短路逻辑,不符合我们的变成习惯。
2、无法模仿“,”运算符的求职顺序。
3、不能重载的运算符:
  . .* :: ?:
    new delete sizeof typeid
  static_cast dynamic_cast const_cast reinterpret_cast
可以重载的运算符:
operator new operator delete
  operator new[] operator delete[]
  + - * / % ^ & | ~
  ! = < > += -= *= /= %=
  ^= &= |= << >> >>= <<= = = !=
  <= >= && || ++ -- , ->* ->
  () []


条款8 理解各种不同含义的new和delete
1、new操作符完成两个工作:
  1、分配足够的内存以便容纳所需类型的对象。
  2、它调用构造函数初始化内存中的对象。new操作符总是做这两件事情,你不能以任何方式改变它的行为。
2、new操作符为分配内存所调用函数的名字是operator new
operator new 的声明:
void * operator new(size_t size);
placement new 的声明:
void * operator new(size_t, void *location)
{
  return location;
}

3、用placement new定义的对象需要显示调用析构函数后再用operator delete释放内存


条款9 使用析构函数防止资源泄漏
用智能指针管理资源

 

 

条款10 在构造函数中防止资源泄漏
用智能指针代替指针可构建异常安全的系统

 

 

条款11 禁止异常信息(exceptions)传递到析构函数外
不要在析构函数中抛出异常

 

原创粉丝点击