C++梁哥笔记day15 | 我的日常分享

C++梁哥笔记day15

类与对象

一、非自动继承的函数

不是所有的函数都能自动从基类继承到派生类中。构造函数和析构函数用来处理对象的创建和析构操作,构造和析构函数只知道对他们的特定层次的对象做什么,也就是说构造函数和析构函数不能被继承,必须为每一个特定的派生类分别创建。另外operator=也不能被继承,因为它完成类似构造函数的行为,父类创建的对象=知道怎么取赋值,但是子类中有自己新的成员,子类创建的对象=是不知道怎么去赋值的。在继承的过程中,如果没有创建这些函数,编译器会自动生成他们,默认的浅拷贝。

二、子类中静态成员与父类同名分析

2.1 静态成员函数和非静态成员变量

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
class Base{
public:
static int num;
static int data;
};
int Base::num = 10;
int Base::data = 20;

class Derived:public Base{
public:
static int data;
};
int Derived::data = 30;

void test01(){
//父类与子类中的成员变量没有重名
cout<<"父类中的num:"<<Base::num<<endl;
cout<<"子类从父类继承的num:"<<Derived::num<<endl;
cout<<"*************"<<endl;
//父类与子类中的成员变量重名
cout<<"父类中的data:"<<Base::data<<endl;
cout<<"子类从父类继承的data:"<<Derived::Base::data<<endl;//当同名时,要加父类作用域修饰 Base::
cout<<"子类中的data:"<<Derived::data<<endl;
}

运行结果:
图片

2.2 静态成员函数和非静态成员函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
class Base{
public:
static int num;
static int data;
static void showNum(){
cout<<"父类中的showNum"<<endl;
}
};
int Base::num = 10;
int Base::data = 20;

class Derived:public Base{
public:
static int data;
static void showNum(){
cout<<"子类中的showNum"<<endl;
}
};
int Derived::data = 30;

void test02(){
//父类中的 showNum
Base::showNum();
//子类中的showNum
Derived::showNum();
//子类中从父类继承的showNum 同名了加父类作用域Base::
Derived::Base::showNum();
}

运行结果:
图片


三、多继承

3.1 多继承概念

我们可以从一个类继承,我们也可以同时从多个类继承,这就是多继承。但是由于多继承非常受争议,从多个类继承可能会导致函数、变量等同名导致较多的歧义。
一般我们要减少使用多继承,所有的多继承都可以使用单继承来完成。

多继承格式:

1
2
3
class 子类名:继承方式 父类1,继承方式 父类2,继承方式 父类3···{

};

案例1:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
class Base1{
public:
int a;
};
class Base2{
public:
int b;
};

class Derived:public Base1,public Base2{
public:
int c;
};
void test01(){
Derived ob1;
ob1.a = 10;
ob1.b = 20;
ob1.c = 30;
cout<<"a:"<<ob1.a<<",b:"<<ob1.b<<",c:"<<ob1.c<<endl;
}

运行结果:
图片

多继承会带来一些二义性的问题,如果两个基类中有同名的函数或者变量,那么通过派生类对象去访问这个函数或者变量的时候就不能明确到底调用从基类1继承的版本还是从基类2中继承的版本?解决方法就是加作用域显示指定调用哪个基类的版本。

3.2 菱形继承(具有公共祖先)

两个派生类继承同一个基类而又有某个类同时继承者两个派生类,这种继承被称为菱形继承,或者钻石型继承。

图片

这种继承所带来的问题:

  1. 羊继承了动物的数据和函数,驼同样继承了动物的数据和函数,当草泥马调用数据或者函数时,就会产生二义性。
  2. 草泥马继承自动物的数据和函数继承了两份,其实我们应该清楚,这份数据或函数我们只需要一份就可以。

上述问题如何解决?对于调用二义性,那么可通过指定调用那么基类的方式即加作用域来解决;那么重复继承怎么解决呢?对于这种菱形继承所带来的两个问题,C++为我们提供了一种方式,采用虚基类。

普通继承方式下利用作用域解决问题1:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
class Animal{
public:
int age;
};
class Sheep:public Animal{

};
class Tuo:public Animal{

};

class SheepTuo:public Sheep,public Tuo{

};
void test01(){
SheepTuo ob;
//ob.age = 100;//error: request for menben 'age' is ambiguous
//解决二义性 要使用作用域
ob.Sheep::age = 10;
ob.Tuo::age = 20;
cout<<"ob.Sheep::age ="<<ob.Sheep::age<<endl;
cout<<"ob.Tuo::age ="<<ob.Tuo::age<<endl;
}

