1.了解多态
多态是C++三大特性之一,也是C++中十分重要的部分!难度适中,需要大家好好理解!
多态的概念:多种形态,不同的类完成不一样的事情。
2.多态的定义和实现
2.1 多态的定义
多态是在不同继承关系的类对象,去调用同一函数,做出不同的行为。
class Person
{
public:
virtual void doing()
{
cout << "开心的生活" << endl;
}
void buy()
{
cout << "买蛋糕" << endl;
}
};
class Student :public Person
{
virtual void doing()
{
cout << "好好上学" << endl;
}
void buy()
{
cout << "买冰淇淋" << endl;
}
};
class Older :public Person
{
virtual void doing()
{
cout << "好好享受" << endl;
}
void buy()
{
cout << "买保健品" << endl;
}
};
int main()
{
Person* p = new Student;
p->doing();
p->buy();
return 0;
}
首先,上面的Student Older都是继承person,并且对应的内部都有一个同名的doing()函数
按照上个博客写的,如果在子类中存在与父类里相同的函数,则构成了隐藏(也称为重定义)。
但是,这里的同名函数前,加上了virtual 关键字。
这是构成 多态的第一个条件!
让我们来看看运行结果!
按照继承的理论来说,这里应该是输出这样的结果:
,那么是什么导致会输出上面的结果呢?
是因为多态造成的!
2.2 多态的形成
多态的两个条件:
1.必须通过基类的指针或者引用调用虚函数
2.被调用的函数必须是虚函数,且派生类必须对基类的虚函数进行重写
回顾上面的代码,是不是构成了多态!
构成了多态,在调用的同名函数的时候,是一定调用子类中的虚函数(有virtual关键字修饰的)
这也是为什么会有这个结果的原因!
那么,构成了多态,还能使用基类中的虚函数吗?
答案是可以的,需要在使用函数前,加上其作用域
2.3 虚函数
虚函数:即被virtual修饰的类成员函数称为虚函数。
virtual void doing()
{
cout << "开心的生活" << endl;
}
2.4虚函数的重写
在子类中就有和父类完全相同的虚函数(返回值,函数名,参数)
class Person
{
public:
virtual void doing()
{
cout << "开心的生活" << endl;
}
};
class Student :public Person
{
virtual void doing()
{
cout << "好好上学" << endl;
}
};
特殊情况:
析构函数!如果基类的析构函数为虚函数,此时派生类析构函数只要定义,无论是否加virtual关键字, 都与基类的析构函数构成重写,虽然基类与派生类析构函数名字不同。
2.5 C++11中的final和override
final:被final修饰的成员函数不可再被重写
override:被override修饰的成员函数,会被检查是否重写,如果没有重写会报错。
2.6 重写 重载 重定义的区别
重写:
1.分别存在子类和父类中
2.函数名,参数,返回值均相同
3.两个函数均为虚函数
重载:
1.在相同的作用域中
2.具有相同的函数名,参数个数或者类型不同
重定义:
1.分别在子类和父类中
2.具有相同的函数名
3.不带virtual
3.抽象类
3.1 概念
在虚函数的后面写上 =0,则这个函数为纯虚函数。包含纯虚函数的类称为抽象类,抽象类不能实例化对象。
如果派生类中不重写纯虚函数,则派生类也无法实例化对象!因此纯虚函数的意义就在于督促派生类重写。
class Person
{
virtual void doing()=0;
}
class Student :public Person
{
virtual void doing()
{
cout << "好好上学" << endl;
}
};
3.2 接口继承和实现继承
普通函数的继承是一种实现继承,派生类继承了基类函数,可以使用函数,继承的是函数的实现。虚函数的继承是一种接口继承,派生类继承的是基类虚函数的接口,目的是为了重写,达成多态,继承的是接口。所以如果不实现多态,不要把函数定义成虚函数。
4.多态的原理
首先:我们来思考这样一个问题:
class Person
{
public:
virtual void doing()
//void doing()
{
cout << "开心的生活" << endl;
}
void buy()
{
cout << "买蛋糕" << endl;
}
private:
int i;
};
请问 sizeof(Person)的大小是多少? 1?4?8?
如果答案是1的话,说明 这个类里面没有成员变量。
如果答案是4的话,说明 这个类里面的i 占了4个字节
如果答案是8的话,该如何理解呢?
int main()
{
Person p;
return 0;
}
可以看到p中不只是有 成员变量i 还有一个 __vfptr,这是什么呢?这叫虚函数表指针!我们也称为虚表指针.
虚函数表用来做啥呢?
虚函数表中用来存储 虚函数指针(很好理解,每个虚函数都具有自己的地址)。
再来看看这样一段代码
class Person
{
public:
virtual void doing()
//void doing()
{
cout << "开心的生活" << endl;
}
virtual void buy()
{
cout << "买蛋糕" << endl;
}
private:
int i;
};
class Student :public Person
{
virtual void doing()
{
cout << "好好上学" << endl;
}
virtual void buy()
{
cout << "买冰淇淋" << endl;
}
private:
int b;
};
int main()
{
Person p;
Student s;
return 0;
}
可以看到 s中不仅有自己的成员变量,还有一部分是继承下来的。
继承下来中的部分,也是有__vfptr(虚表指针)
那么两个__vfptr是存的东西是一样的吗?
答案是 当然不同!这也正是多态的原理!
将函数进行重写后,不同的函数对应不同的地址,调用的时候,都是优先调用自身虚表中的虚函数。
一个很重要的问题(容易混淆):
虚函数存在哪的?虚表存在哪的?
虚函数存在虚表中,虚表在对象中。 这样的回答显然是错误的!但也是大多数人认为的答案。
那么正确答案是什么呢?
虚函数是和虚表都是存在代码段的。而虚表里面存的只是虚函数指针!而对象里面存的是虚表指针,不是虚表。
所以上述分析半天,那么多态的原理到底是什么呢?
让我们来一探究竟
void test(Person& p1)
{
p1.buy();
}
int main()
{
Person p;
Student s;
test(p);
test(s);
return 0;
}
帮助大家复习一下: 子类可以传给父类引用或者指针。
传入p之后,调用 buy(),在p的虚表中,找到buy()的地址,进行调用.
传入q之后,调用 buy(),在q的虚表中,找到buy()的地址,进行调用.
5.单继承和多继承中的虚表
单继承:
class Base
{
public:
virtual void func1() { cout << "Base1::func1" << endl; }
virtual void func2() { cout << "Base1::func2" << endl; }
private:
int b1 =0;
};
class Base3 :public Base
{
public:
virtual void func1() { cout << "Base3::func1" << endl; }
virtual void func2() { cout << "Base3::func2" << endl; }
private:
int b3 =1;
};
这种情况,上面已经分析过了,这里就不多赘述了。
多继承:
class Base1 {
public:
virtual void func1() { cout << "Base1::func1" << endl; }
virtual void func2() { cout << "Base1::func2" << endl; }
private:
int b1;
};
class Base2 {
public:
virtual void func1() { cout << "Base2::func1" << endl; }
virtual void func2() { cout << "Base2::func2" << endl; }
private:
int b2;
};
class Derive : public Base1, public Base2 {
public:
virtual void func1() { cout << "Derive::func1" << endl; }
virtual void func3() { cout << "Derive::func3" << endl; }
private:
int d1;
};
派生类中的虚函数会被隐藏,看不到。可以理解为这是一个bug。
但是可以看到 派生类中,具有两个父类的虚表指针。
与刚开始学多继承一样,会遇到调用函数不明确的情况。因此在调用的时候 需要加上作用域。
那为什么func1没有报错呢? 因为func1进行了重写,而func2并没有进行重写!
6.常见的问题
最后再来看几个问题!
1.inline函数可以是虚函数吗?答:可以,不过编译器就忽略inline属性,这个函数就不再是 inline,因为虚函数要放到虚表中去。
2. 静态成员可以是虚函数吗?答:不能,因为静态成员函数没有this指针,使用类型::成员函数 的调用方式无法访问虚函数表,所以静态成员函数无法放进虚函数表。
3. 构造函数可以是虚函数吗?答:不能,因为对象中的虚函数表指针是在构造函数初始化列表阶段才初始化的。
4. 析构函数可以是虚函数吗?什么场景下析构函数是虚函数?答:可以,并且最好把基类的析构函数定义成虚函数。
目录
5. 对象访问普通函数快还是虚函数更快?答:首先如果是普通对象,是一样快的。如果是指针 对象或者是引用对象,则调用的普通函数快,因为构成多态,运行时调用虚函数需要到虚函数表中去查找。
6. 虚函数表是在什么阶段生成的,存在哪的?答:虚函数表是在编译阶段就生成的,一般情况 下存在代码段(常量区)的。
7. 什么是抽象类?抽象类的作用?答:参考(3.抽象类)。抽象类强制重写了虚函数,另外抽 象类体现出了接口继承关系。