C++继承

本文主要讨论以下六个部分:继承和派生、单继承、多继承、虚继承、派生类的构造函数和析构函数、类的赋值兼容性。

首先来看继承和派生的基本概念。我们假设在动物园中需要为每个动物创建类,现在有两种选择方案,一种是为每个动物创建独立的类,彼此之间没有任何联系,另一种是创建一个大类,类中涵盖所有动物的特性。但这两种方案都有缺陷,前一种是因为割裂了各种动物之间的联系,虽然动物不同,但他们大部分特性还是相同的,这种方式的定义显然是冗余了。另一种则是过于臃肿,难以体现彼此之间的差异性。所以比较好的组织方式就是通过树状继承的方式来定义,基类中保存的是共性部分,派生类继承基类的数据和操作并在此基础上增加自己特有的个性的东西,这样就能很好的解决上面两个问题。

派生类继承自基类,基类派生出派生类。派生类会继承基类的所有属性和行为,并可以在此基础上增加新的属性与行为,但是删减基类的内容却是不被许可的。

有了继承和派生的基本概念,接下来需要了解最简单也是最常用的单继承。在派生类派生自基类时候需要增加派生类保留字,保留字也分为public、protected、private等,这里的保留字将会和基类中的保留字共同得到新的权限,根据组合一共有九种。如果是公有继承(即public继承),则在派生类中对基类成员的访问权限依旧和基类中相同,即可以访问基类中被public和protected修饰的变量或函数。如果是保护继承(protected继承),则原先基类中的public和protected修饰的变量或函数在派生类中全部变为protected类型(即只能在派生类中被访问,外部不能访问),原本private类型的数据或函数依旧是private类型。如果是私有继承(private继承),则无论原本是什么类型的,在派生类中都会变为private类型。

虽然上面有九种数据访问权限的组合,但通常情况下最常用的依旧是公有继承,并且如果基类中某个数据经常被派生类访问,则定义为protected更加方便(如果数据被定义为private类型,则每次操作都需要调用基类的函数)。

下面通过一个简单的例子来解释类的单继承。

// 基类|Point

class Point

{

public:

Point( int x = 0, int y = 0 ) : _x(x), _y(y) { }

Point( const Point& pt ) : _x(pt._x), _y(pt._y) { }

int GetX() const {  return _x;  }

void SetX( int x ) {  _x = x;  }

int GetY() const {  return _y;  }

void SetY( int y ) {  _y = y;  }

friend ostream& operator<<( ostream& os, const Point& pt );

protected:

int _x, _y;

};

// 派生类Point3D

class Point3D: public Point

{

public:

Point3D( int x = 0, int y = 0, int z = 0 ) : Point(x,y), _z(z) { }

Point3D( const Point3D& pt3d ) : Point(pt3d._x, pt3d._y), _z(pt3d._z) { }

int GetZ() const {  return _z;  }

void SetZ( int z ) {  _z = z;  }

friend ostream& operator<<( ostream& os, const Point3D& pt3d );

protected:

int _z;

};

在Point基类中定义了二维空间的点坐标(_x,  _y),现在通过共有派生继承基类的内容,并增加_z的值形成三维坐标。因为是公有派生,所以在派生类中对基类数据和函数的访问权限和基类中原本定义相同。因为派生类中经常需要对_x和_y两个坐标进行操作,所以如果在基类中将其定义成private类型,派生类将不能直接访问基类中的元素,所有访问必须通过基类定义的函数才行。但是这里定义为protected之后,派生类就可以知己对基类中的数据进行操作了,操作更加简单。

继承的时候,如果基类函数和派生类具有同名函数,则有可能发生函数覆盖,调用时候会发生二义性。比如下例,基类和派生类中均定义了Print函数,当使用基类对象调用Print函数时会调用基类的该函数,而使用派生类对象调用Print函数时,虽然在派生类中继承了属于基类的Print同名函数,但只会调用派生类中重新定义的Print函数。这里并不是基类中的函数不存在,而是他被派生类中的同名函数覆盖了,所以如果派生类对象希望访问基类中被“覆盖”的函数时,只需要通过名解析规则在函数前加上该函数所属类名即可。

// 定义同名函数和对象

class Point {  void Print();  };

class Point3D: public Point { void Print();  };

Point pt( 1, 2 );