运行结果:
图片

3.3 菱形继承在普通继承方式下的内存布局

利用vs studio查看
Animal类

1
2
3
4
class Animal{
public:
int age;
};

图片
Sheep类

1
2
3
class Sheep:public Animal{

};

图片
Tuo类

1
2
3
class Tuo:public Animal{

};

图片
SheepTuo类

1
2
3
class SheepTuo:public Sheep,public Tuo{

};

图片

3.4 虚继承 解决菱形继承的问题

virtual修饰继承方式

1
2
3
4
5
//虚继承格式
//父类称为:虚基类
class 子类名:virtual public 父类名{

};

3.5 菱形继承在虚继承方式下的内存布局

Animal类

1
2
3
4
class Animal{
public:
int age;
};

图片
Sheep类

1
2
3
class Sheep:virtual public Animal{

};

图片
Tuo类

1
2
3
class Tuo:virtual public Animal{

};

图片
SheepTuo类

1
2
3
class SheepTuo:public Sheep,public Tuo{

};

图片

通过内存图,Animal是菱形最顶层的类,内存布局图没有发生改变。Sheep和Tuo通过虚继承的方式派生自Animal,从这两个类的内存布局图中可以看出编译器为我们的对象中增加了一个vbptr(virtual base pointer),vbptr指向了一张表,这张表保存了当前的虚指针相对于虚基类Animal的首地址的偏移量。SheepTuo派生于Sheep和Tuo,继承了两个基类的vbptr指针,并调整了vbptr与虚基类的首地址的偏移量。
由此可见编译器帮我们做了一些幕后工作,是的这种菱形问题在继承时候能,只继承一份数据,并且解决了二义性的问题。现在模型就变成了Sheep、Tuo和SheepTuo三个类共享了一份Animal数据。
当使用虚继承时,虚基类是被共享的,也就是在继承体系中无论被继承多少次,类内存模型中只会出现一个虚基类的子对象(这和多继承是完全不同的)。即使共享虚基类,但是必须要有一个类来完成基类的初始化(因为所有的对象都必须被初始化,哪怕是默认的),同时还不能够重复进行初始化,那到底谁应该负责完成初始化呢?C++标准中选择在每一次继承子类中都必须书写初始化语句(因为每一次继承子类可能都会用来定义对象),但是虚基类的初始化是由最后的子类完成,其他的初始化语句都不会调用。

vbptr(虚基类指针)其中v是virtual虚 b是base基类 ptr是指针
vbtable(虚基类表)
vbptr指向虚基类表 虚基类表存放的是vbptr与虚基类的首地址的偏移量

总结:之所以产生vbptrvbtable的目的是保证不管继承多少个虚基类,数据只有一份。

3.6 通过C语言的方式去操作虚继承(了解)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
#include <iostream>

using namespace std;
class Animal {
public:
int age;
char* name;
};
class Sheep :virtual public Animal {

};
class Tuo :virtual public Animal {

};

class SheepTuo :public Sheep, public Tuo {

};

int main(int argc, char* argv[])
{
SheepTuo ob;
//通过对ob去地址得到SheepTuo中从Sheep中继承到的vbptr的地址
int* sheep_vbptr = (int *)&ob;
//sheep_vbptr指向的是虚基表 得到虚基表的位置
int* sheep_vbtable = (int *)*sheep_vbptr;
//虚基表中0位置没有保存任何信息 偏移量保存在1位置中
int* offset_ptr = sheep_vbtable + 1;
//打印偏移量
//int offset = (int)*((int *)(*(int *)&ob)+1);
int offset = int(*offset_ptr);
cout << *offset_ptr << endl;//8
ob.age = 10;
//得到偏移量就可以操作ob中的数据了 通过偏移量就可以得到虚基类Animal的首地址也是第一个数据首地址
cout << ((Animal*)((char*)&ob + offset))->age << endl;
cout << (*(Animal*)((char*)&ob + offset)).age << endl;
return 0;
}

图片

3.7 虚继承总结

注意:虚继承只能解决具备公有祖先的多继承所带来的二义性问题,不能解决没有公共祖先的多继承带来的二义性问题。

工程开发中真正意义上的多继承是几乎不被使用的,因为多重继承带来的代码复杂性远多于其带来的便利,多重继承对代码维护性上的影响是灾难性的,在设计方法上,任何多继承都可以用单继承来代替。