C++派生类与继承(超详细)

一.派生类的概念

1.为什么要使用继承

继承性也是程序设计中的一个非常有用的、有力的特性, 它可以让程序员在既有类的基础上,通过增加少量代码或修改少量代码的方法得到新的类, 从而较好地解决了代码重用的问题。

2.派生类的说明

在类名 employee 的冒号之后, 跟着关键字 public 与类名 person,这就意味着类 employee 将继承类 person 的全部特性。
关键字 public 指出派生的方式,告诉编译程序派生类employee从基类 person 公有派生。

// 定义一个基类( person 类)
class person
{
private :
    char name [10] ;
    int age;
    char sex;
public:
// …
} ;
// 定义一个派生类
class employeepublic person
{
    char department[20] ;
    float salary;
public:
    // …
} ;

声明一个派生类的一般格式为:

class 派生类名∶派生方式 基类名
{
// 派生类新增的数据成员和成员函数
} ;

派生方式”可以是关键字 private 或 public。如果使用了 private, 则称派生类从基类私有派生; 如果使用了 public,则称派生类从基类公有派生。派生方式可以缺省, 这时派生方式默认为private ,即私有派生。

1. 公有派生

class employeepublic person
{
    // …
};

2. 私有派生

class employeeprivate person
{
// …
} ;

这两种派生方式的特点如下:

  1. 无论哪种派生方式, 基类中的私有成员既不允许外部函数访问, 也不允许派生类中的成员函数访问,但是可以通过基类提供的公有成员函数访问。
  2. 公有派生与私有派生的不同点在于基类中的公有成员在派生类中的访问属性。

公有派生时, 基类中的所有公有成员在派生类中也都是公有的。

私有派生时, 基类中的所有公有成员只能成为派生类中的私有成员。

下面我们分别讨论私有派生和公有派生的一些特性。

  1. 私有派生

(1 ) 私有派生类对基类成员的访问
由私有派生得到的派生类, 对它的基类的公有成员只能是私有继承。也就是说基类的所有公有成员都只能成为私有派生类的私有成员, 这些私有成员能够被派生类的成员函数访问,但是基类私有成员不能被派生类成员函数访问。

# include <iostream>
using namespace std;
class base  // 声明一个基类
{
    int x;
public:
    void setx(int n)
    {
        x = n;
    }
    void showx ()
    {
        cout << x << endl;
    }
} ;
class derived: private base  // 声明一个私有派生类
{
    int y;
public:
    void setxy(int n, int m)
    {
        setx( n) ;
        y = m;
    }
    void showxy()
    {
        cout<< x << y << endl; // 非法
    }
};
int main()
{
    derived obj;
    obj.setxy( 10,20) ;
    obj.showxy() ;
    return 0 ;
}

例中首先定义了一个类 base , 它有一个私有数据 x 和两个公有成员函数 setx ( ) 和showx( ) 。将类 base 作为基类,派生出一个类 derived。派生类 derived 除继承了基类的成员外,还有只属于自己的成员: 私有数据成员 y、公有函数成员 setxy( )和 showxy( )。派生方式关键字是 private, 所以这是一个私有派生。
类 derived 私有继承了 base 的所有成员, 但它的成员函数并不能直接使用 base 的私有数据 x, 只能使用两个公有成员函数。所以在 derived 的成员函数 setxy ( ) 中引用 bas的公有成员 setx( )是合法的, 但在成员函数 showxy( ) 中直接引用 base 的私有成员 x 是法的。

如果将例中函数 showxy( )改成如下形式:
void showxy( ) {showx( ) ; cout < < y < < endl; }
重新编译,程序将顺利通过。可见基类中的私有成员既不能被外部函数访问, 也不能被派生类成员函数访问,只能被基类自己的成员函数访问。因此, 我们在设计基类时, 总要为它的私有数据成员提供公有成员函数,以使派生类或外部函数可以间接使用这些数据成员。
(2 ) 外部函数对私有派生类继承来的成员的访问
私有派生时,基类的公有成员在派生类中都成为私有成员, 外部函数不能访问。下面的例子将验证外部函数对私有派生类继承来的成员的访问性。

