《随笔八》—— C++中的“ 多态中的静态联编 、动态联编”

目录

         多态

在编译时的静态联编

在运行时的静态联编

在编译时的动态联编

在运行时的动态联编


C++中的虚函数在这篇:《随笔九》—— C++中的 “ 虚函数 ” https://blog.csdn.net/qq_34536551/article/details/84375823


下面先给出一些代码示例,引用出要使用多态的原因:

class Persion
{
public:
 void sleep()
 {
  cout << "调用的是基类中的sleep" << endl;
 }
 void breathe()
 {
  cout << "调用的是基类中的breathe" << endl;
 }
 
};
class Man :public Persion
{
public:
 void breathe()
 {
  cout << "调用的是派生类中的breathe" << endl;
 }

};


int main()
{
         Man MyMan;
	Persion *an = &MyMan;
	an->breathe(); //调用基类中的该函数

	MyMan.breathe();
	MyMan.Persion::breathe();  //调用基类中的该函数

	Man *s = new Man;
	s->breathe();

	Persion myPersion;
	myPersion.breathe();  //调用基类中的该函数

	system("pause");
	return 0;
}

输出结果为:

调用的是基类中的breathe
调用的是派生类中的breathe
调用的是基类中的breathe

调用的是派生类中的breathe
调用的是基类中的breathe

在主函数中我们首先定义了一个Man类的对象myMan, 另一个Persion类的指针变量an,该指针变量指向Man类的对象myMan, 最后利用该指针变量调用了  an->breathe(),  大家认为 myMan 实际上是 Man类的对象,应该调用 Man类的breathe(), 但结果不是这样的。

这是因为C++ 编译器在编译的时候,要确定每个对象调用的函数的地址,这称为早期绑定。 当Man类的对象myMan 的地址赋给an时,C++编译器进行了类型转换, 此时C++编译器认为变量 an 保存的就是 Persion 对象的地址。 当在主函数中执行  an->breathe() 时,调用的是 Persion 对象的 breathe 函数。

大家可能想调用的是 派生类中的 breathe()函数, 这个问题可以使用多态来解决。


下面再看另外一个程序示例:

class father
{
public:
	father()
	{
		age = 54;
		cout << "调用基类构造函数:" << age << endl;
	}

	~father()
	{
		cout << "调用基类析构函数:" << endl;
	}

	void jump()const
	{
		cout << "基类jump" << endl;
	}

	void run()const
	{
		cout << "基类run" << endl;
	}
protected:
	int age;
};
class son :public father
{
public:
	son()
	{
		cout << "调用派生类构造函数:" << endl;
	}
	~son()
	{
		cout << "调用派生类析构函数:" << endl;
	}
	void math()
	{
		cout << "派生类math" << endl;
	}
	void jump()const
	{
		cout << "派生类jump" << endl;
	}
	void run()const
	{
		cout << "派生类run" << endl;
	}
};
int main()
{
	{
		father *pfather = new son;
		pfather->jump();

		pfather->run();
		//pfather->math();  基类指针不可以访问派生类中的成员
		delete pfather;
	}
	{
		cout << endl;
		son myson;
		father *pfather = &myson;
		pfather->jump();

		pfather->run();
		/*pfather->math();基类指针不可以访问派生类中的成员,
		虽然我们可以把基类指针转换成派生类指针,但是这样会容易出错*/
	}
	{
		father myFather;
		myFather.math();  //错误,基类对象不可以访问派生类中新添加的成员
	}
	{
		cout << endl;
		son mmson;
		mmson.jump();
		mmson.father::jump();
	}
	{
		cout << endl;
		son *sson = new son; /*假如我们需要访问派生类中的成员,就要将该指针声明为son类
							 */
		sson->math();
		delete sson;
	}
	system("pause");
	return 0;
}

输出结果为:
调用基类构造函数:54
调用派生类构造函数:
基类jump
基类run
调用基类析构函数:

调用基类构造函数:54
调用派生类构造函数:
基类jump
基类run
调用派生类析构函数:
调用基类析构函数:

调用基类构造函数:54
调用派生类构造函数:
派生类jump
基类jump
调用派生类析构函数:
调用基类析构函数:

