c++的多态(重载、覆盖、隐藏)

    描述这类的文章有很多,这里用最简洁的方式用于记牢:

    1、什么是重载(overload):

        在同一个作用域下的两个同名函数,并且它们的参数不同(返回值是否相同可选),这样的两个函数叫重载。

        注意理解"在同一个作用域下",至少包括以下:

        a. 在同一个名字空间下面,比如都在名字空间abcde下定义的两个普通函数,或者两个全局函数;

        b. 在同一个类型下的同一个作用域下,比如都在class A下的public/protected/private;

        重载是"静态联编(静态绑定)"的一种。

        

        插:所谓的"联编(或者叫绑定)",是指"将一个标识符号,和一个存储地址关联起来"。"联编"分为两种,一种是"静态联编",指的是编译时即确定关联关系,包括:普通函数重载、类成员函数重载、运算符重载、模板重载、隐藏(没有修饰以virtual的父子类同名成员函数);一种是"动态重载",指的是运行时确定调用哪个,包括:覆盖。"静态联编"和"动态联编"共同组成了c++的"多态"功能。


        如下面的两个全局函数:

double overload_sample_0 () {
    std::cout << "overload sample 0_1 in namespace what_is_overload" << std::endl;
    return 1.1;
}
int overload_sample_0 (int a) {
    std::cout << "overload sample 0_2 in namespace what_is_overload" << std::endl;
    return 1;
}

        两个函数的名称都是overload_sample_0,作用域都是全局作用域,参数不一样,重载;

        再如下面的两个函数:

namespace what_is_overload {
    void overload_sample_1 () {
        std::cout << "overload sample 1_1 in namespace what_is_overload" << std::endl;
    }
    int overload_sample_1 (int a) {
        std::cout << "overload sample 1_2 in namespace what_is_overload" << std::endl;
        return 0;
    }
};

        两个函数的名称都是overload_sample_1,作用域都是名字空间what_is_overload之下,参数不一样,重载;

        再如下面的函数:

class cls {
    public:
        cls() = default;
        ~cls() = default;
        cls(const cls &other) = default;
        cls(cls &&other) = default;

        void overload_1 (){ std::cout << "overload 1" << std::endl; }
        void overload_1 (int a){ std::cout << "overload 1" << std::endl; }
        //int overload_1 () { std::cout << "overload 1" << std::endl; return 0; } // could not be distinguished with the first one
        int overload_1 (char c) { std::cout << "overload 1" << std::endl; return 0; }

    protected:
        void overload_2 (){ std::cout << "overload 2" << std::endl; } 
        void overload_2 (int a){ std::cout << "overload 2" << std::endl; }
        //int overload_2 () { std::cout << "overload 2" << std::endl; return 0; } // could not be distinguished with the first one
        int overload_2 (char c) { std::cout << "overload 2" << std::endl; return 0; }

    private:
        void overload_3 (){ std::cout << "overload 3" << std::endl; }
        void overload_3 (int a){ std::cout << "overload 3" << std::endl; }
        //int overload_3 () { std::cout << "overload 3" << std::endl; return 0; } // could not be distinguished with the first one
        int overload_3 (char c) { std::cout << "overload 3" << std::endl; return 0; }
    };

        public、protected、private下,都有各自内部的类成员函数的,函数名相同,作用域相同,参数不一样,重载;


        插:描述重载时,很多文章注重说,函数的名称一样,参数、返回值不同,事实上精确的是 ,函数的参数必须不同。试想下面两个函数:

        int overload(){.....}

        double overload() {.....}

        然后调用时:overload();    //which one?

        函数的参数一样,编译器怎么可能区分调用时调用的是哪一个。


    2、什么是覆盖(override):

        父子类中的同名、同参数、同返回值的多个成员函数,从子到父形成的关系称为覆盖关系。

        覆盖关系属于"动态联编",即运行时通过确定指针所指向的是哪个对象,决定运行哪个实现,支撑"动态联编"的是虚函数表,关于虚函数表:

        a. 什么是虚函数表:对含有"virtual"修饰符的类,编译器在编译时,会给该类型制造一个该类型所属的虚函数表;

        b. 虚函数表本质上是:一个函数指针表,类的函数名-----地址,这个函数必须用virtual修饰过;

        c. 虚函数表在哪:在应用程序的常量区;

        d. 虚函数表怎么作用:运行时,根据指针所对应的对象是哪个类型的对象,从虚函数表中找到对应函数名的地址进而执行;

        对析构函数用virtual修饰是典型的覆盖的运用,当(父类指针指向的)子类对象发生析构时:

        a. 如果父类的析构函数用virtual修饰,那么析构时,通过"动态联编"会调用子类的析构函数;然后自然析构父类;

        b. 如果父类的析构函数没有用virtual修饰,那么析构时,仅仅自然析构父类;这个过程实际就是所谓的隐藏;


        参考下面的代码:

//what is inherit? inherit is that father and son has the same function name and the same arg and same return-value
//in c++11, if do want to override, you'd better explicit notice the son function with "override", so compiler will do know that it is inherit, but not another base-virtual-function.
    class father {
    public:
        father() = default;
        //~father() { std::cout << "father destruct" << std::endl; };
        ~father() { std::cout << "father destruct" << std::endl; };
        father(const father &other) = default;
        father(father &&other) = default;

        virtual void inherit () { std::cout << "inherit, father" << std::endl; }
    };
    class son : public father {
    public:
        son() = default;
        //~son() { std::cout << "son destruct" << std::endl; };
        virtual~son() { std::cout << "son destruct" << std::endl; };
        son(const son &other) = default;
        son(son &&other) = default;

//explicit notice compiler that, inherit_c11 here is override of function inherit
//if qualify with "final", class grandson could not override.
//"override" and "final" is used in c++11
        virtual void inherit () override { std::cout << "inherit, son" << std::endl; }
        //virtual void inherit () override final { std::cout << "inherit, son" << std::endl; }
    };

    class grandson : public son {
    public:
        grandson() = default;
        virtual ~grandson() { std::cout << "grandson destruct" << std::endl; };
        grandson(const grandson &other) = default;
        grandson(grandson &&other) = default;

        virtual void inherit () override final {std::cout << "inherit, grandson" << std::endl;  }
    };

        父类、子类、孙类,相继是public的继承关系,析构函数均用virtual修饰,下面依次是实验的方式的知识点的巩固:

        a. 如果析构函数去除了virtual修饰,那么尝试下面的测试,看看差别在哪:

son s

        仅仅在栈中定义了son类型的变量s,析构时,可以发现父类也会被析构;

std::shared_ptr<father> p((father *)new son());
        用father指针,指向在堆中创建的son对象,析构时,可以发现仅仅析构了父类对象,即发生内存泄漏;隐藏;
std::shared_ptr<son> pfs((son *)new father());

        用son指针,指向在堆中创建的father对象,这时程序会崩溃;如果加virtual修饰,会正常析构父类对象;

        结论:

        1、父子类的析构函数,要加入virtual修饰;

        2、不要用子类指针,指向父对象;

              用父类指针指向子类对象,实际开发中是具备实际意义的,是动态实现多态的使用呈现。

              而反过来是令人奇怪的,因为子类已经继承了父类的方法,为什么要用子类指针指向父类对象。


        对于普通函数的覆盖,和析构函数是一样道理。注意:

        1、构造函数不可以进行覆盖;

        2、运算符重载属于重载,不可以进行覆盖;

        3、要保证覆盖的函数,函数参数、返回值均相同;


        一定要加入virtual修饰符,但有时可能会忘记,如果忘记则可能会出现隐藏:

        1、父类有成员函数function;没有用virtual修饰;

        2、子类有成员函数function,函数名、参数、 返回值均相同;

        那么如下面的例子,会出现问题:

