C++复习 之oop & 三大特征 封装 继承 多态 & 虚函数

1、oop思想
在这里插入图片描述

   类:类是现实世界在计算机中的反映,它将数据和对这些数据的操作封装在一起(并没有开空间)
   类中: 数据(成员变量)、程序(成员函数);
   对象:类实例化出对象(占有实际的空间);
   类相当于盖楼房的图纸一样,虽然定义了有哪些成员,但并没有实际的空间;
   类可以实例化出多个对象,实例化出的对象占有实际空间(用来存储成员变量);
  
  早绑定和晚绑定 & 静态绑定和动态绑定?
    1、早绑定 或 静态绑定:编译时期的绑定,函数调用是在编译时期就明确的的调用;
                          普通函数对策调用都是早绑定;
    2、晚绑定 或 动态绑定:运行时期的绑定,就是函数调用在编译阶段不确定,
                         只有到运行时才知道调用的是哪一个函数;
                         动态绑定是OOP语言多态性的具体体现 或者 实现;
 
  面向对象中为什么使用组合多于继承?
    面向对象的原则告诉我们,对类的功能的扩展要多用组合,而少用继承。其中的原因有以下几点: 
      1.子类对父类的继承是全部的公有和受保护的继承,
            这使得子类可能继承了对子类无用甚至有害的父类的方法,
            换句话说,子类只希望继承父类的一部分方法,怎么办? 
      2.实际的对象千变万化,如果每一类的对象都有他们自己的类,
            尽管这些类都继承了他们的父类,但有些时候还是会造成类的无限膨胀;
      3.继承的子类,实际上需要编译期确定下来,这满足不了需要在运行内才能确定对象的情况,
            而组合却可以比继承灵活得多,可以在运行期才决定某个对象;

2、封装 (访问限定)

封装 :隐藏对象的属性和实现细节,仅对外公开接口,控制在程序中属性的读取和修改的访问权限;
对象:成员变量的集合加资源
 三个访问限定符:
 public:     任意位置可以访问
 protected:  本类类中和子类类中访问
 private:    本类类中
 类中:默认以 inline 处理;
 类外:默认以普通的函数处理方式处理; 

3、this 指针

this指针:作用域在类内部,当在类的 非静态成员函数 中访问 类的非静态成员 的时候,
          编译器会自动将对象本身的地址作为一个隐含参数传递给函数;
          对各个成员的访问均通过this指针进行;
      类中的默认函数  用户提供  系统不提供  ||  用户不提供  系统提供
 
 this指针用法:
      1. 作为当前类的指针,用来区别形参和成员变量名称相同的情况;
      2. this作为返回值,返回当前对象的引用;
 
 this指针的六大属性:
     1. 名称属性:标识符this表示;
     2. 类型属性:类类型* const(类似于类引用的类型);
     3. 值属性:表示当前调用该函数对象的首地址;
     4. 作用域:this指针是编译器默认传给类中非静态函数的隐含形参,
                    所以其作用域在非静态成员函数的函数体内;
     5. 链接属性:在该类作用域中,不同类的非静态成员函数中,this这个指针变量的链接属性是内部的,
                 但其所指对象是外部的,即this变量是不同的实体,但指向对象是同一个;
     6. 存储类型:this指针是由编译器生成,当类的非静态成员函数的参数个数一定时,
                 this指针存储在ecx寄存器中;若该函数参数个数未定(可变参数函数),则存放在栈中。
       
   this指针的注意事项:
       1. this指针不能为空;
       2. this指针使用的是指针,而不是引用;
       3. this指针并不是对象的一部分,不影响sizeof的结果;
       4. this指针是由编译器自动生成,作为函数的第一个参数,用户不能显示传递;
       5. this指针不能在构造函数的初始化列表中给对象的成员变量赋值;
               初始化列表本义是:在创建类的实例对象时,给其中成员变量赋初值,即此时对象还未创建完毕;
               而this指针是类在创建之后,由编译器自动生成的指针,指向对象的首地址;
               简单来说,先有对象,后有this指针;所以this指针不能在初始化列表中给成员指针赋初值;
       6.  this指针的存储及传参顺序 
            1、当类的非静态成员函数的参数个数是一定时,this指针存储在ecx寄存器上,
                  通过ecx传递给调用者,此时函数调用约定是_thiscall;
            2、若参数个数不确定(可变参数)时,则借助栈,在所有的参数被压栈后,再压入栈中,
                  此时函数调用约定是_cdecl;
            3、当函数参数不同时,函数的调用约定不同,this的存储类型不同,造成了其传参顺序的不同;

