继承(C++)
继承
- 继承的概念及定义
- 继承的概念
- 继承的定义
- 定义格式
- 继承关系和访问限定符
- 继承基类成员访问方式的变化
- 基类和派生类对象赋值转换
- 继承中的作用域
- 派生类的默认成员函数
- 继承与友元
- 继承与静态成员
- 复杂的菱形继承及菱形虚拟继承
- 虚拟继承的原理
继承的概念及定义
继承的概念
继承是面向对象程序设计使代码可以复用的重要手段,允许程序在保持原有类特性的基础上进行扩展,增加功能,所产生的新类,称作派生类。继承呈现了面向对象程序设计的层次结构,体现了有简单到复杂的过程,继承是类设计层次的复用
class Person
{
public:
void Print()
{
cout << "name:" << _name << endl;
cout << "age:" << _age << endl;
}
protected:
string _name="zhangsan";//姓名
int _age=18;//年龄
};
class Student :public Person
{
protected:
int _stuid;//学号
};
class Teacher :public Person
{
protected:
int _jobid;//工号
};
int main()
{
Student s;
Teacher t;
s.Print();
t.Print();
return 0;
}
继承后父类的 Person
的成员,都会变成子类的一部分,在 Print
的打印中体验了复用的效果
继承的定义
定义格式
Person
称作父类,也是基类;Teacher
称作子类,也是派生类
继承关系和访问限定符
继承基类成员访问方式的变化
类成员/继承方式 | public继承 |
---|---|
基类的public成员 | 派生类的public成员 |
基类的protected成员 | 派生类的protected成员 |
基类的private成员 | 派生类的private成员 |
最终的继承成员:类成员和继承方式中权限较低的那个
基类和派生类对象赋值转换
派生类对象可以赋值给基类的对象/指针/引用,与以往所学习的有所不同,子类可以赋值给父类
观察下列代码
int main()
{
int i = 1;
double d = 2.2;
i = d;
const int& ri = d;
return 0;
}
当 i=d
时,并不是直接赋值,而是先创建一个临时变量,临时变量中的值为2,临时变量具有常性,所以在下面引用时,必须加上 const
否则程序便会崩溃
上面的问题在继承中就不会出现,因为赋值的原理都不同;继承中的赋值并不会产生临时变量,而是直接将子类赋值给父类,简单来说是子类中与父类相同类型的数据赋值给父类,也称切片
int main()
{
Person pn;
Teacher t;
pn = t;
return 0;
}
继承中的作用域
- 在继承中基类和派生类都有独立的作用域
- 当子类和父类中有同名成员时,子类成员将屏蔽父类对同名成员的直接访问,此情况称作隐藏,也称作重定义
- 如果成员函数也同名,也构成隐藏
class Person
{
protected:
string _name = "zhangsan";//姓名
int _age = 18;//年龄
};
class Student :public Person
{
public:
void Print()
{
cout << "姓名:" << _name << endl;
cout << "年龄:" << _age << endl;
}
protected:
int _age=20;//年龄
};
void test()
{
Student s;
s.Print();
}
Student
,Person
中的_age
学号构成隐藏,程序运行之后打印的是子类中的_age
,将父类中的_age
隐藏了起来
如果想访问父类中的_age
,可以做如下操作
class Student :public Person
{
public:
void Print()
{
cout << "姓名:" << _name << endl;
cout << "年龄:" << Person::_age << endl;
}
protected:
int _age=20;//年龄
};
派生类的默认成员函数
普通类的成员
- 内置类型
- 自定义类型
派生类的成员
- 基类对象
- 派生类的内置类型
- 派生类的自定义类型
派生类中基类对象调用基类对应的函数完成初始化/清理/拷贝,内置类型/自定义类型的处理与普通类一致
class Person
{
public:
Person(const char* name = "zhangsan")
:_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)
:Person(name)
,_stuid(210202)
{
cout << "Student()" << endl;
}
Student(const Student& st)
:Person(st)
,_stuid(st._stuid)
{
cout << "Student(const Student& st)" << endl;
}
Student& operator=(const Student& st)
{
cout << "Student& operator=(const Student& st)" << endl;
if (this != &st)
{
Person::operator = (st);
_stuid = st._stuid;
}
return *this;
}
~Student()
{
Person::~Person();
cout << "~Student()" << endl;
}
protected:
int _stuid;
};
int main()
{
Student st("zhangsan");
return 0;
}
析构函数有所区别,子类析构函数和父类析构函数构成隐藏关系,由于多态的要求,所有的析构函数都会特殊处理成destructor函数名,所以在子类的析构函数中需要加上访问限定符Person::
才能调用父类的析构函数;从上面的打印结果来看,父类的析构多调用了一次,只是为什么呢???
通过查看汇编,我们发现,在子类的析构函数执行结束后,编译器会自动调用父类的析构函数进行资源清理工作;也就是说,我们不需要在子类中调用父类的析构函数
删除子类中调用的父类析构函数运行结果如下
仔细观察发现其中也有些不寻常,子类后创建为什么先析构呢?
其实这也是析构函数特殊的地方:子类先析构,父类再析构,子类析构函数中不需要显示调用父类析构,子类析构结束后编译器会自动调用父类析构
继承与友元
友元关系不能继承,基类友元不能访问派生类私有和保护成员
class Person
{
public:
friend void Display(const Person& p,const Student s);
Person(const char* name = "zhangsan")
:_name(name)
{
cout << "Person()" << endl;
}
protected:
string _name;
};
class Student :public Person
{
public:
Student(const char* name)
:Person(name)
, _stuid(210202)
{
cout << "Student()" << endl;
}
protected:
int _stuid;
};
void Display(const Person& p,const Student s)
{
cout << p._name << endl;
cout << s._stuid << endl;
}
int main()
{
Person p("zhangsan");
Student s("lisi");
Display(p,s);
return 0;
}
继承与静态成员
如果基类定义了static静态成员,则整个继承体系里面只有一个这样的成员
class Person
{
public:
Person()
{
++_count;
}
protected:
string _name;
public:
static int _count;//统计人数
};
int Person::_count = 0;
class Student :public Person
{
protected:
int _stuid;
};
int main()
{
Person p;
Student s;
p._count++;
cout << p._count << endl;
cout << &p._count << endl;
s._count++;
cout << s._count << endl;
cout << &s._count << endl;
return 0;
}
打印的地址都一样,说明static成员,并不在类中,而是存储在静态区中的;静态成员属于整个类,所有对象,同时也属于所有派生类及对象
复杂的菱形继承及菱形虚拟继承
单继承:一个子类只有一个直接父类的继承关系
多继承:一个子类有两个或者以上直接父类的继承关系
菱形继承:多继承的一种
菱形继承存在着某些问题,观察下列代码
class Person
{
public:
string _name;
};
class Student :public Person
{
protected:
int _stuid;
};
class Teacher :public Person
{
protected:
int _jodid;
};
class Assistant :public Teacher, public Student
{
protected:
string _majorcourse;
};
int main()
{
Assistant a;
a._name = "zhangsan";
return 0;
}
程序运行之后便会报错,因为Assistant a
中有两份_name
,一份是Student
,一份是Teacher
的,所以使用时就造成了二义性;解决方式:使用时加上访问限定符
int main()
{
Assistant a;
a.Student::_name = "zhangsan";
a.Teacher::_name = "lisi";
return 0;
}
指定作用域并没有彻底地解决菱形继承所造成的问题,接下来介绍虚拟继承来彻底地解决问题
在 Student
和 Teacher
中继承 Person
时使用虚拟继承
具体操作如下
class Person
{
public:
string _name;
};
class Student :virtual public Person
{
protected:
int _stuid;
};
class Teacher :virtual public Person
{
protected:
int _jodid;
};
class Assistant :public Teacher, public Student
{
protected:
string _majorcourse;
};
int main()
{
Assistant a;
a._name = "zhangsan";
return 0;
}
既然已经知道虚拟继承可以解决菱形继承存在的问题,那么原理是什么呢?要做到知其然知其所以然,接下来就深入学习虚拟继承的原理
虚拟继承的原理
观察下列代码
菱形继承:
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 :virtual public A
{
public:
int _b;
};
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;
}
通过查看内存,B
和C
中都存在一个指针,分别对指针进行取地址发现,指针所指向的是虚基表,其中保存着距离虚基类对象_a
的偏移量(第二行),通过偏移量可以计算出B
和C
中虚拟继承的_a
的位置,在上面已经标注出来。
虚拟继承原理解释如下
继承和组合
- public继承是一种
is a
的关系,每个派生类对象中都是一个基类对象 - 组合是一种
has a
的关系,比如B
组合了A
,则每个B
对象中都有一个A
对象