《C++Primer》第十二章-类-学习笔记(3)

《C++Primer》第十二章-类-学习笔记(3)

日志:
1,2020-03-03 笔者提交文章的初版V1.0

作者按:
最近在学习C++ primer,初步打算把所学的记录下来。

传送门/推广
《C++Primer》第二章-变量和基本类型-学习笔记(1)
《C++Primer》第三章-标准库类型-学习笔记(1)
《C++Primer》第八章-标准 IO 库-学习笔记(1)
上一篇
《C++Primer》第十二章-类-学习笔记(2)

友元

某些情况下,允许特定的非成员函数访问一个类的私有成员,同时仍然阻止一般的访问,这是很方便做到的。例如,被重载的操作符,如输入或输出操作符,经常需要访问类的私有数据成员。这些操作符不可能为类的成员,具体原因之后再讲述。然而,尽管不是类的成员,它们仍是类的“接口的组成部分”。
友元机制允许一个类将对其非公有成员的访问权授予指定的函数或类。友元的声明以关键字 friend开始。它只能出现在类定义的内部。友元声明可以出现在类中的任何地方:友元不是授予友元关系的那个类的成员,所以它们不受声明出现部分的访问控制影响。

通常,将友元声明成组地放在类定义的开始或结尾是个好主意。

友元关系的例子

想像一下,除了 Screen 类之外,还有一个窗口管理器,管理给定显示器上的一组 Screen。窗口管理类在逻辑上可能需要访问由其管理的 Screen 对象的内部数据。假定 Window_Mgr 是该窗口管理类的名字,Screen 应该允许Window_Mgr 像下面这样访问其成员:

class Screen {
// Window_Mgr members can access private parts of class Screen
friend class Window_Mgr;  //友元类
// ...rest of the 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;   //height与width是Screen的私有成员,缺少friend class Window_Mgr友元声明时,这段代码将会出错
	s.width += c;
	return *this;
}

缺少友元声明时,这段代码将会出错:将不允许使用形参 s 的 height 和width 成员。因为 Screen 将友元关系授予 Window_Mgr,所以,Window_Mgr 中的函数都可以访问 Screen 的所有成员。
友元可以是普通的非成员函数,或前面定义的其他类的成员函数,或整个类。
将一个类设为友元,友元类的所有成员函数都可以访问授予友元关系的那个类的非公有成员。

使其他类的成员函数成为友元

如果不是将整个 Window_Mgr 类设为友元,Screen 就可以指定只允许relocate 成员访问:

扫描二维码关注公众号,回复: 9908738 查看本文章
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 的成员。
更一般地讲,必须先定义包含成员函数的类,才能将成员函数设为友元。另一方面,不必预先声明类和非成员函数来将它们设为友元。
友元声明将已命名的类或非成员函数引入到外围作用域中。此外,友元函数可以在类的内部定义,该函数的作用域扩展到包围该类定义的作用域。
用友元引入的类名和函数(定义或声明),可以像预先声明的一样使用:

//不必预先声明`类和非成员函数`来将它们设为友元
//友元引入的类名和函数(定义或声明),可以像预先声明的一样使用
class X {
friend class Y; //类
friend void f() { /* ok to define friend function in the classbody */ } //非成员函数
};
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 没有特殊访问权。

static 类成员

对于特定类类型的全体对象而言,访问一个全局对象有时是必要的。也许,在程序的任意点需要统计已创建的特定类类型对象的数量;或者,全局对象可能是指向类的错误处理例程的一个指针;或者,它是指向类类型对象的内在自由存储区的一个指针。
然而,全局对象会破坏封装:对象需要支持特定类抽象的实现。如果对象是全局的,一般的用户代码就可以修改这个值。
因此我们采用定义类静态成员:类可以定义类静态成员,而不是定义一个可普遍访问的全局对象。
通常,非 static 数据成员存在于类类型的每个对象中。不像普通的数据成员,static 数据成员独立于该类的任意对象而存在;每个 static 数据成员是与类关联的对象,并不与该类的对象相关联
正如类可以定义共享的 static 数据成员一样,类也可以定义 static 成员函数static 成员函数没有 this 形参,它可以直接访问所属类的 static 成员,但不能直接使用非 static 成员

