文章目录:
1.继承相关的概念
1.1 继承的概念
继承(inheritance)机制是面向对象程序设计使代码可以复用的最重要的手段,它允许程序员在保持原有类特性的基础上进行扩展,增加功能,这样产生新的类,称派生类。继承呈现了面向对象程序设计的层次结构,体现了由简单到复杂的认知过程。以前我们接触的复用都是函数复用,继承是类设计层次的复用
1.2 继承的定义
继承是类与类之间的一种关系,分为父类(基类)和子类(派生类),如下图所示:
1.3.继承存在的意义
继承的本质是将父类中的内容(除析构函数和构造函数)继承到子类中,所以子类中不仅有父类的内容还有子类自己的成员和方法;
eg:假设有个动物类,有吃方法,睡方法,假设有一个鸟类继承了这个动物类,则这个鸟类也可以吃和睡,但它还有自己的飞方法,假设还有一个鱼类继承了这个动物类,则这个鱼类也可以吃和睡,但它还有自己的游方法。此时,使用继承就可以在一定程度上减少代码的复用,但是打破了类的封装性
1.4 继承的性质
从对象的角度分析:
对象
能直接访问
的成员和方法永远是公有的
从子类的角度分析:
父类的公有
成员和方法,保护
成员和方法在子类中都可以进行访问
1.4.1继承方式对访问方式的影响
从对象角度分析:
- 当继承访问方式为public,实例化一个子类对象可以访问父类中的公有成员和方法
- 当继承访问方式为protected或private,实例化一个子类对象不能访问父类中的任何成员和方法
从子类角度分析:
- 不管继承访问方式为public / protected / private ,在子内内部都可以访问父类的公有和保护成员和方法
继承方式只是将子类继承的父类的属性改为了继承方式的属性
eg:假设,有一个animal类,有一个鸟类public / protected 继承animal类,此时,还有一个ostrich类继承鸟类,则此时在ostrich类的内部可以访问鸟类继承下来的animal类的公有和保护的成员和方法,若鸟类private继承animal类,则在ostrich类的内部不能访问鸟类继承下来的animal类的公有和保护的成员和方法。
正确代码如下:
#include<iostream>
using namespace std;
class animal
{
public:
void Eat()
{
cout << "animal::Eat()" << endl;
}
void Sleep()
{
cout << "animal::Sleep()" << endl;
}
protected:
void Foot()
{
cout << "animal::Foot()" << endl;
}
private:
int a_data;
};
class bird : protected animal
{
public:
void Fly()
{
cout << "bird::Fly()" << endl;
}
void Show()
{
Foot();
}
private:
int b_data;
};
class ostrich :public bird
{
public:
void Swim()
{
cout << "ostrich::Look()" << endl;
}
void Show()
{
Eat();
}
private:
int o_data;
};
若将bird类private继承,则在ostrich类中不能访问鸟类继承下来的animal类的公有和保护的成员和方法。如下图所示:
如上分析可转化为如下表格:
注意:
① 基类private成员在派生类中无论以什么方式继承都是不可见的。这里的不可见是指基类的私有成员还是被继承到了派生类对象中,但是语法上限制派生类对象不管在类里面还是类外面都不能去访问它
② 基类private成员在派生类中是不能被访问,如果基类成员不想在类外直接被访问,但需要在派生类中能访问,就定义为protected。可以看出保护成员限定符是因继承才出现的。
③继承方式确保了从父类继承下来的接口在子类是什么属性,它保留的是最小的属性。(private < protected < public)
④ 使用关键字class时默认的继承方式是private,使用struct时默认的继承方式是public,不过最好显示的写出继承方式。
2. 同名隐藏
只要子类有和父类同名的函数,则父类所有的和子类同名的函数都会在子类中被隐藏,此时子类对象就不能直接访问父类的同名函数,(在子类成员函数中,可以使用 基类::基类成员 显示访问)
验证代码如下:
#include<iostream>
using namespace std;
class father
{
public:
void fun()
{
cout << "father::fun()" << endl;
}
void fun(int a)
{
cout << "father::fun(int a)" << endl;
}
private:
int f_data;
};
class child :public father
{
public:
void fun()
{
cout << "child::fun()" << endl;
}
private:
int c_data;
};
int main()
{
child ch;
ch.father::fun(2);
return 0;
}
- 在继承体系中基类和派生类都有独立的作用域。
3. 基类和派生类对象赋值转换
3.1 前言
如下代码所示,我们可以看到,我们用父类实例化一个对象fa,再用子类实例化一个对象ch,我们可以用子类对象给父类对象赋值,用父类指针接收子类对象的地址,用子类对象对父类引用初始化
#include<iostream>
using namespace std;
class father
{
public:
void fun()
{
cout << "father::fun()" << endl;
}
void fun(int a)
{
cout << "father::fun(int a)" << endl;
}
private:
int a = 1;
int b = 2;
};
class child :public father
{
public:
void fun()
{
cout << "child::fun()" << endl;
}
private:
int c = 3;
int d = 4;
};
int main()
{
father fa;
child ch;
//子类对象给父类对象赋值
fa = ch;
//用父类指针接收子类对象的地址
father *pfa = &ch;
//子类对象对父类引用初始化
father &rfa = ch;
return 0;
}
按理说,fa和ch是两个不同的类的对象,不能进行如上操作,为什么此处继承后,就可以完成,子类对象给父类对象赋值,父类指针接收子类对象的地址,子类对象对父类引用初始化呢?
此时就引出了基类和派生类对象的赋值转换和赋值兼容规则
3.2 赋值兼容规则
- 子类对象给父类对象赋值
- 子类对象可以初始化父类的指针
- 子类对象可以初始化父类的引用。
那么赋值兼容规则的依据是什么呢?我们通过如下代码来验证:
#include<iostream>
using namespace std;
class father
{
public:
father()
{
cout << "father()" << endl;
}
public:
~father()
{
cout << "~father()" << endl;
}
public:
void fun()
{
cout << "father::fun()" << endl;
}
void fun(int a)
{
cout << "father::fun(int a)" << endl;
}
private:
int a = 1;
int b = 2;
};
class child :public father
{
public:
child()
{
cout << "child()" << endl;
}
public:
~child()
{
cout << "~child()" << endl;
}
public:
void fun()
{
cout << "child::fun()" << endl;
}
private:
int c = 3;
int d = 4;
};
int main()
{
child ch;
}
通过运行结果和调试,我们可以看出实例化一个子类对象ch它会进行父类的构造然后再进行子类的构造,而且除了父类的构造函数和析构函数,其他所有的东西(包括private属性)均被子类继承了下来。而它的赋值其实就相当于取了子类的切片,如下图所示:
之所以可以通过子类对象给父类对象赋值,就是因为子类实例化一个对象时,子类对象内部包含父类成员和方法,所以可以对父类对象进行赋值,传址,引用操作
3.3 子类对象地址赋值给父类指针,父类指针指向问题
如下代码所示,将子类对象地址赋值给父类指针,父类指针只能访问到子类对象内部属于父类的成员和方法,是访问不到子类独有的成员和方法的。
#include<iostream>
using namespace std;
class father
{
public:
father()
{}
public:
~father()
{}
public:
void fun()
{
cout << "father::fun()" << endl;
}
private:
int a = 1;
int b = 2;
};
class child :public father
{
public:
child()
{}
public:
~child()
{}
public:
void fun()
{
cout << "child::fun()" << endl;
}
private:
int c = 3;
int d = 4;
};
int main()
{
child ch;
father fa;
father *pfa = &ch;
fa.fun();
}
因为子类对象在进行初始化的时候,会先调用父类的构造方法,然后再调用子类的构造方法,所以,子类实例化一个对象时,子类对象内部会包含父类成员和方法
图解如下:
4. 派生类的默认成员函数
6个默认成员函数,“默认”的意思就是指我们不写,编译器会为我们自动生成一个,那么在派生类中,这几个成员函数是如何生成的呢?
- ① 派生类的构造函数必须调用基类的构造函数初始化基类的那一部分成员。 如果基类没有默认的构造函数,则必须在派生类构造函数的初始化列表阶段显示调用。
- ② 派生类的拷贝构造函数必须调用基类的拷贝构造完成基类的拷贝初始化
- ③ 派生类的operator=必须要调用基类的operator=完成基类的复制。
- ④ 派生类的析构函数会在被调用完成后自动调用基类的析构函数清理基类成员。因为这样才能保证派生类对象先清理派生类成员再清理基类成员的顺序
- ⑤ 派生类对象初始化先调用基类构造再调派生类构造
- ⑥ 派生类对象析构清理先调用派生类析构再调基类的析构。
图解如下:
4.1 代码验证
⑤⑥代码验证如下:
#include<iostream>
using namespace std;
class father
{
public:
father()
{
cout << "father()" << endl;
}
~father()
{
cout << "~father()" << endl;
}
private:
int f_data;
};
class child :public father
{
public:
child()
{
cout << "child()" << endl;
}
~child()
{
cout << "~child()" << endl;
}
private:
int c_data;
};
int main()
{
child ch;
return 0;
}
①代码验证:
#include<iostream>
using namespace std;
class father
{
public:
father(int a)
{
cout << "father(int a)" << endl;
}
~father()
{
cout << "~father()" << endl;
}
private:
int f_data;
};
class child :public father
{
public:
child() :father(10)
{
cout << "child()" << endl;
}
~child()
{
cout << "~child()" << endl;
}
private:
int c_data;
};
int main()
{
child ch;
return 0;
}
若没有父类的默认构造函数,且没在子类的构造函数初始化列表阶段显示调用,则会报错,如下图所示:
②③代码验证:
#include<iostream>
using namespace std;
class father
{
public:
father(int a) :f_data(a)
{
cout << "father(int a)" << endl;
}
father(const father& p)
{
f_data = p.f_data;
cout << "father(const father& p)" << endl;
}
father& operator=(const father& p)
{
if (&p != this)
{
f_data = p.f_data;
cout << "father& operator=(const father& p)" << endl;
}
return *this;
}
~father()
{
cout << "~father()" << endl;
}
private:
int f_data = 0;
};
class child :public father
{
public:
child(int a) :father(a)
{
cout << "child()" << endl;
}
child(const child& d) :father(d)
{
c_data = d.c_data;
cout << "child(const child& d)" << endl;
}
child& operator=(const child& d)
{
if (&d != this)
{
//显示的调用父类的赋值语句
father::operator=(d);
c_data = d.c_data;
cout << "child& operator=(const childr& d)" << endl;
}
return *this;
}
~child()
{
cout << "~child()" << endl;
}
private:
int c_data = 1;
};
int main()
{
child ch(2);
//拷贝构造
child ch1 = ch;
child ld(3);
//赋值
ch1 = ld;
return 0;
}
④代码验证:
#include<iostream>
using namespace std;
class father
{
public:
father(int a) :f_data(a)
{
cout << "father(int a)" << endl;
}
father(const father& p)
{
f_data = p.f_data;
cout << "father(const father& p)" << endl;
}
father& operator=(const father& p)
{
if (&p != this)
{
f_data = p.f_data;
cout << "father& operator=(const father& p)" << endl;
}
return *this;
}
~father()
{
cout << "~father()" << endl;
}
private:
int f_data = 0;
};
class child :public father
{
public:
child(int a) :father(a)
{
cout << "child()" << endl;
}
child(const child& d) :father(d)
{
c_data = d.c_data;
cout << "child(const child& d)" << endl;
}
child& operator=(const child& d)
{
if (&d != this)
{
//显示的调用父类的赋值语句
father::operator=(d);
c_data = d.c_data;
cout << "child& operator=(const childr& d)" << endl;
}
return *this;
}
~child()
{
cout << "~child()" << endl;
}
private:
int c_data = 1;
};
int main()
{
child* fa = new child(0);
delete fa;
}
5. 继承中的友元与静态成员
友元关系不能继承,也就是说基类友元不能访问子类的私有和保护成员
基类定义了static静态成员,则整个继承体系里面只有一个这样的成员。无论派生出多少个子类,都只有一个static成员实例 。
如下代码,我们定义一个静态成员变量count用于统计人数,并在类外初始化,通过调试我们可以看到所以对象中的_count是一样的
#include<iostream>
#include<string>
using namespace std;
class Person
{
public:
Person()
{
++_count;
}
protected:
string _name; // 姓名
public:
static int _count; // 统计人的个数。
};
int Person::_count = 0;
class Student : public Person
{
protected:
int _stuNum; // 学号
};
class Graduate : public Student
{
protected:
string _seminarCourse; // 研究科目
};
void TestPerson()
{
Student s1;
Student s2;
Student s3;
Graduate s4;
cout << " 人数 :" << Person::_count << endl;
Student::_count = 0;
cout << " 人数 :" << Person::_count << endl;
}
int main()
{
TestPerson();
return 0;
}
而让_count在父类中定义为非静态的,通过调试可以看到每个对象中会有一个单独的_count
6. 多继承的构造顺序
多继承:一个子类有两个或者两个以上直接父类时称为这个继承关系为多继承
多继承的构造顺序由继承的顺序决定,而不是由初始化的顺序决定
子类的构造顺序:
①构造父类
②构造对象成员
③构造子类
代码验证如下:
#include<iostream>
using namespace std;
class Base1
{
public:
Base1()
{
cout << "Base1()" << endl;
}
~Base1()
{
cout << "~Base1()" << endl;
}
};
class Base2
{
public:
Base2()
{
cout << "Base2()" << endl;
}
~Base2()
{
cout << "~Base2()" << endl;
}
};
class Base3
{
public:
Base3()
{
cout << "Base3()" << endl;
}
~Base3()
{
cout << "~Base3()" << endl;
}
};
class child :public Base3, public Base2, public Base1
{
public:
child()
{
cout << "child()" << endl;
}
~child()
{
cout << "~child()" << endl;
}
private:
Base1 a;
Base2 b;
Base3 c;
};
int main()
{
child ch;
return 0;
}
7. 菱形继承
7.1 菱形继承的概念和存在的问题
菱形继承:菱形继承是多继承的一种特殊情况
图解如下图所示:
如上图的代码如下:
#include<iostream>
#include<string>
using namespace std;
class A
{
public:
int m_a = 1;
};
class B : public A
{
public:
int m_b = 1;
};
class C : public A
{
public:
int m_c = 2;
};
class D : public B, public C
{
public:
int m_d = 3;
};
int main()
{
D d;
cout << d.m_d << endl;
cout << d.m_c << endl;
cout << d.m_b << endl;
}
我们会发现D实例化一个对象d,可以直接访问m_b,m_c 但是不能直接访问m_a,这是为什么呢?
正如上面图中所画,B类和C类都继承了A类中的成员,所以D类在继承B类和C类的时候会继承来自B类继承的A类的成员和来自C类继承的A类的成员,所以,当D实例化一个对象d,要访问A类中的成员时就会存在二义性,且存在数据冗余,此时可以通过类名和作用域限定符来访问
通过d对B类和C类中访问的m_a的地址不同,可以证明我们上面所说的菱形继承存在数据冗余和二义性问题
我们也可以从内存窗口中查看d,我们可以看到数据冗余
7.2 虚拟继承
虚拟继承可以解决菱形继承的二义性和数据冗余的问题,如上继承关系,只需在B和C继承A的时候加上virtual即可解决问题
代码如下:
#include<iostream>
#include<string>
using namespace std;
class A
{
public:
int m_a = 0;
};
class B : virtual public A
{
public:
int m_b = 1;
};
class C : virtual public A
{
public:
int m_c = 2;
};
class D : public B, public C
{
public:
int m_d = 3;
};
int main()
{
D d;
cout << d.m_d << endl;
cout << d.m_c << endl;
cout << d.m_b << endl;
cout << d.m_a << endl;
cout << &d.B::m_a << endl;
cout << &d.C::m_a << endl;
}
此时我们可以看到,d可以直接访问m_a,且d访问从B类和C类继承下来的m_a的地址是相同的,证明使用虚拟继承后冗余只留一份
7.3 虚拟继承的原理
对如上我们加上虚拟继承的代码进行调试,当我们打开内存窗口可以看到如下:
d对象中将A放到了对象组的最下面,这个A是同时属于B和C的,通过B和C的两个指针,指向的一张表。这两个指针叫虚基表指针
,这两个表叫虚基表
。虚基表中存的偏移量。通过偏移量可以找到下面的A。
图解如下: