目录
一、继承的概念
1.继承的引入
在实际生活中我们会遇到很多有着共同特性的事物,例如,我们定义一个学生类和教师类,可以发现成员变量中都将会有姓名、性别、年龄等相关信息,这样的例子还有很多,如果每次写时都要包含这些共同信息就会很麻烦,因此,继承就应运而生。
继承机制是面向对象程序设计中使代码可以复用的重要手段,它允许程序员在保证原有类特性的基础上进行扩展,增加功能,这样产生的新的类称作派生类。
例如在上面的例子中,我们可以写一个人类(成员中有姓名、性别等),然后学生类和教师类可以继承人类,再根据其特性增加相应成员变量和成员函数,在这个例子中人类称作基类(父类),学生类和教师类称作派生类(子类)。
2.继承的格式
class 基类 {
// 基类的成员
};class 派生类 : 继承方式 :基类 {
// 派生类的成员
};
其中继承方式有public、protected和private。
继承有单继承和多继承,当继承了多个类时,依旧是在派生类后写继承方式+继承的类名,注意用逗号分隔,当我们不写继承方式时默认继承方式是private。
二、继承的基本语法
1.不同继承方式的区别
继承方式 | public继承 | protected继承 | private继承 |
---|---|---|---|
基类的public成员 | 为派生类中的public成员 | 为派生类中的protected成员 | 为派生类中的private成员 |
基类的protected成员 | 为派生类中的protected成员 | 为派生类中的protected成员 | 为派生类中的private成员 |
基类的private成员 | 在派生类中不可见 | 在派生类中不可见 | 在派生类中不可见 |
从上面的表格中可以看到, 无论是哪种继承方式,基类中的private成员在派生类中都是不可见的,不可见即不能直接在派生类中访问,而其他的成员的访问权限则取继承方式和原本基类中的访问权限较小的那一个,即min(继承方式,在基类中的访问权限),这也与权限可以缩小而不能放大相对应。
接下来我们来看段代码来加深理解:
可以看到,是不能在Derived类中直接访问Base类中的私有成员变量cc的,但是可以访问公有和保护成员变量,如果是private继承也同样如此,因为在类中是可以直接访问成员变量的,不受访问限定符的限制,但是由于基类中私有成员始终不可见,所以仍不能访问基类中的私有成员。
同时,我们也可以发现,protected的作用出现了,当我们不想让某个成员在类外访问,但想让它在派生类中可访问时,就可以在基类中设置为protected。
2.赋值兼容规则
1)本类的对象指针和对象引用可以指向本类对象
2)基类指针和基类引用可以指向派生类对象,基类对象也可以被赋值为派生类对象
3)派生类的指针和引用可以赋值给基类的指针和引用
这里主要对第二、三点进行讲解:
派生类对象赋值给基类对象的过程通常被称为“切割”,这一过程涉及将派生类对象中的基类部分的数据“切”出来,然后赋值或初始化相应的基类对象、指针或引用。因为我们知道继承后,派生类中是会存在基类中的成员的,当把派生类对象赋值给基类对象时就相当于把这部分切割出来重新赋值。
需要注意的是,派生类的对象可以赋值给基类对象,但是反过来不可以,这个通过上边的“切割”就比较容易理解了。
下面,举具体的例子来演示:
可以看到,退出代码为0,是没有问题的。
3.派生类中的相关函数
派生类不能继承基类中的构造函数、析构函数、赋值运算符重载函数以及友元函数。
派生类中基类成员的构造必须由基类构造函数完成,不能由派生类的构造函数完成,并且是先初始化基类成员,再初始化其他成员,而析构函数则与之相反。
对于基类的构造函数有两种方式,即显式调用和隐式调用,显式调用即在初始化列表中显式调用基类构造函数,隐式调用则是当基类中有默认构造函数时,我们不显式调用基类的构造函数,编译器会自动先调用基类的默认构造函数,然后执行派生类构造函数函数体。
需要注意的是,显式调用基类构造函数初始化派生类对象中的基类数据成员必须使用成员初始化列表。
析构函数不需要我们显式调用,在执行完派生类的析构函数后会自动调用基类的析构函数。
派生类的拷贝构造函数和赋值运算符重载函数都必须调用基类的拷贝构造函数和赋值运算符重载函数完成基类的拷贝初始化和赋值。
接下来简单看个示例:
class Base { public: Base() :aa(0) {} Base(Base& x) :aa(x.aa) {} private: int aa; }; class Derived:public Base { public: Derived() :Base(),bb(1) {} Derived(Derived& x) :Base(x),bb(x.bb) //赋值兼容转换 {} private: int bb; };
因为有了赋值兼容转换规则,派生类对象可以直接赋值给基类对象,所以上面派生类的拷贝构造函数中可以直接传派生类对象给基类拷贝构造函数,如果要在函数体内调用基类拷贝构造函数的话,得加类名,即Base::Base(x);。
友元关系不能继承,基类中的友元函数或者友元类是并不能被派生类继承的,即不能访问派生类的私有成员。
4.函数隐藏
基类和派生类都是独立的作用域,他们是可以有同名成员的,当有同名成员并且使用时,默认调用的就是派生类中的成员,如果要调用基类中的同名成员需要加上类名来显式调用,这种对基类同名成员的屏蔽称作隐藏(亦重定义)。
三、虚继承
在生活中也存在一些特殊的继承关系,例如,某个某个程序员他既是一名员工,也是一名父亲,而员工和父亲类都可以由人类继承过来,这样当继承员工类和父亲类时就会有两份人类中的成员数据,当在调用时就可能发生歧义并且会造成冗余,为了解决这个问题,虚继承就产生了。
虚继承用到的关键字是virtual。
格式如下:
class 基类 {
// 基类的成员
};class 中间类1 : virtual public 基类 {
// 中间类1的成员
};class 中间类2 : virtual public 基类 {
// 中间类2的成员
};class 最派生类 : public 中间类1, public 中间类2 {
// 最派生类的成员
};
在虚继承中,基类成员数据就只有一份,避免了数据冗余和空间浪费。
在虚继承中,对最派生类的构造不仅需要调用直接基类的构造函数,还需要调用最基类的构造函数,因为这样可以保证最基类可以只构造一次。
举个例子:
class Base { public: Base() { std::cout << "Base constructor called.\n"; } }; class Derived1 : virtual public Base { public: Derived1() { std::cout << "Derived1 constructor called.\n"; } }; class Derived2 : virtual public Base { public: Derived2() { std::cout << "Derived2 constructor called.\n"; } }; class MostDerived : public Derived1, public Derived2 { public: MostDerived() : Base(), Derived1(), Derived2() { std::cout << "MostDerived constructor called.\n"; } };