4、类中默认函数

1.构造函数:
      给对象内存做初始化,给对象赋予资源 ;
      可以重载,没有返回值,堆上内存没有名称;
      不依赖对象调动,不能手动调用;
 
2.析构函数:
        释放对象所占的资源
        不可重载,
        依赖对象调用,可以手动调用,调用后会退化成普通函数
 
3.拷贝构造函数:
       拿已存在的对象 给 相同类型的 新对象 做初始化
       形参是引用,不产生新对象,
       浅拷贝,
       (实参传形参,初始化过程,栈溢出程序崩溃)

4.赋值运算符的重载函数
       拿已存在的对象 给 相同类型的 已存在的对象 赋值
       返回值:类类型的引用  可以连续赋值
       浅拷贝

      赋值运算符的重载函数步骤:自赋值,释放旧的资源,开辟新的资源,赋值
      运算符重载的目的:使自定义类型 和 内置类型有相同的逻辑;
    
5.取地址操作符的重载函数;
6.const修饰的取地址操作符的重载函数;
     
      运算符重载:1、迭代器; 2、写实拷贝; 3、内存池;
         1、 迭代器:iterator,存放容器的指针,半开半闭,指针解引用 对象,遍历容器对象;
             面向对象的指针:迭代器、智能指针;
             后置++ 生成临时量  常量;
      
         2、写实拷贝:修改前 浅拷贝;修改后 深拷贝;
               缺点:不知道用户是否是访问还是修改,库里使用深拷贝;
             浅拷贝:问题:产生悬空指针,指针指向空内存,系统崩溃;
             野指针:没有初始化的指针;
           悬空指针:以初始化,但没有意义的指针;
             深拷贝:缺点:只是访问,不修改时:浪费内存,使用率不高;
           
        3、 内存池:自主的内存管理;
                   开辟大内存,自主管理;
                   池:资源的集合,可以循环利用;
                   静态链表
          
           C++的类中:成员函数、成员方法;
  const修饰的成员变量:一定要在初始化列表中初始化
 static修饰的成员变量:1.不属于对象,属于类;2.类外初始化
 static修饰的成员方法:1. 没有this指针 _cdecl调用约定  不依赖对象调用;2.不能访问普通的成员

5、临时对象 & 临时变量

1、临时对象
     建立一个没有命名的非堆(non-heap)对象会产生临时对象;
   这种未命名的对象通常在两种条件下产生:为了使函数成功调用而进行隐式类型转换 和 函数返回对象时。
      1.为使函数成功调用而建立临时对象:
           当传送给函数的对象类型与参数类型不匹配时会产生这种情况;
           仅当通过传值(by value)方式传递对象或传递常量引用(reference-to-const)参数时,
           才会发生这些类型转换。当传递一个非常量引用(reference-to-non-const)参数对象,就不会发生。
      2.函数返回对象时:
          例如给定一个类型Number,这种类型的operator+被这样声明:
          const Number operator+(const Number& lhs, const Number& rhs); 
          这个函数的返回值是临时的,因为它没有被命名;它只是函数的返回值。
          你必须为每次调用operator+构造和释放这个对象而付出代价。 
          
       通常你不想付出这样的开销。对于这种函数,你可以切换到operator=,而避免开销。
       不过对于大多数返回对象的函数来说,无法切换到不同的函数,从而没有办法避免构造和释放返回值。
       至少在概念上没有办法避免它。然而概念和现实之间又一个黑暗地带,叫做优化,
       有时你能以某种方法编写返回对象的函数,以允许你的编译器优化临时对象。
       这些优化中,最常见和最有效的是返回值优化。
       
       临时对象是有开销的,所以你应该尽可能地去除它们,
       然而更重要的是训练自己寻找可能建立临时对象的地方。
       在任何时候只要见到常量引用(reference-to-const)参数,就存在建立临时对象而绑定在参数上的可能性。
       在任何时候只要见到函数返回对象,就会有一个临时对象被建立(以后被释放) 。
 
   临时对象的优化:
       条件:临时对象生成是为了生成新对象;
       方式:以生成临时对象的方式来生成新对象;
   作用域:表达式结束;
   引用能提升临时对象的生存周期:把临时对象提升成和引用变量相同的生存周期;
   内置类型的临时量  ==>  常量
      自定义类型    ==>  变量
    隐示生成临时量  ==> 常量

     隐示:系统推演对象类型,生成对象 ;
     显示:指出对象类型,生成对象;
    
