首页 > 代码库 > C++ Primer 学习笔记_68_面向对象编程 --构造函数和复制控制[续]

C++ Primer 学习笔记_68_面向对象编程 --构造函数和复制控制[续]

面向对象编程

--构造函数和复制控制[]



三、复制控制和继承

合成操作对对象的基类部分连同派生类部分的成员一起进行复制赋值撤销,使用基类的复制构造函数、赋值操作符或析构函数对基类部分进行复制、赋值或撤销。

类是否需要定义复制控制成员完全取决于类自身的直接成员。基类可以定义自己的复制控制而派生类使用合成版本,反之,基类使用合成版本,而派生类使用自己定义的复制控制也可以。

只包含类类型或内置类型的数据成员、不包含指针的类一般可以使用合成操作,复制、赋值或撤销这样的成员不需要使用特殊控制。但是:具有指针成员的类一般要定义自己的复制控制来管理这些成员

先复制基类部分,然后复制派生类部分。



1、定义派生类复制构造函数

如果派生类显式定义自己的复制构造函数或赋值操作符则该定义将完全覆盖默认定义。被继承类的复制构造函数和赋值操作符负责对基类成分以及类自己的成员进行复制或赋值。

如果派生类定义了自己的复制构造函数,则该复制构造函数一般应显式使用基类复制构造函数初始化对象的基类部分


class Base
{
public:
    //...
};

class Derived : public Base
{
public:
    Derived(const Derived &d):Base(d)
    {
        //...
    }
};

初始化函数Base(d)将派生类对象d转换为它的基类部分的引用并调用基类复制构造函数

如果省略基类初始化函数:

    Derived(const Derived &d)
    {
        //...
    }

则运行Base的默认构造函数初始化对象的基类部分。假定Derived成员的初始化从d复制对应成员,则新构造的对象将具有奇怪的配置:它的Base部分将保存默认值,而它的Derived成员是另一对象的副本



2、派生类赋值操作符

如果派生类定义了自己的赋值操作符,则该操作符必须对基类部分进行显式赋值!

    //Base::operator=(const Base &) 不会自动被调用,只有显式调用它!
    Derived &operator=(const Derived &rhs)
    {
        //赋值操作符必须防止自身赋值
        if (this != &rhs)
        {
            /*
            *基类的赋值操作符可以由类定义,也可以是合成赋值操作符
            */
            Base::operator=(rhs);
            //定义自己的派生类赋值操作符部分
            //...
        }

        return *this;
    }

3、派生类析构函数

析构函数的工作与复制构造函数和赋值操作符不同:派生类析构函数不负责撤销基类对象的成员。编译器总是显式调用派生类对象基类部分的析构函数。每个析构函数只负责清除自己的成员:

    //自动调用Base::~Base()
    ~Derived()
    {
        //...
    }

对象的撤销顺序与构造顺序相反:首先运行派生类析构函数,然后按继承层次依次向上调用各基类析构函数!

构造函数&赋值操作符&复制构造与 析构函数的对比

构造函数&赋值操作符&复制构造函数

析构函数

1、既要负责自己的成员,又要负责基类[调用基类相应定义]

只需负责自己的成员就好了

2、首先运行基类的构造|复制|赋值,然后运行派生类的

首先运行派生类的,然后调用基类的

3、不能定义为虚函数

可以定义为虚函数[而且一般是虚函数]



四、虚析构函数

删除指向动态分配对象的指针时,需要运行析构函数在释放对象的内存之前清除对象。处理继承层次中的对象时,指针的静态类型可能与被删除对象的动态类型不同,可能会删除实际指向派生类对象基类类型指针

如果删除基类指针,则需要运行基类析构函数并清除基类的成员,如果对象实际是派生类型的,没有定义该行为。要保证运行适当的析构函数,基类中的析构函数必须为虚函数:

class Item_base
{
public:
    virtual ~Item_base() {}
    //...
};

如果析构函数为虚函数,那么通过指针调用时,运行哪个析构函数将因指针所指向对象的类型的不同而不同

    Item_base *itemP = new Item_base;
    delete itemP;   //调用Item_base版本
    itemP = new Bulk_item;	//此时就是指针的静态类型与动态类型不同
    delete itemP;		//调用Bulk_item版本

如果层次中根类的析构函数为虚函数,则派生类析构函数也将是虚函数,无论派生类显式定义析构函数还是使用合成析构函数,派生类析构函数都是虚函数

三法则”指出:如果类需要析构函数,则类几乎也确实需要其他复制控制成员。基类几乎总是需要析构函数,从而可以将析构函数设为虚函数。如果基类为了将析构函数设为虚函数则具有空析构函数,那么,类具有析构函数并不表示需要赋值操作符复制构造函数

【最佳实践】

即使析构函数没有工作要做,继承层次的根类也应该定义一个虚析构函数



构造函数和赋值操作符不是虚函数

在复制控制成员中,只有析构函数应定义为虚函数,构造函数不能定义为虚函数。构造函数是在对象完全构造之前运行的,在构造函数运行时,对象的动态类型还不完整