#include <iostream>
using namespace std;
class base
{
    // 声明一个基类
    int x;
public:
    void setx(int n)
    {
        x = n;
    }
    void showx ()
    {
        cout << x << endl;
    }
};
class derived: private base
{
    // 声明一个私有派生类
    int y;
public:
    void sety(int n)
    {
        y = n;
    }
    void showy()
    {
        cout << y << endl;
    }
};
int main()
{
    derived obj;
    obj .setx(10) ; // 非法
    obj .sety(20) ; // 合法
    obj .showx() ;  // 非法
    obj .showy() ;  // 合法
    return 0 ;
}

由于是私有派生, 所以基类 base 的公有成员 setx ( ) 和 showx ( ) 被 derived 私有继 承后, 成为 derived 的私 有成员, 只能 被derived的成员函数访问, 不能被外界函数访问。在 main ( )函数中, 定义了派生类 derived的对象 obj, 由于 sety ( ) 和 showy ( ) 在类 derived 中是 公有函 数, 所 以对 obj .sety ( ) 和obj .showy( )的调用是没有问题的, 但是对 obj .setx ( )和obj .showx( ) 的调用是非法的, 因为这两个函数在类 derived 中已成为私有成员。
需要注意的是:无论 setx( ) 和 showx( )如何被一些派生类继承, 它们仍然是 base 的公有成员,因此以下的调用是合法的:

base base-obj;
base-obj.setx (2);
  1. 公有派生
    在公有派生中,基类中的私有成员不允许外部函数和派生类中的成员函数直接访问,但是可以通过基类提供的公有成员函数访问。基类中的公有成员在派生类中仍是公有成员,外部函数和派生类中的成员函数可直接访问。
#include <iostream>
using namespace std;
class base
{
    // 声明一个基类
    int x;
public:
    void setx(int n)
    {
        x = n;
    }
    void showx()
    {
        cout << x << endl;
    }
};
class derived: public base
{
    // 声明一个公有派生类
    int y;
public:
    void sety(int n)
    {
        y = n;
    }
    void showy()
    {
        cout << y << endl;
    }
} ;
int main ()
{
    derived obj;
    obj.setx(10);  // 合法
    obj.sety(20);  // 合法
    obj.showx();   // 合法
    obj.showy();   // 合法
    return 0 ;
}

例中类 derived 从类 base 中公有派生, 所以类 base 中的两个公有成员函数 setx ( ) 和showx( )在派生类中仍是公有成员。因此, 它们可以被程序的其它部分访问。特别是它们可以合法地在 main( )中被调用。
说明:

  1. 派生类以公有派生的方式继承了基类, 并不意味着派生类可以访问基类的私有成员。
class base
{
    int x;
public:
    void setx (int n )
    {
        x = n;
    }
    void showx()
    {
        cout << x << endl;
    }
} ;
class derivedpublic base
{
    int y;
public:
    void sety(int n)
    {
        y = n;
    }
    void show-sum()
    {
        cout << x + y << endl; // 非法
    }
    void showy()
    {
        cout << y << endl;
    }
} ;

派生类 derived 企图访问基类 base 的私有成员 x, 但是这种企图是非法的,因为基类无论怎样被继承, 它的私有成员都针对该基类保持私有性。

  1. 在派生类中声明的名字支配基类中声明的同名的名字, 即如果在派生类的成员函数中直接使用该名字的话,则表示使用派生类中声明的名字
class X
{
public:
    int f() ;
};
class Ypublic X
{
public:
    int f();
    int g() ;
};
void Y∷g()
{
    f() ;   // 表示被调用的函数是 Y∷f( ), 而不是 X∷f( )
}

对于派生类的对象的引用,也有相同的结论, 例如:
Y obj;
obj .f( ) ; / / 被调用的函数是 Y∷f( )
如果要使用基类中声明的名字,则应使用作用域运算符限定, 例如:
obj .X∷f( ) ; / / 被调用的函数是 X∷f( )
在这里插入图片描述