2、临时变量: 
     如果实参与引用参数(形参)不匹配,C++将生成临时变量,
           当前,仅当参数为const引用时,C++才允许这样做。
     临时变量通常在 函数参数传递发生类型转换 以及 函数返回值 时被创建;
     临时变量的三种情况:
       1.以By Value的方式传值。
            我们都知道,引用类型和指针类型传递的都是地址,可以直接对地址中存放的数据进行操作,
            而以传值的方式传递参数,就会在heap中重新分配一个临时区域,将实参中的数据拷贝到临时区域中,
            而你对这份数据进行的任何的操作都不会影响实参的内容,因为实参跟形参只是内容相同,
            分别在两块不同的内存中。而引用和指针操作的是同一块内存,所以形参修改后,实参也修改了。
       2.参数为const的类型。
            因为常量是不能修改,在只需要实参中的数据,而不需对实参进行修改时,
            或是禁止对实参进行修改时,把形参定义为const类型,系统会产生一个临时变量,
            就能起到保护数据的作用,如在函数strlen中,修改参数的值行吗?
            本来只是想得到实参的长度,结果在函数中被修改了,那得到得实参长度还是真实的吗。
            如果你程序中的数据到处都可以被修改,那是多么的可怕(所以我们讨厌全局变量),
            所以const还是有它存在的价值。
       3.类型转换的时候会产生临时变量。
            在用类型转换带来便利的同时,产生临时变量就是我们承担的损失,
            如将一个short类型转换成int类型,他们占用的内存不一样,如果不产生临时变量,
            那不就short类型和int类型占用的字节数不就一样了吗,sizeof不就坑爹了吗。
            C++语言禁止为非常量引用产生临时对象。同时证明引用类型传参不会产生临时变量,
            如char[]转换成string会报错,他们都是引用类型。
            
       临时变量不能作为非const引用参数:
            不是因为他是常量,而是因为c++编译器的一个关于语义的限制。
            如果一个参数是以非const引用传入,c++编译器就有理由认为程序员会在函数中修改这个值,
            并且这个被修改的引用在函数返回后要发挥作用。但如果你把一个临时变量当作非const引用参数传进来,
            由于临时变量的特殊性,程序员并不能操作临时变量,而且临时变量随时可能被释放掉,
            所以,一般说来,修改一个临时变量是毫无意义的,
            据此,c++编译器加入了临时变量不能作为非const引用的这个语义限制,
            意在限制这个非常规用法的潜在错误。

6、模板

1、函数模板的相关概念  :函数模板、模板的实例化、模板函数;
    函数模板:实参推演:1.有实参  ;2.不能产生二义性
   模板特例化
   模板的类型参数列表:
         1.类型参数 :   typename class 
         2.非类型参数 : 1.浮点型  对象  ;2.常量
         3.模板
 模板的编译:
     1.定义点    编译模板头部
     2.调用点    编译模板函数,模板实例化  (.h文件)

 2、类模板
       typename
       1.定义模板类型参数
       2.声明类型
           特例化类型:
              1.完全特例化   全特化
              2.部分特例化   偏特化

7、继承(代码复用)

1、什么是继承:
    1.两个类的关系如果满足 a kind of 的关系,也就是B是A的一种关系,
        那么就可以把A设计成基类,B设计成从A继承而来的派生类;
    2.继承的本质含义就是代码的复用,派生类可以继承除 基类构造、析构函数 以外的
        其他所有的成员,为派生类所用,派生类只需要实现自己特有的成员即可;
    3.基类的 指针或引用 可以指向派生类对象,反之不行;
    4.两个互为继承关系的类,基类和派生类的同名方法(返回值、函数名、参数列表都相同),
         而且基类的该方法是 virtual 虚函数,那么这两个方法之间互为覆盖的关系,否则是隐藏,
         基类和派生类之间同名方法可以覆盖,是C++多态的必备条件;
    5.C++是支持多重继承的,但是典型的菱形继承会发生问题,导致派生类拥有间接基类
         多份成员变量,和实际情况不符,解决这个问题的办法是引入虚继承;
    

