C++多态
多态可以分为编译时的多态和运行时的多态。前者主要是指 函数的重载(包括运算符的重载)、对重载函数的调用,在编译时就能根据实参确定应该调用哪个函数,因此叫编译时的多态;而后者则和继承、虚函数等概念有关。
有了虚函数,基类指针指向基类对象时就使用基类的成员(包括 成员函数 和 成员变量 ),指向派生类对象时就使用派生类的成员。换句话说,基类指针可以按照基类的方式来做事,也可以按照派生类的方式来做事,它有多种形态,或者说有多种表现方式,我们将这种现象称为多态(Polymorphism)。
C++ 提供多态的目的是:可以通过基类指针对所有派生类(包括直接派生和间接派生)的成员变量和成员函数进行 “全方位” 的访问,尤其是成员函数。如果没有多态,我们只能访问成员变量。
C++多态的使用
#include <iostream>
#include <cstring>
using namespace std;
//Person类
class Person
{
public:
Person(string name, int age):m_name(name),m_age(age)
{
}
void info()
{
cout << "Call Person info, Name = " << this->m_name << " Age = " << this->m_age << endl;
}
protected:
string m_name;
int m_age;
};
class Student:public Person
{
public:
Student(string name, int age, float score):Person(name, age),m_score(score)
{
}
void info()
{
cout << "Call Student info, Name = " << this->m_name << " Age = " << this->m_age << " Score = " << m_score << endl;
}
protected:
float m_score;
};
int main()
{
Person *person = new Person("Bob", 18);
person->info();
Student *student = new Student("Bob", 18, 99);
student->info();
return 0;
}
运行结果
Call Person info, Name = Bob Age = 18
Call Student info, Name = Bob Age = 18 Score = 99
我们分别定义了一个 Person 类和一个 Student 类,Student 类继承自 Person 类,接着,在 main 函数里面,我们分别实例化了一个 Person 对象和一个 Student 对象。
最后,我们分别调用了 Person 类对象的 info 方法和 Student 类对象的 info 方法,我们发现,它们各自调用了自己的 info 函数,现在,我们用 Student 来实例化 Person 类,我们修改程序,如下
#include <iostream>
#include <cstring>
using namespace std;
//Person类
class Person
{
public:
Person(string name, int age):m_name(name),m_age(age)
{
}
void info()
{
cout << "Call Person info, Name = " << this->m_name << " Age = " << this->m_age << endl;
}
protected:
string m_name;
int m_age;
};
class Student:public Person
{
public:
Student(string name, int age, float score):Person(name, age),m_score(score)
{
}
void info()
{
cout << "Call Student info, Name = " << this->m_name << " Age = " << this->m_age << " Score = " << m_score << endl;
}
protected:
float m_score;
};
int main()
{
Person *person = new Person("Bob", 18);
person->info();
person = new Student("Bob", 18, 99);
person->info();
return 0;
}
运行结果
Call Person info, Name = Bob Age = 18
Call Person info, Name = Bob Age = 18
这次,我们用 Student 类实例化了 Person 类,最终调用 info 函数,此时的 info 函数还是调用的 Person 类的,这不是我们想要的效果,我们期望的是还是调用 Student 类的 info 函数。
也就是说通过基类指针只能访问派生类的成员变量,但是不能访问派生类的成员函数。为了能让基类指针访问派生类的成员函数,C++ 增加了虚函数(Virtual Function),现在,我们将 info 函数声明为虚函数,修改程序如下
#include <iostream>
#include <cstring>
using namespace std;
//Person类
class Person
{
public:
Person(string name, int age):m_name(name),m_age(age)
{
}
virtual void info()
{
cout << "Call Person info, Name = " << this->m_name << " Age = " << this->m_age << endl;
}
protected:
string m_name;
int m_age;
};
class Student:public Person
{
public:
Student(string name, int age, float score):Person(name, age),m_score(score)
{
}
virtual void info()
{
cout << "Call Student info, Name = " << this->m_name << " Age = " << this->m_age << " Score = " << m_score << endl;
}
protected:
float m_score;
};
int main()
{
Person *person = new Person("Bob", 18);
person->info();
person = new Student("Bob", 18, 99);
person->info();
return 0;
}
运行结果
Call Person info, Name = Bob Age = 18
Call Student info, Name = Bob Age = 18 Score = 99
多态构成条件
- 必须存在继承关系;
- 继承关系中必须有同名的虚函数,并且它们是覆盖关系(函数原型相同)
- 存在基类的指针,通过该指针调用虚函数。
在 C++ 中,多态 的实现,除了可以使用子类的指针指向父类的对象之外,还可以通过引用来实现多态,不过引用不像 指针 灵活,指针可以随时改变指向,而引用只能指代固定的对象,在多态性方面缺乏表现力
#include <iostream>
#include <cstring>
using namespace std;
//Person类
class Person
{
public:
Person(string name, int age):m_name(name),m_age(age)
{
}
virtual void info()
{
cout << "Call Person info, Name = " << this->m_name << " Age = " << this->m_age << endl;
}
protected:
string m_name;
int m_age;
};
class Student:public Person
{
public:
Student(string name, int age, float score):Person(name, age),m_score(score)
{
}
virtual void info()
{
cout << "Call Student info, Name = " << this->m_name << " Age = " << this->m_age << " Score = " << m_score << endl;
}
protected:
float m_score;
};
int main()
{
Person person("Bob", 18);
Student student("Bob", 20, 110);
Person &rPerson = person;
Person &rStudent = student;
rPerson.info();
rStudent.info();
return 0;
}
由于引用类似于常量,只能在定义的同时初始化,并且以后也要从一而终,不能再引用其他数据,所以本例中必须要定义两个引用变量,一个用来引用基类对象,一个用来引用派生类对象。从运行结果可以看出,当基类的引用指代基类对象时,调用的是基类的成员,而指代派生类对象时,调用的是派生类的成员。
在 C++ 中,多态的实现,除了可以使用子类的指针指向父类的对象之外,还可以通过引用来实现多态,不过引用不像指针灵活,指针可以随时改变指向,而引用只能指代固定的对象,在多态性方面缺乏表现力。
C++虚函数
在 C++ 中,使用 virtual 关键字 修饰的 函数 被称为虚函数,虚函数对于 多态 具有决定性的作用,有虚函数才能构成多态。
- 只需要在虚函数的声明处加上 virtual 关键字,函数定义处可以加也可以不加。
- 为了方便,可以只将基类中的函数声明为虚函数,这样所有派生类中具有遮蔽关系的同名函数都将自动成为虚函数。
- 当在基类中定义了虚函数时,如果派生类没有定义新的函数来遮蔽此函数,那么将使用基类的虚函数。
- 只有派生类的虚函数覆盖基类的虚函数(函数原型相同)才能构成多态(通过基类指针访问派生类函数)。
- 构造函数 不能是虚函数。对于基类的构造函数,它仅仅是在派生类构造函数中被调用,这种机制不同于 继承。也就是说,派生类不继承基类的构造函数,将构造函数声明为虚函数没有什么意义。
- 析构函数 可以声明为虚函数,而且有时候必须要声明为虚函数。
虚函数使用
什么时候需要将函数声明为虚函数,首先看 成员函数 所在的类是否会作为基类。然后看成员函数在类的继承后有无可能被更改功能,如果希望更改其功能的,一般应该将它声明为虚函数。
如果成员函数在类被继承后功能不需修改,或派生类用不到该函数,则不要把它声明为虚函数。
#include <iostream>
#include <cstring>
using namespace std;
//Person类
class Person
{
public:
Person(string name, int age):m_name(name),m_age(age)
{
}
virtual void info()
{
cout << "Call Person info, Name = " << this->m_name << " Age = " << this->m_age << endl;
}
protected:
string m_name;
int m_age;
};
class Student:public Person
{
public:
Student(string name, int age, float score):Person(name, age),m_score(score)
{
}
virtual void info()
{
cout << "Call Student info, Name = " << this->m_name << " Age = " << this->m_age << " Score = " << m_score << endl;
}
protected:
float m_score;
};
int main()
{
Person *person = new Person("Bob", 18);
person->info();
person = new Student("Bob", 18, 99);
person->info();
return 0;
}
因为,我们不同的子类需要实现不同的 info 函数,所以,我们必须将 info 函数声明为虚函数,不然,没办法通过子类对象指向父类成员时,访问子类对象的 info 方法。
C++虚析构函数
在 C++ 中,使用 virtual 关键字 修饰的 函数 被称为 虚函数,C++ 的 构造函数 不可以被声明为虚函数,但 析构函数 可以被声明为虚函数,并且有时候必须将析构声明为虚函数。
用 C++ 开发的时候,用来做基类的类的析构函数一般都是虚函数。
虚析构函数的作用
虚析构函数是为了避免内存泄露,而且是当子类中会有指针 成员变量 时才会使用得到的。也就说虚析构函数使得在删除指向子类对象的基类指针时可以调用子类的析构函数达到释放子类中堆内存的目的,而防止内存泄露的。
当父类的析构函数不声明成虚析构函数的时候,当子类继承父类,父类的指针指向子类时,delete掉父类的指针,只调动父类的析构函数,而不调动子类的析构函数。
当父类的析构函数声明成虚析构函数的时候,当子类继承父类,父类的指针指向子类时,delete掉父类的指针,先调动子类的析构函数,再调动父类的析构函数。
#include <iostream>
#include <cstring>
using namespace std;
//Person类
class Person
{
public:
Person(string name, int age):m_name(name),m_age(age)
{
}
virtual void info()
{
cout << "Call Person info, Name = " << this->m_name << " Age = " << this->m_age << endl;
}
~Person() // 析构函数不是虚函数
{
cout << "Call ~Person" << endl;
}
protected:
string m_name;
int m_age;
};
class Student:public Person
{
public:
Student(string name, int age, float score):Person(name, age),m_score(score)
{
}
virtual void info()
{
cout << "Call Student info, Name = " << this->m_name << " Age = " << this->m_age << " Score = " << m_score << endl;
}
~Student()
{
cout << "Call ~Student" << endl;
}
protected:
float m_score;
};
int main()
{
Person *person = new Student("Bob", 18, 99);
person->info();
delete person;
return 0;
}
运行结果
Call Student info, Name = Bob Age = 18 Score = 99
Call ~Person
在 main 函数里面,使用了父类指向了子类对象,最终,我们释放子类对象时,调用了父类的析构函数,这样会导致,我们子类对象的一些数据成员没发得到释放,会造成内存泄露,现在,我们将析构修改为虚析构,修改程序如下:
#include <iostream>
#include <cstring>
using namespace std;
//Person类
class Person
{
public:
Person(string name, int age):m_name(name),m_age(age)
{
}
virtual void info()
{
cout << "Call Person info, Name = " << this->m_name << " Age = " << this->m_age << endl;
}
virtual ~Person()
{
cout << "Call ~Person" << endl;
}
protected:
string m_name;
int m_age;
};
class Student:public Person
{
public:
Student(string name, int age, float score):Person(name, age),m_score(score)
{
}
virtual void info()
{
cout << "Call Student info, Name = " << this->m_name << " Age = " << this->m_age << " Score = " << m_score << endl;
}
~Student()
{
cout << "Call ~Student" << endl;
}
protected:
float m_score;
};
int main()
{
Person *person = new Student("Bob", 18, 99);
person->info();
delete person;
return 0;
}
运行结果
Call Student info, Name = Bob Age = 18 Score = 99
Call ~Student
Call ~Person
这次,我们将父类的析构函数声明为了虚析构,再次运行程序,我们发现,这次首先调用了子类的析构函数,再次调用了父类的构造函数,这样就不会存在资源泄露的问题了,因此,存在继承时,最好将父类的析构声明为虚析构。
C++ 虚函数表
在 C++ 中,多态 是由 虚函数 实现的,而虚函数主要是通过虚函数表(V-Table)来实现的。对象不包含虚函数表,只有虚指针,类 才包含虚函数表,派生类会生成一个兼容基类的虚函数表。
如果一个类中包含虚函数(virtual 修饰的函数),那么这个类就会包含一张虚函数表,虚函数表存储的每一项是一个虚函数的地址。
虚函数表
这个类的每一个对象都会包含一个虚指针(虚指针存在于对象实例地址的最前面,保证虚函数表有最高的性能),这个虚指针指向虚函数表
原始基类的虚函数表
原始基类的对象,可以看到虚指针在地址的最前面,指向基类的虚函数表(假设基类定义了3个虚函数),如下图所示
单继承时的虚函数(无重写基类虚函数)
假设现在派生类继承基类,并且重新定义了 3 个虚函数,派生类会自己产生一个兼容基类虚函数表的属于自己的虚函数表,如下图所示
Derive class 继承了 Base class 中的三个虚函数,准确的说,是该函数实体的地址被拷贝到 Derive 类的虚函数表,派生类新增的虚函数置于虚函数表的后面,并按声明顺序存放
单继承时的虚函数(重写基类虚函数)
现在派生类重写基类的 x 函数,可以看到这个派生类构建自己的虚函数表的时候,修改了 base::x() 这一项,指向了自己的虚函数,如下图所示
Derive class 继承了 Base class 中的三个虚函数,准确的说,是该函数实体的地址被拷贝到 Derive 类的虚函数表,派生类新增的虚函数置于虚函数表的后面,并按声明顺序存放
多重继承时的虚函数
这个派生类多重继承了两个基类 base1,base2,因此它有两个虚函数表,如下图所示
它的对象会有多个虚指针(据说和编译器相关),指向不同的虚函数表。
虚继承时的虚函数表
虚继承的引入把对象的模型变得十分复杂,除了每个基类(MyClassA 和 MyClassB)和公共基类(MyClass)的虚函数表指针需要记录外,每个虚拟继承了 MyClass 的父类还需要记录一个虚基类表 vbtable 的指针 vbptr。MyClassC 的对象模型如图:
虚基类表每项记录了被继承的虚基类子对象相对于虚基类表指针的偏移量。比如 MyClassA 的虚基类表第二项记录值为 24,正是 MyClass::vfptr 相对于 MyClassA::vbptr 的偏移量,同理 MyClassB 的虚基类表第二项记录值 12 也正是 MyClass::vfptr 相对于 MyClassA::vbptr 的偏移量