目录
一、多态的概念
二、多态的实现
三、纯虚函数和多态类
四、多态的原理
一、多态的概念
多态:多态分为编译时多态(静态多态)和运行时多态(动态多态)。编译时多态主要是我们之前学过的函数重载和函数模板,他们在传不同类型的参数就可以调用不同的函数,通过参数不同达到多种形态,之所以叫做编译时多态,是因为实参传给形参的参数匹配是在编译时完成的。
运行时多态,就是去完成某个行为,可以传不同的对象完成不同的行为,就会达到多种的形态。例如买票行为,当普通人买票时,是全价买票;而当学生买票时,是半价买票。
二、多态的实现
1. 多态是一个继承关系下的类对象,去调用同意函数,产生了不同的行为。例如Student类继承了Person类,当我们调用同一个BuyTickets函数,Person对象全价买票,Student对象半价买票。
1.1 实现多态的条件
- 必须是基类的指针或引用
- 被调的函数必须是虚函数
1.2 虚函数
类成员函数前加 virtual 修饰,那么这个成员函数被称为虚函数。注意:非成员函数不能加virtual修饰。
class Base
{
public:
virtual void func()
{
cout << "Base -> func()" << endl;
}
};
这里的 virtual void func() 就是Base内的一个虚函数。
1.3 虚函数的重写/覆盖
派生类中有一个跟基类完全相同的虚函数(这里的完全相同指的是 返回值类型、函数名、参数列表完全相同),称为派生类的虚函数重写了基类的虚函数。
注意:在重写基类的虚函数时,对于派生类,我们其实可以不加 virtual 关键字,因为继承基类后,虚函数也被继承了下来,在派生类中依旧保持虚函数的属性,但是这种写法看起来不是很一目了然,但是如果不写的话,也是构成重写/覆盖的。
class Base
{
public:
virtual void func1()
{
cout << "Base -> func1()" << endl;
}
virtual void func2()
{
cout << "Base -> func2()" << endl;
}
void func3()
{
cout << "Base -> func3()" << endl;
}
};
class Derive : public Base
{
public:
virtual void func1()
{
cout << "Derive -> func1()" << endl;
}
void func2()
{
cout << "Derive -> func2()" << endl;
}
void func3()
{
cout << "Derive -> func3()" << endl;
}
};
上面Derive继承了Base,按照我们刚刚提到的,我们就可以得出结论,派生类Derive中的func1 和 func2是构成重写/覆盖的,而func3时不构成的。
1.4 多态场景的题目
以下程序输出的结果是什么(B)
A: A->0 B: B->1 C: A->1 D: B->0 E: 编译出错 F: 以上都不正确
class A
{
public:
virtual void func(int val = 1)
{
cout << "A->" << val << endl;
}
virtual void test()
{
func();
}
};
class B : public A
{
public:
void func(int val = 0)
{
cout << "B->" << val << endl;
}
};
int main()
{
B* ptr = new B;
ptr->test();
return 0;
}
答案为什么是B呢?首先可以肯定的是,B中的func函数已经对A中的func函数实现了重写/覆盖。但是由于虚函数表的存在,当我们后面讲到虚表的时候,再来看这个问题,会更加的清楚。
三、纯虚函数和多态类
在虚函数的后面写上 =0 ,则这个函数称为纯虚函数,纯虚函数不需要定义实现,只要声明即可。包含纯虚函数的类称为抽象类,抽象类不能实例化出对象,如果派生类继承后不重写纯虚函数,那么派生类也是抽象类。纯虚函数某种程度上强制了派生类重写虚函数,否则实例不出来对象。例如:
class Base
{
public:
virtual void func() = 0;
};
class Derive : public Base
{
public:
virtual void func()
{
cout << "Derive -> func()" << endl;//重写func()
}
};
int main()
{
Base b; //错误,因为Base是抽象类,不能实例化出对象
Derive d; //正确,因为Derive重写了func()
return 0;
}
四、多态的原理
1. 虚函数表指针
下面编译在32位程序的运行结果是什么 ( D )
A: 编译报错 B: 运行报错 C: 8 D: 12
class Base
{
public:
virtual void func()
{
cout << "Base -> func()" << endl;
}
protected:
int _b = 1;
char _ch = 's';
};
int main()
{
Base b;
cout << sizeof(b) << endl;
return 0;
}
这是为什么呢,按内存对齐来看,一个int 和 一个char,算下来不应该是8吗?这是因为,除了_b和_ch成员,还多了一个_vfptr放在对象的前面(有的平台可能放在后面),对象中的这个指针我们叫做虚函数表指针。一个含有虚函数的类中都至少有一个虚函数表指针,因为一个类所有的虚函数的地址要被放到这个类对象的虚函数表中,虚函数表也称为虚表。如下图:
2. 多态的原理
//类省略未写
void Func(Person* p)
{
p->BuyTickets();
}
int main()
{
Person ps;
Student st;
Func(&ps);
Func(&st);
return 0;
}
从底层的角度Func函数中p->BuyTickets(),ptr是如何做到指向Person对象调用Person::BuyTickets,指向Student对象调用Student::BuyTickets的呢?通过上图我们可以看到,满足多态条件之后,底层不再是编译时通过调用对象确定函数的地址,而是运行时到指向的对象的虚表中确定对应虚函数的地址,这样就实现了指针或引用指向基类就调用基类的虚函数,指向派生类就调用派生类的虚函数。
如果派生类重写了基类的虚函数,那么派生类的虚函数表中对应的虚函数就会被覆盖成派生类重写的虚函数地址。
这里我们回头再看看1.4的题目:
class A
{
public:
virtual void func(int val = 1)
{
cout << "A->" << val << endl;
}
virtual void test()
{
func();
}
};
class B : public A
{
public:
void func(int val = 0)
{
cout << "B->" << val << endl;
}
};
int main()
{
B* ptr = new B;
ptr->test();
return 0;
}
有了下面这张图,我们就很容易理解了,为什么最终的结果是B->1了。首先B是继承了A,并且B中的func函数满足了对基类的重写/覆盖,然后B也继承了A中的test()。因此当我们调用ptr->test()时,虚函数test()的地址在虚表中没有改变,调用的时候会去A类里面调用。之后才是调用func函数,此时已经构成重写/覆盖,因此调用的这个虚函数func()地址是指向B类的func()。那么有同学就说了,为什么不是B->0呢?我们要知道,重写/覆盖只是对函数的内容进行重写,因此相当于调用了下面的函数:
void func(int val = 1)
{
cout << "B->" << val << endl;
}
你可以理解为重写/覆盖值改变了{ }内的内容,而函数参数还是用了基类的。
3. 虚函数表
- 基类对象的虚函数表中存放基类所有虚函数的地址。
- 派生类的虚函数表由两部分构成,继承下来的基类和自己的成员,一般情况下,继承下来的基类中中有虚函数表指针,自己就不会再生成虚函数表指针。但是继承下来的基类部分虚函数表指针和基类对象的虚函数表指针不是同一个。
- 派生类中重写的基类的虚函数,派生类的虚函数表中对应的虚函数地址就会被覆盖生成派生类重写的虚函数地址。
- 派生类的虚函数表中包含,基类的虚函数地址,派生类重写的虚函数地址,派生类自己的虚函数地址三个部分。
- 虚函数表本质是一个存放虚函数指针的指针数组。
- 虚函数存在于哪?虚函数和普通函数一样,编译好后是一段指令,都是存在代码段的,只是虚函数的地址又存到了虚表之中
- 虚函数表存在于哪的呢?C++标准并没有规定,但是在VS里面是存在于常量区的。
我们可以通过下面的代码,在VS上证实:
int main()
{
int i = 0;
static int j = 1;
int* p1 = new int;
const char* p2 = "xxxxxxxx";
printf("栈: % p\n", &i);
printf("静态区: % p\n", &j);
printf("堆: % p\n", p1);
printf("常量区: % p\n", p2);
Base b;
Derive d;
Base * p3 = &b;
Derive * p4 = &d;
printf("Base虚表地址: % p\n", *(int*)p3);
printf("Derive虚表地址: % p\n", *(int*)p4);
printf("虚函数地址: % p\n", &Base::func1);
printf("普通函数地址: % p\n", &Base::func5);
return 0;
}
运行结果显示虚表的地址和常量区的地址非常接近,而虚函数的地址和普通函数的地址非常接近。
以上内容如有错误,欢迎批评指正!