std::shared_ptr<father> psf((father *)new son());

        这时如果执行psf->function(),不会在运行时"动态联编",而是出现如下情况:那么指针是什么类型,就执行哪个类型的function;或者说,override退化成hide;隐藏(hide)一般属于程序开发bug;

        为了防止忘写virtual导致的隐藏,c++11规定可以在程序中,对希望是覆盖的成员函数,显式的修饰以override,这样如果没写virtual会在编译时报错,避免忘修饰virtual;

        c++11另外还规定了修饰符final,对于不想再被子类覆盖的虚函数,加入final修饰符,它的子类即便想覆盖也无妨再覆盖了,或者说,覆盖行为到它这里截止;

        

    3、什么是隐藏(hide):

        知道了什么是覆盖,那么忘记修饰virtual导致的"覆盖失误"就是隐藏。隐藏的特点是:不管指针实际指向的对象是什么样的类型,完全根据指针类型,执行对应的成员函数。

        隐藏导致编译器完全没有创建虚函数表(因为没有virtual修饰符),所以完全走的是"静态联编",即按指针类型来。隐藏的危害发生在,原本希望是覆盖,结果发现和预期完全不符。

        下面是隐藏的一些例子:

TEST (test1, what_is_hide) {
//hide: father class and son class, has the function with same name, but without qualify with 'virtual', type of pointer would determine which would be call
    class father {
    public:
        father() = default;
        ~father() = default;
        father(const father &other) = default;
        father(father &&other) = default;

//for hide, whether return-value is also same, is not necessary.so hide_0 equals hide_1
        void hide_0 () { std::cout << "hide_0, father" << std::endl; }
        void hide_1 () { std::cout << "hide_1, father" << std::endl; }
        void hide_2 (int a) { std::cout << "hide_2, father" << std::endl; }
    };

    class son : public father {
    public:
        son() = default;
        ~son() = default;
        son(const son &other) = default;
        son(son &&other) = default;

        void hide_0 () { std::cout << "hide_0, son" << std::endl; }
        int hide_1 () { std::cout << "hide_1, son" << std::endl; }
        void hide_2 () { std::cout << "hide_2, son" << std::endl; }
    };

//when hide, type of pointer determine which would be call
//hide explains that why destruct-function without virtual, would memory leak. destruct-function without virtual, would determined by type of pointer, but not type of object
    father f;
    son s;
    f.hide_0(); //father
    f.hide_1(); //father
    f.hide_2(1);//father
    s.hide_0(); //son
    s.hide_1(); //son
    s.hide_2(); //son

    std::shared_ptr<son> ps(new son());
    ps->hide_0();   //son
    ps->hide_1();   //son
    ps->hide_2();   //son

    std::shared_ptr<son> psf((son *)new father());
    psf->hide_0();  //son
    psf->hide_1();  //son
    psf->hide_2();  //son

    std::shared_ptr<father> pf(new son());
    pf->hide_0();   //father
    pf->hide_1();   //father
    pf->hide_2(1);   //father

    std::shared_ptr<father> pfs((father *)new son());
    pfs->hide_0();  //father
    pfs->hide_1();  //father
    pfs->hide_2(2);  //father
}


        c++多态的总结:

        1、"静态联编":

            1、同一作用域(相同名字空间,类型下相同作用域(public/protected/private))的普通/成员函数,函数名相同,参数不相同;

            2、运算符重载;

            3、编译时确定模板实现类(模板重载);

            如:template<class T> class A{ T data}; A<int> a;

            4、隐藏;隐藏是程序开发的bug应该加以避免;

        2、"动态联编":

            指的就是覆盖,运行时由虚函数表根据所处对象的类型,确定执行哪一个函数;

            良好的程序应该使用覆盖;

        3、如何避免覆盖变成隐藏:

            a. 不要忘了用virtual修饰;

            b. 同时使用override修饰,忘了用virtual时编译器可以报错;

        

        

  • 3
    点赞
  • 11
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值