何为虚方法
- 虚方法可以有实现体,若一个方法的声明中含有 virtual 修饰符,则称该方法为虚方法。虚方法就是可以被子类重写的方法,如果子类重写了虚方法,则在运行时将运行重写的逻辑;如果子类没有重写虚方法,则在运行时将运行父类的逻辑. 在使用了 virtual 修饰符后,不允许再有 static、abstract 或者 override 修饰符。
虚方法的特点作用以及区别示例
1.特点:
- 在虚方法前不能再有static,abstract以及override修饰符.
- 不能在声明虚方法的同时指定重写虚方法,也就是说不能在虚方法前再添加override修饰符,因为虚方法在基类中声明的,所以不可再重写.
- 虚方法不可为私有,由于在子类中要被继承,所以不能有private修饰
2.作用:
- 子类可以对父类进行扩展.
- 可以体现cpp的多态性,让程序清楚明了
3.声明:
virtual void func();
一般在类中声明
4.示例对比:
当我们用一个子类继承一个类的时候,如果以普通的方式如下:
class person
{
public:
person(std::string name);
void eat();
protected:
std::string name;
};
person::person(std::string name)
{
std::cout << "你好!我叫" << name << std::endl;
this->name = name;
}
void person::eat()
{
std::cout << name << "开始吃饭" << std::endl;
}
class men : public person
{
public:
men(std::string name);
void eat();
};
men::men(std::string name) : person(name)
{
}
void men::eat()
{
person::eat();
std::cout << name <<"吃了十碗饭!" << std::endl;
}
class Women : public person
{
public:
Women(std::string name);
void eat();
};
Women::Women(std::string name) : person(name)
{
}
void Women::eat()
{
person::eat();
std::cout << name <<"吃了五碗饭!" << std::endl;
}
int main(void)
{
// men m("张三");
// Women wm("如花"); //一般调用方法
person *m = new men("张三");
// men *m = new men("张三"); //用new来进行空间开创
person *fm = new Women("如花");
m->eat(); //因为是用基类来定义一个指针变量,为了效率会只编译基类定义的函数而不是将子类所覆盖的一同编译,所以需要visual
fm->eat();
delete m;
delete fm;
return 0;
}
结果为:
你好!我叫张三
你好!我叫如花
张三开始吃饭
如花开始吃饭
我们可以看到子类的方法并没有实现调用
如果我们使用虚方法,重新声明方法如下
#include<cctype>
#include<iostream>
#include<string>
class person
{
public:
person(std::string name);
virtual void eat();
protected:
std::string name;
};
person::person(std::string name)
{
std::cout << "你好!我叫" << name << std::endl;
this->name = name;
}
void person::eat()
{
std::cout << name << "开始吃饭" << std::endl;
}
class men : public person
{
public:
men(std::string name);
void eat();
};
men::men(std::string name) : person(name)
{
}
void men::eat()
{
std::cout << name <<"吃了十碗饭!" << std::endl;
}
class Women : public person
{
public:
Women(std::string name);
void eat();
};
Women::Women(std::string name) : person(name)
{
}
void Women::eat()
{
std::cout << name <<"吃了五碗饭!" << std::endl;
}
int main(void)
{
// men m("张三");
// Women wm("如花"); //一般调用方法
person *m = new men("张三");
// men *m = new men("张三"); //用new来进行空间开创
person *fm = new Women("如花");
m->eat(); //因为是用基类来定义一个指针变量,为了效率会只编译基类定义的函数而不是将子类所覆盖的一同编译,所以需要visual
fm->eat();
delete m;
delete fm;
return 0;
}
结果如下:
你好!我叫张三
你好!我叫如花
张三吃了十碗饭!
如花吃了五碗饭!
这一次子类覆盖掉基类中同名的方法,有了虚函数,基类指针指向基类对象时就使用基类的成员(包括成员函数和成员变量),指向子类对象时就使用子类的成员。换句话说,基类指针可以按照基类的方式来做事,也可以按照派生类的方式来做事,它有多种形态,或者说有多种表现方式,我们将这种现象称为多态,同一条语句可以执行不同的操作,看起来有不同表现方式,这就是多态,所以体现了多态性.
虚方法的实现过程
一般方法在编译时就静态地编译到了执行文件中,其相对地址在程序运行期间是不发生变化的,也就是写死了的!但是虚函数在编译期间是不被静态编译的,它的相对地址是不确定的,它会根据运行时期对象实例来动态判断要调用的函数。
具体的检查流程如下:
当调用一个对象的方法时,系统会直接去检查这个对象申明定义的类,即申明类,看所调用的方法是否为虚方法。如果不是虚函数,那么它就直接执行该函数。如果有virtual关键字,也就是一个虚方法,那么这个时候它就不会立刻执行该函数了,而是转去检查对象的实例类,在这个实例类里,它会检查这个实例类的定义中是否有同名虚函数的覆盖,如果有,直接执行该(子类)虚函数的覆盖,反之会继续在其父类中寻找到第一次覆盖的同名虚函数,简单来说就是每个父类虚函数表,每一个表内都有该基类中虚函数的指针,如果子类中重写了父类的虚函数,就会在虚函数表中原本记录父类中虚函数的地址覆盖为子类中对应的重定义后的该函数地址,否则不做变动。如果在子类中定义了新的虚函数,则虚函数表中会追加一条记录,记录该函数的地址(虚函数表中是顺序存放虚函数地址的,记住,虽然虚函数表中添加了内容,但是此时对于该类的大小来说并未发生改变,因为始终只有一个指向虚函数表的指针vfptr).
针对上面的的例子,我们来看
class person
{
public:
person(std::string name);
void eat(); //没有定义虚函数
protected:
std::string name;
};
class men : public person
{
public:
men(std::string name);
void eat();
};
class Women : public person
{
public:
Women(std::string name);
void eat();
};
int main(void)
{
person *m = new men("张三");
/*在new后,先检查person类中是否有虚方法,没有则直接调用person中的eat,对应示例的结果*/
person *fm = new Women("如花");
m->eat();
fm->eat();
delete m;
delete fm;
return 0;
}
class men : public person
{
public:
men(std::string name);
virtual void eat();
};
class Women : public person
{
public:
Women(std::string name);
void eat();
};
int main(void)
{
person *m = new men("张三");
/*在new后,先检查person类中是否有虚方法,发现有再查找men和women有没有同名方法覆盖有则调用重载的函数,没有则调用person中的虚函数,对应示例的结果*/
person *fm = new Women("如花");
m->eat();
fm->eat();
delete m;
delete fm;
return 0;
}
注意利用虚方法避免内存泄漏(虚析构器)
在了解虚函数的大致执行过程后,那么如果有一个基类的内部并不像上例那么简单,在内部如果有开辟动态内存,在完成一系列操作后释放堆内存时会不会出现子类的内存泄漏呢。
class Animal
{
char* ap;
public:
Animal()
{
ap = new char;
std::cout << "Animal ctor" << std::endl;
}
virtual void foo()
{
std::cout << "Animal::foo" << std::endl;
}
~Animal()
{
std::cout << "Animal dtor" << std::endl;
delete ap;
}
};
class Dog : public Animal
{
char* dp;
public:
Dog()
{
dp = new char;
std::cout << "Dog ctor" << std::endl;
}
void foo()
{
std::cout << "Dog::foo" << std::endl;
}
~Dog()
{
delete dp;
std::cout << "Dog dtor" << std::endl;
}
};
int main()
{
Animal* pa = new Dog();
pa->foo();
delete pa;
return 0;
结果:
Animal ctor
Dog ctor
Dog::foo
Animal dtor
我们发现结果中缺少了Dog dtor,这是很危险的,如果不及时delete会造成内存泄漏
所以我们使用虚方法就可以解决这样的的问题
#include <iostream>
class Animal
{
char* ap;
public:
Animal()
{
ap = new char;
std::cout << "Animal ctor" << std::endl;
}
virtual void foo()
{
std::cout << "Animal::foo" << std::endl;
}
virtual ~Animal()
{
std::cout << "Animal dtor" << std::endl;
delete ap;
}
};
class Dog : public Animal
{
char* dp;
public:
Dog()
{
dp = new char;
std::cout << "Dog ctor" << std::endl;
}
void foo()
{
std::cout << "Dog::foo" << std::endl;
}
~Dog()
{
delete dp;
std::cout << "Dog dtor" << std::endl;
}
};
int main()
{
Animal* pa = new Dog();
pa->foo();
delete pa;
return 0;
}
结果:
Animal ctor
Dog ctor
Dog::foo
Dog dtor //成功释放堆内存
Animal dtor