Point3D pt3d( 1, 2, 3 );

// 基类对象和派生类对象分别调用同名函数

pt.Print();    // 调用Point类的Print成员函数

pt3d.Print();    // 调用Point3D类的Print成员函数

pt3d.Point::Print()    // 名解析规则

虽然单继承模式已经几乎可以解决所有问题,但C++本身还提供了多继承机制,即一个派生类可以从多个基类继承,多重继承和但继承一样,派生类会继承所有基类的属性和操作。但是多继承会导致一个很麻烦的问题:一个派生类通过多继承会保存多个来自基类重复的副本。下面的例子就是这样的情况:

// 保存了多副本的情况

class A { … };

class B: public A { … };

class C: public A, protected B { … };

在继承基类时,会在派生类中完全复制基类的存储空间,如上的定义中,类B中保存了类A的副本,但是在定义类C时公有继承A,再保护继承类B就会导致类A的副本被复制了两次(一次来自自身继承,一次来源于类B)。另外,在多继承情况下,同名函数的调用更加复杂,如果要准确调用想要的函数需要最好还是完整给出名解析(自底向上回溯)。

继承中会有保存多副本的问题,所以就引入了虚基类来解决这个问题。我们可以通过下面的例子来学习虚继承:

// 虚继承

class A {  public: void f();  };

class B: virtual public A {  public: void f();  };

class C: virtual public A {  public: void f();  };

class D: public B, public C {  public: void f();  };

虚继承的目的就是为了消除派生类公共基类的对个副本,只保存一份副本。在派生时使用关键字virtual,上例中类B、类C均继承基类A,之后在定义类D时候,继承B时已经包含了A,则在C中的A就不会再被复制到类D中,D中只含有类A的一份副本。虽然在单继承中也可以在继承时加上virtual关键字,但虚基类的概念实际上只有多继承时才有意义。

最后讨论的是继承中最基本问题:构造函数和析构函数的调用顺序。构造函数的执行顺序为:

1.调用基类构造函数,调用顺序和基类在派生类中继承顺序相同;

2.调用派生类中对象成员的构造函数,构造顺序和派生类中定义顺序相同;

3.调用派生类构造函数。

析构函数执行顺序为:

1.调用派生类构造函数;

2.调用你派生类新增对象成员析构函数,析构顺序和定义顺序相反;

3.调用基类构造函数,调用顺序和基类在派生类中继承顺序相反。

在面向对象编程中,我们构建了一个类的层次,在使用时候往往需要使用指向基类的指针或基类的引用,这些都是类的赋值兼容性问题,具有以下情形:

1.将派生类对象赋值给基类对象,仅赋值基类部分;

2.用派生类对象初始化引用对象,仅操作基类部分;

3.使指向基类的指针指向派生类对象,仅引领基类部分。

下面通过一个例子来详细说明赋值兼容性质问题:

#include <iostream>

#include <string>

using namespace std;

// 定义类及派生类

class Base

{

public:

Base(string s) : str_a(s) { }

Base(const Base & that) { str_a = that.str_a; }

void Print() const { cout << “In base: ” << str_a << endl; }

protected:

string str_a;

};

class Derived : public Base

{

public:

Derived(string s1,string s2) : Base(s1), str_b(s2) { }     // 调用基类构造函数初始化

void Print() const { cout << “In derived: ” << str_a + ” ” + str_b << endl; }

protected:

string str_b;

};

// 主函数

int main()

{

Derived d1( “Hello”, “World” );

Base b1( d1 );     // 拷贝构造,派生类至基类,仅复制基类部分

d1.Print();     // Hello World

b1.Print();     // Hello

Base & b2 = d1;     // 引用,不调用拷贝构造函数,仅访问基类部分

d1.Print();

b2.Print();

Base * b3 = &d1;    // 指针,不调用拷贝构造函数,仅引领基类部分

d1.Print();

b3->Print();

return 1;

}

通过上面赋值兼容性的例子可以看出,当我们使用一个基类的指针指向派生类对象时,只能操作定义在基类中的函数,而不能调用派生类中的函数,哪怕是同名函数也不行。为了能够仅使用指向基类的指针,并通过取得不同对象地址使收到相同信息时做出不同响应,引入了虚函数的概念,并进一步引出了纯虚函数和抽象类的概念,具体总结可以见我之前的文章。

Advertisements