c++的多态

来源:互联网 发布:java获取tomcat的端口 编辑:程序博客网 时间:2024/05/29 03:10

1. 子类父类中同名成员的冲突

问:子类中是否可以定义父类中的同名成员变量?

class BaseCls{public:    int a;    void BasePrint()    {        std::cout << "Base::a = " << a << std::endl;    }   };class SubCls : public BaseCls{public:    int a;    void SubPrint()    {        std::cout << "SubCls::a = " << a << std::endl;    }};int main(void){    SubCls s1;    s1.a = 10;      //为基类的a赋值还是为子类的a赋值    s1.SubPrint();    s1.BasePrint();     return 0;}

编译运行:
这里写图片描述

可见,”s1.a = 10;”是为子类中的成员变量a赋值。子类定义的与父类同名的成员变量将会隐藏父类中的同名成员,但是注意,仅仅是隐藏而非销毁,父类中的同名成员依旧存在于子类中,可以通过作用:(作用域分辨符)访问父类中的同名成员。

int main(void){    SubCls s1;    s1.a = 10;              //为子类的a赋值    s1.BaseCls::a = 100;    //为基类的a赋值    s1.SubPrint();    s1.BasePrint();     std::cout << "&s1.a = " << &s1.a << std::endl;    std::cout << "&s1.BaseCls::a = " << &s1.BaseCls::a << std::endl;    return 0;}

编译运行:
这里写图片描述

问:子父类的函数可以发生重载不?

class BaseCls{public:    int Add(int x, int y)    {        return x + y;    }};class SubCls : public BaseCls{public:    int Add(int x, int y, int z)    {        return x + y + z;    }};int main(void){    SubCls s1;    s1.Add(1, 2);     return 0;}

若子类的Add(int x, int y, int z)能和父类的Add(int x, int y)发生重载,那么”s1.Add(1, 2);”将能正确调用。
编译运行:
这里写图片描述

提示说找不到”int SubCls::Add(int, int, int)”这个函数,可见,子类的”int Add(int x, int y, int z)”隐藏了父类的”int Add(int x, int y)”,这种情况称之为函数覆盖。也就是说子父类的不可发生函数重载。重载是发生在相同的作用域的,子父类的函数不在同一作用域自然不可发生重载。

要调用”Add(int x, int y)”函数,还是要加上作用域分辨符:

s1.BaseCls::Add(1, 2); 

问:子类可以定义父类原型相同的函数吗?

class BaseCls{public:    int print()    {        std::cout << "BaseCls::print()" << std::endl;    }};class SubCls : public BaseCls{public:    int print()    {        std::cout << "SubCls::print()" << std::endl;    }};int main(void){    SubCls s1;    s1.print();    return 0;}

编译运行:
这里写图片描述

可见,子类可以定义父类原型相同的函数。要访问父类的原型相同的函数是还是要使用作用域分辨符。子类定义与父类原型相同的函数,称之为函数重写。函数重写是函数覆盖的一种特殊情况。

2. 赋值兼容

问:父类对象可以为子类对象赋值吗?反过来呢?

class BaseCls{public:    int print()    {        std::cout << "BaseCls::print()" << std::endl;    }};class SubCls : public BaseCls{public:    int print()    {        std::cout << "SubCls::print()" << std::endl;    }    int a;};int main(void){    SubCls sub;    BaseCls base;    sub = base;     //父类对象为子类对象赋值       return 0;}

编译报错:
这里写图片描述

反过来,子类对象可以为父类对象赋值:

int main(void){    SubCls sub;    BaseCls base;    base = sub;     //子类对象为父类对象赋值       return 0;}

编译通过:
这里写图片描述

子类继承于父类,拥有父类的所有成员方法和成员变量,所以子类对象可以当做父类对象使用,这便是子父类兼容性原则。它体现在:
(1) 子类对象可以直接初始化、赋值给父类对象
(2) 父类指针可以直接指向子类对象,父类引用可以直接引用子类对象

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

int main(void){    SubCls sub;    BaseCls *p_base = NULL;    p_base = &sub;    p_base->a = 100;    //错误    return 0;}

编译出错:
这里写图片描述

3. 赋值兼容遇上函数重写

如上代码,子类父类中都定义了函数原型相同的”print()”,根据赋值兼容性原则,父类指针可以指向基类对象,当然也可以指向父类对象:

void printType(BaseCls* p){    p->print();}int main(void){    SubCls sub;    BaseCls base;    printType(&sub);    printType(&base);    return 0;}

全局函数printType(BaseCls* p)的p指针参数先后指向子类对象和父类对象,并调用各自的print()函数,从程序设计的思路来看,希望第1次”p->print();”打印”SubCls::print()”,第2次”p_base->print();”打印”BaseCls::print()”。但是根据赋值兼容性原则,父类指针指向子类对象时,子类对象退化为父类对象,只能访问父类中定义的成员。所以不可能通过p_base指针访问到子类的print()函数:
这里写图片描述

两个”p_base->print();”打印的都是父类的print()函数,为什么?
在编译期间,编译器只能根据指针的类型判断所指对象的类型,而p指针究竟是指向父类对象还是子类对象,编译器在编译期间并不知道,但是编译器没理由报错,于是编译器采取了认为最安全的做法,即调用父类的print()函数,因为父类和子类肯定都具有print()函数(可是偏偏子类的print()函数已经重写了)。

程序员想要的效果是对printType()的两次调用,得到的分别是子类和父类的print()函数,怎么搞?这就需要c++的多态。

4. 多态

多态指的是:程序根据实际的对象类型决定调用目标函数,产生的效果为同样的调用语句在实际运行时有多种不同的表现形态。在程序在,具体体现为根据实际的对象类型,调用对应对象的重写函数。

以上面printType()函数代码为例:

p->print();

(1) 当p指向的是父类对象时,调用

int print(){    std::cout << "BaseCls::print()" << std::endl;}

(2) 当p指向子类对象时,调用

int print(){    std::cout << "SubCls::print()" << std::endl;}

c++作为一门面向对象的高级语言,自然支持面向对象的三大特性之一–多态,它是通过虚函数来实现的:
(1) 用virtual声明的函数称为虚函数
(2) 虚函数被重写之后具有多态的特性。
也就是说,要实现c++的子父类的多态,其一是要在父类声明虚函数,其二是子类要重写父类的虚函数。

基于前面的代码,在父类BaseCls的print()函数加上virtual声明(子类的print()函数的virtual可加可不加):

class BaseCls{public:    virtual int print() //声明这是一个虚函数    {        std::cout << "BaseCls::print()" << std::endl;    }};

编译运行:
这里写图片描述

综上,函数重写必须要用多态实现,否则没有重写的意义。