2、基类派生类:
   1.基类和派生类的关系
        基类 派生 派生类  或者  派生类 继承 基类 ;
   2.派生类继承了基类的什么:
        除了 构造 和 析构 以外的所有成员;
   3.派生类的内存布局:
        作用域也继承;
   4.派生对象的构造和析构方式:
        构造方式:                  析构方式:
          1、开辟内内存                1、调用析构
          2、调用构造:                    1.派生类析构
              1.调用基类的构造             2.基类析构
              2.调用派生类的构造       2、释放内存
           
    5.继承方式:
                    继承方式      访问限定符
      1.public      公有继承         任意
      2.protected   保护继承         本类和子类
      3.private     私有继承         本类
 
   基类的不同的访问限定下的成员 以不同的继承方式 继承 在派生类中的访问限定
                    public        protected        private
      public        public        protected        不可访问
      protected     protected     protected        不可访问
      private       private       private          不可访问
    
      基类时:
          基类中没有默认的构造函数可用
          派生类的构造函数的初始化列表中指明基类的构造方式
      基类和派生类指针或引用的相互指向或引用:
          允许基类的指针或引用指向或者引用派生类对象
          不允许派生类的指针或引用指向或引用基类对象
   
3、继承方式:
      类和类的关系:
          1.组合  a part of   has_a
          2.继承  a kind of   is_a
          3.代理
        
      同名函数的关系:
          1. 重载 (overload): 重定义
               三要素:同名、不同参、同作用域
               重载的底层实现过程:
                   C++编译器编译函数符号的时候,是根据 函数名 和 形参的个数、类型 
                   来共同决定的,因此如果一组函数只是函数名相同,而参数列表不同的话,
                   那么它们生成的符号也是不同的,就不会产生冲突了;
                C++重载机制:
                   编译器编译时,根据调用点用户传入的实参,
                   来选择一个类型最为匹配的函数重载版本进行调用;
                   一切都在编译过程中确定的;
          2. 隐藏:派生类中同名的函数隐藏了基类中所有的同名函数;
                     1.继承,不同作用域;
                     2.同名;
          3.覆盖(重写):
                派生类中 同名同参 的函数 覆盖了基类中同名同参的虚函数;
                     1.继承 ,不同作用域;
                     2.虚函数
                     3.同名同参

8、多态 & 虚函数

1、多态
      多态:同一接口 不同形态  
      多态的实现过程:虚函数是实现多态的机制,通过基类访问派生类定义的函数,
                   多态使得程序调用的函数是在运行时动态实现的,而不是编译时静态实现的;
   C++实现多态性的一个最经典的场景:             
      使用一个基类类型的指针或引用,来指向子类对象, 进而调用子类复写的个性化的虚函数;
      多态发生的时机:1.指针调用虚函数 ;2.对象完整;
      静态的多态:函数重载 和 模板(包括函数模板和类模板)
                 编译时期的多态(函数的重载和实例化都发生在编译阶段)、  确定函数的调用;
      动态的多态:继承 和 虚函数
                 运行时期的多态(静态绑定、动态绑定的概念)、  确定函数的调用;    
                    
                 多态的好处 : 通过多态可以减少类中的代码量,可以提高代码的可扩展性和可维护性。
                 
