朋友们好,这篇博客我们进入C++的进阶学习,最近我学习了C++中的继承相关知识,继承是面向对象编程的三大特征之一,十分重要。所以特意整理出来一篇博客供我们一起复习和学习,如果文章中有理解不当的地方,还希望朋友们在评论区指出,我们相互学习,共同进步!
文章目录
一:继承的概念及定义
继承是面向对象三大特性之一。
1.1:继承的概念
继承(inheritance)机制是面向对象程序设计使代码可以复用的最重要的手段,它允许程序员在保持原有类特性的基础上进行扩展,增加功能,这样产生新的类,称派生类。继承呈现了面向对象程序设计的层次结构,体现了由简单到复杂的认知过程。以前我们接触的复用都是函数复用。继承是类设计层次的复用。
1.2:继承的定义
继承的语法:class 子类 : 继承方式 父类
继承方式:
- 共有继承
- 私有继承
- 保护继承
基类private成员在派生类中无论以什么方式继承都是不可见的。这里的不可见是指基类的私有成员还是被继承到了派生类对象中,但是语法上限制派生类对象不管在类里面还是类外面都不能去访问它。
二:基类和派生类对象赋值转换
- 派生类对象可以赋值给基类的对象/基类的指针/基类的引用。这里有个形象的说法叫切片或者切割。寓意把派生类中父类那部分切来赋值过去。
- 基类对象不能赋值给派生类对象。
- 基类的指针或者引用可以通过强制类型转换赋值给派生类的指针或者引用。但是必须是基类的指针是指向派生类对象时才是安全的。
前提:必须是公有继承!
class person{
protected:
string _name;
int _age;
};
class student :public person
{
public:
int _No;
};
void test01()
{
student sobj;
//1.子类对象可以赋值给父类对象/指针/引用
person pobj = sobj;
person* pp = &sobj;
person& rp = sobj;
//2.基类对象不可以赋值给派生类对象
//sobj = pobj;
//3.基类的指针可以通过强制类型转换赋值给派生类的指针
pp = &sobj;
student* ps1 = (student*)pp;//这种情况是可以的
ps1->_No = 10;
pp = &pobj;
student* ps2 = (student*)pp;//这种情况转换时虽然可以,但存在越界访问的问题
ps2->_No = 10;
}
三:继承中的作用域
3.1:继承同名成员处理方式
⚠️问题:当子类与父类出现同名的成员,如何通过子类对象,访问到子类或父类中同名的数据呢?
class Base {
public:
Base(){
m_A = 100;
}
void func(){
cout << "Base - func()调用" << endl;
}
void func(int a){
cout << "Base - func(int a)调用" << endl;
}
public:
int m_A;
};
class Son : public Base {
public:
Son(){
m_A = 200;
}
//当子类与父类拥有同名的成员函数,子类会隐藏父类中所有版本的同名成员函数
//如果想访问父类中被隐藏的同名成员函数,需要加父类的作用域
void func()
{
cout << "Son - func()调用" << endl;
}
public:
int m_A;
};
void test01()
{
Son s;
cout << "Son下的m_A = " << s.m_A << endl;
cout << "Base下的m_A = " << s.Base::m_A << endl;
s.func();
s.Base::func();
s.Base::func(10);
}
⭐️⭐️⭐️总结:
- 子类对象可以直接访问到子类中同名成员
- 子类对象加作用域可以访问到父类同名成员
- 当子类与父类拥有同名的成员函数,子类会隐藏父类中同名成员函数,子类加作用域可以访问到父类中同名函数。
- 当然父类对象随便调用父类成员。
注:子类和父类中有同名成员时构成隐藏关系,也叫重定义。需要注意的是,如果是成员函数的隐藏,只需要函数名相同就构成隐藏。
3.2:继承同名静态成员处理方式
⚠️:问题:继承中同名的静态成员在子类对象上如何进行访问?
静态成员和非静态成员出现同名,处理方式一致:
- 子类对象访问子类同名成员 直接访问即可
- 子类对象访问父类同名成员 需要加作用域
class Base {
public:
static void func()
{
cout << "Base - static void func()" << endl;
}
static void func(int a)
{
cout << "Base - static void func(int a)" << endl;
}
static int m_A;
};
int Base::m_A = 100;
class Son : public Base {
public:
static void func()
{
cout << "Son - static void func()" << endl;
}
static int m_A;
};
int Son::m_A = 200;
//同名成员属性
void test01()
{
//通过对象访问
cout << "通过对象访问: " << endl;
Son s;
cout << "Son 下 m_A = " << s.m_A << endl;
cout << "Base 下 m_A = " << s.Base::m_A << endl;
//通过类名访问
cout << "通过类名访问: " << endl;
cout << "Son 下 m_A = " << Son::m_A << endl;
cout << "Base 下 m_A = " << Son::Base::m_A << endl;
}
//同名成员函数
void test02()
{
//通过对象访问
cout << "通过对象访问: " << endl;
Son s;
s.func();
s.Base::func();
cout << "通过类名访问: " << endl;
Son::func();
Son::Base::func();
//出现同名,子类会隐藏掉父类中所有同名成员函数,需要加作作用域访问
Son::Base::func(100);
}
总结:同名静态成员处理方式和非静态处理方式一样,只不过有两种访问的方式(通过对象 和 通过类名)
3.3:继承与友元
友元关系不可以继承,也就是说基类的友元不要可以访问子类的私有成员和保护成员。
(就好比说爸爸的朋友不一定是我的朋友)
3.4:继承与静态成员
基类定义了static静态成员,则整个继承体系只有这一个成员(我们知道静态成员是整个类共享的),无论派生出多少个子类,都只有这么一个static成员。
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 course;
};
void test()
{
student s1;
student s2;
student s3;
graduate s4;
cout << "人数" << person::_count << endl;
student::_count = 0;
cout << "人数" << person::_count << endl;
}
人数4
人数0
请按任意键继续. .
代码解释:因为子类对象构造是会调用基类的构造函数,所以每实例化一个子类对象都会调用一次基类构造,从而_count++,并且静态成员是整个类共享的,所以无论哪个子类都可修改!!!
四:派生类的默认成员函数
6个默认成员函数,“默认"的意思就是指我们不写,编译器会变我们自动生成一个,那么在派生类中,这几个成员函数是如何生成的呢?
- 派生类的构造函数必须调用基类的构造函数初始化基类的那一部分成员。如果基类没有默认的构造函数,则必须在派生类构造函数的初始化列表阶段显示调用。
- 派生类的拷贝构造函数必须调用基类的拷贝构造完成基类的拷贝初始化。
- 派生类的operator=必须要调用基类的operator=完成基类的复制。
- 派生类的析构函数会在被调用完成后自动调用基类的析构函数清理基类成员。因为这样才能保证派生类对象先清理派生类成员再清理基类成员的顺序。
- 派生类对象初始化先调用基类构造再调派生类构造。
- 派生类对象析构清理先调用派生类析构再调基类的析构。
class person
{
public:
person(const char* name = "pxl")
:_name(name)
{}
person(const person& p)
:_name(p._name)
{}
person& operator=(const person& p)
{
if (this != *p){
_name = p._name;
}
return *this;
}
~person()
{}
protected:
string _name;
};
class student :public person
{
public:
student(const char* name, int num)
:person(name)//显示调用基类的构造函数初始化基类成员
, _num(num)
{}
student(const student& s)
:person(s)//注意这里有个隐式的切片操作 person& p = s;
, _num(s._num)
{}
student& operator=(const student& s)
{
if (this != &s){
person::operator=(s);//调用基类的operator=完成基类的赋值
_num = s._num;
}
return *this;
}
~student()
{
cout << "~student()" << endl;
//注意这里会自动调用父类析构
}
protected:
int _num;
};
void test()
{
student s1("ppp", 20);
student s2(s1);
student s3("xxx", 30);
s1 = s3;
}
⚠️留意代码中注释部分!
五:复杂菱形继承及菱形虚拟继承
5.1:继承分类
单继承:一个子类只有一个直接父类时称为单继承
多继承:一个子类有两个或者两个以上直接父类时称这个继承关系为多继承
菱形继承:两个派生类继承同一个基类,又有某个类同时继承者两个派生类。菱形继承带来的主要问题是子类继承两份相同的数据,导致资源浪费以及毫无意义。
- 利用虚继承可以解决菱形继承问题
⭐️⭐️⭐️⭐️⭐️⭐️⭐️⭐️⭐️⭐️⭐️⭐️⭐️⭐️⭐️⭐️⭐️⭐️⭐️⭐️⭐️⭐️⭐️⭐️⭐️⭐️⭐️⭐️
对于菱形继承的二义性问题,我们可以在访问的时候加上类域,这样是可以解决的,但是数据冗余无法解决。所以下面引入虚拟继承!
5.2:虚拟继承解决菱形继承问题原理
为了研究虚拟继承原理,我们给出一个简单的菱形继承体系,再借助内存窗口观察对象成员模型。
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;
system("pause");
return 0;
}
如图是菱形继承的内存对象成员模型,可以看出来数据冗余!!!
下面是菱形虚拟继承的内存对象成员模型:
这里可以分析出D对象将A放在了对象组成的最下面,这个A同时属于B和C,那么B和C如何去找到公共的A呢?
这里通过B和C的两个指针,指向一张表。这两个指针叫虚基表指针,这两个表叫虚基表,虚基表中存的是偏移量。通过偏移量可以找到下面的A。
朋友们,看到这里还希望支持一下!
赠人玫瑰,手留余香!!!
🌹🌹🌹🌹🌹🌹🌹🌹🌹🌹🌹🌹🌹🌹🌹🌹🌹🌹🌹🌹🌹🌹🌹🌹🌹🌹🌹🌹🌹🌹🌹🌹🌹🌹🌹🌹🌹🌹🌹🌹🌹🌹