zoukankan      html  css  js  c++  java
  • C++之多态

    1. 综述

    问题抛出: 如果子类定义了与父类中原型相同的函数时会发生什么?

    函数重写:在子类中定义与父类中原型相同的函数,函数重写只发生在父类与子类之间。

    父类中被重写的函数依然会继承给子类,默认情况下子类中重写的函数将隐藏父类中的函数,通过作用域分辨符::可以访问到父类中被隐藏的函数。

    1.1 类型兼容性原则遇上函数重写--引出面向对象新需求

    #include <iostream>
    #include <stdlib.h>
    using namespace std;
    
    class Parent
    {
    public:
    	Parent(int a)
    	{
    		this->a = a;
    		cout << "Parent : a = " << a << endl;
    	}
    
    	void print()
    	{
    		cout << "Parent print: a = " << a << endl;
    	}
    
    private:
    	int a;
    };
    
    class Child : public Parent
    {
    public:
    	Child(int b) : Parent(10)
    	{
    		this->b = b;
    		cout << "Parent : b = " << b << endl;
    	}
    
    	void print()
    	{
    		cout << "Child print: b = " << b << endl;
    	}
    
    private:
    	int b;
    };
    
    void howtoPrint1(Parent *base)
    {
    	base->print();
    }
    
    void howtoPrint2(Parent &base)
    {
    	base.print();
    }
    
    void main()
    {
    	Parent *base = NULL;
    	Parent p1(20);
    	Child  c1(30);
    
    	/* 面向对象新需求的提出:
    	 * 如果传一个父类对象,执行父类的print函数;
    	 * 如果传一个子类对象,执行子类的print函数.
    	 */
    
    	/* 如下场景始终执行的是父类的print函数 */ 
    	{
    		/* 场景一 : 指针 */ 
    		base = &p1;
    		base->print(); // 执行父类的打印函数
    
    		base = &c1;    // 父类指针指向子类对象
    		base->print(); // 发现执行的父类的打印函数--因此提出:面向对象新需求
    	}
    
    	{
    		/* 场景二 : 引用 */
    		Parent &base2 = p1;
    		base2.print();
    
    		Parent &base3 = c1;	// base3是c1的别名
    		base3.print(); // 发现还是执行的是父类的打印函数
    	}
    
    	{
    		/* 场景三 : 函数调用 */
    		howtoPrint1(&p1);
    		howtoPrint1(&c1);
    
    		howtoPrint2(p1);
    		howtoPrint2(c1);
    	}
    
    	system("pause");
    	return;
    }
    

    1.2 面向对象新需求

    编译器的做法不是我们所期望的,我们期望的是:

    • 根据实际的对象类型来判断重写函数的调用
    • 如果父类指针指向的是父类对象则调用父类中定义的函数
    • 如果父类指针指向的是子类对象则调用子类中定义的重写函数

    面向对象中的多态

    1.3 解决方案

    • C++ 通过 virtual 关键字对多态进行了支持
    • 使用 virtual 声明的函数被重写后即可展现多态特性

    注:这才是 virtual 真正的应用场景,而不是虚继承中。

    2. 多态

    2.1 多态实例

    #include <iostream>
    #include <stdlib.h>
    using namespace std;
    
    class HzeoFighter
    {
    public:
    	virtual int power()
    	{
    		return 10;
    	}
    };
    
    class EnemyFighter
    {
    public:
    	int attack()
    	{
    		return 15;
    	}
    };
    
    class AdvHzeoFighter : public HzeoFighter
    {
    public:
    	int power()
    	{
    		return 20;
    	}
    };
    
    // 使用多态的方法
    void playobj(HzeoFighter *hf, EnemyFighter *ef)
    {
    	// hf->power()将会有多态发生
    	if (hf->power() > ef->attack())
    	{
    		cout << "主角赢" << endl;
    	}
    	else
    	{
    		cout << "主角输" << endl;
    	}
    }
    
    // 使用多态的方法
    void main()
    {
    	HzeoFighter    hf;
    	AdvHzeoFighter advhf;
    	EnemyFighter   ef;
    
    	playobj(&hf, &ef);
    	playobj(&advhf, &ef);
    }
    
    void main01()
    {
    	HzeoFighter    hf;
    	AdvHzeoFighter advhf;
    	EnemyFighter   ef;
    
        // 这不是使用多态的案例
    	if (hf.power() > ef.attack())
    	{
    		cout << "主角赢" << endl;
    	}
    	else
    	{
    		cout << "主角输" << endl;
    	}
    
    	if (advhf.power() > ef.attack())
    	{
    		cout << "主角赢" << endl;
    	}
    	else
    	{
    		cout << "主角输" << endl;
    	}
    
    	system("pause");
    	return;
    }
    

    2.2 多态成立条件

    2.2.1 间接赋值成立的三个条件

    1. 两个变量(通常一个实参,一个形参)
    2. 建立关系,实参取地址赋给形参
    3. *p 形参去间接修改实参的值

    2.2.2 多态成立的三个条件(是面向对象领域的一个标准)

    1. 要有继承
    2. 要有虚函数重写(即用 virtual 修饰)
    3. 要有父类指针(或父类引用)指向子类对象

    注:多态是设计模式的基础,是框架的基础。

    2.3 多态的理论基础

    2.3.1 静态联编和动态联编

    1. 联编是指一个程序模块、代码之间互相关联的过程。
    2. 静态联编(static binding),是程序的匹配、连接在编译阶段实现,也称为早期匹配。重载函数使用静态联编。
    3. 动态联编是指程序联编推迟到运行时进行,所以又称为晚期联编(迟绑定)。switch 和 if 语句是动态联编的例子。

    2.3.2 理论联系实际

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

    3. 多态原理探究

    3.1 多态理论知识

    • 当类中声明虚函数时,编译器会在类中生成一个虚函数表。
    • 虚函数表是一个存储类成员函数指针的数据结构。
    • 虚函数表示由编译器自动生成和维护的。
    • virtual 成员函数会被编译器放入虚函数表中。
    • 当存在虚函数时,每个对象都有一个指向虚函数表的指针(C++ 编译器给父类对象、子类对象提前布局 vptr 指针;当进行 howtoPrint(Parent *base) 函数时,C++ 编译器不需要区分子类对象或者父类对象,只需要在 base 指针中,找 vptr 指针即可)。
    • vptr 一般作为类对象的第一个成员。

    3.2 多态的实现原理

    3.2.1 多态实现原理图例

    多态实现原理图1


    说明:通过虚函数表指针 vptr 调用重写函数是在程序运行时进行的,因此需要通过寻址操作才能确定真正应该调用的函数。而普通成员函数是在编译时就确定了 应该调用的函数。在效率上,虚函数的效率要低很多。

    多态实现原理图2

    3.2.2 多态原理的探究例子

    #include <iostream>
    #include <stdlib.h>
    using namespace std;
    
    /* 多态成立的三个条件:
     * 1、要有继承
     * 2、要有虚函数重写
     * 3、要有父类指针(或父类引用)指向子类对象
     */
    
    class Parent
    {
    public:
    	Parent(int a = 0)
    	{
    		this->a = a;
    		cout << "Parent 执行" << endl;
    	}
    
    	virtual void print() // 1、为实现多态,可能动手脚的地方
    	{
    		cout << "我是你爹" << endl;
    	}
    
    private:
    	int a;
    };
    
    class Child : public Parent
    {
    public:
    	Child(int a = 0, int b = 0) : Parent(a)
    	{
    		this->b = b;
    		cout << "Chiild 执行" << endl;
    	}
    
    	void print()
    	{
    		cout << "我是儿子" << endl;
    	}
    
    private:
    	int b;
    
    };
    
    void howtoplay(Parent *base)
    {
    	base->print();   // 2、动手脚
    	// 效果:传来 子类对象 执行子类的 print 函数;传来父类对象 执行父类的 print 函数
    	// C++编译器根本不需要区分是 子类对象 还是 父类对象
    	// 父类对象和子类对象都有一个 vptr 指针,根据该指针去找 虚函数表(每个对象都一个虚函数表),
    	// 最终找到函数的入口地址.
    	// 因此 迟绑定(运行时,才去判断调用的函数)
    }
      
    void main()
    {
        /* 3、动手脚  提前布局
         * 用类定义对象的时候 C++ 编译器会在对象中添加一个 vptr 指针,该指针指向虚函数表
         */
        Parent p1; 
        Child c1;  // 子类中也有一个vptr指针
    
        howtoplay(&p1);
        howtoplay(&c1);
    
        system("pause");
        return; 
    } 
    

    3.2.3 如何证明 vptr 指针存在

    #include <iostream>
    using namespace std;
    
    class A
    {
    public:
        void printf()
        {
            cout << "aaa" << endl;
        }
    protected:
    private:
        int a;
    };
    
    class B
    {
    public:
        virtual void printf()
        {
            cout << "aaa" << endl;
        }
    protected:
    private:
        int a;
    };
    
    void main()
    {
        // 加上 virtual 关键字 c++ 编译器会增加一个指向虚函数表的指针
        printf("sizeof(a):%d, sizeof(b):%d 
    ", sizeof(A), sizeof(B));
        cout << "hello..." << endl;
        system("pause");
        return;
    }
    

    3.2.4 构造函数中能调用虚函数,实现多态吗?

    注:这句话的意思是:定义一个子类对象,在子类对象的父类里面调用一个虚函数,问能产生多态吗?

    1. 对象中的 vptr 指针是什么时候被初始化?
      对象在创建的时候,由编译器对 vptr 指针进行初始化,只有当对象的构造完全结束后 vptr 的指向才最终确定。父类对象的 vptr 指向父类的虚函数表,子类对象的 vptr 指向子类的虚函数表。

    2. 分析过程,如下图

    构造函数中不能实现多态,如下例子:

    #include <iostream>
    #include <stdlib.h>
    using namespace std;
    
    /* 构造函数调用虚函数,能发生多态吗?
     * 意思是:定义子类对象时,在子类对象的父类里面能执行虚函数,能实现多态吗?
     */
    
    class Parent
    {
    public:
    	Parent(int a = 0)
    	{
    		this->a = a;
    		print();	// 在父类函数中执行虚函数
    		cout << "Parent 执行" << endl;
    	}
    
    	virtual void print() 
    	{
    		cout << "我是你爹" << endl;
    	}
    
    private:
    	int a;
    };
    
    class Child : public Parent
    {
    public:
    	Child(int a = 0, int b = 0) : Parent(a)
    	{
    		this->b = b;
    		cout << "Chiild 执行" << endl;
    	}
    
    	void print()
    	{
    		cout << "我是儿子" << endl;
    	}
    
    private:
    	int b;
    
    };
    
    void howtoplay(Parent *base)
    {
    	base->print();   
    }
    
    void main()
    {
    	Child c1;    /* 定义子类对象,子类对象在创建中会调用父类的构造函数,那么在父类对象中
    			   * 调用虚函数print,能发生多态吗
    			   * 运行,调试发现:虽然定义的是子类对象,但是仍然执行的是父类的print函数。
    			   * 因此,在此场景下,不能发生多态。
    			   */
    
    	system("pause");
    	return;
    }
    

    4. 多态相关知识

    4.1 重载与重写

    函数重载(属于静态联编):

    • 必须在同一个类中进行
    • 子类无法重载父类的函数,父类同名函数被将被名称覆盖
    • 重载是在编译期间根据参数类型和个数决定函数调用

    函数重写:

    • 必须发生在父类和子类之间
    • 并且父类与子类中的函数必须有完全相同的原型
    • 使用 vitual 声明之后能够产生多态(如果不使用 virtual,那叫重定义)

    注:多态是在运行期间根据具体对象的类型决定函数调用的。

    4.2 类成员函数与虚函数

    问: 是否可将类的每个成员函数都声明为虚函数,为什么?
    答:虽然可以都声明为虚函数,但不建议这样做。因为通过虚函数表指针 vptr 调用重写函数是在程序运行时进行的,因此需要通过寻址操作才能真正确定应该调用的函数。而普通成员函数在编译时就确定了调用的函数。所以在效率上,虚函数的效率要低很多。因此,出于效率的考虑,没有必要将所有的成员函数都定义为虚函数。

    4.3 为什么要定义虚析构函数?

    在什么情况下应当声明虚函数?

    • 构造函数不是虚函数。建立一个派生类对象时,必须从类层次的根开始,沿着继承路径逐个调用基类的虚函数。
    • 析构函数可以是虚的。虚析构函数用于指引 delete 运算符正确析构动态对象。

    普通析构函数在删除动态派生类对象的调用情况示例图:

    问:为什么要定义虚析构函数?
    答:想通过父类指针把所有子类对象的析构函数都执行一遍,释放所有的子类资源。

    虚析构函数案例:

    #define _CRT_SECURE_NO_WARNINGS
    #include <iostream>
    #include <stdlib.h>
    using namespace std;
    
    class A
    {
    public:
    	A()
    	{
    		p = new char[20];
    		strcpy(p, "obja");
    		cout << "A执行" << endl;
    	}
    
    	virtual ~A()
    	{
    		if (p != NULL)
    		{
    			delete[] p;
    			p = NULL;
    			cout << "~A执行" << endl;
    		}
    	}
    
    private:
    	char *p;
    };
    
    class B : public A
    {
    public:
    	B()
    	{
    		p = new char[20];
    		strcpy(p, "objb");
    		cout << "B执行" << endl;
    	}
    
    	~B()
    	{
    		if (p != NULL)
    		{
    			delete[] p;
    			p = NULL;
    			cout << "~B执行" << endl;
    		}
    	}
    
    private:
    	char *p;
    };
    
    class C : public B
    {
    public:
    	C()
    	{
    		p = new char[20];
    		strcpy(p, "objc");
    		cout << "C执行" << endl;
    	}
    
    	~C()
    	{
    		if (p != NULL)
    		{
    			delete[] p;
    			p = NULL;
    			cout << "~C执行" << endl;
    		}
    	}
    
    private:
    	char *p;
    };
    
    void houtodelete(A *base)
    {
    	delete base;
    }
    
    void main()
    {
    	C *myC = new C;
    	houtodelete(myC);
    
    	system("pause");
    }
    

    4.4 父类指针和子类指针的步长

    1. 铁律1: 指针也是一种数据结构,C++ 类对象的指针 p++/p--,仍然可用;
    2. 指针运算是按照指针所指的类型进行的:
      p++ 等价于 p = p + 1,即 p = (unsigned int)basep + sizeof(*p) 步长
    3. 结论:父类 p++ 与子类 p++ 步长不同,不要混搭,不要用父类指针 ++ 方式操作数组。

    父类指针和子类指针的步长不一样的示例:

    #include <iostream>
    #include <stdlib.h>
    using namespace std;
    
    /* 构造函数调用虚函数,能发生多态吗?
    * 意思是:定义子类对象时,在子类对象的父类里面能执行虚函数,能实现多态吗?
    */
    
    class Parent
    {
    public:
    	Parent(int a = 0)
    	{
    		this->a = a;
    		cout << "Parent 执行" << endl;
    	}
    
    	virtual void print()
    	{
    		cout << "我是你爹" << endl;
    	}
    
    private:
    	int a;
    };
    
    // 成功,一次偶然的成功,比必然的失败更可怕
    class Child : public Parent
    {
    public:
    	Child(int b = 0) : Parent(0)
    	{
    		//this->b = b;
    		cout << "Chiild 执行" << endl;
    	}
    
    	virtual void print()
    	{
    		cout << "我是儿子" << endl;
    	}
    
    private:
    	//int b;    // 把该语句的注释撤销前,则此时父类和子类的指针的步长一样,指针++/--不会
    			    // 使程序出现core dump;但是撤销后,则会使子类的步长与父类的步长不一致了,
    			    // 再++/--则会出现core dump现象。
    
    };
    
    void howtoplay(Parent *base)
    {
    	base->print();
    }
    
    void main()
    {
    	Parent *pP = NULL;
    	Child  *pC = NULL;
    
    	Child array[] = { Child(1), Child(2), Child(3) };
    	pP = array;
    	pC = array;
    
    	pP->print();
    	pC->print();	// 多态发生
    
    	pP++;
    	pC++;
    	pP->print();
    	pC->print();	// 多态发生
    
    	pP++;
    	pC++;
    	pP->print();
    	pC->print();	// 多态发生
    	system("pause");
    	return;
    }
    

    上述例子中两个类的内存分布:

  • 相关阅读:
    null in ABAP and nullpointer in Java
    SAP ABAP SM50事务码和Hybris Commerce的线程管理器
    Hybris service layer和SAP CRM WebClient UI架构的横向比较
    SAP ABAP和Linux系统里如何检查网络传输的数据量
    SAP CRM WebClient UI和Hybris的controller是如何被调用的
    SAP CRM和Cloud for Customer订单中的业务伙伴的自动决定机制
    SAP CRM WebClient UI和Hybris CommerceUI tag的渲染逻辑
    SAP BSP和JSP页面里UI元素的ID生成逻辑
    微信jsapi支付
    微信jsapi退款操作
  • 原文地址:https://www.cnblogs.com/jimodetiantang/p/9049355.html
Copyright © 2011-2022 走看看