目录
1.继承的概念和定义
1.1继承的概念
1.2继承的定义
1.2.1定义格式
1.2.1不同继承方式下父类成员访问方式的变化
2.父类和子类对象赋值转换
3.继承中的作用域
3.1成员变量
3.2成员函数
4.子类的默认成员函数
4.1子类的默认构造函数
4.2子类的默认析构函数
4.3子类的默认拷贝构造函数
4.4子类默认operator=
5.继承与友元
5.1友元函数
5.2继承中的友元函数
6.继承与静态成员
7.菱形继承及菱形虚拟继承
7.1单继承、多继承
7.1.1单继承:一个类只有一个直接父类。
7.1.2多继承:一个类有两个或两个以上的直接父类。
7.1.3菱形继承(多继承的一种特殊情况)
7.2菱形继承存在的问题
7.3菱形继承的二义性
7.4虚拟继承
8.继承和组合的对比
继承:代码复用的重要的手段,使得子类具有父类的属性、重新定义、追加属性和函数等;可以在保持原有类特性的基础上进行扩展,增加功能。
继承的概念并不是固定的,只要能够通过自己的语言组织起来,再结合一些实例能够解释就可以了。
既然提到继承的定义,那么至少要有两个类才能够完成,我们可以先定义一个Person类:
class Person
{void show(){cout <<"name:" <
以学生(Student)类为例,继承Person类:
class Student : public Person
{};
1.Person类是父类,也称为基类;Student类是子类,也称为派生类。
2.继承方式和访问限定符一样,也有三种(public、protected、private),但是和访问限定符表示的有所差别。
下面来演示一下,不同限定符的父类成员在子类中的变化:
1.父类中的public成员,公有继承
class Person
{
public:void show(){cout <<"name:" <
class Student : public Person
{};
int main()
{Student s1;s1.name = "阿飞";s1.age = 19;s1.show();return 0;
}
Student中没有任何成员,只有从Person类中继承下来的name和age。
2.父类的protected成员,公有继承
同样使用1中的Person类,只是把成员变量name和age改为了protected:
class Person
{
public:void show(){cout <<"name:" <
};
子类继承之后成员属性为protected,不能在类外进行访问。
protected属性的成员在类内是可以访问的:
class Student : public Person
{
public:void Set(string m_name, int m_age){name = m_name;age = m_age;}
};
int main()
{Student s1;s1.Set("阿飞", 19);return 0;
}
类外无法访问类内的protected/private成员,但是可以设置公有的接口对类内的protected/private成员进行访问。
3.父类的private成员,公有继承
上面提到,父类的private成员在子类中是不可见的,那么这个不可见是什么含义呢?
class Person
{
public:void show(){cout <<"name:" <
};
在子类Student中设置公有的属性去访问父类中的private是否可行?
不可见: 子类对象在类内和类外都无法进行访问。
一般的话,我们不会设置父类的成员为private,除非不行被子类继承的成员。
关于继承方式和访问限定符就演示这三种情况,剩下的几种情况大家感兴趣的话可以自己去演示一下。
下面进行一下总结:
- 父类中的private成员在派生类中无论以什么方式继承都是不可见的。(语法上限制子类对象不管是在类内还是类外都不能访问)
- 如果子类成员不想在类外被访问,但需要在类内访问的,就可以定义为protected。
- 父类的私有成员子类不可见,其他成员在子类中的访问方式 为继承方式和访问限定符中权限小的一个。(public > protected > private)
- 使用class定义类时默认的继承方式是private,使用struct默认继承方式为public,不过最好显示写出继承方式。
- 实际运用中一般使用public继承,很少用到protected和private继承。
Student类公有继承Person类:
class Person
{
public:string name;int age;
};
class Student : public Person
{
public:int id;//学号
};
定义一个子类的对象,那么能不能赋值给父类?如果能是否发生了类型的转换?
int main()
{Student s;Person p = s;return 0;
}
通过编译,可以得出结论:子类对象可以赋值给父类对象。
在子类对象赋值给父类对象的时候,实际上发生了切片,把子类对象中继承父类的成员切割赋值给了父类对象。
下面对以上结论进行扩展,既然子类对象可以赋值给父类,那么子类对象的地址能不能赋值给父类对象的指针?子类对象能不能赋值给父类对象的引用?
int main()
{Student s;Person p = s;Person* pp = &s;Person& ps = s;return 0;
}
这两种情况同样也是正确的,通过画图来加深一下理解:
上面我们遗留了一个问题:赋值的时候发生了切割, 为什么不是类型转换?
int main()
{int a = 10;const double& d = a;//a赋值给d的时候产生一个临时变量,临时变量具有常性,不加上const会报错return 0;
}
子类对象赋值给父类引用的时候,没有加const,而且没有报错,说明没有发生类型转换。
注意:子类对象可以赋值给父类对象,但是父类对象不能赋值给子类对象。
每一个变量都有其对应的作用域,类中也有属于自己的类域;而且不同的类有不同的类域。
父类和子类中的成员在不同的类域中。
class Person
{
public:string name;int age;
};
class Student : public Person
{
public:string name;//父类中有同名的nameint id;//学号
};
int main()
{Student s1;s1.name = "阿飞";s1.age = 19;return 0;
}
通过s1访问name,首先访问的是Student类中的name,因为在这里存在一个就近原则;s1属于Student类,首先调用Student类域中的name。
class Person
{
public:void func(int n){cout <<"func(int n)" <
class Student : public Person
{
public:void func(){cout <<"func()" <
int main()
{Student s1;s1.func();return 0;
}
成员函数的调用同样满足就近原则:
那如果使用子类对象传参数调用func(),运行结果是什么?
答案是:编译报错。
这里来总结一下:
显示访问一下父类中的func():
注意:父类和子类中的同名函数参数不同,并不能构成函数重载;因为函数重载要求函数必须要在相同的作用域。
提供一个Person类,类中提供了构造函数(有缺省值)、拷贝构造函数、operator=、析构函数。
class Person
{
public:Person(const char* name = "peter"): _name(name){cout <<"Person()" <
};
class Student : public Person//子类Student公有继承Person类
{
protected:int _id; //学号
};
Student(const char* name, int id): Person(name)//调用父类的构造函数, _id(id)
{cout <<"Student()" <
注意:子类构造函数的调用顺序是父类先于子类。
~Student()
{~Person();cout <<"~Student()" <
析构函数可以这样写吗?
由于多态的需要,父类和子类析构函数的名字会统一处理为destructor();
这也就造成了子类的构造和父类的构造构成了隐藏。
指定调用父类的析构:
~Student()
{Person::~Person();cout <<"~Student()" <
如果显式的调用析构会存在一个问题,创建一个子类对象,清理的时候会调用两次父类的析构。
注意:子类析构函数不需要显式调用父类的析构函数。
每个子类析构函数后面,会自动调用父类的析构函数,这样才能保证先析构子类,再析构父类(栈中先进后出)。
Student(const Student& s): Person(s)//子类传参给父类时发生切片, _id(s._id)
{cout <<"Student(const Student& s)" <
拷贝构造函数调用之后,s2中的_name和_id都是相等的,显式写的子类拷贝构造完成。
一种错误示例写法:
Student& operator = (const Student& s)
{cout <<"Student& operator= (const Student& s)" <
父类中的operator=和子类中的函数名相同,构成了隐藏,如果不显式调用父类中的operator=,会不断的进行子类operator,最后导致栈溢出。
正确写法:
Student& operator = (const Student& s)
{cout <<"Student& operator= (const Student& s)" <
友元函数:某些虽然不是类中的成员却能够访问类的所有成员的函数,类授予它的友元特别的访问权。
class Person
{friend void Display(const Person& p);//Display是Person类的友元函数
public:void SetName(const string& name){_name = name;}
protected:string _name; // 姓名
};
void Display(const Person& p)
{cout <
int main()
{Person p;p.SetName("阿飞");Display(p);return 0;
}
友元函数解决了类外不能访问类中protected/private的问题。
友元关系不能继承,也就是说父类的友元函数不能访问子类中的protected和private成员。
class Student;//声明子类
class Person
{
public:friend void Display(const Person& p, const Student& s);//父类的友元函数
protected:string _name = "阿飞"; // 姓名
};
class Student : public Person
{
protected:int _stuNum = 666; // 学号
};
void Display(const Person& p, const Student& s)
{cout <
int main()
{Person p;Student s;Display(p, s);return 0;
}
函数Display()是父类Person的友元函数,可以访问父类中的protected/private成员;
但是友元关系不能继承,所以无法访问子类中的_stuNum。
如果同时想要访问子类中的protected/private,可以把函数声明为子类的友元。
class Student : public Person
{friend void Display(const Person& p, const Student& s);
protected:int _stuNum; // 学号
};
一般成员在子类和父类中都是单独的一份,而静态成员在父类和子类中是同一份。
class Person
{
public:static int _count;
};
int Person::_count = 0;//静态成员只能在类外进行初始化
class Student : public Person
{};
int main()
{Person p;Student s;cout <
子类对象和父类对象中的静态成员_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 _majorCourse;
};
int main()
{Assistant a;a._name = "peter";return 0;
}
菱形继承的二义性导致Assistant类中的_name访问不明确的问题,此问题可以显式指定类域来解决。
int main()
{Assistant a;a.Student::_name = "xxx";a.Teacher::_name = "yyy";return 0;
}
虽然可以指定类域来进行访问,但是这样无法从根本上解决菱形继承存在的问题。
虚拟继承可以解决菱形继承的二义性和数据冗余问题。
class Student : virtual public Person
{
protected:int _num; //学号
};
class Teacher : virtual public Person
{
protected:int _id; // 职工编号
};
下面来借助VS2019中的调试内存窗口来观察一下类对象成员分配:
1.菱形继承(非虚拟继承)
B中和C中都有一份A,导致数据冗余。
2.菱形虚拟继承
下图是菱形虚拟继承的内存对象成员模型:A同时属于B和C,那么B和C如何去找公共的A呢?
这里通过了B和C的两个指针,指向的一张表;这两个指针叫虚基表指针,这两个表叫虚基表。虚基表中存的是偏移量,通过偏移量可以找到A。
使用到虚基表指针和虚基表的两种情况举例:
int main()
{//1.切片->需要找到AD d;B b = d;B* pb = &d;//2.通过父类B、C访问A中的成员->通过偏移量和地址找到_apb->_a = 10;return 0;
}
在编程中,我们追求的是一种“高内聚,低耦合”:继承一定程度上破坏了基类的封装,父类中的protected属性的成员在子类内是可以使用的,父类和子类之间的依赖关系强,耦合度高。
但如果使用的是组合,那么protected属性脱离了类内,在类外不能够使用,依赖关系较弱,耦合较低。
也有些关系适合使用继承,例如要实现多态就必须要使用继承。
面向对象三大特性之——继承到这里就结束了,喜欢这部分内容的铁汁们可以给博主一个三连支持,你们的支持是博主最大的动力,后续会继续更新面向对象三大特性中的多态,喜欢的铁汁们记得三连哈。