1.继承的概念
继承(inheritance)机制是面向对象程序设计使代码可以复用的最重要的手段,它允许程序员在保 持原有类特性的基础上进行扩展,增加功能,这样产生新的类,称派生类。继承呈现了面向对象 程序设计的层次结构,体现了由简单到复杂的认知过程。以前我们接触的复用都是函数复用,继 承是类设计层次的复用。
//父类
class Pet
{
public:
void Eat()
{
cout << "在吃饭" << endl;
}
void Sleep()
{
cout << "在睡觉" << endl;
}
string _name;
string _gender;
};
//继承后父类的Pet的成员(成员函数+成员变量)都会变成子类的一部分。这里体现出了
//Dog和Cat复用了Pet的成员。
class Dog :public Pet
{
public:
void Bark()
{
cout << _name << "狂吠" << endl;
}
string _color;
};
class Cat :public Pet
{
public:
void mew()
{
cout << _name << "喵喵喵" << endl;
}
string _temper;
};
int main()
{
Dog dog;
dog._name = "小七";
dog._gender = "公";
dog._color = "金黄色";
dog.Eat();
dog.Sleep();
dog.Bark();
return 0;
}
2.继承定义
下面我们看到Person是父类,也称作基类。Student是子类,也称作派生类。
2.1 继承关系和访问限定符
代码演示不同继承方式下的情况
public继承方式下:
//基类 class Fu { public: void SetFu(int pub, int pro, int pri) { _pub = pub; _pro = pro; _pri = pri; } void Print() { cout << _pub << " " << _pro << " " << _pri << endl; } public: int _pub; protected: int _pro; private: int _pri; }; //public继承方式下: class Zi :public Fu { public: void SetZi() { _pub = 10; _pro = 20; //_pri = 30; //基类中private修饰的成员在子类中不可见(不可被访问) } }; int main() { Zi zi; zi._pub = 100; //基类中public修饰成员的子类中权限没有发生变化 //zi._pro = 200; //编译失败:_pro不能直接在类外呗访问(Protected 或者 Private) return 0; }
protected继承方式下:
/基类 class Fu { public: void SetFu(int pub, int pro, int pri) { _pub = pub; _pro = pro; _pri = pri; } void Print() { cout << _pub << " " << _pro << " " << _pri << endl; } public: int _pub; protected: int _pro; private: int _pri; }; //protected继承方式下: class Zi :protected Fu { public: void SetZi() { SetFu(1, 2, 3); _pub = 100;//基类public的成员变量在子类中的访问权限降为protected _pro = 200; //基类中protected的成员变量在子类中访问权限仍旧是protected //_pri = 300; //基类私有的成员变量在子类中不可见(在子类中不呢个被直接使用) } }; class D :public Zi { public: void testfunc() { SetFu(1, 2, 3); _pub = 10; _pro = 20; } }; int main() { Zi zi; //zi.SetFu(); return 0; }
private继承方式下:
//基类 class Fu { public: void SetFu(int pub, int pro, int pri) { _pub = pub; _pro = pro; _pri = pri; } void Print() { cout << _pub << " " << _pro << " " << _pri << endl; } public: int _pub; protected: int _pro; private: int _pri; }; //private继承方式下: class Zi :private Fu { public: void SetZi() { SetFu(1, 2, 3); _pub = 100;//基类public的成员变量在子类中的访问权限降为private _pro = 200; //基类中protected的成员变量在子类中访问权限为private //_pri = 300; //基类私有的成员变量在子类中不可见(在子类中不呢个被直接使用) } }; class D :public Zi { public: void func() { //SetFu(1, 2, 3); 编译失败 //_pub = 10; 编译失败 //_pro = 20; //编译失败 } }; int main() { Zi zi; //zi.SetFu(); 编译失败 return 0; }
总结:
1. 基类private成员在派生类中无论以什么方式继承都是不可见的。这里的不可见是指基类的私有成员还是被继承到了派生类对象中,但是语法上限制派生类对象不管在类里面还是类外面 都不能去访问它。
2. 基类private成员在派生类中是不能被访问,如果基类成员不想在类外直接被访问,但需要在你 派生类中能访问,就定义为protected。可以看出保护成员限定符是因继承才出现的。
3. 实际上面的表格我们进行一下总结会发现,基类的私有成员在子类都是不可见。基类的其他 成员在子类的访问方式 == Min(成员在基类的访问限定符,继承方式),public > protected > private。
4. 使用关键字class时默认的继承方式是private,使用struct时默认的继承方式是public,不过最好显示的写出继承方式。
5. 在实际运用中一般使用都是public继承,几乎很少使用protetced/private继承,也不提倡 使用protetced/private继承,因为protetced/private继承下来的成员都只能在派生类的类里 面使用,实际中扩展维护性不强。
3.基类和派生类对象赋值转换
基类和子类对象之间的相互赋值(赋值兼容规则),大前提:在public的继承方式下
1.可以将子类的对象直接赋值给基类对象,反正则不行
2.可以让基类的引用引用子类的对象,反之则不行
3.可以让基类的指针指向子类的对象,反之则不行-----如果一定要让子类的指针指向基类的对象,只能强转。
代码验证:
class B
{
public:
void SetB(int b)
{
_b = b;
}
void PrintB()
{
cout << _b << endl;
}
protected:
int _b;
};
//赋值兼容规则的大前提:public的继承方式
class D :public B
{
public:
void SetD(int b, int d)
{
SetB(b);
_d = d;
}
void PrintD()
{
PrintB();
cout << _d << endl;
}
protected:
int _d;
};
int main()
{
B b;
b.SetB(1);
D d;
d.SetD(2, 3);
//1.可以使用子类对象给基类对象赋值,反之则不行
b = d;
//d=b; //编译失败
//2.可以让基类的引用引用子类的对象,反之则不行
B& rb = d;
//D& rd = b;
//3.可以让基类的指针指向子类的对象,反之则不行-----如果一定要让子类的指针指向基类的对象,只能强转
B* pb = &d;
//D* pd = &b;
D* pd = (D*)&b; //不推荐 因为不安全
return 0;
}
演示:
1.可以将子类的对象直接赋值给基类对象,反正则不行
4.继承中的作用域
1. 在继承体系中基类和派生类都有独立的作用域。
2. 子类和父类中有同名成员,子类成员将屏蔽父类对同名成员的直接访问,这种情况叫隐藏, 也叫重定义。(在子类成员函数中,可以使用 基类::基类成员 显示访问)
3. 需要注意的是如果是成员函数的隐藏,只需要函数名相同就构成隐藏。
4. 注意在实际中在继承体系里面最好不要定义同名的成员。
/*
同名隐藏:
1.在继承的体系中,子类和基类存在相同名称的成员(成员变量或者成员方法)
2.如果通过子类对象直接调用相同名称的成员时,优先调用的是子类自己的成员,
基类同名的成员无法被子类对象直接访问到
3.如果子类对象要直接访问同名的成员,必须在同名成员前添加
基类名称:: 即明确告诉编译器现在要访问的基类的同名成员
4.建议:在继承体系中,基类和子类尽量避免的定义相同名称的成员。
*/
class B
{
public:
void func()
{}
int _b;
};
class D :public B
{
public:
void func(int b)
{
_b = 65;
}
char _b;
int _d;
};
int main()
{
D d;
d._b = 'A'; //直接访问优先访问到的是自己的
d.B::_b = 'B'; //明确告诉编译器 现在要访问的是从基类继承下来的 _b
return 0;
}
5.派生类的默认成员函数
6个默认成员函数,“默认”的意思就是指我们不写,编译器会变我们自动生成一个,那么在派生类 中,这几个成员函数是如何生成的呢?
构造函数:
1.如果基类没有显示定义任何构造方法,则子类可以根据自己的选择性实现。
2.如果基类定义了无参或者全缺省的构造方法,则子类可以根据自己是否需要选择性实现。
3. 如果基类定义了带有参数的非全缺省的构造方法,则子类必须实现自己的构造方法,并且必须在其初始化列表的位置显示调用基类的构造方法。 目的:从基类继承下来的成员变量进行初始化。
拷贝构造:
1.基类的拷贝构造如果没有实现,则子类可以实现也可以不去实现。
2.如果基类和子类的拷贝构造都定义了,子类的构造方法必须在其初始化列表的位置显示基类的拷贝构造。
赋值运算符重载:
1.基类的运算符如果没有写,则子类也可以不用写
2.如果子类中涉及到资源的管理时,子类必须要实现自己的赋值运算符重载
3.子类赋值运算符重载实现方式:(1)先调用基类的赋值运算符重载完成从基类继承下来的成员的赋值.(2)在给子类自己的成员赋值。
析构函数:
1.如果子类中未涉及到资源管理时,子类的析构函数可以定义也可以不用
2.如果子类中涉及到资源管理,子类的析构函数就需要定义。
注意:严格来说并不是先调用基类的构造方法,再调用子类的构造方法
实际应该是:创建那个类的对象,则调用那个类的构造方法。现在创建的是子类的对象,编译器肯定要调用子类的构造方法。
构造方法有两部分组成:初始化列表+方法体
因此子类的构造方法在执行时,先执行其初始化列表,然后执行其方法体
因此:在子类构造方法体执行之前,先跳转到基类构造方法的位置执行基类的构造方法以完成子类对象中从基类继承下来成员的初始化工作,然后再去初始化子类自己的成员,最后才是子类构造方法方法体的执行。
6.继承和友元
友元关系不能继承,也就是说基类友元不能访问子类私有和保护成员
class B
{
friend void Print();
public:
B(int b)
:_b(b)
{
cout << "B()" << endl;
}
protected:
int _b;
};
class D :public B
{
public:
D(int b, int d)
:B(b), _d(_d)
{
cout << "D(int,int)" << endl;
}
protected:
int _d;
};
void Print()
{
B b(1);
cout << b._b << endl; //开始因为_b在类中是protected权限所以在类外不能被访问,加了友元函数后
//因为B中有了Print()的友元函数 所有_b可以在类外被访问 代码通过编译。
//结论:友元关系不能被继承
//创建一个子类的对象,如果子类对象D中的_d在该方法中不能直接被访问 则可以证明
D d(1, 2);
//cout << d._d << endl; 编译失败
}
7. 继承与静态成员
基类定义了static静态成员,则整个继承体系里面只有一个这样的成员。无论派生出多少个子 类,都只有一个static成员实例 。
8. 复杂的菱形继承及菱形虚拟继承
1.单继承:一个子类只有一个直接父类时称这个继承关系为单继承
2.多继承:一个子类有两个或以上直接父类时称这个继承关系为多继承
3.菱形继承:菱形继承是多继承的一种特殊情况(将单继承和多继承组合起来形成的一种继承方式)
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";
// 需要显示指定访问哪个父类的成员可以解决二义性问题,但是数据冗余问题无法解决
a.Student::_name = "xxx";
a.Teacher::_name = "yyy";
}
上述解决方式只是让代码通过了编译,但最顶层基类中的成员在子类中仍旧是存在多份。
不足:最顶层基类中的成员在子类中仍旧是存在多份的,浪费空间,而且二义性并没有真正的解决。
如果要真正的解决,考虑:能否让最顶层的类中的成员在子类(类型继承中最底下的类)只储存一份呢?
答案是可以的:c++中引入了菱形虚拟继承解决上述问题。
4.虚拟继承
在继承权限前加上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;
}
下图是菱形继承的内存对象成员模型:这里可以看到数据冗余
加了虚拟继承的菱形继承:
class A
{
public:
int _a;
};
// class B : public A
class B : virtual public A
{
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;
}
下图是菱形虚拟继承的内存对象成员模型:这里可以分析出D对象中将A放到的了对象组成的最下 面,这个A同时属于B和C,那么B和C如何去找到公共的A呢?这里是通过了B和C的两个指针,指 向的一张表。这两个指针叫虚基表指针,这两个表叫虚基表。虚基表中存的偏移量。通过偏移量 可以找到下面的A。
总结: