继承的概念及定义
继承概念
继承(inheritance)机制是面向对象程序设计使代码可以复用的最重要的手段,它允许程序员在保 持原有类特性的基础上进行扩展,增加功能,这样产生新的类,称派生类。继承呈现了面向对象 程序设计的层次结构,体现了由简单到复杂的认知过程。以前我们接触的复用都是函数复用,继承是类设计层次的复用
继承定义
定义格式
继承后,父类的成员,包括成员函数和成员变量,都会变成子类的一部分。
默认继承方式
在使用继承时,可以不指定继承方式,使用关键字class时默认的继承方式是private,使用struct时,默认的继承方式是public
继承关系和访问限定符
访问限定符:
继承关系
私有成员的意义:不想被子类继承的成员,可以设计成私有
基类中想给子类复用,但是又不想暴露直接访问的成员,就应该定义成保护
继承基类成员访问方式的变化
类成员/继承方式 |
public继承 |
protected继承 |
private继承 |
基类的public成员 |
派生类的public成员 |
派生类的protected成员 |
派生类的private成员 |
基类的protected成员 |
派生类的protected成员 |
派生类的protected成员 |
派生类的private成员 |
基类的private成员 |
在派生类不可见 |
在派生类不可见 |
在派生类不可见 |
:::info
取成员限定符和继承方式的较小
:::
protected/private 类外边不能访问 类里面可以访问
不可见 隐身,类里面外面都无法访问
总结
- 基类private成员在派生类中无论以什么方式继承都是不可见的。这里的不可见是指基类的私
有成员还是被继承到了派生类对象中,但是语法上限制派生类对象不管在类里面还是类外面
都不能去访问它。
- 基类private成员在派生类中是不能被访问,如果基类成员不想在类外直接被访问,但需要在
派生类中能访问,就定义为protected。可以看出保护成员限定符是因继承才出现的。
- 实际上面的表格我们进行一下总结会发现,基类的私有成员在子类都是不可见。基类的其他
成员在子类的访问方式 == Min(成员在基类的访问限定符,继承方式),public > protected>private。
- 使用关键字class时默认的继承方式是private,使用struct时默认的继承方式是public,不过
最好显示的写出继承方式。
- 在实际运用中一般使用都是public继承,几乎很少使用protetced/private继承,也不提倡
使用protetced/private继承,因为protetced/private继承下来的成员都只能在派生类的类里
面使用,实际中扩展维护性不强
基和派生类对象赋值转换
- 派生类对象 可以赋值给 基类的对象 / 基类的指针 / 基类的引用。这里有个形象的说法叫切片
或者切割。寓意把派生类中父类那部分切来赋值过去。
- 基类对象不能赋值给派生类对象。
- 基类的指针或者引用可以通过强制类型转换赋值给派生类的指针或者引用。但是必须是基类
的指针是指向派生类对象时才是安全的。这里基类如果是多态类型,可以使用RTTI(RunTime Type Information)的dynamic_cast 来进行识别后进行安全转换。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32
| class Person { protected: string _name; string _sex; int _age; };
class Student : public Person { public: int _No; };
int main() { Student sobj;
Person pobj = sobj; Person* pp = &sobj; Person& rp = sobj;
int i = 0; const double& d = i;
return 0; }
|
注意:基类的对象不能赋值给派生类对象,基类的指针可以通过强制类型转换赋值给派生类的指针,但是此时基类的指针必须是指向派生类的对象才是安全的。
继承中的作用域
- 在继承体系中,基类和派生类都有独立的作用域
- 子类和父类中有同名成员,子类成员将屏蔽父类对同名成员的直接访问,这种情况叫隐藏,也叫重定义。(在子类成员函数中,可以使用 基类::基类成员 显示访问)
- 注意在实际中在继承体系里最好不要定义同名的成员
- 需要注意的是如果是成员函数的隐藏,只需要函数名相同就构成隐藏
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34
| class Person { public: string _name; int _num; void fun() { cout << "parent" << endl; } };
class Student : public Person { public: int _num; void fun() { cout << "child" << endl; } };
int main() { Student s; s._num = 1; cout << s._num << endl; s.Person::_num = 2; cout << s.Person::_num << endl;
s.fun(); s.Person::fun();
return 0; }
|
派生类的默认成员函数
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103
| class Person { public: Person(const char* name) : _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 num) :Person(name) , _num(num) { cout << "Student()" << endl; }
Student(const Student& s) :Person(s) , _num(s._num) { cout << "Student(const Student& s)" << endl; }
Student& operator=(const Student& s) { if (this != &s) { Person::operator=(s);
_num = s._num; }
cout << "Student& operator=(const Student& s)" << endl;
return *this; }
~Student() {
cout << "~Student()" << endl; }
protected: int _num;
};
int main() { Student s1("张三", 1);
Student s2 = s1;
Student s3("李四", 2);
s1 = s3;
cout << &s1 << endl;
return 0; }
|
构造时,先调用父类的构造,再调用子类的,析构时先析构子,再析构父类
子类默认生成的构造函数
- 自己的成员,跟类和对象一样,内置类型不处理,自定义类型调用他的默认构造
- 继承父类成员必须调用父类构造函数初始化
编译器默认生成的析构函数
- 自己的成员, 内置类型不处理,自定义类型调用它的析构
- 继承的成员,调用父类析构函数处理
编译生成的默认拷贝构造
- 自己成员,跟类和对象一样(内置类型,值拷贝,自定义类型调用它的拷贝构造)
- 继承的父类成员,必须调用父类拷贝构造初始化
编译器默认生成的operator=
同上
总结
- 派生类的构造函数调用时,会自动调用基类的构造函数初始化基类的一部分成员,如果基类中没有默认构造函数则必须在派生类构造函数的初始化列表中显示调用基类的构造函数
- 派生类的拷贝构造函数必须调用基类的构造函数完成基类成员的拷贝构造
- 派生类的赋值运算符重载函数必须调用基类的赋值运算符重载函数完成基类成员的赋值
- 派生类的析构函数会会在被调用完成后自动调用基类的析构函数清理基类成员
- 派生对象初始化时,会先调用基类的构造函数,再调用派生类的构造函数
另外几点注意
- 派生类和基类的赋值运算符重载函数因为函数名相同构造隐藏,因此在派生类当中,调用基类的赋值运算符重载函数时,需要使用作用域限定符指定调用
- 由于多态的某些原因,任何类的析构函数名都被统一处理为destructor(),因此,派生类和基类的析构函数就会因为函数名相同构成隐藏,如若我们需要在某处显示调用基类的析构函数,那么就要使用作用域限定符指定调用
- 在派生类的拷贝构造函数和operator=当中调用基类的拷贝构造函数,传参方式都是切片行为,都是将派生类对象直接赋值给基类的引用
- 基类的构造、拷贝构造、赋值我们都可以在派生类中自行调用,但是基类析构是当派生类析构函数调用后由编译器自动调用的,如果我们显示调用基类析构函数,会导致基类被析构多次
- 创建派生类对象时是先创建基类成员再创建派生类成员,编译器为了保证先定义的后析构,所以自动调用基类的析构函数
继承与友元
友元关系不能继承,基类的友元函数,不能访问派生类的私有和保护
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28
| class Student; class Person { public: friend void Display(const Person& p, const Student& s); protected: string _name; };
class Student : public Person { friend void Display(const Person& p, const Student& s); protected: int _stuNum; };
void Display(const Person& p, const Student& s) { cout << p._name << endl; cout << s._stuNum << endl; }
void main() { Person p; Student s; Display(p, s); }
|
继承与静态成员
若基类中定义了一个static静态成员变量,那么在整个继承体系中只有一个该静态成员,无论派生出多少个子类,都只有一个static成员实例
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36
| class Person { public: Person() { ++_count; }
string _name; public: static int _count; };
int Person::_count = 0;
class Student : public Person { protected: int _stuNum; };
int main() { Person p; Student s;
p._name = "张三"; cout << s._name << endl;
cout << Student::_count << endl; ++Person::_count; cout << Student::_count << endl;
cout << &Person::_count << endl; cout << &Student::_count << endl;
return 0; }
|
代码说明:
- 子类 count++ 父类 也会++
- 两个count 地址相同
复杂的菱形继承及菱形虚拟继承
如何定义一个不能被继承的类
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27
| class A final { private: A() {}
protected: int _a; };
class B : public A {
};
int main() {
return 0; }
|
继承方式
单继承:一个子类只有一个直接父类
多继承:一个子类有两个或两个以上直接父类
菱形继承:多继承的一种特殊情况
菱形继承的方式存在数据冗余和二义性的问题
一个题
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| class Base1 { public: int _b1; }; class Base2 { public: int _b2; }; class Derive : public Base1, public Base2 { public: int _d; };
int main() { Derive d; Base1* p1 = &d; Base2* p2 = &d; Derive* p3 = &d;
d._b1 = 1; d._b2 = 2; d._d = 3;
cout << p1 << endl; cout << p2 << endl; cout << p3 << endl;
return 0; }
|
P1 P2 P3的关系: “先使用了低地址”
p1==p3<p2
解释:
该类对象模型:
&d 看到内存信息,01地址最低,也就是_b1的地址最低,base1是先继承的,所以地址最低,我们传统的想法是栈是向下增长,先使用高地址再使用低地址,但是在这个对象里成员就像倒着存一样
菱形继承的问题
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39
| 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 at;
at.Student::_name = "张三"; at.Teacher::_name = "李四"; return 0; }
|
二义性可以通过指定类域解决,但是数据冗余的问题还是无法解决,Assistant这个类中有两份的_name.
这时候需要用菱形虚拟继承,用法就是在继承方式前边加一个virtual,在菱形继承的腰部加virtual
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38
| 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 at;
at._name = "小张"; at.Student::_name = "张三"; at.Teacher::_name = "李四";
return 0; }
|
这里解决了数据冗余的问题,因为_name都是一个
虚拟继承的对象模型
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44
| 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; d.B::_a = 1; d.C::_a = 2; d._a = 0; d._b = 3; d._c = 4; d._d = 5;
B b = d; B* pb = &d;
C c = d; C* pc = &d;
return 0; }
|
有虚拟继承
我们来看内存的具体信息
虚拟继承后类对象模型变成了如下
对象里面有了两个指针,叫做虚基表指针,他们分别指向一个虚基表,虚基表中包含两个数据,第一个是为多态的虚表预留的存偏移量的位置,第二个数据就是当前类对象位置距离公共虚基类的偏移量
如果没有虚拟继承
这里有个问题,好像使用虚拟继承和不用虚拟继承空间是消耗了相同的,但是,如果A的_a是个大对象,就会省空间。
注:如果,将D类对象,赋值给B类对象,在这个切片过程中,就需要通过虚基表中的第二个数据找到公共虚基类A的成员,得到切片后,该B类对象在内存中仍然保持这种分布。
继承的总结和反思
所以一般不建议设计出多继承,一定不要设计出菱形继承。否则在复杂度及性能上都有问题。多继承可以认为是C++的缺陷之一,很多后来的OO语言都没有多继承,如Java。
继承和组合
- public继承是一种is-a的关系。也就是说每个派生类对象都是一个基类对象。
- 组合是一种has-a的关系。假设B组合了A,每个B对象中都有一个A对象。
继承允许你根据基类的实现来定义派生类的实现。这种通过生成派生类的复用通常被称
为白箱复用(white-box reuse)。术语“白箱”是相对可视性而言:在继承方式中,基类的
内部细节对子类可见 。继承一定程度破坏了基类的封装,基类的改变,对派生类有很
大的影响。派生类和基类间的依赖关系很强,耦合度高。
对象组合是类继承之外的另一种复用选择。新的更复杂的功能可以通过组装或组合对象
来获得。对象组合要求被组合的对象具有良好定义的接口。这种复用风格被称为黑箱复
用(black-box reuse),因为对象的内部细节是不可见的。对象只以“黑箱”的形式出现。
组合类之间没有很强的依赖关系,耦合度低。优先使用对象组合有助于你保持每个类被
封装。
实际尽量多去用组合。组合的耦合度低,代码维护性好。不过继承也有用武之地的,有
些关系就适合继承那就用继承,另外要实现多态,也必须要继承。类之间的关系可以用
继承,可以用组合,就用组合