虚函数与函数覆盖
普通的成员函数使用virtual关键字修饰就是虚函数,虚函数的主要用途是函数覆盖,函数覆盖的主要用途是多态。
虚函数的使用需要注意:
- 静态成员函数不能设置为虚函数
- 构造函数不能设置为虚函数,但是析构函数可以设置为虚函数
- 如果声明和定义分离,只需要在声明时使用virtual关键字修饰
- 函数覆盖:虚函数具有传递性,当基类中某一个成员函数设置为虚函数之后,派生类中的同名函数(函数名称相同、参数列表完全相同、返回值类型相关)会自动变为虚函数。此时使用派生类对象调用此函数的效果类似于函数隐藏,但是相比于函数隐藏,函数覆盖支持:
- 多态
- 在C++11中可以使用override关键字验证是否覆盖成功
#include <iostream>
using namespace std;
class Animal
{
public:
virtual void eat() // 虚函数
{
cout << "吃东西" << endl;
}
};
class Dog:public Animal
{
public:
void eat() override
{
cout << "吃骨头" << endl;
}
};
int main()
{
Animal a;
a.eat();
Dog d;
// 类似于函数隐藏
d.Animal::eat();
d.eat();
return 0;
}
多态
首先从字面意思上讲,多态的意思是“多种状态”,可以认为是“一个接口,多种状态”。接口在运行期间,根据传入的参数来决定具体调用的函数,最终采取不同的执行策略。
多态与模板的区别在于,多态虽然也支持多种数据类型,但是不同类型的处理逻辑可能不同;模板对所有类型的处理方式是相同的。
使用多态的条件有三个:
- 要使用公有继承
- 要有函数覆盖
- 基类引用/指针指向派生类对象
#include <iostream>
using namespace std;
class Animal
{
public:
virtual void eat() // 虚函数
{
cout << "吃东西" << endl;
}
};
class Dog:public Animal
{
public:
void eat() override
{
cout << "吃骨头" << endl;
}
};
class Cat:public Animal
{
public:
void eat()
{
cout << "吃鱼" << endl;
}
};
class Husky:public Dog
{
void eat()
{
cout << "吃熊" << endl;
}
};
/**
* @brief 基类引用
*/
void test_dt1(Animal& a)
{
a.eat();
}
/**
* @brief 基类指针
*/
void test_dt2(Animal* a)
{
a->eat();
}
int main()
{
Animal a1;
Dog d1;
Cat c1;
Husky h1;
test_dt1(a1);
test_dt1(d1);
test_dt1(c1);
test_dt1(h1);
Animal* a2 = new Animal;
Dog* d2 = new Dog;
Cat* c2 = new Cat;
Husky* h2 = new Husky;
test_dt2(a2);
test_dt2(d2);
test_dt2(c2);
test_dt2(h2);
delete a2;
delete d2;
delete c2;
delete h2;
return 0;
}
多态原理
拥有虚函数的类,会拥有一份虚函数表,此类中所有的对象共享这一张表,这些对象内部会增加一个隐藏的成员变量:虚函数表指针,指向虚函数表。可以使用sizeof运算符观察此指针的存在
当派生类继承了拥有虚函数的基类后,也会继承虚函数表,如果此时派生类覆盖了基类中的虚函数,则会修改这样表的内容为新的函数内容
如果派生类中新增了新的虚函数,则会在表的尾部新增此函数
每个类型对象的虚函数表指针都指向自己类的虚函数表。因此代码在运行阶段,可以通过查询这个虚函数表,来找到对应的类型应该调用的函数地址。
多态与继承一样,提高了代码的编码效率,牺牲了一部分执行效率
虚析构函数
当基类指针指向派生类对象时,虽然对象还是派生类对象,但是在销毁的时候会按照基类的销毁方式只调用基类的析构函数,不会调用派生类的析构函数,此时可能会导致内存泄漏等未定义的非正常现象出现
#include <iostream>
using namespace std;
class Animal
{
public:
virtual void eat()
{
cout << "吃东西" << endl;
}
~Animal()
{
cout << "Animal 析构函数" << endl;
}
};
class Dog:public Animal
{
public:
void eat() override
{
cout << "吃骨头" << endl;
}
~Dog()
{
cout << "Dog 析构函数" << endl;
}
};
int main()
{
Dog* d = new Dog;
Animal* a = d; // 对象是Dog,但是类型被Animal限制了
cout << d << " " << a << endl;
delete a; // 按照Animal的方式析构,但是实际上是Dog对象
return 0;
}
使用虚析构函数,就可以解决这个问题,虚析构函数可以加入到虚函数表中,在对象销毁时,查询虚函数表依次调用各个继承层次的析构函数
#include <iostream>
using namespace std;
class Animal
{
public:
virtual void eat()
{
cout << "吃东西" << endl;
}
virtual ~Animal()
{
cout << "Animal 析构函数" << endl;
}
};
class Dog:public Animal
{
public:
void eat() override
{
cout << "吃骨头" << endl;
}
~Dog()
{
cout << "Dog 析构函数" << endl;
}
};
int main()
{
Dog* d = new Dog;
Animal* a = d; // 对象是Dog,但是类型被Animal限制了
cout << d << " " << a << endl;
delete a;
return 0;
}
因为在设计一个类时,编译器自动生成的析构函数不是虚函数,此类应用多态时,可能造成内存泄漏的问题。建议把一个类的析构函数设置为虚函数,除非这个类确定不会有派生类
抽象类
概念
如果某个类只表达一些抽象的概念,并不与具体的对象相联系,但是这个类又必须为它的派生类提供一个公共的框架,此时就需要用到抽象类。
如果一个类是抽象类,那么必须有至少一个纯虚函数;
如果一个类有至少一个纯虚函数,那么这个类就是抽象类。
纯虚函数是一种特殊的虚函数,纯虚函数只有函数声明,没有定义
使用方式
方式一
派生类继承抽象类,实现所有的纯虚函数。此时派生类变为普通的类,可以创建对象正常使用
#include <iostream>
using namespace std;
/**
* @brief 形状类
*/
class Shape
{
public:
// 纯虚函数
virtual void perimeter() = 0;
virtual void area() = 0;
virtual ~Shape(){} // 虚析构函数
};
class Circle:public Shape
{
public:
void perimeter()
{
cout << "2πR" << endl;
}
void area()
{
cout << "πR2" << endl;
}
};
int main()
{
// Shape s; 错误
Circle c;
c.area();
c.perimeter();
return 0;
}
方式二
如果派生类B没有把抽象基类A的所有纯虚函数实现,那么这个派生类B也会变为抽象类,直到它的派生类C把剩余的纯虚函数实现,派生类C才可以创建对象
#include <iostream>
using namespace std;
/**
* @brief 形状类
*/
class Shape
{
public:
// 纯虚函数
virtual void perimeter() = 0;
virtual void area() = 0;
virtual ~Shape(){} // 虚析构函数
};
/**
* @brief 多边形
*/
class Polygon:public Shape
{
public:
void perimeter()
{
cout << "∑边长" << endl;
}
};
/**
* @brief 矩形
*/
class Rectangle:public Polygon
{
public:
void area()
{
cout << "w*h" << endl;
}
};
int main()
{
// Shape s; 错误
// Polygon p; 错误
Rectangle r;
r.area();
r.perimeter();
return 0;
}
在实际开发中,如果一个类有抽象基类,通常表示绝大多数接口都在这个抽象基类中规定
多态——抽象类
需要注意的是,抽象类支持多态,所以虽然抽象类没有对象,但是其指针或引用在代码是可以以指针或引用的方式存在的
#include <iostream>
using namespace std;
/**
* @brief 形状类
*/
class Shape
{
public:
virtual void perimeter() = 0;
virtual void area() = 0;
virtual ~Shape(){}
};
class Polygon:public Shape
{
public:
void perimeter()
{
cout << "∑边长" << endl;
}
};
class Rectangle:public Polygon
{
public:
void area()
{
cout << "w*h" << endl;
}
};
void test_dt1(Shape& s)
{
s.area();
s.perimeter();
}
void test_dt2(Shape* s)
{
s->area();
s->perimeter();
}
int main()
{
Rectangle r1;
test_dt1(r1);
Rectangle *r2 = new Rectangle;
test_dt2(r2);
delete r2;
return 0;
}