在派生类中,成员可以按访问属性分为以下四种:
(1)不可访问成员。这是从基类私有成员继承下来的,派生类或是建立派生类对象的模块都无法访问到它们,如果从派生类继续派生新类,也是无法访问的。
(2)私有成员。包括从基类继承过来成员以及新增的成员,在派生类内部可以访问,但是建立派生类对象的模块无法访问,继续派生,就变成了新的派生类中的不可访问成员。
(3)保护成员。可能是新增也可能是从基类继承过来的,派生类内部成员可以访问,建立派生类对象的模块无法访问,进一步派生,在新的派生类中可能成为私有成员或者保护成员。
(4)公有成员。派生类、建立派生类对象的模块都可以访问,继续派生,可能是新派生类中的私有或者保护成员。
在对派生类的访问中,我们只能访问一个能够唯一标识的可见成员。如果通过某一个表达式能引用的成员不只一个,称为有二义性。
1.作用域分辨符
作用域分辨符就是我们常见的“::
”,它可以用来限定要访问的成员所在的类的名称。一般的使用形式为:
类名::成员名//数据成员
类名::成员名(参数表)//函数成员
2.作用域分辨符在类族层次结构中唯一标识成员
对于在不同的作用域声明的标识符,可见性原则是:如果存在两个或多个具有包含关系的作用域,外层声明了一个标识符,而内层没有再次声明同名标识符,那么外层标识符在内层仍然可见;如果在内层声明了同名标识符,则外层标识符在内层不可见,这时称为内层标识符隐藏了外层同名标识符,这种现象叫做隐藏规则。
在类的派生层次结构中,基类的成员和派生类新增的成员都具有作用域。二者的作用范围不同,是相互包含的两个层,派生类在内层。这时,如果派生类声明了一个和某个基类成员同名的新成员,派生类的新成员隐藏了外层基类中的同名成员,直接使用成员名只能访问到派生类的成员。如果派生类中声明了与基类成员函数同名的新函数,即使函数的参数表不同,从基类继承的同名函数的所有重载形式也都会被隐藏。。如果要访问隐藏的成员,就需要使用作用域分辨符和基类名来限定。
对于多继承情况,首先考虑各个基类之间有没有继承关系,同时也没有共同基类的情况。最经典的情况就是所有基类都没有上级基类。如果某派生类的多个基类拥有同名成员,同时,派生类又新增这样的同名成员,在这种情况下,派生类成员将隐藏所有基类的同名成员。 这时,使用“对象名.成员名”或者“对象指针->成员名”的方式可以唯一标识和访问派生类的新增成员,基类的同名成员也可以使用基类名和作用域分辨符访问。但是,如果派生类没有声明同名成员,使用“对象名.成员名”或者“对象指针->成员名”的方式就无法唯一标识成员。这时,从不同基类继承过来的成员具有相同的名称,同时具有相同的作用域,这时就必须通过基类名和作用域分辨符来标识成员。
【例】定义基类B1,B2,由基类B1,B2共同公有派生产生新类D。两个基类中都声明了数据成员v和函数fun,在派生类中新增同名的两个成员。这时的D类中共含有6个成员,而这6个成员只有两个名字。
#include<iostream>
using namespace std;
class B1//定义基类B1
{
public:
int v;
void fun()
{
cout << "基类B1的成员" << endl;
}
};
class B2//定义基类B2
{
public:
int v;
void fun()
{
cout << "基类B2的成员" << endl;
}
};
class D :public B1, public B2//定义派生类D
{
public:
int v;//同名数据成员
void fun()//同名函数成员
{
cout << "派生类D的成员" << endl;
}
};
int main()
{
D d;
D* p = &d;
d.v = 1;//对象名.成员名标识
d.fun();//D类对象d访问D类成员函数fun
d.B1::v = 2;//作用域分辨符标识
d.B1::fun();//D类对象d访问B1类成员函数fun
p->B2::v = 3;//作用域分辨符标识
p->B2::fun();//D类对象d访问B2类成员函数fun
return 0;
}
在主函数中,创建了一个派生类D的对象d,根据隐藏规则,如果通过成员名称来访问该类的成员,就只能访问到派生类新增的两个成员,从基类继承过来的成员由于外层作用域被隐藏。这时,就必须使用类名和作用域分辨符来访问从基类继承来的成员。
主函数中后面两组语句:
d.B1::v = 2;//作用域分辨符标识
d.B1::fun();//D类对象d访问B1类成员函数fun
p->B2::v = 3;//作用域分辨符标识
p->B2::fun();//D类对象d访问B2类成员函数fun
就是分别访问由基类B1、B2继承来的成员。通过作用域分辨符,明确地唯一标识了派生类中由基类所继承来的成员,达到了访问的目的,解决了成员被隐藏的问题。
如果在上例中,派生类没有声明与基类同名的成员,那么采用“对象名.成员名”就无法访问到任何成员,来自B1、B2 类的同名成员具有相同的作用域,系统根本无法进行唯一标识,这时就需要使用作用域分辨符。将上例中的派生类改为如下形式:
class D :public B1, public B2//定义派生类D
{};
程序其余部分不改变,主函数中“对象名.成员名”的访问方式就会出错:
如果希望 d.v = 1;
和d.fun();
的用法不产生二义性,可以使用using关键字加以澄清。例如:
class D :public B1, public B2//定义派生类D
{
public:
using B1::v;
using B1::fun;
};
这样,主函数中的 d.v = 1;
和d.fun();
都可以明确表示对B1中的相关成员的引用了。
using的一般功能是将一个作用域中的名字引入到另一个作用域中,它还有一个非常有用的用法:将using用于基类中的函数名,这样派生类中如果定义同名但参数不同的函数,基类的函数不会被隐藏,两个重载函数将会并存在派生类的作用域中。例如:
#include<iostream>
using namespace std;
class B1//定义基类B1
{
public:
int v;
void fun()
{
cout << "基类B1的成员" << endl;
}
};
class D2 :public B1
{
public:
using B1::fun;
void fun(int i)
{
cout << i << endl;
}
};
int main()
{
D2 dd;
dd.fun();
dd.fun(5);
return 0;
}
运行结果:
这时使用D2的对象,既可以直接调用基类B1中的无参数的fun,又可以直接调用派生类D2中带int型参数的fun函数。
如果某个派生类的部分或全部直接基类是从另一个共同的基类派生而来的,在这些直接基类中,从上一级基类继承来的成员就拥有相同的名称,因此派生类中也就会产生同名的现象,对这种类型的同名成员也要使用作用域分辨符来唯一标识,而且必须用直接基类来进行限定。
【例】有一个基类B0,声明了数据成员v0和函数成员fun0,由B0公有派生了类B1和类B2,在以B1,B2作为基类共同公有派生了新类D。在派生类中不再添加新的同名成员,这时的D类,就含有通过B1,B2继承来的基类B0中的同名成员v0和fun0。
class B0
{
public:
int v0;
void fun0()
{
cout << "基类B0的成员" << endl;
}
};
class B1 :public B0
{
public:
int v1;
};
class B2 :public B0
{
public:
int v2;
};
class D :public B1, public B2
{
public:
int v;
void fun()
{
cout << "基类D的成员" << endl;
}
};
int main()
{
D d;
d.B1::v0 = 2;
d.B1::fun0();
d.B2::v0 = 3;
d.B2::fun0();
return 0;
}
运行结果:
分析:
在主函数中,创建了派生类D的对象d,如果只通过成员名来访问该类的成员v0和fun0,系统无法唯一确定要引用的成员。这时,必须采用作用域分辨符,通过直接基类名来确定要访问的从基类继承来的成员。
这种情况下,派生类的对象在内存中就同时拥有成员v0的两份同名副本。如下图所示:
对于数据成员来讲,虽然两个v0可以分别通过B1和B2调用B0的构造函数进行初始化,可以存放不同的数值,也可以使用作用域分辨符通过直接基类名限定来分别进行访问,但是很多情况下,我们只需要一个数据副本。同一成员的多份副本增加了内存的开销。C++中提供了虚基类技术解决这一问题。
【注意】上例中,其实B0类的函数成员fun0()的代码始终只有一个副本,之所以调用fun0函数时仍然需要用基类名B1和B2加以限定,是因为调用非静态成员函数总是针对特定的对象,执行函数时需要将指向该类的一个对象的指针作为隐含的参数传递给被调函数来初始化this指针。上例中,D类的对象中存在两个B0类的子对象,因此调用fun0函数时,需要使用B1和B2加以限定,这样才能明确针对哪个B0对象调用。