c++面向对象三大特性——多态

本文详细解释了多态的概念,包括其定义、实现方式(如虚函数、重写),以及抽象类的作用。重点介绍了虚函数表的工作原理,多继承下的虚函数处理,以及重载、重写和重定义的区别。同时说明了静态成员和构造函数不能是虚函数,析构函数可以是虚函数,且生成虚函数表的时间和位置。
摘要由CSDN通过智能技术生成

目录

一,什么是多态?

二,多态的定义和实现

2.1虚函数

2.2重写

2.3多态实现的条件和引用场景

2.4虚函数重写的两个例外

2.4.1协变

2.4.2析构函数的重写

三,抽象类

四,多态的原理

4.1虚函数表

4.1.1虚函数表是什么?

4.1.2虚函数表里有什么?

4.1.3打印虚表

4.2多继承的虚函数表

五,补充


一,什么是多态?

通俗来说,就是不同的对象去完成某个行为时,会产生出不同的形态,为不同的数据类型的对象提供统一的接口

举个例子:买票时,普通人买票时全价购买,学生买票是半价,军人买票时是优先买票。

二,多态的定义和实现

2.1虚函数

在类中,被virtual关键字修饰的成员函数称为虚函数

class Person1
{
public:
	//虚函数
	virtual void BuyTicket()
	{
		cout << "买票全价" << endl;
	}
};

2.2重写

重写也称为覆盖,当派生类有一个函数名函数参数返回值和基类对应的函数都相同的时候,称为派生类虚函数重写了基类的虚函数。(如果函数参数不同但是没有使用,也不构成重写)

class Person
{
public:
	virtual void BuyTicket()
	{
		cout << "买票全价" << endl;
	}
};

class Student : public Person
{
public:
	//这里叫做虚函数的额重写,函数名参数和返回值都要求相同,如果参数不同但是没有使用参数,也不构成重写
	//如果不符合重写就是隐藏关系
	virtual void BuyTicket()
	{
		cout << "买票半价" << endl;
	}
};

注:如果基类虚函数加了virtual而派生类虚函数没加,依然可以构成重写,因为基类虚函数被继承下来后,派生类依然保留了基类虚函数的特性,但是为了规范,建议子类虚函数也加上virtual。

2.3多态实现的条件和引用场景

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

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

class Person
{
public:
	virtual void BuyTicket()
	{
		cout << "买票全价" << endl;
	}
};

class Student : public Person
{
public:
	virtual void BuyTicket()//完成虚函数重写
	{
		cout << "买票半价" << endl;
	}
};
//此处完成对基类虚函数的引用调用
void Func(Person& p)
{
	p.BuyTicket();
}

int main()
{
	Person p;
	Func(p);

	Student s;
	Func(s);
	return 0;
}

如果不通过引用或者指针调用,就变成下面这样

2.4虚函数重写的两个例外

2.4.1协变

派生类重写虚函数时,与基类虚函数返回值类型不同,如下代码,依然也构成重写。

class Person
{
public:
	//基类虚函数返回基类对象的指针或引用
	virtual Person& BuyTicket()
	{
		return *this;
	}
};
		
class Student : public Person
{
public:
	//派生类返回派生类对象的指针或引用
	virtual Student& BuyTicket()
	{
		return *this;
	}
};

2.4.2析构函数的重写

当基类的析构函数为虚函数时,此时派生类析构函数只要定义,无论是否加virtual关键字,无论名字是否相同,都与基类的析构函数构成重写。函数名不同看起来违背了重写的规则,但正如小标题所言,这是重写的一个特例。之所以有这个特例,是因为编译器对析构函数的名称做了特殊处理,编译后析构函数的名称统一处理成destructor

class Person {
public:
	virtual ~Person() 
	{ 
		cout << "~Person()" << endl;
	}
};
class Student : public Person {
public:
	virtual ~Student() 
	{ 
		cout << "~Student()" << endl; 
	}
};
int main()
{
	Person* p1 = new Person;
	Person* p2 = new Student;
	delete p1;
	delete p2;
	return 0;
}

三,抽象类

在虚函数后面加上“=0”,这个虚函数被称为纯虚函数,包含纯虚函数的类叫做抽象类,也叫做接口类,抽象类不能实例化出对象,并且被派生类继承后也不能实例化出对象。只有重写虚函数派生类才能实例化出对象。纯虚函数规范了派生类必须重写虚函数,纯虚函数也体现了接口继承

class Car
{
public:
	virtual void Drive() = 0;
};

class BMW : public Car
{
public:
	virtual void Drive()
	{
		cout << "操控-好开" << endl;
	}
};

class Benz : public Car
{
public:
	virtual void Drive()
	{
		cout << "环境-舒适" << endl;
	}
};
int main5()
{
	//Car c;
	//BMW b;//纯虚函数不能实例化出对象,父类子类都不能
	Car* ptr = new BMW;
	ptr->Drive();

	ptr = new Benz;
	ptr->Drive();
	return 0;
}

