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;
};

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

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

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

时间: 2024-10-04 23:00:50

C++ Primer 学习笔记_68_面向对象编程 -构造函数跟复制控制[续]的相关文章

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

面向对象编程 --构造函数和复制控制[续] 三.复制控制和继承 合成操作对对象的基类部分连同派生类部分的成员一起进行复制.赋值或撤销,使用基类的复制构造函数.赋值操作符或析构函数对基类部分进行复制.赋值或撤销. 类是否需要定义复制控制成员完全取决于类自身的直接成员.基类可以定义自己的复制控制而派生类使用合成版本,反之,基类使用合成版本,而派生类使用自己定义的复制控制也可以. 只包含类类型或内置类型的数据成员.不包含指针的类一般可以使用合成操作,复制.赋值或撤销这样的成员不需要使用特殊控制.但是:

C++ Primer 学习笔记_72_面向对象编程 --句柄类与继承[续]

面向对象编程 --句柄类与继承[续] 三.句柄的使用 使用Sales_item对象能够更easy地编写书店应用程序.代码将不必管理Item_base对象的指针,但仍然能够获得通过Sales_item对象进行的调用的虚行为. 1.比較两个Sales_item对象 在编写函数计算销售总数之前,须要定义比較Sales_item对象的方法.要用Sales_item作为关联容器的keyword,必须能够比較它们.关联容器默认使用keyword类型的小于操作符,可是假设给Sales_item定义小于操作符,

C++ Primer 学习笔记_31_面向对象编程(2)--继承(二):继承与构造函数、派生类到基类的转换 、基类到派生类的转换

C++ Primer 学习笔记_31_面向对象编程(2)--继承(二):继承与构造函数.派生类到基类的转换 .基类到派生类的转换 一.不能自动继承的成员函数 构造函数 拷贝构造函数 析构函数 =运算符 二.继承与构造函数 基类的构造函数不被继承,派生类中需要声明自己的构造函数. 声明构造函数时,只需要对本类中新增成员进行初始化,对继承来的基类成员的初始化调用基类构造函数完成(如果没有给出则默认调用默认构造函数). 派生类的构造函数需要给基类的构造函数传递参数 #include <iostream

C++ Primer 学习笔记_73_面向对象编程 --再谈文本查询示例

面向对象编程 --再谈文本查询示例 引言: 扩展第10.6节的文本查询应用程序,使我们的系统可以支持更复杂的查询. 为了说明问题,将用下面的简单小说来运行查询: Alice Emma has long flowing red hair. Her Daddy says when the wind blows through her hair, it looks almost alive, like a fiery bird in flight. A beautiful fiery bird, he

C++ Primer 学习笔记_34_面向对象编程(5)--虚函数与多态(二):纯虚函数、抽象类、虚析构函数、动态创建对象

C++ Primer 学习笔记_34_面向对象编程(5)--虚函数与多态(二):纯虚函数.抽象类.虚析构函数.动态创建对象 一.纯虚函数 1.虚函数是实现多态性的前提 需要在基类中定义共同的接口 接口要定义为虚函数 2.如果基类的接口没办法实现怎么办? 如形状类Shape 解决方法 将这些接口定义为纯虚函数 3.在基类中不能给出有意义的虚函数定义,这时可以把它声明成纯虚函数,把它的定义留给派生类来做 4.定义纯虚函数: class <类名> { virtual <类型> <函

C++ Primer 学习笔记_65_面向对象编程 --概述、定义基类和派生类

面向对象编程 --概述.定义基类和派生类 引言: 面向对象编程基于的三个基本概念:数据抽象.继承和动态绑定. 在C++中,用类进行数据抽象,用类派生从一个类继承另一个:派生类继承基类的成员.动态绑定使编译器能够在运行时决定是使用基类中定义的函数还是派生类中定义的函数. 继承和动态绑定在两个方面简化了我们的程序:[继承]能够容易地定义与其他类相似但又不相同的新类,[派生]能够更容易地编写忽略这些相似类型之间区别的程序. 面向对象编程:概述 面向对象编程的关键思想是多态性(polymorphism)

C++ Primer 学习笔记33_面向对象编程(4)--虚函数与多态(一):多态、派生类重定义、虚函数的访问、 . 和-&gt;的区别、虚析构函数、object slicing与虚函数

C++ Primer学习笔记33_面向对象编程(4)--虚函数与多态(一):多态.派生类重定义.虚函数的访问. . 和->的区别.虚析构函数.object slicing与虚函数 一.多态 多态可以简单地概括为"一个接口,多种方法",前面讲过的重载就是一种简单的多态,一个函数名(调用接口)对应着几个不同的函数原型(方法). 更通俗的说,多态行是指同一个操作作用于不同的对象就会产生不同的响应.或者说,多态性是指发出同样的消息被不同类型的对象接收时有可能导致完全不同的行为. 多态行分

C++ Primer 学习笔记_67_面向对象编程 --转换与继承、复制控制与继承

面向对象编程 --转换与继承.复制控制与继承 I.转换与继承 引言: 由于每一个派生类对象都包括一个基类部分,因此能够像使用基类对象一样在派生类对象上执行操作. 对于指针/引用,能够将派生类对象的指针/引用转换为基类子对象的指针/引用. 基类类型对象既能够作为独立对象存在,也能够作为派生类对象的一部分而存在,因此,一个基类对象可能是也可能不是一个派生类对象的部分,因此,没有从基类引用(或基类指针)到派生类引用(或派生类指针)的(自己主动)转换. 关于对象类型,尽管一般能够使用派生类型的对象对基类

C++ Primer学习笔记32_面向对象编程(3)--继承(三):多重继承、虚继承与虚基类

C++ Primer学习笔记32_面向对象编程(3)--继承(三):多重继承.虚继承与虚基类 一.多重继承 在C++语言中,一个派生类可以从一个基类派生,称为单继承:也可以从多个基类派生,称为多继承. 多重继承--一个派生类可以有多个基类 class <派生类名> : <继承方式1> <基类名1>,<继承方式2> <基类名2>,... { <派生类新定义成员> }; 可见,多继承与单继承的区别从定义格式上看,主要是多继承的基类多于一个