> 文档中心 > 【C++】继承

【C++】继承

目录

1. 概念

2. 定义 

2.1. 使用格式

2.2. 继承方式与限定修饰符

3. 基类和派生类对象赋值转换 (public继承)

4. 继承中的作用域

5. 子类类的默认成员函数

6. 继承与友元

7. 静态成员

8. 菱形继承和虚继承

8.1 菱形继承

8.2 虚继承(virtual)

8.3 虚继承解决数据冗余和二义性的原理

9. 继承总结、继承与组合

9.1总结

9.2 继承与组合 


1. 概念

继承(inheritance)机制是面向对象程序设计使代码可以复用的最重要的手段,它允许程序员在保持原有类特性的基础上进行扩展,增加功能,这样产生新的类,称子类或者派生类,被继承的类称为父类基类。继承呈现了面向对象程序设计的层次结构,体现了由简单到复杂的认知过程。

继承是类设计层次的复用

例如 :

#include#includeusing namespace std;class Person{public:void Print(){cout << "_name  _age  _sex" << endl;}protected:string _name;int _age;char _sex;};// 继承后父类的Person的成员(成员函数+成员变量)都会变成子类的一部分。class Student :public Person {protected :string _id;};int main(){Student s;s.Print();return 0;}

2. 定义 

2.1. 使用格式

class Person{public:void Print(){cout << "_name  _age  _sex" << endl;}protected:string _name;int _age;char _sex;};class Student :public Person // 子类的类名后加 冒号+继承方式+继承父类名{protected :string _id;};

 

2.2. 继承方式与限定修饰符

与访问限定修饰符一样,继承方式也有三种分别是:public、protected、private

而不同的继承方式对于不同访问方式的父类成员,在子类中的访问方式也不同。

所以根据两两组合,父类成员在子类中就有9中访问方式。

总结:

  1. 基类private成员在派生类中无论以什么方式继承都是不可见的。不可见是指基类的私有成员还是被继承到了派生类对象中,但是语法上限制派生类对象不管在类里面还是类外面都不能去访问它。

  2. 基类private成员在派生类中是不能被访问,如果基类成员不想在类外直接被访问,但需要在派生类中能访问,就定义为protected

  3. 基类的私有成员在子类都是不可见。基类的其他成员在子类的访问方式 == Min(成员在基类的访问限定符,继承方式),public > protected > private。

  4. 使用关键字class时默认的继承方式是private,使用struct时默认的继承方式是public

  5. 在实际运用中一般使用都是public继承,几乎很少使用protetced/private继承

3. 基类和派生类对象赋值转换 (public继承)

派生类对象 可以赋值基类的对象 / 基类的指针 / 基类的引用。这里有个形象的说法叫切片或者切割。寓意把派生类中父类那部分切来赋值过去。

基类对象不能赋值给派生类对象,基类的指针可以通过强制类型转换赋值给派生类的指针(子类指针在使用时可能会存在越界风险)。但是必须是基类的指针是指向派生类对象时才是安全的

 这里相当于将子类中 父类和子类都有的东西切割给父类

    Person p;Student s;p = s; // 这里不存在类型转换,语法支持Person* ptr = &s;Person& ref = s;

这不支持private继承和protected继承的子类与父类之间的赋值,是因为存在权限的改变

例如:

class Person{public:string _name;int _age;char _sex;void Print(){cout << "_name  _age  _sex" << endl;}};class Student :private Person // 继承方式为private{    // 那么在Student中父类的成员都是私有的protected:string _id;};int main(){Person p;Student s;p = s;     // 将子类成员赋值给父类时,子类成员与Person* ptr = &s; // 父类成员的访问限定符不一样,所以无法赋值    Person& ref = s;  // protected继承同理return 0;}

4. 继承中的作用域

  1. 在继承体系中基类派生类都有独立的作用域

  2. 子类和父类中有同名成员,子类成员将屏蔽父类对同名成员的直接访问,这种情况叫隐藏,也叫重定义。(在子类成员函数中,可以使用 基类::基类成员 显示访问)

  3. 需要注意的是如果是成员函数的隐藏,只需要函数名相同就构成隐藏

  4. 注意在实际中在继承体系里面最好不要定义同名的成员

