继承和多态

2017-04-29  本文已影响0人  MinoyJet

继承和多态

1. 继承的优缺点

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. 类成员函数的重载、覆盖和隐藏区别

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个字节。

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. 设计模式

设计模式汇总

上一篇下一篇

猜你喜欢

热点阅读