C++ 类和对象 继承 静态与非静态同名成员的访问 多继承语法 菱形继承 虚继承及其底层实现

文章介绍了在C++中处理子类与父类同名成员的方法,包括直接访问子类成员和通过作用域访问父类成员。对于静态成员,无论是变量还是函数,处理方式与非静态成员类似,可以通过对象或类名访问,并同样需要作用域解析。在多继承情况下,若出现同名成员,也需要使用作用域来区分。文章还探讨了菱形继承问题,指出它可能导致数据冗余,提出虚继承作为解决方案,以确保只有一份数据并避免资源浪费。
摘要由CSDN通过智能技术生成
  1. 继承同名成员处理方式

  1. 问题

当子类与父类出现同名的成员,如何通过子类对象,访问到子类或父类中同名的数据呢

  1. 解决方法

  • 访问子类同名成员 直接访问即可;

  • 访问父类同名成员 需要加作用域。

  1. 场景描述

创建父类和子类,两个类中包含同名的成员。

class Base
{
public:
    Base()
    {
        mA = 55;
    }
    int mA;
};

class Son :public Base
{
public:
    Son()
    {
        mA = 200;
    }
    int mA;
};
  1. 同名成员的访问方式

void test1()
{
    Son s;
    cout << "Son\tmA:" << s.mA << endl;//直接访问 子类属性
    cout << "Base\tmA:" << s.Base::mA << endl;//加作用域 父类属性
}

int main()
{
    test1();

    system("pause");
    return 0;
}

直接访问:创建子类对象后,如果是直接访问,此时访问的是子类的成员属性

根据上一篇的学习笔记,我们知道,在创建子类对象时,就已经创建父类对象了,并且子类也继承了父类的属性mA。那么,如何访问父类的同名成员呢?

加作用域:如果想要访问父类中的同名成员,那么只需要在子类对象的.之后加上作用域Base::”,即可访问父类的成员属性。

  1. 同名函数的访问方式

在子类和父类都加上同名函数,再次讨论同名函数的访问方式。

class Base
{
public:
    Base()
    {
        mA = 55;
    }
    void func()//同名函数
    {
        cout << "Base-func调用" << endl;
    }
    int mA;//同名成员
};

class Son :public Base
{
public:
    Son()
    {
        mA = 200;
    }
    void func()//同名函数
    {
        cout << "Son-func调用" << endl;
    }
    int mA;//同名成员
};

直接访问:结论和同名成员直接访问的方式一样,创建子类对象后,如果是直接访问,此时访问的是子类的函数属性。

那么是不是加作用域之后,也可以访问父类中同名函数了呢?

加上作用域之后,就可以通过子类对象访问与父类同名的函数了。

父类当中同名重载函数,如果此时在父类中,多加一个重载函数,那么又该如何访问?结合之前的学习,应该是需要加上作用域,并且传入相应的参数。

class Base
{
public:
    Base()
    {
        mA = 55;
    }
    void func()
    {
        cout << "Base-func调用" << endl;
    }
    void func(int a)//重载
    {
        cout << "Base-重载func调用" << endl;
    }
    int mA;
};

当父类中有重载函数也同名时,根据重载函数的形参类型和个数,在子类对象中加上作用域传入相应的参数就可以访问父类中同名重载函数了。

也就是说,如果子类中出现和父类同名的成员函数,子类的同名成员函数会隐藏掉父类中所有同名成员函数(包括重载函数)。如果想访问父类中被隐藏的同名成员函数,需要加上作用域。

放在这个例子中来说,Son中的同名成员函数func会隐藏掉Base中所有同名成员函数func,包括其重载版本。如果想访问Base中的func需要加上作用域;如果想访问Base中func的重载版本,不仅要加上作用域,还要传入参数。

  1. 总结

  • 子类对象可以直接访问到子类中同名成员;

  • 子类对象加作用域可以访问到父类同名成员,访问父类同名成员,需要加作用域;

  • 当子类与父类拥有同名的成员函数,子类会隐藏父类中同名成员函数,加作用域可以访问到父类中同名函数;

  • 访问父类同名成员函数的重载版本,需要加作用域和传入参数。

  1. 继承同名静态成员处理方式

  1. 问题

继承中同名的静态成员在子类对象上如何进行访问?

  1. 解决方法

静态成员和非静态成员出现同名,处理方式一致:

  • 访问子类同名成员 直接访问即可

  • 访问父类同名成员 需要加作用域

  1. 场景描述

创建父类和子类,两个类中包含静态同名成员变量静态同名成员函数

  1. 静态同名成员变量

class Base
{
public:
    static int mA;//静态成员变量类内声明
};
int Base::mA = 55;//类外初始化

class Son :public Base
{
public:
    static int mA;//静态成员变量类内声明
};
int Son::mA = 45;//类外初始化
  1. 通过对象访问
