05-📝C++核心语法|面向对象3【 继承和派生、多态、静态成员、const成员、引用类型成员、VS的内存窗口】

简介: 复习`C++核心语法`,且适当进行汇编探索底层实现原理,进一步夯实基础,为以后的`底层开发`、`音视频开发`、`跨平台开发`、`算法`等方向的进一步学习埋下伏笔。

一、前言

最近刚好有空,趁这段时间,复习一下C++语言,进一步夯实基础,为以后的底层开发音视频开发跨平台开发算法等方向的进一步学习埋下伏笔

我们在上一篇文章中,已经充分说明,C++语言是对C的扩展,建立在对C语言知识掌握的基础上学习C++是事半功倍的\
如果你对C语言已经淡忘,或者没有学过C语言,且一时半会没有思路如何筛选可靠的C语言学习资料,可以借鉴我的这几篇文章:

1. C语言核心知识

二、继承和派生

1. 继承概述

1.1 为什么需要继承




1.2 继承基本概念

  • C++最重要的特征是代码重用,通过继承机制可以利用 已有的数据类型来定义新的数据类型,新的类不仅拥有旧类的成员,还拥有新定义的成员
  • 一个B类继承于A类,或称从类A派生类B。这样的话,类A成为基类(父类), 类B成为派生类(子类)。
  • 派生类中的成员,包含两大部分:

    • 一类是从基类继承过来的,一类是自己增加的成员。
    • 从基类继承过过来的表现其共性,而新增的成员体现了其个性

1.3 派生类定义

  • 派生类定义格式:

    Class 派生类名 :  继承方式 基类名{
        //派生类新增的数据成员和成员函数
    }
  • 三种继承方式:

    • public : 公有继承
    • private : 私有继承
    • protected : 保护继承
  • 从继承源上分:

    • 单继承:

      • 指每个派生类只直接继承了一个基类的特征
    • 多继承:

      • 指多个基类派生出一个派生类的继承关系,多继承的派生类直接继承了不止一个基类的特征

2. 派生类访问控制

  • 派生类继承基类

    • 派生类拥有基类中全部成员变量和成员方法(除了构造和析构之外的成员方法)
    • 但是在派生类中,继承的成员并不一定能直接访问,不同的继承方式会导致不同的访问权限
  • 派生类的访问权限规则如下:\

  • 代码示例:

    //基类
    class A{
        public:
            int mA;
        protected:
            int mB;
            private:
            int mC;
    };
    // 公有(public)继承
    class B : public A{
        public:
        void PrintB(){
            cout<< mA << endl; //可访问基类public属性
            cout<< mB << endl; //可访问基类protected属性
            //cout << mC <<endl; //不可访问基类private属性
        }
    };
    
    class SubB : public B{
        void PrintSubB(){
            cout<< mA << endl; //可访问基类public属性
            cout<< mB << endl; //可访问基类protected属性
            //cout << mC <<endl; //不可访问基类private属性
        }
    };
    
    void test01(){
        B b;
        cout<< b.mA << endl; //可访问基类public属性
        //cout << b.mB <<endl; //不可访问基类protected属性
        //cout << b.mC <<endl; //不可访问基类private属性
    }
    
    //2. 私有(private)继承
    class C : private A{
        public:
            void PrintC(){
            cout<< mA << endl; //可访问基类public属性
            cout<< mB << endl; //可访问基类protected属性
            //cout << mC <<endl; //不可访问基类private属性
        }
    };
    
    class SubC : public C{
        void PrintSubC(){
            //cout << mA <<endl; //不可访问基类public属性
            //cout << mB <<endl; //不可访问基类protected属性
            //cout << mC <<endl; //不可访问基类private属性
        }
    };
    
    void test02(){
        Cc;
        //cout << c.mA <<endl; //不可访问基类public属性
        //cout << c.mB <<endl; //不可访问基类protected属性
        //cout << c.mC <<endl; //不可访问基类private属性
    }
    
    //3. 保护(protected)继承
    class D : protected A{
        public:
            void PrintD(){
                cout<< mA << endl; //可访问基类public属性
                cout<< mB << endl; //可访问基类protected属性
                //cout << mC <<endl; //不可访问基类private属性
            }
    };
    
    class SubD : public D{
        void PrintD(){
            cout<< mA << endl; //可访问基类public属性
            cout<< mB << endl; //可访问基类protected属性
            //cout << mC <<endl; //不可访问基类private属性
        }
    };
    
    void test03(){
        Dd;
        //cout << d.mA <<endl; //不可访问基类public属性
        //cout << d.mB <<endl; //不可访问基类protected属性
        //cout << d.mC <<endl; //不可访问基类private属性
    }