保护成员的作用

protected 说明符可以放在类声明的任何地方,通常将它放在私有成员声明之后, 公有成员声明之前。类声明的一般格式如下所示:

class 类名
{
    [private:]
    私有数据成员和成员函数
protected:
    保护数据成员和成员函数
public:
    公有数据成员和成员函数
};

保护成员可以被派生类的成员函数访问,但是对于外界是隐藏起来的, 外部函数不能访问它。因此,为了便于派生类的访问, 可以将基类私有成员中需要提供给派生类访问的成员定义为保护成员。
下面的程序说明类的私有成员、公有成员与保护成员是如何被访问的。

# include < iostream .h >
class samp
{
    int a;
protected:
    int b;      // 定义变量 b 为保护成员
public:
    int c;
    samp(int n, int m)
    {
        a = n;
        b = m;
    }
    int geta()
    {
        return a ;
    }
    int getb()
    {
        return b;
    }
};
int main()
{
    samp obj(20, 30);
    obj.b = 99;     // 非法,类的保护成员不能被外部函数访问
    obj .c = 50;    // 合法,类的公有成员能被外部函数访问
    cout << obj .geta()  <"";    // 合法
    cout << obj .getb() <<""<< obj .c << endl;    // 合法
    return 0 ;
}

对象 obj 的数据成员b 不能被访问,这是因为 b 是保护成员, 而类的保护成员是不允许外部函数访问的。
C + + 规定, 派生类对于保护成员的继承与公有成员的继承很相似, 也分为两种情况: 若为公有派生, 则基类中的保护成员在派生类中也为保护成员;若为私有派生, 则基类中的保护成员在派生类中成为私有成员。
下面的程序说明保护成员以公有方式被继承后的访问特性。

#include <iostream>
using namespace std;
class base
{
protected:
    int a, b;
public:
    void setab(int n, int m)
    {
        a = n;
        b = m;
    }
};
class derive : public base
{
    int c;
public:
    void setc(int n)
    {
        c = n;
    }
    void showabc()
    {
        cout << a <<" "<< b <<" "<< c << endl;
    } // 允许派生类成员函数
    // 访问保护成员 a 和 b
} ;
int main ()
{
    derive obj;
    obj .setab(2, 4);
    obj .setc(3) ;
    obj .showabc() ;
    return 0 ;
}

由于 a 和 b 是基类 base 的保护成员, 而且被派生类以公有方式继承,所以它们可以被派生类的成员函数访问。但是基类的保护成员 a 与 b 不能被外部函数访问。

下面程序说明保护成员以私有方式被继承后的访问特性。

#include <iostream>
using namespace std;
class base
{
protected:
    int a;
public:
    void seta (int sa)
    {
        a = sa;
    }
} ;
class derive1: private base
{
protected:
    int b;
public:
    void setb(int sb)
    {
        b = sb;
    }
} ;
class derive2: public derive1
{
    int c;
public:
    void setc(int sc)
    {
        c = sc ;
    }
    void show( )
    {
        cout << "a = "<< a << endl;      // 非法
        cout << "b = "<< b << endl;      // 合法
        cout << "c = "<< c << endl;      // 合法
    }
} ;
int main()
{
    base op1;
    op1.seta(1);
    derive1 op2;
    op2.setb(2);
    derive2 op3;
    op3.setc(3);
    op3.show();
}

基类 base 中的保护成员 a 被其派生类 derive1 私有继承后成为私有成员, 所以不能被 derive1 的派生类 derive2中的成员函数访问。derive1 类中的保护成员 b,被其派生类 derive2 公有继承后仍是保护成员,所以可以被 derive2 中的成员函数 show( )访问。

派生方式基类中的访问权限派生类中的访问权限
public(公有派生)public、protect、privatepublic、protect、private
protect(私有派生)public、protect、privateprivate、private、private

在公有派生情况下, 基类中所有成员的访问特性在派生类中维持不变;在私有派生情况下, 基类中所有成员在派生类中成为私有成员。

