28、不一样的C++系列--继承与多态

来源:互联网 发布:php推广链接代码 编辑:程序博客网 时间:2024/06/04 21:02

继承与多态

父子间的同名冲突

首先来看一段代码:

#include <iostream>#include <string>using namespace std;class Parent{public:    int mi;};class Child : public Parent{public:    int mi;};int main(){    Child c;    //这里的mi是Parent中的还是Child中的呢?    c.mi = 100;    return 0;}

编译通过,说明子类可以定义和父类相同的同名成员。

  • 子类可以定义父类中的同名成员
  • 子类中的成员将隐藏父类中的同名成员
  • 父类中的同名成员依然存在于子类中
  • 通过作用域分辨符( : : )访问父类中的同名成员
  • 访问父类中的同名成员
Child c;//子类中的mic.mi = 100;//父类中的mic.Parent::mi = 1000;

再来看一个例子:

#include <iostream>#include <string>using namespace std;//定义一个命名空间Anamespace A{    int g_i = 0;}//定义一个命名空间Bnamespace B{    int g_i = 1;}class Parent{public:    int mi;    Parent()    {        cout << "Parent() : " << "&mi = " << &mi << endl;    }};class Child : public Parent{public:    int mi;    Child()    {        cout << "Child() : " << "&mi = " << &mi << endl;    }};int main(){    Child c;    //向子类的mi成员赋值100    c.mi = 100;        //通过作用域向父类的mi成员赋值1000    c.Parent::mi = 1000;    //打印子类中mi的地址    cout << "&c.mi = " << &c.mi << endl;    //打印子类中mi的内容    cout << "c.mi = " << c.mi << endl;    //打印父类中mi的地址    cout << "&c.Parent::mi = " << &c.Parent::mi << endl;    //打印父类中mi的内容    cout << "c.Parent::mi = " << c.Parent::mi << endl;    return 0;}

输出结果为:

Parent() : &mi = 0x7fff57f57a90Child() : &mi = 0x7fff57f57a94&c.mi = 0x7fff57f57a94c.mi = 100&c.Parent::mi = 0x7fff57f57a90c.Parent::mi = 1000
  • 类中的成员函数可以进行重载
    • 重载函数的本质为多个不同的函数
    • 函数名和参数列表是唯一的标识
    • 函数重载必须发生在同一个作用域中
    • 所以父子之间的同名成员不构成重载

比如像这样:

class Parent{public:    int mi;    void add(int v)    {        mi += v;    }    void add(int a, int b)    {        mi += (a + b);    }};class Child : public Parent{public:    int mi;    void add(int v)    {        mi += v;    }    void add(int a, int b)    {        mi += (a + b);    }    void add(int x, int y, int z)    {        mi += (x + y + z);    }};

代码 Parent类 和 Child类 中有同名的函数add ,但是两个类之间不构成重载,只有Parent类中多个add函数构成重载。

  • 子类中的函数将隐藏父类的同名函数
  • 子类无法重载父类中的成员函数
  • 使用作用域分辨符访问父类中的同名函数
  • 子类可以定义父类中完全相同的成员函数

父子间的赋值兼容

  • 子类对象可以当作父类对象使用(兼容性)
    • 子类对象可以直接赋值给父类对象
    • 子类对象可以直接初始化父类对象
    • 父类指针可以直接指向子类对象
    • 父类引用可以直接引用子类对象

举个例子:

#include <iostream>#include <string>using namespace std;class Parent{public:    int mi;    void add(int i)    {        mi += i;    }    void add(int a, int b)    {        mi += (a + b);    }};class Child : public Parent{public:    int mv;    void add(int x, int y, int z)    {        mv += (x + y + z);    }};int main(){    Parent p;    Child c;    //子类对象可以直接赋值给父类对象    p = c;    //子类对象可以直接初始化父类对象    Parent p1(c);    //父类引用可以直接引用子类对象    Parent& rp = c;    //父类指针可以直接指向子类对象    Parent* pp = &c;    return 0;}

在main函数中进行上述几条的操作都没有出现编译出错。现在进行这样操作:

rp.mi = 100;rp.add(5);        rp.add(10, 10);  

发现可以编译通过,并没有出现同名覆盖的问题。但是如果这样操作:

pp->mv = 1000;pp->add(1, 10, 100);

运行以后就会报错,报错信息如下:

48-1.cpp:51:10: error: no member named 'mv' in 'Parent'     pp->mv = 1000;     ~~  ^48-1.cpp:52:10: error: no matching member function for call to 'add'     pp->add(1, 10, 100);     ~~~~^~~48-1.cpp:16:10: note: candidate function not viable: requires 2 arguments, but 3      were provided    void add(int a, int b)         ^48-1.cpp:11:10: note: candidate function not viable: requires single argument      'i', but 3 arguments were provided    void add(int i)         ^2 errors generated.

信息提示没有找到带有3个参数的add函数。为什么呢?

  • 当使用父类指针(引用)指向子类对象时
    • 子类对象退化为父类对象
    • 只能访问父类中定义的成员
    • 可以直接访问被子类覆盖的同名成员

特殊的同名函数

  • 子类中可以冲定义父类中已经存在的成员函数
  • 这种冲定义发生在继承中,叫做函数重写
  • 函数重写是同名覆盖的一种特殊情况

例如:

class Parent{    public:        void print()        {            cout << "I'm Parent." << endl;        }};//函数重写class Child : public Parent{    public:        void print()        {            cout << "I'm Child" << endl;        }};

假如函数重写和赋值兼容同时出现呢? 就像这样:

#include <iostream>#include <string>using namespace std;class Parent{public:    int mi;    void add(int i)    {        mi += i;    }    void add(int a, int b)    {        mi += (a + b);    }    void print()    {        cout << "I'm Parent." << endl;    }};class Child : public Parent{public:    int mv;    void add(int x, int y, int z)    {        mv += (x + y + z);    }    void print()    {        cout << "I'm Child." << endl;    }};void how_to_print(Parent* p){    p->print();}int main(){    Parent p;    Child c;    how_to_print(&p);    // Expected to print: I'm Parent.    how_to_print(&c);    // Expected to print: I'm Child.    return 0;}

预期输出是 I'm Parent.I'm Child. 。但是实际输出:

I'm Parent.I'm Parent.
  • 问题分析
    • 编译期间,编译器只能根据指针的类型判断所指向的对象
    • 根据赋值兼容,编译器认为父类指针指向的是父类对象
    • 因此,编译结果只可能是调用父类中定义的同名函数

在编译 void how_to_print(Parent* p) 这个函数时,编译器不可能知道指针p究竟指向了什么,但是编译器没有理由报错。于是,编译器认为最安全的做法是调用父类的print函数,因为父类和子类肯定都有相同的print函数。

多态的概念和意义

  • 函数重写回顾
    • 父类中被重写的函数依然会继承给子类
    • 子类中重写的函数将覆盖父类中的函数
    • 通过作用域分辨符( : : )可以访问到父类中的函数

就像这样:

Child c;Parent* p = &c;c.Parent::print();  //从父类中继承c.print();          //从子类中重写p->print();         //父类中定义

虽然程序逻辑是这样,但并不是我们所期望的。面向对象中期望的行为:

  • 根据 实际的对象类型 判断如何调用重写函数
  • 父类指针(引用) 指向
    • 父类对象 则调用 父类 中定义的函数
    • 子类对象 则调用 子类 中定义的重写函数

这里就引出了面向对象中的 多态 的概念:

  • 根据实际的 对象类型决定函数调用 的具体目标
  • 同样的 调用语句 在实际运行时有 多种不同的表现形态

例如:

p->print();

p指向父类对象时,会执行

void print(){    cout << "I'm Parent" << end;}

p指向子类对象时,会执行

void print(){    cout << "I'm Child" << endl;}
  • C++语言直接支持多态的概念
    • 通过使用 virtual 关键字对多态进行支持
    • virtual 声明的函数被重写后具有多态特性
    • virtual 声明的函数叫做虚函数

举个例子:

#include <iostream>#include <string>using namespace std;class Parent{public:    //用 virtual 关键字修饰,则具有多态特性    virtual void print()    {        cout << "I'm Parent." << endl;    }};class Child : public Parent{public:    void print()    {        cout << "I'm Child." << endl;    }};void how_to_print(Parent* p){    // 展现多态的行为    p->print();     }int main(){    Parent p;    Child c;    how_to_print(&p);    // Expected to print: I'm Parent.    how_to_print(&c);    // Expected to print: I'm Child.    return 0;}

执行结果为:

I'm Parent.I'm Child.
  • 多态的意义
    • 在程序运行过程中展现出动态的特性
    • 函数重写必须多态实现,否则没有意义
    • 多态是面向对象组件化程序设计的基础特性

静态联编和动态联编

  • 理论中的概念
    • 静态联编
      • 在程序的编译期间就能确定具体的函数调用。 如:函数重载
    • 动态联编
      • 在程序实际运行后才能确定具体的函数调用。如:函数重写

举个例子:

#include <iostream>#include <string>using namespace std;class Parent{public:    //函数重载  并且用 virtual 关键字修饰    virtual void func()    {        cout << "void func()" << endl;    }    //函数重载  并且用 virtual 关键字修饰    virtual void func(int i)    {        cout << "void func(int i) : " << i << endl;    }    //函数重载  并且用 virtual 关键字修饰    virtual void func(int i, int j)    {        cout << "void func(int i, int j) : " << "(" << i << ", " << j << ")" << endl;    }};class Child : public Parent{public:    //函数重载    void func(int i, int j)    {        cout << "void func(int i, int j) : " << i + j << endl;    }    //函数重载    void func(int i, int j, int k)    {        cout << "void func(int i, int j, int k) : " << i + j + k << endl;    }};void run(Parent* p){    p->func(1, 2);     // 展现多态的特性                       // 动态联编}int main(){    Parent p;    p.func();         // 静态联编    p.func(1);        // 静态联编    p.func(1, 2);     // 静态联编    cout << endl;    Child c;    c.func(1, 2);     // 静态联编    cout << endl;    run(&p);    run(&c);    return 0;}

运行结果为:

void func()void func(int i) : 1void func(int i, int j) : (1, 2)void func(int i, int j) : 3void func(int i, int j) : (1, 2)void func(int i, int j) : 3

小结

  • 子类可以定义父类的 同名成员 ,定义时子类中的成员将 隐藏 父类中的 同名成员
  • 子类和父类 中的函数 不能构成重载关系
  • 使用 作用域分辨符 可以访问父类中的 同名成员
  • 子类对象 可以当做 父类对象 使用
  • 父类指针 可以正确的指向 子类对象
  • 父类引用 可以正确的代表 子类对象
  • 子类中可以重写父类中的 成员函数
  • 函数重写只可能发生在 父类与子类 之间
  • 多态是根据 实际对象的类型 确定调用的 具体函数
  • virtual关键字 是C++中支持 多态 的唯一方式
  • 被重写的 虚函数 可表现出多态的特性
原创粉丝点击