3. 继承中的构造和析构

3.1 继承中的对象模型

在C++编译器的内部可以理解为结构体,子类是由父类成员叠加子类新成员而成:

class Aclass{
    public:
        int mA;
        int mB;
};

class Bclass : public Aclass{
    public:
        int mC;
};

class Cclass : public Bclass{
    public:
        int mD;
};

void test(){
    cout << "A size:" << sizeof(Aclass) << endl;
    cout << "B size:" << sizeof(Bclass) << endl;
    cout << "C size:" << sizeof(Cclass) << endl;
}

3.2 对象构造和析构的调用原则

继承中的构造和析构

  • 子类对象在创建时会首先调用父类的构造函数
  • 父类构造函数执行完毕后,才会调用子类的构造函数
  • 当父类构造函数有参数时,需要在子类初始化列表(参数列表)中显示调用父类构造函数
  • 析构函数调用顺序和构造函数相反
  • 代码示例:

    class A{
        public:
            A(){
                cout<< "A类构造函数!" << endl;
            }
            ~A(){
                cout<< "A类析构函数!" << endl;
            }
    };
    class B : public A{
        public:
            B(){
                cout<< "B类构造函数!" << endl;
            }
            ~B(){
                cout<< "B类析构函数!" << endl;
            }
    };
    class C : public B{
        public:
            C(){
                cout<< "C类构造函数!" << endl;
            }
            ~C(){
                cout<< "C类析构函数!" << endl;
            }
    };
    void test(){
        C c;
    }
  • 继承与组合混搭的构造和析构\

  • 代码示例:

    class D{
        public:
            D(){
                cout<< "D类构造函数!" << endl;
            }
            ~D(){
                cout<< "D类析构函数!" << endl;
            }
    };
    class A{
        public:
        A(){
            cout<< "A类构造函数!" << endl;
        }
        ~A(){
            cout<< "A类析构函数!" << endl;
        }
    };
    class B : public A{
        public:
            B(){
                cout<< "B类构造函数!" << endl;
            }
            ~B(){
                cout<< "B类析构函数!" << endl;
            }
    };
    class C : public B{
        public:
            C(){
                cout<< "C类构造函数!" << endl;
            }
            ~C(){
                cout<< "C类析构函数!" << endl;
            }
        public:
            D c;
    };
    
    void test(){
        C c;
    }
  • 注意: 如果重新定义了基类中的重载函数,将会发生什么?

class Base{
    public:
        void func1(){
            cout<< "Base::void func1()" << endl;
        };
        void func1(int param){
            cout<< "Base::void func1(int param)" << endl;
        }
        void myfunc(){
            cout<< "Base::void myfunc()" << endl;
        }
};

class Derived1 : public Base{
    public:
        void myfunc(){
            cout<< "Derived1::void myfunc()" << endl;
        }
};

class Derived2 : public Base{
    public:
    //改变成员函数的参数列表
    void func1(int param1, int param2){
        cout<< "Derived2::void func1(int param1,intparam2)" << endl;
    };
};

class Derived3 : public Base{
    public:
    //改变成员函数的返回值
    int func1(int param){
        cout<< "Derived3::int func1(intparam)" << endl;
        return 0;
    }
};

int main(){
    Derived1derived1;
    derivedfunc1();
    derivedfunc1(20);
    derivedmyfunc();
    cout<< "-------------" << endl;
    Derived2derived2;
    //derived2.func1();  //func1被隐藏
    //derived2.func1(20); //func2被隐藏
    derived2.func1(10,20); //重载func1之后,基类的函数被隐藏
    derived2.myfunc();
    cout<< "-------------" << endl;
    Derived3derived3;
    //derived3.func1();  没有重新定义的重载版本被隐藏
    derived3.func1(20);
    derived3.myfunc();

    return EXIT_SUCCESS;
}
- Derive1重定义了Base类的myfunc函数,derive1可访问func1及其重载版本的函数。
- Derive2通过改变函数参数列表的方式重新定义了基类的func1函数,则从基类中继承来的其他重载版本被隐藏,不可访问
- Derive3通过改变函数返回类型的方式重新定义了基类的func1函数,则从基类继承来的没有重新定义的重载版本的函数将被隐藏。

