美文网首页
继承和多态

继承和多态

作者: MinoyJet | 来源:发表于2017-04-29 22:24 被阅读0次

    继承和多态

    1. 继承的优缺点

    • 优点
      (1)子类可以灵活地改变父类中的已有方法;
      (2)能够最大限度的实现代码重用。
    • 缺点
      (1)子类无法在运行时改变与父类的继承关系;
      (2)修改父类的某些方法,可能会影响到所有的子类;
      (3)继承会使系统的架构层次增多,给开发和维护带来困难。

    2. 子类不能继承父类的私有成员,只能通过父类的成员函数来访问父类的私有成员

    3. 继承方式

    在从父类派生一个子类时可以有 3 种派生方式。分别为 public、private 和 protected 。其中 public 派生方式表示父类中的公有方法和受保护方法仍然为私有方法、共有方法和受保护方法。private 派生方式表示父类中的公有方法、受保护方法在子类中都是私有的。protected 派生方式表示示父类中的公有方法、受保护方法在子类中都是受保护的。

    4. 如何访问被隐藏的基类成员?

    #include <iostream>
    #include <cstring>
    
    #define MAXLEN 128 //定义一个宏
    
    using namespace std;
    
    class CEmployee //定义员工类
    {
    protected: //定义 protected 数据成员
        char m_szName[MAXLEN]; //定义员工姓名
    public:
        CEmployee() //定义默认构造函数
        {
            memset(m_szName, 0, MAXLEN); //初始化 m_szName
        }
        void SetName(const char* pszName) //设置员工姓名
        {
            strcpy(m_szName, pszName);
        }
        char* GetName()const //获取员工姓名
        {
            return (char*) m_szName;
        }
        void OutputName() //输出员工姓名
        {
            cout << "CEmployee-->员工姓名: " << m_szName << endl;
        }
    };
    
    class COperator : public CEmployee //定义一个操作员类,从 CEmployee 类派生而来
    {
    private:
        char m_szPassword[MAXLEN]; //定义密码
    public:
        COperator() //构造函数
        {
            memset(m_szPassword, 0, MAXLEN);
        }
        void SetPassword(const char* pszPassword) //设置密码
        {
            strcpy(m_szPassword, pszPassword);
        }
        char* GetPassword()const //获取密码
        {
            return (char*) m_szPassword;
        }
        bool Login() //定义登录方法
        {
            if (strcmp(m_szName, "MR")==0 //比较用户名
                && strcmp(m_szPassword, "KJ")==0) //比较密码
            {
                cout << "登录成功!" << endl; //输出信息
                return true; //设置返回值
            }
            else
            {
                cout << "登录失败!" << endl; //输出信息
                return false; //设置返回值
            }
        }
        void OutputName() //输出员工姓名
        {
            cout << "COperator-->员工姓名: " << m_szName << endl;
        }
    };
    
    int main(int argc, char* argv[])
    {
        COperator Operator;
        Operator.SetName("sk");
        Operator.OutputName();
        return 0;
    }
    

    上述代码中 CEmployee 类和 COperator 类都定义了一个 OutputName() 方法。在 main 函数中执行 Operator.OutputName(); 语句将访问的是子类(COperator 类)中的方法,请对该语句进行修改,使其能够访问父类(CEmployee 类)中的 OutputName() 方法。


    在本题中,父类 CEmployee 定义了一个 OutputName() 公有方法。子类 COperator 又定义了一个 OutputName() 方法。那么在子类中将存在两个 OutputName() 方法。默认情况下,子类对象调用的 OutputName 方法将是子类中定义的方法。如果需要访问父类中的方法需要进行强制类型转换。
    例如:

    ((CEmployee)Operator).OutputName();
    

    5. 构造函数和析构函数的调用顺序

    请写出下面代码的运行结果。

    #include <iostream>
    
    using namespace std;
    
    class A
    {
    public:
        A()
        {
            cout << "A 构造函数被调用!" << endl;
        }
        ~A()
        {
            cout << "A 析构函数被调用!" << endl;
        } 
    };
    
    class B : public A
    {
    public:
        B()
        {
            cout << "B 构造函数被调用!" << endl;
        }
        ~B()
        {
            cout << "B 析构函数被调用!" << endl;
        } 
    };
    
    class C : public B
    {
    public:
        C()
        {
            cout << "C 构造函数被调用!" << endl;
        }
        ~C()
        {
            cout << "C 析构函数被调用!" << endl;
        } 
    };
    
    int main(int argc, char* argv[])
    {
        C object;
        return 0;
    }
    

    输出结果为:

    A 构造函数被调用!
    B 构造函数被调用!
    C 构造函数被调用!
    C 析构函数被调用!
    B 析构函数被调用!
    A 析构函数被调用!
    
    

    类 C 继承自类 B,而类 B 又继承自类 A 。当构建一个 C 对象时,将至顶向下执行基类的构造函数,最后执行自身的构造函数。因此,本题中将首先调用类 A 的构造函数,然后调用类 B 的构造函数,最后调用类 C 的构造函数。当 C 类对象释放时,将至下向上执行析构函数。本题中将首先调用 C 类的析构函数,然后调用 B 类的析构函数,最后调用 A 类的析构函数。

    6. 子类和父类的关系

    下面有关基类与其派生类的叙述中,正确的是:
    A.派生类对象不能赋给基类对象
    B.派生类对象的地址不能赋给其基类的指针变量
    C.基类对象不能赋给派生类对象
    D.基类对象的地址能赋给其派生类的指针变量


    选 C

    子类在继承基类时,通常会额外添加一些属性或方法。也就是子类除了具有基类的功能外,还添加了一些自己的功能。将子类对象赋值给基类对象是完全合法的,因为基类能够访问到它所定义的方法。与之相反,将一个基类赋值给子类对象是非法的,因为子类具有基类不具备的行为。上述描述中选项 A 是错误的,选项 C 是正确的。选项 B 和选项 D 围绕的对象的地址赋值。这其实与对象间的赋值原理是相同的。子类对象的地址是可以赋值给基类指针对象的,而基类对象的地址是不能够赋值给子类指针对象的。所以选项 B 和选项 D 都是错误的。

    7. 动态绑定

    请写出下面代码的运行结果。

    #include <iostream>
    
    using namespace std;
    
    class Shape
    {
    public:
        Shape()
        {
            cout << "Shape was invoked!" <<endl;
        }
        virtual void Draw()
        {
            cout << "Draw Shape!" << endl;
        }
    };
    
    class Circle : public Shape
    {
    public:
        Circle()
        {
            cout << "Circle was invoked!" << endl;
        }
        void Draw()
        {
            cout << "Draw Circle!" << endl;
        }
    };
    
    int main(int argc, char* argv[])
    {
        Shape *shape = new Circle(); //定义一个基类指针对象
        shape->Draw(); //调用 Draw 方法
        delete shape; //释放对象
        return 0;
    }
    

    输出结果是:

    Shape was invoked!
    Circle was invoked!
    Draw Circle!
    

    本题中关键代码是 main 函数中的前两行语句。

    Shape *shape = new Circle();
    shape->Draw();
    

    第一行语句定义了一个 Shape 类型的指针对象,但是调用的是子类的构造函数构建对象。第二行语句调用 Draw 方法。由于 Shape 类中的 Draw 方法为虚方法(virtual),所以在执行 shape->Draw();语句时将采用动态绑定的机制,也就是根据运行时 shape 对象的实际类型来确定具体调用哪一个方法。在本题中,将调用 Circle 类的 Draw 方法,因为 shape 对象是通过 Circle 类的构造函数创建的。此外,还需要注意一点,就是调用 Circle 类的构造函数时,会先调用父类 Shape 的构造函数,然后再调用 Circle 类的构造函数。

    8. 简述虚函数的用法和作用

    在定义类的成员函数时,如果在函数前使用 virutal 关键字,表示该成员函数为虚函数。虚函数采用动态绑定的机制,当调用虚函数时,它会根据运行时对象的实际类型来确定具体调用哪个函数,而不是根据对象定义时的数据类型来确定。虚函数是现实多态性的最佳方式。

    注意:父类中定义为虚方法,子类中重新定义该方法( 函数名和参数列表相同)时,则该方法永远是虚方法,无论是否使用 virtual 关键字。此种情况就不是方法的隐藏了,而是方法的改写或覆盖。

    9. 一个父类写了一个 virtual 函数,如果子类覆盖它的函数不加 virtual,也能实现多态? 在子类的空间里, 有没有父类的这个函数,或者父类的私有变量 ?

    在父类中定义一个虚函数,子类在改写该函数时,可以不加 virtual 关键字,它默认也是虚函数,不影响多态的实现。在子类的空间里有父类的虚函数,也有父类的所有变量(静态成员变量除外)。

    注意:此时所说的情况是子类的空间里,子类可以通过父类的成员函数访问父类的私有成员,所以子类的空间中是有父类的虚函数和所有变量。但是父类中的静态成员变量在内存中只有一份,所以子类的空间中是没有父类的静态成员变量的。

    10. 隐藏父类重载的所有方法

    请指出下面代码中的错误,并说明原因。

    #include <iostream>
    
    using namespace std;
    
    class Animal
    {
    public:
        void Cry() 
        {
            cout << "Unname animal can cry!" << endl;
        }
        void Cry(char* szName) 
        {
            cout << szName << " animal can cry!" << endl;
        }
    };
    
    class Bird : public Animal 
    {
    public:
        void Cry()
        {
            cout<<"Bird can cry!" << endl;
        }
    };
    
    int main(int argc, char* argv[])
    {
        Bird Bird;
        Bird.Cry("bird");
        return 0;
    }
    

    语句 Bird.Cry("bird"); 编译错误。

    上述代码中子类 Bird 隐藏了父类中的 Cry() 方法。但是在父类中有两个重载版本的 Cry() 方法。Bird 类将隐藏所有父类同名的方法,,因此语句 Bird.Cry("bird"); 试图访问父类中的 void Cry(char* szName) 重载方法时出现编译错误。

    当子类隐藏父类中的方法时,会连同父类中同名的重载方法一同隐藏,因此,子类对象无法访问父类中重载的其他方法。

    11. 类成员函数的重载、覆盖和隐藏区别

    • 重载:是指在同一个类中有多个同名的方法,这些方法参数类型、参数个数或者方法属性(const 属性)不同。
    • 覆盖:是指父类中定义了一个虚方法,子类中又重新定义了的该方法。通过覆盖父类的虚方法,可以实现动态绑定。
    • 隐藏:是指子类重新定义了父类中的非虚方法,此时,子类中的方法将隐藏父类中的方法。有两种情况会发生隐藏:
      • 如果子类的函数与父类的函数同名,但是参数不同。此时,不论有无 virtual 关键字,父类的函数将被隐藏。
      • 如果子类的函数与父类的函数同名,并且参数也相同,但是父类函数没有 virtual 关键字。此时,父类的函数被隐藏。

    12. const 对象不能够调用非 const 方法

    13. 在程序中,重载成员函数可以实现静态多态性,而虚函数可以实现动态多态性。它们在范围上有着明显的不同。重载成员函数发生在同一个类中;虚函数需要在父类和子类中才能得到体现。

    14. 析构函数为什么要设计为虚函数?

    析构函数设计为虚函数,在动态绑定时可以保证子类的析构函数能够被调用,有效阻止了内存泄露的产生。

    考虑这样一种情况:定义一个基类类型的指针,调用子类的构造函数为其构建对象,当对象释放时,先调用父类的析构函数还是先调用子类的析构函数,再调用父类的析构函数呢?答案是如果析构函数是虚函数,则先调用子类的析构函数,然后再调用父类的析构函数,如果析构函数不是虚函数,则只调用父类的析构函数。可以想象,如果在子类中为某个数据成员在堆中分配了空间,父类中的析构函数不是虚方法,上述情况将使子类的析构函数不会被调用,其结果是对象不能被正确地释放,导致内存泄露的产生。

    15. 动态多态的两个必要条件

    • 父类需要定义虚函数,子类改写该函数
    • 定义一个基类指针,调用子类构造函数构建对象

    多态性分为静态多态性和动态多态性两种。其中静态静态性是指在编译期间确定具体执行哪一项操作,它主要是通过方法重载和运算符重载来实现的;动态多态性是指在运行时确定具体执行哪一项操作。它主要是通过虚函数来实现的。

    16. 类占用的内存空间

    关于 a 的定义,请判断 sizeof(a) 的结果。

    class a
    {
    public:
        virtual void funa( );
        virtual void funb( );
        void func( );
        static void fund( );
        static int si;
    private:
        int i;
        char c;
    };
    

    sizeof(a) = 12

    本题中虚拟方法表指针占 4 个字节,i 成员占 4 个字节,c 成员占 1 个字节。但是由于字节对齐,c 成员当前 “ 索引位置 ” 是 9 不是 4 的整数倍,需要额外在分配 3 个字节空间。因此 sizeof(a) 的结果为 12。

    • 如果类中含有虚方法,则编译器需要为类构建虚拟方法表,类中需要有一个指针,指向这个虚拟方法表的地址。在 32 位的系统中,它占用 4个字节。
    • 类中的静态成员是被类所有实例所共享的,它不计入 sizeof 计算的空间。
    • 类成员采用字节对齐的方式分配空间。

    17. 类的继承和多态

    以下程序的输出结果是什么?

    #include <iostream>
    
    using namespace std;
    
    class A
    {
    public:
        void f(void)
        {
            cout << "A::f" << " ";
        }
        virtual void g(void)
        {
            cout << "A::g" << " ";
        }
    };
    
    class B : public A
    {
    public:
        void f(void)
        {
            cout << "B::f" << " ";
        }
        void g(void)
        {
            cout << "B::g" << " ";
        }
    };
    
    int main()
    {
        A* pA = new B;
        pA->f();
        pA->g();
        B* pB = (B*)pA;
        pB->f();
        pB->g();
        return 0;
    }
    

    输出结果为:A::f B::g B::f B::g

    main 函数中首先定义了一个类 A 的指针对象,调用子类 B 的构造函数进行构建。语句 pA->f(); 将调用类 A 中的 f 方法,因为类 A 中的 f 方法是普通方法,不是虚方法,编译器将根据 pA 定义时的类型(类 A)确定调用哪一个类的方法。 pA->g(); 语句将调用类 B 中的 g 方法,因为类 A 中的 g 方法为虚方法,编译器将根据 pA 运行时的类型(由类 B 的构造函数构建)来确定调用哪一个类的 g 方法。接着又定义了一个 B 指针对象 pB,将其指向 pA 对象。语句 pB->f(); 将调用类 B 的 f 方法,因为 f 是普通方法,pB 定义的类型是 B 指针类型。语句 pB->g(); 调用类 B 中的 g 方法。

    18. 类的多层继承

    请写出下面程序的运行结果:

    #include <iostream>
    
    using namespace std;
    
    class A
    {
    public:
        virtual void print(void)
        {
            cout << "A::print()" << endl;
        }
    };
    
    class B : public A
    {
    public:
        virtual void print(void)
        {
            cout << "B::print()" << endl;
        }
    };
    
    class C : public B
    {
    public:
        virtual void print(void)
        {
            cout << "C::print()" << endl;
        }
    };
    
    void print(A a)
    {
        a.print();
    }
    
    int main()
    {
        A a, *pa, *pb, *pc;
        B b;
        C c;
        
        pa = &a;
        pb = &b;
        pc = &c;
        
        a.print();
        b.print();
        c.print();
        
        pa->print();
        pb->print();
        pc->print();
        
        print(a);
        print(b);
        print(c);
        return 0;
    }
    

    输出结果为:

    A::print()
    B::print()
    C::print()
    A::print()
    B::print()
    C::print()
    A::print()
    A::print()
    A::print()
    

    第一组输出语句 a.print();b.print();c.print(); 的输出结果为 A::print() B::print() C::print() 。因为对象 a、b、c 的类型分别为类 A、类 B 和类 C。它们会各自调用各自类中定义的 print 方法。

    第二组输出语句 pa->print();pb->print();pc->print(); 的输出结果为 A::print() B::print() C::print() 。因为pa、pb 和 pc 对象分别指向类 A 对象 a、类 B 对象 b 和类 C 对象 c。

    第三组输出语句是本题的难点,也是本题的精华 print(a);print(b);print(c); 。他们都调用 print 函数来输出语句,而 print 函数包含了一个类A类型的参数a,该函数采用值传递方式。语句 print(a); 执行结果为 A::print() ,这没有任何疑问。关键是语句 print(b);print(c); 的执行结果。调用 print(b); 语句时,由于 print 函数采用值传递,将调用类 A 的拷贝构造函数(系统默认提供)根据实际参数 b 来构建类 A 对象。在 print 函数体中参数 a 的实际类型为 A。因此调用 print(b); 语句输出结果为A::print()print(c); 语句也同样输出 A::print()

    如果在本题中将 print 函数修改为引用方式传递,例如:

    void print(A &a)
    {
        a.print();
    }
    

    则第三组的输出结果为:A::print() B::print() C::print()

    19. 怎样定义一个纯虚函数?含有纯虚函数的类称为什么?

    纯虚函数的定义是在定义虚函数的基础上,在虚函数末尾添加 “ = 0 ” ,同时函数没有函数体,也就是没有函数的实现部分。含有纯虚函数的类被称为抽象类,不能够实例化一个抽象类,即不能定义抽象类对象。

    在 C++语言中,除了能够定义虚函数之外,还可以定义纯虚函数,也就是通常所说的抽象函数。一个包含纯虚函数的类被称为抽象类,抽象类是不能够被实例化的,通常用于实现接口的定义。
    例如:

    #define MAXLEN 128  //定义一个宏
    class CEmployee  //定义一个抽象类
    {
    protected:
        int m_nID;  //定义员工 ID
        char m_szName[MAXLEN];  //定义员工姓名
        char m_szDepart[MAXLEN];  //定义所属部门
    public:
        virtual void OutputName() = 0;  //定义抽象方法
    };
    

    上述代码中为 CEmployee 类定义了一个纯虚方法 OutputName 。纯虚方法的定义是在虚方法定义的基础上在末尾添加 “ = 0 ” 。对于包含纯虚方法的类来说,是不能够实例化的。抽象类通常用于作为其他类的父类,从抽象类派生的子类如果不是抽象类,则子类必须实现父类中的所有纯虚函数。
    例如:

    class COperator : public CEmployee  //定义一个操作员类,从 CEmployee 类派生而来
    {
    public:
        COperator()
        {
            strcpy(m_szName, "MR");
        }
        virtual void OutputName()  //实现纯虚方法
        {
            cout << "操作员姓名: " << m_szName << endl;  //输出操作员姓名
        }
    };
    
    class CSystemManager : public CEmployee  //定义一个管理类,从 CEmployee 类派生而来
    {
    public:
        CSystemManager()
        {
            strcpy(m_szName, "MRSoft");
        }
        virtual void OutputName()  //实现纯虚方法
        {
            cout << "系统管理员: " << m_szName << endl;  //输出操作员姓名
        }
    };
    

    上述代码从 CEmployee 类派生了两个子类,分别为 COperator 和 CSystemManager 。这两个类分别实现了父类的纯虚方法 OutputName 。下面定义一个 CEmployee 类的指针,然后分别利用 COperator 类的构造函数和 CSystemManager 类的构造函数创建对象,并调用 OutputName 方法。

    int main()
    {
        CEmployee *pWorker;  //定义 CEmployee 类型指针对象
        pWorker = new COperator();  //调用 COperator 类的构造函数为 pWorker 赋值
        pWorker->OutputName();  //调用 COperator 类的 OutputName 方法
        delete pWorker;  //释放 pWorker 对象
        pWorker = NULL;  //将 pWorker 对象设置为空
        //调用 CSystemManager 类的构造函数与为 pWorker 赋值
        pWorker = new CSystemManager();
        pWorker->OutputName();  //调用 CSystemManager 类的 OutputName 方法
        delete pWorker;  //释放 pWorker 对象
        pWorker = NULL;  //将 pWorker 对象设置为空
        return 0;
    }
    

    运行结果为:

    操作员姓名: MR
    系统管理员: MRSoft
    

    在抽象类中也可以定义普通的数据成员和成员函数,但是不能够实例化抽象类。一个类无论有多少个方法,只要有一个方法是抽象方法(纯虚函数),那么这个类就是抽象类。

    20. 什么是多继承?它的格式是什么?

    多继承是指一个子类能够从多个类派生,也就是它可以同时具有多个父类。它的语法格式与单继承类似。只是可以指定多个父类。
    例如:

    class CWaterBird : public CBird, public CFish
    

    C++语言除了支持单继承外,还支持多继承,即允许一个子类同时从多个类派生。下面通过一个例子来介绍多继承的设计过程。我们需要设计一个鸟类,它具有飞翔功能,然后设计一个鱼类,它具有水里游的功能。如果我们设计既可以飞翔,又可以在水中游的水鸟类,则可以直接从鸟类和鱼类派生。

    #include <iostream>
    
    using namespace std;
     
    class CBird  //定义一个鸟类 
    {
    public:
        void FlyInSky()
        {
            cout << "鸟能够在天空中翱翔!" << endl;
        }
        void Breath()
        {
            cout << "鸟能够呼吸!" << endl;
        }
    };
    
    class CFish  //定义一个鱼类 
    {
    public:
        void SwimInWater()
        {
            cout << "鱼能够在水中游!" << endl;
        }
        void Breath()
        {
            cout << "鱼能够呼吸!" << endl;   
        } 
    };
    
    class CWaterBird : public CBird, public CFish  //定义水鸟类 
    {
    public:
        void Action()
        {
            cout << "水鸟既能飞又能游!" << endl; 
        }
    };
    
    int main()
    {
        CWaterBird waterbird;
        waterbird.FlyInSky();
        waterbird.SwimInWater();
        waterbird.CBird::Breath();
        waterbird.CFish::Breath();
        return 0;
    }
    

    运行结果为:

    鸟能够在天空中翱翔!
    鱼能够在水中游!
    鸟能够呼吸!
    鱼能够呼吸!
    

    上述代码定义了鸟类 CBird,定义了鱼类 CFish、然后从鸟类和鱼类派生了一个子类水鸟类 CWaterBird。水鸟类自然继承了鸟类和鱼类的所有共有和受保护的成员。因此 CWaterBird 类对象能够调用 FlyInSky 和 SwimInWater 方法。在 CBird 类中提供了一个 Breath 方法,在 CFish 类中同样提供了 Breath 方法,如果 CWaterBird 类对象调用 Breath 方法,需要在 Breath 方法前具体指定类名。
    例如:

    Waterbird.CFish::Breath();  //调用 CFish 类的 Breath 方法
    Waterbird.CBird::Breath();  //调用 CBird 类的 Breath 方法
    

    21. 虚继承的作用

    在多继承中,子类可以同时拥有多个父类,如果这些父类还有相同的父类(祖先类),那么在子类中就会有两份祖先类。例如,类 B 和类 C 均继承于类 A,如果类 D 派生于类 B 和类 C,那么类 D 中将有两份类 A。为了防止在多继承中,子类存在重复的父类情况,可以在父类继承时使用虚继承。即在类 B 和类 C 继承类 A 时使用 virtual 关键字
    例如:

    class B : virtual public A
    class C : virtual public A
    

    在程序开发过程中,多继承虽然带来了很多方便,但是很少有人愿意使用它,因为多继承会带来很多复杂的问题,并且多继承能够完成的功能,通过单继承同样可以实现。因此,在开发应用程序时,如果能够使用单继承实现,尽量不要使用多继承。

    22. 设计模式

    设计模式汇总

    相关文章

      网友评论

          本文标题:继承和多态

          本文链接:https://www.haomeiwen.com/subject/fopzzttx.html