调用基类构造函数:54
调用派生类构造函数:
派生类math
调用派生类析构函数:
调用基类析构函数:

多态


class father
{
public:
	father()
	{
		age = 54;
		cout << "调用基类构造函数:" << age << endl;
	}

	~father()
	{
		cout << "调用基类析构函数:" << endl;
	}

	void jump()const
	{
		cout << "基类jump" << endl;
	}

	virtual void run()const
	{
		cout << "基类run" << endl;
	}
protected:
	int age;
};
class son :public father
{
public:
	son()
	{
		cout << "调用派生类构造函数:" << endl;
	}
	~son()
	{
		cout << "调用派生类析构函数:" << endl;
	}

	void math()
	{
		cout << "派生类math" << endl;
	}

	void jump()const
	{
		cout << "派生类jump" << endl;
	}

	virtual void run()const
	{
		cout << "派生类run" << endl;
	}
};
int main()
{

	{
		father *pfather = new son; //基类对象的指针指向派生类
		pfather->jump();  //访问基类中的jump() 函数,因为它没有声明成virtual 函数,所以调用的还是基类中该函数
		pfather->father::jump(); // 显式调用基类中的jump 函数

		pfather->run(); //访问基类中的run()虚函数,也就是访问派生类中的该函数
		pfather->father::run(); //可以这样调用基类中的run函数

		//pfather->math();  基类指针不可以访问派生类中新添加的成员
		delete pfather;
	}
	{
		cout << endl;
		son myson;
		father *pfather = &myson;
		pfather->jump();  //访问基类中的jump() 函数,因为它没有声明成virtual 函数,所以调用的还是基类中该函数
		pfather->father::jump(); // 显式调用基类中的jump 函数

		pfather->run(); //访问基类中的run()虚函数,也就是访问派生类中的该函数
		pfather->father::run(); //可以这样调用基类中的run函数

		//pfather->math();  基类指针不可以访问派生类中新添加的成员
	}

	/* 当用基类指针指向派生类对象时,不管用new 在堆上创建派生类对象,还是基类指针指向派生类对象的地址,还是基类对象引用
	派生类对象,这三种情况下
	当用基类指针调用某一个函数的时候, 如果该函数是virtual函数,那么调用的就是派生类中的该函数, 如果该函数不是virtual
	那么调用的就是基类中的该函数,*/

	system("pause");
	return 0;
}

输出结果为:

调用基类构造函数:54
调用派生类构造函数:
基类jump
基类jump
派生类run
基类run
调用基类析构函数:

调用基类构造函数:54
调用派生类构造函数:
基类jump
基类jump
派生类run
基类run
调用派生类析构函数:
调用基类析构函数:

这个程序跟上面的那个程序的输出有显著的区别, 我们在该程序中基类的函数 run() 函数声明为虚函数,派生类中有无virtual关键字,都为虚函数。 当我们在主函数中使用基类的指针指向 派生类对象时, 使用该指针访问 run() 成员函数, 访问的是 派生类中的, 上面的那个程序访问的是 基类中的run() 成员函数。

那这是为什么呢?

这是因为不使用virtual 关键字之前,C++ 对重载的函数(原型相同,层次不同),使用了静态联编, 而使用了 virtual 以后, C++ 则对该函数使用动态联编。

那什么是静态联编和动态联编呢?

将一个调用函数链接上正确的被调函数, 这一过程叫做函数联编,一般称为联编。

因此在未加virtual 时, 该函数是静态联编,即被调函数和主调函数的关系以及它们的内存地址是在编译时都已经确定好的。  程序运行时不在变化, 这样的好处是速度快,因为运行的时候不用对各个对象的函数进行追踪, 只需要传递参数, 执行确定好的函数并在函数调用完毕后清理内存即可 。

 

那么由静态联编支持的多态性称为编译时的多态性或者静态多态性, 也就是说确定同名操作的具体操作对象的过程是在编译过程中完成的。  在C++中,我们可以使用函数重载 和 运算符重载来实现编译时的多态性。

 

由动态联编支持的多态性称为运行时的多态性 或者 动态多态性,即确定同名操作的具体操作对象的过程是在运行过程中完成的。 在C++中, 可以使用虚函数来实现运行时的多态性。