2、虚函数
      虚函数:是成员方法前面加了 virtual 关键字;
             virtual {};
            (虚函数:在函数段和只读数据段存放数据)
       虚函数的底层实现过程:
             通过对象内存中的虚函数指针vfptr找到虚函数表vftable,
             再通过vftable中的函数指针找到对应虚函数的实现区域并进行调用,
             所以虚函数的调用时由指针所指向的内存块的具体类型决定;
            (当使用类的指针调用成员函数时:普通函数指针由指针类型决定);
     
      虚函数机制:为 动多态 提供支持;
                 1. 编译过程中 生成虚函数表;
                 2. 一个类共享一个虚函数表;
                 3. 对象里面给出虚函数指针;
      如果一个类有虚函数,那么这个类生成的对象就会多出4个字节;
        (多出个 vfptr 虚函数指针 这四个字节)
      vfptr 指向的是一张虚函数表 vftable ;
      虚函数表中放的是虚函数的地址;(数据段存放)
      虚函数表的写入时机:1、构造函数的第一行代码调用前写入; 2、二次写入;
      虚函数表放在那里:
          虚函数是在 编译 过程中生成的,一个类只要有虚函数,或者 是从基类继承了虚函数,
          那么它就会有一个虚函数表生成,虚函数表运行时放在 .rodata 段,只能读,不能写,
          和常量字符串是放在同一段的,它的生命期是整个应用程序的生命周期;
      
      虚函数作用:
          允许在派生类中重新定义与基类同名的函数,
          并且可以通过基类指针或引用来访问基类和派生类中的同名函数;
      补充:
          在派生类的基类部分中,派生类的虚函数取代了基类原来的虚函数,
          因此在使基类指针指向派生类对象后,调用虚函数时就调用了派生类的虚函数;
          要注意的是:只有用 virtual 声明了虚函数后才具有以上作用,
                     如果不声明为虚函数,企图通过基类指针调用派生类中的非虚函数是不行的;
      
      那些函数可以成为虚函数:
          1. 取地址:虚函数的地址要存在虚函数表中,因此必须能产生函数符号地址;
          2. 依赖函数调用:只能通过对象的前四个字节 vfptr 才能访问虚函数表,
                          进而访问虚函数的地址;
                可以成为虚函数: 普通类成员方法、析构函数 
              不可以成为虚函数: 普通的全局变量、静态的成员方法、inline 函数、构造函数;
                                                             不生成符号  对象还没产生
       析构函数什么时候必须写成虚函数:
           当使用基类的指针指向 堆上的派生类对象时,如下代码:
           Base *p = new Derive();
           delete p;
           当你想通过delete释放派生类对象的内存时,
           会导致派生类对象的析构函数无法调用(依赖对象), 只调用了基类部分的析构函数,
           如果此时派生类的析构函数有释放额外系统资源的代码;那么就造成资源泄漏了,
           delete p;这句代码,编译的时候是静态绑定的,就只调用基类的析构;
           解决:如果把基类的析构函数写成虚析构函数,就是动态绑定了,
           由于派生类提供了自定义的析构函数,那么虚函数表写的就是派生类析构函数的地址,
           此时,派生类和基类的析构函数就都可以调用了,解决资源泄漏问题;
        
       
       纯虚函数:
           纯虚函数:在基类中声明的虚函数,
                   virtual void fun() const = 0;// '=0' 为纯虚指示符
                   virtual {} = 0;
                    它要求任何派生类都要定义自己的实现方法,以实现多态性;
                    实现了纯虚函数的子类,该纯虚函数在子类中就变成了虚函数;
              (基类的方法需要依赖具体实体的都必须实现成纯虚函数)
       纯虚函数的作用:
           1、定义纯虚函数是为了实现一个接口,来规范派生类的行为,
             也即规范继承这个类的程序员必须实现这个函数,派生类仅仅是继承函数的接口;
           2、让所有的类对象(主要是派生类对象)都可以执行纯虚函数的动作,
                 但基类无法为纯虚函数提供一个合理的缺省实现;
        
        抽象类:含有纯虚函数的类;
               它不能生成对象(创建实例),只能创建它的派生类的实例、做指针或引用;
               它是为了抽象和设计的目的建立的,处于继承层次结构的较上层;
          作用:
                将有关的操作作为结果接口组织在一个继承层次结构中,
                由它来为派生类提供一个公共的根,
                派生类将具体实现在其基类中作为接口的操作;
              
          多继承:菱形继承;
          画内存布局(虚基类):非虚继承的内存优先 ==> 虚基类;
          
          虚函数 & 纯虚函数,如何选择:
               1、当基类中的某个成员方法,在大多数情形下,都由子类提供个性化实现,
                    但基类也可以提供缺省备选方案的时候,该方法应该设计为虚函数;
               2、当基类中的某个成员方法,必须由子类提供个性化实现的时候,
                    应该设计为纯虚函数;

9、实现一个不能被继承的类

1.使用静态函数来实现:从构造函数着手;
      将这个类的构造函数和析构函数都声明是私有的,使用虚继承;
      这样它的子类构造时就会报错,子类就不能被继承了;
      用一个static函数来帮助创建这个类的实例,使之可以实例化;
     (在继承关系中:基类定义了一个static成员;
        那么在整个继承体系中都只有一个static成员;
        且该静态成员被所有基类对象和派生类对象所共有;)
   缺点:
       这是一个有效的方法,但是static函数创建出来的实例必然是static的;
       而且,这个类不能像普通的类那样构建对象;
2.使用友元函数和模板:
     1.将A的构造函数和析构函数都声明为private的,但是将B作为A的友元类,
          这样B就可以访问A的构造函数和析构函数了,此时B能正常构造;         
     2.为了使B的子类C不能被正常构造,可以让C直接调用A的构造函数,
          那么将B设置成虚拟继承自A;
     3.因为友元关系是不能被继承的,所以C调用A的构造函数时会报错;

    主要使将A的构造函数和析构函数声明是private的了,
    并且将B声明是A的友元类让B能调用A的构造函数,
    让B的子类不能调用A的构造函数;
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值