c++学习笔记(五、继承和多态)

今天学习到了类的其他特性,继承和多态,面向对象的三大特性,今天学完了之后,就可以学习c++高级的STL了。

5.1 类和类的关系

类B拥有类A的成员变量,B has A, //类B 依赖于 类A
例子:

class A
{
public:
private:
    int a;
};

//先有类A ,才能构造类B
class B
{
public:

    A a;
private:
};

类C的成员方法需要类A的形参, C use A , //类C 依赖于 类A

class C
{
public:
    void func(A &a){
        
    }
private:

};

类D如果是继承类A 类D is A //类D继承于A 耦合度很高

class D : public A
{
public:
    void printf_D(){
        printf("a = %d\n", a);
    }
private:
};

最后这个就是我们要说的继承。

5.2 类的继承

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

派生与继承,是同一种意义的两个称谓。 is A的关系。
例子:

class D : public A
{
public:
 //初始化参数列表,调用A的有参构造函数
//如果没有显示调用,会默认调用父类的无参构造函数
    D() : A(10)   
    {
        printf("D 构造\n");
    }

    void printf_D(){
        printf("a = %d\n", a);
    }
private:
};

5.3 继承类型

当一个类派生自基类,该基类可以被继承为 public、protected 或 private 几种类型。继承类型是通过上面讲解的控制访问修饰符是一样的,但含义不一样。

我们几乎不使用 protected 或 private 继承,通常使用 public 继承。当使用不同类型的继承时,遵循以下几个规则:
在这里插入图片描述
类的访问控制权限会根据继承类型的不同,而不同,上面表格就说明了这一点。
私有继承的时候,子类中是可以访问public和protected这些变量的,只是把public和protected控制权限变成private。这个子类的子类就不能访问了,因为子类的权限是private。

5.4 子类和父类之间的赋值

5.4.1 子类对象可以当做父类对象使用

这个应该不用写代码解释了吧。
子类是继承父类的,父类的方法子类都有,除了private的,所以可以直接把子类对象当父类对象使用。

5.4.2 子类对象可以直接赋值给父类对象

class A
{
public:
    A(int a) 
    {
        this->a = a;
    }

    A()
    {
        printf("A 无参构造函数\n");
    }

    int a;
private:
};

class D : public A
{
public:
    D() : A()
    {
        printf("D 构造\n");
    }

    void printf_D(){
        printf("a = %d\n", a);
    }
private:
};

int main(int argc, char **argv)
{
     cout << "hello c++ " << my_spaceA::my_spaceB::haha << endl;

     D d;   //子类对象
     A a;	//父类对象
     a = d;	//子类对象赋值给父类对象
	 A e(d);  //这样也直接
     return 0;
 }

这个为什么可以赋值呢,还是因为继承的原因:
子类开辟的内存空间一定包含父类的成员变量,所以子类的对象里面一定有父类的对象,直接赋值的话,是直接把子类对象里的父类对象的那部分拷贝都父类对象中。
另外子类对象可以直接初始化父类对象

5.4.3 父类指针可以直接指向子类对象、

int main(int argc, char **argv)
 {
     cout << "hello c++ " << my_spaceA::my_spaceB::haha << endl;

     D d;
     A a;
     //a = d;

     D *pd = NULL;
     A *pa = NULL;

     pa = &d;

     return 0;
 }

父类的指针指向子类的对象时,如果父类的指针调用了父类的方法,这时,子类也是有父类的方法的,所以可以调用。如果反过来子类调用自己子类方法的时候,父类就没有,所以反过来就有问题。
另外父类引用可以直接引用子类对象。

5.5 父类变量和子类变量重名

父类的变量有可能和子类变量重名,重名的时候,怎么调用父类的变量。

void printf_D(){
		//类名::a  这样调用父类变量
		//如果父类中的a的访问权限是private的话,就只能调用函数了。
        printf("a = %d %d\n", A::a, a);   
 }

变量名相同会存在覆盖,但是要强制调用父类的变量也是可以的,用类名::变量。
成员方法也是一样。

如果类中static的静态变量,有一个类继承了这个类,那这个static静态变量就变成了一个家族共有的。

5.6 多继承

c++是唯一存在的多继承,Java应该没有多继承了,因为有点问题。问题等下再说,还是先说多继承。

class B : public A
{
public:
private:
    
};

class C : public A
{
public:
    void func(A &a){

    }
private:

};

//这个是多继承,类D即继承于类B,也继承于C,所以类D既有类B的成员也有类C的成员
class D : public B, public C
{
public:
    D() : B(),C()
    {
        printf("D 构造\n");
        //this->a = 20;
    }