二.派生类的构造函数和析构函数

1.派生类构造函数和析构函数的执行顺序

通常情况下,当创建派生类对象时, 首先执行基类的构造函数, 随后再执行派生类的构造函数; 当撤消派生类对象时, 则先执行派生类的析构函数, 随后再执行基类的析构函数。
下列程序的运行结果,反映了基类和派生类的构造函数及析构函数的执行顺序。

#include<iostream>
using namespace std;
class base
{
public:
    base()
    {
        cout<< "Constructing base class \n";
    } // 基类的构造函数
    ~base()
    {
        cout <<"Destructing baes class \n";
    } // 基类的析构函数
} ;
class derive : public base
{
public:
    derive() // 派生类的构造函数
    {
        cout << "Constructing derived class \n";
    }
    ~derive() // 派生类的析构函数
    {
        cout<< "Destructing derived class \n";
    }
};
int main()
{
    derive op;
    return 0 ;
}

程序运行结果如下:

Constructing base class
Constructing derived class
Destructing derived class
Destructing base class
构造函数的调用严格地按照先调用基类的构造函数, 后调用派生类的构造函数的顺序执行。析构函数的调用顺序与构造函数的调用顺序正好相反,先调用派生类的析构函数, 后调用基类的析构函数。

2.派生类构造函数和析构函数的构造规则

当基类的构造函数没有参数,或没有显式定义构造函数时, 派生类可以不向基类传递参数,甚至可以不定义构造函数。
派生类不能继承基类中的构造函数和析构函数。当基类含有带参数的构造函数时,派生类必须定义构造函数,以提供把参数传递给基类构造函数的途径。
在 C + + 中,派生类构造函数的一般格式为:

派生类构造函数名(参数表) :基类构造函数名( 参数表)
{
/ /}

其中基类构造函数的参数,通常来源于派生类构造函数的参数表, 也可以用常数值。
下面的程序说明如何传递一个参数给派生类的构造函数和传递一个参数给基类的构造函数。

#include<iostream>
using namespace std;
class base
{
    int i;
public:
    base(int n) // 基类的构造函数
    {
        cout << "Constructing base class \n";
        i = n;
    }
    ~base () // 基类的析构函数
    {
        cout << "Destructing base class \n";
    }
    void showi()
    {
        cout << i << endl;
    }
};
class derive : public base
{
    int j;
public:
    derive(int n, int m) : base (m) // 定义派生类构造函数时,
    {
        // 缀上基类的构造函数
        cout << "Constructing derived class"<< endl;
        j = n;
    }
    ~derive() // 派生类的析构函数
    {
        cout << "Destructing derived class"<< endl;
    }
    void showj()
    {
        cout << j << endl;
    }
};
int main()
{
    derive obj(30, 40) ;
    obj.showi();
    obj.showj();
    return 0 ;
}

程序运行结果为:
Constructing base class
Constructing derived class
40
30
Destructing derived class
Destructing base class
当派生类中含有对象成员时,其构造函数的一般形式为:

派生类构造函数名(参数表) :基类构造函数名( 参数表),对象成员名 1 (参数表),,对象成员名 n (参数表)
{
    // …
}

在定义派生类对象时,构造函数的执行顺序如下:

  • 基类的构造函数
  • 对象成员的构造函数
  • 派生类的构造函数

撤消对象时,析构函数的调用顺序与构造函数的调用顺序正好相反。
下面这个程序说明派生类构造函数和析构函数的执行顺序。

#include<iostream>
using namespace std;
class base
{
    int x;
public:
    base(int i) // 基类的构造函数
    {
        x = i;
        cout << "Constructing base class \n";
    }
    ~base() // 基类的析构函数
    {
        cout << "Destructing base class \n";
    }
    void show()
    {
        cout << "x = "<< x << endl;
    }
} ;
class derived: public base
{
    base d;
    // d 为基类对象,作为派生类的对象成员
public:
    derived(int i) : base (i), d(i) // 派生类的构造函数, 缀上基类构造函数和
    {
        // 对象成员构造函数
        cout
                << "Constructing derived class \n";
    }
    ~derived() // 派生类的析构函数
    { cout << "Destructing derived class \n"; }
} ;
int main ()
{
    derived obj(5 );
    obj.show();
    return 0 ;
}