使用类的 static 成员的优点

使用 static 成员而不是全局对象有三个优点。

  1. static 成员的名字是在类的作用域中,因此可以避免与其他类的成员或全局对象名字冲突。
  2. 可以实施封装。static 成员可以是私有成员,而全局对象不可以。
  3. 通过阅读程序容易看出 static 成员是与特定类关联的。这种可见性可清晰地显示程序员的意图。

定义 static 成员

在成员声明前加上关键字 static 将成员设为staticstatic 成员遵循正常的公有/私有访问规则。
例如,考虑一个简单的表示银行账户的类。每个账户具有余额和拥有者,并且按月获得利息,但应用于每个账户的利率总是相同的。可以按下面的这样编写这个类

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 成员

像使用其他成员一样,类成员函数可以不用作用域操作符来引用类的static 成员:

class Account {
public:
// interface functions here
void applyint() { amount += amount * interestRate; }
};

static 成员函数

Account 类有两个名为 rate 的 static 成员函数,其中一个定义在类的内部。当我们在类的外部定义 static 成员时,无须重复指定 static 保留字,该保留字只出现在类定义体内部的声明处

void Account::rate(double newRate)
{
interestRate = newRate;
}

static 函数没有 this 指针

static 成员是类的组成部分但不是任何对象的组成部分,因此,static 成员函数没有 this 指针。通过使用非 static 成员显式或隐式地引用 this 是一个编译时错误。
因为 static 成员不是任何对象的组成部分,所以 static 成员函数不能被声明为 const。毕竟,将成员函数声明为 const 就是承诺不会修改该函数所属的对象。最后,static 成员函数也不能被声明为虚函数

static 数据成员

static 数据成员可以声明为任意类型,可以是常量、引用、数组、类类型,等等。
static 数据成员必须在类定义体的外部定义(正好一次)。不像普通数据成员,static 成员不是通过类构造函数进行初始化,而是应该在定义时进行初始化
保证对象正好定义一次的最好办法,就是将 static 数据成员的定义放在包含类非内联成员函数定义的文件中。
定义 static 数据成员的方式与定义其他类成员和变量的方式相同:先指定类型名,接着是成员的完全限定名。
可以定义如下 interestRate:

// define and initialize static class member
double Account::interestRate = initRate();

这个语句定义名为 interestRate 的 static 对象,它是类 Account 的成员,为 double 型。像其他成员定义一样,一旦成员名出现,static 成员的就是在类作用域中。因此,我们可以没有限定地直接使用名为 initRate 的 static成员函数,作为 interestRate 初始化式。注意,尽管 initRate 是私有的,我们仍然可以使用该函数来初始化 interestRate。像任意的其他成员定义一样,interestRate 的定义是在类的作用域中,因此可以访问该类的私有成员。
像使用任意的类成员一样,在类定义体外部引用类的 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 的维。
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 成员的访问权授予其他类或函数,并通过将其他的类或函数设为友元来授予其访问权。
类也可以定义 mutablestatic 成员mutable 成员永远都不能为const;它的值可以在 const 成员函数中修改。static 成员可以是函数或数据独立于类类型的对象而存在。

参考资料

【1】C++ Primer 中文版(第四版·特别版)

注解

本文许可证

本文遵循 CC BY-NC-SA 4.0(署名 - 非商业性使用 - 相同方式共享) 协议,转载请注明出处,不得用于商业目的。
CC BY-NC-SA 4.0

发布了52 篇原创文章 · 获赞 72 · 访问量 4万+

猜你喜欢

转载自blog.csdn.net/engineerxin/article/details/104640699