首页
登录 | 注册

C/C++拾遗(十七):面向对象补充——复制控制

     昨天粗略地看了《面向对象编程》这一章,简单地梳理了一下自己感觉重要的几个知识点,今天就昨天没有来得及仔细看的部分做些补充,主题是涉及类继承的复制控制以及类作用域的讨论。
一、构造函数与复制控制
     我们知道对于一个类而言,构造函数、复制构造函数、赋值运算符与析构函数是必需的部分,换句话说,如果我们没有显式定义,C++会启用默认的系统版本。在类的继承中,以上四种函数同样是不能被继承的,如果在派生类中没有定义,结果就是使用默认的版本。为了便于说明,我们首先引入一个基类和派生类。基类用来描述计算一般图书销售的价格,而派生类则用来描述数量复合要求的图书的折扣价格总额。具体定义如下:

  1. //图书基类
  2. class Item_base
  3. {
  4.       public:
  5.              Item_base(const std::string &book = "",
  6.                        double sales_price = 0.0):
  7.                        book_no(book), price(sales_price) {}
  8.              std::string book() const {return book_no;}
  9.              virtual double net_price(std::size_t n) const
  10.                        {return n*price;}
  11.              virtual ~Item_base() {}
  12.       private:
  13.               std::string book_no;
  14.       protected:
  15.                 double price;
  16.                        
  17.       } ;
  18. //含有折扣的图书类,必须满足最小的min_qyt数量要求才可以享有折扣
  19. class Bulk_item : public Item_base
  20. {
  21.       public:
  22.              double net_price(std::size_t) const;
  23.       private:
  24.               std::size_t min_qyt;
  25.               double discount;
  26.       }
  27. double Bulk_item::net_price(std::size_t cnt) const
  28. {
  29.        if (cnt >= min_qty)
  30.           return cnt * (1 - discount ) * price;
  31.        else
  32.           return cont * price;
  33.        }
1. 构造函数      
     在上面的类定义中,可以看到基类定义了自身的带有默认参数值的构造函数,一般来说,基类的构造函数受继承关系的影响不大,唯一需要考虑的是允许哪些成员被派生类访问,即确定是否需要protected标号。派生类的构造函数与基类的构造函数唯一的不同是:初始化派生类成员的同时还要初始化派生类对象的基类部分,基类部分由基类提供的构造函数初始化。即我们可以定义派生类的构造函数如下:

  1. Bulk_item::Bulk_item(const std::string& book, double sales_price,
  2.                      std::size_t qty = 0, double disc_rate = 0.0):
  3.                      Item_base(book, sales_prices),min_qty(qyt), discount(disc_rate) {}
      上述的构造函数接受实参用来初始化基类对象和自身的成员,需要记住的一点是,当调用派生类的构造函数时,首先调用基类的构造函数初始化基类成员,然后按照类定义中成员的声明顺序依次初始化派生类成员。析构函数调用时顺序则与此相反。另一个很重要的是:每一个派生类都只能初始化其直接基类。即若A派生B,B派生C,则在C的构造函数中只能初始化B,调用B的构造函数时会自动调用A的构造函数。构造函数只能初始化其直接基类的原因是每个类都定义了自己的接口,C++的原则是一旦某个类定义了自身的接口,那么所有与该类对象的交互都应该通过这些接口实现。
2. 复制控制与继承
     派生类的复制构造、赋值运算以及析构函数,都需要考虑进基类的影响。当定义派生类的复制构造函数时,必须显示的调用基类的复制构造函数,如:
Derived (const Derived& d): Base(d)  {}
     注意这里的Base(d)的作用在于将派生类对象d转换为其基类部分的引用,然后调用基类的复制构造函数初始化派生类对象的基类部分。派生类的赋值操作与此类似,但是不同的是需要防止自身赋值的出现,因此要增加一步自身赋值的判断:
Derived& Derived::opreator=(const Derived &rhs)
{
    if (this != rhs)
        Base::operator=(rhs);
     return *this;
}

二、虚析构函数与构造函数/析构函数中的虚函数
     一般情况下,派生类的析构函数调用时会自动调用其基类的析构函数,但是对于多态性质的动态绑定而言,我们析构的对象是一个基类的指针或者引用,但是其代表的往往可能是一个派生类对象。基类的析构函数无法自动析构一个派生类对象,因此导致内存泄漏。解决这类办法的便捷途径是使得析构函数也可以实现动态绑定,没错!就是将基类的析构函数声明为虚函数就可以啦!可以看看下面的例子程序:

  1. //Check out Virtual Destructor
  2. #include <iostream>
  3. #include <cstdlib>

  4. using namespace std;

  5. class Base
  6. {
  7.       public:
  8.              Base(int a):num(a) {cout << "Base Constructor..."<<endl;}
  9.              virtual void print() {cout << "num is "<< num<<endl;}
  10.              ~Base() {cout << "Base Destructor..."<<endl;}
  11.       protected:
  12.                 int num;
  13.       };
  14. class Derived : public Base
  15. {
  16.       public:
  17.              Derived(int a, int b):Base(a), data(b) {cout << "Derived Constructor..."<<endl;}
  18.              void print() {cout << "data is "<<data<<endl;}
  19.              ~Derived() {cout << "Derived Destructor..."<<endl;}
  20.       private:
  21.               int data;
  22.       };

  23. int main()
  24. {
  25.     Base *pB = new Derived(100, 200);
  26.     pB->print();
  27.     delete pB;
  28.     
  29.     
  30.     system("pause");
  31.     return 0;
  32. }
      函数运行结果如下:对象析构时竟然只调用了基类的析构函数!

     只要将12行修改为:virtual ~Base() {cout << "Base Destructor..."<<endl;},
