前言 本篇博客我们来看一下C++里集成的概念,了解有关继承的一些基础知识 💓 个人主页:小张同学zkf ⏩ 文章专栏:C++ 若有问题 评论区见📝 🎉欢迎大家点赞👍收藏⭐文章
继承(inheritance)机制是⾯向对象程序设计使代码可以复⽤的最重要的⼿段,它允许我们在保持原有类特性的基础上进⾏扩展,增加⽅法(成员函数)和属性(成员变量),这样产⽣新的类,称⼦类。继承呈现了面向对象程序设计的层次结构,体现了由简单到复杂的认知过程。以前我们接触的函数层次的复用,继承是类设计层次的复用。
下面我们看到没有继承之前我们设计了两个类Student和Teacher,Student和Teacher都有姓名/地址/电话/年龄等成员变量,都有identity⾝份认证的成员函数,设计到两个类⾥⾯就是冗余的。当然他们也有⼀些不同的成员变量和函数,⽐如⽼师独有成员变量是职称,学⽣的独有成员变量是学号;学生的独有成员函数是学习,⽼师的独有成员函数是授课。
class Student { public : // 进⼊校园 / 图书馆 / 实验室刷⼆维码等⾝份认证 void identity () { // ... } // 学习 void study () { // ... } protected : string _name = "peter" ; // 姓名 string _address; // 地址 string _tel; // 电话 int _age = 18 ; // 年龄 int _stuid; // 学号 }; class Teacher { public : // 进⼊校园 / 图书馆 / 实验室刷⼆维码等⾝份认证 void identity () { // ... } // 授课 void teaching () { //... } protected : string _name = " 张三 " ; // 姓名 int _age = 18 ; // 年龄 string _address; // 地址 string _tel; // 电话 string _title; // 职称 }; int main () { return 0 ; }
下⾯我们公共的成员都放到Person类中,Student和teacher都继承Person,就可以复⽤这些成员,就不需要重复定义了,省去了很多麻烦。
class Person { public : // 进⼊校园 / 图书馆 / 实验室刷⼆维码等⾝份认证 void identity () { cout << "void identity()" <<_name<< endl; } protected : string _name = " 张三 " ; // 姓名 string _address; // 地址 string _tel; // 电话 int _age = 18 ; // 年龄 }; class Student : public Person { public : // 学习 void study () { // ... } protected : int _stuid; // 学号 }; class Teacher : public Person { public : // 授课 void teaching () { //... } protected : string title; // 职称 }; int main () { Student s; Teacher t; s. identity (); t. identity (); return 0 ; }
下⾯我们看到Person是⽗类,也称作基类。Student是⼦类,也称作派⽣类。(因为翻译的原因,所以既叫⽗类/⼦类,也叫⽗类/⼦类)
1. ⽗类private成员在⼦类中⽆论以什么⽅式继承都是不可⻅的。这⾥的不可⻅是指⽗类的私有成员还是被继承到了⼦类对象中,但是语法上限制⼦类对象不管在类⾥⾯还是类外⾯都不能去访问它。
2. ⽗类private成员在⼦类中是不能被访问,如果⽗类成员不想在类外直接被访问,但需要在⼦类中能访问,就定义为protected。可以看出保护成员限定符是因继承才出现的。
3. 实际上⾯的表格我们进⾏⼀下总结会发现,⽗类的私有成员在⼦类都是不可⻅。⽗类的其他成员在⼦类的访问⽅式 == Min(成员在⽗类的访问限定符,继承⽅式),public > protected > private。
4. 使⽤关键字class时默认的继承⽅式是private,使⽤struct时默认的继承⽅式是public,不过最好显⽰的写出继承⽅式。
5. 在实际运⽤中⼀般使⽤都是public继承,⼏乎很少使⽤protetced/private继承,也不提倡使⽤
protetced/private继承,因为protetced/private继承下来的成员都只能在⼦类的类⾥⾯使⽤,实际
中扩展维护性不强。
// 实例演⽰三种继承关系下⽗类成员的各类型成员访问关系的变化 class Person { public : void Print () { cout<<_name <<endl; } protected : string _name ; // 姓名 private : int _age ; // 年龄 }; //class Student : protected Person //class Student : private Person class Student : public Person { protected : int _stunum ; // 学号 };
namespace zkf { //template<class T> //class vector //{}; // stack 和 vector 的关系,既符合 is-a ,也符合 has-a template < class T > class stack : public std::vector<T> { public : void push ( const T& x) { // ⽗类是类模板时,需要指定⼀下类域, // 否则编译报错 :error C3861: “push_back”: 找不到标识符 // 因为 stack<int> 实例化时,也实例化 vector<int> 了 // 但是模版是按需实例化, push_back 等成员函数未实例化,所以找不到 vector<T>:: push_back (x); //push_back(x); } void pop () { vector<T>:: pop_back (); } const T& top () { return vector<T>:: back (); } bool empty () { return vector<T>:: empty (); } }; } int main () { bit::stack< int > st; st. push ( 1 ); st. push ( 2 ); st. push ( 3 ); while (!st. empty ()) { cout << st. top () << " " ; st. pop (); } return 0 ; }
• public继承的⼦类对象 可以赋值给 ⽗类的对象 / ⽗类的指针 / ⽗类的引⽤。这⾥有个形象的说法叫切⽚或者切割。寓意把⼦类中⽗类那部分切来赋值过去。
• ⽗类对象不能赋值给⼦类对象。
• ⽗类的指针或者引⽤可以通过强制类型转换赋值给⼦类的指针或者引⽤。但是必须是⽗类的指针是指向⼦类对象时才是安全的。这⾥⽗类如果是多态类型,可以使⽤RTTI(Run-Time Type
Information)的dynamic_cast 来进⾏识别后进⾏安全转换。(ps:这个我们后⾯类型转换章节再
单独专⻔讲解,这⾥先提⼀下)
class Person { protected : string _name; // 姓名 string _sex; // 性别 int _age; // 年龄 }; class Student : public Person { public : int _No ; // 学号 }; int main () { Student sobj ; // 1. ⼦类对象可以赋值给⽗类对象 / 指针 / 引⽤ Person pobj = sobj ; Person* pp = &sobj; Person& rp = sobj; //2. ⽗类对象不能赋值给⼦类对象,这⾥会编译报错 sobj = pobj; return 0 ; }
1. 在继承体系中⽗类和⼦类都有独⽴的作⽤域。
2. ⼦类和⽗类中有同名成员,⼦类成员将屏蔽⽗类对同名成员的直接访问,这种情况叫隐藏。(在⼦类成员函数中,可以使⽤ ⽗类::⽗类成员 显⽰访问)
3. 需要注意的是如果是成员函数的隐藏,只需要函数名相同就构成隐藏。
4. 注意在实际中在继承体系⾥⾯最好不要定义同名的成员。
// Student 的 _num 和 Person 的 _num 构成隐藏关系,可以看出这样代码虽然能跑,但是⾮常容易混淆 class Person { protected : string _name = " ⼩李⼦ " ; // 姓名 int _num = 111 ; // ⾝份证号 }; class Student : public Person { public : void Print () { cout<< " 姓名 :" <<_name<< endl; cout<< " ⾝份证号 :" <<Person::_num<< endl; cout<< " 学号 :" <<_num<<endl; } protected : int _num = 999 ; // 学号 }; int main () { Student s1; s1. Print (); return 0 ; };
6个默认成员函数,默认的意思就是指我们不写,编译器会变我们⾃动⽣成⼀个,那么在⼦类中,这⼏个成员函数是如何⽣成的呢?
1. ⼦类的构造函数必须调⽤⽗类的构造函数初始化⽗类的那⼀部分成员。如果⽗类没有默认的构造函数,则必须在⼦类构造函数的初始化列表阶段显⽰调⽤。
2. ⼦类的拷⻉构造函数必须调⽤⽗类的拷⻉构造完成⽗类的拷⻉初始化。
3. ⼦类的operator=必须要调⽤⽗类的operator=完成⽗类的复制。需要注意的是⼦类的operator=隐
藏了⽗类的operator=,所以显⽰调⽤⽗类的operator=,需要指定⽗类作⽤域
4. ⼦类的析构函数会在被调⽤完成后⾃动调⽤⽗类的析构函数清理⽗类成员。因为这样才能保证⼦类对象先清理⼦类成员再清理⽗类成员的顺序。
5. ⼦类对象初始化先调⽤⽗类构造再调⼦类构造。
6. ⼦类对象析构清理先调⽤⼦类析构再调⽗类的析构
7. 因为多态中⼀些场景析构函数需要构成重写,重写的条件之⼀是函数名相同(这个我们多态章节会讲解)。那么编译器会对析构函数名进⾏特殊处理,处理成destructor(),所以⽗类析构函数不加
virtual的情况下,⼦类析构函数和⽗类析构函数构成隐藏关系
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 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 ) { cout<< "Student& operator= (const Student& s)" << endl; if ( this != &s) { // 构成隐藏,所以需要显⽰调⽤ Person:: operator =(s); _num = s ._num; } return * this ; } ~ Student () { cout<< "~Student()" <<endl; } protected : int _num ; // 学号 }; int main () { Student s1 ( "jack" , 18 ); Student s2 (s1); Student s3 ( "rose" , 17 ); s1 = s3 ; return 0 ; }
⽅法1:⽗类的构造函数私有,⼦类的构成必须调⽤⽗类的构造函数,但是⽗类的构成函数私有化以后,⼦类看不⻅就不能调⽤了,那么⼦类就⽆法实例化出对象。
⽅法2:C++11新增了⼀个final关键字,final修改⽗类,⼦类就不能继承了。
// C++11 的⽅法 class Base final { public : void func5 () { cout << "Base::func5" << endl; } protected : int a = 1 ; private : // C++98 的⽅法 /*Base() {}*/ }; class Derive : public Base { void func4 () { cout << "Derive::func4" << endl; } protected : int b = 2 ; }; int main () 23 { Base b; Derive d; return 0 ; }
友元关系不能继承,也就是说⽗类友元不能访问⼦类私有和保护成员 。
class Student ; class Person { public : friend void Display ( const Person& p, const Student& s); protected : string _name; // 姓名 }; class Student : public Person { protected : int _stuNum; // 学号 }; void Display ( const Person& p, const Student& s) { cout << p._name << endl; cout << s._stuNum << endl; } int main () { Person p; Student s; // 编译报错: error C2248: “Student::_stuNum”: ⽆法访问 protected 成员 // 解决⽅案: Display 也变成 Student 的友元即可 Display (p, s); return 0 ; }
⽗类定义了static静态成员,则整个继承体系⾥⾯只有⼀个这样的成员。⽆论派⽣出多少个⼦类,都只有⼀个static成员实例
class Person { public : string _name; static int _count; }; int Person::_count = 0 ; class Student : public Person { protected : int _stuNum; }; int main () { Person p; Student s; // 这⾥的运⾏结果可以看到⾮静态成员 _name 的地址是不⼀样的 // 说明⼦类继承下来了,⽗⼦类对象各有⼀份 cout << &p._name << endl; cout << &s._name << endl; // 这⾥的运⾏结果可以看到静态成员 _count 的地址是⼀样的 // 说明⼦类和⽗类共⽤同⼀份静态成员 cout << &p._count << endl; cout << &s._count << endl; // 公有的情况下,⽗⼦类指定类域都可以访问静态成员 cout << Person::_count << endl; cout << Student::_count << endl; return 0 ; }
单继承:⼀个⼦类只有⼀个直接⽗类时称这个继承关系为单继承
多继承:⼀个⼦类有两个或以上直接⽗类时称这个继承关系为多继承,多继承对象在内存中的模型
是,先继承的⽗类在前⾯,后⾯继承的⽗类在后⾯,⼦类成员在放到最后⾯。
菱形继承:菱形继承是多继承的⼀种特殊情况。菱形继承的问题,从下⾯的对象成员模型构造,可以看出菱形继承有数据冗余和⼆义性的问题,在Assistant的对象中Person成员会有两份。⽀持多继承就 ⼀定会有菱形继承,像Java就直接不⽀持多继承,规避掉了这⾥的问题,所以实践中我们也是不建议设计出菱形继承这样的模型的。
class Person { public : string _name; // 姓名 }; class Student : public Person { protected : int _num; // 学号 11 }; class Teacher : public Person { protected : int _id; // 职⼯编号 }; class Assistant : public Student, public Teacher { protected : string _majorCourse; // 主修课程 }; int main () { // 编译报错: error C2385: 对 “_name” 的访问不明确 Assistant a; a._name = "peter" ; // 需要显⽰指定访问哪个⽗类的成员可以解决⼆义性问题,但是数据冗余问题⽆法解决 a.Student::_name = "xxx" ; a.Teacher::_name = "yyy" ; return 0 ; }
class Person { public : string _name; // 姓名 }; // 使⽤虚继承 Person 类 class Student : virtual public Person { protected : int _num; // 学号 }; // 使⽤虚继承 Person 类 class Teacher : virtual public Person { protected : int _id; // 职⼯编号 }; // 教授助理 class Assistant : public Student, public Teacher { protected : string _majorCourse; // 主修课程 }; int main () { // 使⽤虚继承,可以解决数据冗余和⼆义性 Assistant a; a._name = "peter" ; return 0 ; }
• public继承是⼀种is-a的关系。也就是说每个⼦类对象都是⼀个⽗类对象。
• 组合是⼀种has-a的关系。假设B组合了A,每个B对象中都有⼀个A对象。
• 继承允许你根据⽗类的实现来定义⼦类的实现。这种通过⽣成⼦类的复⽤通常被称为⽩箱复⽤
(white-box reuse)。术语“⽩箱”是相对可视性⽽⾔:在继承⽅式中,⽗类的内部细节对⼦类可⻅
。继承⼀定程度破坏了⽗类的封装,⽗类的改变,对⼦类有很⼤的影响。⼦类和⽗类间的依赖关系
很强,耦合度⾼。
• 对象组合是类继承之外的另⼀种复⽤选择。新的更复杂的功能可以通过组装或组合对象来获得。对象组合要求被组合的对象具有良好定义的接⼝。这种复⽤⻛格被称为⿊箱复⽤(black-box reuse), 因为对象的内部细节是不可⻅的。对象只以“⿊箱”的形式出现。 组合类之间没有很强的依赖关系,耦合度低。优先使⽤对象组合有助于你保持每个类被封装。
• 优先使⽤组合,⽽不是继承。实际尽量多去⽤组合,组合的耦合度低,代码维护性好。不过也不太那么绝对,类之间的关系就适合继承(is-a)那就⽤继承,另外要实现多态,也必须要继承。类之间的关系既适合⽤继承(is-a)也适合组合(has-a),就⽤组合。
• 很多⼈说C++语法复杂,其实多继承就是⼀个体现。有了多继承,就存在菱形继承,有了菱形继承就有菱形虚拟继承,底层实现就很复杂,性能也会有⼀些损失,所以最好不要设计出菱形继承。多继承可以认为是C++的缺陷之⼀,后来的⼀些编程语⾔都没有多继承,如Java。
结束语 C++继承的知识总结到这里,有了继承也就代表C++语言有了灵活性 OK,感谢观看!!!