【二十三】【C++】继承

继承之隐藏 及访问隐藏信息

  • 在继承的体系中,基类和派生类都有独立的作用域

  • 隐藏(重定义):如果派生类声明了一个与基类中同名的成员,无论它们的参数是否相同,都会隐藏基类中所有同名的成员。(只要名字相同就构造隐藏)

  • 如果派生类需要访问被隐藏的基类成员(例如,因为派生类定义了一个同名的成员),它可以使用作用域解析::运算符来指定要访问基类的成员。(基类::基类成员)

 
/*隐藏和显示访问隐藏信息*/
#if 1
#include<iostream>
using namespace std;
class Person{
protected:
    string _name="小华";
    int _num=3601234;//身份证号
};

class Student: public Person{
public:
    void Show(){
        cout<<"姓名:"<<_name<<endl;
        cout<<"身份证号:"<<Person::_num<<endl;
        cout<<"学号:"<<_num<<endl;
    }
protected:
    int _num=001;//学号
};
int main(){
    Student s;
    s.Show();
 }
#endif 

基类 Person

Person 类包含两个受保护的成员变量:_name_num。由于它们被声明为受保护的,因此可以在派生类 Student 中直接访问它们。

派生类 Student

Student 类公有地继承自 Person。在 Student 中,有一个公有的成员函数 Show() 用于输出学生的信息。Student 类也定义了一个名为 _num 的受保护的成员变量,这导致基类 Person 中的 _num 被隐藏。

名称隐藏

当派生类 Student 中的 _num 被声明时,它隐藏了同名的基类 Person 中的 _num 成员。在 Student 类的作用域内,不带任何类限定符的 _num 引用的是 Student 类中的学号,而不是 Person 类中的身份证号。

显示访问隐藏的成员

Show() 函数中,_name 直接访问的是 Person 类中的 _name 成员,因为在 Student 类中没有同名成员。为了访问隐藏的基类成员 _num(身份证号),Show() 使用了作用域解析运算符 Person::_num,这样就明确指出要访问 Person 类中的 _num

继承的默认成员函数

 
/*继承中的默认成员函数*/
#if 1
#include <iostream>
using namespace std;

class Person {
public:
    
     Person(const char* name = "peter")
    : _name(name) {
        cout << "Person(const char* name='peter')" << endl;
    }
    
     Person(const Person&p)
    : _name(p._name) {
        cout << "Person(const Person&p)" << endl;
    }
    
     Person& operator=(const Person&p) {
        cout << "Person& operator=(const Person&p)" << endl;
        if (this != &p) {
            _name = p._name;
        }
        return *this;
    }
    
     ~Person() {
        cout << "~Person()" << endl;
    }
protected:
    string _name;
 };

class Student: public Person {
protected:
    int _num;
public:

 };

int main(){

    Student s1;
    Student s2(s1);
    Student s3;
    s1=s3;
    
}
#endif

默认构造函数:如果派生类没有定义任何构造函数,编译器会提供一个默认的构造函数。这个默认构造函数会调用基类的默认构造函数来初始化基类部分。

拷贝构造函数:如果派生类没有显式定义,编译器会生成一个默认的拷贝构造函数,它会调用基类的拷贝构造函数来拷贝基类部分。

拷贝赋值运算符:如果派生类没有显式定义拷贝赋值运算符,编译器会提供一个默认的拷贝赋值运算符,它会调用基类的拷贝赋值运算符来赋值基类部分。

析构函数:编译器总是会为派生类生成一个默认的析构函数。当派生类对象被销毁时,派生类的析构函数会被调用,随后是基类的析构函数。

 
/*继承中的默认成员函数*/
#if 1
#include <iostream>
using namespace std;

class Person {
    public:
        Person(const char* name = "peter")
            : _name(name) {
            cout << "Person(const char* name='peter')" << endl;
        }

        Person(const Person&p)
            : _name(p._name) {
            cout << "Person(const Person&p)" << endl;
        }

        Person& operator=(const Person&p) {
            cout << "Person& operator=(const Person&p)" << endl;
            if (this != &p) {
                _name = p._name;
            }
            return *this;
        }

        ~Person() {
            cout << "~Person()" << endl;
        }
    protected:
        string _name;
 };

class Student: public Person {
    protected:
        int _num;
    public:
        Student(const char*name, int num)
            : Person(name)
            , _num(num) {
            cout << "Student(const char*name,int num)" << endl;
        }

        Student(const Student&s)
            : Person(s)
            , _num(s._num) {
            cout << "Student(const Student&s)" << endl;
        }

        Student& operator=(const Student& s) {
            cout << "Student& operator=(const Student& s)" << endl;
            if (this != &s) {
                Person:: operator=(s);
                _num = s._num;
            }
            return *this;
        }

        ~Student() {
            cout << "~Student()" << endl;
        }
 };

int main(){
    Student s1("张三",18);
    Student s2(s1);
    Student s3("李四",19);
    s1=s3;
 }
#endif

  • 派生类的构造函数必须调用基类的构造函数初始化基类的那一部分成员。如果基类没有默认的构造函数,则必须在派生类构造函数的初始化列表阶段显示调用。

  • 派生类的拷贝构造函数必须调用基类的拷贝构造完成基类的拷贝初始化。

  • 派生类的operator=必须要调用基类的operator=完成基类的复制。

  • 派生类的析构函数会在被调用完成后自动调用基类的析构函数清理基类成员。因为这样才能保证派生类对象先清理派生类成员再清理基类成员的顺序。

  • 派生类对象初始化先调用基类构造再调派生类构造。

  • 派生类对象析构清理先调用派生类析构再调基类的析构。

友元关系无法通过继承传递

 
/*友元没办法继承*/
#if 1
#include<assert.h>
#include<iostream>
using namespace std;
class Student;
class Person{
protected:
    string _name;
public:
    friend void Show(const Person& p,const Student&s);
 };

class Student: public Person{
protected:
    int _stuNum;
 };

void Show(const Person& p,const Student&s){
    cout<<p._name<<endl;
//    cout<<s._stuNum<<endl;//报错
}
int main(){
    Person p;
    Student s;
    Show(p,s);
 }
#endif

我们定义了Show函数是Person类的友元函数,所以我们可以在Show函数内访问Person类的成员变量。我们又定义了Person类的派生类Student类,此时我们并没有办法在Show函数内访问Student类的成员变量。ShowPerson类的友元函数,但不是Student的友元函数。说明友元关系没办法通过继承传递。

继承与静态变量

 
/*继承和静态变量*/
#if 1
#include<iostream>
using namespace std;

class Person{
public:
    static int _count;
protected:
    string _name;
public:
    Person(){++_count;}
 };

int Person::_count=0;
class Student: public Person{
protected:
    int _stuNum;
 };

class Graduate: public Student{
protected:
    string _seminarCourse;
 };
int main(){
    Student s1;
    Student s2;
    Student s3;
    Graduate s4;
    cout<<"人数:"<<Person::_count<<endl;
    Student::_count=0;
    cout<<"人数:"<<Person::_count<<endl;
 }


#endif

基类定义了static静态成员,则整个继承体系里面只有一个这样的成员。派生类的成员变量可以理解为两部分,一部分来自于基类,一部分来自于自身。基类中的静态变量属于每一个基类部分。

继承的分类

继承的种类有三种,单继承、多继承,或者菱形继承(钻石继承)。

单继承(Single Inheritance)

单继承指的是一个派生类只继承自一个基类。

 
class Base {
    // 基类成员
};

class Derived : public Base {
    // 派生类成员
};

在单继承中,派生类继承了基类的所有公有和保护成员。这是最常见和最简单的继承方式,它确保了类层次结构的清晰和易于管理。

多继承(Multiple Inheritance)

多继承允许一个派生类同时继承自多个基类。

 
class Base1 {
    // 基类1成员
};

class Base2 {
    // 基类2成员
};

class Derived : public Base1, public Base2 {
    // 派生类成员
};

多继承可能导致更复杂的层次结构,尤其是当多个基类有相同的成员时,可能需要使用作用域解析运算符来指定要访问的基类成员。多继承需谨慎使用,因为它可能带来歧义和复杂性。

菱形继承(Diamond Inheritance)

菱形继承是一种特殊的多继承,其中两个基类继承自同一个更高层的基类,而一个派生类同时继承这两个基类。

 
class Base {
    // 最顶层的基类成员
};

class Derived1 : public Base {
    // 第一层派生类成员
};

class Derived2 : public Base {
    // 第一层派生类成员
};

class FinalDerived : public Derived1, public Derived2 {
    // 最终派生类成员
};

在菱形继承中,FinalDerived 会通过 Derived1Derived2 两条路径继承 Base 类的成员,这可能导致 Base 类成员的重复,以及对同一个基类成员的多个副本。

菱形继承的二义性和数据冗余

 
/*菱形继承的二义性*/
#if 1
#include<iostream>
using namespace std;
class Person{
public:
    string _name;
 };

class Student: public Person{
protected:
    int _num;
 };

class Teacher: public Person{
protected:
    int _id;
 };

class Assistant: public Student,public Teacher{
protected:
    string _majorCourse;
 };

int main(){
    Assistant a;
    //a._name="zhangsan";
     a.Student::_name="zhangsan";    
     a.Teacher::_name="lisi";    
     
}
#endif

派生类的成员变量可以理解为一部分来自于基类,一部分来自于自身。如果出现了菱形继承,那么对象就可能同时拥有两份最高级的基类的信息。此时直接访问最高级的基类成员变量,就会发生二义性。如果想要访问a._name的属性,编译器不知道你要访问Student中的_name还是访问Teacher中的_name。此时需要用作用域限定符告知编译器具体是哪个部分内的 _name成员变量。并且一个对象中会存在相同的基类部分,导致数据冗余。

虚拟继承

在C++中,虚拟继承是一种用来解决多重继承时可能出现的菱形继承问题(也称为“钻石问题”)的机制。在菱形继承结构中,一个类继承自两个类,而这两个类又共同继承自另一个类,这会导致最底层的派生类通过不同路径继承自顶层基类的成员,从而造成多份基类成员的副本存在,引发歧义。

为了解决这个问题,C++引入了虚拟继承。通过虚拟继承,可以确保在继承层次中被多次继承的类只有一个实例。

虚拟继承的基本语法

在C++中,使用关键字virtual声明虚拟继承。

 
/*虚拟继承*/
#if 1
#include<iostream>
using namespace std;
class Person{
public:
    string _name;
 };

class Student: virtual public Person{
protected:
    int _num;
 };

class Teacher: virtual public Person{
protected:
    int _id;
 };

class Assistant: public Student,public Teacher{
protected:
    string _majorCourse;
 };

int main(){
    Assistant a;
    a._name="zhangsan";  
     
}
#endif

一般的继承,对于派生类的成员变量,我们可以理解为一部分来自于基类,一部分来自于自身。而虚拟继承,我们可以理解为,派生类a一部分来自于Teacher,一部分来自于Assistant,而基类中含有相同的虚基类,则会发生去重,只会保留一个虚基类部分。而TeacherAssistant中,一部分来自于虚基类,一部分来自于自身,一部分是虚基指针。

虚拟继承的原理

虚拟继承的工作原理主要是通过引入一个间接层来解决信息冗余现象,即确保在继承体系中虚拟基类的数据成员只有一份实例。这个间接层主要包括虚拟基类指针(Virtual Base Pointer, VBP)和虚拟基类表(Virtual Base Table, VBT)。

虚拟基类指针(VBP)

在虚拟继承中,每个对象都会包含一个或多个虚拟基类指针(VBP),这些指针指向一个或多个虚拟基类表(VBT)。这个指针的主要作用是在运行时提供对虚拟基类成员的直接访问。

VBP是对象布局的一部分,它在对象被创建时被初始化,以确保可以正确地访问虚拟基类的成员。

虚拟基类表(VBT)

虚拟基类表(VBT)是编译时生成的,包含了虚拟基类相对于派生类对象的偏移量。这个偏移量对于解决多重继承中的菱形继承问题至关重要,因为它确保了无论通过哪个派生类路径访问虚拟基类,访问的都是同一份实例。

VBT使得在复杂的继承体系中,虚拟基类的成员可以被正确地定位,即使在多个派生类中被多次继承。

实现信息冗余现象的解决

在没有虚拟继承的情况下,如果一个类通过不同的路径继承自同一个基类,则该基类的数据成员会在派生类中存在多份副本,这就是信息冗余现象。虚拟继承通过以下方式解决这个问题:

单一实例:通过虚拟继承,确保虚拟基类在整个继承体系中只有一个实例。不管这个虚拟基类被继承了多少次,由于所有的虚拟继承路径共享同一个虚拟基类实例,因此避免了成员的重复。

运行时偏移量计算:通过使用VBP和VBT,即使在复杂的继承结构中,也能够在运行时计算出虚拟基类成员的正确偏移量,从而确保对这些成员的访问是准确的。这种机制允许对象动态地定位其虚拟基类的部分,无论它在内存中的实际位置如何。

虚拟继承与继承的探究

 
/*虚拟继承的探究*/
#if 1
#include<iostream>
using namespace std;
class A{
public:
    int _a;
 };

class B: virtual public A{
public:
    int _b;

 };

class C: virtual public A{
public:
    int _c;

};

class D: public B,public C{
public:
    int _d;
 };

int main(){
    D d;
    B b;
    C c;
    A a;
    cout<<sizeof(a)<<endl;
    cout<<sizeof(b)<<endl;
    cout<<sizeof(c)<<endl;
    cout<<sizeof(d)<<endl;    
     cout<<sizeof(int*)<<endl;    
}
#endif

结尾

最后,感谢您阅读我的文章,希望这些内容能够对您有所启发和帮助。如果您有任何问题或想要分享您的观点,请随时在评论区留言。

同时,不要忘记订阅我的博客以获取更多有趣的内容。在未来的文章中,我将继续探讨这个话题的不同方面,为您呈现更多深度和见解。

谢谢您的支持,期待与您在下一篇文章中再次相遇!

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

妖精七七_

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值