类继承的语法

类的继承允许通过继承的方式生成新类,继承自的类为基类,继承自基类的类成为派生类,类的继承写法如下:

1
2
3
4
class derivedClass : public/protect/private baseClass
{
// statements
}

其中基类前有一个访问限定符,不写的时候默认为private,但是我们主要使用public,又称作公有继承,三个不同的访问限定词的区别在于:

  • 公有继承(public):当一个类派生自公有基类时,基类的公有成员也是派生类的公有成员,基类的保护成员也是派生类的保护成员,基类的私有成员不能直接被派生类访问,但是可以通过调用基类的公有和保护成员来访问
  • 保护继承(protecte): 当一个类派生自保护基类时,基类的公有和保护成员将成为派生类的保护成员
  • 私有继承(private):当一个类派生自私有基类时,基类的公有和保护成员将成为派生类的私有成员

例如我们定义一个基类再定义一个派生类:

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
40
41
class baseClass
{
private:
baseInt;
baseDouble;
public:
baseClass(const int pInt = 1, cosnt double pDouble = 2.5);
int function(double d);
}

baseClass :: baseClass(const int pInt = 1, cosnt double pDouble = 2.5)
{
baseInt = pInt;
baseDouble = pDouble;
}

baseClass :: function(double d)
{
cout << d;
return 1;
}


class derivedClass : public baseClass
{
private:
derivedChar;
public:
derivedClass(const int pInt, const double pDouble, const char pChar);
void anotherFunction();
}

derivedClass :: derivedClass(const int pInt, const double pDouble, const char pChar) : baseClass(pInt,pDouble)
{
derivedChar = pChar;
}

void derivedClass :: anotherFunction()
{
cout << "I am the derived class.";
}

从这个例子中,我们可以看到,基类有两个私有变量,baseIntbaseDouble,两个方法,一个构造函数用于给两个私有变量赋值,一个用于充数。派生类继承自基类,于是派生类derivedClass就拥有了基类的两个公有方法,但是它不能访问基类的两个私有变量。派生类又定义了自己的一个新私有变量,同时有自己的构造函数和一个用于充数的函数。可见继承的作用在于方便地进行代码的重用以及组织管理项目设计。

基类和派生类的关系

类的继承是“is-a”的关系,或者说是“is-a-kind-of”,即派生类对象也是一个基类对象。

  • 派生类是可以调用基类的protectedpublic修饰的成员变量和方法的,而派生类也可以定义自己的变量和方法。
  • 同时,派生类还可以重载基类的方法,即声明一个和基类中相同名称的成员变量,但是在派生类中对其进行重新的定义。如果基类和派生类同时拥有同名同变量参数同返回值但是定义不同的函数,在使用基类对象调用该函数时,调用的是基类的函数,在使用派生类的对象调用该函数时,调用的是派生类的定义。
  • 基类指针可以在不进行显式类型转换的情况下指向派生类对象;基类引用可以在不进行显式类型转换的情况下引用派生类对象。C++要求的引用和指针类型与赋给的类型匹配的规则对继承来说例外。然而,这种例外只是单向的,不可以将基类对象和地址赋给派生类引用和指针。基类指针或引用只能用于调用基类方法。

多态公有继承

多态即一个派生类继承自一个基类后,希望可以定义一个和基类中相同名称、参数列表、返回值的函数,但这个函数的定义却与基类中的不同,即一种派生类对基类方法的重载。

首先,派生类可以重载基类的方法,如果派生类使用基类相同的函数和函数定义,那么就不需要再在派生类中声明该函数,即共有的函数需要放在基类中。如果派生类想对基类的函数进行新的定义,则需要在派生类中对其再次进行声明并定义,定义时也需要表明定义的是那个类的函数。如 baseClass::function()derivedClass::function() 这样。

虚方法(virtual method),需要使用关键词 virtual 修饰基类中的函数,如下面这样:

1
virtual void function(int i);

它的作用如下:当基类和派生类都有定义过某个相同方法后,我们需要确定调用的是哪个类下的方法,特别是当方法是通过引用或指针而不是对象调用的。

  • 如果没有使用关键字 virtual ,程序将根据引用类型或指针类型选择方法
  • 如果使用了关键字 virtual ,程序将根据引用或指针指向的对象的类型来选择方法
  • 如果有派生类重载了基类的方法,一般需要将基类的析构函数设置成virtual的以保证释放派生类对象时能够按照正确的顺序调用析构函数
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 不使用virtual
BaseClass baseClass();
DerivedClass derivedClass();
BaseClass & reference1 = baseClass; // 指向baseClass的类型是BaseClass的引用变量
BaseClass & reference2 = derivedClass; // 指向derivedClass的但是类型是BaseClass的引用变量
reference1.function(); // 会根据引用的类型即BaseClass调用BaseClass下的function方法
reference2.function(); // 会根据引用的类型即BaseClass调用BaseClass下的function方法

