我们知道,C++中的派生类继承了基类的所有数据成员以及函数:
对于基类数据成员的继承,体现在了占用内存上面。
而对于基类成员函数的继承,则不应该从占用内存的角度区去理解。应是继承对基类成员函数的调用权(子类可以调用父类的公有函数,这种行为体现了经典的面向对象编程,此子类函数或这种做法被称为Template Method,此处Method 套用了Java中的函数method)
根据是否需要在派生类中重新定义(override,覆写)基类的成员函数,可以决定基类成员函数的使用形式:
1)如果不希望derived class中重新定义基类的成员函数,则使用non-virtual函数
2)如果希望在derived class中对它进行重新定义
a)如果基类中已经有对它的默认定义,则使用虚函数。
b)如果基类中的函数因其抽象性而无法定义,则使用纯虚函数。此时所在的类称为抽象类。
从上面可以看出,在使用虚函数的时候,利用了类型兼容规则。
类型兼容规则,是指在需要基类对象的任何地方,都可以使用公有派生类的对象来替代基类对象。所指的替代包括三种情况:
class Base{...}
class Devried:public Base{...}
Base b, *pb;
Devried d;
1)派生类的对象转化为基类的对象。
b = d;
2)派生类的对象初始化基类对象的引用。
Base &rb = d;
3)派生类对象的地址转换为指向基类的指针。
pb = &d;
类型兼容规则的引入,使得基类及其公有派生类的对象,可以使用相同的函数对它们统一进行处理。因为当函数的形参为基类的对象(或引用、指针)时,实参可以是派生类的对象(或指针),而没有必要为每一个类设计一个单独的模块,大大提高了程序的效率。可以说,类型兼容规则是多态性的重要基础之一。
下面是一般虚函数的使用:
#include<iostream>
using namespace std;
class Base1
{
public:
virtual void display() const;
};
void Base1::display()const
{
cout<<"Base1::display()"<<endl;
}
class Base2:public Base1
{
public:
void display() const; //覆盖基类的虚函数
};
void Base2::display()const
{
cout<<"Base2::display()"<<endl;
}
class Derived:public Base2
{
public:
void display() const; //覆盖基类的虚函数
};
void Derived::display() const
{
cout<<"Derived::display()"<<endl;
}
void f(Base1* ptr) //参数为基类对象的指针
{
ptr->display(); //只有通过基类的指针或引用调用虚函数时,才会发生动态绑定
}
int main()
{
Base1 base1;
Base2 base2;
Derived derived;
f(&base1);
f(&base2);
f(&derived);
return 0;
}
输出结果如下:
一般虚函数中还有虚析构函数:
#include<iostream>
using namespace std;
class A
{
public:
~A();
};
A::~A()
{
cout<<"A destructor"<<endl;
}
class B:public A
{
public:
B();
~B();
private:
int* p;
};
B::B()
{
p=new int(0); //体现构造函数的作用
}
B::~B()
{
cout<<"B destructor"<<endl;
delete p; //体现虚构函数的作用
}
void f(A* a) //参数为基类对象的指针
{
delete a; //删除基类
}
int main()
{
A* a = new B(); //通过派生类的构造函数创建基类对象的指针,在java中常用于体现多态性
f(a); //程序输出结果是: A destructor,说明通过基类指针删除派生类对象时,调用的是基类的析构函数,并没有调用派生类的析构函数
//因此,程序只是调用了派生类的构造函数动态分配了数据成员的内存空间,但是这内存空间没有得到释放,而且对象消失后对象成员又不能被本程序继续使用
//这就造成了内存泄漏,对于内存需求量较大,长期连续运行的程序来说,如果持续发生着这样的错误是很危险的,最终将导致因内存不足而引起程序终止
//此时,将基类的析构函数声明为虚析构函数可以解决问题,派生类的析构函数将会被执行
return 0;
}
纯虚函数:
#include<iostream>
using namespace std;
class Base1 //带有纯虚函数的类是抽象类
{
public:
virtual void display() const=0; //纯虚函数,不需要给出定义,为整个类族提供了通用的外部接口语义,派生类中再对同名函数进行具体实现
};
class Base2:public Base1
{
public:
void display() const; //覆盖基类的虚函数
};
void Base2::display()const
{
cout<<"Base2::display()"<<endl;
}
class Derived:public Base2
{
public:
void display() const; //覆盖基类的虚函数
};
void Derived::display() const
{
cout<<"Derived::display()"<<endl;
}
void f(Base1* ptr) //参数为基类对象的指针
{
ptr->display(); //只有通过基类的指针或引用调用虚函数时,才会发生动态绑定,注意 "->"
}
int main()
{
Base2 base2;
Derived derived;
f(&base2);
f(&derived);
return 0;
}
输出结果为:
以上均体现了C++中类的多态性,这也是C++语言常盛不衰的经典之处。
既然提到了“虚”,那就By the way一下虚基类:(不体现多态性)
#include<iostream>
using namespace std;
class Base0
{
public:
int var0;
void fun0()
{
cout<<"Member of Base0"<<endl;
}
};
class Base1:virtual public Base0
{
public:
int var1;
};
class Base2:virtual public Base0
{
public:
int var2;
};
class Derived:public Base1,public Base2
{
public:
int var;
void fun()
{
cout<<"Member of Derived"<<endl;
}
};
int main()
{
Derived d;
d.var0; //直接访问虚基类的数据成员
d.fun0(); //直接访问虚基类的函数成员
return 0;
}
程序输出:
Member of Base0
所以,被声明为虚基类的Base0的成员var0和fun0只有一份副本,而通过两条派生路径继承,更加节省了内存空间。
以上类族的相关类成员的UML图形表示如下:
如果没有声明虚基类,则是:
此时,若通过Derived的对象去访问fun0和var0(要避免造成二义性),则是:
main()
{
Derived d;
d.Base1::var0=1;
d.Base1::fun0();
d.Base2::var0=2;
d.Base2::fun0();
}