//静态同名成员的访问方式
void test1()
{
    Son s;
    cout << "Son\tmA:" << s.mA << endl;//直接访问 子类同名成员属性
    cout << "Base\tmA:" << s.Base::mA << endl;//加作用域 父类同名成员属性
}

int main()
{
    test1();

    system("pause");
    return 0;
}

这里主要是回顾了静态变量的基本用法,再次巩固了访问同名成员变量的方式。知识点和上面的一样,不赘诉,只是多了个静态的定义。

  1. 通过类名访问
//静态同名成员的访问方式
void test1()
{
    //1. 通过对象访问
    cout << "通过对象访问" << endl;
    Son s;
    cout << "Son\tmA:" << s.mA << endl;//直接访问 子类同名成员属性
    cout << "Base\tmA:" << s.Base::mA << endl << endl;//加作用域 父类同名成员属性

    //2. 通过类名访问
    cout << "通过类名访问" << endl;
    cout << "Son\tmA:" << Son::mA << endl;//子类类名访问
    cout << "Base\tmA:" << Son::Base::mA << endl << endl;//通过子类访问父类的同名成员
    cout << "Base\tmA:" << Base::mA << endl << endl;//注意这是通过父类直接访问
}

“Son::Base::mA”这种方式是通过子类类名来访问父类作用域下的同名成员属性。也就是说,第一个双冒号表示通过类名的方式访问第二个双冒号表示访问父类作用域下的同名成员属性

  1. 静态同名成员函数

class Base
{
public:
    static int mA;//静态成员变量类内声明
    static void func()//静态同名成员函数
    {
        cout << "Base - static void func()" << endl;
    }
};
int Base::mA = 55;//类外初始化

class Son :public Base
{
public:
    static int mA;//静态成员变量类内声明
    static void func()//静态同名成员函数
    {
        cout << "Son - static void func()" << endl;
    }
};
int Son::mA = 45;//类外初始化

访问方式,与静态同名成员变量的方式相同

void test2()
{
    //1. 通过对象访问
    cout << "通过对象访问" << endl;
    Son s;
    s.func();
    s.Base::func();
    s.Base::func(10);//父类静态同名函数 重载版本

    //2. 通过类名访问
    cout << "通过类名访问" << endl;
    Son::func();//子类类名访问
    //第一个双冒号表示 通过类名的方式访问;第二个双冒号表示 访问父类作用域下的同名成员属性
    Son::Base::func();//通过子类访问父类的同名成员
    Son::Base::func(10);//父类静态同名函数 重载版本
}

同样地,如果子类中有与父类同名的静态成员函数,子类的静态同名成员函数会隐藏掉父类中所有的静态静态成员函数,包括重载版本。

如果子类中出现和父类同名的成员函数,子类的同名成员函数会隐藏掉父类中所有同名成员函数(包括重载函数)。如果想访问父类中被隐藏的同名成员函数,需要加上作用域。

  1. 总结

同名静态成员处理方式和非静态处理方式一样,只不过有两种访问的方式(通过对象 和 通过类名)。

  1. 多继承语法

  1. 多继承语法及注意点

  • C++允许一个类继承多个类

  • 语法:class 子类 :继承方式 父类1 , 继承方式 父类2...;

  • 多继承可能会引发父类中有同名成员出现,需要加作用域区分

  • C++实际开发中不建议用多继承

  1. 场景描述

创建多个父类,让一个子类继承多个父类

class Base1
{
public:
    Base1()
    {
        mA = 100;
    }
    int mA;
};

class Base2
{
public:
    Base2()
    {
        mA = 200;
    }
    int mA;
};

//子类继承Base1和Base2
//语法:class 子类: 继承方式 父类1, 继承方式 父类2...
class Son :public Base1, public Base2
{
public:
    Son()
    {
        mC = 1000;
        mD = 2000;
    }
    int mC;
    int mD;
};
  1. 子类继承所有父类的属性

void test1()
{
    Son s;
    cout << "Size of Son" << sizeof(s) << endl;//16
}

工具查看分布,Son继承了Base1的mA和Base2的mB,还有自己的属性mC、mD,所以内存大小是16个字节。

当多继承出现同名成员属性时,如何访问不同父类中的同名成员属性

void test1()
{
    Son s;
    cout << "Size of Son" << sizeof(s) << endl;//16

    //当父类中出现同名成员,需要假作用域区分
    cout << "Base1\tmA = " << s.Base1::mA << endl;
    cout << "Base2\tmA = " << s.Base2::mA << endl;
}

int main()
{
    test1();

    system("pause");
    return 0;
}

所以在开发中,不建议使用多继承,有可能会导致同名成员属性的问题。

  1. 总结

  • 语法:class 子类 :继承方式 父类1 , 继承方式 父类2...;

  • 注意点:多继承中如果父类中出现了同名情况,子类使用时候要加作用域。

  1. 菱形继承

  1. 菱形继承概念

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

