多态

本文深入探讨C++中的多态概念及其实现机制,包括虚函数的应用、静态与动态联编的区别、虚析构函数的重要性、多态原理及其实现细节。此外,还介绍了纯虚函数和抽象类的概念,以及如何利用抽象类作为接口。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

一、多态:同样的调用语句有多种不同的表现形式。

通俗的说就是根据传入对象类型的不同,调用不同的派生类的相应函数

多态的实现条件:

1、要存在继承关系

2、对虚函数的重写

3、基类指针 (引用) 指向派生类对象


二、静态联编与动态联编

静态联编:程序匹配,连接在编译阶段实现,也称为早期联编(在编译的时候,就知道了该去调用谁)

例如:函数重载

动态联编:程序联编推迟到运行时进行,也称为晚期联编(在执行时,才会知道调用哪一个函数)

例如:switch 和 if 语句

#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)
{
	// 1、p 是一个Animal 类型指针
	// 2、print 是一个普通的成员函数
	// 3、结论:调用 Animal 的 print 函数

	// 编译阶段完成 ---->  静态联编    ---->  早期联编
	p->print();
}

// 问题:
// 根据传入的对象类型的不同,调用不同派生类的相应函数
// 函数根据指针类型,只能调用该类型的函数,也就是只能调用基类的函数

// 期望的结果:根据传入的对象类型的不同,调用不同派生类的相应函数
// 解决的办法:虚函数,在基类函数之前 加上 virtual 关键字,将该函数变为虚函数
// 则当基类指针调用 虚函数的时候 会根据对象的不同调用不同的函数
void func1(Animal *p)
{
	// 1、p 是一个Animal 类型指针
	// 2、sleep 是一个 虚函数
	// 3、结论:调用 谁的sleep?  因为不知道 p 的类型,所以不知道调用哪个 sleep
	// 何时确定调用谁?   ------>   运行的时候   ----->  动态联编   晚期联编  迟绑定
	p->sleep();      // 多态
}

// 多态 实现条件:
// 1、继承
// 2、虚函数
// 3、父类指针指向子类对象
int main2_1()
{
	Animal *pa = new Animal(2, "动物");
	Cat    *pc = new Cat(3, "猫");
	Fish   *pf = new Fish(2, "鱼");

	//func(pa);      动物睡觉
	//func(pc);	 动物睡觉
	//func(pf);	 动物睡觉

	func1(pa); 	 动物睡觉
	func1(pc);	 猫 趴着睡觉
	func1(pf);	 鱼 睁着眼睡觉
	
    return 0;
}

分析:不是虚函数之前,func的形参为 Animal 类的指针,基类指针指向派生类对象,基类指针

的本质依旧是 Animal* ,因此在编译的时候就明确了,该调用哪个函数,这是个静态联编。

而加了 virtual 来修饰基类的成员函数,以此来实现多态,成为虚函数之后,再通过基类指针

去调用函数时,会根据传入对象的不同,找到相应的函数来执行。


二、虚析构函数

通过基类指针释放派生类对象,基类的析构函数一定是虚析构函数


构造函数:从当前类往上找 父类 => 最上层的父类,从最上层的父类开始构造

(调用构造函数),类似于前序递归

析构函数:从当前类开始析构  析构完 沿着继承路径往上找父类 析构父类 => 找到

最上层的父类析构,类似于后序递归

#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;
};


// abcdefg   
void printS(char *p) 
{	
	if (*p == '\0') 
		return ;
	printf ("%c", *p);

	printS(p+1);   // 递归
	//printf ("%c", *p);

}

// 先递归  后执行
// 先执行  后递归

// 构造 从当前类网上找 父类 ------>  最上层的父类, 从最上层的父类开始构造(调用构造函数)       前序递归
// 析构 从当前类开始析构  析构完 沿着继承路径网上找父类 析构父类 ----->  找到最上层的父类 析构   后序递归

void func(A *pa)
{
	// 指针是 A  所以认为析构的是A 对象
	
	// 通过基类指针释放派生类对象, 基类的析构函数一定要是 虚析构函数
	delete pa;
}


int main()
{
	//A *pa = new A;
	//func(pa);

	A *pa = new B;
	func(pa);

    return 0;
}

// 运行结果:
// A的构造函数
// B的构造函数
// A的析构函数
// 没有B的析构函数,会造成内存的泄漏

原因:A *pa = new B 定义了一个B类的对象,会先调用A类的构造函数,再调用B类的构造函数,再调用 func() 函数,释放的是基类的指针

被认为析构的是基类的对象,所以要想基类指针释放派生类对象,基类的析构函数一定要是虚析构函数

 

三、多态的原理

多态通过一个虚函数指针 (vfptr) 来实现,这个虚函数指针指向一个虚函数表,这张表里存放了该类对应的虚函数。