程序执行结果如下:
Constructing base class
Constructing base class
Constructing derived class
x = 5
Destructing derived class
Destructing base class
Destructing base class
说明:

  1. 当基类构造函数不带参数时, 派生类不一定需要定义构造函数, 然而当基类的构造函数哪怕只带有一个参数,它所有的派生类都必须定义构造函数, 甚至所定义的派生类构造函数的函数体可能为空,仅仅起参数的传递作用。
  2. 若基类使用缺省构造函数或不带参数的构造函数, 则在派生类中定义构造函数时可略去“∶基类构造函数名(参数表)”; 此时若派生类也不需要构造函数, 则可不定义构造函数。
  3. 如果派生类的基类也是一个派生类, 则每个派生类只需负责其直接基类的构造,依次上溯。
  4. 由于析构函数是不带参数的, 在派生类中是否要定义析构函数与它所属的基类无关,故基类的析构函数不会因为派生类没有析构函数而得不到执行, 它们各 自是独立的。

三.多重继承

前面我们介绍的派生类只有一个基类, 这种派生方法称为单基派生或单一继承。当一个派生类具有多个基类时,这种派生方法称为多基派生或多重继承。

1.多重继承的声明

一般形式如下:

class 派生类名: 派生方式 1 基类名 1,,派生方式 n 基类名 n
{
// 派生类新增的数据成员和成员函数
} ;

冒号后面的部分称基类表,各基类之间用逗号分隔, 其中“派生方式 i”( i = 1, 2, …, n )规定了派生类从基类中按什么方式继承: private 或 public,缺省的派生方式是 private

class z: public x, y
{
    // 类 z 公有继承了类 x,私有继承了类 y 
    // …
};
class z: x, public y
{
    // 类 z 私有继承了类 x,公有继承了类 y
    // …
};
class z: public x, public y
{
    // 类 z 公有继承了类 x 和类 y
    // …
};

在多重继承中,公有派生和私有派生对于基类成员在派生类中的可访问性与单继承的规则相同。

#include <iostream>
using namespace std;
class X
{
    int a ;
public:
    void setX(int x)
    {
        a = x;
    }
    void showX()
    {
        cout << "a = "<< a << endl;
    }
};
class Y
{
    int b;
public:
    void setY(int x)
    {
        b = x;
    }
    void showY()
    {
        cout << "b = "<< b << endl;
    }
};
class Z: public X, private Y
{
    int c;
public:
    void setZ(int x, int y)
    {
        c = x;
        setY(y) ;
    }
    void showZ()
    {
        showY();
        cout << "c = "<< c << endl;
    }
};
int main()
{
    Z obj;
    obj .setX(3);
    obj .showX();
    //obj .setY(4);   // 错误
   // obj .showY();   // 错误
    obj .setZ(6, 8 );
    obj .showZ();
}

根据派生的有关规则,类 X 的公有成员在 Z 中仍是公有成员, 类 Y 的公有成员在 Z 中成为私有成员。所以, 在主函数中对类 X 的公有成员函数的引用是正确的, 因为在 Z 中它们仍是公有成员; 对 Y的成员函数的引用是错误的,因为 Y 的成员函数在 Z 中已成为私有成员,不能直接引用。
删去标有错误的两条语句,程序运行结果如下:
a = 3
b = 8
c = 6
说明:对基类成员的访问必须是无二义的, 例如下列程序段对基类成员的访问是二义的,必须想法消除二义性。

class X
{
public:
    int f();
};
class Y
{
public:
    int f();
    int g();
};
class Zpublic X, public Y
{
public:
    int g();
    int h();
};

假如定义类 Z 的对象 obj: Z obj;
则以下对函数 f( )的访问是二义的:
obj .f( ) ;

  • / / 二义性错误,不知调用的是类 X 的 f( ) ,还是类 Y 的 f( )

