【C++】多态(上)


个人主页~


多态

一、多态的概念

用大白话讲就是完成某个行为,不同对象去完成会产生不同状态,C++多态就是在不同继承关系的类对象,去调用同一函数,产生了不同的行为

二、多态的定义以及实现

1、多态的构成条件

必须通过基类的指针或者引用调用虚函数

被调用的函数必须是虚函数,且派生类必须对基类的虚函数进行重写

2、虚函数

被virtual修饰的类成员函数被称为虚函数

cpp 复制代码
class A
{
public:
	virtual void B()
	{
		cout << "virtual void B()" << endl;
	}
};

B函数是一个虚函数,这里的virtual与虚拟继承的virtual没有关系,只是它们表示相同的意思,关键字用在不同的对象上有不同的效果

3、虚函数的重写

派生类中有一个跟基类相同的虚函数(即派生类虚函数与基类虚函数的返回值类型、函数名字、参数列表完全相同),称子类的虚函数重写了基类的虚函数

cpp 复制代码
class A 
{
public:
	virtual void C()
	{
		cout << "A : virtual void C()" << endl;
	}
};

class B : public A
{
public:
	virtual void C()
	{
		cout << "B : virtual void C()" << endl;
	}
};

void test(A& a)
{
	a.C();
}

int main()
{
	A a;
	B b;
	test(a);
	test(b);

	return 0;
}

虚函数重写的两个特殊情况

①协变

所谓协变就是基类与派生类虚函数返回值类型不同

派生类重写基类虚函数时,与基类虚函数返回值类型不同,即基类虚函数返回基类对象的指针或者引用,派生类虚函数返回派生类对象的指针或者引用时,称为协变

cpp 复制代码
class A 
{};
class B : public A 
{};

class C 
{
public:
	virtual A* f() 
	{
		return new A; 
	}
};
class D : public C 
{
public:
	virtual B* f() 
	{
		return new B; 
	}
};
② 析构函数的重写

析构函数的重写的特征是基类与派生类析构函数的名字不同

如果基类的析构函数为虚函数,此时派生类析构函数只要定义,无论是否加virtual关键字,都与基类的析构函数构成重写,虽然基类与派生类析构函数名字不同,虽然函数名不相同,看起来违背了重写的规则,其实不然,这里可以理解为编译器对析构函数的名称做了特殊处理,编译后析构函数的名称统一处理成destructor()

cpp 复制代码
class A 
{
public:
	virtual ~A() 
	{
	 cout << "~A()" << endl; 
	}
};
class B : public A 
{
public:
	virtual ~B() 
	{
	 cout << "~B()" << endl; 
	}
};

int main()
{
	A* pa = new A;
	B* pb = new B;

	delete pa;
	cout << endl;
	delete pb;
	return 0;
}

这样的设计巧妙的将父子类的析构函数关系在一起,当然这其实是一个设计失误,这个多态或者说继承的概念是在析构函数的概念之后产生的,所以作为第一个面向对象的语言,就只能通过打补丁,也就是编译器将两个析构函数转化为destructor()保证它们同名

4、C++11的override和final

从上面可以看出,C++对函数重写的要求比较严格,但是有些情况下由于疏忽,可能会导致函数名字母次序写反而无法构成重载,而这种错误在编译期间是不会报出的,只有在程序运行时没有得到预期结果才来debug会得不偿失,因此:C++11提供了override和final两个关键字,可以帮助用户检测是否重写

(1)final

final用来修饰虚函数,表示该虚函数不能再被重写

(2)override

override用来检查派生类虚函数是否重写了基类某个虚函数,如果没有重写编译报错

5、重载、重写、隐藏的对比

重写也叫覆盖,隐藏也叫重定义

三、抽象类

1、概念

在虚函数的后面写上 =0 ,则这个函数为纯虚函数包含纯虚函数的类叫做抽象类 (也叫接口类),抽象类不能实例化出对象 ,派生类继承后也不能实例化出对象,只有重写纯虚函数,派生类才能实例化出对象,纯虚函数规范了派生类必须重写,另外纯虚函数更体现出了接口继承

cpp 复制代码
class A
{
public:
	virtual void D() = 0;
};
class B :public A
{
public:
	virtual void D()
	{
		cout << "BBBBBBBBBB" << endl;
	}
};
class C :public A
{
public:
	virtual void D()
	{
		cout << "CCCCCCCCCC" << endl;
	}
};
void Test()
{
	A* pb = new B;
	A* pc = new C;
	pb->D();
	pc->D();
}

2、接口继承和实现继承

普通函数的继承是一种实现继承,派生类继承了基类函数,可以使用函数,继承的是函数的实现,虚函数的继承是一种接口继承,派生类继承的是基类虚函数的接口,目的是为了重写,达成多态,继承的是接口 ,所以如果不实现多态,不要把函数定义成虚函数

派生类继承基类虚函数接口的一个实例

cpp 复制代码
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(int argc, char* argv[])
{
    B* p = new B;
    p->test();
    return 0;
}

这里就是因为派生类继承基类接口,重写函数,但是接口还是基类的,函数是被重写的,所以这里给的缺省值应该取基类A类func函数中的

四、多态的原理

1、虚函数表

cpp 复制代码
class A
{
public:
	virtual void func1()
	{
		cout << "virtual void func1()" << endl;
	}
private:
	int _a;
};

int main()
{
	cout << sizeof(A) << endl;
	return 0;
}

我们发现A类的大小为8bytes,除了私有成员_a以外还有一个_vtfptr的指针,我们把它叫做虚函数表指针,一个含有虚函数的类中都至少都有一个虚函数表指针,因为虚函数的地址要被放到虚函数表中,虚函数表简称虚表

cpp 复制代码
class A
{
public:
	virtual void Func1()
	{
		cout << "A::Func1()" << endl;
	}
	virtual void Func2()
	{
		cout << "A::Func2()" << endl;
	}
	void Func3()
	{
		cout << "A::Func3()" << endl;
	}
private:
	int _a = 1;
};

class B : public A
{
public:
	virtual void Func1()
	{
		cout << "B::Func1()" << endl;
	}
private:
	int _b = 2;
};

int main()
{
	A a;
	B b;
	return 0;
}

派生类对象b中也有一个虚表指针,b对象由两部分构成,一部分是父类继承下来的成员,虚表指针也就是存在部分的另一部分是自己的成员

基类a对象和派生类b对象虚表是不一样的,这里我们发现Func1完成了重写,所以b的虚表中存的是重写的B::Func1,所以虚函数的重写也叫作覆盖,覆盖就是指虚表中虚函数的覆盖,重写是语法的叫法,覆盖是原理层的叫法

另外Func2继承下来后是虚函数,所以放进了虚表,Func3也继承下来了,但是不是虚函数,所以不会放进虚表

虚函数表本质是一个存虚函数指针的指针数组,一般情况这个数组最后面放了一个nullptr

总结一下派生类的虚表生成:

a.先将基类中的虚表内容拷贝一份到派生类虚表中

b.如果派生类重写了基类中某个虚函数,用派生类自己的虚函数覆盖虚表中基类的虚函数

c.派生类自己新增加的虚函数按其在派生类中的声明次序增加到派生类虚表的最后

注意虚表存的是虚函数指针,不是虚函数,虚函数和普通函数一样的,都是存在代码段的 ,只是他的指针又存到了虚表中,另外对象中存的不是虚表,存的是虚表指针,虚表在VS下存在于代码段


今日分享就到这里了~

相关推荐
XiaoLeisj2 小时前
【JavaEE初阶 — 多线程】单例模式 & 指令重排序问题
java·开发语言·java-ee
dayouziei2 小时前
java的类加载机制的学习
java·学习
励志成为嵌入式工程师3 小时前
c语言简单编程练习9
c语言·开发语言·算法·vim
捕鲸叉3 小时前
创建线程时传递参数给线程
开发语言·c++·算法
A charmer3 小时前
【C++】vector 类深度解析:探索动态数组的奥秘
开发语言·c++·算法
Peter_chq3 小时前
【操作系统】基于环形队列的生产消费模型
linux·c语言·开发语言·c++·后端
aloha_7894 小时前
从零记录搭建一个干净的mybatis环境
java·笔记·spring·spring cloud·maven·mybatis·springboot
记录成长java5 小时前
ServletContext,Cookie,HttpSession的使用
java·开发语言·servlet
前端青山5 小时前
Node.js-增强 API 安全性和性能优化
开发语言·前端·javascript·性能优化·前端框架·node.js
青花瓷5 小时前
C++__XCode工程中Debug版本库向Release版本库的切换
c++·xcode