4. 继承中同名成员的处理方法

  • 当子类成员和父类成员同名时,子类依然从父类继承同名成员
  • 如果子类有成员和父类同名,子类访问其成员默认访问子类的成员(本作用域,就近原则)
  • 在子类通过作用域::进行同名成员区分(在派生类中使用基类的同名成员,显示使用类名限定符)
  • 代码示例:

        class Base{
            public:
                Base():mParam(0){}
                void Print(){ cout << mParam << endl; }
            public:
                int mParam;
        };
    
        class Derived : public Base{
            public:
                Derived():mParam(10){}
                void Print(){
                    //在派生类中使用和基类的同名成员,显示使用类名限定符
                    cout<< Base::mParam << endl;
                    cout<< mParam << endl;
                }
                //返回基类重名成员
                int& getBaseParam(){ return  Base::mParam; }
            public:
                int mParam;
        };
    
        int main(){
            Derived derived;
            //派生类和基类成员属性重名,子类访问成员默认是子类成员
            cout<< derived.mParam << endl; //10
            derived.Print();
            //类外如何获得基类重名成员属性
            derived.getBaseParam() = 100;
            cout<< "Base:mParam:" << derived.getBaseParam() << endl;
            return EXIT_SUCCESS;
       }

5 非自动继承的函数

  • 不是所有的函数都能自动从基类继承到派生类中
  • 构造函数和析构函数用来处理对象的创建和析构操作,构造和析构函数只知道对它们的特定层次的对象做什么,也就是说构造函数和析构函数不能被继承,必须为每一个特定的派生类分别创建
  • 另外operator=也不能被继承,因为它完成类似构造函数的行为。也就是说尽管我们知道如何由=右边的对象如何初始化=左边的对象的所有成员,但是这个并不意味着对其派生类依然有效
  • 在继承的过程中,如果没有创建这些函数,编译器会自动生成它们

6. 继承中的静态成员特性

  • 静态成员函数和非静态成员函数的共同点:

      1. 他们都可以被继承到派生类中
      1. 如果重新定义一个静态成员函数,所有在基类中的其他重载函数会被隐藏
      1. 如果我们改变基类中一个函数的特征,所有使用该函数名的基类版本都会被隐藏。
    • 静态成员函数不能是虚函数(virtual function)

      class Base{
          public:
              static int getNum(){ return sNum; }
              static int getNum(int param){
                  return sNum + param;
              }
          public:
              static int sNum;
      };
      
      int Base::sNum = 10;
      
      class Derived : public Base{
          public:
              static int sNum; //基类静态成员属性将被隐藏
          #if 0
          //重定义一个函数,基类中重载的函数被隐藏
          static int getNum(int param1, int param2){
              return sNum + param1 + param2;
          }
          #else
          //改变基类函数的某个特征,返回值或者参数个数,将会隐藏基类重载的函数
          static void getNum(int param1, int param2){
              cout <<  sNum + param1 + param2 << endl;
          }
          #endif
      };
      
      int Derived::sNum = 20;

7. 多继承

7.1 多继承概念

  • 我们可以从一个类继承,我们也可以能同时从多个类继承,这就是多继承。
  • 但是由于多继承是非常受争议的,从多个类继承可能会导致函数、变量等同名导致较多的歧义。\

  • 代码示例:

    class Base1{
        public:
            void func1(){ cout << "Base1::func1" << endl; }
    };
    class Base2{
        public:
        void func1(){ cout << "Base2::func1" << endl; }
        void func2(){ cout << "Base2::func2" << endl; }
    };
    //派生类继承Base1、Base2
    class Derived : public Base1, public Base2{};
    
    int main(){
        Derived derived;
        //func1是从Base1继承来的还是从Base2继承来的?
        //derived.func1(); 
        derived.func2();
        //解决歧义:显示指定调用那个基类的func1
        derived.Base1::func1(); 
        derived.Base2::func1();
        return EXIT_SUCCESS;
    }
  • 多继承会带来一些二义性的问题, 如果两个基类中有同名的函数或者变量,那么通过派生类对象去访问这个函数或变量时就不能明确到底调用从基类1继承的版本还是从基类2继承的版本?
  • 解决方法就是显示指定调用那个基类的版本。

7.2 菱形继承虚继承

  • 两个派生类继承同一个基类而又有某个类同时继承者两个派生类,这种继承被称为菱形继承,或者钻石型继承

  • 这种继承所带来的问题:

      1. 羊继承了动物的数据和函数,鸵同样继承了动物的数据和函数,当草泥马调用函数或者数据时,就会产生二义性。
      1. 草泥马继承自动物的函数和数据继承了两份,其实我们应该清楚,这份数据我们只需要一份就可以

    • 上述问题如何解决?对于调用二义性,那么可通过指定调用那个基类的方式来解决,那么重复继承怎么解决?
    • 对于这种菱形继承所带来的两个问题,c++为我们提供了一种方式,采用虚基类。那么我们采用虚基类方式将代码修改如下

  • 以上程序Base1 ,Base2采用虚继承方式继承BigBase,那么BigBase被称为虚基类。
  • 通过 虚继承 解决了菱形继承所带来的二义性问题。
  • 但是虚基类是如何解决二义性的呢?并且derived大小为12字节,这是怎么回事?
  • 菱形继承|总结

    • 菱形继承带来的问题



    - 最底下子类从基类继承的成员变量冗余、重复
    - 最底下子类无法访问基类的成员,有二义性

