继承的概念
继承简单明白就是把公共的部门提取出来,作为父类(基类),不同的部门作为子类(派生类),以达到公共部门复用,子类继承父类的目的。
通过下图明白继承
上图中,门生,老师,宿管,保安都具有一些相同的属性,和其他不同的属性,就可以将相同的属性提取出来,作为父类,剩下不同的属性作为子类,构成一个新类,子类继承父类去拥有那些相同的属性。
专业点说就是:
继承(inheritance) 机制是面向对象程序计划使代码可以复用的最紧张的本领,它允许程序员在保持原有类特性的底子上举行扩展,增加功能,这样产生新的类,称派生类。继承出现了面向对象程序计划的条理结构,体现了由简单到复杂的认知过程。以前我们打仗的复用都是函数复用,继承是类计划条理的复用。
继承界说
界说格式
Person是父类,也称作基类。Student是子类,也称作派生类。
继承关系和访问限定符
类成员/继承方式public继承protected继承private继承基类的public成员派生类的public成员派生类的protected成员派生类的private成员基类的protected成员派生类的protected成员派生类的protected成员派生类的private成员基类的private成员在派生类中不可见在派生类中不可见在派生类中不可见 总结:
1.基类private成员在派生类中无论以什么方式继承都是不可见的。这里的不可见是指基类的私有成员照旧被继承到了派生类对象中,但是语法上限制派生类对象不管在类内里照旧类外面都不能去访问它。
2.基类private成员在派生类中是不能被访问,假如基类成员不想在类外直接被访问,但必要在派生类中能访问,就界说为protected。可以看出掩护成员限定符是因继承才出现的。若没有继承,则掩护和私有没有区别。
3.实际上面的表格我们举行一下总结会发现,基类的私有成员在子类都是不可见。基类的其他成员在子类的访问方式 == Min(成员在基类的访问限定符,继承方式),public > protected > private。
4.使用关键字class时默认的继承方式是private,使用struct时默认的继承方式是public,不过
最好显示的写出继承方式。
5.在实际运用中一样平常使用都是public继承,几乎很少使用protetced/private继承,也不提倡使用protetced/private继承,因为protetced/private继承下来的成员都只能在派生类的类内里使用,实际中扩展维护性不强。
public继承:
protected继承
private继承
继承的构造函数次序
在C++中,创建一个派生类会同时创建一个基类。换句话说,派生类包罗了基类的全部成员(属性和方法),在它的内存结构中也会有基类的部门。这是继承的基本特性之一。
内存结构:
当你创建一个派生类的对象时,系统会为该对象分配足够的内存,以存储基类的部门以及派生类自己的成员。这意味着每个派生类实例都会包罗一个基类的实例。
访问基类成员:
在派生类中,你可以直接访问基类的公有成员(public)和掩护成员(protected)。假如基类有构造函数,在派生类的构造函数中通常会先调用基类的构造函数,以确保基类的部门被正确初始化。
- #include <iostream>
- using namespace std;
- class Base {
- public:
- Base() { cout << "Base constructor called" << endl; }
- void display() { cout << "Display Base" << endl; }
- };
- class Derived : public Base {
- public:
- Derived() { cout << "Derived constructor called" << endl; }
- void show() { cout << "Show Derived" << endl; }
- };
- int main() {
- Derived d; // 创建一个 Derived 对象
- d.display(); // 调用基类的成员函数
- d.show(); // 调用派生类的成员函数
- return 0;
- }
复制代码 输出:
- Base constructor called
- Derived constructor called
- Display Base
- Show Derived
复制代码 基类和派生类对象赋值转换
- 派生类对象 可以赋值给 基类的对象 / 基类的指针 / 基类的引用。这里有个形象的说法叫切片大概切割。寓意把派生类中父类那部门切来赋值过去。
- 基类对象不能赋值给派生类对象。
- class Person
- {
- protected:
- string _name; // 姓名
- string _sex; // 性别
- int _age;
- // 年龄
- };
- class Student : public Person
- {
- public:
- int _No; // 学号
- };
- int main()
- {
- Student s;
- Person p = s;//赋值兼容
- Person* pp = &s;//指针,指向子类中的父类那一部分
- Person& rp = s;//引用,相当于子类中父类那一部分别名
- return 0;
- }
复制代码
注意,必须是public继承才行,protect和private继承都无法举行切片
切割赋值的行为和前面学习的类的赋值行为是一样的,不论是赋值照旧拷贝都是一样的行为。
继承中的作用域
- 在继承体系中基类和派生类都有独立的作用域。
- 子类和父类中有同名成员,子类成员将屏蔽父类对同名成员的直接访问,这种情况叫隐蔽,也叫重界说。(在子类成员函数中,可以使用 基类::基类成员 显示访问)
- 必要注意的是假如是成员函数的隐蔽,只必要函数名相同就构成隐蔽。
- 注意在实际中在继承体系内里最好不要界说同名的成员。
父类和子类拥有独立的作用域,访问时就近原则,访问自己的成员,如下面的_num.
- class Person
- {
- protected:
- string _name; // 姓名
- string _sex; // 性别
- int _num = 0;
- };
- class Student : public Person
- {
- public:
- void func()
- {
- cout << _num << endl;
- cout << Person::_num << endl;//若要访问父类的成员就要加域作用限定符
- }
- protected:
- int _num = 1; // 学号
- };
- int main()
- {
- Student s;
- s.func();//输出1 0
- return 0;
- }
复制代码 父类成员函数的隐蔽:
- class Person
- {
- public:
- void func()
- {
- cout << "Person:func()" << endl;
- }
- protected:
- string _name; // 姓名
- string _sex; // 性别
- int _num = 0;
- };
- class Student : public Person
- {
- public:
- void func()
- {
- cout << "Student:func()" << endl;
- }
- protected:
- int _num = 1; // 学号
- };
- int main()
- {
- Student s;
- s.func();
- s.Person::func();
- return 0;
- }
复制代码 上述代码父类的func()函数被隐蔽,函数名相同就会被隐蔽,即使参数不同也不行。
加入将上述的父类的func()改为void func(int i),我们使用使用s.func(1),就会报错,因为父类的函数被隐蔽,无法找到。
- class Person
- {
- public:
- void func(int i)
- {
- cout << "Person:func()" << endl;
- }
- };
- class Student : public Person
- {
- public:
- void func()
- {
- cout << "Student:func()" << endl;
- }
- };
- int main()
- {
- Student s;
- s.func(1);//报错
- s.Person::func();
- return 0;
- }
复制代码 派生类的默认成员函数
1.派生类的构造函数必须调用基类的构造函数初始化基类的那一部门成员。假如基类没有默认
的构造函数,则必须在派生类构造函数的初始化列表阶段显示调用。
- class Person
- {
- public:
- Person(const string& s)
- {
- _name = s;
- }
- private:
- string _name;
- };
- class Student : public Person
- {
- public:
- Student(const string& s)
- :Person(s)
- {
- }
- private:
- int _num = 0;
- };
- int main()
- {
- Student s("Peter");
- return 0;
- }
复制代码 上述代码,Person中没有默认的构造,就要在派生类内里显示的在初始化列表调用。
2.派生类的拷贝构造函数必须调用基类的拷贝构造完成基类的拷贝初始化。
- class Person
- {
- public:
- Person(const string& s)
- {
- _name = s;
- }
- private:
- string _name;
- };
- class Student : public Person
- {
- public:
- Student(const string& s)
- :Person(s)
- {
-
- }
- Student(const Student& s)
- :Person(s)
- {
- }
- private:
- string str;
- int _num = 0;
- };
- int main()
- {
- Student s("Peter");
- return 0;
- }
复制代码 子类在调用拷贝构造的过程中,使用了初始化列表,这里也是切片的使用
3.派生类的operator=必须要调用基类的operator=完成基类的复制。
- class Person
- {
- public:
- Person(const string& s)
- {
- _name = s;
- }
- void operator=(const Person& s)
- {
- if (this != &s)
- {
- operator=(s);
- _name = s._name;
- }
- }
- private:
- string _name;
- };
- class Student : public Person
- {
- public:
- Student(const string& s)
- :Person(s)
- {
-
- }
- void operator=(const Student& s)
- {
- if (this != &s)
- {
- Person::operator=(s);
- str = s.str;
- _num = s._num;
- }
- }
- private:
- string str;
- int _num = 0;
- };
- int main()
- {
- Student s("Peter");
- Student s1("LiHua");
- s1 = s;
- return 0;
- }
复制代码 注意必须要加域作用限定符,不然会栈溢出,因为Person和Student中都有赋值运算符重载,会构成隐蔽,不加的话会不停递归指向Student的赋值运算符重载函数。
4.派生类的析构函数会在被调用完成后自动调用基类的析构函数清理基类成员。因为这样才华包管派生类对象先清理派生类成员再清理基类成员的次序。
在子类中显示调用析构函数,也会堕落,因为父类的析构被隐蔽,为什么名字不一样也会被隐蔽呢,因为后续多态的必要,析构函数被同意处理成destructor。以是假如要显示调用仍然要加域作用限定符。
但是不发起显示写,因为编译器默认的是先析构子在析构父,假如我们手动调用了,先析构了父,那么子的析构中访问了父的成员就会堕落,以是应该先析构子,避免子类成员对父类举行访问,这样可以避免堕落。
继承与友元
友元关系不能继承,也就是说基类友元不能访问子类私有和掩护成员
继承与静态成员
基类界说了static静态成员,则整个继承体系内里只有一个这样的成员。 无论派生出多少个子类,都只有一个static成员实例 。静态成员属于当前类,也属于当前类的全部派生类。
菱形继承及菱形虚拟继承
单继承: 一个子类只有一个直接父类时称这个继承关系为单继承
多继承: 一个子类有两个或以上直接父类时称这个继承关系为多继承
存在多继承就必然存在菱形继承
菱形继承: 菱形继承是多继承的一种特殊情况。
菱形继承的题目: 从下面的对象成员模型构造,可以看出菱形继承有数据冗余和二义性的题目。在Assistant的对象中Person成员会有两份。
- 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; // 主修课程
- };
- void Test()
- {
- // 这样会有二义性无法明确知道访问的是哪一个
- Assistant a;
- a._name = "peter";//报错,存在二义性,编译器不知道_name是哪一个
- // 需要显示指定访问哪个父类的成员可以解决二义性问题,但是数据冗余问题无法解决
- a.Student::_name = "xxx";
- a.Teacher::_name = "yyy";
- }
复制代码 虚拟继承可以办理菱形继承的二义性和数据冗余的题目。如上面的继承关系,在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; // 主修课程
- };
- void Test()
- {
- Assistant a;
- a._name = "peter";
- a.Student::_name = "xxx";
- a.Teacher::_name = "yyy";
- }
- int main()
- {
- Test();
- return 0;
- }
复制代码
可以发现当我们修改了a的_name时,全部的_name都被修改了,说明那是同一个_name。
虚拟继承办理数据冗余和二义性的原理
为了研究虚拟继承原理,我们给出了一个简化的菱形继承 继承体系,再借助内存窗口观察对象成员的模型。
- class A
- {
- public:
- int _a;
- };
- //class B : public A
- class B : virtual public A//加virtual关键字就代表虚继承
- {
- public:
- int _b;
- };
- // class C : public A
- class C : virtual public A
- {
- public:
- int _c;
- };
- class D : public B, public C
- {
- public:
- int _d;
- };
- int main()
- {
- D d;
- d.B::_a = 1;
- d.C::_a = 2;
- d._b = 3;
- d._c = 4;
- d._d = 5;
- return 0;
- }
复制代码
可以发现,菱形继承每个父类都有一个a,但是菱形虚拟继承整体就只有一个a,放在了公共地区。也可以发现,菱形虚拟继承还分别存在一个不知道的什么数字,因为是小端存储,B中的是0x0038ae1c,C中的是0x0038ae24
找到这个地址会发现,
地址处存的是0,但是下一个位置分别是20和12.而20和12刚好就是B和C的首地址和a的地址的差值。
于是得出结论,B和C处存的是一个地址,而这个地址处存的是对象到a的偏移量
这样存储的方案主要是为了切片的时候方便,当子类赋值给父类的时候,只切割B那一块显然是不符合的,因为a在其他地方,必要通过这个偏移量去探求a。
说的专业点就是
这里可以分析出D对象中将A放到的了对象构成的最下面,这个A同时属于B和C,那么B和C怎样去找到公共的A呢?这里是通过了B和C的两个指针,指向的一张表。这两个指针叫虚基表指针,这两个表叫虚基表。虚基表中存的偏移量。通过偏移量可以找到下面的A。
在取地址的时候,B范例的指针和C范例的指针也会发生偏移,指向自己的地址。
- int main()
- {
- D d;
- B* pb = &d;
- C* pc = &d;
- return 0;
- }
复制代码
下面是上面的Person关系菱形虚拟继承的原明白释:
总结: 实践中可以计划多继承但是不要计划菱形继承,因为太复杂容易出各种题目。
实践中也不常见菱形继承,但是库中确实有一个菱形继承,istream和ostream都继承了ios,iostream又继承了istream和ostream。
值得知道的是,当举行虚继承时,并不是只要D中的a变量是公共的,单独创建一个B大概C都会有虚基表指针。本质就是将模型保持一致,在举行剪切的时候规则相同。
继承的总结和反思
- 很多人说C++语法复杂,实在多继承就是一个体现。有了多继承,就存在菱形继承,有了菱形继承就有菱形虚拟继承,底层实现就很复杂。以是一样平常不发起计划出多继承,一定不要计划出菱形继承。否则在复杂度及性能上都有题目。
- 多继承可以以为是C++的缺陷之一,很多后来的OO语言都没有多继承,如Java。
- 继承和组合
public继承是一种is-a的关系。也就是说每个派生类对象都是一个基类对象。
组合是一种has-a的关系。假设B组合了A,每个B对象中都有一个A对象。
优先使用对象组合,而不是类继承
继承允许你根据基类的实现来界说派生类的实现。这种通过生成派生类的复用通常被称为白箱复用(white-box reuse)。术语“白箱”是相对可视性而言:在继承方式中,基类的内部细节对子类可见 。继承一定程度破坏了基类的封装,基类的改变,对派生类有很大的影响。派生类和基类间的依赖关系很强,耦合度高。
对象组合是类继承之外的另一种复用选择。新的更复杂的功能可以通过组装或组合对象来得到。对象组合要求被组合的对象具有良好界说的接口。这种复用风格被称为黑箱复用(black-box reuse),因为对象的内部细节是不可见的。对象只以“黑箱”的形式出现。 组合类之间没有很强的依赖关系,耦合度低。优先使用对象组合有助于你保持每个类被封装。
实际只管多去用组合。组合的耦合度低,代码维护性好。不过继承也有用武之地的,有些关系就得当继承那就用继承,另外要实现多态,也必须要继承。类之间的关系可以用继承,可以用组合,就用组合。
简单来说,is-a就用继承,has-a就用组合,二者都得当就使用组合。
免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!更多信息从访问主页:qidao123.com:ToB企服之家,中国第一个企服评测及商务社交产业平台。 |