C++继承模式详解
目录
- 继承
- 继承的概念
- 继承的定义
- 继承关系和访限定符
- 继承方式
- 父类和子类对象赋值转化
- 继承中的作用域
- 子类的默认成员函数
- 继承与友元
- 继承与静态成员
- 复杂的菱形继承
- 虚继承
- 继承的总结
继承
继承的概念
继承机制是面向对象程序设计使代码可以复用的最重要的手段,它允许程序员在保持原有类特性的基础上进行扩展,增加功能。继承呈现了面向对象程序设计的层次结构,体现了由简单到复杂的认知过程。
继承的定义
上面的基类也可以叫父类,派生类也可以叫子类。
继承关系和访限定符
继承方式
接下来用代码测试上面的继承方式
class Person { public : void Print () { cout<<_name <<endl; } protected : string _name = "张三" ; // 姓名 private : int _age = 18 ; // 年龄 }; class Student : public Person { protected : int _stunum = 22; // 学号 };
public继承
上面是给的缺省值来测试没写构造函数
s就继承了Person的name,age,基类中private的age在物理上继承了但在语法上但是不能访问的。
也可以调用基类的成员函数,但是不能直接访问基类中private的成员,prootected可以在派生类中访问,不能再在类外访问
protected继承
protected继承,在类外连基类的public成员函数都不能用了,只能在派生类的类里面使用。
同样基类中私有的不能访问
private继承就都是私有的了。
总结:
- 1.基类private成员在派生类中无论以什么方式继承都是不可见的。这里的不可见是指基类的私有成员还是被继承到了派生类对象中,但是语法上限制派生类对象不管在类里面还是类外面都不能去访问它。
- 2.基类private成员在派生类中是不能被访问,如果基类成员不想在类外直接被访问,但需要在派生类中能访问,就定义为protected。可以看出保护成员限定符是因继承才出现的.
- 3.基类的私有成员在子类中都是不可见的,其他成员在子类中等于权限最小的那个
- 4.class的默认继承方式是private,struct默认的继承方式是public,最好显示的写出继承方式
- 5.在实际应用一般使用public继承,很少使用protected和private。
父类和子类对象赋值转化
class Person { protected: string _name; // 姓名 string _sex; // 性别 int _age; // 年龄 }; class Student : public Person { public: int _No; // 学号 };
子类可以给父类,父类不能给子类,不仅可以是子类的对象,也可以是指针和引用
Student s; Person p; p = s; Person *ptr = &s;//子类赋给父类指针 Person &ref = s;//子类赋给父类引用
派生类对象 可以赋值给 基类的对象 / 基类的指针 / 基类的引用。这里有个形象的说法叫切片或者切割。寓意把派生类中父类那部分切来赋值过去。
基类对象不能赋值给派生类对象
基类的指针可以通过强制类型转换赋值给派生类的指针。但是必须是基类的指针是指向派生类对象时才是安全的。等到子类中的默认函数就会用到切片
继承中的作用域
class Person { protected: string _name = "法外狂徒"; // 姓名 int _num = 11; // 身份证号 }; class Student : public Person { public: void Print() { cout << " 姓名:" << _name << endl; cout << " 身份证号:" << _num << endl; cout << " 学号:" << _num << endl; } protected: int _num = 2; // 学号 };
还有成员函数的隐藏
class A { public: void fun(double x) { cout << "fun()->x"<< x << endl; } }; class B : public A { public: void fun(int i) { cout << "fun()->" << i << endl; } }; int main() { B b; b.fun(10); b.A::fun(11.1);//加作用域 return 0; }
父类和子类函数名相同不是重载而是隐藏,函数重载是在同一作用域,不同的作用域是隐藏
在子类成员函数中,可以使用 基类::基类成员 显示访问
在写代码中最好不要定义同名的成员
子类的默认成员函数
在类和对象的时候讲了6个默认的成员函数,现在子类中讲4个,构造,拷贝构造,赋值和析构
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) { 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; //学号 };
总结:
- 派生类的构造函数必须调用基类的构造函数初始化基类的那一部分成员。如果基类没有默认的构造函数,则必须在派生类构造函数的初始化列表阶段显示调用。
- 派生类的拷贝构造函数必须调用基类的拷贝构造完成基类的拷贝初始化。
- 派生类的operator=必须要调用基类的operator=完成基类的复制。
- 派生类的析构函数会在被调用完成后自动调用基类的析构函数清理基类成员。因为这样才能保证派生类对象先清理派生类成员再清理基类成员的顺序。
- 派生类对象初始化先调用基类构造再调派生类构造。
- 派生类对象析构清理先调用派生类析构再调基类的析构。
继承与友元
友元关系不能继承,父类友元不能访问子类私有和保护成员
class Student; class Person { public: friend void Display(const Person& p, const Student& s); protected: string _name; // 姓名 }; class Student : public Person { public: 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;//要在子类中加上友元才能访问,不加会报错 } int main() { Person p; Student s; Display(p, s); return 0; }
继承与静态成员
基类定义了static静态成员,则整个继承体系里面只有一个这样的成员。无论派生出多少个子类,都只有一个static成员实例 。
class Person { public: Person() { ++_count; } protected: string _name; // 姓名 public: static int _count; // 统计人的个数。 }; int Person::_count = 0; class Student : public Person { protected: int _id; // 学号 }; class Graduate : public Student { protected: string _Course; // 科目 }; int main() { Student s1; Student s2; Student s3; Graduate s4; cout << " 人数 :" << Person::_count << endl; cout << " 人数 :" << Student::_count << endl; cout << " 人数 :" << &Person::_count << endl; cout << " 人数 :" << &Student::_count << endl; return 0; }
再加上count的地址可以看出是同一个的count。计算出子类实例化了多少个对象就可以在父类中定义个count自加。
复杂的菱形继承
单继承:一个子类只有一个直接父类时称这个继承关系为单继承
多继承:一个子类有两个或以上直接父类时称这个继承关系为多继承
菱形继承:菱形继承是多继承的一种特殊情况。
菱形继承的问题:从下面的对象成员模型构造,可以看出菱形继承有数据冗余和二义性的问题。在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 _Course; // 课程 }; int main() { // 这样会有二义性无法明确知道访问的是哪一个 Assistant a; //a._name = "peter"; // 显示的调用解决了二义性,但数据冗余了 a.Student::_name = "盖伦"; a.Teacher::_name = "亚索"; return 0; }
虚继承
虚拟继承可以解决菱形继承的二义性和数据冗余的问题。如上面的继承关系,在Student和Teacher的继承Person时使用虚拟继承,即可解决问题。需要注意的是,虚拟继承不要在其他地方去使用。
在菱形的腰部加上virtual关键字可以解决冗余。那虚继承是怎么解决的呢?先来看看不用虚继承的
class A { public: int _a; }; class B : public A { public: int _b; }; class C : 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; }
我们可以通过内存窗口来观察对象成员的模型
菱形继承带来了二义性和数据冗余。
再来看看虚继承的
虚继承就解决了数据冗余和二义性,B和C中多了地址,在用内存窗口看看这里的地址
2个指针叫虚基表指针指向虚基表,可以通过偏移量找到公共虚基类,此时A是在下面那为什么要找呢?
D d; d.B::_a = 1; d.C::_a = 2; d._b = 3; d._c = 4; d._d = 5; B b = d;//把d赋给b,把d切过去那此时要怎么找到A呢?,所以就要用虚基表找
B类中各个成员在内存中的分布:
通过偏移量找到虚基类。
还是不要用菱形继承出现问题,虚继承使得对象模型很复杂,并且会有效率的影响。
继承的总结
- C++语法复杂,其实多继承就是一个体现。有了多继承,就存在菱形继承,有了菱形继承就有菱形虚拟继承,底层实现就很复杂。所以一般不建议设计出多继承,一定不要设计出菱形继承。否则在复杂度及性能上都有问题。
- 多继承可以认为是C++的缺陷之一.
组合
继承是建立了父类与子类的关系,是一种“是”的关系,例如白猫是猫,组合是“有”的关系实际尽量多去用组合。组合的耦合度低,代码维护性好。不过继承也有用武之地的,有些关系就适合继承那就用继承,另外要实现多态,也必须要继承。类之间的关系可以用继承,可以用组合就用组合。
面试题
- 什么是菱形继承?菱形继承的问题是什么?
菱形继承是多继承的一种特殊继承,两个子类继承同一个父类,而又有子类同时继承这两个子类。可以看出菱形继承有数据冗余和二义性的问题。- 什么是菱形虚拟继承?如何解决数据冗余和二义性的
在菱形继承的腰部加上virtual,通过虚基表指针和虚基表中的偏移量可以找到虚基类,只存1份- 继承和组合的区别?什么时候用继承?什么时候用组合?
继承是一种"是",组合是"有"的关系,父类和子类是的关系用继承,是有的关系用组合。
以上就是C++继承,由于作者水平有限,如有问题还请指出!
到此这篇关于C++继承模式详解的文章就介绍到这了。希望对大家的学习有所帮助,也希望大家多多支持我们。