7.3 虚继承实现原理


  • 通过内存图,我们发现 普通继承虚继承 的对象内存图是不一样的。 我们也可以猜测到编译器肯定对我们编写的程序做了一些手脚

    • BigBase 菱形最顶层的类,内存布局图没有发生改变。
    • Base1和Base2通过虚继承的方式派生自BigBase

      • 这两个对象的布局图中可以看出编译器为我们的对象中增加了一个vbptr (virtual base pointer)
      • vbptr指向了一张表,这张表保存了当前的虚指针相对于虚基类的首地址的偏移量。
    • Derived派生于Base1和Base2,继承了两个基类的vbptr指针,并调整了vbptr与虚基类的首地址的偏移量
    • 由此可知编译器帮我们做了一些幕后工作

      • 使得这种菱形问题在继承时候能只继承一份数据,并且也解决了二义性的问题。
      • 现在模型就变成了Base1和Base2 Derived三个类对象共享了一份BigBase数据
  • 当使用 虚继承 时,虚基类 是被共享的

    • 也就是在继承体系中无论被继承多少次,对象内存模型中均只会出现一个虚基类的子对象(这和多继承是完全不同的)。
  • 即使共享 虚基类 ,但是 必须要有一个类来完成基类的初始化

    • (因为所有的对象都必须被初始化,哪怕是默认的)
  • 同时还不能够重复进行初始化,那到底谁应该负责完成初始化呢?

    • C++标准中选择在每一次继承子类中都必须书写初始化语句(因为每一次继承子类可能都会用来定义对象),但是虚基类的初始化是由最后的子类完成,其他的初始化语句都不会调用

    image.png
    image.png

  • 注意

    • 虚继承只能解决具备公共祖先的多继承所带来的二义性问题,不能解决没有公共祖先的多继承的.
  • 工程开发中真正意义上的多继承是几乎不被使用,因为多重继承带来的代码复杂性远多于其带来的便利,多重继承对代码维护性上的影响是灾难性的,在设计方法上,任何多继承都可以用单继承代替
  • 虚继承|总结

    • 虚继承可以解决菱形继承带来的问题
    • Person类被称为 虚基类\


    image.png
    image.png

8. 继承|总结

  • 继承,可以让子类拥有父类的所有成员(变量\函数)

  • 关系描述

    • Student是子类(subclass,派生类)
    • Person是父类(superclass,超类)
  • C++中没有像Java、Objective-C的基类

    • Java:java.lang.Object
    • Objective-C:NSObject

9. 父类指针、子类指针

  • 父类指针可以指向子类对象,是安全的,开发中经常用到(继承方式必须是public)
  • 子类指针指向父类对象是不安全的




三、多态

1. 多态基本概念

  • 默认情况下,编译器只会根据指针类型调用对应的函数,不存在多态
  • 多态是面向对象程序设计语言中数据抽象和继承之外的第三个基本特征,是面向对象非常重要的一个特性

    • 同一操作作用于不同的对象,可以有不同的解释,产生不同的执行结果
    • 在运行时,可以识别出真正的对象类型,调用对应子类中的函数
  • 多态的要素

    • 子类重写父类的成员函数(override)
    • 父类指针指向子类对象
    • 利用父类指针调用重写的成员函数
  • 多态性(polymorphism)提供接口与具体实现之间的另一层隔离,从而将”what”和”how”分离开来。
  • 多态性改善了代码的可读性和组织性,同时也使创建的程序具有可扩展性,项目不仅在最初创建时期可以扩展,而且当项目在需要有新的功能时也能扩展。
  • C++支持编译时多态(静态多态)和运行时多态(动态多态)

    • 运算符重载和函数重载就是编译时多态
    • 派生类和虚函数实现运行时多态
  • 静态多态和动态多态的区别

    • 静态多态和动态多态的区别就是函数地址是早绑定(静态联编)还是晚绑定(动态联编)。
    • 如果函数的调用,在编译阶段就可以确定函数的调用地址,并产生代码,就是静态多态(编译时多态),就是说地址是早绑定的
    • 而如果函数的调用地址不能编译不能在编译期间确定,而需要在运行时才能决定,这这就属于晚绑定(动态多态,运行时多态)
    • 代码示例:\

