C++三大特性

青春壹個敷衍的年華 提交于 2020-03-10 23:58:03

封装继承和多态

封装:隐藏实现细节,使得代码模块化,封装就是把过程和数据包装,将客观事物封装成抽象的类,并且类可以把自己的数据和方法只让可信的类或者对象操纵,对其他信息隐藏。

类继承是指C++提供来扩展和修改类的方法,类继承就是从已有的类中派生出新的类,派生类继承了基类的特性,同时可以添加自己的特性,继承又分为

  1. 单一继承
  2. 多重继承
  3. 菱形继承

多态是在具有继承关系的类对象中去调用某一虚函数时(使用基类的指针/引用去调用同一函数),产生了不同的行为,构成多态的条件有两个(说白了就是通过指针/引用在不同时候调用同一函数可能调用的是不同的版本,多态是指接口的多种不同实现方式

  1. 调用函数的对象必须是指针或者引用
  2. 被调用的函数必须是虚函数,且完成了虚函数的重写(不覆盖会调用派生类的函数吗?)

动态(类型)绑定/静态(类型)绑定

  1. 静态类型:对象在声明时的类型,其在编译时决定
  2. 动态类型:变量所指向内存中该对象的类型(通常指指针/引用所绑定对象的类型),在运行期决定
  3. 静态类型决定了某个函数能不能被调用,而动态类型则在动态绑定发生时决定调用该函数的哪个版本

如果不使用指针和引用,则静态类型和动态类型一定相同

  1. 静态绑定:也叫静态联编,绑定的是对象的静态类型,某特性(比如函数)依赖于对象的静态类型,发生在编译器
  2. 动态绑定:也叫动态联编,绑定的是对象的动态类型,某特性(比如函数)依赖于对象的动态类型,发生在运行期

当函数被调用时,编译器将选择应该执行的代码,此时即编译器绑定了函数名。即,当函数被调用时,编译器就会将该函数的名称和它的定义绑定在一起(将函数调用和函数定义绑定起来)。

静态绑定发生在编译时,将函数实现和函数函数调用关联起来,因此静态绑定也叫早绑定,在编译阶段就必须了解所有的函数模块执行所需要检测的信息,它对函数的选择是基于指向对象的指针/引用的类型(即只能使用静态类型的信息)

动态绑定发生在运行时,产生动态绑定是因为编译器在编译时不确定使用基类还是子类的函数定义,所以需要在运行时来确定,产生动态绑定使用的不是指针/引用的类型,而是其绑定的对象的类型C++默认不触发动态绑定,产生动态绑定的条件如下:

  1. 必须通过基类指针/引用来进行函数调用
  2. 只有指定为虚函数的成员函数才可以进行动态绑定

下面是关于C++中一个指针/引用调用一个函数的过程,假设我们要调用的是p->mem()

  • 首先确定p的静态类型
  • 在p的静态类型中查找该函数mem,如果找不到则在直接基类中找,直到达到继承链的顶端,如果还是找不到则报错
  • 一旦找到了mem,则进行常规的类型检查,以确定当前找到的mem调用是否合法(形参和实参是否匹配等)
    • 如果不合法则报错
    • 如果合法,则还是需要判断调用的该函数是不是虚函数,从而可能产生不同的代码
      • 如果mem是虚函数且我们是通过指针/引用来进行的调用,则进行动态绑定,即编译器产生的代码将在运行时确定到底运行该函数的哪个版本,依据变量的动态类型
      • 如果mem不是虚函数或者我们是通过对象直接调用,则进行静态绑定,则编译器产生一个常规的函数调用

多态和函数重载

重载:有两个或多个函数名相同的函数,但是函数的形参列表不同。在调用相同函数名的函数时,编译器根据形参列表确定到底调用哪个函数。使用的是静态绑定

多态:采用动态绑定来实现,通过基类的指针/引用去调用某一虚函数时,可能执行不同的版本,多态中调用的这个函数在基类和派生类中是同名同参数的函数,是通过对象的动态类型来决定实际调用哪个函数。

多重继承和虚继承

多重继承
  1. 一个派生类有多个直接基类,多重继承的派生类继承了所有父类的属性
  2. 多重继承的构造顺序和派生列表中基类顺序相同
  3. 多重继承的析构顺序和构造顺序相反
  4. 多重继承的派生类可以向各个基类进行隐式转换
  5. 多重继承的派生类在执行某个函数时,如果不同基类中都定义了相同名字的函数,但是派生类中没有覆盖/隐藏,则调用将出现二义性错误
虚继承

虚继承是解决C++多重继承问题的一种手段,从不同途径继承来的同一基类(即可能直接/间接的继承多个相同的基类),会在子类中存在多份拷贝。这可能会出现两个问题:

  1. 浪费存储空间
  2. 导致二义性问题

虚继承的实现原理

通过虚基类指针来实现,该指针指向了一个虚基类表,虚表中记录了虚基类(虚基类首地址)与本类的偏移地址(即在派生类对象内存中,虚基类成员相对于该虚表指针的偏移量);同时虚基类在字类中也会存在拷贝,但是仅仅拷贝一份而已,这样当需要访问虚基类成员时,通过访问虚基类指针得到虚基类成员的偏移即可找到虚基类成员。这样通过在派生类中保存多个(几个虚基类就是几个虚指针)虚指针,然后仅保存一份(这里指继承都是同一个基类)虚基类成员,就可以访问到该虚基类的成员了

虚继承的构造

需要注意的是如果存在虚继承,则虚基类必须由最底层的派生类在构造函数中显示的通过虚基类的构造函数来构造,不能和普通的继承一样,等着虚继承的直接派生类去构造。如果不显示的写出则编译器会默认调用虚基类的默认构造函数,如果虚基类不存在默认构造函数,则报错

C++内存模型与多态原理

类布局
  1. 普通类

    • 这里只讨论普通的类布局,C++中按照成员遍历的声明顺序在内存中进行布局
    • 类中声明的函数定义被存放在独立于对象的存储空间中
    • 当类对象在调用成员函数时,通过传入该对象的this指针,来区分不同的对象
  2. 单继承

    • 如果基类中没有虚函数,则派生类对象中包含基类子对象和派生类子对象
    • 如果基类含有虚函数,则每个基类对象中含有一个虚表指针,该指针指向一个虚函数表,虚函数表中保存有各个虚函数的函数指针。派生类对象同样还是含有基类子对象和派生类子对象,后面详述
    • 虚表指针位于派生类对象内存的最前面
    • 注意基类子对象和派生类子对象不一定是连续存储的,即可能派生类对象内存中存放的只是指向基类子对象的指针
  3. 多继承

    • 派生类对象中含有多个基类子对象,基类子对象在内存中的排列顺序按照派生列表中的顺序排列
    • 如果各个基类中都含有虚函数,则在派生类中的各个基类子对象中都分别含有各自的虚表指针
  4. 虚继承

    • 单虚继承,则派生类对象中含有一个虚基类指针,派生类成员和虚基类成员,其中虚基类指针指向虚基类表,表中存放虚基类成员相对该虚基类指针的偏移量
    • 多虚继承(菱形继承),则派生类对象中则分别含有各个继承的基类的子对象,以及虚基类的成员,各个子对象按照派生列表中的顺序排列,各个子对象中含有虚基类指针和该基类对象的成员,其中虚基类指针指向虚基类表,表中存放虚基类成员相对该虚基类指针的偏移量
    • 含有虚函数的虚继承,派生类对象首先含有一个虚表指针,指向的虚函数表中存放该派生类自定义的虚函数,同时包含虚基类指针和派生类对象,最后才是虚基类的成员(包括虚基类的虚函数指针和成员)

虚函数被virtual关键字修饰的成员函数,该函数可以在一个或多个派生类中被重新定义,从而覆盖基类中的该函数,多态就是通过虚函数来实现的,通过使用基类的指针/引用绑定到派生类,从而调用派生类中被覆盖的函数

多态原理

C++实现多态的原理是通过虚函数机制,即虚函数表和虚表指针,即每个类用了一个虚函数表,每个对象中含有一个虚表指针,指向该类的虚函数,然后多态发生时,通过动态绑定机制使得编译器知道在运行时调用哪个虚函数。

  1. 单继承

    • 派生类对象中含有基类子对象,同时在内存最开始的位置包含一个虚表指针
    • 如果派生类中的虚函数覆盖了基类虚函数,则直接在虚表指针指向的虚函数表中将基类虚函数替换即可
    • 如果派生类中声明了新的虚函数,则直接在虚表指针指向的虚函数中添加新的虚函数即可
  2. 多继承(THUNK原理)

    • 派生类对象中含有多个基类子对象,每个基类子对象中分别含有一个虚表指针,并指向各自的基类的虚函数表,即此时派生类中含有了多个虚表指针
    • 如果派生类的某个虚函数和覆盖了所有基类中的某个虚函数,则所有虚表中的该虚函数都会被替换为派生类虚函数
    • 如果派生类声明新的虚函数,则该虚函数会被加到第一个基类(派生顺序的第一个)的虚函数表中
      • 如果第一个基类没有虚函数,则会将第一个有虚函数的基类放到内存最开始,同时将新虚函数添加到此虚函数表中,
      • 如果所有基类都没有虚函数,但是派生类新定义了虚函数,则派生类还是会在内存起始处创建虚表指针,指向自己的虚函数表
      • https://www.cnblogs.com/vaecn/p/5362645.html
  3. 虚继承

    • 当虚基类中没有虚函数时,见上虚继承内容
    • 当虚基类中有虚函数时,对于单继承,则派生类对象中将含有虚表指针,虚基类指针,派生类成员,和基类成员(基类成员中包含基类虚表指针和基类成员),第一个虚表指针所指向的虚函数表中存放的是派生类所声明的新的虚函数,而第二个基类中所含有的虚表指针指向的虚函数表中存放的是基类的虚函数或者被覆盖的派生类虚函数
    • https://blog.csdn.net/xiejingfa/article/details/48028491
  4. 虚函数表的原理:

    • 虚函数表类似于一个函数指针的数组,其中存放指向虚函数的指针
    • 虚函数表的最后多加了一个节点,用于作为虚函数表的结束标志(就是0)
  5. 总结

    • 多态就是通过虚函数表和虚表指针来实现的,其中虚函数表每个类所有对象公用一份,而虚表指针则是每个对象都有。
    • 在派生类中,一定要注意,如果派生类中有虚函数(无论是继承来的,还是自定义的),那么派生类中其虚表指针一定在对象内存的第一个地址处.
    • 多态发生时,通过虚表指针去调用相应的虚函数,将虚函数的调用变成通过虚函数指针来访问虚函数表,从而从虚函数表中访问调用虚函数
虚函数表和虚函数的地址怎么获取?

https://blog.csdn.net/qianghaohao/article/details/51356718

派生类和基类

一个派生类对象包含多个组成成分:一个含有派生类自己定义的非静态成员的子对象,以及一个与该派生类继承的基类对应的子对象,如果有多个基类,则有多个子对象(一个对象中,继承自基类的部分和派生类自定义部分不一定是连续存储的)

也是因为派生类中还有基类子对象,所以派生类可以进行向基类的类型转换(这种类型转换只针对引用/指针有效,如果是两个对象之间的转换,最后还是通过拷贝控制成员,即还是引用/指针来完成的,这样会忽略掉派生类对象中的自定义部分,从而导致派生类对象被切掉了)

基类向派生类的转换不能是隐式的(编译器不允许),但是可以通过强制类型转换来完成

纯虚函数与抽象基类

纯虚函数也是一个虚函数,但是其无需定义,以=0结尾即为纯虚函数

含有纯虚函数的类即为抽象基类

纯虚函数可以定义,但是只能定义在类外,抽象基类不能创建对象

final和override关键字

final关键字的作用有两个

  1. 用于类后面,则该类不可被继承
  2. 用于虚函数后面,则该虚函数不可被覆盖

阻止继承还有一个办法,就是将基类的构造函数声明为私有的

override关键字用于某个虚函数后面,则指定该函数覆盖其基类中的相应虚函数。如果override放在某个函数后面,而该函数不是继承自基类的虚函数,则报错

继承中的类作用域

在继承体系中,派生类的作用域嵌套在基类的作用域中,所以编译在解析时的步骤如下:

  1. 根据该对象的静态类型,在其作用域中寻找该函数/变量的声明,如果找不到则到基类作用域中寻找,直到继承链的顶端

  2. 如果派生类中定义了和基类中重名的变量/函数(非虚函数),则派生类中的该函数/变量会隐藏基类中对应的函数/变量,如果要使用基类中的该重名变量/函数则需要指出作用域

  3. 注意隐藏成员函数时,只要函数重名就可以隐藏了,即便形参列表可能不同。

  4. 如果派生类中某个继承来的虚函数被重载了,而继承来的虚函数覆盖基类虚函数,则该虚函数将被重载函数覆盖

    #include<iostream>
    using namespace std;
    class Base1
    {
    public:
        virtual void baseFunc1()
        {
            cout << "Base::baseFunc1" << endl;
        }
        int base1_;
    };
    class D1 : public Base1
    {
    public:
        /*virtual void baseFunc1() 该函数在被注释后,则该虚构函数被下面重载的函数隐藏,此时用D1的对象将无法调用该函数
        {
            cout << "D1::baseFunc1" << endl;
        }*/
        void baseFunc1(int a)
        {
            cout << a << endl;
        }
        virtual void depriveFunc1()
        {
            cout << "D1::depriveFunc1()" << endl;
        }
        int D1_;
    };
    int main()
    {
        Base1 *b = new D1();
        D1 d;
        d.baseFunc1();
        system("pause");
        return 0;
    }
    

派生访问限定符

派生访问限定符只影响派生类对象对派生类中基类成员的访问

即分清楚派生类对象和派生类内部的区别

派生类对象是指用派生类定义的对象,而派生类内部是指在派生内内部对于基类成员的访问控制

  1. 一个派生类对象内部能不能访问基类成员只和基类成员的访问控制限定相关,和该派生类是公有继承还是私有继承无关
  2. 一个派生类继承了基类的成员,那么基类成员在派生类中是什么样的访问权限呢?即派生类的对象能否访问该基类的成员呢?这个和派生的访问控制相关。

友元与继承

友元不具有继承性,即Base是Test的友元,而D1是Base的派生类,但是D1不是Test的友元,但是Test可以访问D1中的base部分

class Test;
class Base
{
  public:
    friend class Base;
    int b1;
};
class D1:public Base
{
  public:
	int D1;	
};
class Test
{
  public:
    void func(Base b)
    {
        cout<<b.b1<<endl;//正确
    }
    void func1(D1 d)
    {
        cout<<d.D1<<endl;//错误,友元关系没有传递性,即D1的基类是Test的友元但是D1不是
    }
    void func2(D1 d)
    {
        cout<<d.b1<<endl;//正确,但是b1是Base的部分,而base是Test的友元,所以正确
    }
};

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!