C++类的作用域详细讲解
C++:类作用域
每个类都定义了自己的新作用域和唯一的类型。在类的定义体内声明类成员,将成员名引入类的作用域。两个不同的类具有两个不同的作用域。
即使两个类具有完全相同的成员列表,它们也是不同的类型。每个类的成员不同于任何其他类(或任何其他作用域)的成员。
例如:
class First {
public:
intmemi;
double memd;
};
class Second {
public:
intmemi;
doublememd;
};
First obj1;
Second obj2=obj1; //error: obj1 and obj2 have different types
1. 使用类的成员
在类作用域之外,成员只能通过对象或指针分别使用成员访问操作符 . 或->来访问。这些操作符左边的操作数分别是一个类对象或指向类对象的指针。跟在操作符后面的成员名字必须在相关联的类的作用域中声明:
Class obj; //Class is some class type
Class *ptr = &obj;
// member is a data member of that class
ptr->member; //fetches member from the object to which ptr points
obj.member; //fetches member from the object named obj;
// memfcn is a function member of that class
ptr->memfcn( ); //runs memfcn on the object to which ptr points
obj.memfcn( ); // runs memfcn on the object named obj
一些成员使用成员访问操作符来访问,另一些直接通过类使用作用域操作符(::)来访问。一般的数据或函数成员必须通过对象来访问。定义类型的成员,如Screen::index,使用作用域操作符来访问。
2. 作用域与成员定义
尽管成员是在类的定义体之外定义的,但成员定义就好像它们是在类的作用域中一样。回忆一下,出现在类的定义体之外的成员定义必须指明成员出现在哪个类中:
double Sales_item::avg_price( ) const
{
if (units_sold )
return revenue/units_sold;
else
return 0;
}
在这里,我们用完全限定名Sales_item::avg_price来指出这是类Sales_item作用域中的avg_price成员的定义。一旦看到成员的完全限定名,就知道该定义是在类作用域中。因为该定义是在类作用域中,所以我们可以引用revenue或units_sold,而不必写this->revenue或this->units_sold。
3. 形参表和函数体处于类作用域中
在定义于类外部的成员函数中,形参表和成员函数体都出现在成员名之后。这些都是在类作用域中定义,所以可以不用限定而引用其他成员。例如,类Screen中get的二形参版本的定义:
char Screen::get( index r, index c ) const
{
indexrow= r * width; // compute the rowlocation
returncontents[ row+c ]; //offset by c tofetch specified character
}
该函数用Screen内定义的index类型来指定其形参类型。因为形参表是在Screen类的作用域内,所以不必指明我们想要的是Screen::index。我们想要的是定义在当前类作用域中的,这是隐含的。同样,使用index、width和contents时指的都是Screen类中声明的名字。
4. 函数返回类型不一定在类作用域中
与形参类型相比,返回类型出现在成员名字前面。如果函数在类定义体之外定义,则用于返回类型的名字在类作用域之外。如果返回类型使用由类定义的类型,则必须使用完全限定名。例如,考虑get_cursor函数:
class Screen {
public:
typedef std::string::size_type index;
indexget_cursor ( ) const;
};
inline Screen::index Screen::get_cursor ( ) const
{
returncursor;
}
该函数的返回类型是index,这是在Screen类内部定义的一个类型名。如果在类定义体之外定义get_cursor,则在函数名被处理之前,代码不在类作用域内。当看到返回类型时,其名字是在类作用域之外使用。必须用完全限定的类型名Screen::index来指定所需要的index是在类Screen中定义的名字。
类作用域中的名字查找
迄今为止,在我们所编写的程序中,名字查找(寻找与给定的名字使用相匹配的声明的过程)是相对直接的。
(1)首先,在使用该名字的块中查找名字的声明。只考虑在该项使用之前声明的名字。
(2)如果找不到该名字,则在包围的作用域中查找。
如果找不到任何声明,则程序出错。在C++程序中,所有名字必须在使用之前声明。
类作用域也许表现得有点不同,但实际上遵循同一规则可能引起混淆的是函数中名字确定的方式,而该函数是在类定义体内定义的。
类定义实际上在两个阶段中处理:
(1)首先,编译成员声明;
(2)只有在所有成员都出现之后,才编译它们的定义本身。
当然,类作用域中使用的名字并非必须是类成员名。类作用域中的名字查找也会发现在其他作用域中声明的名字。在名字查找期间,如果类作用域中使用的名字不能确定为类成员名,则在包含该类或成员定义的作用域中查找,以便找到该名字的声明。
1. 类成员声明的名字查找
按以下方式确定在类成员的声明中用到的名字。
l 检查出现在名字使用之前的类成员的声明
l 如果第一步查找不成功;则检查包含类定义的作用域中出现的声明以及出现在类定义之前的声明。
例如:
typedef double Money;
class Account {
public:
Moneybalance ( ) { return bal; }
private:
Money;
//...
};
在处理balance函数的声明时,编译器首先在类Account的作用域中查找Money的声明。编译器只考虑出现在Money使用之前的声明。因为找不到任何成员声明,编译器随后在全局作用域中查找Money的声明。只考虑出现在类Account的定义之前的声明。找到全局的类型别名Money的声明,并将它用作函数balance的返回类型和数据成员bal的类型。
必须在类中先定义类型的名字,才能将它们用作数据成员的类型,或者成员函数的返回类型或形参类型。
编译器按照成员声明在类中出现的次序来处理它们。通常,名字必须在使用之前进行定义。而且,一旦一个名字被用作类型名,该名字就不能被重复定义:
typedef double Money;
class Account {
public:
Money balance ( ) { return bal; } // uses global definition of Money
Private:
//error: cannot change meaning of Money
typedef long double Money;
Money bal;
//......
};
2. 类成员定义中的名字查找
按以下方式确定在成员函数的函数体中用到的名字。
l首先检查成员函数局部作用域中的声明。
l如果在成员函数中找不到该名字的声明,则检查对所有类成员的声明。
l如果在类中找不到该名字的声明,则检查在此成员函数定义之前的作用域中出现的声明。
3. 类成员遵循常规的块作用域名字查找
示例名字查找的程序经常不得不依赖一些坏习惯。下面的几个程序故意包含了坏的风格。
下面的程序使用了相同的名字来表示形参和成员,这是通常应该避免的。这样做的目的是展示如何确定名字:
// Note: This code is for illustration purposesonly and reflects bad practice
// It is a bad idea to use the same name for aparameter and a member
int height;
class Screen {
public:
void dummy_fcn( index height ) {
cursor=width + height; // whichheight? The parameter
}
private:
index cursor;
index height, width;
}
查找dummy_fcn的定义中使用的名字height的声明时,编译器首先在该函数的局部作用域中查找,函数的局部作用域中声明了一个函数形参。dummy_fcn的函数体中使用的名字height指的就是这个形参声明。
在本例中,height形参屏蔽名为height的成员。
尽管类的成员被屏蔽了,但仍然可以通过用类名来限定成员名或显式使用this指针来使用它。
如果我们想覆盖常规的查找规则,应该这样做:
// bad practice: Names local to member functtionsshouldn't hide member names
void dummy_fcn ( index height ) {
cursor =width * this->height; //member height
//alternative way to indicate the member
cursor =width * Screen::height; // member height
}
4. 函数作用域之后,在类作用域中查找
如果想要使用height成员,更好的方式也许是为形参取一个不同的名字:
// good practice: Don't use member name for aparameter or other local variable
void dummy_fcn( index ht ) {
cursor =width * height; // member height
}
现在当编译器查找名字height时,它将不会在函数内查找该名字。编译器接着会在Screen类中查找。因为height是在成员函数内部使用,所以编译器在所有成员声明中查找,尽管height是先在dummy_fcn中使用,然后再声明,编译器还是确定这里使用的是名为height的数据成员。
5. 类作用域之后,在外围作用域中查找
如果编译器不能在函数或类作用域中找到,就在外围作用域中查找。在本例子中,出现在Screen定义之前的全局作用域中声明了一个名为height的全局对象。然而,该对象被屏蔽了。
尽管全局对象被屏蔽了,但通过用全局作用域确定操作符来限定名字,仍然可以使用它。
//bad practice: Don't hide names that are needed from surrounding scopes
voiddummy_fcn ( index height ) {
cursor= width * ::height; // which height? The global one
}
6. 在文件中名字的出现处确定名字
当成员定义在类定义的外部时,名字查找的第3步不仅要考虑在Screen类定义之前的全局作用域中的声明,而且要考虑在成员函数定义之前出现的全局作用域声明。例如:
class Screen {
public:
// ...
voidsetHeight ( index );
private:
indexheight;
};
Screen::index verify ( Screen:: index );
void Screen::setHeight( index var ) {
// var:refers to the parameter
//height: refers to the class member
//verify: refers to the global function
height =verify( var );
}
注意,全局函数verify的声明在Screen类定义之前是不可见的。然而,名字查找的第3步要考虑那些出现在成员定义之前的外围作用域声明,并找到全局函数verify的声明。