首先说说父类子类间的赋值兼容问题,什么意思呢,就是子类对象可以当做父类对象使用的兼容性问题。
子类对象可以直接赋值给父类对象。
子类对象可以直接初始化父类对象。
父类指针可以直接指向子类对象。
父类引用可以直接引用子类对象。
我们通过小例子说明这几个规则,首先定义一个父类一个子类,他们之间有继承关系。
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);
}
};
按照我们前面说的,子类中有与父类中的同名函数,所以子类中的同名函数会覆盖父类中的同名函数,除非使用作用域分辨符来引用。
首先测试前面的规则是否可用:
Parent p;
Child c;
p = c; //子类对象直接赋值给父类对象
Parent p1(c); //子类对象初始化父类对象
Parent& rp = c; //父类引用可以直接引用子类对象
Parent* pp = &c; //父类指针可以直接指向子类对象
我们发现编译器是能够通过编译的,表明规则是可用的,父类和子类之间是存在赋值兼容性的。
问题就是在不断的探索中发现的,我们执行下面的代码;
rp.mi = 100;//使用父类引用
rp.add(5);
rp.add(10, 10);
看看编译器能否通过编译,惊讶的发现编译竟然没错,那么问题来了,子类中并没有带有一个参数和带有两个参数的add函数,说好的会同名覆盖呢?为什么不报错?
再看看执行这几条代码情况会怎么样?
pp->mv = 1000;//使用父类指针
pp->add(1, 10, 100);
失望的是,编译结果报错了,这究竟是为什么?
通过了解终于知道了原因:
当父类指针(引用)指向(替代)子类对象时:
子类对象退化成父类对象。
此时父类指针(引用)只能访问父类中定义的成员。
因此也可以直接访问子类中被覆盖的父类成员。
在继承中有一个特殊的同名函数,因为在子类中可以重定义父类中已经存在的成员函数,这种重定义发生在继承中,叫做函数重写,函数重写是同名覆盖的一种特殊情况。
但是有必要定义函数重写吗?当然有必要,当我们定义了一个子类对象并需要打印子类对象特有的信息时,就可以对函数重写,要是不进行重写,那定义的是子类对象,可打印的却是父类信息。当函数重写遇上赋值兼容后会发生什么呢?
我们在父类和子类里各自定义一个打印自身信息的print函数。
void print()
{
cout << "I'm Parent." << endl;
}
void print()
{
cout << "I'm Child." << endl;
}
定义一个父类对象p和一个子类对象c,分别执行:
p.print();
c.print();
输出了:
I'm Parent.
I'm Child.
我们注释子类中的print函数后再次调用同样的语句看看,
输出了:
I'm Parent.
I'm Parent.
这就是函数重写存在的必要了,函数重写只发生在父类间。
我们又来写一个全局的函数:
void how_to_print(Parent* p)
{
p->print();
}
调用:
how_to_print(&p);
how_to_print(&c);
语句。
结果打印了:
I'm Parent.
I'm Parent.
这是什么情况?程序向表达什么?在全局作用域中,有一个函数,参数是父类指针,函数体为调用父类指针的print函数,分别使用父类对象和子类对象来作为参数,结果打印的都是父类信息,这就是因为父类指针指向的子类对象退化成了父类对象。
编译期间,编译器只能根据指针的类型判断所指向的对象,,根据兼容赋值原则,编译器认为父类指针指向的就是父类对象,,因此编译器只能调用父类的成员函数。
当编译器在对这个函数进行编译时,由于程序没有运行,编译器不可能知道指针p究竟指向的是谁,但是由于对这个函数又没理由报错,所以编译器需要选择一种合理的方式进行继续编译,由于当p为子类对象时编译器也能够通过赋值兼容性的原则将子类对象退化成父类对象,所以编译器就选择调用父类中的print函数,因为这是最安全的做法,因为子类继承父类所有代码,所以子类父类至少都有同一个print函数,这样就保证了不会出错。
这就是由于重写一个函数后同名覆盖带来的问题,做法虽然合理,但是却不是我们期望的,那么怎么解决呢?答案是多态。