2. 向上类型转换及问题

2.1 问题抛出

  • 对象可以作为自己的类或者作为它的基类的对象来使用。还能通过基类的地址来操作它。取一个对象的地址(指针或引用),并将其作为基类的地址来处理,这种称为向上类型转换。
  • 也就是说:父类引用或指针可以指向子类对象,通过父类指针或引用来操作子类对象。

- 运行结果: 动物在唱歌
- 问题抛出: 我们给DoBussiness传入的对象是dog,而不是animal对象,输出的结果应该是Dog::speak

2.2 问题解决思路

  • 解决这个问题,我们需要了解下绑定(捆绑,binding)概念。
  • 当绑定在程序运行之前(由编译器和连接器)完成时,称为早绑定(early binding).C语言中只有一种函数调用方式,就是早绑定。
  • 上面的问题就是由于早绑定引起的,因为编译器在只有Animal地址时并不知道要调用的正确函数。编译是根据指向对象的指针或引用的类型来选择函数调用。这个时候由于DoBussiness的参数类型是Animal&,编译器确定了应该调用的speak是Animal::speak的,而不是真正传入的对象Dog::speak
  • 解决方法就是迟绑定(迟捆绑,动态绑定,运行时绑定,latebinding),意味着绑定要根据对象的实际类型,发生在运行。
  • C++语言要实现这种动态绑定,必须有某种机制来确定运行时对象的类型并调用合适的成员函数。对于一种编译语言,编译器并不知道实际的对象类型(编译器并不知道Animal类型的指针或引用指向的实际的对象类型)。

2.3 问题解决方案(虚函数,vitual function)

  • 虚函数

    • C++中的多态通过虚函数(virtual function)来实现
    • 虚函数:被virtual修饰的成员函数
    • 只要在父类中声明为虚函数,子类中重写的函数也自动变成虚函数(也就是说子类中可以省略virtual关键字)
    • 虚函数允许子类(派生类)重新定义父类(基类)成员函数
    • 而子类(派生类)重新定义父类(基类)虚函数的做法称为覆盖(override),或者称为重写
  • 对于特定的函数进行动态绑定,C++要求在基类中声明这个函数的时候使用virtual关键字,动态绑定也就对virtual函数起作用

    • 为创建一个需要动态绑定的虚成员函数,可以简单在这个函数声明前面加上virtual关键字,定义时候不需要.
    • 如果一个函数在基类中被声明为virtual,那么在所有派生类中它都是virtual的.
    • 在派生类中virtual函数的重定义称为重写(override).
    • Virtual关键字只能修饰成员函数.
    • 构造函数不能为虚函数
  • 注意:

    • 仅需要在基类中声明一个函数为virtual.调用所有匹配基类声明行为的派生类函数都将使用虚机制。
    • 虽然可以在派生类声明前使用关键字virtual(这也是无害的),但这个样会使得程序显得冗余和杂乱。(我建议写上)

3. C++如何实现动态绑定

  • 动态绑定什么时候发生?所有的工作都是由编译器在幕后完成。当我们告诉通过创建一个virtual函数来告诉编译器要进行动态绑定,那么编译器就会根据动态绑定机制来实现我们的要求, 不会再执行早绑定
问题:C++的动态捆绑机制是怎么样的?


  • 首先,我们看看编译器如何处理虚函数

    • 虚表

      • 虚函数的实现原理是虚表,这个虚表里面存储着最终需要调用的虚函数地址,这个虚表也叫虚函数表

      • 虚表(x86环境的图)\

        • 所有的Cat对象(不管在全局区、栈、堆)共用同一份虚表
      • 虚表汇编分析\

      • 虚表(x86环境的图)\
    • 当编译器发现我们的类中有虚函数的时候,编译器会创建一张虚函数表,把虚函数的函数入口地址放到虚函数表中,并且在类中秘密增加一个指针,这个指针就是vpointer(缩写vptr),这个指针是指向对象的虚函数表
    • 在多态调用的时候,根据vptr指针,找到虚函数表来实现动态绑定
  • 在编译阶段,编译器秘密增加了一个vptr指针,但是此时vptr指针并没有初始化指向虚函数表(vtable),什么时候vptr才会指向虚函数表?

    • 在对象构建的时候,也就是在对象初始化调用构造函数的时候。
    • 编译器首先默认会在我们所编写的每一个构造函数中,增加一些vptr指针初始化的代码。
    • 如果没有提供构造函数,编译器会提供默认的构造函数,那么就会在默认构造函数里做此项工作,初始化vptr指针,使之指向本对象的 虚函数表
  • 起初,子类继承基类,子类继承了基类的vptr指针,这个vptr指针是指向基类虚函数表,当子类调用构造函数,使得子类的vptr指针指向了子类的 虚函数表
