C++基础知识归纳(3) -进阶篇《Effective C++必懂条款》

本文探讨了C++编程中的一些最佳实践,包括析构函数为何需要设置为虚函数以支持多态,如何避免名字遮掩,明智使用private继承,以及避免在构造函数中抛出异常。此外,还讲解了编译期多态与运行时多态的区别,以及如何利用析构函数避免资源泄露。最后,强调了避免在析构函数中抛出异常的重要性,因为这可能导致系统不稳定或资源无法正确释放。

析构函数需要设置为虚函数(virtual)的原因

带多态性性质的base class应该声明一个virtual虚构函数。否则使 指针指向派生类时使用delete操作只会删除基类成员而不会删除派生对象中的成员从而造成内存泄露。如果类不是作为基类使用或者不是为了具备多态性,就不应该声明virtual析构函数。

  1. 每个析构函数只会清理自己的成员(成员函数前没有virtual)。
  2. 可能是基类的指针指向派生类的对象,当析构一个指向派生类的成员的基类指针,这时程序不知道这么办,可能会造成内存的泄露,因此此时基类的析构函数要定义为虚函数;
    基类指针可以指向派生类的对象(多态),如果删除该指针delete[]p,就会调用该指针指向的派生类的析构函数,而派生类的对象又会自动调基类的成员函数,这样就会把派生类的对象释放,如果基类的析构函数没有定义成虚函数,则编译器实现的静态绑定,在删除基类的指针,只会释放基类的析构函数而不会释放派生类的成员函数,此时会导致释放内存不完全,就会导致内存泄露的问题。
  • 释放放pd的过程是:只是释放了基类的资源,而没有调用派生类的析构函数;这样的删除只能删除基类对象,而不能删除派生类的对象,造成内存泄露;
  • 在公有继承中,基类指针对派生类的对象的操作,只能影响哪些继承的成员,如果想要基类对非继承进行操作,则要把这个基类函数定义为虚函数;
  • 当基类的析构函数被定义成为虚函数,编译器就不会实现静态绑定,这样当释放pd时,会先释放派生类的析构函数,再释放基类的析构函数。

析构函数不要吐出异常

  1. 析构函数绝对不要吐出异常。如果一个被析构函数调用的函数可能抛出异常,析构函数应该捕捉任何异常,然后吞下他们(不传播)或结束程序。
  2. 如果需要对某个操作函数抛出的异常做出反应,那么class应该提供一个普通函数而非在析构函数中执行该操作。

绝对不要在类的构造和析构函数中调用虚函数

  1. 从语法上讲,调用完全没有问题。
  2. 但是从效果上看,往往不能达到需要的目的。
    Effective 的解释是:
  • 派生类对象构造期间进入基类的构造函数时,对象类型变成了基类类型,而不是派生类类型。
    同样,进入基类析构函数时,对象也是基类类型。
    所以,虚函数始终仅仅调用基类的虚函数(如果是基类调用虚函数),不能达到多态的效果,所以放在构造函数中是没有意义的,而且往往不能达到本来想要的效果。
  • C++构造函数和析构函数调用虚函数时都不会使用动态联编

总结:构造函数和析构函数调用虚函数时都不使用动态联编,如果在构造函数或析构函数中调用虚函数,则运行的是为构造函数或析构函数自身类型定义的版本。–所以没有意义

在operator =中处理自我赋值

在有些情况下需先删除自身所指向资源,再根据s指向资源创建新资源,此时自我复制会出现错误)

  • 解决自我赋值只要一句话:if(this == &s) return *this; // 解决自我赋值的一句话

  • 不能写成if(*this == s) return *this; // 注意条件判断的不同,这样写有问题!(因为判断是地址是否重合,而不是指针指向的内容是否相同。)

两种解决思路:

  1. 保存好旧的,再试着申请新的,若申请有问题,旧的还能保存。
 SampleClass& operator= (const SampleClass& s)
  {
   
   
           if(this == &s) return *this; //可以删掉
           a = s.a;
           b = s.b;
           float* tmp = p; // 先保存了旧的指针(深拷贝)
           p = new float(*s.p); // 再申请新的空间,如果申请失败,p仍然指向原有的地址空间
           delete tmp; // 能走到这里,说明申请空间是成功的,这时可以删掉旧的内容了
           return *this;
}
  1. 先用临时的指针申请新的空间并填充内容,没有问题后,再释放到本地指针所指向的空间,最后用本地指针指向这个临时指针。
SampleClass& operator= (const SampleClass& s){
   
   
          if(this == &s) return *this; //可以删掉
           a = s.a;
           b = s.b;
          float* tmp = new float(*s.p); // 先使用临时指针申请空间并填充内容
           delete p; // 若能走到这一步,说明申请空间成功,就可以释放掉本地指针所指向的空间
           p = tmp; // 将本地指针指向临时指针
           return *this;
 }

上述两种方法都是可行,但还要注意拷贝构造函数里面的代码与这段代码的重复性,试想一下,如果此时对类增加一个私有的指针变量,这里面的代码,还有拷贝构造函数里面类似的代码,都需要更新,有没有可以一劳永逸的办法?

SampleClass& operator= (const SampleClass& s)
{
   
   
         SampleClass tmp(s);
         swap(*this, tmp);
         return *this;
}
  1. 一种进一步优化的方案如下:
1 SampleClass& operator= (const SampleClass s)
2 {
   
   
3          swap(*this, s);
4          return *this;
5 }

这里去掉了形参的引用,将申请临时变量的任务放在形参上了,可以达到优化代码的作用。

以对象管理资源(RAII)

class Node {
   
   };
