C++继承和派生——派生类成员的标识与访问(作用域分辨符和虚基类技术)

时间:2022-09-07 23:18:40

在派生类中,成员可以按访问属性划分为以下四种:

  • 不可访问成员
    准确说是不可以直接访问。这种成员是从基类私有成员继承而来,派生类或者派生类对象的模块都无法访问这些成员,当然,派生类继续派生的新类也是无法访问它们的。
  • 私有成员
    这个可以是从基类继承过来的成员和新增加的成员,在派生类内部可以访问,但是建立派生类对象的模块中无法访问,继续派生,这些成员就会变成新的派生类中的不可访问成员。
  • 保护成员
    可以是从基类继承过来的也可以是新增的,在派生类的内部可以访问,建立派生类对象的模块无法访问,继续派生,这些成员在新的派生类中可能成为私有成员或者保护成员。
  • 公有成员
    派生类、派生类的对象模块都可以访问它们,继续派生,这些公有成员可能成为新派生类中的私有、保护或者公有成员。

因为派生类可能会多重继承,所以在对派生类的访问中,需要解决两个问题:
     ①唯一标识符问题,②成员属性问题(成员可见性问题)

只有能够唯一标识的可见性对象我们才可直接访问。如果通过某一个表达式能够引用的成员不止一个,那么就有二义性。

下面将介绍两种方法:作用域分辨符和虚基类,来解决以上问题。

作用域分辨符

在C++中,经常见到的“::”就是作用域分辨符,它的作用就是限定访问的成员所在的类的名称。
使用作用域分辨符的一般形式为:


类名::成员名             //数据成员
类名::成员名             //函数成员


在说明作用域分辨符如何在类族中唯一标识成员时,先介绍一个现象。

在嵌套的作用域中,当外层作用域声明了一个标识符,而内层没有再次声明此标识符,那么外层标识符在内层仍然可见;否则,外层标识符在内层将不可见,内层重新声明的这个标识符会隐藏了外层同名标识符,这个现象称为隐藏规则

============== 假设条件:当所有基类之间都没有继承关系 ==============

当派生类继承的多个基类拥有同名的成员,并且派生类新增了这样的同名成员,如上所述,派生类的这个成员会隐藏所有基类中的同名成员。使用“对象.成员名”或者“对象指针->成员名”可以唯一标识和访问派生类新增的成员,访问隐藏的基类成员则需要使用基类名和作用域分辨符访问

若是派生类中没有新增同名成员,使用“对象.成员名”或者“对象指针->成员名”就无法唯一标识成员!因为从不同基类继承过来的成员具有相同的名称,以及相同的作用域,系统根据这些信息无法判断调用哪一个基类的成员,这时就必须要使用基类名和作用域标识符来表示成员。

这里需要注意:子类中若是有与父类同名但是参数不同的函数,这不属于函数重载,子类中的函数将会隐藏父类中的同名函数,此时要调用父类中的函数需要使用父类名来限定。只有作用域相同函数可以进行重载。

访问基类的隐藏同名成员示例

例1
#include <iostream>
#include <cstdlib>
using namespace std;
class Base1
{
public:
    int var;
    void fun() { cout << "In Base1,var is " << var << endl; }
};
class Base2
{
public:
    int var;
    void fun() { cout << "In Base2, var is " << var << endl; }
};
class Derived:public Base1,public Base2
{
public:
    int var;
    void fun() { cout << "In Derived,var is " << var << endl; }
};

int main(int argc, char * argv[])
{
    Derived objd;
    Derived * objp = new Derived;

    objd.var = 1;           //对象.成员名访问 Derived中的同名对象
    objd.fun();

    objp->Base1::var = 2;   //类名加作用域分辨符访问 唯一标识访问成员
    objp->Base1::fun();

    objp->Base2::var = 3;
    objp->Base2::fun();

    system("pause");
    return 0;
}

运行结果

In Derived,var is 1
In Base1,var is 2
In Base2, var is 3

上述举例是以派生类Derived中新增了同名成员为例,若是Derived中没有新增同名成员,main函数中对象.成员名的访问方式就会出错,提示访问对象不明确即这种访问方式具有二义性。
若要使对象.成员名的访问方式没有二义性,则可在Derived类的定义中使用using关键字具体说明访问成员为哪个基类的。
如下所示,使用using关键字说明,要访问的成员为基类Base1中的。

class Derived:public Base1,public Base2
{
public:
    using Base1::var;
    using Base1::fun;
};