将类的赋值操作符设为虚函数很可能会令人混淆,而且不会有什么好处!

//P496 习题15.18 说明在什么情况下该具有虚析构函数?
    /*
    *作为基类使用的类应该具有虚析构函数:
    *以保证在删除(指向动态分配对象的)基类指针时,
    *根据指针实际指向的对象所属的类型运行适当的析构函数!
    *如:
    */
    Item_base *itemP = new Bulk_item;
    delete itemP;   //需要运行的是Bulk_item的析构函数

//习题15.20
class Item_base
{
public:
    Item_base(const std::string &book = "",
              double sales_price = 0.0):
        isbn(book),price(sales_price)
    {
        cout <<
             "Item_base(const std::string &,double)"
             << endl;
    }

    Item_base(const Item_base &rhs);
    Item_base &operator=(const Item_base &rhs);
    virtual ~Item_base();

private:
    std::string isbn;

protected:
    double price;
};

Item_base::Item_base(const Item_base &rhs):isbn(rhs.isbn),price(rhs.price)
{
    cout << "Item_base(const Item_base &)" << endl;
}
Item_base &Item_base::operator=(const Item_base &rhs)
{
    isbn = rhs.isbn;
    price = rhs.price;
    cout << "operator=(const Item_base &)" << endl;

    return *this;
}
Item_base::~Item_base()
{
    cout << "~Item_base()" << endl;
}

class Bulk_item : public Item_base
{
public:
    Bulk_item(const std::string &book = "",
              double sales_price = 0.0,
              std::size_t qty = 0,
              double disc_rate = 0.0):
        Item_base(book,sales_price),
        min_qty(qty),discount(disc_rate)
    {
        cout <<
             "Bulk_item(const std::string &book,double,std::size_t,double)"
             << endl;
    }

    Bulk_item(const Bulk_item &rhs);
    Bulk_item &operator=(const Bulk_item &rhs);
    ~Bulk_item();

private:
    std::size_t min_qty;
    double discount;
};

Bulk_item::Bulk_item(const Bulk_item &rhs):
    Item_base(rhs),min_qty(rhs.min_qty),discount(rhs.discount)
{
    cout << "Bulk_item(const Bulk_item &)" << endl;
}
Bulk_item &Bulk_item::operator=(const Bulk_item &rhs)
{
    if (this != &rhs)
    {
        Item_base::operator=(rhs);
        min_qty = rhs.min_qty;
        discount = rhs.discount;
    }
    cout << "operator=(const Bulk_item &)" << endl;

    return *this;
}
Bulk_item::~Bulk_item()
{
    cout << "~Bulk_item()" << endl;
}

int main()
{
    Item_base *itemP = new Bulk_item;
    delete itemP;

    Item_base base;
    Bulk_item bulk;
    base = bulk;

    Item_base text(bulk);
}

五、构造函数和析构函数中的虚函数

构造派生类对象时首先运行基类构造函数初始化对象的基类部分。在执行基类构造函数时,对象的派生类部分是未初始化的。实际上,此时对象还不是一个派生类对象

撤销派生类对象时,首先撤销它的派生类部分,然后按照与构造顺序的逆序撤销它的基类部分。

在这两种情况下,运行构造函数或析构函数时候,对象都是不完整的。为了适应这种不完整,编译器将对象的类型视为在构造函数或析构期间发生了变化。在基类构造函数或析构函数中,将派生类对象当做基类类型对象对待

如果在构造函数或析构函数中调用虚函数,则运行的是为构造函数或析构函数自身类型定义的版本

class Item_base
{
public:
    Item_base(const std::string &book = "",
              double sales_price = 0.0):
        isbn(book),price(sales_price)
    {
        display();
    }

    virtual ~Item_base()
    {
        display();
    }

    virtual void display()
    {
        cout << "in Item_base!" << endl;
    }

private:
    std::string isbn;
    double price;
};

class Bulk_item : public Item_base
{
public:
    Bulk_item(const std::string &book = "",
              double sales_price = 0.0,
              std::size_t qty = 0,
              double disc_rate = 0.0):
        Item_base(book,sales_price),
        min_qty(qty),discount(disc_rate)
    {
        display();
    }

    void display()
    {
        cout << "in Bulk_item!" << endl;
    }

    ~Bulk_item()
    {
        display();
    }

private:
    std::size_t min_qty;
    double discount;
};

无论由构造函数(或析构函数)直接调用虚函数,或者从构造函数(或析构函数)所调用的函数间接调用虚函数,都应用这种绑定。

[理解]如果从基类构造函数(或析构函数)调用虚函数的派生类版本会怎么样?

虚函数的派生类版本很可能会访问派生类对象的成员,毕竟,如果派生类版本不需要使用派生类对象的成员,派生类多半能够使用基类中的定义。但是,对象的派生部分的成员不会在基类构造函数运行期间初始化,实际上,如果允许这样的访问,程序很可能会崩溃