当子类无重写基类虚函数时:


  • 过程分析:

    • Animal*animal = new Dog;
    • animal->fun1();
    • 当程序执行到这里,会去animal指向的空间中寻找vptr指针,通过vptr指针找到func1函数,此时由于子类并没有重写也就是覆盖基类的func1函数,所以调用func1时,仍然调用的是基类的func1.执行结果:
  • 执行结果:

    • 我是基类的func1
  • 测试结论:

    • 无重写基类的虚函数,无意义
  • 多态的成立条件:

    • 有继承
  • 子类重写父类虚函数函数

    • a) 返回值,函数名字,函数参数,必须和父类完全一致(析构函数除外)
    • b) 子类中virtual关键字可写可不写,建议写
    • 类型兼容,父类指针,父类引用 指向 子类对象

4. 抽象基类和纯虚函数(purevirtual function)

  • 在设计时,常常希望基类仅仅作为其派生类的一个接口
  • 这就是说,仅想对基类进行向上类型转换,使用它的接口,而不希望用户实际的创建一个基类的对象
  • 同时创建一个纯虚函数允许接口中放置成员原函数,而不一定要提供一段可能对这个函数毫无意义的代码
  • 做到这点,可以在基类中加入至少一个纯虚函数(pure virtualfunction),使得基类称为抽象类(abstract class).

    • 纯虚函数使用关键字virtual,并在其后面加上=0。如果试图去实例化一个抽象类,编译器则会阻止这种操作。
    • 当继承一个抽象类的时候,必须实现所有的纯虚函数,否则由抽象类派生的类也是一个抽象类。
    • Virtual void fun() =0;告诉编译器在vtable中为函数保留一个位置,但在这个特定位置不放地址
  • 纯虚函数|抽象类

    • 纯虚函数:没有函数体且初始化为0的虚函数,用来定义接口规范
    • 抽象类(Abstract Class)

      • 含有纯虚函数的类,不可以实例化(不可以创建对象)
      • 抽象类也可以包含非纯虚函数、成员变量
      • 如果父类是抽象类,子类没有完全重写纯虚函数,那么这个子类依然是抽象类
案例: 模板方法模式

  • 代码示例:

        //抽象制作饮品
        class AbstractDrinking{
            public:
                //烧水
                virtual void Boil() = 0;
                //冲泡
                virtual void Brew() = 0;
                //倒入杯中
                virtual void PourInCup() = 0;
                //加入辅料
                virtual void PutSomething() = 0;
                //规定流程
                void MakeDrink(){
                    Boil();
                    Brew();
                    PourInCup();
                    PutSomething();
                }
        };
    
       //制作咖啡
        class Coffee : public AbstractDrinking{
            public:
                //烧水
                virtual void Boil(){
                    cout<< "煮农夫山泉!" << endl;
                }
                //冲泡
                virtual void Brew(){
                    cout<< "冲泡咖啡!" << endl;
                }
                //倒入杯中
                virtual void PourInCup(){
                    cout<< "将咖啡倒入杯中!" << endl;
                }
                //加入辅料
                virtual void PutSomething(){
                    cout<< "加入牛奶!" << endl;
                }
        };
    //制作茶水
    class Tea : public AbstractDrinking{
        public:
            //烧水
            virtual void Boil(){
                cout<< "煮自来水!" << endl;
            }
            //冲泡
            virtual void Brew(){
                cout<< "冲泡茶叶!" << endl;
            }
            //倒入杯中
            virtual void PourInCup(){
                cout<< "将茶水倒入杯中!" << endl;
            }
            //加入辅料
            virtual void PutSomething(){
                cout<< "加入食盐!" << endl;
            }
    };

    //业务函数
    void DoBussiness(AbstractDrinking* drink){
        drink->MakeDrink();
        delete drink;
    }
    void test(){
        DoBussiness(new Coffee);
        cout<< "--------------" << endl;
        DoBussiness(new Tea);
    }
