多态
概念:同样的调用语句有多种不同的表现形式。
通俗的来说:根据传入的对象类型的不同,调用不同的派生类的相应函数。
多态实现的条件:
1.要存在继承关系;
2.对虚函数的重写;
3.基类指针(引用)指向派生类对象
静态联编与动态联编
静态联编:程序匹配,连接在编译阶段实现,也称为早期联编(在编译的时候,就知道了该去调用谁)
例如:函数重载
动态联编:程序联编推迟到运行时进行,也称为晚期联编(在执行时,才会知道调用那一个函数)
例如:switch 和 if 语句
我们通过动物类来说明:
#include <iostream>
using namespace std;
// 动物: 基类
class Animal
{
public:
Animal(int age, char *name)
{
this->age = age;
this->name = name;
}
void sleep()
{
printf ("动物睡觉\n");
}
void print()
{
printf ("age = %d, name = %s\n", age, name);
}
private:
int age;
char *name;
};
class Cat:public Animal
{
public:
Cat(int age, char *name):Animal(age, name)
{
}
void sleep()
{
printf ("猫 趴着睡觉\n");
}
};
class Fish:public Animal
{
public:
Fish(int age, char *name):Animal(age, name)
{
}
void sleep()
{
printf ("鱼 睁着眼睡觉\n");
}
};
void func(Animal *p)
{
p->sleep();
}
int main()
{
Animal *pa = new Animal(2, "动物");
Cat *pc = new Cat(3, "猫");
Fish *pf = new Fish(2, "鱼");
func(pa); //动物睡觉
func(pc); //动物睡觉
func(pf); //动物睡觉
return 0;
}
分析:
基类中的 sleep()函数是一个普通类型的函数,并且 func 的形参为 Animal 类的指针,虽然 pc 和 pf 是一个指向 猫类 和 鱼类 的指针,调用函数 func 时,基类指针指向了派生类的对象,基类指针的本质依旧是 Animal*。因此,在编译的时候就明确了,该调用哪一个函数,这是一个静态联编。
实际上,我们希望能根据传入的指针的不同,调用不同的sleep();
那么,我们需要使用 virtual 来修饰基类的成员函数,以此来实现多态。
#include <iostream>
using namespace std;
// 动物: 基类
class Animal
{
public:
Animal(int age, char *name)
{
this->age = age;
this->name = name;
}
virtual void sleep()
{
printf ("动物睡觉\n");
}
void print()
{
printf ("age = %d, name = %s\n", age, name);
}
private:
int age;
char *name;
};
class Cat:public Animal
{
public:
Cat(int age, char *name):Animal(age, name)
{
}
void sleep()
{
printf ("猫 趴着睡觉\n");
}
};
class Fish:public Animal
{
public:
Fish(int age, char *name):Animal(age, name)
{
}
void sleep()
{
printf ("鱼 睁着眼睡觉\n");
}
};
void func(Animal *p)
{
p->sleep();
}
int main()
{
Animal *pa = new Animal(2, "动物");
Cat *pc = new Cat(3, "猫");
Fish *pf = new Fish(2, "鱼");
func(pa); // 动物睡觉
func(pc); // 猫 趴着睡觉
func(pf); // 鱼 睁着眼睡觉
return 0;
}
此时,我们将基类中的 sleep() 函数声明为虚函数后,再通过基类指针去调用函数时,会根据传入对象的不同,找到相应的函数来执行。
多态的原理,随后为大家讲解。
虚析构函数
通过基类指针释放派生类对象, 基类的析构函数一定要是 虚析构函数。
举个例子说明一下:
#include <iostream>
using namespace std;
// 基类
class A
{
public:
A()
{
printf ("A 的构造函数\n");
}
~A()
{
printf ("A 的析构函数\n");
}
};
class B:public A
{
public:
B()
{
p = new char[20];
printf ("B 的构造函数\n");
}
~B()
{
if (p != NULL)
delete[] p;
printf ("B 的析构函数\n");
}
private:
char *p;
};
void func(A *pa)
{
delete pa;
}
int main()
{
A *pa = new B;
func(pa);
return 0;
}
运行结果是:
A 的构造函数
B 的构造函数
A 的析构函数
发现,没有了B的析构函数,这样会造成内存的泄漏,为什么会出现这种现象呢?
首先,我们得明确构造和析构的一个基本特性:
构造函数:从当前类往上找 父类 ⇒ 最上层的父类, 从最上层的父类开始构造(调用构造函数),类似于前序递归
析构函数:从当前类开始析构 析构完 沿着继承路径往上找父类 析构父类 ⇒ 找到最上层的父类 析构,类似于后序递归
那么,对于上面的程序的结果,原因在于,执行语句“A *pa = new B;”定义了一个 B 类的对象,那么会先调用 基类的 构造函数,再调用自身的构造函数,再调用 func()函数时,释放的是基类的指针,它认为基类的指针是当前的对象,所以调用的是基类的构造函数。
Tips:
char *ptr = "abcdefg";
void printS(char *p)
{
if (*p == '\0')
return ;
printf ("%c", *p);
printS(p+1); // 递归
//printf ("%c", *p);
}
// 先打印,再递归的,称为后序递归,打印结果:abcdefg
// 先递归,再打印的,称为前序递归,打印结果:gfedcba
为了解决这个问题,我们只需要将基类的析构函数声明为虚函数。
class A
{
public:
A()
{
printf ("A 的构造函数\n");
}
virtual ~A()
{
printf ("A 的析构函数\n");
}
};
多态的原理
多态通过一个虚函数指针(vfptr)来实现。这个虚函数指针指向一个虚函数表,这张表里存放了该类对应的虚函数。
我们还是使用动物类来说明:
class Animal
{
public:
Animal(int age, char *name)
{
this->age = age;
this->name = name;
}
virtual void sleep()
{
printf ("动物睡觉\n");
}
virtual void eat()
{
printf ("动物吃饭\n");
}
void print()
{
printf ("age = %d, name = %s\n", age, name);
}
private:
int age;
char *name;
};
class Cat:public Animal
{
public:
Cat(int age, char *name):Animal(age, name)
{
}
void sleep()
{
printf ("猫 趴着睡觉\n");
}
};
我们知道 基类中的 sleep() 和 eat()函数是一个虚函数,那么基类的虚函数指针就会指向一张虚函数表,这张表由编译器保管。
那么,当 Cat 类继承 Animal 时,由于 Cat 类中有与 Animal 类中的虚函数同名的函数,因此 Cat类中的同名函数也是虚函数,但与基类有所不同的是,派生类中的虚函数指针不在指向基类中虚函数表,而是指向自己的虚函数表。
因此,当调用 func()函数时,编译器会根据对象的虚函数指针找到对应的虚函数表,再找到虚函数。
注意:由于虚函数在编译的时候,并不知道谁在调用,所以会在执行的时候才会确定,因此是动态联编,并且效率会低于一般的函数。
虚继承与虚函数共存的情况
(以下内存模型的结果,本人通过 VS2010 和 VS2015进行验证)
1.基类有虚函数,派生类中没有虚函数,普通继承时
#include <iostream>
using namespace std;
class AA
{
public:
AA(int a = 0)
{
this->a = a;
}
virtual void print()
{
printf("AA 的print函数\n");
}
private:
int a;
};
class BB :public AA
{
public:
BB(int b) :AA(1)
{
this->b = b;
}
void show()
{
printf("BB 的show函数\n");
}
private:
int b;
};
int main()
{
BB b(2);
printf("sizeof b is %d\n", sizeof(b));
b.print();
return 0;
}
执行结果:
sizeof b is 12
AA 的print函数
来看一下内存模型:
我们看到对象 b 所占内存为 12 字节,查看一下内存分布可以看出来,地址从低到高依次是 vfptr,a,b,此时虚函数表里存放的是 AA 中的 print 函数。
2.基类有虚函数,派生类中没有虚函数,虚继承时
#include <iostream>
using namespace std;
class AA
{
public:
AA(int a = 0)
{
this->a = a;
}
virtual void print()
{
printf("AA 的print函数\n");
}
private:
int a;
};
class BB :virtual public AA
{
public:
BB(int b) :AA(1)
{
this->b = b;
}
void show()
{
printf("BB 的show函数\n");
}
private:
int b;
};
int main()
{
BB b(2);
printf("sizeof b is %d\n", sizeof(b));
b.print();
return 0;
}
执行结果:
sizeof b is 16
AA 的print函数
内存模型:
在这里,说明一下虚函数指针的存放位置,一般 vfptr 都会与自身类的成员在内存上连续存放,就像这里的 vfptr 和 a
3.基类没有虚函数,派生类中有虚函数,普通继承时
#include <iostream>
using namespace std;
class AA
{
public:
AA(int a = 0)
{
this->a = a;
}
void print()
{
printf("AA 的print函数\n");
}
private:
int a;
};
class BB :public AA
{
public:
BB(int b) :AA(1)
{
this->b = b;
}
virtual void show()
{
printf("BB 的show函数\n");
}
private:
int b;
};
int main()
{
BB b(2);
printf("sizeof b is %d\n", sizeof(b));
return 0;
}
运行结果:
sizeof b is 12
内存分析:
4.基类没有虚函数,派生类中有虚函数,虚继承时
#include <iostream>
using namespace std;
class AA
{
public:
AA(int a = 0)
{
this->a = a;
}
void print()
{
printf("AA 的print函数\n");
}
private:
int a;
};
class BB :virtual public AA
{
public:
BB(int b) :AA(1)
{
this->b = b;
}
virtual void show()
{
printf("BB 的show函数\n");
}
private:
int b;
};
int main()
{
BB b(2);
printf("sizeof b is %d\n", sizeof(b));
return 0;
}
运行结果:
sizeof b is 16
内存分析:
5.基类有虚函数,派生类中有虚函数,普通继承时
(1)派生类 中的 虚函数 与 基类 中的虚函数不同名时
#include <iostream>
using namespace std;
class AA
{
public:
AA(int a = 0)
{
this->a = a;
}
virtual void print()
{
printf("AA 的print函数\n");
}
private:
int a;
};
class BB :public AA
{
public:
BB(int b) :AA(1)
{
this->b = b;
}
virtual void show()
{
printf("BB 的show函数\n");
}
private:
int b;
};
int main()
{
BB b(2);
printf("sizeof b is %d\n", sizeof(b));
b.print();
return 0;
}
运行结果:
sizeof b is 12
AA 的print函数
内存模型:
(2)派生类 中的 虚函数 与 基类 中的虚函数同名时
#include <iostream>
using namespace std;
class AA
{
public:
AA(int a = 0)
{
this->a = a;
}
virtual void print()
{
printf("AA 的print函数\n");
}
private:
int a;
};
class BB :public AA
{
public:
BB(int b) :AA(1)
{
this->b = b;
}
virtual void print()
{
printf("AA 的print函数\n");
}
virtual void show()
{
printf("BB 的show函数\n");
}
private:
int b;
};
int main()
{
BB b(2);
printf("sizeof b is %d\n", sizeof(b));
b.print();
b.show();
return 0;
}
运行结果:
sizeof b is 12
BB 的print函数
BB 的show函数
这两种情况其实是一样的,唯一的区别在于 虚函数指针 指向的虚函数列表中虚函数不同,当派生类中没有与基类虚函数同名的函数时,虚函数列表里有 基类 的虚函数,但当派生类中有与基类虚函数同名的函数时,虚函数列表里有 派生类 的虚函数
6.基类有虚函数,派生类中有虚函数,虚继承时
(1)派生类 中的 虚函数 与 基类 中的虚函数不同名时
#include <iostream>
using namespace std;
class AA
{
public:
AA(int a = 0)
{
this->a = a;
}
virtual void print()
{
printf("AA 的print函数\n");
}
private:
int a;
};
class BB :virtual public AA
{
public:
BB(int b) :AA(1)
{
this->b = b;
}
virtual void show()
{
printf("BB 的show函数\n");
}
private:
int b;
};
int main()
{
BB b(2);
printf("sizeof b is %d\n", sizeof(b));
b.print();
b.show();
return 0;
}
运行结果:
sizeof b is 20
AA 的print函数
BB 的show函数
内存分析:
此时的 虚基类指针 的第一个参数为 -4 ,因为当前对象的 虚函数指针 在最上面。对象 b 的虚函数表中有两个函数,一个是 基类的 print 函数,一个是 自己的 show 函数,并且 自己的 show 函数 在内存中的位置 在基类的 print 函数之前,而AA 的虚函数表中 只有自己的 print 函数。
(2)派生类 中的 虚函数 与 基类 中的虚函数同名时
#include <iostream>
using namespace std;
class AA
{
public:
AA(int a = 0)
{
this->a = a;
}
virtual void print()
{
printf("AA 的print函数\n");
}
private:
int a;
};
class BB :virtual public AA
{
public:
BB(int b) :AA(1)
{
this->b = b;
}
virtual void print()
{
printf("BB 的print函数\n");
}
virtual void show()
{
printf("BB 的show函数\n");
}
private:
int b;
};
int main()
{
BB b(2);
printf("sizeof b is %d\n", sizeof(b));
b.print();
b.show();
return 0;
}
运行结果:
sizeof b is 24
BB 的print函数
BB 的show函数
内存分析: