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

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

  • 相关阅读:
    (Relax njuptoj)1009 数的计算(DP)
    Eclipse使用技巧总结(二)
    Ibatis的分页机制的缺陷
    TFT ST7735的Netduino驱动
    超级求爱程序--为我们的程序工作找乐子
    Selenium Grid跨浏览器-兼容性测试
    PHP一般情况下生成的缩略图都比较不理想
    库目录和头文件目录中生成画图函数
    根据PHP手册什么叫作变量的变量?
    数据库的最基本的逻辑结构组成架构
  • 原文地址:https://www.cnblogs.com/jimodetiantang/p/9049355.html
Copyright © 2011-2022 走看看