    void printf_D(){
       //printf("a = %d %d\n", A::a, a);
    }
private:
    
};

5.7 虚继承

但是由于上面的多继承,会出现一些问题,还是上面的代码,但是加了类A的定义:

class A
{
public:
    A(int a) 
    {
       // this->a = a;
    }

    A()
    {
        printf("A 无参构造函数\n");
    }

    int a;
private:
};

类A中定义一个变量a,由于类B继承了类A,所以类B中也有变量a,类C也继承了类A,类C中也有一个变量a,这时候类D继承了类B和类C,所以类D中有两个变量a,这样就会导致二义性。
在这里插入图片描述
这里二义性的图。哪这个问题怎么解决呢?
c++中又引入了一个新的关键字virtual,虚继承,也就是在类B继承类A的时候加了这个关键字,类C继承类A的时候也加这个关键字,到最后类D的之后就只会出现一个类A的变量,这样就不会引起二义性。

class B : virtual public A
{
public:
private:
    
};


class C : virtual public A
{
public:
    void func(A &a){

    }
private:

};

其他的代码都不变,最后来一个虚继承之后的内存图:
在这里插入图片描述
会生成两个指针指向一块真正的类A的内存,这样就不会出现二义性了。

5.8 多态

5.8.1 多态定义

多态按字面的意思就是多种形态。当类之间存在层次结构,并且类之间是通过继承关联时,就会用到多态。

C++ 多态意味着调用成员函数时,会根据调用函数的对象的类型来执行不同的函数。
看下面的例子:

class A
{
public:
    A(int a) 
    {
       // this->a = a;
    }

    A()
    {
        //printf("A 无参构造函数\n");
    }

    void printf_aa() {
        printf("class A printf\n");
    }

    int a;
private:
};

class B : virtual public A
{
public:
    B()
    {

    }

    void printf_aa() {
        printf("class B printf\n");
    }
private:
};

void main_printf(A *a)
{
    a->printf_aa();
}

int main(int argc, char **argv)
{
      A a;
      B b;

      main_printf(&a);
      main_printf(&b);
      return 0;
  }

这种写法的话,执行的结构
在这里插入图片描述
导致错误输出的原因是,调用函数 printf_aa() 被编译器设置为基类中的版本,这就是所谓的静态多态,或静态链接 - 函数调用在程序执行前就准备好了。有时候这也被称为早绑定,因为 printf_aa() 函数在程序编译期间就已经设置好了。

但现在,让我们对程序稍作修改,在 A类中,printf_aa() 的声明前放置关键字 virtual,如下所示:

class A
{
public:
    A(int a) 
    {
       // this->a = a;
    }

    A()
    {
        //printf("A 无参构造函数\n");
    }

    virtual void printf_aa() {
        printf("class A printf\n");
    }

    int a;
private:
};

执行的结构:
在这里插入图片描述
此时,编译器看的是指针的内容,而不是它的类型。因此,由于 A 和B 类的对象的地址存储在 *a中,所以会调用各自的 printf_aa() 函数。

正如您所看到的,每个子类都有一个函数 printf_aa() 的独立实现。这就是多态的一般使用方式。有了多态,您可以有多个不同的类,都带有同一个名称但具有不同实现的函数,函数的参数甚至可以是相同的。

5.8.2 虚函数

虚函数 是在基类中使用关键字 virtual 声明的函数。在派生类中重新定义基类中定义的虚函数时,会告诉编译器不要静态链接到该函数。

我们想要的是在程序中任意点可以根据所调用的对象类型来选择调用的函数,这种操作被称为动态链接,或后期绑定。

5.8.3 纯虚函数

您可能想要在基类中定义虚函数,以便在派生类中重新定义该函数更好地适用于对象,但是您在基类中又不能对虚函数给出有意义的实现,这个时候就会用到纯虚函数。
例子:

class A
{
public:
    A(int a) 
    {
       // this->a = a;
    }

    A()
    {
        //printf("A 无参构造函数\n");
    }
    int a;
    virtual void printf_aa() = 0;

private:
};

= 0 告诉编译器,函数没有主体,上面的虚函数是纯虚函数。
参考菜鸟教程:https://www.runoob.com/cplusplus/cpp-polymorphism.html

5.8.4 虚析构函数

为什么有虚析构函数,还是因为有多态的出现,还是用例子说明:

class A
{
public:
    A(int a) 
    {
       // this->a = a;
    }

    A()
    {
        printf("A 无参构造函数\n");
    }