使用成员名限定可以消除二义性,例如:
obj .X∷f( ) ;

  • / / 调用类 X 的 f( )

obj .Y∷f( ) ;

  • / / 调用类 Y 的 f( )

2.多重继承的构造函数与析构函数

多重继承构造函数的定义形式与单继承构造函数的定义形式相似, 只是 n 个基类的构造函数之间用“,”分隔。多重继承构造函数定义的一般形式如下:

派生类构造函数名(参数表) :基类 1 构造函数名 ( 参数表), 基类 2 构造函数名 (参数表),,基类 n 构造函数名(参数表)
{
    // …
}

例如,由一个硬件类 Hard 和一个软件类 Soft ,它们共同派生出系统类 System, 声明如下:

class Hard
{
protected:
    char bodyname[20];
public:
    Hard(char * bdnm );     // 基类 Hard 的构造函数
    // …
};
class Soft
{
protected:
    char os[10];
    char Lang[15];
public:
    Soft( char * o, char * lg);// 基类 Soft 的构造函数
    // …
} ;
class System: public Hard, public Soft
{
private:
    char owner[10] ;
public:
    System( char * ow, char * bn, char * o, char * lg) // 派生类 System 的构造函数Hard( bn), Soft(o, lg);
    // 缀上了基类 Hard 和 Soft 的构造函数
    // …
};

注意:在定义派生类 System 的构造函数时,缀上了 Hard 和 Soft 的构造函数。
再如,现有一个窗口类 window 和一个滚动条类 scrollbar, 它们可以共同派生出一个带有滚动条的窗口,声明如下:

class window
{
    // 定义窗口类 window
    // …
public:
    window(int top, int left, int bottom, int right);
    ~window();
    // …
} ;
class scrollbar
{
    // 定义滚动条类 scrollbar
    // …
public:
    scrollbar(int top, int left, int bottom, int right);
    ~scrollbar();
    // …
};
class scrollbarwind∶window,scrollbar
{
    / / 定义派生类
    / /public:
    scrollbarwind(int top, int left, int bottom, int right);
    ~scrollbarwind();
    // …
};
scrollbarwind∷scrollbarwind(int top, int left, int bottom, int right)window( top, left,bot tom, right),scrollbar(top, right - 20, bottom, right)
{
    // …
}

在这个例子中, 定义派生类 scrollbarwind 的构造函数时, 也缀上了基类 window 和scrollbar 的构造函数。
下面我们再看一个程序,其中类 X 和类 Y 是基类, 类 Z 是类 X 和类 Y 共同派生出来的,请注意类 Z 的构造函数的定义方法。

#include<iostream>
using namespace std;
class X
{
    int a;
public:
    X(int sa ) // 基类 X 的构造函数
    {
        a = sa;
    }
    int getX()
    {
        return a ;
    }
};
class Y
{
    int b;
public:
    Y(int sb) // 基类 Y 的构造函数
    {
        b = sb;
    }
    int getY()
    {
        return b;
    }
} ;
class Z: public X, private Y
{
    // 类 Z 为基类 X 和基类 Y 共同的派生类
    int c;
public:
    Z(int sa, int sb, int sc ) :X(sa), Y(sb) // 派生类 Z 的构造函数,缀上
    {
        c = sc ;
    } // 基类 X 和 Y 的构造函数
    int getZ()
    {
        return c;
    }
    int getY()
    {
        return Y::getY();
    }
};
int main()
{
    Z obj( 2, 4, 6) ;
    int ma = obj.getX();
    cout << "a = "<< ma << endl;
    int mb = obj .getY();
    cout << "b = "<< mb << endl;
    int mc = obj .getZ();
    cout << "c = "<< mc << endl;
    return 0 ;
}

上述程序运行的结果如下:
a = 2
b = 4
c = 6
由于派生类 Z 是 X 公有派生出来的, 所以类 X 中的公有成员函数 getX( )在类 Z 中仍是公有的, 在 main ( ) 中可以直接引用, 把成员 a 的值赋给 main ( ) 中的变量 ma ,并显示在屏幕上。类 Z 是从类 Y 私有派生出来的, 所以类 Y 中的公有成员函数 getY( ) 在类 Z 中成为私有的,在 main( ) 中不能直接引用。为了能取出 b 的值,在 Z 中另外定义了一个公有成员函数 Z∷getY( ) ,它通过调用 Y∷getY( ) 取出 b 的值。主函数 main( )中的语句:int mb = obj .getY( ) ;调用的是派生类 Z 的成员函数 getY( ) , 而不是基类 Y 的成员函数 getY( )。由于类 Z 中的成员函数 getZ( ) 是公有成员,所以在 main( ) 中可以直接调用取出 c 的值。

总结:
多重继承的构造函数的执行顺序与单继承构造函数的执行顺序相同, 也是遵循先执行基类的构造函数,再执行对象成员的构造函数, 最后执行派生类构造函数的原则。在多个基类之间, 则严格按照派生类声明时从左到右的顺序来排列先后。而析构函数的执行顺序则刚好与构造函数的执行顺序相反。

3.虚基类

为什么要引入虚基类

当引用派生类的成员时, 首先在派生类自身的作用域中寻找这个成员, 如果没有找到,则到它的基类中寻找。如果一个派生类是从多个基类派生出来的, 而这些基类又有一个共同的基类,则在这个派生类中访问这个共同的基类中的成员时, 可能会产生二义性。

#include <iostream>
using namespace std;
class base
{
protected:
    int a;
public:
    base()
    {
        a = 5;
    }
};
class base1:public base
{
public:
    base1()
    {
        cout << "base1 a = "<< a << endl;
    }
};
class base2:public base
{
public:
    base2()
    {
        cout << "base2 a = "<< a << endl;
    }
};
class derived: public base1, public base2
{
public:
    derived()
    {
        cout << "derived a = "<< a << endl;
    }
};
int main()
{
    derived obj;
    return 0 ;
}

上述程序中,类 base 是一个基类, 从类 base 派生出类 base1 和类 base2, 这是两个单一继承;从类 base1 和类 base2 共同派生出类 derived, 这是一个多重继承。
这是一个存在问题的程序, 问题出在派生类 derived 的构造函数的定义上, 它试图输出一个它有权访问的变量 a, 表面上看来这是合理的,但实际上它对 a 的访问存在二义性,即函数中的变量 a 的值可能是从 base1 的派生路径上来的, 也有可能是从类 base2 的派生路径上来的,这里没有明确的说明。
虽然 base1 和 base2 是从同一个基类 base 派生而来的, 但它们所对应的是基类 base的不同拷贝。类 derived 是 base1 和 base2 的派生类,因此类 base 是类 derived 的间接基类,它有两个拷贝与类 derived 相对应,一个是 base1 派生路径上的拷贝,另一个是 base2 派生路径上的拷贝。当类 derived 要访问这个间接基类 base 时, 必须指定要 访问的是哪个路径上 的base 拷贝。
为了解决这种二义性, C + + 引入了虚基类的概念。

2.虚基类的概念

不难理解,如果在上例中类 base 只存在一个拷贝, 那么对 a的引用就不会产生二义性。在 C + + 中,如果想使这个公共的基类只产生一个拷贝,则可以将这个基类说明为虚基类。这就要求从类 base 派生新类时, 使用关键字 virtual 将类base 说明为虚基类。

#include <iostream>
using namespace std;
class base
{
protected:
    int a;
public:
    base()
    {
        a = 5;
    }
};
class base1:virtual public base
{
public:
    base1()
    {
        cout << "base1 a = "<< a << endl;
    }
};
class base2:virtual public base
{
public:
    base2()
    {
        cout << "base2 a = "<< a << endl;
    }
};
class derived:public base1, public base2
{
public:
    derived()
    {
        cout << "derived a = "<< a << endl;
    }
};
int main ()
{
    derived obj;
    return 0 ;
}