```

5. 纯虚函数和多继承

  • 多继承带来了一些争议,但是接口继承可以说一种毫无争议的运用了
  • 绝大数面向对象语言都不支持多继承,但是绝大数面向对象对象语言都支持接口的概念,C++中没有接口的概念,但是可以通过纯虚函数实现接口\

  • 多重继承接口不会带来二义性和复杂性问题。接口类只是一个功能声明,并不是功能实现,子类需要根据功能说明定义功能实现
  • 注意:除了析构函数外,其他声明都是纯虚函数
  • 多继承

    • C++允许一个类可以有多个父类(不建议使用,会增加程序设计复杂度)
    • 多继承体系下的构造函数调用
    • 多继承-虚函数

      • 如果子类继承的多个父类都有虚函数,那么子类对象就会产生对应的多张虚表

6. 虚析构函数

6.1 虚析构函数作用

虚析构函数是为了解决基类的指针指向派生类对象,并用基类的指针删除派生类对象

6.2 虚析构函数

  • 如果存在父类指针指向子类对象的情况,应该将析构函数声明为虚函数(虚析构函数)
  • delete父类指针时,才会调用子类的析构函数,保证析构的完整性

6.3 纯虚析构函数

  • 纯虚析构函数在c++中是合法的,但是在使用的时候有一个额外的限制:必须为纯虚析构函数提供一个函数体。
  • 那么问题是:如果给虚析构函数提供函数体了,那怎么还能称作纯虚析构函数呢?
  • 纯虚析构函数和非纯析构函数之间唯一的不同之处在于纯虚析构函数使得基类是抽象类,不能创建基类的对象


7. 重写 重载 重定义

image.png
image.png

  • 重载,同一作用域的同名函数

      1. 同一个作用域
      1. 参数个数,参数顺序,参数类型不同
      1. 和函数返回值,没有关系
      1. const也可以作为重载条件 //do(const Teacher&t){} do(Teacher& t)
  • 重定义(隐藏)

      1. 有继承
      1. 子类(派生类)重新定义父类(基类)的同名成员(非virtual函数)
  • 重写(覆盖)

      1. 有继承
      1. 子类(派生类)重写父类(基类)的virtual函数
      1. 函数返回值,函数名字,函数参数,必须和基类中的虚函数一致

8. 指向类成员的指针

8.1 指向成员变量的指针

  • 定义格式

  • 赋值/初始化

  • 解引用

image.png
image.png

8.2 指向成员函数的指针

  • 定义格式
  • 赋值/初始化
  • 解引用
  • 代码示例:

8.3 指向静态成员的指针

  • 指向类静态数据成员的指针

    • 指向静态数据成员的指针的定义和使用与普通指针相同,在定义时无须和类相关联,在使用时也无须和具体的对象相关联。
  • 指向类静态成员函数的指针

    • 指向静态成员函数的指针和普通指针相同,在定义时无须和类相关联,在使用时也无须和具体的对象相关联·
  • 代码示例:
    image.png
    image.png

8.4 调用父类的成员函数实现

9. 同名函数

10. 同名成员变量

四、静态成员(static)

  • 静态成员:被static修饰的成员变量\函数

    • 可以通过对象(对象.静态成员)、对象指针(对象指针->静态成员)、类访问(类名::静态成员)
  • 静态成员变量

    • 存储在数据段(全局区,类似于全局变量),整个程序运行过程中只有一份内存
    • 对比全局变量,它可以设定访问权限(public、protected、private),达到局部共享的目的
    • 必须初始化,必须在类外面初始化,初始化时不能带static,如果类的声明和实现分离(在实现.cpp中初始化)
  • 静态成员函数\

    • 内部不能使用this指针(this指针只能用在非静态成员函数内部)
    • 不能是虚函数(虚函数只能是非静态成员函数)
    • 内部不能访问非静态成员变量\函数,只能访问静态成员变量\函数
    • 非静态成员函数内部可以访问静态成员变量\函数
    • 构造函数、析构函数不能是静态
    • 当声明和实现分离时,实现部分不能带static
  • 静态成员经典应用 – 单例模式

五、const成员

  • const成员:被const修饰的成员变量、非静态成员函数
  • const成员变量

    • 必须初始化(类内部初始化),可以在声明的时候直接初始化赋值
    • 非static的const成员变量还可以在初始化列表中初始化
  • const成员函数(非静态)

    • const关键字写在参数列表后面,函数的声明和实现都必须带const
    • 内部不能修改非static成员变量
    • 内部只能调用const成员函数、static成员函数
    • 非const成员函数可以调用const成员函数
    • const成员函数和非const成员函数构成重载
    • 非const对象(指针)优先调用非const成员函数
    • const对象(指针)只能调用const成员函数、static成员函数

六、引用类型成员

  • 引用类型成员变量必须初始化(不考虑static情况)

    • 在声明的时候直接初始化
    • 通过初始化列表初始化

七、VS的内存窗口

专题系列文章

1. 前知识

2. 基于OC语言探索iOS底层原理

3. 基于Swift语言探索iOS底层原理

关于函数枚举可选项结构体闭包属性方法swift多态原理StringArrayDictionary引用计数MetaData等Swift基本语法和相关的底层原理文章有如下几篇:

  • [01-📝Swift5常用核心语法|了解Swift【Swift简介、Swift的版本、Swift编译原理】]
  • [02-📝Swift5常用核心语法|基础语法【Playground、常量与变量、常见数据类型、字面量、元组、流程控制、函数、枚举、可选项、guard语句、区间】]
  • [03-📝Swift5常用核心语法|面向对象【闭包、结构体、类、枚举】]
  • [04-📝Swift5常用核心语法|面向对象【属性、inout、类型属性、单例模式、方法、下标、继承、初始化】]
  • [05-📝Swift5常用核心语法|高级语法【可选链、协议、错误处理、泛型、String与Array、高级运算符、扩展、访问控制、内存管理、字面量、模式匹配】]
  • [06-📝Swift5常用核心语法|编程范式与Swift源码【从OC到Swift、函数式编程、面向协议编程、响应式编程、Swift源码分析】]

4. C++核心语法

其它底层原理专题

1. 底层原理相关专题

2. iOS相关专题

3. webApp相关专题

4. 跨平台开发方案相关专题

5. 阶段性总结:Native、WebApp、跨平台开发三种方案性能比较

6. Android、HarmonyOS页面渲染专题

7. 小程序页面渲染专题

相关文章
|
7天前
|
存储 编译器 Linux
【c++】类和对象(上)(类的定义格式、访问限定符、类域、类的实例化、对象的内存大小、this指针)
本文介绍了C++中的类和对象,包括类的概念、定义格式、访问限定符、类域、对象的创建及内存大小、以及this指针。通过示例代码详细解释了类的定义、成员函数和成员变量的作用,以及如何使用访问限定符控制成员的访问权限。此外,还讨论了对象的内存分配规则和this指针的使用场景,帮助读者深入理解面向对象编程的核心概念。
26 4
|
1月前
|
C++
C++番外篇——对于继承中子类与父类对象同时定义其析构顺序的探究
C++番外篇——对于继承中子类与父类对象同时定义其析构顺序的探究
53 1
|
1月前
|
C++
C++番外篇——虚拟继承解决数据冗余和二义性的原理
C++番外篇——虚拟继承解决数据冗余和二义性的原理
39 1
|
1月前
|
安全 编译器 程序员
C++的忠实粉丝-继承的热情(1)
C++的忠实粉丝-继承的热情(1)
18 0
|
3月前
|
存储 编译器 C语言
【C语言篇】数据在内存中的存储(超详细)
浮点数就采⽤下⾯的规则表⽰,即指数E的真实值加上127(或1023),再将有效数字M去掉整数部分的1。
366 0
|
21天前
|
存储 C语言
数据在内存中的存储方式
本文介绍了计算机中整数和浮点数的存储方式,包括整数的原码、反码、补码,以及浮点数的IEEE754标准存储格式。同时,探讨了大小端字节序的概念及其判断方法,通过实例代码展示了这些概念的实际应用。
44 1
|
26天前
|
存储
共用体在内存中如何存储数据
共用体(Union)在内存中为所有成员分配同一段内存空间,大小等于最大成员所需的空间。这意味着所有成员共享同一块内存,但同一时间只能存储其中一个成员的数据,无法同时保存多个成员的值。
|
30天前
|
存储 弹性计算 算法
前端大模型应用笔记(四):如何在资源受限例如1核和1G内存的端侧或ECS上运行一个合适的向量存储库及如何优化
本文探讨了在资源受限的嵌入式设备(如1核处理器和1GB内存)上实现高效向量存储和检索的方法,旨在支持端侧大模型应用。文章分析了Annoy、HNSWLib、NMSLib、FLANN、VP-Trees和Lshbox等向量存储库的特点与适用场景,推荐Annoy作为多数情况下的首选方案,并提出了数据预处理、索引优化、查询优化等策略以提升性能。通过这些方法,即使在资源受限的环境中也能实现高效的向量检索。
|
1月前
|
存储 编译器
数据在内存中的存储
数据在内存中的存储
41 4
|
1月前
|
存储 Java
JVM知识体系学习四:排序规范(happens-before原则)、对象创建过程、对象的内存中存储布局、对象的大小、对象头内容、对象如何定位、对象如何分配
这篇文章详细地介绍了Java对象的创建过程、内存布局、对象头的MarkWord、对象的定位方式以及对象的分配策略,并深入探讨了happens-before原则以确保多线程环境下的正确同步。
53 0
JVM知识体系学习四:排序规范(happens-before原则)、对象创建过程、对象的内存中存储布局、对象的大小、对象头内容、对象如何定位、对象如何分配