#include <iostream>
using namespace std;
class Animal{
public:
void sleep(void)
{
cout << "Animal::sleep() - 动物在睡觉" << endl;
}
};
class Cat:public Animal{
public:
void sleep(void)
{
cout << "Cat::sleep() - 猫在睡觉" << endl;
}
};
int main(int argc, char *argv[])
{
Animal *p1 = new Cat;
//Animal::sleep() - 动物在睡觉
p1->sleep();
//error: invalid conversion from 'Animal*' to 'Cat*' [-fpermissive]
Cat *p2 = new Animal;
return 0;
}
使用基类指针指向子类对象是安全的
p1
指向Cat
类的实例对象时,访问的内存范围不会超出Animal
类的大小,因此也不会超出Cat
类实例对象的大小,即不会访问非法空间。
但是使用p1
调用sleep()
成员方法时,p1
为Animal
类的指针,会去代码区寻找Animal
类中的方法sleep()
,而我们想要的调用的是Cat
类中的sleep()
方法
使用子类指针指向基类对象是不安全的
当p2指向Animal的实例对象时,因为子类的内存大小一定是大于等于父类的,因此p2可能访问到Animal的实例对象后面的内存数据,而访问非法空间是不允许的。为了安全起见,编译器会报错,禁止子类指针指向父类实例化对象。
为了使用基类指针访问子类对象中的成员方法,C++引入了虚函数
Animal类修改如下:
class Animal
{
public:
//虚函数
virtual void sleep(void)
{
cout<< "Animal::sleep() - 动物在睡觉" <<endl;
}
};
当成员函数用virtual修饰成为虚函数时,实例化对象中将产生一个虚基类指针,该指针指向一个虚基类表,该表中存放着类中的虚函数和虚函数对应的函数入口地址。当通过基类指针访问子类中的virtual成员函数时,会去调用虚基类表中的对应函数。
为何要使用基类指针指向子类对象
当函数的形参为基类对象时,传入其子类对象为实参,可以实现一个函数对不同对象的实现多种操作。避免代码的冗杂
#include <iostream>
using namespace std;
class Base
{
public:
virtual void sleep(void)
{
cout<<"父亲在睡觉"<<endl;
}
};
class Son1:public Base
{
public:
void sleep(void)
{
cout<<"Son1在安静的睡觉"<<endl;
}
};
class Son2:public Base
{
public:
virtual void sleep(void)
{
cout<<"Son2在轻度的睡觉"<<endl;
}
};
class Son3:public Base
{
public:
virtual void sleep(void)
{
cout<<"Son3在雨声般的睡觉"<<endl;
}
};
class Son4:public Base
{
public:
virtual void sleep(void)
{
cout<<"Son4在鼾声如雷"<<endl;
}
};
//以基类指针作为函数的参数 函数可以操作该基类派生出的任意子类对象
void sleepFun(Base &ob)
{
ob.sleep();
}
int main(int argc, char *argv[])
{
Son1 ob1;
Son2 ob2;
Son3 ob3;
Son4 ob4;
sleepFun(ob1);
sleepFun(ob2);
sleepFun(ob3);
sleepFun(ob4);
return 0;
}
运行结果:
使用基类指针指向子类对象时,程序结束只能调用父类的析构函数
使用前面的Animal类和Cat类,执行下方代码
void test01()
{
//通过基类 指针、引用 访问子类的成员函数
Animal *p = new Cat;
//调用的子类的sleep
p->sleep();
//出现的问题:只能释放 父类析构
delete p;
}
使用new创建Cat示例对象,先调用Animal的构造函数,再调用Cat类的构造函数。但是使用new动态创建的对象不会自动释放。delete p;时,根据p的类型去调用了Animal类的析构函数,而没有调用子类的析构函数。
将父类的析构函数用virtual修饰,形成虚析构
virtual ~Animal()
{
cout<<"animal析构"<<endl;
}
原理分析:
编译器识别虚析构,将析构函数放入虚基类表,delete基类指针时,将根据子类中维护的虚基类表调用子类的析构函数,调用完子类的析构函数,编译器将自动调用其父类的析构函数。
原理图:
纯虚函数和抽象类
使用基类指针指向子类对象时,通过虚函数调用的实际成员方法是子类自己的方法。那么基类中的方法根本得不到调用,为何不直接不去实现基类的成员函数呢?
为此,C++引入纯虚函数,使基类中的成员函数不需要实现。代码如下:
virtual void sleep(void) = 0;
如果基类中存在纯虚函数,也就意味着代码区的父类方法根本没有得到定义,如果使用该父类实例化对象,也根本找不到成员方法。因此,C++规定一旦类中存在纯虚函数,该类即为抽象类,抽象类不能实例化对象
纯析构函数
和纯虚函数不一样的是,基类中的纯析构函数需要在类外定义,因为子类析构完成后,会自动调用父类的析构函数
代码示例如下:
class Base
{
public:
virtual ~Base()=0;
};
Base::~Base()
{函数体}
总结:
虚函数:virtual修饰函数体(作用于成员函数)
目的:通过基类指针或引用操作子类的方法
class Base
{
public:
virtual my_fun(void)
{
//有函数体;
}
}
纯虚函数:virtual修饰,参数列表后加=0,没有函数体,所在的类为抽象类
目的:为子类提供固定的流程和接口
class Base
{
public:
virtual my_fun(void)=0;
}
虚析构:virtual修饰类中的析构函数
目的:为了解决基类的指针指向派生类对象,并用基类的指针删除派生类对象
class Base
{
public:
virtual ~Base()
{函数体}
}
纯虚析构:virtual修饰函数,参数列表后加=0, 必须实现析构的函数体
目的:用基类的指针删除派生类对象、同时提供固定接口
class Base
{
public:
virtual ~Base()=0;
}
Base::~Base()
{函数体}
补充:
重载:
同一作用域的同名函数、参数个数,参数顺序,参数类型不同
和函数返回值,没有关系
const也可以作为重载条件
int fun(int a){}
int fun(int b,int c){}
int fun(char b,int c){}
重定义(隐藏)
子类继承父类后,重新定义父类(基类)的同名成员(非virtual函数)
class Base{
public:
void fun(int){}
void fun(int,int){}
}
class Son:public Base{
public:
void fun(参数可以不同){}//重定义
}
重写(覆盖)
子类重写父类的virtual函数,函数返回值,函数名字,函数参数,必须和基类中的虚函数一致
class Base{
public:
virtual void fun(int){}
}
class Son:public Base{
public:
virtual void fun(int){}//重写
}