目录
- 继承
- 基类和派生类对象赋值转换
- 继承中的作用域
- 派生类的默认成员函数
- 静态成员、友元与继承
- 菱形继承和菱形虚拟继承
- 虚拟继承原理
- 继承和组合
- 习题练习
继承
继承机制是面向对象程序设计使代码可以复用的最重要的手段,它允许程序员在保持原有类特性的基础上进行扩展,增加功能,这样产生新的类,称派生类。继承呈现了面向对象程序设计的层次结构,体现了由简单到复杂的认知过程。以前我们接触的复用都是函数复用,继承是类设计层次的复用
定义格式
Person是父类,也称作基类。Student是子类,也称作派生类
继承基类成员
- 基类private成员在派生类中无论以什么方式继承都是不可见的。这里的不可见是指基类的私有成员还是被继承到了派生类对象中,但是语法上限制派生类对象不管在类里面还是类外面都不能去访问它。
- 基类private成员在派生类中是不能被访问,如果基类成员不想在类外直接被访问,但需要在派生类中能访问,就定义为protected。可以看出保护成员限定符是因继承才出现的。
- 实际上面的表格我们进行一下总结会发现,基类的私有成员在子类都是不可见。基类的其他成员在子类的访问方式 == Min(成员在基类的访问限定符,继承方式),public > protected > private。
- 使用关键字class时默认的继承方式是private,使用struct时默认的继承方式是public,不过最好显示的写出继承方式。
- 在实际运用中一般使用都是public继承,几乎很少使用protetced/private继承,也不提倡使用protetced/private继承,因为protetced/private继承下来的成员都只能在派生类的类里面使用,实际中扩展维护性不强
基类和派生类对象赋值转换
不同类型的对象赋值时,相近类型可以隐式类型转换(产生临时变量)
int和int* 就是相近类型,int表示数据大小,int* 是地址,本质是一个编号
r引用的不是d,而是中间的临时变量,临时变量有常性,所以要加const
基类对象不能赋值给派生类对象。
继承中的作用域
- 在继承体系中基类和派生类都有独立的作用域。
- 子类和父类中有同名成员,子类成员将屏蔽父类对同名成员的直接访问,这种情况叫隐藏,也叫重定义。(在子类成员函数中,可以使用 基类::基类成员 显示访问)
- 需要注意的是如果是成员函数的隐藏,只需要函数名相同就构成隐藏。
- 注意在实际中在继承体系里面最好不要定义同名的成员。
派生类的默认成员函数
class Person
{
public:Person(const char* name = "peter"): _name(name){cout << "Person()" << endl;}
protected:string _name; // 姓名
};class Student : public Person
{
public:Student(const char* name, int id):_id(id), Person(name){cout << "Student(const char* name, int id)" << endl;}
protected:int _id;
}
要把父类当成一个完整的对象初始化,而不是单个成员初始化(否则会编译报错)。要初始化父类就要显示调用父类的构造函数,不显示调用也会自动调用父类的默认构造函数,自动调用时:父类如果没有默认构造就会报错。
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 id):_id(id)//, Person(name){cout << "Student(const char* name, int id)" << endl;}Student(const Student& s):Person(s), _id(s._id){cout << "Student(const Student& s)" << endl;}Student& operator=(const Student& s){if (&s != this){Person::operator=(s);_id = s._id;}cout << "Student& operator=(const Student& s)" << endl;return *this;}~Student(){//Person::~Person();cout << "~Student()" << endl;}protected:int _id;
};int main()
{Student s1("张三", 18);Student s2(s1);Student s3("李四", 19);s1 = s3;return 0;
}
由于多态的原因,析构函数统一会被处理成destructor
父子类的析构函数构成隐藏
为了保证析构安全,先子后父
父类析构函数不需要显示调用,子类析构函数结束时会自动调用父类析构,不要显示调用,不然会多次调用。
保证先子后父
如何实现一个不能被继承的类?
C++98 私有化父类的构造函数
C++11 新增final,修饰父类,直接不能被继承,如:class A final
静态成员、友元与继承
友元关系不能继承
基类定义了static静态成员,则整个继承体系里面只有一个这样的成员。无论派生出多少个子类,都只有一个static成员实例 。
静态成员可以说继承的是使用权,静态成员不属于某个对象,存在静态区,属于整个类,突破类域就能访问。
如果静态成员在父类是私有,在子类中这个静态成员不可见
菱形继承和菱形虚拟继承
单继承:一个子类只有一个直接父类时称这个继承关系为单继承
多继承:一个子类有两个或以上直接父类时称这个继承关系为多继承
菱形继承:菱形继承是多继承的一种特殊情况
尽量不要使用菱形继承!
菱形继承有数据冗余和二义性的问题。
在Assistant的对象中Person成员会有两份。
class Person
{
public:string _name; // 姓名int _age;int _tel;// ...
};
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";// 需要显示指定访问哪个父类的成员可以解决二义性问题,// 但是数据冗余问题无法解决a.Student::_name = "小张";a.Teacher::_name = "老张";
}int main()
{Test();return 0;
}
虚拟继承可以解决菱形继承的二义性和数据冗余的问题。如上面的继承关系,在Student和Teacher的继承Person时使用虚拟继承,即可解决问题。需要注意的是,虚拟继承不要在其他地方去使用。
class Student : virtual public Person
class Teacher : virtual public Person
a.Student::_name = "小张";a.Teacher::_name = "老张";a._name = "张三";都是同一份
虚拟继承原理
先继承谁,谁就在前
B的指针可以访问b也要可以访问a,为了解决二义性付出了一些代价,用存的地址去表里找偏移量,偏移量在00 的下一个位置存是因为00的位置要存其他值,和多态有关,用自己的地址加上偏移量就取到a了
如果存的地址直接就是a的地址或者存的就是偏移量都是可以的,但引入多态之后情况会复杂,所以选择了这个方法.
为了解决数据冗余用了虚拟继承,但我们发现用了虚拟继承之后空间用的更多了,就像做生意,少存了一份a,当a越大,节省的空间就越多
如果D对象有很多,都只需要指向这个表会很方便
继承和组合
public继承是一种is-a的关系。也就是说每个派生类对象都是一个基类对象。
组合是一种has-a的关系。假设B组合了A,每个B对象中都有一个A对象。
//继承
class A
{
public:void func(){}
protected:int _a;
};class B : public A
{
public:void f(){func();_a++;}
protected:int _b;
};// 组合
class C
{
public:void func(){}
protected:int _c;
};class D
{
public:void f(){_c.func();//_c._a++;}
protected:C _c;int _d;
};
组合类的公有可以使用,组合类的保护就不可以使用了
对象的内部细节是不可见的。对象只以“黑箱”的形式出现。组合类之间没有很强的依赖关系,耦合度低。优先使用对象组合有助于你保持每个类被封装。
在继承方式中,基类的内部细节对子类可见。继承一定程度破坏了基类的封装,基类的改变,对派生类有很大的影响。派生类和基类间的依赖关系很强,耦合度高。
继承用起来比组合方便,但还是尽量多用组合,因为组合的耦合度低,代码维护性好。
习题练习