C++ 之继承
文章目录
0.利用 cl.exe
工具查看某个类的对象内存模型
Visual Studio
自带了一个很有用的工具cl.exe
。使用该工具可以查看 【该类对象的内存模型】
。
该工具的使用方法:
- 找到
VS 20XX的开发人员命令提示符
,单击打开 - 使用
cd
命令,跳转至我们要查看的类所在目录 - 使用命令:
cl /d1 reportSingleClassLayout类名 "文件名"
即可查看【指定类对象的内存模型】
例如:cl /d1 reportSingleClassLayoutDerive "DeriveDemo.cpp"
1.继承的基本语法
在为某个基类定义派生类对象时,可以指定继承的方式:public 继承、protected 继承、private 继承。
class Base{
public:
Base(){}
~Base(){}
};
// : 之后指定继承的方式,此处选择的 public 继承
class Derive1:public Base{
public:
Derive1(){}
~Derive1(){}
};
// : 之后指定继承的方式,此处选择的 protected继承
class Derive2:protected Base{
public:
Derive2(){}
~Derive2(){}
};
// : 之后指定继承的方式,此处选择的 private继承
class Derive3:private Base{
public:
Derive3(){}
~Derive3(){}
};
2.不同继承方式区别
- public 继承
class Base{
public:
int m_BaseA;
protected:
int m_BaseB;
private:
int m_BaseC;
};
class Derive public Base{
public:
int m_DeriveD;
};
此时,派生类
Derive
的实例化对象中包含:m_BaseA
、m_BaseB
、m_BaseC
、m_Derive
四个成员,其中前三个继承自基类Base
,【基类的私有成员变量m_BaseC
被编译器隐藏,子类中有这个变量,但是没有权限访问该变量】。
m_BaseA
在派生类中的访问权限是 【public】,m_BaseB
在派生类中的访问权限是 【protected】 。
- protected 继承
class Base{
public:
int m_BaseA;
protected:
int m_BaseB;
private:
int m_BaseC;
};
class Derive protected Base{
public:
int m_DeriveD;
};
此时,派生类
Derive
的实例化对象中包含:m_BaseA
、m_BaseB
、m_BaseC
、m_Derive
四个成员,其中前三个继承自基类Base
,【基类的私有成员变量m_BaseC
被编译器隐藏,子类中有这个变量,但是没有权限访问该变量】。
m_BaseA
在派生类中的访问权限是 【protected】,m_BaseB
在派生类中的访问权限是 【protected】 。
- private 继承
class Base{
public:
int m_BaseA;
protected:
int m_BaseB;
private:
int m_BaseC;
};
class Derive private Base{
public:
int m_DeriveD;
};
此时,派生类
Derive
的实例化对象中包含:m_BaseA
、m_BaseB
、m_BaseC
、m_Derive
四个成员,其中前三个继承自基类Base
,【基类的私有成员变量m_BaseC
被编译器隐藏,子类中有这个变量,但是没有权限访问该变量】。
m_BaseA
在派生类中的访问权限是 【private】,m_BaseB
在派生类中的访问权限是 【private】 。
因此,我们可以得出一个结论:
继承方式只会提升访问权限低于该继承方式的成员变量的访问权限
。例如:继承方式是public,则基类中所有权限在派生类中不变;继承方式是protected,则基类中public 成员会提升至protected,其余不变;继承方式是private,则public,protected成员会被提升至private。
3.继承中的对象模型
使用,cl.exe
工具查看,上述例子中 Derive 类对像的内存模型。我们可以得知,在 Derive 类的内存模型中,基类 Base 的成员在内存模型的上边,Derive 自己定义的成员在内存模型的下边。 同时,【基类的私有成员是包含在派生类中的,只是被编译器隐藏,派生类无法访问】。
4.构造函数与析构
#include <iostream>
using namespace std;
class Base{
public:
Base(){
cout << "Base constructor run" << endl;
}
~Base(){
cout << "~Base destructor run" << endl;
}
};
class Derive:public Base{
public:
Derive(){
cout << "Deriveconstructor run" << endl;
}
~Derive(){
cout << "~Derivedestructor run" << endl;
}
};
void test(){
Derive d1;
}
int main(int argc, char* argv[])
{
test();
// Base constructor run
// Deriveconstructor run
// ~Derivedestructor run
// ~Base destructor run
return 0;
}
由本例我们可以得知:定义派生类对象时,会首先调用基类构造函数,然后调用派生类构造函数,析构函数与构造函数调用顺序相反。
5.继承中基类与派生类同名成员
5.1 派生类含有与基类同名的成员变量
#include <iostream>
using namespace std;
class Base{
public:
Base(){
m_number = 1;
m_age = 25;
}
int m_number;
protected:
int m_age;
};
class Derive:public Base{
public:
Derive(){
m_number = 10;
}
int m_number;
int getNumber(){
return m_number;
}
};
int main(int argc, char const *argv[])
{
Derive d;
cout << d.m_number << endl; // 10
cout << d.Base::m_number << endl; // 1
return 0;
}
通过cl.exe
工具,我们查看派生类 Derive 对象内存分布如下:
派生类中同时拥有来自基类的 m_number
成员变量与自身定义的m_number
成员,直接访问m_number
访问的时派生类自己定义的m_number
,此时,如果想要访问基类来自基类的m_number
,只需要添加基类作用域即可,如d.Base::m_number
通过作用域Base::
明确指定,我们要访问的时基类的m_number
。
5.2 派生类含有与基类同名的成员函数
5.2.1 同名且同类型
#include <iostream>
using namespace std;
class Base{
public:
void fun(){
cout << "Base::fun..." << endl;
}
};
class Derive:public Base{
public:
void fun(){
cout << "Derive::func..." << endl;
}
};
int main(int argc, char const *argv[])
{
Derive d;
d.fun(); // Derive::fun...
return 0;
}
派生类出现 同名函数 会 【
隐藏基类中所有同名成员函数
】。
此处,基类的fun
函数被派生类同名函数fun
所隐藏,因此此处调用的是派生类的fun
函数
5.2.2 同名不同参
#include <iostream>
using namespace std;
class Base{
public:
void fun(){
cout << "Base::fun..." << endl;
}
};
class Derive:public Base{
public:
void fun(int){
cout << "Derive::func(int)..." << endl;
}
};
int main(int argc, char const *argv[])
{
Derive d;
d.fun(); // Error
d.fun(1); // Derive::func(int)...
return 0;
}
派生类中如果声明了基类的同名函数,
即使参数列表不同
,新声明的同名函数也会将**【基类中的同名函数隐藏,在派生类中无法访问】**。
本例,d.fun()
调用出错,因为 派生类声明了void fun(int)
同名函数,虽然参数列表不同,也会将基类中的同名函数隐藏,导致无法访问,因此此处报错。
而,派生类声明了void fun(int)
,故而d.fun(1)
调用不会出错。
如果想要调用基类中的同名成员函数,可以添加基类作用域,这样即使基类成员函数已被隐藏,也可以调用,如:d.Base::fun()
。
6.继承中基类与派生类同名静态成员处理
#include <iostream>
using namespace std;
class Base{
public:
static void fun(){
cout << "static Base::fun..." << endl;
}
static int s_num;
static int s_age;
};
int Base::s_num = 1;
int Base::s_age = 2;
class Derive:public Base{
public:
static void fun(int){
cout << "static Derive::func..." << endl;
}
int s_num = 100;
static int s_age;
};
int Derive::s_age = 200;
int main(int argc, char const *argv[])
{
Derive d;
d.Base::fun(); // static Base::fun...
// d.fun(); // Error
d.fun(1); // static Derive::func...
cout << d.Base::s_num << endl; // 1
cout << Derive::Base::s_num << endl; // 1
cout << d.s_num << endl; // 100
cout << d.s_age << endl; // 200
cout << Derive::s_age << endl; // 200
return 0;
}
在本例中,我们定义了三种情况的同名情况:
- 派生类
普通成员变量
与基类静态变量
同名
访问基类静态成员同样可以通过添加基类作用域实现,只不过访问静态成员变量时也可以直接使用
派生类::基类::成员
的方式访问,如:Derive::Base::s_num
。
- 派生类
静态变量
与基类静态变量
同名
同非静态变量性质类似,访问基类需要添加基类作用域限定。
- 派生类
静态函数
与基类静态函数
同名不同参
静态成员函数,同非静态成员函数性质类似,同名同样会发生**派生类同名成员函数隐藏基类同名成员函数。**访问基类的静态成员函数,添加基类作用域限定符即可。
Tips:
类内含静态成员变量时,其初始化需要在类外完成。
例如,Base 类内定义了static int s_age;
则,类外初始化需要去掉 static 关键字
添加类作用域再赋值,如:int Base::s_age = 100;
7.菱形继承问题
C++ 允许派生类继承多个父类,此即多继承,语法如下:
class C:public B1,public B2{};
那什么时菱形继承呢?
下图可以形象的说明菱形继承问题,B1、B2 继承自 A,当 C 把 B1、B2 同时当作父类时,C 类中会保存两份来自 A 类的数据。
#include <iostream>
using namespace std;
class A{
public:
int m_A;
};
class B1:public A{
public:
int m_B1;
};
class B2:public A{
public:
int m_B2;
};
class C:public B1, public B2{
public:
int m_C;
};
int main(int argc, char const *argv[])
{
C c;
return 0;
}
此时,使用 cl.exe
工具,查看类 C 的内存模型,我们发现 类 C 中 保存了两份来自 A 的成员变量 m_A,此时在使用 c.m_A
使用该变量时会出现歧义,同时保存两份数据也完全没有必要。
为了解决菱形继承的问题,C++ 引入了
虚继承
的概念,即将中间层的类 B1、B2 继承方式前添加virtual
关键字,这时 B1、B2 变成了虚基类,C 类继承 B1、B2 时指挥保存一份来自 A 的数据。
#include <iostream>
using namespace std;
class A{
public:
int m_A;
};
class B1:virtual public A{
public:
int m_B1;
};
class B2:virtual public A{
public:
int m_B2;
};
class C:public B1, public B2{
public:
int m_C;
};
int main(int argc, char const *argv[])
{
C c;
return 0;
}
此时,类 C 的内存模型如下图所示:可以发现,类 C 中只包含一份 m_A
的数据了,解决了上述的问题。