12.5. 友元
在某些情况下,允许特定的非成员函数访问一个类的私有成员,同时仍然阻止一般的访问,这是很方便做到的。例如,被重载的操作符,如输入或输出操作符,经常需要访问类的私有数据成员。这些操作符不可能为类的成员,具体原因参见第十四章。然而,尽管不是类的成员,它们仍是类的“接口的组成部分”。
友元机制允许一个类将对其非公有成员的访问权授予指定的函数或类。友元的声明以关键字 friend 开始。它只能出现在类定义的内部。友元声明可以出现在类中的任何地方:友元不是授予友元关系的那个类的成员,所以它们不受声明出现部分的访问控制影响。<Tips>:通常,将友元声明成组地放在类定义的开始或结尾是个好主意。
友元关系:一个例子
想像一下,除了 Screen 类之外,还有一个窗口管理器,管理给定显示器上的一组 Screen。窗口管理类在逻辑上可能需要访问由其管理的 Screen 对象的内部数据。假定 Window_Mgr 是该窗口管理类的名字,Screen 应该允许 Window_Mgr 像下面这样访问其成员:
class Screen {
// Window_Mgr members can access private parts of class Screen
friend class Window_Mgr;
// ...restofthe Screen class
};
Window_Mgr 的成员可以直接引用 Screen 的私有成员。例如,Window_Mgr 可以有一个函数来重定位一个 Screen:
Window_Mgr&
Window_Mgr::relocate(Screen::index r, Screen::index c,
Screen& s)
{
// ok to refer to height and width
s.height += r;
s.width += c;
return *this;
}
缺少友元声明时,这段代码将会出错:将不允许使用形参 s 的 height 和 width 成员。因为 Screen 将友元关系授予 Window_Mgr,所以,Window_Mgr 中的函数都可以访问 Screen 的所有成员。
友元可以是普通的非成员函数,或前面定义的其他类的成员函数,或整个类。将一个类设为友元,友元类的所有成员函数都可以访问授予友元关系的那个类的非公有成员。
使其他类的成员函数成为友元
如果不是将整个 Window_Mgr 类设为友元,Screen 就可以指定只允许 relocate 成员访问:
class Screen {
// Window_Mgrmust be defined before class Screen
friend Window_Mgr&
Window_Mgr::relocate(Window_Mgr::index,
Window_Mgr::index,
Screen&);
// ...restofthe Screen class
};
当我们将成员函数声明为友元时,函数名必须用该函数所属的类名字加以限定。
友元声明与作用域
为了正确地构造类,需要注意友元声明与友元定义之间的互相依赖。在前面的例子中,类 Window_Mgr 必须先定义。
否则,Screen 类就不能将一个 Window_Mgr 函数指定为友元。然而,只有在定义类 Screen 之后,才能定义 relocate 函数——毕竟,它被设为友元是为了访问类Screen 的成员。
更一般地讲,必须先定义包含成员函数的类,才能将成员函数设为友元。另一方面,不必预先声明类和非成员函数来将它们设为友元。
<Note>:友元声明将已命名的类或非成员函数引入到外围作用域中。此外,友元函数可以在类的内部定义,该函数的作用域扩展到包围该类定义的作用域。
用友元引入的类名和函数(定义或声明),可以像预先声明的一样使用:
class X { friend class Y; friend void f() { /* ok to define friend function in the class body */ } }; class Z { Y *ymem; // ok: declaration for class Y introduced by friend in X void g() { return ::f(); } // ok: declaration of f introduced by X };重载函数与友元关系
类必须将重载函数集中每一个希望设为友元的函数都声明为友元:
// overloaded storeOn functions extern std::ostream& storeOn(std::ostream &, Screen &); extern BitMap& storeOn(BitMap &, Screen &); class Screen { // ostream version of storeOn may access private parts of Screen objects friend std::ostream& storeOn(std::ostream &, Screen &); // ... };类 Screen 将接受一个 ostream& 的 storeOn 版本设为自己的友元。接受一个 BitMap& 的版本对 Screen 没有特殊访问权。
12.6. static 类成员
对于特定类类型的全体对象而言,访问一个全局对象有时是必要的。也许,在程序的任意点需要统计已创建的特定类类型对象的数量;或者,全局对象可能是指向类的错误处理例程的一个指针;或者,它是指向类类型对象的内在自由存储区的一个指针。
然而,全局对象会破坏封装:对象需要支持特定类抽象的实现。如果对象是全局的,一般的用户代码就可以修改这个值。类可以定义类 静态成员,而不是定义一个可普遍访问的全局对象。
通常,非 static 数据成员存在于类类型的每个对象中。
不像普通的数据成员,static 数据成员独立于该类的任意对象而存在;每个 static 数据成员是与类关联的对象,并不与该类的对象相关联。
正如类可以定义共享的 static 数据成员一样,类也可以定义 static 成员函数。static 成员函数没有 this 形参,它可以直接访问所属类的 static 成员,但不能直接使用非 static 成员。
使用类的 static 成员的优点
使用 static 成员而不是全局对象有三个优点。
-
static 成员的名字是在类的作用域中,因此可以避免与其他类的成员或全局对象名字冲突。
-
可以实施封装。static 成员可以是私有成员,而全局对象不可以。
-
通过阅读程序容易看出 static 成员是与特定类关联的。这种可见性可清晰地显示程序员的意图。
定义 static 成员
在成员声明前加上关键字 static 将成员设为 static . static 成员遵循正常的公有/私有访问规则。
例如,考虑一个简单的表示银行账户的类。每个账户具有余额和拥有者,并且按月获得利息,但应用于每个账户的利率总是相同的。可以按下面的这样编写这个类
class Account { public: // interface functions here void applyint() { amount += amount * interestRate; } static double rate() { return interestRate; } static void rate(double); // sets a new rate private: std::string owner; double amount; static double interestRate; static double initRate(); };
这个类的每个对象具有两个数据成员:owner 和 amount。对象没有与 static 数据成员对应的数据成员,但是,存在一个单独的 interestRate 对象,由Account 类型的全体对象共享。
使用类的 static 成员
可以通过作用域操作符从类直接调用 static 成员,或者通过对象、引用或指向该类类型对象的指针间接调用。
Account ac1; Account *ac2 = &ac1; // equivalent ways to call the static member rate function double rate; rate = ac1.rate(); // through an Account object or reference rate = ac2->rate(); // through a pointer to an Account object rate = Account::rate(); // directly from the class using the scope operator
像使用其他成员一样,类成员函数可以不用作用域操作符来引用类的 static 成员:
class Account { public: // interface functions here void applyint() { amount += amount * interestRate; } };
12.6.1. static 成员函数
Account 类有两个名为 rate 的 static 成员函数,其中一个定义在类的内部。当我们在类的外部定义 static 成员时,无须重复指定 static 保留字,该保留字只出现在类定义体内部的声明处:
void Account::rate(double newRate) { interestRate = newRate; }
static 函数没有 this 指针
static 成员是类的组成部分但不是任何对象的组成部分,因此,static 成员函数没有 this 指针。通过使用非 static 成员显式或隐式地引用 this 是一个编译时错误。
12.6.2. static 数据成员
static 数据成员可以声明为任意类型,可以是常量、引用、数组、类类型,等等。
static 数据成员必须在类定义体的外部定义(正好一次)。不像普通数据成员,static 成员不是通过类构造函数进行初始化,而是应该在定义时进行初始化。
<Tips>:保证对象正好定义一次的最好办法,就是将 static 数据成员的定义放在包含类非内联成员函数定义的文件中。
定义 static 数据成员的方式与定义其他类成员和变量的方式相同:先指定类型名,接着是成员的完全限定名。
可以定义如下 interestRate:
// define and initialize static class member double Account::interestRate = initRate();
这个语句定义名为 interestRate 的 static 对象,它是类 Account 的成员,为 double 型。像其他成员定义一样,一旦成员名出现,static 成员的就是在类作用域中。因此,我们可以没有限定地直接使用名为 initRate 的 static 成员函数,作为 interestRate 初始化式。注意,尽管 initRate 是私有的,我们仍然可以使用该函数来初始化 interestRate。像任意的其他成员定义一样,interestRate 的定义是在类的作用域中,因此可以访问该类的私有成员。
<Note>:
像使用任意的类成员一样,在类定义体外部引用类的 static 成员时,必须指定成员是在哪个类中定义的。然而,static 关键字只能用于类定义体内部的声明中,定义不能标示为 static。
特殊的整型 const static 成员
一般而言,类的 static 成员,像普通数据成员一样,不能在类的定义体中初始化。相反,static 数据成员通常在定义时才初始化。
这个规则的一个例外是,只要初始化式是一个常量表达式,整型 const static 数据成员就可以在类的定义体中进行初始化:
class Account { public: static double rate() { return interestRate; } static void rate(double); // sets a new rate private: static const int period = 30; // interest posted every 30 days double daily_tbl[period]; // ok: period is constant expression };
用常量值初始化的整型 const static 数据成员是一个常量表达式。同样地,它可以用在任何需要常量表达式的地方,例如指定数组成员 daily_tbl 的维。
<Note>:const static 数据成员在类的定义体中初始化时,该数据成员仍必须在类的定义体之外进行定义。
在类内部提供初始化式时,成员的定义不必再指定初始值:
// definition of static member with no initializer; // the initial value is specified inside the class definition const int Account::period;
static 成员不是类对象的组成部分普通成员都是给定类的每个对象的组成部分。static 成员独立于任何对象而存在,不是类类型对象的组成部分。因为 static 数据成员不是任何对象的组成部分,所以它们的使用方式对于非 static 数据成员而言是不合法的。
例如,static 数据成员的类型可以是该成员所属的类类型。非 static 成员被限定声明为其自身类对象的指针或引用:
class Bar { public: // ... private: static Bar mem1; // ok Bar *mem2; // ok Bar mem3; // error };
类似地,static 数据成员可用作默认实参:
class Screen { public: // bkground refers to the static member // declared later in the class definition Screen& clear(char = bkground); private: static const char bkground = '#'; };
非 static 数据成员不能用作默认实参,因为它的值不能独立于所属的对象而使用。使用非 static 数据成员作默认实参,将无法提供对象以获取该成员的值,因而是错误的。
本章小结
--类是 C++ 中最基本的特征,允许定义新的类型以适应应用程序的需要,同时使程序更短且更易于修改。
--数据抽象是指定义数据和函数成员的能力,而封装是指从常规访问中保护类成员的能力,它们都是类的基础。成员函数定义类的接口。通过将类的实现所用到的数据和函数设置为 private 来封装类。--类可以定义构造函数,它们是特殊的成员函数,控制如何初始化类的对象。可以重载构造函数。每个构造函数就初始化每个数据成员。初始化列表包含的是名—值对,其中的名是一个成员,而值则是该成员的初始值。
--类可以将对其非 public 成员的访问权授予其他类或函数,并通过将其他的类或函数设为友元来授予其访问权。
--类也可以定义 mutable 或 static 成员。mutable 成员永远都不能为 const;它的值可以在 const 成员函数中修改。static 成员可以是函数或数据,独立于类类型的对象而存在。
术语
abstract data type(抽象数据类型)-
使用封装来隐藏其实现的数据结构,允许使用类型的程序员抽象地考虑该类型做什么,而不是具体地考虑类型如何表示。C++ 中的类可用来定义抽象数据类型。
public 或 private 标号,指定后面的成员可以被类的使用者访问或者只能被类的友元和成员访问。每个标号为在该标号到下一个标号之间声明的成员设置访问保护。标号可以在类中出现多次。
是 C++ 中定义抽象数据类型的一种机制,可以有数据、函数或类型成员。一个类定义了新的类型和新的作用域。
-
class declaration(类声明)
-
类可以在定义之前声明。类声明用关键字 class(或 struct)表示,后面加类名字和一个分号。已声明但没有定义的类是一个不完全的类型。
-
class keyword(class 关键字)
-
用在 class 关键字定义的类中,初始的隐式访问标号是 private。
-
class scope(类作用域)
-
每个类定义一个作用域。类作用域比其他作用域复杂得多——在类的定义体内定义的成员函数可以使用出现在该定义之后的名字。
-
concrete class(具体类)
-
暴露其实现细节的类。
-
const member function(常量成员函数)
-
一种成员函数,不能改变对象的普通(即,既不是 static 也不是 mutable)数据成员。const 成员中的 this 指针指向 const 对象。成员函数是否可以被重载取决于该函数是否为 const。
-
constructor initializer list(构造函数初始化列表)
-
指定类的数据成员的初始值。在构造函数体现执行前,用初始化列表中指定的值初始化成员。没有在初始化列表中初始化的类成员,使用它们的默认构造函数隐式初始化。
-
conversion constructor(转换构造函数)
-
可用单个实参调用的非 explicit 构造函数。隐式使用转换构造函数将实参的类型转换为类类型。
-
data abstraction(数据抽象)
-
注重类型接口的编程技术。数据抽象允许程序员忽略类型如何表示的细节,而只考虑该类型可以执行的操作。数据抽象是面向对象编程和泛型编程的基础。
-
default constructor(默认构造函数)
-
没有指定初始化时使用的构造函数。
-
encapsulation(封装)
-
实现与接口的分离。封闭隐藏了类型的实现细节。在 C++ 中,实施封装可以阻止普通用户访问类的 private 部分。
-
explicit constructor(显式构造函数)
-
可以用单个实参调用但不能用于执行隐式转换的构造函数。通过将关键字 explicit 放在构造函数的声明之前而将其设置为explicit。
-
forward declaration(前向声明)
-
对尚未定义的名字的声明。大多用于引用出现在类定义之前的类声明。参见不完全类型。
-
friend(友元)
-
Mechanism by which a class grants access to its nonpublic members. Both classes and functions may be named as friends.friends have the same access rights as members.
类授权访问其非 public 成员的机制。类和函数都可以被指定为友元。友元拥有与成员一样的访问权。
-
incomplete type(不完全类型)
-
已声明但未定义的类型。不能使用不完全类型来定义变量或类成员。定义指向不完全类型的引用或指针是合法的。
-
member function(成员函数)
-
类的函数成员。普通成员函数通过隐式的 this 指针绑定到类类型的对象。static 成员函数不与对象绑定且没有 this 指针。成员函数可以被重载,只要该函数的版本可由形参的数目或类型来区别。
-
mutable data member(可变数据成员)
-
一种永远也不能为 const 对象的数据成员,即使作为 const 对象的成员,也不能为 const 对象。mutable 成员可以在 const 函数中改变。
-
name lookup(名字查找)
-
将名字的使用与其相应的声明相匹配的过程。
-
private members(私有成员)
-
在 private 访问标号之后定义的成员,只能被友元和其他的类成员访问。类所使用的数据成员和实用函数在不作为类型接口的组成部分时,通常声明为 private。
-
public members(公用成员)
-
在 public 访问标号之后定义的成员,可被类的任意使用者访问。一般而言,只有定义类接口的函数应定义在 public 部分。
-
static member(静态成员)
-
不是任意对象的组成部分、但由给定类的全体对象所共享的数据或函数成员。
-
struct keyword(struct 关键字)
-
用在 struct 关键字定义的类中,初始的隐式访问标号为 public。
-
synthesized default constructor(合成的默认构造函数)
-
编译器为没有定义任何构造函数的类创建(合成)的构造函数。这个构造函数通过运行该类的默认构造函数来初始化类类型的成员,内置类型的成员不进行初始化。