继承
继承的概念及继承方式
继承是面向对象三大特性之一,它的优点就是减少重复代码。
在class A:puclic B;
这条语句中,A称为子类或派生类;B称为父类或基类。
派生类的成员包含两大部分:
- 一部分是从基类继承过来的,一部分是自己增加的成员。
- 从基类继承过来的表现其共性,而新增加的成员体现了其个性。
继承的方式一共有三种
- 公有继承
- 保护继承
- 私有继承
接下来分别用三个例子来解释三种继承方式各有什么特点:
公有继承:
- 子类可以访问父类中的公有成员
- 子类可以访问父类中的保护成员
- 子类不可以访问父类中的私有成员
- 类外可以访问子类的公有成员
- 类外不可以访问子类的私有成员和保护成员
保护继承:
私有继承:
下面用一张图来做一下简单的总结:
继承中的对象模型
首先我们先问一个问题:从父类继承过来的成员,哪些属于子类对象中?
我们先来看一段代码:
#include <iostream>
using namespace std;
class Base
{
public:
int m_a;
protected:
int m_b;
private:
int m_c;
};
class Son :public Base
{
public:
int m_d;
};
void test01()
{
cout << "sizeof son:" << sizeof(Son) << endl;
}
int main()
{
test01();
return 0;
}
运行结果如下所示:
结论如下:
- 父类中所有非静态成员属性都会被子类继承下去
- 父类中私有成员属性是被编译器给隐藏了,因此是访问不到,但是确实是被继承下去了
利用工具对以上结论再做一个验证:
- 首先在开始里面找到VS2019中的开发人员命令提示工具,如下图所示:
- 然后将其打开出现如下所示的对话框:
- 然后在VS中找到该文件所在的文件夹,将其路径进行复制,然后
cd
命令进入到该路径下,进入到该目录下之后用命令dir
查看该目录中有哪些内容。
- 然后通过具体查看命令查看某个类的布局情况。
- 命令如下:
cl /d1 reportSingleClassLayout类名 文件名
继承中的构造和析构顺序
子类继承父类后,当创建子类对象,也会调用父类的构造函数,那么父类和子类的构造和析构顺序是谁先谁后呢?
首先我们来简单写段代码,看看它的运行结果是什么
#include <iostream>
using namespace std;
class Base
{
public:
Base()
{
cout << "Base的构造函数" << endl;
}
~Base()
{
cout << "Base的析构函数" << endl;
}
};
class Son :public Base
{
public:
Son()
{
cout << "Son的构造函数" << endl;
}
~Son()
{
cout << "Son的析构函数" << endl;
}
};
void test01()
{
Son s;
}
int main()
{
test01();
return 0;
}
运行结果如下图所示:
从这个结果我们可以得出如下的结论:
- 继承中先构造父类,再构造子类
- 析构顺序一般与构造顺序相反,先析构子类,后析构父类
继承中同名的处理方式
问题:当子类与父类出现同名的成员,如何通过子类对象访问到子类或父类中同名的数据呢?
- 访问子类同名成员,直接访问即可
- 访问父类同名成员,需要加作用域
下面我们写一段通过子类对象访问子类和父类同名成员属性的示例代码:
#include <iostream>
using namespace std;
class Base
{
public:
Base()
{
m_a = 100;
}
int m_a;
};
class Son :public Base
{
public:
Son()
{
m_a = 200;
}
int m_a;
};
void test01()
{
Son s;
cout << "Son m_a = " << s.m_a << endl;
cout << "Base m_a = " << s.Base::m_a << endl;
}
int main()
{
test01();
return 0;
}
通过子类对象调用子类和父类的同名成员函数示例代码:
#include <iostream>
using namespace std;
class Base
{
public:
Base()
{
m_a = 100;
}
void func()
{
cout << "Base func()调用" << endl;
}
void func(int a)
{
cout << "Base func(int a)调用" << endl;
}
int m_a;
};
class Son :public Base
{
public:
Son()
{
m_a = 200;
}
void func()
{
cout << "Son func()调用" << endl;
}
int m_a;
};
void test01()
{
Son s;
s.func();//直接调用的是子类中的同名成员函数
s.Base::func();//调用父类中的同名成员函数
s.Base::func(100);
//注意:如果子类中出现和父类同名的成员函数,子类的同名成员函数会隐藏掉父类中所有的同名成员函数
//如果想访问到父类中被隐藏的同名成员函数,需要加作用域
}
int main()
{
test01();
return 0;
}
运行结果如下:
总结:
- 子类对象可以直接访问到子类中的同名成员
- 子类对象加作用域可以访问到父类中的同名成员
- 当子类与父类拥有同名的成员函数,子类会隐藏父类中所有的同名函数,加作用域可以访问到父类中的同名成员函数
继承中同名静态成员处理方式
问题:继承中同名的静态成员在子类对象上如何进行访问?
静态成员和非静态成员出现同名,处理方式一致
- 访问子类同名成员,直接访问即可
- 访问父类同名成员,需要加作用域
练习代码如下:
#include <iostream>
using namespace std;
class Base
{
public:
static void func()
{
cout << "Base static void func()" << 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()
{
//通过对象访问
Son s;
cout << "通过对象方式访问" << endl;
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()
{
Son s;
cout << "通过对象访问" << endl;
s.func();
s.Base::func();
cout << "通过类名访问" << endl;
Son::func();
Son::Base::func();
//子类出现和父类同名静态成员函数,也会隐藏父类中所有同名成员函数,如果想访问父类中被隐藏的同名成员函数,需要加作用域
}
int main()
{
test01();
cout << "*********************************" << endl;
test02();
return 0;
}
运行结果如下:
总结
- 同名静态成员处理方式和同名非静态成员处理方式一样,只不过有两种访问的方式(通过对象访问和通过类名访问)。
多继承语法
C++允许一个类继承多个类
语法:class 子类:继承方式 父类1,继承方式 父类2,...
多继承可能会引发父类中有同名成员出现,需要加作用域区分
C++实际开发中不建议用多继承
练习代码:
#include <iostream>
using namespace std;
class Base1
{
public:
Base1()
{
m_a = 100;
}
int m_a;
};
class Base2
{
public:
Base2()
{
m_a = 200;
}
int m_a;
};
class Son :public Base1, public Base2
{
public:
Son()
{
m_c = 300;
m_d = 400;
}
int m_c;
int m_d;
};
void test01()
{
Son s;
cout << "sizeof Son = " << sizeof(s) << endl;//16
//当父类中出现同名成员时,需要加作用域区分
cout << "Base1 m_a = " << s.Base1::m_a << endl;
cout << "Base2 m_a = " << s.Base2::m_a << endl;
}
int main()
{
test01();
return 0;
}
运行结果如下:
通过工具验证子类对象所占内存大小:
菱形继承问题以及解决方法
菱形继承概念:
- 两个派生类继承同一个基类
- 又有某个类同时继承这两个派生类
- 这种继承被称为菱形继承或者钻石继承
菱形继承的问题
- 某个类继承两个派生类之后使用派生类中的数据会产生二义性
- 数据会出现两份,实际我们只需要一份数据即可
下面这段代码的运行结果解决了菱形继承的两个问题
#include <iostream>
using namespace std;
class Animal
{
public:
int m_age;
};
//利用虚继承可以解决菱形继承的问题
//在家继承之前加上关键字virtual变为虚继承
//Animal类称为虚基类
class Sheep :virtual public Animal
{
};
class Tuo :virtual public Animal
{
};
class SheepTuo :public Sheep, public Tuo
{
};
void test01()
{
SheepTuo st;
st.Sheep::m_age = 18;
st.Tuo::m_age = 28;
//当菱形继承,两个父类拥有相同成员时,需要加以作用域区分
cout << "Sheep::m_age = " << st.Sheep::m_age << endl;
cout << "Tuo::m_age = " << st.Tuo::m_age << endl;
cout << "st.m_age = " << st.m_age << endl;
//这份数据我们知道只要有一份就可以了,菱形继承导致了数据有两份,资源浪费
//要解决菱形继承的问题,我们就需要了解虚继承
}
int main()
{
test01();
return 0;
}
运行结果如下所示:
所以为什么虚继承可以解决菱形继承所带来的问题,我们此时就要打开开发人员命令工具查看一下羊驼这个类的布局到底是什么情况,如下图所示:
🤣🤣🤣
时隔多日