析构时就会自动线调用派生类的析构函数然后是基类的析构函数:

     问题解决,需要注意区分只有析构函数可以声明为虚函数,构造函数与复制构造、赋值运算都不能作为虚函数。说完了虚析构函数,我们再来看看构造函数和析构函数中的虚函数。这里大致有两种情况:一种是构造函数/析构函数调用虚函数,另一种是构造函数/析构函数调用的函数调用了虚函数,但是不管哪一种,虚函数都绑定到构造函数或者析构函数自身类型定义的版本。具体来说,就是对于一个派生类对象的构造与析构,C++编译器认为在这个过程中对象类型会发生变化,一开始是调用派生类对象的虚函数(以析构的情况为例),析构基类部分时就认为是一个基类型的对象,调用基类的析构函数。

三、继承情况下的类作用域
     说到作用域,大致涉及对象名称解析的查找规则以及由此带来的名称冲突解决方案。说起来麻烦,但是只要平时在编程时小心避免出现对象成员同名的情况,一般就不会出现这部分的问题。在列出名称解析规则前,先感性地说几个规则:
1. 名称查找在编译时发生:除了动态绑定以外,更多的情况是对象、指针与引用的静态绑定,即在编译时即确定了对象;
2. 当发生成员或函数名称冲突时,派生类的成员会覆盖基类成员,成员函数只看函数名,无视其他;
     好了,是不是与函数的变量声明作用域规则很像?接下来我们给出函数名称查找与继承的规则:
<1>首先确定进行函数调用的对象、引用或指针的静态类型;
<2>在该类中查找函数,如果找不到就直接在基类中查找,如此依次向上递推,直到找到最后一个类,找不到返回错误;
<3>一旦找到该名称即进行常规检查(原型),检查调用是否合法;
<4>若函数调用合法,C++编译器生成代码直接调用函数;若存在动态绑定,则生成代码以确定根据对象类型进行运行时绑定;

      C++的学习是一个漫长的过程,还有浩如烟海的知识需要自己去学习。但是现在针对C/C++基础的学习就先告一段落了,接下来的一段时间一方面要将更多的精力投身到手头的项目上,另一方面则要梳理一下这段时间自己学习的东西,准备开始MFC的学习。
     路漫漫其修远兮,加油!

相关文章

  •      面向对象编程(OPP:Object-oriented programming)基于三个基本的概念:数据抽象.继承和动态绑定.之前在学习"基于对象的编程"时已经了解到了数据封装和抽象的作用,今天学习的这部分主要来 ...
  • C/C++拾遗(十八):面向对象——句柄类与继承
          昨天由于时间的关系剩下一个小尾巴,今天忙里偷闲来把这个洞洞填上昨天学习了"面向对象编程"的部分,详细讨论了复制控制与类作用域需要注意的问题.这里有一个新的问题,如何实现一个类似"购物车"的 ...
  • C# 操作Word页眉页脚——奇偶页/首页不同、不连续设置页码、复制/锁定/删除页眉页脚
    本文是对Word页眉页脚的操作方法的进一步的阐述.在"C# 添加Word页眉页脚.页码"一文中,介绍了添加简单页眉页脚的方法,该文中的方法可满足于大多数的页眉页脚添加要求,但是对于比较复杂一点的文档,对页眉页脚的添加要求 ...
  • 结论: 这种情况下复制节点(即从节点)无法提升为主节点,复制节点会一直尝试和主节点建立连接,直接成功.主节点恢复后,复制节点仍然保持为复制节点,并不会成为主节点. 复制节点无法提升为主节点的原因是复制节点未发起成为主节点的选举.   复制节 ...
  • C# 复制PPT幻灯片
    概述 本篇文档中将介绍如何在C#中复制PPT幻灯片.关于复制方法及内容,在示例中将分别从以下几个要点来分别阐述: 1.复制幻灯片 1.1在同一个PPT文档内复制(如:PPT文档A中的幻灯片1复制到幻灯片2) 1.2在不同PPT文档间复制(如 ...

2020 unjeep.com webmaster#unjeep.com
12 q. 0.015 s.
京ICP备10005923号