C++中基类指针指向派生类对象

在看深入浅出MFC这本书时,谈到了C++中基类指针指向派生类对象的情况. 先说结论:

如果你以一个【基类之指针】指向派生类对象, 那么经由该指针(这个基类指针)只能够调用基类中所定义的函数,而不能调用派生类中定义而基类中不存在的函数

我们来看个例子

#include <string.h>
class CEmployee
{
private:
char m_name[30];
public:
CEmployee();
CEmployee(const char* em) { strcpy(m_name,em);}
float computePay(){return 0.0;}

}
class CSales : public CEmployee
{
private:
float m_sale;
public:
CSales(const char* nm) : CEmployee(nm) {m_sale = 0.0;}
void setSales(float sale) {m_sale = sale;}
float computePay(){return 100.00;}
}

// 我们现在来看基类指针
CSales testSale("吴棉"); //派生类对象
CEmployee* pEmployee; //基类指针
CSales* pSale; //派生类指针
pSale = &testSale; //派生类指针指向自己的(派生类)对象
pEmployee = &testSale; //基类指针指向派生类对象

pEmployee->setSales(300.0); //这个语句会报错,pEmployee是基类指针,CEmployee中并没有setSales函数
pSale->setSales(300.0); //正确,这里会成功调用CSales的setSales函数

这里要特别注意,也就是说,虽然基类指针pEmployee和派生类指针pSale都指向同一个对象(派生类), 但却因为指针的原始类型不一样而使得两者之间有了差异,我们再看:
pEmployee->computePay(); //调用CEmployee::computePay()
pSale->computePay();//调用CSales::computePay()

这里可以看到,虽然基类指针pEmployee和派生类指针pSale都指向同一个对象(派生类)CSales,但是它们调用的computePay方法时,调用的却是不同的类的方法。到底应该调用哪个函数,必须视指针的原始类型而定,而与指针实际所指的对象无关



结论:

 

1.  如果你以一个[基类之指针] 指向 [派生类对象], 那么经由该指针你只能调用基类所定义的函数

2. 如果基类和派生类都定义了【相同名称的成员函数】,那么通过对象指针调用该相同名称的成员函数时,到底调的是基类还是派生类中的成员函数,必须由该指针的原始类型决定,而不是由指针实际所指向的对象的类型来定的

 

显然,现实写代码中经常会出现相反的情况,也就是说我们经常需要一个【基类指针】指向不同的【派生类对象】,但该指针调用的是每个派生类自己的同名函数(这个函数也在基类中存在)

怎么才能实现这个功能么?=》 这里就需要用到虚函数了

 我们需要把这个方法定义成虚函数,这样,就可以做到【基类指针】指向不同的 【派生类对象】时,该基类指针调用的是每个派生类自己的函数,而不是基类中的同名函数

Example:

#include <string.h>

class CEmployee
{
   private:
      char m_name[30];
   public:
      CEmployee();
      CEmployee(const char* em) { strcpy(m_name,em);}
      virtual float computePay() {return 0.00;}
   
}
class CSales : public CEmployee
{
   private:
       float m_sale;
   public:
       CSales(const char* nm) : CEmployee(nm) {m_sale = 0.0;}
       void setSales(float sale) {m_sale = sale;}
       virtual float computePay(return 100.00);
}

class CManager : public CEmployee
{
public:
CManager(const char* nm) : CEmployee(nm) {}
virtual float computePay(return 300.00);
}
// 我们现在来看基类指针 CSales testSale("吴棉"); //派生类对象
CManager testManager("张三"); // 派生类对象

CEmployee* pEmployee; //基类指针 pEmployee = &testSale; //基类指针指向派生类对象CSale pEmployee->computePay(); //调用CSale::computePay(), 这里虽然pEmployee是基类指针,但这里指向的不再是基类CEmployee的函数computePay,而是派生类CSales的函数computePay,原因就是我们把函数computePay定义成了虚函数

pEmployee = &testManager; //基类指针指向派生类对象CManager
pEmployee->computePay();
//调用CManager::computePay(), 这里虽然pEmployee是基类指针,但这里指向的不再是基类CEmployee的函数computePay,而是派生类CManager的函数computePay,原因就是我们把函数computePay定义成了虚函数
 

 上面的代码中, 

pEmployee->computePay(); C++编译器在编译时是无法判断它调用的是基类函数还是派生类函数的,必须在运行时才能评估之,这个我们称为后期联编late binding 或者动态联编 dynamic binding
而如果不是虚函数,比如最开始,computePay是non-virtual函数,那么它在编译时期就会转换为一个固定地址的调用了,这个我们称为 前期联编early binding或静态联编satic binding


纯虚函数和抽象类
上面提到了虚函数,我们来看一下
class CShape
{
    public:
        virtual void display() {}
}

这里CShape是个基类,它本身在现实世界中没有实际意义,也不会存在display这个动作。但是为了在它的派生类比如正方形,圆形,长方形等中绘图显示,各个派生类中需要继承实现这个函数,所以我们不得不在基类CShape中加上这么一个虚函数display, 上面,我们把它定义成了一个空函数,什么都不做. 但其实这样是不高明的,因为这个函数根本就不应该被调用(CSharp是个基类,它在现实世界中没有实际意义,也就不会去调用它的display方法), 所以我们最好是根本就不定义它. 但是我们又必须保留一块空间(spaceholder)给它,也就是说不定义但却需要保留一块空间,C++中怎么做到了 =》 纯虚函数, C++提供了所谓的纯虚函数:

class CShape
{
    public:
        virtual void display() = 0; //注意 "=0"
}

纯虚函数不需要定义其实际动作,它的存在只是为了能够在派生类中被重新定义, 只是为了提供一个多态接口。 

特别注意: 拥有纯虚函数的类,就是一种抽象类,它是不能够被实例化的(instantiate).  也就是说,你不能根据它产生一个对象 (我们不能说去产生一个形状为 "Shape"的物体)

如果你实例化抽象类,会产生如下错误的编译信息

error: illegal attempt to instantiate abstract class

 关于抽象类,有一点要注意: 比如现在我们写一个类CCircle, 继承基类(抽象类) CShape, 如果在CCircle中我们没有去改写CShape中的纯虚函数,那么CCircle本身也就成为了一个拥有纯虚函数的类,于是它也成了抽象类

 

posted on 2024-04-02 17:14  新西兰程序员  阅读(98)  评论(0编辑  收藏  举报