前边我们知道,C++类之间有继承关系,子类能够哦继承父类并且拥有父类的属性,也能够定义子类自己的属性,古语就是:青出于蓝而胜于蓝。
但是子类中是否可以定义父类中的同名成员,使用时如何区分该成员是父类中的还是子类中的?下边代码来验证一下
#include <iostream>
using namespace std;
class parent
{
public:
int m_value;
void getValue()
{
cout << "I'm parent, value = " << m_value << endl;
}
};
class child : public parent
{
public:
int m_value; //定义与父类同名的成员
void getValue()
{
cout << "I'm child, value = " << m_value << endl;
}
};
void main()
{
child c;
c.m_value = 12; //访问的是子类中的成员
c.getValue();
c.parent::m_value = 23; //通过parent::作用域符访问父类中的成员
c.parent::getValue();
system("pause");
}
编译输出
从输出结果看
- 子类中可以定义与父类中同名的成员
- 通过子类对象访问与父类中同名的成员,那么子类中的同名成员会隐藏父类的同名成员
- 父类的同名成员依旧存在于子类中,可以通过作用域分辨符(::)访问父类中的同名成员
上边的代码,我们在子类中定义了一个与父类同名同参数的函数(即重写了父类的成员函数),那么父类中的成员函数与子类中的成员函数能不能构成重载关系?我们知道类中的函数可以重载,但是对于父子类之间的成员函数能否重载,我们还未知,在此之前,先来回顾下重载函数需要的条件
- 本质是多个不同的函数
- 函数名和参数列表是唯一的标识
- 必须发生在同一个作用域中
根据几个条件以及上边的代码,我们来看下下边的代码
#include <iostream>
using namespace std;
class parent
{
public:
void fun(int i)
{
cout << "I'm parent, fun(int i) "<< endl;
}
};
class child : public parent
{
public:
void fun(int i, int j)
{
cout << "I'm child, fun(int i, int j) " << endl;
}
};
void main()
{
child c;
c.fun(12, 34); //OK,正常访问成员函数fun(int i, int j)
c.fun(12); //期望访问父类的成员函数fun(int i)
c.parent::fun(12); //OK,使用作用域分辨符(::)访问父类的同名函数
system("pause");
}
编译结果如下
很明显,提示我们没有匹配参数的fun函数使用,也就是说,子类中的函数与父类中的函数不能构成重载
- 子类中的函数将隐藏父类的同名函数
- 子类无法重载父类中的成员函数
- 使用作用域分辨符(::)可访问父类的同名成员函数
- 子类中可以定义与父类完全相同的成员函数
知道了父类与子类之间的函数不能重载,而我们前边又知道,子类是特殊的父类,是直接可以初始化或赋值给父类的,并且父类的指针或引用也可以直接指向子类,那么我们在来看一段代码
#include <iostream>
using namespace std;
class parent
{
public:
void fun()
{
cout << "I'm parent "<< endl;
}
};
class child : public parent
{
public:
void fun()
{
cout << "I'm child " << endl;
}
};
void fun (parent* p) //定义一个全局函数
{
p->fun();
}
void main()
{
parent p;
child c;
fun(&p); //传入父类对象指针,期望输出:I'm parent
fun(&c); //传入子类对象指针,期望输出:I'm child
system("pause");
}
上边的代码中我们在子类中重定义了在父类中已经存在的函数fun(),这种情况在C++中叫函数重写,重写属于同名覆盖的一种特殊情况。
在main函数中,我们分别把父类对象指针与子类对象指针传给全局函数fun(parent* p),我们希望的是传入父类对象指针时,调用的是父类中的fun()函数,传入子类对象指针时,调用的是子类中的fun(),编译执行如下
从输出结果看,貌似跟我们想要的不一样,当我们把子类对象的指针传入时,调用的却依旧是父类的fun()函数。在C++中,使用父类指针或引用指向子类对象时会发生如下变化
- 子类对象退化为父类对象
- 只能访问父类中的成员
- 可以直接访问被子类覆盖的同名成员
在编译期间,编译器只能根据指针的类型判断所指的对象,赋值兼容性原则,编译器会认为父类指针指向的是父类对象,所以也就调用了父类中的同名函数。
要解决这个问题,就需要引入面向对象中的多态,面向对象中多态的概念就是:根据实际的对象类型决定函数调用的具体目标
- 父类指针(引用)指向父类对象,便调用父类中定义的函数
- 父类指针(引用)指向子类对象,便调用子类中定义的重写函数
C++中支持多态的概念,通过virtual关键字对多态进行支持
- 被virtual声明的函数在子类中重写后具有多态特性
- 被virtual声明的函数叫虚函数
有了以上多态的知识,我们来改写下上边的代码
#include <iostream>
using namespace std;
class parent
{
public:
virtual void fun() //使用virtual关键字修饰fun函数,使其重写后具有多态特性
{
cout << "I'm parent "<< endl;
}
};
class child : public parent
{
public:
void fun()
{
cout << "I'm child " << endl;
}
};
void fun (parent* p) //定义一个全局函数
{
p->fun();
}
void main()
{
parent p;
child c;
fun(&p); //传入父类对象指针,期望输出:I'm parent
fun(&c); //传入子类对象指针,期望输出:I'm child
system("pause");
}
我们将父类中的fun()函数修饰为virtual虚函数,使其具有多态特性,编译执行结果如下,这个时候就是我们想要的结果了
多态的意义
- 在程序运行过程中展现出动态的特性
- 函数重写必须实现多态,否则没意义
- 多态是面向对象组件化程序设计的基础特性
从理论上来说,编译过程我们可以分为静态连编与动态连编
- 静态连编:编译期间就能确定具体的函数调用,例如:函数重载
- 动态连编:运行时才能确定具体的函数调用,例如:函数重写
总结
- 函数重写只能发生在父类与子类之间
- C++中通过virtual关键字来是支持多态
- virtual修饰的虚函数可以表现出多态的特性