Effective C++摘要《第3章:构造函数、析构函数、赋值操作符》20090207

来源:互联网 发布:淘宝能在手机上开店吗 编辑:程序博客网 时间:2024/04/29 14:36

===条款11: 为需要动态分配内存的类声明一个拷贝构造函数和一个赋值操作符===
对于有些类,当实现拷贝构造函数和赋值操作符非常麻烦的时候,特别是可以确信程序中不会做拷贝和赋值操作的时候,去实现它们就会相对来说有点得不偿失。
这种情况下,很简单,照本条款的建议去做:可以只声明这些函数(声明为private成员)而不去定义(实现)它们
这就防止了会有人去调用它们,也防止了编译器去生成它们
===条款12: 尽量使用初始化而不要在构造函数里赋值===
1、const和引用数据成员只能用初始化,不能被赋值。
2、用成员初始化列表还是比在构造函数里赋值要好,原因在于效率。
对象的创建分两步:
  a. 数据成员初始化。
  b. 执行被调用构造函数体内的动作。
对有基类的对象来说,基类的成员初始化和构造函数体的执行发生在派生类的成员初始化和构造函数体的执行之前
template<class t>
class namedptr {
public:
  namedptr(const string& initname, t *initptr);
  ...

private:
  string name;
  t *ptr;
};
namedptr::namedptr(const string& initname, t *initptr)
{
    name = initname;
}
没有初始化列表,在数据成员初始化时name会调用string缺省构造函数,当在namedptr的构造函数里对name执行赋值时,会对name调用operator=函数。
这样总共有两次对string的成员函数的调用:一次是缺省构造函数,另一次是赋值。
namedptr::namedptr(const string& initname, t *initptr):name(initname)
{
}
相反,如果用一个成员初始化列表来指定name必须用initname来初始化,name就会通过拷贝构造函数以仅一个函数调用的代价被初始化。
3、但有一种情况下,对类的数据成员用赋值比用初始化更合理。这就是当有大量的固定类型的数据成员要在每个构造函数里以相同的方式初始化的时候。
class manydatambrs {
public:
  // 缺省构造函数
  manydatambrs();
  // 拷贝构造函数
  manydatambrs(const manydatambrs& x);

private:
  int a, b, c, d, e, f, g, h;
  double i, j, k, l, m;
 
  void init();        // 用于初始化数据成员
};
这种情况下,你可以利用固定数据类型的(非const, 非引用)对象其初始化和赋值没有操作上的不同的特点,安全地将成员初始化列表用一个对普通的初始化函数的调用来代替。
void manydatambrs::init()
{
  a = b = c = d = e = f = g = h = 1;
  i = j = k = l = m = 0;
}
manydatambrs::manydatambrs()
{
  init();
  ...
}
4、注意static类成员永远也不会在类的构造函数初始化
===条款13: 初始化列表中成员列出的顺序和它们在类中声明的顺序相同===
template<class t>
class array {
public:
  array(int lowbound, int highbound);
private:
  vector<t> data;               // 数组数据存储在vector对象中
                                // 关于vector模板参见条款49
  size_t size;                  // 数组中元素的数量
  int lbound, hbound;           // 下限,上限
};