// 使用virtual
BaseClass baseClass();
DerivedClass derivedClass();
BaseClass & reference1 = baseClass; // 指向baseClass的类型是BaseClass的引用变量
BaseClass & reference2 = derivedClass; // 指向derivedClass的但是类型是BaseClass的引用变量
reference1.function(); // 会根据引用指向的类型即BaseClass调用BaseClass下的function方法
reference2.function(); // 会根据引用指向的类型即DerivedClass调用BaseClass下的function方法

抽象基类

抽象基类(abstract base class,ABC)是一种特殊的基类,从概念上讲,将所有派生类的公用的方法进行抽象汇总声明(定义)到的一个类中,这种设计下的类可以视作一个抽象基类。但是真正的抽象基类应该是至少包含了一个纯虚函数(pure virtual function)的类,这种类不能声明对应的对象,只能作为基类。

纯虚函数是一种只在抽象基类中给出原型,但是部给出定义的函数,更像是一个接口,由所有的派生类对纯虚函数根据自己类的需求来实现其定义。纯虚函数的写法是在虚函数后面以 =0 结尾

1
virtual double pureVirtualFunction(int i) const = 0;

应用这种方式,可以将所有派生类共有但是却又各自有着不同实现的方法抽象到一个基类中,提供其原型但是不对其进行定义(也只有纯虚函数C++允许不给出定义),然后使得各个派生类自己给出其定义。

私有继承

私有继承即继承的基类使用private修饰符修饰的继承,如果没有访问限定符的修饰,默认也是私有继承,私有继承是一种“has-a”的关系。

1
2
class DerivedClass : private BaseClass{ }
class DerivedClass : BaseClass{ }

私有继承使得基类的公有成员、保护成员都被成为派生类的私有成员,这就使得基类的那些方法都不能再被派生类的实例化对象使用,而只能被派生类的成员函数在类内部使用。即派生类部继承基类的接口。

这里比较了三种继承之间的区别:

特征 公有继承 保护继承 私有继承
公有成员变成 派生类的公有成员 派生类的保护成员 派生类的私有成员
保护成员变成 派生类的保护成员 派生类的保护成员 派生类的私有成员
私有成员变成 只能通过基类接口访问 只能通过基类接口访问 只能通过基类接口访问
能否隐式向上转换 只能在派生类中 不能

多重继承

多重继承(Multiple Inheritance)也是“is-a”的关系,它允许一个类继承自多个类,只需要将继承的类使用逗号隔开即可,像下面这样:

1
2
class DerivedClass : public BaseClass1, public BaseClass2 {……}
class DerivedClass : public BaseClass1, BaseClass2 {……} // BaseClass2 is a private base

多重继承中每一个被继承的基类都需要设置访问限定符,根据需要可以使用不同的访问限定符,不写默认为private

例如设置一个基类Worker表示工人,然后工人可以是歌手也可以是服务员,我们使用两个类继承自这个基类,Singer和Waiter,最后,我们可以定义一个既是歌手有时服务员的类,所以它同时继承自Singer和Waiter,他们的关系就像下边这样:

1
2
3
4
class Worker {……}
class Singer : public Worker {……}
class Waiter : public Worker {……}
class SingingWaiter : public Singer, public Waiter {……}
一个多重继承
一个多重继承

虚基类

首先多重继承导致了一个问题就是,当一个SingingWaiter的实例继承自Singer和Waiter时,也就间接地两次继承了Worker,也就是说一个SingingWaiter的实例结构应该是如下这样的:

在不使用虚基类的时候
在不使用虚基类的时候

这样引发的问题就是,当我们把派生类对象的地址赋给一个基类的指针时就无法区分是赋给哪个基类,导致二义性:

1
2
SingingWaiter sw;
Worker * psw = &sw;

因为sw中包含两个Worker对象,从而有两给地址可以选择,于是正确的写法应该是:

1
2
Worker * psw1 = (Waiter *) &sw;
Worker * psw2 = (Singer *) &sw;

所以虚基类(Virtual Base Classes)将解决这个问题。虚基类使得从多个类派生出的对象只继承一个基类对象,需要在类继承的声明中使用virtual关键词,virtual和public的次序无所谓:

1
2
3
class Singer : virtual public Worker {……}
class Waiter : public virtual Worker {……}
class SingingWaiter : public Singer, public Waiter {……}

现在,SingingWaiter对象就只包含Worker对象的一个副本,从本质上说是,继承的Singer和Waiter共享一个Worker对象,而不是各自引入一个Worker对象的副本,从而可以使用多态。

在使用虚基类的时候
在使用虚基类的时候