class Person{public:string _name;int _age;char _sex;void Print(){cout << "_name  _age  _sex" << endl;}}; class Student :public Person{public:void Print(){cout << "_id" << endl;}protected :string _id;};    // Student中的Print和Person中的Print不是构成重载,因为不是在同一作用域// Student中的Print和Person中的Print构成隐藏,成员函数满足函数名相同就构成隐藏int main(){Student s;s.Print(); // 子类调用自己的打印函数s.Person::Print(); // 子类调用父类的打印函数return 0;}

 

5. 子类类的默认成员函数

6个默认成员函数,默认的意思就是指我们不写,编译器会变我们自动生成一个,那么在派生类中,这几个 成员函数是如何生成的呢?

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

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

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

  4. 派生类的析构函数会在被调用完成后自动调用基类的析构函数清理基类成员。

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

  6. 派生类对象析构清理先调用派生类析构再调基类的析构

class Person{public:string _name;int _age;char _sex;Person(){cout << "Person()" << endl;}~Person(){cout << "~Person()" << endl;}}; class Student :public Person{public:    // 构造函数中,如果没有自己显式去实现,默认构造函数会先去调用父类的构造函数    // 然后对自己的成员,内置类型不处理,自定义类型调用他们自己的默认构造函数Student(){cout << "Student()" << endl;}    // 析构函数中,子类会对自己先析构,然后去调用父类的析构函数(不需要自己显式调用)~Student(){cout << "~Student()" << endl; // 子类自己先析构后,再调用父类的析构函数}    // 拷贝构造和operator=同理protected :string _id;};// 需要自己显式实现默认成员函数的情况// 1.如果父类没有默认构造函数,这里就需要自己写// 2.如果子类有资源需要释放,就需要自己写析构// 3.如果子类存在深拷贝问题,就需要自己写拷贝构造和operator=int main(){Student s;return 0;}// 需要自己写的情况class Person{public:string _name;int _age;char _sex;Person(string name, int age, char sex)// 父类没有默认构造函数 :_name(name) ,_age(age) ,_sex(sex){}}; class Student :public Person{public:Student(string name = "wt", int age = 20, char sex = '男', string id = "123"):Person(name,age,sex) // 显式调用父类的构造函数  ,_id(id)    {} Student(const Student& s)    :Person(s) // 调用父类的拷贝构造,使用子类对象切片拷贝给父类那部分成员    {    _id = s.id;   } Student& operator=(const Student& s)    { if(this != &s) {     Person::operator=(s); // 调用父类的赋值,去赋值父类的那部分成员     _id = s.id; }    } ~Student()    { //Person::~Person(); // 手动调用父类的析构函数时也需要加作用域    } // 因为C++在类中将析构函数名统一处理成destructorprotected :  // 因为子类的析构函数胡自动调用父类的,所以自己不要显式去调用,不然会导致父类被析构两次string _id;};

6. 继承与友元

友元关系不能继承,也就是说基类友元不能访问子类私有和保护成员

class Person{friend void Print(const Person& p, const Student& s); // 友元关系不能继承protected:string _name;int _age;char _sex;};class Student :public Person{protected:string _id;};void Print(const Person& p, const Student& s){cout << p._name << " " << p._age << " " << p._sex << endl;cout << s._id << endl;}int main(){Person p;Student s;Print(p, s); // 由于Print函数不是子类的友元,不能访问到子类的保护成员,这里会编译报错return 0;}

7. 静态成员

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

class Person{public:Person(){++_count;}static int _count;};int Person::_count = 0;class Student :public Person // Student调用父类构造函数,在原有的_count的值上++{};int main(){Person p;Student s;cout << Person::_count << endl; // count = 2return 0;}

 

8. 菱形继承和虚继承

8.1 菱形继承

单继承:一个子类只有一个直接父类时称这个继承关系为单继承

多继承:一个子类有两个或以上直接父类时称这个继承关系为多继承

菱形继承:菱形继承是多继承的一种特殊情况。

通过上面的图可以发现,D相当于继承了两次A,那么D中就会存在两份A的成员,从而造成数据冗余和访问A成员时存在二义性的问题。

例如:

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 = "peter"; // 这样会有二义性无法明确知道访问的是哪一个// 需要显示指定访问哪个父类的成员可以解决二义性问题,但是数据冗余问题无法解决    // 如果Person类中的数据量很大,那么数据冗余量也会很大a.Student::_name = "张三";a.Teacher::_name = "李四";    return 0;}

8.2 虚继承(virtual)

虚拟继承可以解决菱形继承的二义性和数据冗余的问题,虚继承关键字:virtual

如上面的继承关系,在Student和Teacher的继承Person时使用虚拟继承,即可解决问题。

注意:虚继承不能在其他地方去使用,只能用来解决菱形继承问题

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 = "张三";return 0;}

8.3 虚继承解决数据冗余和二义性的原理

class A{public:    int _a;};// class B : public Aclass B : virtual public A{public:    int _b;};// class C : public Aclass C : virtual public A{public:    int _c;};class D : public B, public C{public:    int _d;};int main(){    D d;    d.B::_a = 1; // B类中的_a成员    d.C::_a = 2; // C类中的_a成员    d._b = 3;    d._c = 4;    d._d = 5;    return 0;}

开始调试,打开内存窗口,可以看到对象中各成员成员的内存分布,如果不是虚继承就会是一下情况:为_a成员开辟了两份空间

下面是使用了虚继承的情况:

这里可以看出,不会再为成员_a分配两块空间,而是直接在一块空间上修改

但是发现,这里其实还多了两块空间(dc 7b b5 00)(e4 7b b5 00),他们是干什么的?

所以通过虚继承的方式就解决了菱形继承的数据冗余和二义性的问题。

9. 继承总结、继承与组合

9.1总结

1. C++语法复杂,其实多继承就是一个体现。有了多继承,就存在菱形继承,有了菱形继承就有菱形虚拟继承,底层实现就很复杂。所以一般不建议设计出多继承,一定不要设计出菱形继承。否则在复杂度及性能上都有问题。
2. 多继承可以认为是C++的缺陷之一,很多后来的面向对象语言都没有多继承。。

9.2 继承与组合 

public继承是一种is-a的关系。也就是说每个派生类对象都是一个基类对象。
组合是一种has-a的关系。假设B组合了A,每个B对象中都有一个A对象。

优先使用对象组合,而不是类继承 。

继承允许你根据基类的实现来定义派生类的实现。这种通过生成派生类的复用通常被称为白箱复用。术语“白箱”是相对可视性而言:在继承方式中,基类的内部细节对子类可见 。

继承一定程度破坏了基类的封装,基类的改变,对派生类有很大的影响。派生类和基类间的依赖关系很强,耦合度高。

对象组合是类继承之外的另一种复用选择。新的更复杂的功能可以通过组装或组合对象来获得。对象组合要求被组合的对象具有良好定义的接口。这种复用风格被称为黑箱复用,因为对象的内部细节是不可见的。对象只以“黑箱”的形式出现。 组合类之间没有很强的依赖关系,耦合度低。

优先使用对象组合有助于你保持每个类被封装。组合的耦合度低,代码维护性好。

例如:下面就是一个组合的例子,汽车中包括轮胎,在car类中定义tire类型的成员,那么它就可以访问到tire类中的所以public成员,而私有成员不能被访问到,更好的保护了封装。 

class tire{public:void Print(){cout << "tire" << endl;}private:size_t _size; // 大小};class car{public:void Print(){cout << "car" << endl;}private:string _id; // 车牌号tire _t; //轮胎};

而开始使用的Person类和Student类,学生是人这一关系,就很好的满足了继承。