C++18 -- 虚析构函数构成多态、纯虚函数、抽象类、虚继承

23 篇文章 1 订阅
3 篇文章 0 订阅

多态的条件:
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,内部还含有两个虚指针,指向虚表,虚表内保存的是偏移量
}

在这里插入图片描述

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值