>>>>>> using <<<<<<
using的一般功能是将一个作用域中的名字引入到另一个作用域中
利用这个作用可以做一件有趣的事情:使用using将基类的函数名引入到派生类中,在派生类中定义同名但参数不同的函数,实现函数重载。基类的这个函数不会被隐藏,与派生类中的同名函数共存在派生类的作用域中。

class Derived:public Base1,public Base2
{
public:
    using Base1::fun;
    void fun(int a,int b){...}
};


============== 基类之间有继承关系 ==============

假设所有的基类都没有继承关系不太可能存在,若是派生类的部分或者全部直接基类是从另外一个共同基类派生而来,那么在这些直接基类中,从上一级基类继承来的成员就拥有相同的名称,因此派生类中也会出现同名现象,对这种类型的成员要使用作用域分辨符来唯一标识,并且必须用直接基类限定!

类之间的一个派生关系如下:

C++继承和派生——派生类成员的标识与访问(作用域分辨符和虚基类技术)

例2
#include <iostream>
#include <cstdlib>
using namespace std;
class Base0
{
public:
    int var0;
    void fun0(){ cout << "In Base1,var is " << var0 << endl; }
};

class Base1:public Base0
{
public:
    int var1;
    void fun() { cout << "In Base1,var is " << var1 << endl; }
};
class Base2:public Base0
{
public:
    int var2;
    void fun() { cout << "In Base2, var is " << var2 << endl; }
};
class Derived:public Base1,public Base2
{
public:
    int var;
    void fun() { cout << "In Derived,var is " << var << endl; }
};

int main(int argc, char * argv[])
{
    Derived d;
    d.Base1::var0 = 1;  //使用直接基类访问
    d.Base1::fun0();

    d.Base2::var0 = 2;
    d.Base2::fun0();

    system("pause");
    return 0;
}

运行结果

In Base1,var is 1
In Base1,var is 2

在上述程序中,派生类对象在内存中拥有var0的两份同名副本,很多时候我们只需要一份,多的一份增加了内存开销。C++中提供了虚基类技术来解决这个问题。

需要注意上例中Base0类的函数成员fun0的代码始终只有一个副本,之所以还要加直接基类名限定是因为调用非静态成员函数是针对特定的对象,执行函数调用时需要将指向该类的一个对象指针作为隐含地参数传递给被调用的函数初始化this指针。
上例中,Derived类的对象中存在两个Base0类的对象,所以在调用fun0时,需要使用Base1和Base1限定,明确针对的是哪个Base0对象

虚基类

当派生类中的部分或者全部直接基类从另外一个共同基类派生而来时,在这些直接基类中,从上一级基类继承来的成员就拥有相同的名称。在派生类对象中同名数据成员会有多个副本,同一个函数名会有多个映射。
除了使用作用域分辨符唯一识别成员,还可以将共同基类设置为虚基类,这样从不同路径继承过来的同名数据成员在内存中只有一个副本,同名函数成员也只有一个映射。这样也可以解决同名成员唯一标识问题。

虚基类的声明是在派生类的定义过程中进行的,语法形式如下:

class 派生类名:virtual 继承方式 基类名

这样声明了基类为派生类的虚基类。在多继承的情况下,虚基类关键字的作用范围和继承方式关键字相同,只对紧跟其后的基类起作用。

虚基类示例

例3
#include <iostream>
#include <cstdlib>
using namespace std;
class Base0
{
public:
    int var0;
    void fun0(){ cout << "In Base1,var is " << var0 << endl; }
};

class Base1:virtual public Base0
{
public:
    int var1;
    void fun() { cout << "In Base1,var is " << var1 << endl; }
};
class Base2:virtual public Base0
{
public:
    int var2;
    void fun() { cout << "In Base2, var is " << var2 << endl; }
};
class Derived:public Base1,public Base2
{
public:
    int var;
    void fun() { cout << "In Derived,var is " << var << endl; }
};

int main(int argc, char * argv[])
{
    Derived d;
    d.var0 = 0;//直接访问虚基类数据成员和函数成员
    d.fun0();
    system("pause");
    return 0;
}

运行结果

In Base1,var is 0

作用域分辨符和虚基类技术的区别

在例1和例2中可以看到,派生类中拥有的同名成员的多个副本,在其中使用直接基类名和作用域分辨符唯一标识成员,可以存放不同数据和进行不同操作。在例3中,只维护了一份成员副本。前者可以容纳更多的数据,而后者使用更为简洁,内存空间也更为节省。