在上述程序中,从类 base 派生出类 base1 和类 base2 时,使用了关键字 virtual ,把类base 声明为 base1 和 base2 的虚基类。这样, 从 base1 和base2 派生出的类 derived 只有一个基类 base, 从而可以消除二义性。

3.虚基类的初始化

虚基类的初始化与一般的多重继承的初始化在语法上是一样的,但构造函数的调用顺序不同。虚基类构造函数的调用顺序是这样规定的:

  1. 若同一层次中包含多个虚基类, 这些虚基类的构造函数按对它们说明的先后次序调用。
  2. 若虚基类由非虚基类派生而来, 则仍然先调用基类构造函数, 再调用派生类的构造函数。
  3. 若同一层次中同时包含虚基类和非虚基类, 应先调用虚基类的构造函数, 再调用非虚基类的构造函数,最后调用派生类构造函数, 例如:
class Xpublic Y, virtual public Z
{
    // …
};
X one;

定义类 X 的对象 one 时,将产生如下的调用次序:
Z( ) ;
Y( ) ;
X( ) ;

#include<iostream>
using namespace std;
class base
{
    int a ;
public:
    base (int sa)
    {
        a = sa;
        cout << "Constructing base"<< endl;
    }
};
class base1: virtual public base
{
    int b;
public:
    base1 (int sa, int sb) : base(sa)
    {
        b = sb;
        cout << "Constructing baes1"<< endl;
    }
};
class base2: virtual public base
{
    int c;
public:
    base2 (int sa, int sc) : base (sa)
    {
        c = sc ;
        cout << "Constructing baes2"<< endl;
    }
};
class derived: public base1, public base2
{
    int d;
public:
    derived(int sa, int sb, int sc, int sd ) :
        base(sa ), base1 (sa,sb), base2(sa, sc )
    {
        d = sd;
        cout << "Constructing derived"<< endl;
    }
};
int main()
{
    derived obj(2, 4, 6, 8 ) ;
    return 0 ;
}

在上述程序中, base 是一个虚基类,它只有一个带参数的构造函数, 因此要求在派生类 base1、base2 和 derived 的构造函数的初始化表中,都必须带有对 base 构造函数的调用。

如果 base 不是虚基类,在派生类 derived 的构造函数的初始化表中调用 base 的构造函数是错误的,但是当 base 是虚基类且只有带参数的构造函数时, 就必须在类 derived 构造函数的初始化表中调用类 base 的构造函数。因此, 在 derived 构造函数的初始化表中,不仅含有对 base1 和 base2 构造函数的调用, 还有对虚基类 base 构造函数的调用。上述程序运行的结果为:
Constructing base
Constructing base1
Constructing base2
Constructing derived

不难看出,上述程序中虚基类 base 的构造函数只执行了一次。显然, 当 derived 的构造函数调用了虚基类 base 的构造函数之后, 类 base1 和类 base2 对 base 构造函数的调用被忽略了。这也是初始化虚基类和初始化非虚基类不同的地方。
说明:

  1. 关键字 virtual 与派生方式关键字 ( public 或 private ) 的先后顺序无关紧要, 它只说明是“虚拟派生”。例如以下两个虚拟派生的声明是等价的。
class derived: virtual public base{
// …
} ;
class derived: public virtual base{
// …
} ;
  1. 一个基类在作为某些派生类虚基类的同时, 又作为另一些派生类的非虚基类, 这种情况是允许存在的,例如:
class B{
// …
} ;
class Xvirtual public B{
// …
} ;
class Yvirtual public B{
// …
} ;
class Zpublic B{
// …
} ;
class AApublic X, public Y , public Z{
// …
} ;

此例中,派生类 AA 由类 X、类 Y 和类 Z 派生而来。AA 与它的间接基类 B 之间的对应关系是:类 B 既是 X、Y 继承路径上的一个虚基类, 也是 Z 继承路径上的一个非虚基类。

已标记关键词 清除标记
©️2020 CSDN 皮肤主题: 岁月 设计师:pinMode 返回首页