虚函数是实现运行时多态的一个重要方式,是重载的另一种形式, 实现的是动态的重载即函数调用与函数体之间的联系是在运行时建立的,也就是动态联编。


在编译时的静态联编


在编译时的静态联编程序示例:

class A
{
public:
	int get()
	{
		return 1;
	}
};

class B :public A
{
public:
	int get()
	{
		return 2;
	}
};
int main()
{
	A a;
	cout << "a的值:" << a.get() << endl;
	//a.B::get(); 错误

	B b;
	cout << "b的值:" << b.get() << endl;
	cout << "a的值:" << b.A::get() << endl;  //调用的是基类中的get 函数
	system("pause");
	return 0;
}

输出结果为:

a的值:1
b的值:2
a的值:1

当函数的参数完全相同,但不属于同一个类时,为了让编译器能正确区分调用哪个类的同名函数, 可采用以下两种方法:

用对象名区别: 在函数名前加上对象名来限制。  如: 对象名. 函数名

用类名和作用域运算符加以区别:  在函数名前加“ 类名 : : ”  来限制,  如 类名 ::函数名。

在上面的程序采用静态联编,即在编译时就处理好了程序中主调函数和被调用函数之间的关系。 因此代码在编译时与执行时的效果都是一样的。


在运行时的静态联编


运行时的静态联编代码示例:

class A
{
public:
	int get()
	{
		return 1;
	}
};

class B :public A
{
public:
	int get()
	{
		return 2;
	}
};
int main()
{
	A *p = new A;
	cout << "输出p的值为:" << p->get() << endl;

	A *pp = new B; // 基类指针指向派生类对象
	cout << "输出pp的值为:" << pp->get() << endl;

       delete p;
       delete pp;
	system("pause");
	return 0;
}

输出结果为:

输出p的值为:1
输出pp的值为:1

由于静态联编的对象与指针的关系在编译时就已确定, 因此运行时再对它改变也是无效的。为啥是运行时的静态联编呢,因为它们的get() 函数不是虚函数。


在编译时的动态联编


在编译时的动态联编的代码示例:

class A
{
public:
	virtual int get() //声明虚函数
	{
		return 1;
	}
};

class B :public A
{
public:
	virtual int get()
	{
		return 2;
	}
};
int main()
{
	A a;
	cout << "a的值:" << a.get() << endl;

	B b;
	cout << "b的值:" << b.get() << endl;
	cout << "a的值:" << b.A::get() << endl;
	system("pause");
	return 0;
}

输出结果为:

a的值:1
b的值:2
a的值:1

上面的程序我们可以看到,尽管我们在基类中使用 virtual 关键字声明 get函数为虚函数, 但是我们在主函数中没有使用基类对象的指针或者引用,  那么我们就无法实现动态联编, 那么该程序还是静态联编。


在运行时的动态联编


在运行时的动态联编的代码示例:

class A
{
public:
	virtual int get() //声明虚函数
	{
		return 1;
	}
};

class B :public A
{
public:
	virtual int get()
	{
		return 2;
	}
};
int main()
{
	{
		A *a = new A;
		cout << "基类的值:" << a->get() << endl;
		delete a;
	}
	B b;
	A *p = &b;
	cout << "派生类的值:" << p->get() << endl;
	//使用派生类对象和基类对象的指针间接访问基类中的成员
	cout << "基类的值:" << p->A::get() << endl;
	cout << "基类的值:" << b.A::get() << endl;

	A &ss = b; //基类对象引用派生类对象,实现动态联编
	cout << "派生类的值:" << ss.get() << endl;
	cout << "基类的值:" << ss.A::get() << endl;

	system("pause");
	return 0;
}
输出结果为:

基类的值:1
派生类的值:2
基类的值:1

基类的值:1
派生类的值:2
基类的值:1

只有在使用基类对象的指针 或者引用时来调用虚函数时, 虚函数才能起到运行时的多态作用,才能实现在运行时的动态联编

不过需要注意的是:  只有被声明为虚函数的那个成员函数才具有多态性,当用基类对象的指针 或者引用时来调用虚函数时,才会调用派生类中的该虚函数,  如果调用的该函数,不是虚函数,调用的是基类中的该函数 。

  • 10
    点赞
  • 13
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值