虚基类及其派生类构造函数

当类中没有声明构造函数时,编译器会为类自动生成默认构造函数。但是虚基类中声明了带形参的构造函数,那情况可能有点复杂。

例4
#include <iostream>
#include <cstdlib>
using namespace std;
class Base0
{
public:
    Base0(int var):var0(var){
        cout << "Constructor in Base1,var is " << var0 << endl; }
    ~Base0() { cout << "Destroy Base0" << endl;}
    int var0;
};

class Base1:virtual public Base0  //Base0为虚基类
{
public:
    Base1(int var):Base0(var),var1(var){
        cout << "Constructor in Base1,var is " << var1 << endl; }
    ~Base1() { cout << "Destroy Base1" << endl; }
    int var1;
};
class Base2:virtual public Base0
{
public:
    Base2(int var):Base0(var),var2(var0){
        cout << "Constructor in Base2,var is " << var2 << endl; }
    ~Base2() { cout << "Destroy Base2" << endl; }
    int var2;

};
class Derived:public Base1,public Base2
{
public:
    Derived(int var):Base0(var),Base1(var),Base2(var),var(var){
        cout << "Constructor in Derived,var is " << var << endl;
    }
    ~Derived(){ cout << "Destroy Derived" << endl; }
    int var;
};

int main(int argc, char * argv[])
{
    Derived d(1);
    system("pause");
    return 0;
}

以上述派生关系为例,可能会疑惑:Base0基类中声明了带形参的构造函数,当派生类Derived创建对象时,会调用Base0、Base1和Base2的构造函数,在调用Base0的构造函数时,对成员var0进行了初始化,而Base1和Base2的初始化列表中也会调用Base0构造函数对var0进行初始化,这样var0岂不是被初始化了3次?!

C++编译器中有解决这个问题的方法:假设创建对象的派生类为最远派生类这里为Derived。若是最远派生类创建对象,这个对象中含有从虚基类那里继承过来的成员,那么虚基类的这个成员的初始化是由最远派生类调用虚基类的构造函数进行初始化。
也就是说只有最远派生类才会调用虚基类的构造函数,该派生类的其他基类如Base1和Base2对虚基类构造函数的调用都会自动被忽略!

例4执行结果

Constructor in Base0,var is 1
Constructor in Base1,var is 1
Constructor in Base2,var is 1
Constructor in Derived,var is 1
Destroy Derived
Destroy Base2
Destroy Base1
Destroy Base0

可以看出,虚基类的构造函数只被调用了一次。

创建派生类的对象,构造函数的执行顺序

当一个类创建一个对象时,构造函数的调用是有一定顺序的:
(但是与派生类构造函数初始化形参类表出现次序无关)

  1. 如果该类有直接或者间接的虚基类,则先执行虚基类的构造函数

  2. 该类若有其他基类,则按它们在继承声明时的顺序分别执行它们的构造函数,但不执行它们虚基类中的构造函数

  3. 按照在类的定义中出现的顺序,对派生类中新增的成员对象进行初始化。

  4. 执行构造函数的函数体

析构函数的执行次序相反

例5
#include <iostream>
#include <cstdlib>
using namespace std;

class Base1
{
public:
Base1(int i) { cout << "Constructing Base1 " << i << endl; }
~Base1() { cout << "Destructing Base1..." << endl; }
};

class Base2
{
public:
Base2(int i) { cout << "Constructing Base2 " << i << endl; }
~Base2() { cout << "Destructing Base2..." << endl; }
};

class Base3
{
public:
Base3() { cout << "Constructing Base3" << endl; }
~Base3() { cout << "Destructing Base3..." << endl; }
};

class Derived :public Base1, public Base2, public Base3
{
public://执行次序(与派生类构造函数初始化形参类表出现次序无关)
Derived(int a,int b,int c,int d):Base1(a),member2(d),member1(c),Base2(b){}
private:
Base1 member1;
Base2 member2;
Base3 member3;
};

int main()
{
Derived(1, 2, 3, 4);
system("pause");
return 0;
}

例5执行结果

Constructing Base1 1
Constructing Base2 2
Constructing Base3
Constructing Base1 3
Constructing Base2 4
Constructing Base3
Destructing Base3...
Destructing Base2...
Destructing Base1...
Destructing Base3...
Destructing Base2...
Destructing Base1...

运行结果符合上述介绍的执行顺序


这篇博客主要记录了在唯一标识类中成员的两种方法:作用域分辨符和虚基类技术,并且附加一些细节点。