继承是很大程度上解决了代码重复的问题。比如狗和猫都属于动物,我们需要在猫和狗的类里面写上动物年龄,动物名字等。但是动物都可以有年龄和起名称,这个时候我们写个动物类,把动物都有的属性写进去,狗和猫继承动物类就好了,就不需要对狗和猫的类里面分别写年龄和名字了。
本篇博客主要记录以下问题:
一、基本语法和继承方式
二、继承中的对象模型
三、继承中对象的构造析构顺序
四、继承同名成员的处理方式(包括普通成员、静态成员)
五、多继承语法
六、棱形继承
一、基本语法和继承方式
1.基本语法
继承的基本格式为:
class 子类名 :继承方式 父类名
还允许一个类继承多个类,多继承语法格式为:
class 子类名 : 继承方式 父类名1,继承方式 父类名2…
2.继承方式
继承方式分为公有(public)继承,保护(protect)继承,私有(private)继承。
不同的继承方式,继承的成员的属性会发生变化,如下图:
总结:
(1)首先,不管哪种继承方法,父类的私有成员子类都没办法访问。
(2)子类以公有方式继承父类的话,父类的公有成员在子类中依旧是公有属性,保护成员依旧是保护属性。
(3)子类以私有方式继承父类的话,父类的公有成员和保护属性成员都统一为保护属性。
二、继承中的对象模型
代码:
class A
{
public:
int a_a;
protected:
int a_b;
private:
int a_c;
};
class B :public A
{
int Geta_a()
{
return a_a;
}
int Geta_b()
{
return a_b;
}
/*//不可访问a_c,因为a_c是父类的私有属性,子类不可访问
int Geta_c()
{
return a_c;
}
*/
private:
int b_a;
};
父类的私有成员,子类无法访问。无法访问不代表没有继承。父类的所有成员,子类都会完全继承下来,只不过编译器将继承下来的私有成员给隐藏了,不允许子类访问。
比如我们用类大小看一下:
int main()
{
cout << "sizeof(class B) = " << sizeof(B) << endl;
return 0;
}
输出结果如下:
类B的大小为16字节。类B自己有一个int类型成员。那么还有12个字节都是从父类继承下来的。所以父类三个成员都被子类继承了,包括不允许子类访问的私有属性。
我们可以用vs2019的开发者工具进行查看:
发现从A类继承下来的是有私有成员a_c的,只不过编译器对其进行了隐藏不让访问而已。
总结:
父类的所有非静态成员,子类都会完全继承下来,只不过编译器将继承下来的私有成员给隐藏了,不允许子类访问。
三、继承中的构造和析构顺序
代码:
class A
{
public:
A()
{
cout << "这里是A构造函数" << endl;
}
~A()
{
cout << "这里是A析构函数" << endl;
}
public:
int a_a;
protected:
int a_b;
private:
int a_c;
};
class B :public A
{
public:
B()
{
cout << "这里是B构造函数" << endl;
}
~B()
{
cout << "这里是B析构函数" << endl;
}
private:
int b_a;
};
int main()
{
B b;
return 0;
}
发现是类A先调用构造函数的,这是因为只有A类先调用构造函数,类B构造的对象才能拿到类A的成员。
四、继承同名成员的处理方式
继承会引发一些问题,比如说如果子类有和父类相同名称的成员,或者说子类继承了两个父类,两个父类有同名的成员,这样的话,使用成员的时候,编译器会犯难,不知道要使用哪一个成员。这里我们拿子类和父类同名成员时的处理方式。
1.继承普通同名成员的处理方式
1.继承普通同名成员变量的处理方式
class A
{
public:
int a;
int b;;
private:
int c;
};
class B :public A
{
public:
int a;
};
int main()
{
B b;
b.a = 10; //B类里面本身的变量a
b.A::a = 20;//B类继承的成员变量a
cout << b.a << " " << b.A::a << endl;
b.b = 20; //B类继承的成员变量b
b.A::b = 30; //B类继承的成员变量b
cout << b.b << " " << b.A::b << endl;
return 0;
}
运行结果:
总结:
(1)当访问的成员变量不是同名成员变量时,直接以对象点的形式访问就可以。
(2)当访问的成员变量是同名成员变量时,以对象点的方式访问本类本身的同名成员变量,以对象点+作用域的方式访问父类的成员变量。
2.继承普通同名成员函数的处理方式
代码:
class A
{
public:
void fun()
{
cout << "this is A fun()" << endl;
}
void fun(int tmp)
{
cout << "this is fun(int)" << endl;
}
public:
int a;
int b;;
private:
int c;
};
class B :public A
{
public:
void fun()
{
cout << "this B fun()" << endl;
}
};
int main()
{
B b;
b.fun(); //B类fun()
//b.fun(10);//error
b.A::fun();//A类fun()
b.A::fun(10);//A类fun(int)
return 0;
}
运行结果:
当子类与父类有同名成员函数的时候,发现哪怕父类的成员函数和子类的成员函数形参列表不同(比如父类的fun(int)和子类的fun()明显不同,构成了函数重载),但是子类对象却不能以对象点的方式调用父类函数fun(int)。这是由于只要子类有同名的成员函数,那么父类的同名成员函数都会被隐藏,统一不能以对象点的方式调用,必须以对象点加作用域的方式调用。
总结:
(1)如果子类和父类不存在同名的函数,那么直接以对象点的方式调用即可。
(2)如果子类和父类的成员函数的函数名相同,那么对于父类的所有同名函数,子类必须以对象点+作用域的方式调用父类的同名函数。此时子类以对象点的方式调用子类本身的同名函数。
2.继承同名静态成员的处理方式
首先,要知道静态成员只有一个,继承的话也是只有一个,即子类和父类的对象都共用同一个静态成员。
如下验证代码:
class A
{
public:
static int a;
int b;
};
int A::a = 10;
class B :public A {};
int main()
{
A a;
cout << a.a << endl;
B b;
cout << b.a << endl;
b.a = 20;
cout << a.a << " " << b.a << endl;
cout << &a.a << " " << &b.a << endl;
return 0;
}
通过类B对象修改静态成员变量的值,类A的静态成员变量值也会修改,打印出来地址,两个类的对象都是用同一个静态成员变量。
1.继承同名静态成员变量的处理方式
代码:
class A
{
public:
static int a;
int b;
};
int A::a = 10;
class B :public A
{
public:
static int a;
};
int B::a = 20;
int main()
{
B b;
cout << b.a << endl; //访问的是子类本身的静态成员变量
cout << b.A::a << endl;//访问的是父类的静态成员变量
cout << B::A::a << endl;//访问的是父类的静态成员变量
return 0;
}
总结:
发现有同名静态成员变量时,和同名普通成员变量处理方式一样。只不过对于静态成员变量,多了一个直接用域名访问的方式B::A::a。
2.继承同名静态成员函数的处理方式
class A
{
public:
static void fun()
{
cout << "this is A fun()" << endl;
}
static void fun(int tmp)
{
cout << "this is A fun(int)" << endl;
}
public:
int a;
int b;;
private:
int c;
};
class B :public A
{
public:
static void fun()
{
cout << "this B fun()" << endl;
}
};
int main()
{
B b;
//B类本身的静态fun()函数
b.fun();
B::fun();
//调用继承的静态fun函数
b.A::fun();
b.A::fun(10);
B::A::fun();
B::A::fun(10);
return 0;
}
运行结果:
总结:
发现有同名静态成员函数时,和同名普通成员函数处理方式一样。只不过对于静态成员函数,多了一个直接用域名访问的方式B::A::fun()。
但是注意,两个"::"的意义不同,第一个代表用类名访问,第二个是代表A类作用域下。
五、棱形继承(钻石继承)
1.棱形继承的概念
两个派生类继承一个基类,又有一个派生类继承这两个派生类。
如下图:
可能举例不太恰当,但是就是这么个意思。
2.棱形继承带来的问题
1.羊继承了动物的数据,驼同样继承了动物的数据,羊驼继承了羊和驼的数据,继承本身是有继承性的,那么羊驼就有了两份动物的数据。
2.羊驼继承自动物的数据有两份,造成了资源浪费。
总的来说就是相同作用的数据有两份,就这么个问题。
平常中,是不允许这样继承的,尽量避免多继承和棱形继承的发生。
class Animal
{
public:
int m_age;
};
class Sheep :public Animal {};
class Tuo :public Animal {};
class SheepTuo :public Sheep, public Tuo {};
int main()
{
SheepTuo st;
//st.m_age;//error,产生二义性,编译器不知道用Sheep的m_age还是Tuo的m_age
st.Sheep::m_age; //访问Sheep的m_age
st.Tuo::m_age; //访问Tuo的m_age
return 0;
}
类SheepTuo的m_age有两份,但是只需要一份就够了,所以资源浪费。
可以清楚的看见Sheep 和Tuo 分别从Animal继承了m_age,而SheepTuo 又继承了Sheep 和Tuo,所以SheepTuo有两份不同作用域下的m_age。我们要做的就是让SheepTuo的m_age只有一份。
下面解决数据有两份的问题。
3.解决棱形继承带来的问题
利用虚继承解决棱形继承问题。
虚继承,即在继承方式前面再加一个virtual关键字。
class Animal
{
public:
int m_age;
};
class Sheep :virtual public Animal {};
class Tuo :virtual public Animal {};
class SheepTuo :public Sheep, public Tuo {};
vbptr是虚指针,由于Sheep和Tuo都是虚继承的方式继承Animal的,所以这俩类只是各多了一个虚指针,指向对应的vbtable,vbtable是虚表,Sheep和Tuo的虚指针会查虚表,记录偏移量,比如Sheep的虚指针地址偏移量为0,Sheep的虚表里面的记录的偏移量是8,0+8就是Animal的m_age的地址偏移量;同理对于Tuo来说4+4也是Animal的m_age的地址偏移量。这样的话,SheepTuo就只包含一个m_age。那么如果把SheepTuo继承Sheep和Tuo的方式都改为虚继承呢?那么SheepTuo自己也会有一个虚指针,这个虚指针指向一个虚表,虚表里放着从Sheep和Tuo还有Animal继承过来的内容。此时仍旧只有一份Animal的m_age,如下图: