面向对象程序设计中最重要的一个概念是继承。继承允许我们依据另一个类来定义一个类,这使得创建和维护一个应用程序变得更容易。这样做,也达到了重用代码功能和提高执行效率的效果。
当创建一个类时,您不需要重新编写新的数据成员和成员函数,只需指定新建的类继承了一个已有的类的成员即可。这个已有的类称为基类,新建的类称为派生类。
派生类继承了基类的所有成员变量和函数,在这个基础上再去定义派生类的数据。
继承代表了一种包含关系。例如,动物作为基类,动物的基本行为有进食和行走,哈士奇也属于动物,我们不必要再去声明一个哈士奇类,可以基础动物类去创建一个哈士奇类,再去添加哈士奇的一些特征,例如拆家,鸟也是动物,也可以基础动物类,在可以加入鸟自己的特征,等等等等。
继承的语法为:
class 派生类名: 继承方式 基类名
{
...
}
其中继承方式是决定在派生类中,从基类继承过来的那一部分的成员的访问权限,分为公有继承(
public
),受保护继承(
protected
)和私有继承(
private
)。
其中对于公有继承,基类中的数据在派生类中的访问权限如下:
通过公有继承后,基类中的公有成员在派生类中也是公有的,派生类可以正常访问,基类
的私有数据在派生类中是私有的,派生类不可访问,受保护数据在派生类中是受保护的,派生
类可以访问,但类外无法访问。
对于私有继承,基类中数据在派生类中的访问权限如下:
通过私有继承后,基类的所有成员在派生类中都是私有的,都不可以访问
对于受保护继承,基类中数据在派生类中的访问权限如下:
通过受保护继承后,基类中的公有成员在派生类中变成受保护的,派生类可以正常访问,基类的私有数据在派生类中是私有的,派生类不可访问,受保护数据在派生类中还是受保护的,派生类可以访问。
继承示例:
class A
{
private:
int a;
int b;
public:
A(int x,int y):a(x),b(y){}
void fun()
{
a = b;
}
};
class B:public A //公有继承
{
private:
int c;
public:
B(int x,int y,int z):A(x,y),c(z){}
};
class C:private A //私有继承
{
private:
int d;
public:
C(int x,int y,int z):A(x,y),d(z){}
};
class D:protected A //受保护继承
{
private:
int e;
public:
D(int x,int y,int z):A(x,y),e(z){}
};
继承中的构造和析构
虽然派生类中含有从基类继承来的成员,但是可能没有权限访问(如:基类的私有成员),所以派生类是不能直接初始化从基类继承来成员,所以,在 C++中,规定每个类控制它自己成员的初始化以及销毁过程,也就是说,基类的成员由基类的构造函数和析构函数来进行初始化和销毁,派生类中新添加的成员,就由派生类的构造函数和析构函数来进行初始化和销毁。
那么,如果基类有默认构造函数,则实例化派生类对象时,会自动调用基类的默认构造函 数,来初始化从基类继承过来的成员变量,如果基类没有默认构造函数(即基类的构造函数有参数)或者想调用有参数的构造函数,就需要传参数给基类的构造函数,则必须在派生类的构造函数初始化列表中调用基类的构造函数,来完成基类成员的初始化,在上面的例子中就是这样一种情况,但我们还是来说一下在派生类中构造函数初始化列表中如何去初始化基类,格式如下:
派生类的构造函数名(参数列表):基类构造函数名(参数列表),...
{
//函数体
}
例如:
B(int x,int y,int z):A(x,y),c(z){}
B 是派生类,A 是基类,A 的非默认构造函数中有两个参数。
派生类中构造函数和析构函数的调用顺序:
创建时,先调用基类的构造函数,再调用派生类的构造函数
销毁时,先调用派生类的析构函数,再调用基类的析构函数
派生类和基类的调用关系
由于在派生类对象中包含了基类的所有成员,所以我们可以把派生类对象当做基类对象来使 用,具体如下:注意:以下都要建立在公有继承的基础上
,因为公有继承不会改变基类在派生类中的结构,而其他继承方式都会改变基类在派生类中的结构。
1.可以用派生类对象初始化基类对象(公有继承情况下)
例如:
class A
{
private:
int a;
int b;
public:
A(int x,int y):a(x),b(y){}
void fun()
{
cout << "a:" << a << "b:" << b << endl;
}
};
class B:public A //公有继承
{
private:
int c;
public:
B(int x,int y,int z):A(x,y),c(z){}
};
int main(int argc, char const *argv[])
{
B b(1,2,3); //定义一个派生类对象
A a = b; //用派生类初始化基类
a.fun();
return 0;
}
2.可以用派生类对象给基类对象赋值(公有继承情况下)
例如:
int main(int argc, char const *argv[])
{
B b(1,2,3); //定义一个派生类对象
A a(3,4); //定义一个基类对象
a = b; //用派生类给基类赋值
a.fun();
return 0;
}
3. 基类指针可以指向派生类对象(公有继承情况下)
例如:
class A
{
private:
int a;
int b;
public:
A(int x,int y):a(x),b(y){}
void fun()
{
cout << "a:" << a << "b:" << b << endl;
}
};
class B:public A //公有继承
{
private:
int c;
public:
B(int x,int y,int z):A(x,y),c(z){}
void sun()
{
cout << "c:" << c << endl;
}
};
int main(int argc, char const *argv[])
{
B b(1,2,3); //定义一个派生类对象
A *p = &b;
p->fun();
return 0;
}
但是,使用基类指针访问派生类,只能访问从基类中继承下来的数据,不能访问派生类中
新增的数据。
4.基类引用可以绑定到派生类对象(公有继承情况下)
例如:
int main(int argc, char const *argv[])
{
B b(1,2,3); //定义一个派生类对象
A &p = b;
p.fun();
return 0;
}
同样,使用基类引用指向派生类,也只能访问从基类中继承下来的数据,不能访问派生类中新增的数据。
多继承
多继承即一个子类可以有多个父类,它继承了多个父类的特性。
C++
类可以从多个类继承成员,语法如下:
class <派生类名>:<继承方式 1><基类名 1>,<继承方式 2><基类名 2>,…
{
<派生类类体>
};
例如:
class A
{
private:
int a;
int b;
public:
A(int x,int y):a(x),b(y){}
void fun()
{
cout << "a:" << a << "b:" << b << endl;
}
};
class B
{
private:
int c;
int d;
public:
B(int x,int y):c(x),d(y){}
void fun()
{
cout << "c:" << c << "d:" << d << endl;
}
};
class C:public A,private B
{
private:
int e;
public:
C(int a1,int a2,int a3,int a4,int a5):A(a1,a2),B(a3,a4),e(a5){}
};
在查看上面多继承例子的时候,我们会发现一个问题,如果继承的基类中有相同名字的元素该怎么去访问,或者,不讲多继承,就单继承中基类中有个成员函数名和派生类中的一个成员函数名相同,怎么去访问才不会访问错?方法就是在访问函数名前加上作用域就好了,例如:
基类 A 中有一个成员函数叫 fun(),派生类 B 继承 A,B 类中又定义了一个 fun()函数,那么在用派生类访问 fun()函数时,如果要访问基类中的 fun()就需要加上作用域来指定,如果不加上,默认为是访问派生类中的 fun()。在多继承中也是如此,例如下面例子
#include <iostream>
#include<string.h>
using namespace std;
class A
{
private:
int a;
int b;
public:
A(int x,int y):a(x),b(y){}
void fun()
{
cout << "a:" << a << "b:" << b << endl;
}
};
class B
{
private:
int c;
int d;
public:
B(int x,int y):c(x),d(y){}
void fun()
{
cout << "c:" << c << "d:" << d << endl;
}
};
class C:public A,public B
{
private:
int e;
public:
C(int a1,int a2,int a3,int a4,int a5):A(a1,a2),B(a3,a4),e(a5){}
void fun()
{
cout << "e:" << e << endl;
}
};
int main(int argc, char const *argv[])
{
C c(1,2,3,4,5); //定义一个派生类 C
c.fun(); //默认是访问派生类 C 的 fun 函数
c.A::fun(); //访问基类 A 的 fun 函数
c.B::fun(); //访问基类 B 的 fun 函数
B *p = &c;
p->fun(); //访问 B 的 fun 函数
return 0;
}
菱形继承
菱形继承就是有两个派生类继承一个基类,然后又有一个派生类继承这两个派生类,例如:A 为基类,B 和 C 都继承了 A,D 又继承了 B 和 C,这样的继承关系就叫菱形继承。
例如:
#include <iostream>
#include<string.h>
using namespace std;
class A
{
private:
int a;
int b;
public:
A(int x,int y):a(x),b(y){}
void fun()
{
cout << "a:" << a << "b:" << b << endl;
}
};
class B:public A
{
private:
int c;
int d;
public:
B(int x,int y,int z,int k):A(x,y),c(z),d(k){}
void fun()
{
cout << "c:" << c << "d:" << d << endl;
}
};
class C:public A
{
private:
int e;
public:
C(int a1,int a2,int a3):A(a1,a2),e(a3){}
void fun()
{
cout << "e:" << e << endl;
}
};
class D:public B,public C
{
private:
int w;
public:
D(int a1,int a2,int a3,int a4,int a5,int a6,int a7,int a8):B(a1,a2,a3,a4),C(a5,a6,a7),w(a8){}
};
int main(int argc, char const *argv[])
{
D d(1,2,3,4,5,6,7,8);
d.B::fun(); //访问 B 中的 fun()
B *p = &d;
p->A::fun(); //访问 B 中 A 中的 fun()
return 0;
}
如上,B 继承了 A,那么 B 中就有一份 A 的拷贝,C 又继承了 A,那么 C 中也有一份 A 的拷贝,这两个还不是同一个,D 多继承了 B 和 C 后,那么在 D 中就有两份 A 的拷贝,一份来自 B,
一份来自 C,那么就会产生一个问题,D 中有两份名字相同但是来源不同的数据,使得 D 在访问
基类元素时,就会产生二义性,那有没有办法可以使得菱形继承后最后继承的派生类只保存一 份基类元素呢?那就用来学习下我们接下来要说的一个虚继承方式了。
虚继承
为了解决多继承时的命名冲突和冗余数据问题,C++
提出了虚继承,使得在派生类中只保留一份间接基类的成员。在继承方式前面或后面加上 virtual
关键字就是虚继承。
虚继承的语法:
class 派生类名称:继承方式 virtual 基类名称
{
//...
};
或
class 派生类名称: virtual 继承方式 基类名称
{
//...
};
例如:
class A
{
private:
int a;
int b;
public:
A(int x,int y):a(x),b(y){}
void fun()
{
cout << "a:" << a << "b:" << b << endl;
}
};
class B:public virtual A
{
private:
int c;
int d;
public:
B(int x,int y,int z,int k):A(x,y),c(z),d(k){}
void fun()
{
cout << "c:" << c << "d:" << d << endl;
}
};
使用虚继承的类,在内部会生成一个指针,这个指针指向一张虚表,虚表中去维护了基类元素。也就是说上面例子中的派生类 B 的结构如下图所示:
进行虚继承后,在虚继承类 B 的开头就多了一个指向虚表的指针,这个和虚函数的虚表不一样,这个虚表是用来维护基类用于解决菱形继承问题的。
其中,容易让人产生误解的是,是不是虚继承之后,我内部有一个虚表了,就不需要去继承基类的元素了,答案是还是要继承一份基类的元素的,如果单纯是单继承或多继承,虚继承的意义其实并不大,但是对于菱形继承,因为存在数据的二义性,所以对于菱形继承,较有重要的意义。
我们来看虚继承后的菱形继承示例:
class A
{
public:
int a;
int b;
public:
A(int x,int y):a(x),b(y){}
void fun()
{
cout << "a:" << a << "b:" << b << endl;
}
};
class B:public virtual A
{
private:
int c;
int d;
public:
B(int x,int y,int z,int k):A(x,y),c(z),d(k){}
void funb()
{
cout << "c:" << c << "d:" << d << endl;
}
};
class C:public virtual A
{
private:
int e;
int r;
public:
C(int a1,int a2,int a3):A(a1,a2),e(a3){}
void func()
{
cout << "e:" << e << endl;
}
};
class D:public B,public C
{
private:
int w;
public:
D(int a1,int a2,int a3,int a4,int a5,int a6,int a7,int a8):A(a1,a2),B(a1,a2,a3,a4),C(a5,a6,a7),w(a8){}
};
可以看到,没有虚继承时,在派生类 D 中我们是没法对 A 进行构造的,但是,当 B,C 都是 虚继承之后,B 和 C 的内部都会有一个指向虚表的指针,在 D 继承 B,C 之后,D 也会生成一份指向虚表的指针,这个虚表管理着基类 A,所以在构造函数初始化列表中,就可以对 A 进行初始化构造,也必须对 A 进行初始化构造。这个时候 D 中的数据情况如下:
那么这个时候去访问基类 A 的元素时就不会产生这种不知道是来自 B 中继承的 A 的元素还是来自 C 中继承 A 的元素的问题了,因为基类 A 现在只提供虚表指针进行管理,而 D 中只要有这个虚表指针就可以直接访问基类的元素,和访问 B 中的和 C 中的都一样,因为它们的虚表指针所管理的基类是一样的。