C++ 继承(基类与派生类)详解附代码

文章详细介绍了C++中的继承机制,包括继承的概念、实例演示、继承的三种方式(公有、私有、保护),基类和派生类对象的赋值转换,继承中的作用域,以及派生类的默认成员函数。同时,讨论了友元关系不被继承的特性,静态成员的继承行为,以及如何解决多继承中的菱形继承问题,提出了虚拟继承的概念和作用。
摘要由CSDN通过智能技术生成

继承是C++语言的三大特性之一,通过继承联系在一起的类构成一种层次关系。通常在层次关系的根部有一个基类,其他类则直接或间接地从基类继承而来,这些继承得到的类称为派生类。继承机制是面向对象程序设计使代码可以复用的最重要的手段,它允许程序员在保持原有类型特性的基础上进行扩展,增加功能。

一、继承的实例演示

#include<iostream>
#include<vector>
using namespace std;

// 声明一个基类Person
class Person 
{
public:
    Person(string n = "张三",int age = 18):_name(n),_age(age) {}
protected:
    string _name;
private:
    int _age;
};

// 声明一个派生类Student,继承自Person(公有继承)
class Student : public Person {
public:
    void Print()
    {
        cout << _name << endl;
        cout << _age <<endl;//这句会报错,因为_age属于Persong私有,派生类中不可见
    }
protected:
    string _stuid;
};

int main() {
    Student s;
    s.Print();

    return 0;
}

继承的三种方式

继承的方式有三种:公有继承、私有继承、保护继承

  1. 基类的private成员在派生类中不可见,如上述例子中Person中的_age成员在派生类Student中不可见。

  1. 如果基类成员不想在类外面被访问,但需要其派生类可以访问,那么我们将基类成员定义为protected,这也是private和protected的区别。

  1. 限定域:public>protected>private

  1. 若基类中的A种成员被派生类以B方式继承,那么基类的成员在派生类中的访问方式我们取A和B限定域小的那个,如:基类中的public成员被派生类以protected方式继承,那么派生类访问该成员的方式为protected;基类中的protected成员被派生类以public方式继承,那么派生类访问该成员的方式为protected。

二、基类和派生类对象赋值转换

在C++中,派生类对象可以赋值给基类的对象/基类的指针/基类的引用。这是因为由于派生类继承了基类的成员,所以派生类对象在内存中的布局与基类对象是兼容的,可以直接进行类型转换。

具体地说,将一个派生类对象赋值给基类对象时,编译器会自动调用从基类继承的构造函数,将派生类对象的基类部分复制到基类对象中,而忽略掉派生类独有的成员。这样就可以通过基类对象来访问派生类继承的成员了。

上述这种赋值行为也被叫做切片,即切去派生类中属于派生类的独有部分,而将属于基类的部分赋值给基类对象。

注意一点,除非基类的指针指向派生类对象,否则基类对象不能给派生类对象赋值

#include <iostream>
using namespace std;

class Person {
public:
    string _name;
    int _age;
    char _sex;

    Person(string name, int age, char sex) 
    : _name(name), _age(age), _sex(sex) {}
    void Print()
    {
        cout << "基类" << endl;
        cout << "name: " << _name << "\nage: " << _age << "\nsex: " << _sex << endl<<endl;
    }
};

class Student : public Person {
public:
    int _uid;

    Student( string name, int age, char sex,int uid)
     : Person(name, age, sex), _uid(uid) {}
    void display() {
        cout << "派生类";
        cout << "\nname: " << _name << "\nage: " << _age << "\nsex: " << _sex << "\nuid: " << _uid << endl << endl;
    }
};

int main() {
    Student s("张三", 18, 'm',1);
    Person p = s; // 派生类对象赋值给基类对象

    s.display();
    p.Print();
    //这样也可以
    Person* ptr = &s;//指向子类当中父类对象的一部分
    Person& ref = s;//变成子类对象当中父类一部分的别名
    
    return 0;
}

结果演示:

三、继承中的作用域

在派生类中,如果定义了与基类同名的成员变量或成员函数,那么该成员会屏蔽基类中同名的成员。这种情况下,在派生类内部访问同名成员时,实际上是访问派生类自己的成员。

下面以Student类为例,演示派生类对基类相同成员的屏蔽:

#include <iostream>
using namespace std;

class Person
{
protected:
    string _name = "张三";
    int _age = 18;
    int _num = 10;
};

class Student :public Person
{
public:
    void Print()
    {
        cout << _num << endl;//默认访问的是Student中的_num
        cout << Person::_num<<endl;//可以指定访问Persong中的_num
    }
protected:
    int _num = 20;
};

int main()
{
    Student s;
    s.Print();
    return 0;
}

我们可以总结以下几点:

  1. 在继承体系中基类和派生类都有独立的作用域,因此当派生类中也定义了与基类相同的_num时,不会报错。

  1. 基类与派生类中有同名成员时,派生类成员将屏蔽基类对同名成员的直接访问,这种情况叫隐蔽,也叫重定义。

  1. 对于成员函数来说,只需要函数名相同就构成隐蔽/重定义。

  1. 虽然有隐蔽,但我们仍然可以指定访问父类中的成员(如上述例子中的Person::_num),实际中我们应该尽量避免在派生类中定义与基类相同的成员名,即要避免隐蔽/重定义。

  1. 一定要区分开函数重载与函数重定义,前者发生在同一作用域,后者发生在不同作用域。

四、派生类的默认成员函数

#include <iostream>
using namespace std;

class Person
{
public:
    Person(const char* name = "peter")
        :_name(name)
    {
        cout << "Person()" << 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
{
public:
    Student(const char* name, int stuid)
    :Person(name)//显式调用基类构造函数
    ,_stuid(stuid)
    {
        cout << "Student()" << endl;
    }

    Student(const Student& s)
        :Person(s)//显式调用基类构造函数,派生类赋值给基类(切割)
        ,_stuid(s._stuid)
    {
        cout << "Student(const Student& s)" << endl;
    }
    
    //s1 = s3
    Student& operator=(const Student& s)
    {
        if (this != &s)
        {
            //由于函数名相同即构成隐藏,所以需要显式调用基类的赋值
            this->Person::operator=(s);
            _stuid = s._stuid;
        }
        cout << "Student& operator=(con) " << endl;
        return *this;
    }

    //派生类的析构函数与基类的析构函数也构成隐藏
    ~Student()
    {
        cout << "~Student()" << endl;
        //这里我们不需要显式调用基类的析构函数了
        //派生类析构结束以后会自动调用基类的析构函数
    }

protected:
    int _stuid;
};

总结以下几点:

  1. 基类的构造函数,赋值重载需要再派生类中显式调用。

  1. 为保证先析构派生类,再析构基类,基类的析构函数会在派生类完成析构后自动调用,所以不需要我们再隐式调用了。

五、友元不会被继承

在C++中,友元关系不会被继承。也就是说,派生类不会自动获得基类的友元关系。我们将基类看做父类,派生类看做子类,还可以理解为:你父亲的朋友不是你的朋友。

虽然友元关系不会被继承,但是我们可以在派生类中重新声明一下友元关系。

#include <iostream>
using namespace std;

class Student;
class Person
{
public:
    friend void Print(const Person& p, const Student& s);
    string _name;// 姓名
};

class Student :public Person
{
protected:
    int _stuNum;// 学号
};

void Print(const Person& p, const Student& s)
{
    cout << p._name << endl;
    cout << s._stuNum << endl;//编译不通过
}

int main()
{
    Person p;
    Student s;
    Print(p, s);

    return 0;
}

由于Print不是Student的友元,所以Print不能访问到Student里面的成员这时候我们可以给Student里面声明一下friend void Display(const Person& p, const Student& s);

这样Print就能访问到Student里面的成员了。

#include <iostream>
using namespace std;

class Student;
class Person
{
public:
    friend void Print(const Person& p, const Student& s);
    string _name = "张三";// 姓名
};

class Student :public Person
{
public:
    friend void Print(const Person& p, const Student& s);
protected:
    int _stuNum = 1;// 学号
};

void Print(const Person& p, const Student& s)
{
    cout << p._name << endl;
    cout << s._stuNum << endl;
}

int main()
{
    Person p;
    Student s;
    Print(p, s);

    return 0;
}

六、继承与静态成员

静态成员在C++中属于整个类,而不是属于某个具体的对象。因此,静态成员可以被继承,但不会被子类所隐藏。

如下面:基类定义了static静态成员,无论派生出多少个子类,整个继承体系里面都只有一个这样的成员。

#include <iostream>
using namespace std;

class A {
public:
    static int num;

};

int A::num = 1;

class B : public A {
public:

};

int main() {
    A::num = 2;
    B::num = 3;//改的是同一个num
    cout << "A::num = " << A::num << endl;//3
    cout << "B::num = " << B::num << endl;//3

    return 0;
}

静态成员变量的继承方式跟普通静态变量一样,在子类中可以直接使用父类的静态成员变量。如果在子类中重新定义同名的静态成员变量,则会覆盖父类中的静态成员变量。

静态成员函数也可以被继承,在子类中可以不加任何修饰符地直接使用父类的静态成员函数。注意,静态成员函数中不能使用this指针,因为this指针只能用于非静态成员函数中。

#include <iostream>
using namespace std;

class A {
public:
    static int num;
    static void print() {
        cout << "A::num = " << num << endl;
    }
};

int A::num = 1;

class B : public A {
public:
    static int num;

    static void print() {
        cout << "B::num = " << num << endl;
    }
};

int B::num = 5;

int main() {
    A::print();
    B::print();
    return 0;
}

从输出结果来看,静态成员变量num被成功继承,并且在子类中重新定义同名的静态成员变量也能被成功使用;静态成员函数print()也被成功继承,并且子类中重新定义同名的静态成员函数也能被成功调用。

七、复杂继承-菱形继承

单继承是指一个派生类只继承自一个基类。在单继承中,每个类仅具有一个直接基类,并与该基类构成了一条继承链。通常情况下,子类与父类之间具有某种"是一种"("is-a")的关系。例如,一只狗是一种动物,一个圆形是一种图形等等。

多继承是指一个派生类可以继承自多个基类(子类有两个或两个以上的直接父类)。在多继承中,每个类可以具有多个直接基类,并形成多个继承链。

菱形继承是多继承的一种特殊情况,由多继承引发的菱形继承问题会造成数据冗余与二义性。

我们看下面代码:

#include<iostream>
using namespace std;

class A
{
public:
    int num;
};

class B:public A
{
public:
    int b;
};

class C :public A
{
public:
    int c;
};

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

int main()
{
    D tmp;
    tmp.num = 1;//由于指向不明确,所以此句编译不通过
    //我们需要指定才能修改
    tmp.B::num = 2;
    tmp.C::num = 3;
    
    return 0;
}

图解:

我们可以看到,d中有两个num,所以我们需要指定B中或C中才能访问,虽然解决了问题,但这并不是我们的初衷,D中有两个num,我们认为这种菱形继承造成了数据冗余与二义性

那么如何具体解决菱形继承造成的数据冗余与二义性问题呢?我们选择引入虚拟继承

虚拟继承

虚拟继承的核心思想是在多重继承中,将公共基类在派生类中仅存在一份副本,而不是每个基类都继承自一份公共基类,从而避免了菱形继承问题的出现。

我们观察一下使用虚拟继承前后的内存情况:

使用虚拟继承前:

class A
{
public:
    int num;
};

class B: public A
{
public:
    int b;
};

class C : public A
{
public:
    int c;
};

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

int main()
{
    D tmp;
    cout << sizeof(tmp) << endl;//这里变成了24
    tmp.B::num = 1;
    tmp.C::num = 2;
    tmp.b = 3;
    tmp.c = 4;
    tmp.d = 5;

    return 0;
}

使用虚拟继承之后:

class A
{
public:
    int num;
};

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 tmp;
    cout << sizeof(tmp) << endl;//这里变成了24
    tmp.B::num = 1;
    tmp.C::num = 2;
    tmp.b = 3;
    tmp.c = 4;
    tmp.d = 5;
    tmp.num = 6;

    return 0;
}

num放到了公共位置上去了,既没有放到B中,也没有放到C。

B和C中原来存储num的位置存储了一个指针,我们称之为虚基表指针,它指向一个虚基表,虚基表中存储了到num位置的偏移量。所以,由于原来存储num的位置转而存储虚基表指针,又将num存到了公共位置,所以多了四个字节(32位情况下)。

小结:

关键字virtual告诉编译器将类B和类C对类A的继承定义为虚拟继承,这样在类D中就只有一份类A的副本了(由于A被虚拟继承了,A因此也被称为虚基类),避免了菱形继承问题。

使用虚拟继承可以避免由于多重继承而导致的变量或函数二义性问题,但同时也会增加一定的开销,因为编译器需要维护虚基表,以便在派生类中访问正确的基类成员。同时,使用虚拟继承可能也会增加代码复杂度和可读性,因此需要根据具体情况进行选择。比如,在都适用的情况下,我们优先选择类的组合而非类的继承

### 回答1: 在C++中,继承是一种重要的面向对象编程概念,它允许我们定义一个新的类,该类继承现有类的所有属性和方法。派生类继承类的子类,它可以使用继承类的所有属性和方法,并且可以添加自己的属性和方法。 在实验中,我们学习了C++继承派生类的相关知识,包括: 1. 继承类的访问控制:公有继承、私有继承和保护继承。 2. 多重继承:一个派生类可以同时继承多个基类。 3. 虚函数和纯虚函数:虚函数是在基类中定义的函数,可以在派生类中重写;纯虚函数是没有实现的虚函数,必须在派生类中实现。 4. 虚函数表和虚函数指针:虚函数表是用于存储虚函数地址的表,虚函数指针指向虚函数表。 通过实验,我们能够更深入地理解C++继承派生类的概念和应用,能够更加灵活地使用面向对象编程思想来设计和实现程序。 ### 回答2: 继承是面向对象编程中的重要概念之一,它允许我们在已有类的基础上创建新的类,新类可以继承并拥有已有类的属性和方法。通过继承,可以减少代码的重复性,并且使代码更加可维护和扩展。 在进行派生类实验的过程中,我深刻体会到了继承的重要性和灵活性。首先,通过定义一个基类,我可以将一些通用的属性和方法抽象出来,避免在每个派生类中都重复定义。这为程序的整体结构设计提供了便捷。 其次,派生类可以在继承基类的基础上进行扩展,添加新的属性和方法。这种灵活性使得派生类在满足基本功能的同时,也能根据具体需求进行定制化开发。例如,在一个动物类的基础上,我可以派生出猫、狗等具体的动物子类,它们各自有着自己的特点和行为。 另外,继承还支持多层次的派生关系。我可以从一个派生类中再派生出新的派生类,这样可以形成类的层次结构。这种层次化设计可以更好地组织代码,使得代码更加清晰可读。 通过这次实验,我进一步理解了继承性与派生类的概念,学会了如何设计和使用继承关系。同时,我也认识到了继承关系的合理运用能够提高代码的效率和可维护性。继承不仅是面向对象编程的基础,也是实现代码重用和扩展的重要工具之一。在今后的编程实践中,我将更加灵活地运用继承,提高代码的质量和可扩展性。 ### 回答3: 继承性是面向对象编程中的一个重要特性,指的是子类能够继承父类的属性和方法。通过继承,子类可以重用父类的代码,并且可以在此基础上进行扩展和修改。 派生类是指通过继承父类而创建的新类。在派生类中,可以通过重写父类的方法,改变其行为,实现多态性。派生类还可以新增自己的成员变量和成员方法,以满足自身的特殊需求。 在实验中,我们通过创建父类和派生类的关系,研究了继承性和派生类的特性。 通过继承,我们可以将通用的属性和方法放在父类中,让子类共享这些代码。这样可以提高代码的重用性和可维护性。同时,当需要对父类中的方法进行修改时,只需在子类中进行重写,不会对其他子类造成影响。 在派生类中,我们可以根据需要重写父类的方法,改变其行为。这使得可以根据实际情况来实现多态性,同一个方法在不同的派生类中可能表现出不同的行为。 派生类还可以新增自己的成员变量和成员方法。通过这样的方式,可以为子类添加独特的功能,以满足特定的需求。 继承性和派生类是面向对象编程中非常重要的概念。通过合理运用这两个特性,可以使代码更加模块化和可扩展,提高代码的复用性和可维护性。同时,派生类的特性也使得面向对象编程更加灵活,可以根据实际需求进行扩展和修改。
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值