C++之 继承 (inheritance)
目录
启示
一、基本语法
二、继承的方式
三种:
公共基础 / 保护继承 / 私有继承
三、继承中的对象模型
①父类中所有非静态成员属性都会继承给子类
②而私有成员属性同样继承过去,但是被编译器隐藏,因此无法访问
四、继承中构造和析构顺序
构造的顺序: 父类 > 子类
析构的顺序: 子类 > 父类
五、继承同名成员处理方式
六、继承同名静态成员处理方式
七、多继承
八、菱形继承
启示
首先,对于一个学校,他有自己的学院/部门,学院下有自己的专业,如图
继承就是承接上一级的内容,同时延续自己下一级的内容
一、基本语法
class 子类 : 继承方式 父类
{}
子类又称派生类 ,体现了个性
父类又称基类,体现了共性
继承方式有public/private/protected
二、继承的方式
三种:
公共基础 / 保护继承 / 私有继承
他们有如下关系
显而易见的,父类中的private,无论是哪种继承方式,子类都不可访问
接下来我们使用代码进行测试
首先是public公共继承
class B :public A
{
public:
void func()
{
a = 1;// 父类中公共权限成员,到子类中仍然是公共权限成员
b = 2;// 父类中保护权限成员,到子类中仍然是保护权限成员
c = 3;// 父类中私有权限成员,不可访问
}
};
c不可访问
同时,带上测试函数
因为b是保护权限成员,只能在类内访问,类外不可访问
然后是protected保护继承
class B :protected A // 保护继承
{
public:
void func()
{
a = 1;// 父类中公共权限成员,到子类中变成是保护权限成员
b = 2;// 父类中保护权限成员,到子类中仍然是保护权限成员
c = 3;// 父类中私有权限成员,不可访问
}
};
c不可访问
接下来是测试函数
可以看到,由于a/b是保护权限成员,因此类外都不能访问
最后是private私有继承
class B :private A // 私有继承
{
public:
void func()
{
a = 1;// 父类中公共权限成员,到子类中变成是私有权限成员
b = 2;// 父类中保护权限成员,到子类中仍然是私有权限成员
c = 3;// 父类中私有权限成员,不可访问
}
c不可访问
测试函数:
同样的,由于a/b是私有权限成员,因此类外都不能访问
三、继承中的对象模型
①父类中所有非静态成员属性都会继承给子类
②而私有成员属性同样继承过去,但是被编译器隐藏,因此无法访问
class Base // 父类
{
public:
int a;
protected:
int b;
private:
int c;
};
class Son :public Base
{
public:
int d;
};
void test01()
{
cout << sizeof(Son) << endl;
}
接下来我们使用vs的开发人员命令提示符工具展示真实的分布情况
①打开 vs2022的开发人员命令提示符工具
② 跳转盘符:输入F:,点击回车
③再接着输入 cd 具体路径, 点击回车
④接下来输入 cl d1 reportSingleClassLayout子类名 文件名
文件名可以输入首字符,然后按一下Tab,就会自动补全
可以看到子类Son,父类Base,总Size是16。
子类包含父类的abc变量和自身的d
四、继承中构造和析构顺序
子类继承父类后,创建子类对象,也会调用父类的构造函数
那么构造和析构的顺序是什么?
下面创建父类Base与子类Son,并添加各自的构造和析构函数
class Base // 父类
{
public:
Base()
{
cout << "父类构造函数" << endl;
}
~Base()
{
cout << "父类析构函数" << endl;
}
};
class Son :public Base // 子类
{
public:
Son()
{
cout << "子类构造函数" << endl;
}
~Son()
{
cout << "子类析构函数" << endl;
}
};
void test01()
{
Son s;
}
因此,
构造的顺序: 父类 > 子类
析构的顺序: 子类 > 父类
五、继承同名成员处理方式
当父类和子类出现同名对象或函数
①访问子类同名成员,直接访问
②访问父类同名成员,加作用域(所有同名)
创建父类Base和子类Son,并在2者种均加入属性m_A,在测试函数test01中尝试输出m_A
class Base
{
public:
Base()
{
m_A = 50;
}
int m_A;
};
class Son :public Base
{
public:
Son()
{
m_A = 100;
}
int m_A;
};
直接输出,是子类同名成员
若要输出父类同名成员,要加上作用域
同理,如果要输出同名成员函数,也是要加父类的作用域
class Base
{
public:
Base()
{
m_A = 50;
}
void func()
{
cout << "Base_func" << endl;
}
int m_A;
};
class Son :public Base
{
public:
Son()
{
m_A = 100;
}
void func()
{
cout << "Son_func" << endl;
}
int m_A;
};
void test02()
{
Son s;
s.func();
s.Base::func();
}
同时,如果子类出现了和父类的同名函数,子类将会隐藏掉父类中所有的同名函数,如下
class Base
{
public:
Base()
{
m_A = 50;
}
void func()
{
cout << "Base_func" << endl;
}
void func(int a) // 同名函数,而且重载
{
cout << "Base_fun(int a)" << endl;
}
int m_A;
};
class Son :public Base
{
public:
Son()
{
m_A = 100;
}
void func()
{
cout << "Son_func" << endl;
}
int m_A;
};
子类中有func函数,而父类也有func函数以及其重载函数func(int a)
此时即使子类没有同名重载函数,也不能直接调用重载函数,因为被隐藏
需要加父类的作用域
六、继承同名静态成员处理方式
与同名成员相同的处理方式
①访问子类同名成员,直接访问
②访问父类同名成员,加作用域(所有)
父类Base与子类Son,都有静态成员static m_a;
class Base
{
public:
static int m_a; // 类内声明
};
int Base::m_a = 10; // 类外初始化
class Son :public Base
{
public:
static int m_a;
};
int Son::m_a = 5;
接下来使用2种方式访问:①通过对象访问②通过类名访问
void test01()
{
// 通过对象访问
Son s;
cout << s.m_a << endl;
cout << s.Base::m_a << endl;
// 通过类名访问
cout << Son::m_a << endl;
cout << Base::m_a << endl;
cout << Son::Base::m_a << endl;
}
值得注意的是,Son::Base::m_a ,意思是,
Son::通过类名访问Base::作用域下的m_a数据
而Base::m_a是直接通过类名访问m_a
同样的,如果要访问静态成员函数,也是加作用域或不加作用域
class Base
{
public:
static int m_a;
static void func()
{
cout << "Base的静态成员函数" << endl;
}
};
int Base::m_a = 10;
class Son :public Base
{
public:
static int m_a;
static void func()
{
cout << "Son的静态成员函数" << endl;
}
};
int Son::m_a = 5;
void test02()
{
// 通过对象访问
Son s;
s.func();
s.Base::func();
// 通过类名访问
Son::func();
Base::func();
Son::Base::func();
}
同理,如果父类中出现了同名静态成员函数的重载,也会被隐藏,在调用时必须要加父类的作用域
七、多继承
语法:
class 子类: 继承方式 父类1,继承方式 父类 2......
不建议使用
例:
class Base1 // 父类1
{
public:
Base1()
{
m_a = 100;
}
int m_a;
};
class Base2 // 父类2
{
public:
Base2()
{
m_b = 200;
}
int m_b;
};
//class 子类: 继承方式 父类1,继承方式 父类 2......
class Son :public Base1, public Base2
{
public:
Son()
{
m_c = 300;
m_d = 400;
}
int m_c;
int m_d;
};
使用Son类创建对象,并查看其大小
void test01()
{
Son s;
cout << sizeof(s) << endl;
}
4个int,大小16
使用开发人员命令提示符也可以看到包含父类1和父类2的两个int,以及自己的2个int,一共是16
而当父类中出现同名成员时,不可以直接输出
class Base1 // 父类1
{
public:
Base1()
{
m_a = 100;
}
int m_a;
};
class Base2 // 父类2
{
public:
Base2()
{
m_a = 200;
}
int m_a;
};
//class 子类: 继承方式 父类1,继承方式 父类 2......
class Son :public Base1, public Base2
{
public:
Son()
{
m_c = 300;
m_d = 400;
}
int m_c;
int m_d;
};
可以看到,父类1和父类2都有m_a
尝试输出时,会提示不明确,因为重名了
因此,需要加作用域
八、菱形继承
①传电动汽车继承了汽车的price,纯汽油汽车同样继承了汽车的price,当混合动力汽车使用公共属性price时,就会产生二义性
②混合动力汽车继承自汽车的属性price继承了2份,造成额外开销,只需一份即可
代码:
class car // 父类 车类
{
public:
int price;
};
class pure_gasoline_car:public car // 纯汽油汽车
{};
class pure_electric_vehicle:public car // 纯电动汽车
{};
class hybrid_electric_vehicle :public pure_gasoline_car, public pure_electric_vehicle //混合动力汽车
{};
而在测试函数中,不可直接调用任何一个的price
void test01()
{
hybrid_electric_vehicle c;
c.price = 10;
}
必须要加上作用域
void test01()
{
hybrid_electric_vehicle c;
c.pure_gasoline_car::price = 10;
c.pure_electric_vehicle::price = 20;
cout << c.pure_gasoline_car::price << endl;
cout << c.pure_electric_vehicle::price << endl;
}
打印输出
但是,菱形继承导致数据有2份,造成资源浪费
使用开发人员命令提示符工具查看
确实会有2份
接下来,使用虚继承解决
// 在public前加上virtua,变为虚继承
class pure_gasoline_car:virtual public car // 纯汽油汽车
{};
class pure_electric_vehicle:virtual public car // 纯电动汽车
{};
在2个子类的继承方式前加上virtual,即变成虚继承
此时,原父类/基类称为虚基类
再次输出
可以发现 打印出了同样的数据,因为此时两个子类的price共用同一块空间
因此我们不再需要区分作用域,也可以直接打印出price
使用开发人员命令提示符工具查看
可以看到,pure_gasoline_car的vbptr(虚基类指针)指向它的虚基类表(下面红色),起始位置是0(左边红色),偏移量是8(下面虚基类表里写的8),因此指向8的位置
同理,pure_electric_vehicle的虚基类指针从4偏移4到8的位置,也指向8
因此,两个虚基类指针指向同一块空间