补充:普通函数的继承是一种实现继承,派生类继承了基类函数,可以使用函数,继承的是函数的实现。虚函数的继承是一种接口继承,派生类继承的是基类虚函数的接口,目的是为了重写,达成多态,继承的是接口。如果不实现多态,不要把函数定义成虚函数
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*p = new B;
    p->test();
    return 0;
}

四,多态的原理

4.1虚函数表

4.1.1虚函数表是什么?

注:下面打印虚函数的操作都需要在32位下才能运行

上图是一个普通的虚函数创建和实例化对象,查看对象的地址后可以发现,除了_a有自己的地址外,还有一个_vfptr的东西。

这个_vfptr默认创建在类前面的位置,我们叫做虚函数表指针,简称虚表,咱们在类里面创建的虚函数的地址都存在这个东西里面,由于_vgptr存的都是指针,所以本质是一个指针数组。

4.1.2虚函数表里有什么?

class Person
{
public:
	virtual void BuyTicket()
	{
		cout << "Person::买票-全价" << endl;
	}

	virtual void Func1()
	{
		cout << "Person::Func1()" << endl;
	}
};
//只要是虚函数都会存虚表里去
class Student : public Person 
{
public:
	virtual void BuyTicket()
	{
		cout << "Student::买票-半价" << endl;
	}
	virtual void Func2()
	{
		cout << "Student::Func2()" << endl;
	}
};

可以看到,不同对象有着自己的虚表,调用时也会去自己的虚表里调用自己实现的虚函数,这样就实现了不同对象完成同一行为时,展现不同的形态。

4.1.3打印虚表

从上图也可以发现BuyTicket和Func1都存进虚表里了,但是没有显示Func2,其实这里不是没有存进去,只是VS在这里做了点处理,导致Func2没有显示。

那我们可以通过下面的函数打印虚表

typedef void(*VFPTR) ();//指针数组的typedef
void PrintVFTable(VFPTR vTable[])
{
	cout << " 虚表地址>" << vTable << endl;
	for (int i = 0; vTable[i] != nullptr; ++i)
	{
		printf(" 第%d个虚函数地址 :%p,->", i, vTable[i]);
		vTable[i]();//直接调用指针数组中指针指向的函数
	}
	cout << endl;
}
int main()
{
	Person p;
	Student s;
	PrintVFTable((VFPTR*)*(int*)&p);//先把地址搞成四个,然后强转int得到四个字符,然后再强转
	PrintVFTable((VFPTR*)*(int*)&s);
	return 0;
}

上图可以看出,Func2确实是有被放进虚表里。

4.2多继承的虚函数表

class Base1 {
public:
	virtual void func1() { cout << "Base1::func1" << endl; }
	virtual void func2() { cout << "Base1::func2" << endl; }
private:
	int b1 = 1;
};

class Base2 {
public:
	virtual void func1() { cout << "Base2::func1" << endl; }
	virtual void func2() { cout << "Base2::func2" << endl; }
private:
	int b2 = 2;
};

class Derive : public Base1, public Base2 {
public:
	virtual void func1() { cout << "Derive::func1" << endl; }
	virtual void func3() { cout << "Derive::func3" << endl; }
private:
	int d = 3;
};

typedef void(*VFPTR) ();//指针数组的typedef
void PrintVFTable(VFPTR vTable[])
{
	cout << " 虚表地址>" << vTable << endl;
	for (int i = 0; vTable[i]!=nullptr; ++i)
	{
		printf(" 第%d个虚函数地址 :%p,->", i, vTable[i]);
		vTable[i]();//直接调用指针数组中指针指向的函数
	}
	cout << endl;
}
int main()
{
	Derive d;
	VFPTR* vTableb1 = (VFPTR*)(*(int*)&d);
	PrintVFTable(vTableb1);

	VFPTR* vTableb2 = (VFPTR*)(*(int*)((char*)&d + sizeof(Base1)));
	PrintVFTable(vTableb2);
	return 0;
}

五,补充

1,重载,重写,重定义。

重载:两个函数在同一作用域,并且函数名和参数(类型,顺序,个数)要不同。

重写(覆盖):两个函数分别在基类和派生类两个作用域,且函数名,参数,返回值也必须相同,而且必须是虚函数。

重定义(隐藏):两个函数分别在基类和派生类两个作用域,函数名相同的情况下,如果不构成重写那就是构成重定义。

 2,静态成员不能是虚函数,因为静态成员没有this指针,使用访问作用域操作符“::”无法访问虚函数表,所以静态成员无法放进虚函数表。

3,构造函数不能是虚函数,因为对象创建虚表就是在构造函数初始化列表的时候创建的。

4,析构函数可以是虚函数,且最好把基类的析构函数定义成虚函数。

5,虚函数表在什么阶段生成,存在哪里?

如下代码:

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

class B : public A {};

int main()
{
	B b;
	printf("虚表:%p\n", *((int*)&b));//可以看出虚表存在常量区

	static int x = 0;
	printf("static变量:%p\n", &x);

	const char* ptr = "hello world";
	printf("常量:%p\n", ptr);

	int y = 0;
	printf("局部变量:%p\n", &y);

	printf("new变量:%p\n", new int);


	return 0;
}

可以看出,虚表存在常量区

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值