Effective c++笔记02

Effective c++笔记02

    

item3:尽可能使用const(补充)

     其实这一节应该在上一次笔记的里面,但是当时还不太理解,今天回头翻看,顺手补上。
     作者在书中提到了对待常量成员函数的两种流派,一种叫bitwise constness,另一种叫logical constness。
     虽然看着有点蒙,但是名字还是很好的能说明些问题。bitwise认为只有当成员函数不改变任何变量的时候才是cosnt的(不改变对象的任何一个bit)
     但是这样并不能保证对象内的成员就不会被改变。有个特殊情况,当成员函数返回一个引用的时候。举个例子

class Text {
private:char *m_char;
public:
  Text() {
    m_char = new char[30];
  }
/*** this is bitwise ***/
  char &operator[](size_t pos) 
  const{ return m_char[pos];}
};

/***main***/
int main() 
{
    const Text t;
    t[0] = '3';
}

    可以看到,虽然我把对象声明成了const,虽然我的函数是const的成员函数,但是我依然可以改变对象里面的内容!而且编译器还默许了它!
    因为这样,又有一派人认为:“bitwise太虚伪了,就应该允许常量成员函数修改对象内的数据”
    现在提出了一种需求,在Text中还应该加入位数校验,长度校验,这就可能会对某些成员进行更改,但我们依然想要在一个常量成员函数中完成这些事。于是他们发明的关键字 ***mutable***。再来个例子
    

class Text {
private:
  char *m_char;
  mutable size_t len;
public:
  Text() {
    len = 0;
    m_char = new char[30];
  }
/*** this is logical constness ***/
  char &operator[](size_t pos) 
  const{
    /**do something with len**/
    /**like len = len + 1;**/
    return m_char[pos];
    }
};

    看起来很棒!能在常量成员函数中更改成员了!
    但是设想一下,当后续有越来越多的需求需要解决时,我们要不停的给两方加入数据变动,大量的重复.......这也太烦了,有什么简便的方法么?
     答案案肯定是有,我们可以让non-const成员函数调用const成员函数。略有区别的是const的成员函数这次会返回一个const的返回值(const对象的成员函数返回值当然得是const的咯)

class Text {
private:
  char *m_char;
public:
  Text() {
//.........................................
  }
char& Text::operator[](size_t pos)
{
    return const_cast<char&>(
        static_cast<const Text&>(*this)[pos]);
}
};

    在这里,为了能达到让非常量函数调用常量函数的目的,得把这个类先转换成常量(不然他会一直无限循环调用本身),然后再把返回的常量转化为非常量。于是就出现了这一个丑到不行的巨大式子。
    这里我有个疑惑(可能是我个人概念没搞清楚吧hh),为什么在转化对象为常量时要化为const Text&?     然后我百度了一下才发现...原来*this返回的是个reference...干

item7:为多态基类声明virtual构析函数

    这个item其实很好理解
    太长不看版本:如果你用基类声明子类,而基类构析函数不为virtual,可能会导致内存泄漏。
    先从虚表说起,在c++中只要类中存在virtual函数,那么它就会包含一张虚表,在虚表里记录了函数的地址,用来在运行时确定到底应该调用哪个函数。关于虚表讲的文章非常多,我就不展开了。
    那么如果你给基类的构析函数加上了virtual,那么它在运行时就会调用正确的构析函数 举个例子

class a {
public: int *ab;
  a() { ab = new int(10); }
  ~a() {
    delete ab;
    std::cout << "use a's destuctor\n";
  }
};

class b : public a {
public: int *cd;
  b() : a() {
    cd = new int(20);
  }

  ~b() {
    std::cout << "use b's destructor\n"; 
    delete cd;
  }
};

int main()
{
  a aa = new b();
  delete aa;
}

    简单说一下,我写了两个类,a与b,a为b的基类,接着我用a声明了一个b,再释放掉这些空间。看看运行结果。

use a's destuctor

    毫无疑问是调用了a的构析函数,那这样也就意味着只释放掉了a中的int* ab的空间,而b中int* cd的空间没有正确释放,造成了内存泄漏。
那如果我们给基类a的构析函数加上virtual呢

