1.c++多态的概念以及用途。
1.1虚函数实现多态
通过基类指针只能够访问派生类的成员变量,不能够访问派生类的成员函数。
解决问题的办法:使用虚函数(virtual function),只需要在函数声明前面增加virtual关键字。看下面的代码。
通过基类指针只能够访问派生类的成员变量,不能够访问派生类的成员函数。
解决问题的办法:使用虚函数(virtual function),只需要在函数声明前面增加virtual关键字。看下面的代码。
#include <iostream>
using namespace std;
class people{
public :
people(char *name, int age);
virtual void show();
protected :
char *m_name;
int m_age;
};
people :: people (char *name, int age) : m_name(name), m_age(age){}
void people :: show()
{
cout<<m_name<<", "<<m_age<<" old"<<endl;
}
class student : public people{
public :
student (char *name, int age, float score);
virtual void show();
protected :
float m_score;
};
student :: student (char *name, int age, float score) : people(name, age), m_score(score){}
void student :: show()
{
cout<<m_name<<", "<<m_age<<" old, "<<m_score<<endl;
}
int main()
{
people *p = new people("xiaoli", 22);
p->show();
p = new student("xiaozhang", 16, 95.6);
p->show();
delete p;
return 0;
}
结果:
aoli, 22 old
xiaozhang, 16 old, 95.6
如果不加virtual关键字,则运行结果是:
xiaoli, 22 old
xiaozhang, 16 old
有了虚函数,基类指针可以按照基类的方式来做事,也可以按照派生类的方式来做事,它有多种形态,或者说有多种表现方式,这就是多态(Polymorphism)!
c++中虚函数的唯一用处就是构成多态。
1.2借用引用也可以实现多态
在之前的学习中,可以了解到,指针在本质上是通过指针的方式实现的(引用只是对指针进行了封装,底层还是通过指针实现的)。
把上面的例子改一下:
int main()
{
people p("xiaoli", 22);
student q("xiaozhang", 16, 95.6)
people &rp = p;
people &rq = q;
rp.show();
rq.show();
return 0;
}
实现的功能和上面的例子是一样的。
1.3虚函数的要点
虚函数对于多态具有决定性的作用,有虚函数才能构成多态!
对于虚函数还有以下几点注意点:
1) 只需要在虚函数的声明处加上 virtual 关键字,函数定义处可以加也可以不加。
2) 为了方便,你可以只将基类中的函数声明为虚函数,这样所有派生类中具有遮蔽(覆盖)关系的同名函数都将自动成为虚函数。
3) 当在基类中定义了虚函数时,如果派生类没有定义新的函数来遮蔽此函数,那么将使用基类的虚函数。
4) 只有派生类的虚函数遮蔽基类的虚函数(函数原型相同)才能构成多态(通过基类指针访问派生类函数)。例如基类虚函数的原型为virtual void func();,派生类虚函数的原型为virtual void func(int);,那么当基类指针 p 指向派生类对象时,语句p -> func(100);将会出错,而语句p -> func();将调用基类的函数。
5) 构造函数不能是虚函数。对于基类的构造函数,它仅仅是在派生类构造函数中被调用,这种机制不同于继承。也就是说,派生类不继承基类的构造函数,将构造函数声明为虚函数没有什么意义。
6) 析构函数可以声明为虚函数,而且有时候必须要声明为虚函数。
构成多态的条件:
1.必须存在继承关系!
2.继承关系中必须有同名的虚函数,并且它们是遮蔽(覆盖)关系!
3.存在基类的指针,通过该指针调用虚函数!
看下面的代码。
#include <iostream>
using namespace std;
class base{
public :
virtual void func();
virtual void func(int);
};
void base :: func()
{
cout<<"base:func()"<<endl;
}
void base :: func(int a)
{
cout<<"base:func(int)"<<endl;
}
class der : public base{
public :
void func();
void func(char *);
};
void der :: func()
{
cout<<"der:func()"<<endl;
}
void der :: func(char *b)
{
cout<<"der:func(char *)"<<endl;
}
int main()
{
base *p = new der;
p->func(); //调用的是派生类的虚函数,构成了多态。
p->func(100); //调用的是基类的虚函数,因为派生类中没有函数遮蔽它。
//p_>func("hello"); //出现编译错误,因为通过基类的指针只能访问从基类继承过去的成员,不能访问派生类新增的成员。
return 0;
}
再来补充一下什么时候需要用到虚函数:
首先看成员函数所在的类是否会作为基类。
然后看成员函数在类的继承后有无可能被更改功能,如果希望更改其功能的,一般应该将它声明为虚函数。
如果成员函数在类被继承后功能不需修改,或派生类用不到该函数,则不要把它声明为虚函数。
2.虚析构函数
首先要明白,构造函数不能是虚函数,因为派生类不能继承基类的构造函数,将构造函数声明为虚函数没有意义!
先看代码
先看代码
#include <iostream>
using namespace std;
class base{
public :
base();
virtual ~base();
protected :
char *ptr;
};
base :: base()
{
ptr = new char[100];
cout<<"base construct"<<endl;
}
base :: ~base()
{
delete [] ptr;
cout<<"~base deconstruct"<<endl;
}
class der : public base{
public :
der();
~der();
protected :
char *name;
};
der :: der()
{
name = new char[100];
cout<<"der construct"<<endl;
}
der :: ~der()
{
delete[] name;
cout<<"~der deconstruct"<<endl;
}
int main()
{
base *p = new der;
delete p;
return 0;
}
将基类的析构函数声明为虚函数后,派生类的析构函数也会自动成为虚函数。这个时候编译器会忽略指针的类型,而根据指针的指向来选择函数;
也就是说,指针指向哪个类的对象就调用哪个类的函数。
(补充!!:指针访问非虚函数时,编译器会根据指针的类型来确定要调用的函数。如果指针是派生类的指针,编译器会根据它的类型匹配到派生类的析构函数,
在执行派生类的析构函数的过程中,又会调用基类的析构函数。派生类析构函数始终会调用基类的析构函数,并且这个过程是隐式完成的)。
在实际开发中,一旦我们自己定义了析构函数,就是希望在对象销毁时用它来进行清理工作,比如释放内存、关闭文件等,如果这个类又是一个基类,
那么我们就必须将该析构函数声明为虚函数,否则就有内存泄露的风险。也就是说,大部分情况下都应该将基类的析构函数声明为虚函数。
3.虚函数表和多态实现机制。
强调!!!当通过指针访问类的成员函数时:
1.如果该函数是非虚函数,那么编译器会根据指针的类型找到该函数;也就是说,指针是哪个类的类型就调用哪个类的函数。
2.如果该函数是虚函数,并且派生类有同名的函数遮蔽它,那么编译器会根据指针的指向找到该函数;
也就是说,指针指向的对象属于哪个类就调用哪个类的函数。这就是多态。
1.如果该函数是非虚函数,那么编译器会根据指针的类型找到该函数;也就是说,指针是哪个类的类型就调用哪个类的函数。
2.如果该函数是虚函数,并且派生类有同名的函数遮蔽它,那么编译器会根据指针的指向找到该函数;
也就是说,指针指向的对象属于哪个类就调用哪个类的函数。这就是多态。
编译器之所以能通过指针指向的对象找到虚函数,是因为在创建对象时额外地增加了虚函数表。
看一个例子:
#include <iostream>
#include <string>
using namespace std;
//People类
class People{
public:
People(string name, int age);
public:
virtual void display();
virtual void eating();
protected:
string m_name;
int m_age;
};
People::People(string name, int age): m_name(name), m_age(age){ }
void People::display(){
cout<<"Class People:"<<m_name<<"今年"<<m_age<<"岁了。"<<endl;
}
void People::eating(){
cout<<"Class People:我正在吃饭,请不要跟我说话..."<<endl;
}
//Student类
class Student: public People{
public:
Student(string name, int age, float score);
public:
virtual void display();
virtual void examing();
protected:
float m_score;
};
Student::Student(string name, int age, float score):
People(name, age), m_score(score){ }
void Student::display(){
cout<<"Class Student:"<<m_name<<"今年"<<m_age<<"岁了,考了"<<m_score<<"分。"<<endl;
}
void Student::examing(){
cout<<"Class Student:"<<m_name<<"正在考试,请不要打扰T啊!"<<endl;
}
//Senior类
class Senior: public Student{
public:
Senior(string name, int age, float score, bool hasJob);
public:
virtual void display();
virtual void partying();
private:
bool m_hasJob;
};
Senior::Senior(string name, int age, float score, bool hasJob):
Student(name, age, score), m_hasJob(hasJob){ }
void Senior::display(){
if(m_hasJob){
cout<<"Class Senior:"<<m_name<<"以"<<m_score<<"的成绩从大学毕业了,并且顺利找到了工作,Ta今年"<<m_age<<"岁。"<<endl;
}else{
cout<<"Class Senior:"<<m_name<<"以"<<m_score<<"的成绩从大学毕业了,不过找工作不顺利,Ta今年"<<m_age<<"岁。"<<endl;
}
}
void Senior::partying(){
cout<<"Class Senior:快毕业了,大家都在吃散伙饭..."<<endl;
}
int main(){
People *p = new People("赵红", 29);
p -> display();
p = new Student("王刚", 16, 84.5);
p -> display();
p = new Senior("李智", 22, 92.0, true);
p -> display();
return 0;
}
各个类的对象内存模型如下所示:
这里的东西及原理有些复杂,况且还是单继承。对于多继承,复杂程度不可想象,这里不做深究,有兴趣的同学自己研究吧。知道怎么用虚函数的我,先满足一下。代以后再深究。
4.纯虚函数
在C++中,可以将虚函数声明为纯虚函数,
语法格式为:
virtual 返回值类型 函数名 (函数参数) = 0;
1.纯虚函数没有函数体,只有函数声明,在虚函数声明的结尾加上=0,表明此函数为纯虚函数。
2. 最后的=0并不表示函数返回值为0,它只起形式上的作用,告诉编译系统“这是纯虚函数”。
语法格式为:
virtual 返回值类型 函数名 (函数参数) = 0;
1.纯虚函数没有函数体,只有函数声明,在虚函数声明的结尾加上=0,表明此函数为纯虚函数。
2. 最后的=0并不表示函数返回值为0,它只起形式上的作用,告诉编译系统“这是纯虚函数”。
包含纯虚函数的类称为抽象类(Abstract Class)。抽象类通常是作为基类,让派生类去实现纯虚函数。派生类必须实现纯虚函数才能被实例化。看代码
#include <iostream>
using namespace std;
class line{
public:
line(float len);
virtual float area() = 0;
virtual float volume() = 0;
protected:
float m_len;
};
line :: line(float len): m_len(len){}
class rec: public line{
public:
rec(float len, float width);
float area();
protected:
float m_width;
};
rec :: rec(float len, float width): line(len), m_width(width){}
float rec :: area()
{
return m_len * m_width;
float area();
protected:
float m_width;
}
Rec::Rec(float len, float width): Line(len), m_width(width){}
float Rec::area()
{
return m_len * m_width;
}
class cuboid: public rec{
public:
cuboid(float len, float width, float height);
float area();
float volume();
protected:
float m_height;
};
Cuboid::Cuboid(float len, float width, float height): Rec(len, width), m_height(height){}
float Cuboid :: area()
{
return 2 * ( m_len*m_width + m_len*m_height + m_width*m_height);
}
float Cuboid :: volume(){
return m_len * m_width * m_height;
}
class Cube: public Cuboid{
public:
Cube(float len);
float area();
float volume();
};
Cube::Cube(float len): Cuboid(len, len, len){}
float Cube::area(){
return 6 * m_len * m_len;
}
float Cube::volume(){
return m_len * m_len * m_len;
}
int main()
{
Line *p = new Cuboid(10, 20, 30);
cout<<"The area of Cuboid is "<<p->area()<<endl;
cout<<"The volume of Cuboid is "<<p->volume()<<endl;
p = new Cube(15);
cout<<"The area of Cube is "<<p->area()<<endl;
cout<<"The volume of Cube is "<<p->volume()<<endl;
return 0;
}
在实际开发中,你可以定义一个抽象基类,只完成部分功能,未完成的功能交给派生类去实现(谁派生谁实现)。这部分未完成的功能,往往是基类不需要的,或者在基类中无法实现的。虽然抽象基类没有完成,但是却强制要求派生类完成,这就是抽象基类的“霸王条款”。
注意:
1) 一个纯虚函数就可以使类成为抽象基类,但是抽象基类中除了包含纯虚函数外,还可以包含其它的成员函数(虚函数或普通函数)和成员变量。
2) 只有类中的虚函数才能被声明为纯虚函数,普通成员函数和顶层函数均不能声明为纯虚函数。
2) 只有类中的虚函数才能被声明为纯虚函数,普通成员函数和顶层函数均不能声明为纯虚函数。