菱形继承就是形如:派生类1和派生类2继承基类,同时派生类3(对派生类1和派生类2而言,派生类1和派生类2是派生类3的基类,派生类3是派生类1和派生类2的派生类,)继承派生类1和派生类2

  1. 典型的菱形继承案例

羊和驼继承了动物的共性,草泥马又继承了羊和驼的属性。

  1. 菱形继承问题

上面的典型例子中,有如下问题:

  • 羊继承了动物的数据,驼同样继承了动物的数据,当草泥马使用数据时,就会产生二义性。这个在前面的学习中,可以加上作用域来访问不同父类的同名成员属性。

  • 草泥马继承了两份来自动物的数据。其实,这份数据我们只需要一份就可以。这该如何解决?

  1. 菱形继承问题解决办法

  1. 场景描述

创建一个基类,两个派生类,继承基类;再创建另一个类继承两个派生类。

//动物
class Animal 
{
public:
    int mAge;
};

//羊类
class Sheep :public Animal{};

//驼类
class Camel :public Animal{};

//羊驼类
class Alpaca :public Sheep, public Camel {};
  1. 加作用域访问不同父类下的同名成员属性

void test1()
{
    Alpaca a;
    a.Sheep::mAge = 55;//加作用域区分
    a.Camel::mAge = 45;//加作用域区分
    //菱形继承,两个父类拥有相同的数据时,需要加以作用域区分
    cout << "a.Sheep::mAge = " << a.Sheep::mAge << endl;
    cout << "a.Camel::mAge = " << a.Camel::mAge << endl;
}
  1. 问题二,怎么解决羊驼只需要一份数据?

菱形继承导致羊驼有两份数据,浪费资源,实际上只需要一份即可。

1)首先开发人员命令提示工具查看羊驼的对象模型,操作顺序:

  • 利用开发人员命令提示工具查看对象模型

  • 跳转盘符 F:(源文件所在盘符)

  • 跳转文件路径(源文件所在路径)

  • cd 具体路径下查看命名(源文件所在具体文件夹)

  • c1 /dl reportSingleClassLayout类名 文件名(也可以输入31,再按tab键补全)

2)羊驼的对象模型

可以看到羊驼继承了羊类和驼类,而羊类和驼类又继承了动物类。所以,羊驼有两份mAge,实际只需要一份,说明菱形继承会导致资源浪费。那么羊驼的mAge到底是55还是45呢?

  1. 虚继承:用于解决菱形继承带来的资源浪费的问题

虚继承关键字virtual语法:class 子类: virtual 继承方式 父类。

//虚继承 解决菱形继承问题
/*
继承前,加virtual关键字后,变为虚继承
此时公共的父类Animal称为虚基类
*/ 
//羊类
class Sheep :virtual public Animal{};

//驼类
class Camel :virtual public Animal{};

//羊驼类
class Alpaca :public Sheep, public Camel {};

void test1()
{
    Alpaca a;
    a.Sheep::mAge = 55;//加作用域区分
    a.Camel::mAge = 45;//加作用域区分
    //菱形继承,两个父类拥有相同的数据时,需要加以作用域区分
    cout << "a.Sheep::mAge = " << a.Sheep::mAge << endl;
    cout << "a.Camel::mAge = " << a.Camel::mAge << endl;

    //菱形继承导致羊驼有两份数据,浪费资源,实际上只需要一份即可
    //此时羊驼只有一份数据mAge
    cout << "a.mAge = " << a.mAge << endl;
}

此时,羊驼只有一份数据了,使用工具再次查看羊驼类的对象模型。

  1. 虚继承的底层实现过程:

  • 可以看到,羊驼只有一个mAge,在虚继承之前,羊驼有两个mAge。

  • vbptr是虚基类指针,vbtable是虚基类列表,虚基类指针vbptr指向虚基类列表vbtable。

  • 羊驼类继承羊类时,有一个虚基类指针vbptr,这个虚基类指针vbptr,指向羊类的虚基类列表vbtable。羊类的虚基类列表vbtable记录的偏移量是8,通过0+8(偏移量)找到唯一的数据mAge。

  • 同样,当子类羊驼继承驼类时,也有一个虚基类指针vbptr,这个指针指向驼类的虚基类列表,驼类的虚基类列表记录的偏移量是4。所以继承驼类时,4+4(偏移量)=8,也可以找到唯一的数据mAge。

因此,无论羊驼通过羊类继承还是驼类继承,都会通过虚基类指针指向的虚基类列表,再通过虚基类列表记录的偏移量找到唯一的数据mAge。

  1. 总结

  • 菱形继承带来的主要问题是子类继承两份相同的数据,导致资源浪费以及毫无意义

  • 利用虚继承可以解决菱形继承问题

黑马老师

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值