C++学习笔记 封装 继承 多态 重写 重载 重定义

时间:2022-05-23 23:13:42

C++ 三大特性 封装,继承,多态

  封装

    定义:封装就是将抽象得到的数据和行为相结合,形成一个有机的整体,也就是将数据与操作数据的源代码进行有机的结合,形成类,其中数据和函数都是类的成员,目的在于将对象的使用者和设计者分开,

       以提高软件的可维护性和可修改性

    特性:1. 结合性,即是将属性和方法结合    2. 信息隐蔽性,利用接口机制隐蔽内部实现细节,只留下接口给外界调用    3. 实现代码重用

  继承

    定义:继承就是新类从已有类那里得到已有的特性。 类的派生指的是从已有类产生新类的过程。原有的类成为基类或父类,产生的新类称为派生类或子类,

        子类继承基类后,可以创建子类对象来调用基类函数,变量等

        单一继承:继承一个父类,这种继承称为单一继承,一般情况尽量使用单一继承,使用多重继承容易造成混乱易出问题

        多重继承:继承多个父类,类与类之间要用逗号隔开,类名之前要有继承权限,假使两个或两个基类都有某变量或函数,在子类中调用时需要加类名限定符如c.a::i = 1;

        菱形继承:多重继承掺杂隔代继承1-n-1模式,此时需要用到虚继承,例如 B,C虚拟继承于A,D再多重继承B,C,否则会出错

        继承权限:继承方式规定了如何访问继承的基类的成员。继承方式指定了派生类成员以及类外对象对于从基类继承来的成员的访问权限

        继承权限:子类继承基类除构造和析构函数以外的所有成员

    继承可以扩展已存在的代码,目的也是为了代码重用

    继承也分为接口继承和实现继承:

      普通成员函数的接口总是会被继承:  子类继承一份接口和一份强制实现

      普通虚函数被子类重写     :  子类继承一份接口和一份缺省实现

      纯虚函数只能被子类继承接口  :  子类继承一份接口,没有继承实现

    访问权限图如下:

        C++学习笔记 封装 继承 多态 重写 重载 重定义

    为了便于理解,伪代码如下,注意这个例子编译是不过的,仅是为了可以更简洁的说明继承权限的作用:

    class Animal    //父类

    {

    public:

    void eat(){

      cout<<"animal eat"<<endl;

          }

    protected:

      void sleep(){    

           cout<<"animal sleep"<<endl;

          }

    private:

        void breathe(){

            cout<<"animal breathe"<<endl;

         }

    };
    class Fish:public Animal    //子类

    {

    public:

      void test() {

           eat();       //此时eat()的访问权限为public,在类内部能够访问

          sleep();     //此时sleep()的访问权限为protected,在类内部能够访问

           breathe();   //此时breathe()的访问权限为no access,在类内部不能够访问

          }

    };

    int main(void) {

        Fish f;
          f.eat();          //此时eat()的访问权限为public,在类外部能够访问

        f.sleep();        //此时sleep()的访问权限为protected,在类外部不能够访问

        f.breathe()       //此时breathe()的访问权限为no access,在类外部不能够访问

    }

        

  多态

    定义:可以简单概括为“一个接口,多种方法”,即用的是同一个接口,但是效果各不相同,多态有两种形式的多态,一种是静态多态,一种是动态多态

        动态多态:    是指在程序运行时才能确定函数和实现的链接,此时才能确定调用哪个函数,父类指针或者引用能够指向子类对象,调用子类的函数,所以在编译时是无法确定调用哪个函数

             使用时在父类中写一个虚函数,在子类中分别重写,用这个父类指针调用这个虚函数,它实际上会调用各自子类重写的虚函数。

                 运行期多态的设计思想要归结到类继承体系的设计上去。对于有相关功能的对象集合,我们总希望能够抽象出它们共有的功能集合,在基类中将这些功能声明为虚接口(虚函数),

             然后由子类继承基类去重写这些虚接口,以实现子类特有的具体功能。

                 运行期多态的实现依赖于虚函数机制。当某个类声明了虚函数时,编译器将为该类对象安插一个虚函数表指针,并为该类设置一张唯一的虚函数表,虚函数表中存放的是该类虚函数地址。

             运行期间通过虚函数表指针与虚函数表去确定该类虚函数的真正实现。

           优点: OO设计重要的特性,对客观世界直觉认识; 能够处理同一个继承体系下的异质类集合

              vector<Animal*>anims;

              Animal * anim1 = new Dog;

              Animal * anim2 = new Cat;

               //处理异质类集合

              anims.push_back(anim1);

              anims.push_back(anim2);

           缺点:运行期间进行虚函数绑定,提高了程序运行开销;庞大的类继承层次,对接口的修改易影响类继承层次;由于虚函数在运行期才绑定,所以编译器无法对虚函数进行优化

    虚函数

        定义:用virtual关键字修饰的函数,本质:由虚指针和虚表控制,虚指针指向虚表中的某个函数入口地址,就实现了多态,作用:实现了多态,虚函数可以被子类重写,虚函数地址存储在虚表中

        虚表:虚表中主要是一个类的虚函数的地址表,这张表解决了继承,覆盖的问题,保证其真实反应实际的函数,当我们用父类指针来指向一个子类对象的时候,虚表指明了实际所应调用的函数

           基类有一个虚表,可以被子类继承,个人理解子类和基类是分别拥有一张虚表的,子类如果重写了基类的某虚函数,那么子类虚表中虚函数的地址也会相应改变,指向自身的虚函数实现,

           如果子类有自己的虚函数,那么子类虚表中就会增加该项,编译器为每个类对象定义了一个虚指针,来定位虚表,所以虽然是父类指针指向子类对象,但因为对象是子类,所以查找的是

           子类的虚表,又因为此时子类重写了该虚函数,该虚函数地址在子类虚表中的地址已经被改变了,所以它实际调用的是子类的重写后的函数,正是由于每个对象调用的虚函数都是通过虚表

           指针来索引的,也就决定了虚表指针的正确初始化是非常重要的,换句话说,在虚表指针没有正确初始化之前,我们是不能调用虚函数的,因为生成一个对象是构造函数的工作,所以设置

           虚指针也是构造函数的工作,编译器在构造函数的开头部分秘密插入能初始化虚指针的代码, 在构造函数中进行虚表的创建和虚指针的初始化

           一但虚指针被初始化为指向相应的虚表,对象就“知道”它自己是什么类型,但只有当虚函数被调用时这种自我认知才有用

           类中若没有虚函数,类对象的大小正好是数据成员的大小,包含有一个或者多个虚函数的类对象。编译器会向里面插入一个虚指针,指向虚表,这些都是编译器为我们做的,我们完全不必关心

           这些,所有有虚函数的类对象的大小是数据成员的大小加一个虚指针的大小;对于虚继承,若子类也有自己的虚函数,则它本身需要有一个虚指针,指向自己的虚表,另外子类继承基类时,

           首先要通过加入一个虚指针来指向基类,因此可能会有两个或多个虚指针(多重继承会多个),其他情况一般是一个虚指针,一张虚表

           每一个带有virtual函数的类都有一个相应的虚表,当对象调用某一virtual函数时,实际被调用的函数取决于该对象的虚指针所指向的那个虚表-编译器在其中寻找适当的函数指针。

        效率漏洞:我们必须明白,编译器正在插入隐藏代码到我们的构造函数中,这些隐藏代码不仅必须初始化虚指针,而且还必须检查this的值(以免operator new返回零)和调用基类构造函数。放在一起,

           这些代码可以影响我们认为是一个小内联函数的调用,特别是,构造函数的规模会抵消函数调用代价的减少,如果做大量的内联函数调用,代码长度就会增长,而在速度上没有任何好处,

           当然,也许不会立即把所有这些小构造函数都变成非内联,因为它们更容易写为内联构造函数,但是,当我们正在调整我们的代码时,请务必去掉这些内联构造函数

        虚函数使用:将函数声明为虚函数会降低效率,一般函数在编译期其相对地址是确定的,编译器可以直接生成imp/invoke指令,如果是虚函数,那么函数的地址是动态的,譬如取到的地址在eax寄存

           器里,则在call eax之后的那些已经被预取到流水线的所有指令都将失效, 流水线越长,那么一次分支预测失败的代价越大,建议若不打算让某类成为基类,那么类中最好不要出现虚函数,

        纯虚函数:含有至少一个纯虚函数的类叫抽象类,因为抽象类含有纯虚函数,所以其虚表是不健全的,在虚表不健全的情况下是不能实例化对象的,子类继承抽象基类后必须重写基类的所有纯虚函数

           否则子类仍为纯虚函数子类将抽象基类的纯虚函数全部重写后会将虚表完善,此时子类才能实例化对象,纯虚函数只声明不定义,形如 virtual void print() = 0

       静态多态:是在编译期就把函数链接起来,此时即可确定调用哪个函数或模板,静态多态是由模板和重载实现的,在宏多态中,是通过定义变量,编译时直接把变量替换,实现宏多态

          优点: 带来了泛型编程的概念,使得C++拥有泛型编程与STL这样的武器; 在编译期完成多态,提高运行期效率; 具有很强的适配性和松耦合性,(耦合性指的是两个功能模块之间的依赖关系)

          缺点: 程序可读性降低,代码调试带来困难;无法实现模板的分离编译,当工程很大时,编译时间不可小觑 ;无法处理异质对象集合

        调用基类指针创建子类对象,那么基类应该有虚析构函数,因为如果基类没有虚析构函数,那么在删除这个子类对象的时候会调用错误的析构函数而导致删除失败产生不明确行为,

          int main() {

            Base *p = new Derive();    //调用基类指针创建子类对象,那么基类应有虚析构函数,不然当删除的时候会调用错误的析构函数而导致删除失败产生不明确行为,

            delete p;            //删除子类对象时,如果基类有虚析构函数,那么delete时会先调用子类的析构函数,然后再调用基类的析构函数,成功删除

            return 0;            //如果基类没有虚析构函数,那么就只会调用父类的析构函数,只删除了对象内的父类部分,造成一个局部销毁,可能导致资源泄露

          }                  //注:只有当此类希望成为 基类时才会打算声明一个虚析构函数,否则不必要给此类声明一个虚函数

  

重写 ,重载, 重定义

    1. 重写 

      定义:子类重写基类的虚函数

        特点:(1) 函数名相同 (2) 作用域不同 (3) 参数列表相同 (4) 基类函数必须有virtual关键字且不能有static (5) 返回值相同 (6)  重写函数的访问修饰符可以不同

        例子:基类:virtual void restrictionChanged(); 子类:void restrictionChanged()

    2. 重载      

      定义:函数名相同,但是参数列表不同,注意main函数不能重载,每个程序的main函数只有一个

        特点:(1) 函数名相同 (2) 作用域相同 (3) 参数列表不同 (4) virtual关键字可有可无 (5) 返回值可以不同 (6) 访问修饰符可以不同

        例子:某类:void restrictionChanged(int); void restrictionChanged(double);

    3. 重定义

      定义:子类重定义基类的函数

        特点:(1) 函数名相同 (2) 作用域不同 (3) 参数列表可以不同 (4) virtual关键字可有可无 (5) 返回值可以不同 (6) 访问修饰符可以不同

        例子:基类:void restrictionChanged(int); 子类:void restrictionChanged(double);

    参数列表不同:指的是个数或类型,但是不能靠返回类型来判断

  关于作者

    姓名:张坤武

    邮箱:1498462303@qq.com