【多态机制的实现原理】

82 阅读4分钟

前言

面向对象有三大特点:封装、继承、多态。封装可以把属性和方法封装在一个类中,这样当类对象做函数参数时即可以属性也可以使用方法;继承可以实现对代码的复用;而多态则实现了接口和功能的解耦合。


1. 问题引出,为什么要有多态?

当我们在子类和父类定义了同名函数时(函数重写),并把子类对象传给父类指针或引用时,我们调用该同名函数,最终都是调用父类的成员函数,这是由于类型兼容性原则导致的(使用public继承时,子类可以当作父类使用)。但是我们现在希望编译器能够自动判断我们传入的是子类还是父类,并调用相应的成员函数。

class Parent
{
public:
	void func()
    {
        ;
    }
};

class Child
{
public:
	void func()
    {
        ;
    }
};

//void test(Parent& p)
void test(Parent* p)
{
    //p.func();
    p->func(); //传入子类对象,依然调用父类的 func 成员
}

int main()
{
    Child c;
    // test(&c);
    test(c);
    return 0;
}

2. 多态的基础知识

2.1 类型兼容性原则

类型兼容性原则是指:当派生类是公有制继承时,派生类的对象可以当作基类的对象使用(但是基类不能当派生类使用)。这包括

子类对象传递给父类的引用; 子类对象传递给父类的指针; 用子类对象给父类对象赋值; 子类对象初始化父类;

#include <iostream>
using namespace std;

class A
{
public:
	void ptint_a()
	{
		cout << "===a===" << endl;
	}
protected:
private:
};

class B : public A
{
public:
	void ptint_b()
	{
		cout << "===b===" << endl;
	}
protected:
private:
};

void func1(A* a)
{
	a->ptint_a();
}

void func2(A& a)
{
	a.ptint_a();
}

/*
void func2(B& b)
{
	b.ptint_b();
}*/

void func3(B& b)
{
	b.ptint_b();
}

int main()
{
	A a1;
	B b1;
	A a2 = b1; //子类对象初始化父类//会调用拷贝构造函数
	func1(&b1); //子类对象传递给父类的指针
	func2(b1);//会优先匹配 func2(B& b)//子类对象传递给父类的引用
	//func3(a1); 基类不能当派生类用

	a1 = b1; //可以直接用子类对象给父类对象赋值
	
	system("pause");
	return 0;
}

2.2 重载重写重定义

  • 重载 同一个类中,函数名相同,参数类型、个数不同;
  • 重写 在子类与父类中,父类与子类的函数原型完全相同; 有 virtual 关键字,虚函数重写,多态; 无 virtual 关键字,重定义;
  • 名称覆盖 当父类和子类有相同的函数名、变量名出现,发生名称覆盖,子类的函数名,覆盖了父类的函数名。
#include <iostream>
using namespace std;

class MyClassA
{
public:
	void PrintFunc()
	{
		cout << "MyClassA 无参函数" << endl;
	}
	void PrintFunc(int a)
	{
		cout << "MyClassA 一个参数" << endl;
	}
	virtual void PrintFunc(int a, int b)
	{
		cout << "MyClassA 两个参数" << endl;
	}
};

class MyClassB : public MyClassA
{
public:
	void PrintFunc(int a)
	{
		cout << "MyClassB 一个参数" << endl;
	}
	virtual void PrintFunc(int a, int b)
	{
		cout << "MyClassB 两个参数" << endl;
	}
};

/*	MyClassA 和 MyClassB 的函数 void PrintFunc(int a)  属于重写(重定义)
*	MyClassA 和 MyClassB 的函数 virtual void PrintFunc(int a, int b)  属于虚函数重写(多态)
*	MyClassA 的 void PrintFunc() 在子类中被名称覆盖,子类中无法直接引用
*	MyClassA 中的三个 PrintFunc() 属于函数重载
*	MyClassB 中的两个 PrintFunc() 属于函数重载
*/

int main()
{
	MyClassB b1;

	//b1.PrintFunc(); //错误	C2661	“MyClassB::PrintFunc”: 没有重载函数接受 0 个参数	
	//因为子类中也有名为 PrintFunc 的函数,所以他把父类中的同名函数覆盖了,编译器只会在
	//子类中查找 PrintFunc 函数,当找不到匹配的参数时,就会报没有重载函数
	b1.MyClassA::PrintFunc();

	system("pause");
	return 0;
}

2.3 动态联编与静态联编

C++与C语言都是静态编译型语言,在编译时,编译器自动根据指针的类型判断指向的是一个什么样的对象;所以编译器认为父类指针指向的是父类对象。由于程序没有运行,所以不可能知道父类指针指向的具体是父类对象还是子类对象。从程序安全的角度,编译器假设父类指针只指向父类对象,因此编译的结果为调用父类的成员函数。这种特性就是静态联编。

联编是指一个程序模块、代码之间互相关联的过程。 静态联编(static binding),是程序的匹配、连接在编译阶段实现,也称为早期匹配。比如重载函数使用静态联编(在编译的时候就决定了怎么执行,函数怎么调用)。 动态联编是指程序联编推迟到运行时进行,所以又称为晚期联编(迟绑定)。比如switch 语句和 if 语句是动态联编的例子(在程序执行的过程中才知得调用哪一个,根据类型决定)。 通过对重写函数加 virtual 关键字来实现动态联编。

3. 多态案例

#include <iostream>
using namespace std;

class GetArea //抽象类//抽象类不能建立对象
{
public:
	virtual double get_area() = 0; //纯虚函数
};

class Square : public GetArea
{
public:
	Square(int a, int b)
	{
		this->a = a;
		this->b = b;
	}
public:
	virtual double get_area()
	{
		return a * b;
	}
private:
	int a;
	int b;
};

class Circular : public GetArea
{
public:
	Circular(int r)
	{
		this->r = r;
	}
public:
	virtual double get_area()
	{
		return 3.14 * r * r;
	}
private:
	int r;
};

class Triangle : public GetArea
{
public:
	Triangle(int h, int l)
	{
		this->h = h;
		this->l = l;
	}
public:
	virtual double get_area()
	{
		return 0.5 * h * l;
	}
private:
	int h;
	int l;
};

void PrintArea(GetArea& obj)//同一调用语句表现出不同状态
{
	cout << obj.get_area() << endl;
}

int main()
{
	Square		s(3, 4);
	Circular	c(10);
	Triangle	t(5, 2);

	PrintArea(s);
	PrintArea(c);
	PrintArea(t);
	
	system("pause");
	return 0;
}