基类有虚函数,基类的虚函数指针就会指向一张虚函数表,这张表由编译器保管。

派生类中有与基类虚函数同名的函数也是虚函数,但与基类有所不同的是,派生类中的虚函数指针不在指向基类中

的虚函数表,而是指向自己的虚函数表

因此,当调用函数时,编译器会根据对象的虚函数指针找到对应的虚函数表,再找到虚函数。

虚函数在编译的时候,并不知道谁在调用,在运行的时候才会确定,因此是动态联编,效率

会低于一般的函数


四、虚函数和虚继承

1、基类有虚函数,派生类没有虚函数,普通继承 

vfptr -> AA ::print();

a

b


2、基类有虚函数,派生类没有虚函数,虚继承

vbptr -> 当前对象指针  虚基类指针

b

vfptr -> 基类指针

a


3、基类没有虚函数,派生类有虚函数,普通继承

vfptr -> BB::show()

a

b


4、基类没有虚函数,派生类中有虚函数,虚继承

vfptr -> 当前对象指针

vbptr

b

a


5、基类有虚函数,派生类中有虚函数,普通继承

派生类中的虚函数 与 基类中的虚函数 不同名时

vfptr

a

b

派生类中的虚函数 与 基类中的虚函数 同名时

对象 b

vfptr -> BB::print()  BB ::show()

a

b


6、基类有虚函数,派生类中有虚函数,虚继承时

派生类中的虚函数 与 基类中的虚函数 不同名时

vfptr -> 当前对象指针 -> BB::show() AA::print()

vbptr

b

vfptr -> 基类指针 -> AA::print()

a

派生类中的虚函数 与 基类中的虚函数 同名时

vfptr -> 当前对象指针 BB::print() BB::show() AA::print()

vbptr

b

00 00 00 00 

vfptr

a


五、构造函数中的虚函数调用

虚函数指针是分步初始化的

构造的顺序是先构造父类再构造子类

当调用父类的构造函数时,虚函数指针vfptr指向父类的虚函数表

当父类构造完,调用子类的构造函数的时候,虚函数指针vfptr指向子类的虚函数表

结论:构造函数中无法实现多态


六、用基类操作派生类数组

一般来讲,基类和派生类中的成员参数是不一样的

导致基类指针和派生类指针的步长不一致,所以不要

用基类指针操作派生类数组


七、纯虚函数和抽象类

纯虚函数:虚函数,只有声明,不需要实现,函数体部分改成 = 0

抽象类:拥有纯虚函数的类叫抽象类

抽象类不能实例化对象,但可以定义抽象类指针,用来操作派生类对象

派生类必须实现抽象类的所有 纯虚函数, 如果不实现,则派生类是一个抽象类

#include <iostream>
using namespace std;

// 图形的基类

// 抽象类:拥有纯虚函数的类叫 抽象类
// 抽象类不能实例化对象,但可以定义 抽象类指针, 用来操作派生类对象
// 派生类必须实现抽象类的所有  纯虚函数, 如果不实现, 则派生类将是一个抽象类
class Shape
{
public:
	// 纯虚函数:虚函数,只有声明,不需要实现  函数体部分改成  = 0;
	virtual double getS() = 0;
};

class Circle:public Shape
{
public:
	Circle(int r)
	{
		this->r = r;
	}
	virtual double getS()
	{
		return r*r*3.14;
	}
private:
	int r;
};

class Rectangle :public Shape
{
public:
	Rectangle(int a = 0,  int b = 0)
	{
		this->a = a;
		this->b = b;
	}
private:
	int a;
	int b;
};

void func (Shape *p)
{
	printf ("s = %.2f\n", p->getS());
}

int main()
{
	// Shape s;
	Rectangle r;

	Circle c(2);
	func(&c);

    return 0;
}

八、抽象类接口

#include <iostream>

using namespace std;

// 加法接口
class IAdd 
{
public:
	virtual void add() = 0;
	virtual void printResult() = 0;
};

// 乘法接口
class IMul 
{
public:
	virtual void mul() = 0;
	virtual void printResult() = 0;
};

class A:public IAdd, public IMul
{
public:
	A(int a = 0, int b = 0)
	{
		this->a = a;
		this->b = b;
	}
	virtual void add()
	{
		res = a + b;
	}
	virtual void mul()
	{
		res = a * b;
	}

	virtual void printResult()
	{
		printf ("res = %d\n", res);
	}
private:
	int a; 
	int b;
	int res;
};


void add(IAdd *p)
{
	p->add();
	p->printResult();
}

void mul(IMul *p)
{
	p->mul();
	p->printResult();
}


int main()
{
	//A b(2, 3);
	//b.add();
	//b.printResult();


	//b.mul();
	//b.printResult();

	A *pb = new A(2,3);
	add(pb);

	mul(pb);

    return 0;
}






 

 



评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值