template<class t>
array<t>::array(int lowbound, int highbound)
: size(highbound - lowbound + 1),
  lbound(lowbound), hbound(highbound),
  data(size)
{}
规则:类成员是按照它们在类里被声明的顺序进行初始化的,和它们在成员初始化列表中列出的顺序没一点关系。
用上面的array模板生成的类里,data总会被首先初始化,然后是size, lbound和hbound。
由于data在size初始化之前,因此data并没有被正确初始化!!
===条款14: 确定基类有虚析构函数===
1、c++语言标准关于这个问题的阐述非常清楚:当通过基类的指针去删除派生类的对象,而基类又没有虚析构函数时,结果将是不可确定的。
class enemytank: public enemytarget
{};
enemytarget *targetptr = new enemytank;
...
delete targetptr;
如果enemytarget没有将析构函数定义成虚的,那么delete targetptr时,派生类的析构函数永远不会被调用,targetptr指向的enemytank对象永远得不到析构
2、实现虚函数需要对象附带一些额外信息,以使对象在运行时可以确定该调用哪个虚函数。
对大多数编译器来说,这个额外信息的具体形式是一个称为vptr(虚函数表指针)的指针。vptr指向的是一个称为vtbl(虚函数表)的函数指针数组。每个有虚函数的类都附带有一个vtbl。
当对一个对象的某个虚函数进行请求调用时,实际被调用的函数是根据指向vtbl的vptr在vtbl里找到相应的函数指针来确定的。
// 一个表示2d点的类
class point {
public:
  point(short int xcoord, short int ycoord);
  ~point();

private:
  short int x, y;
};
对于类point,一个point对象将刚好适合放进一个32位的寄存器中,
但如果point的析构函数为虚,情况就会改变。它的对象的体积将不知不觉地因虚函数翻番,从2个16位的short变成了2个16位的short加上一个32位的vptr
===条款15: 让operator=返回*this的引用===
用固定类型的情况下,赋值操作可以象下面这样链起来:
int w, x, y, z;
w = x = y = z = 0;
为支持这种连续赋值,operator=的返回值必须可以作为一个输入参数被函数自己接受。
string& string::operator=(const string& rhs)//返回值没有const
{
  ...
  return *this;
}
===条款16: 在operator=中对所有数据成员赋值===
1、一个正确的派生类的赋值运算符必须调用它的每个基类的的赋值运算符
class base {
public:
  base(int initialvalue = 0): x(initialvalue) {}
private:
  int x;
};

class derived: public base {
public:
  derived(int initialvalue)
  : base(initialvalue), y(initialvalue) {}
  derived& operator=(const derived& rhs);
private:
  int y;
};

逻辑上说,derived的赋值运算符应该象这样:
// erroneous assignment operator
derived& derived::operator=(const derived& rhs)
{
  if (this == &rhs) return *this;    // 见条款17 检查给自己赋值的情况

  y = rhs.y;                         // 给derived仅有的
                                     // 数据成员赋值
  return *this;                      // 见条款15 返回*this
}
不幸的是,它是错误的,因为derived对象的base部分的数据成员x在赋值运算符中未受影响。例如,考虑下面的代码段:
void assignmenttester()
{
  derived d1(0);                      // d1.x = 0, d1.y = 0
  derived d2(1);                      // d2.x = 1, d2.y = 1

  d1 = d2;         // d1.x = 0, d1.y = 1!
}
注意d1的base部分没有被赋值操作改变。
解决这个问题最显然的办法是在derived::operator=中对x赋值。
// 正确的赋值运算符
derived& derived::operator=(const derived& rhs)
{
  if (this == &rhs) return *this;

  base::operator=(rhs);    // 调用this->base::operator=
  y = rhs.y;

  return *this;
}
但如果基类赋值运算符是编译器生成的,有些编译器会拒绝这种对于基类赋值运算符的调用
derived& derived::operator=(const derived& rhs)
{
  if (this == &rhs) return *this;
  static_cast<base&>(*this) = rhs;      // 对*this的base部分调用operator= ,注意转换成引用base&
  y = rhs.y;
  return *this;
}
这段怪异的代码将*this强制转换为base的引用,然后对其转换结果赋值。这里只是对derived对象的base部分赋值。
还要注意的重要一点是,转换的是base对象的引用,而不是base对象本身。如果将*this强制转换为base对象,就要导致调用base的拷贝构造函数,创建出来的新对象(见条款m19)就成为了赋值的目标,而*this保持不变。这不是所想要的结果。
2、另一个经常发生的和继承有关的类似问题是在实现派生类的拷贝构造函数时。
derived(const derived& rhs): base(rhs), y(rhs.y) {}//别忘了调用基类的拷贝构造函数base(rhs)
===条款17: 在operator=中检查给自己赋值的情况===
class x { ... };
x a;
a = a;                     // a赋值给自己
这种事做起来好象很无聊,但它完全是合法的
判定规则:
1、值相等
c& c::operator=(const c& rhs)
{
  // 检查对自己赋值的情况
  if (*this == rhs)
    return *this;
  ...
}
2、相同的地址
c& c::operator=(const c& rhs)
{
  // 检查对自己赋值的情况
  if (this == &rhs) return *this;
  ...
}
3、实现一个返回某种对象标识符的成员函数:
class c {
public:
  objectid identity() const;    // 参见条款36
  ...
};

对于两个对象指针a和b,当且仅当 a->identity() == b->identity()的时候,它们所指的对象是完全相同的。
当然,必须自己来实现objectids的operator==。