虚函数
概念
-
虚函数是动态联编的基础,允许函数调用与函数体之间的联系在运行时才建立
- 也就是在运行时才决定如何动作,即所谓的动态联编
-
虚函数是成员函数,而且是非静态成员函数
-
声明:
virtual <类型说明符><函数名>(<参数表>)
-
若某类中一个成员函数被说明为虚函数,则该成员函数在派生类中可能有不同的实现。
- 当使用这个成员函数操作指针或引用所标识对象时,对该成员函数调用采取动态联编方式,即在运行时进行关联或束定
- 动态联编只能通过指针或引用标识对象来操作虚函数。
- 如果采用一般类型的标识对象来操作虚函数,则将采用静态联编方式调用虚函数
-
派生类对基类的虚函数进行替换时,要求派生类中说明的虚函数与基类中的被替换的基函数满足:
- 有相同的参数个数
- 参数类型对应相同
- 返回值类型相同
- 或都返回指针或引用,且,派生类虚函数所返回的指针或引用的基类型,是,基类中被替换的虚函数所返回的指针或引用的基类型的子类型
-
动态联编实现的三个条件
- 要有说明的虚函数
- 调用虚函数操作的是指向对象的指针或对象引用;或,由成员函数调用虚函数
- 子类型关系的建立
-
构造函数中调用虚函数时,采用静态联编,即构造函数调用的虚函数是自己类中实现的虚函数
- 如果自己类中没有实现这个虚函数,则调用基类中的虚函数,而不是任何派生类中实现的虚函数
-
虚函数的作用
- 首先是在基类中的成员函数,并在派生类中被重载
- 使C++支持运行时的多态性;“同一接口,多种方法”
-
定义
-
virtual 函数类型 函数名(形参表) { //函数体 }
-
派生类中重新定义时,其函数原型,包括返回类型、函数名、参数个数、参数类型的顺序,都必须与其基类中的原型完全相同
-
Example
Example 0. 无虚函数→虚函数
/* 一般函数 */
#include <iostream>
using namespace std;
class Base
{
private:
int a, b;
public:
Base(int x, int y) { a = x, b = y; }
void show()
{
cout << "Base--------" << endl;
cout << a << " " << b << endl;
}
};
class Derived : public Base
{
private:
int c;
public:
Derived(int x, int y, int z) : Base(x, y) { c = z; }
void show()
{
cout << "Derived--------" << endl;
cout << "c=" << c << endl;
}
};
int main()
{
Base mb(60, 60), *pc;
Derived mc(10, 20, 30);
pc = &mb;
pc->show();
pc = &mc;
pc->show();
return 0;
}
/*
Base--------
60 60
Base--------
10 20
*/
上例中,C++静态联编机制造成基类对象的指针pc
,首先与基类的成员函数show()
连接在一起,这使得不管指针pc
指向哪个对象,pc->show()
调用的总是基类中的成员函数show()
。
若将pc定义为
Derived
的指针,则指针将与派生类中的成员函数show()
连接在一起。![]()
#include <iostream>
using namespace std;
class Base
{
private:
int a, b;
public:
Base(int x, int y) { a = x, b = y; }
virtual void show() //定义虚函数show()
{
cout << "Base--------" << endl;
cout << a << " " << b << endl;
}
};
class Derived : public Base
{
private:
int c;
public:
Derived(int x, int y, int z) : Base(x, y) { c = z; }
void show() //重新定义虚函数show()
{
cout << "Derived--------" << endl;
cout << "c=" << c << endl;
}
};
int main()
{
Base mb(60, 60), *pc;
Derived mc(10, 20, 30);
pc = &mb;
pc->show();
pc = &mc;
pc->show();
return 0;
}
/*
Base--------
60 60
Derived--------
c=30
*/
Example 2. Grandma
#include <iostream>
using namespace std;
class Grandma
{
public:
virtual void introduce_self()
{
cout << "I am grandma." << endl;
}
};
class Mother : public Grandma
{
public:
void introduce_self()
{
cout << "I am mother." << endl;
}
};
class Daughter : public Mother
{
public:
void introduce_self()
{
cout << "I am daughter." << endl;
}
};
int main()
{
Grandma *ptr;
Grandma g;
Mother m;
Daughter d;
ptr = &g;
ptr->introduce_self();
ptr = &m;
ptr->introduce_self();
ptr = &d;
ptr->introduce_self();
return 0;
}
/*
I am grandma.
I am mother.
I am daughter.
*/
- 程序只在基类
Grandma
中显示定义了introduce_self()
为虚函数- 如果在派生类中,没有virtual显示地给出虚函数声明,系统按以下规则判断成员函数是否为虚函数
- 该函数与基类的虚函数有相同的名称
- 该函数与基类的虚函数有相同的参数个数和相同的对应参数类型
- 该函数与基类的虚函数有相同的返回类型,或满足,赋值兼容规则的指针、引用型的返回类型
- 如果在派生类中,没有virtual显示地给出虚函数声明,系统按以下规则判断成员函数是否为虚函数
Example 3. student
#include <iostream>
#include <string>
using namespace std;
class student
{
protected:
int num;
string name;
float score;
public:
student(int n, string nam, float s)
{
num = n, name = nam, score = s;
}
virtual void display()
{
cout << "num:" << num << endl
<< "name:" << name << endl
<< "score:" << score << endl
<< endl;
}
};
class graduate : public student
{
private:
float pay;
public:
graduate(int n, string nam, float s, float p) : student(n, nam, s), pay(p) {}
void display()
{
cout << "num:" << num << endl
<< "name:" << name << endl
<< "score:" << score << endl
<< "pay:" << pay << endl
<< endl;
}
};
int main()
{
student stud1(1001, "Li", 97.5);
graduate grad1(2001, "Wang", 98.5, 563.5);
student *pt = &stud1;
pt->display();
pt = &grad1;
pt->display();
return 0;
}
/*
num:1001
name:Li
score:97.5
num:2001
name:Wang
score:98.5
pay:563.5
*/
- 虚函数实现的动态多态性:统一类族中不同类的对象,对同一函数调用做出不同的响应
说明
- 通过定义虚函数实现多台机制时,派生类应该从它的基类公有派生。赋值兼容规则成立的前提条件是派生类从其基类公有派生。
- 必须首先在基类中定义虚函数。实际应用中,应该在等级内需要具有动态多态性的几个层次中的最高层类内首先声明虚函数。
- 在派生类对基类声明的虚函数进行重新定义时,关键词virtual可以写也可以不写。
- 使用对象名和点运算符的方式也可以调用虚函数,但是这种调用在编译时进行的是静态联编,它没有充分利用虚函数的特性。只有通过基类指针访问虚函数时残念获得运行时的多态性。
- 一个虚函数无论被公有继承多少次,它仍然保持其虚函数的特性。
- 虚函数必须是其所在类的成员函数,而不能是友元函数,也不能是静态成员函数。因为虚函数调用要靠特定的对象来决定该激活哪个函数。但是虚函数可以在另一个类中被声明为友元函数。
- 内联函数不能是虚函数。因为内联函数是不能在运行中动态确定其位置的。即使虚函数在类的内部定义,编译时仍将其看做是非内联的。
- 构造函数不能是虚函数。因为虚函数作为运行过程中多态的基础,主要是针对对象的,而构造函数是在对象产生之前运行的,因此虚构造函数时没有意义的。
- 析构函数可以是虚函数。而且通常说明为虚函数。
虚析构函数
- 析构函数设置为虚函数后,在使用指针引用时可以动态联编,实现运行时的多态,保证使用基类类型的指针能够调用适当的析构函数针对不同的对象进行清理工作
- 如果一个类的析构函数是虚函数,那么,由它派生而来的所有派生类的析构函数也是虚析构函数,不管它们是够使用了关键词virtual说明
- 虚析构函数的声明:
virtual ~类名()
Example 4.
#include <iostream>
using namespace std;
class Grandma
{
public:
Grandma() {}
virtual ~Grandma()
{
cout << "this is Grandma::~Grandma()" << endl;
}
};
class Mother : public Grandma
{
public:
Mother() {}
~Mother()
{
cout << "this is Mother::~Mother()" << endl;
}
};
int main()
{
Grandma *f;
f = new Mother;
delete f;
return 0;
}
/*
this is Mother::~Mother()
this is Grandma::~Grandma()
*/
虚函数与重载函数的关系
在派生类中重新定义基类的虚函数,是函数重载的另一种形式,但它不同于一般的函数重载
- 普通的函数重载时,其函数的参数个数或参数类型必须有所不同,函数的返回类型也可以不同
- 当重载一个虚函数时,也就是说在派生类中重新定义虚函数时,要去函数名、返回类型、参数个数、参数的类型和顺序与基类中的虚函数原型完全相同
- 如果仅仅返回类型不同,其余均相同,系统会给出错误信息
- 若仅仅函数名相同,而参数的个数、类型或顺序不同,系统将他作为普通的函数重载,这时将丢失虚函数的特性
Example 5. 虚函数与重载函数的关系
#include <iostream>
using namespace std;
class base
{
public:
virtual void func1();
virtual void func2();
virtual void func3();
void func4();
};
class derived : public base
{
public:
virtual void func1();
void func2(int x);
//char func3(); //ERROR
void func4();
};
void base::func1() { cout << "---base func1---" << endl; }
void base::func2() { cout << "---base func2---" << endl; }
void base::func3() { cout << "---base func3---" << endl; }
void base::func4() { cout << "---base func4---" << endl; }
void derived::func1() { cout << "---derived func1---" << endl; }
void derived::func2(int x) { cout << "---derived func2---" << endl; }
void derived::func4() { cout << "---derived func4---" << endl; }
int main()
{
base d1, *bp;
derived d2;
bp = &d2;
bp->func1(); //基类虚函数在派生类中被重载,调用派生类中func1()
bp->func2(); //基类虚函数在派生类中未被重载,调用基类func2()
bp->func4(); //基类一般成员函数,调用基类func4()
return 0;
}
/*
---derived func1---
---base func2---
---base func4---
*/
Example 6. 继承情况下虚函数的调用
#include <iostream>
using namespace std;
class base1
{
public:
virtual void fun() { cout << "---base1---" << endl; }
};
class base2
{
public:
void fun() { cout << "---base2---" << endl; }
};
class derived : public base1, public base2
{
public:
void fun() { cout << "---derived---" << endl; }
};
int main()
{
base1 obj1, *ptr1;
base2 obj2, *ptr2;
derived obj3;
ptr1 = &obj1;
ptr1->fun(); //调用基类成员函数
ptr2 = &obj2;
ptr2->fun(); //调用基类成员函数
ptr1 = &obj3;
ptr1->fun(); //派生类虚函数重载,调用派生类中重载函数
ptr2 = &obj3;
ptr2->fun(); //基类一般成员函数,调用该函数
return 0;
}
/*
---base1---
---base2---
---derived---
---base2---
*/
虚函数的限制
- 只有成员函数才能声明为虚函数。因为虚函数仅适用于有继承关系的类对象,所以普通函数不能声明为虚函数。
- 虚函数必须是非静态成员函数。这是因为静态成员函数不受限于某个对象。
- 内联函数不能声明为虚函数。因为内联函数不能在运行中动态确定其位置。
- 构造函数不能声明为虚函数。多态是指不同的对象对同一消息有不同的行为特性。虚函数作为运行过程中多态的基础,主要是针对对象的,而构造函数是在对象产生之前运行的,因此,虚构造函数是没有意义的。
- 析构函数可以声明为虚函数。析构函数的功能是在该类对象消亡之前进行一些必要的清理工作,析构函数没有类型,也没有参数,和普通成员函数相比,虚析构函数情况略为简单些。
纯虚函数
纯虚函数
- 纯虚函数是一个在基类中说明的虚函数,它在该基类中没有定义,但要求它的派生类中必须定义自己的版本,或重新说明为纯虚函数
- 定义:
virtual <函数类型><函数名>(<参数表>)=0;
- 纯虚函数与一般虚函数成员的原型在书写形式上的不同就在于后面加了
=0
,表明在基类中不用定义该函数,它的实现部分(函数体)留给派生类去做 - 纯虚函数没有函数体,最后的
=0
并不表示函数返回值为0。这是一个声明语句,最后应有分号 - 纯虚函数只有函数的名字,而不具有函数的功能,不能被调用。在派生类中对此函数提供定义后,它才能具备函数的功能,可被调用
- 如果在一个类中,声明了纯虚函数,而在其派生类中没有对该函数定义,则该函数在派生类中仍然为纯虚函数
Example 1.
#include <iostream>
using namespace std;
#define PI 3.14159
class circle
{
protected:
int r;
public:
void setr(int x) { r = x; }
virtual void show() = 0;
};
class area :public circle
{
public:
void show() { cout << "area is " << PI * r * r << endl; }
};
class perimeter :public circle
{
public:
void show() { cout << "perimeter is " << 2 * PI * r << endl; }
};
int main()
{
circle* ptr;
area ob1;
perimeter ob2;
ob1.setr(10);
ob2.setr(10);
ptr = &ob1;
ptr->show();
ptr = &ob2;
ptr->show();
return 0;
}
/*
area is 314.159
perimeter is 62.8318
*/
抽象类
- 如果一个类中至少有一个纯虚函数,则该类为抽象类
- 抽象类只能作为其他类的基类来使用,不能建立抽象类对象,其纯虚函数的实现由派生类给出
- 派生类中必须重载基类中的纯虚函数,否则该派生类仍为抽象类
- 不允许从具体类中派生出抽象类
- 抽象类不能作为参数类型、函数返回类型、显式转换的类型
- 可以声明指向抽象类的指针或引用,此指针可以指向它的派生类,进而实现多态性
- 抽象类的析构函数可以声明为纯虚函数,这时,应该至少提供析构函数的一个实现
- 抽象类中也可以定义普通成员函数,可以通过派生类对象来调用这些不是纯虚函数的函数
Example 0.
#include <iostream>
using namespace std;
#define PI 3.1415926
class Shape
{
protected:
int x, y;
public:
void setValue(int xx = 0, int yy = 0) { x = xx; y = yy; }
virtual void display() = 0;
};
class Rectangle :public Shape
{
public:
void display() {
cout << "the area of rectangle is: " << x * y << endl;
}
};
class Circle :public Shape
{
public:
void display() {
cout << "the area of circle is: " << PI * x * x << endl;
}
};
int main()
{
Shape* ptr[2];
Rectangle rect;
Circle cir;
ptr[0] = ▭
ptr[1] = ○
ptr[0]->setValue(5, 8);
ptr[0]->display();
ptr[1]->setValue(10);
ptr[1]->display();
return 0;
}
/*
the area of rectangle is: 40
the area of circle is: 314.159
*/
Example 1.
#include <iostream>
using namespace std;
class A
{
public:
virtual void f() { cout << "A.f" << endl; }
};
class B :public A
{
public:
void f() { cout << "B.f" << endl; }
};
class C :public B
{
public:
void f() { cout << "C.f" << endl; }
};
int main()
{
A a; a.f();
B b; b.f();
C c; c.f();
A* p;
p = &a; p->f();
p = &b; p->f();
p = &c; p->f();
return 0;
}
/*
A.f
B.f
C.f
A.f
B.f
C.f
*/