    ~A()    //A的虚构函数
    {
        printf("A的虚构函数\n");
    }
    int a;
    virtual void printf_aa() {

    }

private:
};


class B : virtual public A
{
public:
    B()
    {
        printf("B 无参构造函数\n");
    }

    ~B()    //B的虚构函数
    {
        printf("B的虚构函数\n");
    }

    void printf_aa() {
        printf("class B printf\n");
    }
private:
};

int main(int argc, char **argv)
{

   //A a;
   A *a = new B();
   delete a;

   return 0;
}

这个是用多态的思想,父类的指针指向子类对象。
执行的结果:
在这里插入图片描述
出现问题,B的构造函数调用了,但是没有调用析构函数,所以会出现问题,这时候我们也需要把析构函数多态化,就是在析构函数前面加virtual,这样就是我们说的虚析构函数。
修改完之后的结果:

class A
{
public:
    A(int a) 
    {
       // this->a = a;
    }

    A()
    {
        printf("A 无参构造函数\n");
    }

    virtual ~A()    //A的虚构函数
    {
        printf("A的虚构函数\n");
    }
    int a;
    virtual void printf_aa() {

    }

private:
};


class B : virtual public A
{
public:
    B()
    {
        printf("B 无参构造函数\n");
    }

    virtual ~B()    //B的虚构函数
    {
        printf("B的虚构函数\n");
    }

    void printf_aa() {
        printf("class B printf\n");
    }
private:
};

执行结果:
在这里插入图片描述
是不是很和谐。

5.9 重载 重写 重定义

1、重载 一定是同一个作用域下,函数才可以重载。
2、重定义 是发生在两个不同类中,一个是父类,一个是子类
普通函数重定义:如果父类的普通函数,被子类重写,说是重定义
虚函数重写:如果父类的虚函数,被子类重写,就是虚函数重写,这个函数会发生多态。

5.10 多态原理

5.10.1 vptr指针和虚函数表

  1. 当类中声明虚函数时,编译器会在类中生成一个函数表。
  2. 虚函数表是一个存储类成员函数指针的数据结构。
  3. 虚函数表是由编译器自动生成与维护。
  4. virtual成员函数会被编译器放入虚函数表中。
  5. 存在虚函数时,每个对象中都有一个指向虚函数表的指针(vptr指针)。

在这里插入图片描述
上面图片就是一个例子。左边是两个类,右边是两个虚函数表。

编译器确定func函数是否为虚函数:

  1. 如果不是虚函数,是普通函数的话,编译器不会去查虚函数的表,根据指针的类型调用自己对象的函数。编译器编译的时候会选择静态联编。
  2. func是虚函数的话 ,编译器会根据对象p的vptr,所指向的虚函数表中查找func函数,并调用。查找和调用都是在运行时完成,实现所谓的动态联编。

说明:

  1. 通过虚函数表指针vptr调用重写函数是在程序运行时进行的,因此需要通过寻址操作才能真正确定应该调用的函数。而普通成员函数是在编译时就确定了调用函数,在效率上,虚函数的效率要低很多。
  2. 出于效率考虑,没有必要将所有成员函数都声明为虚函数。
  3. c++编译器,执行函数时,不需要区分是子类对象还是父类对象,而是直接通过p的vptr指针所指向的对象函数执行即可。

用sizeof来验证vptr是否存在。

5.11 抽象类

C++ 接口是使用抽象类来实现的,抽象类与数据抽象互不混淆,数据抽象是一个把实现细节与相关的数据分离开的概念。

如果类中至少有一个函数被声明为纯虚函数,则这个类就是抽象类。纯虚函数是通过在声明中使用 “= 0” 来指定的。

设计抽象类(通常称为 ABC)的目的,是为了给其他类提供一个可以继承的适当的基类。抽象类不能被用于实例化对象,它只能作为接口使用。如果试图实例化一个抽象类的对象,会导致编译错误。

因此,如果一个 ABC 的子类需要被实例化,则必须实现每个虚函数,这也意味着 C++ 支持使用 ABC 声明接口。如果没有在派生类中重写纯虚函数,就尝试实例化该类的对象,会导致编译错误。

可用于实例化对象的类被称为具体类。

c++中还是需要又一个类继承这个抽象类,Java中就只要实现了一个接口就可以了。

多抽象类遇到多继承的时候,申请了那个抽象类的对象,只能调用自己类方法。

发布了32 篇原创文章 · 获赞 26 · 访问量 2万+

猜你喜欢

转载自blog.csdn.net/C1033177205/article/details/104079444
今日推荐