多态的条件:
1)覆盖
2)基类的指针或者引用
虚表的运行原理:
一、多态的特例 – 虚析构函数构成多态
类有指针作为数据成员,必须要写析构函数,如果当前类被继承了,则析构函数写成virtual,
为了实现多态,将子类的空间被合理的释放掉,防止内存泄漏
1、为什么
1)没有加虚的情况示例:
class A
{
public:
//基类的构造、析构函数析构函数不能继承
A()
{
cout<< "A" <<endl;
m_i = new int;
}
//virtual ~A()//基类的虚析构函数,可以继承
~A()
{
cout << "~A" <<endl;
delete[] m_i;
}
private:
int* m_i;
};
class B:public A
{
public:
B()
{
cout<< "B" <<endl;
m_j = new int;
}
~B()
{
cout << "~B" <<endl;
delete[] m_j;
}
private:
int *m_j;
};
void test(A* pb)
{
delete pb;
}
//结果为AB~A,没有运行B的析构函数
void main()
{
A* pb = new B;//定义了一个基类类型的指针pb,让其指向子类对象,pb为A类类型的指针,
delete pb;
//test(pb);
}
运行结果:
发现并没有运行基类A的析构函数,造成了内存泄漏。
2)加上虚:virtual ~A()
而加了虚之后,就正常运行了基类的析构函数,解决了内存泄漏的问题。
2、虚析构函数:
析构函数是类的一个特殊的成员函数:
1)当一个对象的生命周期结束时,
系统会自动调用析构函数注销该对象并进行善后工作,
对象自身也可以调用析构函数;
2)析构函数的善后工作是:
释放对象在生命期内获得的资源(如动态分配的内存,内核资源);
3)析构函数也用来执行对象即将被撤销之前的任何操作。
根据赋值兼容规则,可以用基类的指针指向派生类对象,如果使用基类型指针指向动态创建的派生类对象,由该基类指针撤销派生类对象,则必须将析构函数定义为虚函数,实现多态性,自动调用派生类析构函数,否则可能存在内存泄漏问题。
总结:在实现运行时的多态,无论其他程序员怎样调用析构函数都必须保证不出错,所以必须把析构函数定义为虚函数。
注意:类中没有虚函数,类中没有指针,就不要把析构函数定义为虚。
在动态分配内存时所有C++的标准库函数都采用这种格式。
3、定义虚函数的规则
类的成员函数定义为虚函数,但必须注意以下几条:
1)派生类中定义虚函数必须与基类中的虚函数同名外,还必须同参数表,同返回类型。否则被认为是同名覆盖,不具有多态性。如基类中返回基类指针,派生类中返回派生类指针是允许的,这是一个例外(协变)。
2)有类的成员函数才能说明为虚函数。这是因为虚函数仅适用于有继承关系的类对象。友元函数和全局函数也不能作为虚函数。
3)静态成员函数,是所有同一类对象共有,不受限于某个对象,不能作为虚函数。
4)内联函数每个对象一个拷贝,无映射关系,不能作为虚函数。
5)构造函数和拷贝构造函数不能作为虚函数。构造函数和拷贝构造函数是设置虚表指针。
6)析构函数可定义为虚函数,构造函数不能定义虚函数,因为在调用构造函数时对象还没有完成实例化(虚表指针没有设置)。在基类中及其派生类中都动态分配的内存空间时,必须把析构函数定义为虚函数,实现撤消对象时的多态性。
7)实现运行时的多态性,必须使用基类类型的指针变量或引用,使该指针指向该基类的不同派生类的对象,并通过该指针指向虚函数,才能实现运行时的多态性。
8)在运行时的多态,函数执行速度要稍慢一些:为了实现多态性,每一个派生类中均要保存相应虚函数的入口地址表,函数的调用机制也是间接实现。所以多态性总是要付出一定代价,但通用性是一个更高的目标。
9)如果定义放在类外,virtual只能加在函数声明前面,不能(再)加在函数定义前面。正确的定义必须不包括virtual。
4、虚函数的默认参数是静态绑定的
虚函数的默认参数是静态绑定的,在子类中重新定义虚函数的时候,并没有重新定义继承而来的参数值。
除非是在实际调用过程中传递了想要的参数。
代码示例:
class Parent
{
public:
virtual void fn(int a = 10)
{
cout << "parent fn a = " << a << endl;
}
};
class Child:public Parent
{
public:
virtual void fn(int b = 100)//进行了重写/覆盖
{
cout << "child fn b = " << b << endl;
}
};
void main()
{
Child cc;
Parent *p = &cc;
p->fn(200);//child fn b = 200
p->fn();//产生了动态绑定,变成了10,不是100,
cc.fn();//为静态绑定,为100
}
运行结果:
5、虚函数设计
如果使用对象调用,只能调用基类和子类两者共有的函数;
如果没有重写,那么调用也没有意义;
class A
{
public:
virtual void fn(){ cout << "A::fn" << endl; }
virtual void ff(){ cout << "A::ff" << endl; }
};
class B:public A
{
public:
virtual void ff(){ cout << "B::ff" << endl; }
virtual void fg(){ cout << "B::fg" << endl; }
};
void test(A& a)
{
//a.fg();//error,不能调用
//a.fn();//可以调用,但是无意义,没有产生多态
a.ff();
}
void main()
{
A a;
B b;
test(a);
test(b);
}
运行结果:
6、为什么构造函数不可以是虚函数呢?
1)构造函数的用途:
(1)创建对象,
(2)初始化对象中的属性,
(3)类型转换。
⒉)类中定义了虚函数就会有一个虚函数表(vftable),对象模型中就含有一个指向虚表的指针(__vfptr)。在定义对象时构造函数设置虚表指针指向虚函数表。
3)使用指针和引用调用虚函数,在编译只需要知道函数接口,运行时指向具体对象,才能关联具体对象的虚方法(通过虚函数指针查虚函数表得到具体对象中的虚方法)
4)构造函数是类的一个特殊的成员函数:
(1)定义对象由系统自动调用构造函数,对象自己是不可以调用构造函数;
(2)构造函数的调用属于静态联编,在编译时必须知道具体的类型信息。
5)如果构造函数可以定义为虚构造函数,使用指针调用虚构造函数,如果编译器采用静态联编,构造函数就不能为虚函数。如果采用动态联编,运行时指针指向具体对象,使用指针调用构造函数,相当于已经实例化的对象在调用构造函数,这是不容许的调用,对象的构造函数只执行一次。
6)如果指针可以调用虚构造函数,通过查虚函数表,调动虚构造函数,那么,当指针为nullptr,如何查虚函数表呢?
7)构造函数的调用是在编译时确定,如果是虚构造函数,编译器怎么知道你想构建是继承树上的哪种类型呢?
总结:构造函数不允许是虚函数。
测试代码
class A
{
public:
A(){ cout << "A" << endl; }
~A(){ cout << "~A" << endl; }
private:
int *m_i;
};
class B
{
public:
B(){m_i = new int; cout << "B" << endl; }
~B()
{
cout << "~B" << endl;
if( m_i != NULL )//防止显示的调用析构,导致出错
{
delete[] m_i;
m_i = NULL;
}
}
private:
int *m_i;
};
A a()//一般不这么写,但语法正确
{
cout << "aaa" << endl;
return A();
}
void main()
{
A();//声明了一个局部的无名对象
A a();//定义了一个函数,返回值为A类类型,函数名为a,函数内无参数
a();
cout<<endl;
/*
A a;
a.A();//error
a.~A();//程序员显示的调用析构函数
B b;
b.~B();
*/
}
二、纯虚函数
纯虚函数,不需要写实现体。
1、纯虚函数的概念:
纯虚函数(pure virtual function)是指没有具体实现的虚成员函数。它用于这样的情况:设计一个类型时,会遇到无法定义类型中虚函数的具体实现,其实现依赖于不同的派生类。
定义纯虚函数的一般格式为:
virtual 返回类型 函数名(参数表) = 0;
“=O" 表明程序员将不定义该虚函数实现,没有函数体,只有函数的声明;函数的声明是为了在虚函数表中保留一个位置。
“=O"本质上是将指向函数体的指针定义为nullptr。
三、抽象类
包含纯虚函数的类,为抽象类,不能定义对象,但是可以定义抽象类的指针或者引用 来 指向或者引用具体类的对象
抽象类的作用:派生子类,作为类族最上面的基类出现,如果派生出子类,则在子类中必须要全部重写基类中的纯虚函数,其才可称为具体类,如果在子类中,没有实现纯虚函数,则子类也是抽象类
1、抽象类的概念:
含有纯虚函数的类是抽象类。
抽象类是一种特殊的类,它是为抽象的目而建立的,它处于继承层次结构的较上层。
抽象类不能实例化对象,因为纯虚函数没有实现部分,所以含有纯虚函数类型不能实例化对象;
2、抽象类的主要作用:
将相关的类型组织在一个继承层次结构中,抽象类为派生类型提供一个公共的根,相关的派生类型是从这个根派生而来。
3、抽象类的使用规则:
(1) 抽象类只能用作其他类的基类,不能创建抽象类的对象。(2)抽象类不能用作参数类型、函数返回类型或显式类型转换。
(3) 可以定义抽象类的指针和引用,此指针可以指向(引用可以引用)它的派生类的对象,从而实现运行时多态。
4、注意:
抽象类只能作为基类来使用,其纯虚函数的实现由派生类给出。如果派生类没有重新定义纯虚函数,而派生类只是继承基类的纯虚函数,则这个派生类仍然还是一个抽象类。如果派生类中给出了基类纯虚函数的实现,则该派生类就不再是抽象类了,它是一个可以建立对象的具体类型。
四、使用示例:
设计一个:形状类 – 包含矩形、圆形、三角形,分别计算三种图形的周长和面积
或:人类 – 教师类、学生类、工人。。。
1、一种实现示例(没有使用纯虚函数)
class Shape//形状类
{
public:
//这两个函数不实现又不使用,为什么要写?
//为了实现后面的覆盖,为了实现多态
virtual void Area(){}//面积
virtual void Girth(){}//周长
};
class Circle:public Shape//圆形类
{
public:
virtual void Area()
{
cout << "Circle Area" << endl;
}
virtual void Girth()
{
cout << "Circle Girth" << endl;
}
private:
int m_ra;
};
class Rectangle:public Shape//矩形
{
public:
virtual void Area()
{
cout << "Rectangle Area" << endl;
}
virtual void Girth()
{
cout << "Rectangle Girth" << endl;
}
private:
int m_length;
int m_width;
};
class Triangle:public Shape//三角形类(这里设计为等边三角形)
{
public:
virtual void Area()
{
cout << "Triangle Area" << endl;
}
virtual void Girth()
{
cout << "Triangle Girth" << endl;
}
private:
int m_length;
};
//一种可行的调用形式
void test(Shape* p)
{
p->Area();
p->Girth();
}
int main()
{
Shape *pf[3];//指针数组
pf[0] = new Rectangle;
pf[1] = new Circle;
pf[2] = new Triangle;
for(int i = 0;i < 3 ;i++)
{
pf[i]->Area();
pf[i]->Girth();
delete pf[i];
pf[i] = NULL;
}
Shape a;
a.Area();
a.Girth();
}
2、使用示例(纯虚函数 – 抽象类)
class Shape//形状类 --为抽象类
{
public:
//这两个函数不实现又没有办法去写其实现体,为什么要写?
//为了实现后面的覆盖,为了实现多态
//纯虚函数,不需要写实现体
virtual void Area() = 0;//面积
virtual void Girth() = 0;//周长
};
class Circle:public Shape//圆形类
{
public:
Circle(int r) :m_ra(r) {}
virtual void Area()
{
cout << "Circle Area = " << 3.14 * m_ra * m_ra << endl;
}
virtual void Girth()
{
cout << "Girth Girth = " << 2 * 3.14 * m_ra<< endl;
}
private:
int m_ra;
};
class Rectangle:public Shape//矩形类
{
public:
Rectangle(int l,int w):m_length(l),m_width(w){}
virtual void Area()
{
cout << "Rectangle Area = " << m_length * m_width << endl;
}
virtual void Girth()
{
cout << "Rectangle Girth = " << 2 * (m_length + m_width) << endl;
}
private:
int m_length;
int m_width;
};
class Triangle:public Shape//等边三角形类
{
public:
Triangle(int l):m_length(l){}
virtual void Area()
{
cout << "Triangle Area = " << endl;
}
virtual void Girth()
{
cout << "Triangle Girth = "<< 3 * m_length << endl;
}
private:
int m_length;//边长
};
int main()
{
Shape *pf[3];//指针数组
pf[0] = new Rectangle(6,6);
pf[1] = new Circle(6);
pf[2] = new Triangle(6);
for(int i = 0;i < 3 ;i++)
{
pf[i]->Area();
pf[i]->Girth();
delete pf[i];
pf[i] = NULL;
}
//Shape a;//error,抽象类不能定义对象
//a.Area();
//a.Girth();
}
运行结果:
五、虚继承的设计 – 虚基类
虚基类 – 多继承情况下的菱形继承,将相同的属性或操作只保留一份。
示例:
再现实生活中,没有两个没有关系的类,生成第三个类
错误的设计
class Sofa//沙发类
{
public:
void sit()
{
cout << "sit" << endl;
}
private:
int m_size;
};
class Bed//床类
{
public:
void sit()
{
cout << "sit" << endl;
}
private:
int m_size;
};
class Sofabed:public Sofa,public Bed
{
};
void main()
{
Sofabed ss;
//ss.sit();//error,不明确
ss.Sofa::sit();//显示调用
ss.Bed::sit();
}
运行结果:
菱形设计 – 未添加虚继承 – 不合理设计
class Furnitrue//家具类
{
public:
void sit(){ cout << "sit" << endl; }
private:
int m_size;
};
class Sofa:public Furnitrue//沙发类
{
};
class Bed:public Furnitrue//床类
{
};
class Sofabed:public Sofa,public Bed
{
};
void main()
{
Sofabed ss;
Sofa s;
Bed b;
cout << sizeof(ss) << endl;
cout << sizeof(Sofa) << endl;
cout << sizeof(Bed) << endl;
}
菱形设计 – 虚继承 – 虚基类 – 合理设计
相同的部分只继承了一份,只在第一次继承的时候完成。
class Furnitrue//家具类
{
public:
void sit(){ cout << "sit" << endl; }
private:
int m_size;
};
class Sofa:public virtual Furnitrue//沙发类
{
};
class Bed:public virtual Furnitrue//床类
{
};
class Sofabed:public Sofa,public Bed
{
};
void main()
{
Sofa s1;
Bed s2;
Sofabed ss;
cout << sizeof(s1) << endl;//为8字节,其中还有一个虚指针,指向当前的虚基类
cout << sizeof(s2) << endl;//8
cout << sizeof(ss) << endl;//12,内部还含有两个虚指针,指向虚表,虚表内保存的是偏移量
}