多态与虚函数
多态与虚函数
在引入虚函数概念之前,我们先看这个例子
class Person
{
public:
void BuyTickets()
{
cout << "person: 买票-全价" << endl;
}
protected:
string _name; // 姓名
};
class Student : public Person
{
public:
void BuyTickets()
{
cout << "student: 买票-半价 " << endl;
}
protected:
int _num; //学号
};
int main()
{
Person p, *pp; //定义父类对象p和对象指针pp
Student s; //定义子类对象s
pp = &p; //对象指针pp指向父类对象p
pp->BuyTickets();
pp = &s; //对象指针pp指向子类对象s
pp->BuyTickets();
return 0;
}
这个程序的运行结果不是预想的,为什么会出现这样的情况呢?
在C++中规定:父类的对象指针可以指向它公有继承的子类对象,但是当其指向其公有继承的子类对象时,他只能访问子类中从父类继承来的成员,而不能访问子类中定义的成员。
所以当我们执行语句
pp = &s; //对象指针pp指向子类对象s
pp->BuyTicket();
后,指针pp指向了子类对象s,但是执行语句“pp->BuyTicket();”后,调用的不是子类的派生成员函数BuyTicket();而是子类中从基类继承继承来的同名函数BuyTicket()。
要解决这种情况,我们需要将基类的成员函数前面加上关键字virtual,并在子类中重写。
class Person
{
public:
virtual void BuyTickets()
{
cout << "person: 买票-全价" << endl;
}
protected:
string _name; // 姓名
};
class Student : public Person
{
public:
virtual void BuyTickets()
{
cout << "student:买票-半价 " << endl;
}
protected:
int _num; //学号
};
int main()
{
Person p, *pp; //定义父类对象p和对象指针pp
Student s; //定义子类对象s
pp = &p; //对象指针pp指向父类对象p
pp->BuyTickets();
pp = &s; //对象指针pp指向子类对象s
pp->BuyTickets();
return 0;
}
这样就和我们预想的结果一样了。可是为什么把基类中的成员函数定义为虚函数运行结果就正确了呢?
这是因为,关键字virtual指示C++编译器,函数调用 “pp->BuyTickets();” 要在运行时确定所要调用的函数,即要对该调用进行动态连编。因此,程序在运行时根据指针pp所指向的实际对象,调用该对象的成员函数。
我们把这种使用同一种调用形式,调用的确是同一类族中不同的类的虚函数称为多态。运行时的多态性。可见,虚函数可使C++支持运行时的多态性。
多态形成的条件:(协变例外)
1、虚函数的重写(virtual + 函数完全相同)
2、父类的指针或引用(指向父类调父类,指向子类调子类)
协变:函数返回值可以不相同,返回值是父子继承关系的指针或引用就可以。
多态:与类型无关,与对象有关
多态可以划分为:编译时多态和运行时多态。多态的实现和连编这一概念有关。
所谓连编就是把函数名与函数体的程序代码连接(联系)在一起的过程。静态连编就是在编译阶段完成的连编。动态联编是运行阶段完成的连编。
对虚函数定义的几点说明:
- 由于虚函数使用的基础是赋值兼容规则,而赋值兼容规则成立的前提条件是子类从父类公有继承。因此,通过定义虚函数来使用多态性机制时,子类必须从它的父类公有继承。
- 必须首先在父类中定义虚函数。
- 在子类对父类中声明的虚函数重新定义时,关键字virtual不是必须的,因为虚函数属性可从父类中继承得到。虽然如此,但最好还是在子类中使用virtual,这样可以省却用户查看类定义以确定其是否为虚函数的麻烦。
- 一个虚函数无论被公有继承多少次,它仍然保持虚函数的特性。
- 虚函数必须是其所在类的成员函数,而不能是友元函数,也不能是静态成员函数,因为虚函数调用要靠特定的对象来决定该**哪个函数。
- 内联函数不能是虚函数,因为内联函数是不能在运行中动态确定其位置的。即使虚函数在类的内部定义,编译时仍将其看做是非内联的。
- 构造函数不能是虚函数,但是最好把父类的析构函数声明为虚函数。因为父类的指针有可能指向父类对象,也有可能指向子类对象,当父类指针指向子类对象时,只会析构父类对象,不会对子类对象析构,会造成内存泄漏。若把析构函数声明为虚函数,就构成多态,只与对象有关,父类指向子类对象时,就会调子类的析构。
//最好把基类的析构函数定义为虚函数
class A
{
public:
~A()
{
cout << "~A()" << endl;
}
};
class B : public A
{
public:
~B()
{
cout << "~B()" << endl;
}
};
int main()
{
A* p = new B;//父类的指针new出子类对象,
//这样会出现问题,没有调子类的析构函数,会造成内存泄漏
delete p; // p->destructor() + free(p)
//所以必须把基类的析构函数定义为虚函数
return 0;
}
运行结果显示:只析构了父类对象,而没有析构子类对象,这样会造成内存泄漏。
把基类的析构函数定义为虚函数后:
class A
{
public:
virtual ~A()
{
cout << "~A()" << endl;
}
};
class B : public A
{
public:
~B()
{
cout << "~B()" << endl;
}
};
int main()
{
A* p = new B;
delete p; // p->destructor() + free(p)
//所以必须把基类的析构函数定义为虚函数
return 0;
}
这样就会调子类的析构了。
纯虚函数
有时,基类往往表示一种抽象的概念,它并不与具体的事物相联系。这时在基类中将某一成员函数定义为虚函数,并不是基类本身要求,而是考虑到派生类的需要,在基类中预留了一个函数名,具体功能留给派生类根据需要去定义。
纯虚函数是在声明虚函数时被“初始化”为0的函数。
class Person
{
public:
virtual void Display() = 0; // 纯虚函数
protected:
string _name; // 姓名
};
class Student : public Person
{
public:
virtual void Display()//子类必须重写,子类才能实例化出对象
{
cout << "Student" << endl;
}
};
int main()
{
//Person p; //error: “Person”:不能实例化抽象类
Student s;
return 0;
}
- 纯虚函数没有函数体,它后面的“=0”并不表示函数返回值为0,只是起形式上的作用,告诉编译系统“这是纯虚函数”。
- 纯虚函数不具备函数的功能,不能被调用。只有在派生类中重新定义以后,派生类才能实例化出对象。
上一篇: Dijkstra-单源最短路径
下一篇: 最短路。
推荐阅读
-
多态与虚函数
-
C++ --类的默认成员函数是否可以被定义为虚函数
-
C++的虚函数及虚指针
-
JVM(4)——垃圾收集与引用类型(强、软、弱和虚引用)
-
C++ -- 探索虚函数表
-
C#语法-虚方法详解 Virtual 虚函数
-
Oracle函数ROUND与TRUNC 博客分类: Oracle ROUNDTRUNCOracle
-
java中子类与父类中的静态代码块、非静态代码块和构造函数的加载顺序 博客分类: JAVA基础 静态代码块非静态代码块加载顺序
-
java中子类与父类中的静态代码块、非静态代码块和构造函数的加载顺序 博客分类: JAVA基础 静态代码块非静态代码块加载顺序
-
reentrant函数与thread safe函数 博客分类: 综合 thread多线程AIX编程PHP