class a {
public: int *ab;
  a() { ab = new int(10); }
  //changeing a's destructor to a virtual function!
  virtual ~a() {
    delete ab;
    std::cout << "use a's destuctor\n";
  }
};
结果>>
use b's destructor
use a's destuctor

    可以看到现在空间被正确释放了,先调用了b的构析函数,再调用了a的构析函数。
    虽然给构析函数加上virtual确实很好用,但是也会导致对象的体积增加,所以也不能乱加。
    还有,如果当一个类中没有纯虚函数但你想让它成为一个抽象类,不能被实例化,怎么办呢?
    可以给构析函数声明为纯虚,并且还得给它一份定义

class a
{
  public: virtual~a() = 0;
};
a::~a(){}

    众所周知编译器调用构析函数的顺序是先从最最深处的派生类开始,最后到基类。
    而如果没有基类构析函数的定义,那么链接器就会送给你一个连接错误(喜闻乐见link2019/undefined reference

item8:别让异常逃离构析函数

    C++不禁止但是不鼓励在构析函数中抛出异常。举个例子

class widget{
  public:
  .....
  ~widget(){}
  void dosomething(){
    std::vector<widget> v;
    ...
  }
};

    在正常情况下,当函数dosomething执行完以后,v将自动销毁(显而易见,这会调用其中每一个元素的构析函数)。但如果在vector v被销毁时,其中的第一个widget元素抛出异常,那其余的元素该怎么办?销毁还是不销毁(导致内存泄漏哦),这是个问题。那假如第二个widget也抛出了异常,那么就同时存在两个异常了!在这种情况下,程序若是不结束运行,那么就是未定义行为
    为了避免这种情况,我们可以选择在接收到异常的时候直接终止程序运行(std:abort();)也可以把选择权交给使用者。比如说,可以在构析函数之前提供一个“自制版的构析函数”,让使用者拥有处理的机会。

item8:绝不在构造和构析过程调用virtual函数

    太长不看版:在基类构造构析期间virtual函数是基类的版本而不是派生类的版本
    在构造函数中调用虚函数会带来意料之外的结果。举个小例子

class a {
public:
  virtual void doSomething() = 0;
  a() { doSomething(); }
};
void a::doSomething() { std::cout << "haha it's a\n"; }
class b : public a {
public:
  b() : a() {}
  void doSomething() override { std::cout << "haha it's b\n"; }
};

    就在我打出这串代码的时候我的clangd已经贴心的给我跳了warn(仿佛在嘲笑我的愚蠢........)至于这个warn我先按下不表。
    这几行代码很简单,大意就是在基类a中调用了一个纯虚函数,但在这里我做了点手脚,把这个纯虚函数给定义了。现在该谈谈main函数了

int main() {
  a *a1 = new b();
  delete a1;
}

    好吧这边又给我报了第二个warn嘲笑我忘记了上一条item:给基类的构析函数加virtual,但我们的重点不是这个!
现在来看,这行代码执行后,会调用得是哪个doSomething呢,a的还是b的,润一下看看

结果>>
haha it's a

答案:调用的是a的,这个答案在某些程度上不令人意外,但是万一我没给那个纯虚函数写定义那么出来的可就是undefined reference了,程序会直接退出,足以说明问题的严重!

    而之前我所述报的第一个warn内容如下all to pure virtual member function 'doSomething' has undefined behavior; overrides of 'doSomething' in subclasses are not available in the constructor of 'a'大意是:在子类中重写的函数在父类中无效!而且在构造构析函数中调用纯虚函数会引发未定义行为!
    这意味着如果你的本意是调用派生类的函数doSomething,那就大错特错了,你会一直纠结在为什么调用了错误版本的函数。
    为了避免这种情况,你可以选择在向父类的构造函数传递足够的信息,并且把共同的部分抽象出来。

item10:返回一个reference to this*

    太长不看版:让赋值操作符返回*this的引用。
    无了,这条太简单了。

item11:在operator'='中处理自我赋值

    在把自己赋值给自己时可能发生一些问题,比如提前释放掉了还在用的资源。举个例子:

  class a
  {
    public: int *something;
    ...........
    a& operator=(const a& rhs)
    {
      delete something;//释放掉自身的资源
      something = new int(*rhs.something);//复制一份值
      return *this;
    } 
  }

    这段程序的问题在于,如果自己给自己赋值,那么就会把自己的资源释放掉!然后把自己的指针指向一个被删除的对象!
    为了防止这种问题,可以做一个简单的自我认同测试

a& operator=(const a& rhs)
    {
      //自我认同!
      if(this == &rhs) return *this;
      delete something;//释放掉自身的资源
      something = new int(*rhs.something);//复制一份值
      return *this;
    } 

    还有一种方法是在复制之前先别释放掉那片空间。

a& operator=(const a& rhs)
    {
      //做个备份
      int* tmp = something;
      something = new int(*rhs.something);//复制一份值
      delete tmp;//释放掉自身的资源

      return *this;
    } 

    一种方法是使用copy and swap技术

void swap(a& tmp){...}//交换 *this 和tmp的数据
a& operator=(const a& rhs)
    {
      a tmp(this); //调用复制函数
      sawp(tmp)//交换tmp和*this的值
      return *this;
    } //tmp的值在执行完自动释放

    由此也引申出来一种方法

a& operator=(a rhs)//pass by value,传值就是复制
    {
      sawp(rhs)//把rhs的复制品与*this交换
      return *this;
    } //tmp的值在执行完自动释放

    这种方法得益于这个操作符是传值不是传址,这代表着在函数里的只是rhs的一份复制品,你可以随意操作!

item12:复制对象时勿忘记每一个成分

    如标题所写,自己写copy函数的时候把每一个成分都复制上就行啦,添加其他内容后也记得要加。
    所以这章就完了?不,有一种情况很特殊。

class a
{
  public: a(const a& rhs)//copy数据
  {
    ........
  }

  a& operator=(const a& rhs)//copy数据
  {
    .......
  }
  private:int private_data;
};

    到现在为止一切都很好,但如果我把a作为基类呢?

class b:public a
{
  public: int some_data;
  public: b(const b& rhs)//copy数据
  :some_data(rhs.some_data)
  {}
  b& operator=(const b& rhs)//copy数据
  {
    some_data = rhs.some_data;
  }
};

    到现在为止一切也都正常.....吗?我们是不是忘记处理基类的数据复制了?但是基类的数据是私有的,我们够不着啊
    不要慌,我们还有最后的手段,调用基类的复制函数

class b:public a
{
  public: int some_data;
  public: b(const b& rhs)//copy数据
  :a(rhs),//调用基类的复制函数!
  some_data(rhs.some_data)
  {}
  b& operator=(const b& rhs)//copy数据
  {
    a::operator=(rhs);//调用基类的复制函数!
    some_data = rhs.some_data;
  }
};

    可能有人会发现,复制函数和复制操作符实现的内容好像很相近,那我们能不能用一个去调用另一个呢?在你问之前,不,没有语法去支持它!

后记

    其实按照我的懒惰程度,这篇文章本不应该出现的这么早,但今天很特殊,浦东封区后的第一个雨天。
    自从楼道里出了个阳性,一天一抗原两天一核酸,除了核酸和扔垃圾,我就没有下过楼,我只能盯着窗户看。窗外下着雨,雨水打在玻璃上,雨滴汇成了一条线蜿蜒而下,汇集在排水槽上,流到楼下。路上几乎没有人,只有一个穿着防护服的志愿者,在运物资,他打着伞,慢慢的走出我的视线,于是街上一个人也没有了。我没见过这么冷清的雨天,孤寂的好像世界末日。
    雨总是能让我想到泥土,湿滑的路面和松动的街砖,这让我蠢蠢欲动,只待雨过天晴便能出门。这时候的空气里有股清香,那是被雨水浸湿的草香,路面上落满了被雨冲下来的香樟果,黑溜溜一片,经受着腐烂,雨水在地上成了小条的河流,带出些黑色的汁水。街上的人们被雨水冲散尚未聚拢,为了跟上人群,他们的步伐加快,不自觉的踏入地面上的小水坑,鞋子带起地上的水,他们仍然义无反顾,大步向前。有的人还不知道雨停了,他们面无表情,直视前方,于是手上还撑着伞,或者是身上还披着雨衣,仿佛有一道咒语,横亘在他们与世界之间,让他们的时间永远停在了下雨的时候,让他们的路一直延伸,直到地平线之下。
    然而此时我什么都做不了,我盯着窗外看,窗外没有我所说的那些景象,只有疫情下的上海。我只好回到书桌边,找点事做。

END