Node* CreateNode()
{
   
   
    
}
void Solve()
{
   
   
    Node *p=CreateNode();  //调用CreateNode函数
    ...
    delete p;         //释放资源
}

Solve函数可能因为continue、break等跳过不执行而导致内存泄露。
为使申请的资源能够自动释放,使用智能指针:

void Solve()
{
   
   
    std::auto_prt<Node>p(CreateNode());
    ...                    ///经由auto_ptr析构函数自动删除p对象
}

它还有另外一个性质:即通过copy构造函数或者copy assignment函数对他们进行复制操作时,它们会变成null,而复制所得的指针将拥有对资源的唯一支配权。

void Solve()
{
   
   
    std::auto_prt<Node>p1(CreateNode());
    std::auto_prt<Node>p2(p1);  //p2指向对象,p1为null
    p1=p2;                      //p1指向对象,p2为null
}

通过上面的例子藐视auto_ptr智能指针的弊端已经显露出来了,即无法行使正常的复制行为。

tr1::shared_ptr(引用计数智能指针):它不仅拥有auto_ptr智能指针的功能,最重要的是它还能进行复制行为。

void Solve()
{
   
   
   std::tr1::shared_ptr<Node>p1(CreateNode());
   std::tr1::shared_ptr<Node>p2(p1);  //p1和p2指向同一对象
   p1=p2;           //p1和p2指向同一对象
   ...             //p1,p2同时被销毁,它们所指的对象也被自动销毁
}
1. 为防止资源泄露,请使用RAII对象。它们在构造函数过程中获得资源,并在析构函数过程中释放资源。
2. 两个常用的RAII classes分别是auto_ptr,tr1::shared_ptr,后者更佳。

shared_ptr在最新的c++11中,已经被列入了标准指针,而auto_ptr则出局了。
shared_ptr(#include )采用RAII技术,是防止内存泄露的神器。(RAII,也称为“资源获取就是初始化”,是c++等编程语言常用的管理资源、避免内存泄露的方法。它保证在任何情况下,使用对象时先构造对象,最后析构对象。)

在资源管理类中提供对原始资源的访问方式

1.显示访问:

所谓显示访问就是在管理类的内部提供某个函数,使得外界可以得到资源的指针。通过这个函数被命名为get()函数,当然为了方便,我们也可以重载*,->运算符。

假设有一类资源FontHandle 字体处理类:

FontHandle getHandle();得到字体资源
void releaseFont(FontHandle fh);释放字体资源
class Font
{
   
   
public:
Font(FontHandle fh):f(fh)
{
   
   
}
~Font()
{
   
   
releaseFont(f);
}
FontHandle get() const
{
   
   
    Return f;
}
private:
FontHandle f;
};

通过get()函数访问原始资源。

2. 隐式转换

假设资源管理类已经提供了显示访问的API,那么用户每次访问底层资源都需要显示地调用get()函数,这样既有好处也有不足。好处在于这种转换都是用户知晓的,由用户控制的,不会发生一些用户不愿意转换却转换的事情。不足在于,如果这类显示访问太于频繁将很影响管理类的便利性。
于是隐式转换就出现了,隐式转换提供一种自动将资源管理对象转换为原始资源指针的功能。这主要是通过重载类型转换运算符实现的。

class Font
{
   
   
public:
Font(FontHandle fh):f(fh)
{
   
   
}
~Font()
{
   
   
releaseFont(f);
}
FontHandle get() const
{
   
   
    Return f;
}
operator FontHandle() const    //重载类型转换运算符
{
   
   
    Return f;
}
private:
FontHandle f;
};

以独立语句将newed对象置入智能指针

对以下代码:

processWidget(std::tr1::shared_ptr<Widget>(new Widget), priority());

编译器创建代码做以下三件事:

  1. 调用priority()
  2. 执行new Widget
  3. 调用shared_ptr构造函数

C++不同于很多语言,C++完成上面的事的顺序可能是123,213, 231。
如果顺序是213,那么当priority发生异常时,new Widget产生的指针就会丢失,引发资源泄漏。
避免的方法很简单,使用分离语句:

std::tr1::shared_ptr<Widget> pw(new Widget);
processWidget(pw, priority());

尽量以pass-by-reference-to-const来代替pass-by-value

  • 本质传的是地址,所以不存在对象的切割(地址类型Person&,可以理解为引用类型或者指针类型,存放的都是对象的地址,对于32位机而言,是4字节的整数,Person型的地址只是告诉编译器应该把这段地址的内容解释成什么)。Person&只是告诉编译器它保存的地址对应的内容是一个Person类型的,它会优先把这个内容往Person上去套,但如果里面有虚函数,即使用了virtual关键字,那么编译的时候就会往类中安插一个虚指针,这个虚指针的指向将在运行时决定,这就是多态的机制了,它会调用实际传入的那个对象的虚函数,而不是基类对象的虚函数。

  • 如果没有接触过多态,上面说的可能就会有难度,其实也可以简单的理解成,如果不加引用或指针,那么形参就会复制实参,但形参是基类的,它没有实参多出来的那部分,所以它就不能复制了,只能丢弃了;但如果加了引用或指针,那么无论是什么类型的,都是保存实参的地址,地址是个好东西啊,它不会发生切割,不会丢弃。

总结一下pass-by-reference-to-const的优点:
一是可以节省资源复制的时间和空间;
二是可以避免切割,触发多态,在运行时决定调用谁的虚函数。

那么是不是pass-by-reference-to-const一定好呢?
答案显示是否定的。一是任何事物都不是绝对的,二是C++默认是pass-by-value,自然有它的道理。
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

秋风遗梦

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值