使用场景:
当使用基类对象的指针指向派生类的对象或使用基类对象的引用作为派生类对象的别名,并通过基类指针或基类引用访问某个与基类同名的成员函数时,需要在基类中将这个同名函数说明为虚函数。
例1:
#include <iostream>
using namespace std;
class A {
private:
int a;
public:
A(int i=1):a(i){}
void print() { cout << a << endl; }
};
class B:public A {
private:
int b;
public:
int c;
B(int j = -1) :b(j),c(6) {}
void print() { cout << b << endl; }
};
int main() {
A aa(10), *pa;
B bb(20), *pb;
pa = &bb;
pa->print();//基类指针指向派生类的对象,但是其指针的属性并没有改变,仍然是一个基类指针,于是只能调用基类的print()函数
//基类指针指向派生类的对象,只能访问派生类对象中的基类部分
system("pause");
return 0;
}
分析:
1.基类指针指向派生类的对象,只能访问派生类对象中的基类部分。
2.基类指针指向派生类的对象,但是其指针的属性并没有改变,仍然是一个基类指针,于是只能调用基类的print()函数。但是这并不是我们希望的结果,因为基类指针已经指向了派生类对象,我们希望pa->print();
调用的是派生类中的print()函数。
解决方法:
class A {
private:
int a;
public:
A(int i=1):a(i){}
virtual void print() { cout << a << endl; }
};
把基类中的print()函数声明为虚函数即可。这样,当使用基类指针指向派生类的对象时,系统会自动用派生类中的同名函数去覆盖基类的虚函数。
如果有时候需要先调用基类中被覆盖的虚函数,那么就可以用“基类名::函数名(…)”来调用基类中被覆盖的虚函数。(如pa->A::print();
)
#include <iostream>
using namespace std;
class A {
private:
int a;
public:
A(int i=1):a(i){}
virtual void print() { cout << a << endl; }
};
class B:public A {
private:
int b;
public:
int c;
B(int j = -1) :b(j),c(6) {}
void print() { cout << b << endl; }
};
int main() {
A aa(10), *pa;
B bb(20), *pb;
pa = &bb;
pa->print();//调用的是派生类的函数
pa->A::print();//调用的是基类被覆盖的虚函数
system("pause");
return 0;
}
例2:
通过这个例子,可以加深对使用场景(动态联编)的理解,这里函数传参的过程涉及到了类型兼容规则。
#include<iostream>
using namespace std;
class B {
public:
virtual void display()const { cout << "B::display()" << endl; }
};
class D1 :public B {
public:
void display()const { cout << "D1::display()" << endl; }
};
class D2 :public D1 {
public:
void display()const { cout << "D2::display()" << endl; }
};
void fun1(B*pb) { pb->display(); }
void fun2(D1*pd1) { pd1->display(); }
void fun3(B&ref) { ref.display(); }
int main() {
B b;
D1 d1;
D2 d2;
fun1(&b);
fun1(&d1);
fun1(&d2);
cout << endl;
fun2(&d1);
fun2(&d2);
cout << endl;
fun3(b);
fun3(d1);
fun3(d2);
system("pause");
return 0;
}
例3:通过虚函数(动态联编)实现多态
#include<iostream>
using namespace std;
class shape {//抽象类(因为类中有纯虚函数),抽象类不可以实例化
protected:
double x, y;
public:
void set(double i, double j) {
x = i;
y = j;
}
virtual void area() = 0;//纯虚函数(没有函数体,放在基类的定义体中)
virtual void display() = 0;//纯虚函数
};
class triangle :public shape {//因为派生类triangle中给出了所有(注意是所有!!)纯虚函数的实现,所以该类不再是抽象类,可以实例化
public:
void area() { cout << 0.5*x*y << endl; }
void display() { triangle::area(); }
};
class rectangle :public shape {//因为派生类rectangle中给出了所有(注意是所有!!)纯虚函数的实现,所以该类不再是抽象类,可以实例化
public:
void area() { cout << x*y << endl; }
void display() { rectangle::area(); }
};
int main() {
triangle t;
rectangle r;
shape *pb1 = &t;//基类指针指向派生类的对象
shape *pb2 = &r;
shape& ref1 = t;//基类的引用作为派生类对象的别名
shape& ref2 = r;
pb1->set(5, 6);
pb2->set(5, 6);
pb1->display();
pb2->display();
cout << endl;
ref1.display();
ref2.display();
system("pause");
return 0;
}
上面的不同对象r,t(来自继承同一基类的不同派生类)接受同一消息(求面积,来自基类的成员函数area()),但是却根据自身情况调用不同的面积公式(执行了不同的行为,它是通过虚函数实现的)。我们可以理解为,继承同一基类的不同派生对象,对来自基类的同一消息执行了不同的行为,这就是多态,它是通过继承和虚函数实现的。而接受同一消息的实现就是基于基类指针或基类引用。
例4:虚析构函数
#include<iostream>
using namespace std;
class B {
public:
~B() { cout << "Base destructor" << endl; }
};
class D :public B {
public:
D() { p = new int(0); }
~D(){ cout << "Derived destructor" << endl; }
private:
int *p;
};
int main() {
B *b;
b = new D();//基类指针指向派生类的对象
delete b;//只调用了基类的析构函数,并没有调用派生类的析构函数,造成了内存泄漏!!!
system("pause");
return 0;
}
分析:B *b;b = new D();delete b;
基类指针指向派生类的对象,delete b;
只调用了基类的析构函数,并没有调用派生类的析构函数,造成了内存泄漏!!!
和例1一样,这说明虽然基类指针指向派生类的对象,但指针本身的属性仍然是基类指针没有改变,系统调用的仍然是基类的析构函数,而我们希望的是调用派生类的析构函数。因此,这里也要对析构函数进行动态联编,即将基类中的析构函数说明为虚函数,这样delete b;
就会调用派生类的析构函数。
修改后的程序:
#include<iostream>
using namespace std;
class B {
public:
virtual ~B() { cout << "Base destructor" << endl; }
};
class D :public B {
public:
D() { p = new int(0); }
~D(){ cout << "Derived destructor" << endl; }
private:
int *p;
};
int main() {
B *b;
b = new D();//基类指针指向派生类的对象
delete b;//动态联编,调用派生类的析构函数
system("pause");
return 0;
}
(首先执行的派生类的析构函数是动态联编的结果,最后执行基类的析构函数是因为要把基类指针b的内存释放)