类(三):类的其他特性

<基于 C++ Primer P243>
包括:类型成员、类的成员的类内初始值、可变数据成员、内联成员函数、从成员函数返回 *this、如何定义并使用类类型及友元类。
1.类成员再探
(1)定义一个类型成员
除了定义数据和函数成员之外,类还可以自定义某种类型在类中的别名。由类定义的类型名字和其他成员一样存在访问限制。

class Screen
{
public:
	typedef std::string::size_type pos;
	// using pos = std::string::size_type;
private:
	pos cursor = 0;
	pos height = 0, width = 0;
	std::string contents;
};

通过把 pos 定义成 public 成员可以隐藏 Screen 实现的细节,用户不知道 Screen 使用了一个 string 对象来存放它的数据。
用来定义类型的成员必须先定义后使用,这一点与普通成员有所区别。因此,类型成员通常出现在类开始的地方。
(2)Screen 类的成员函数

class Screen
{
public:
	typedef std::string::size_type pos;
	// using pos = std::string::size_type;
	Screen() = default;
	Screen(pos ht, pos wd, char c) : height(ht), width(wd), contents(ht * wd, c) { }
	// 读取光标处的字符
	char get() cosnt {return contents[cursor];}
	// 读取给定位置的字符
	inline char get(pos ht, pos wd) const;
	// 移动光标
	Screen &move(pos r, pos c);
private:
	pos cursor = 0;
	pos height = 0, width = 0;
	std::string contents;
};

(3)令成员作为内联函数
定义在类内部的成员函数是自动 inline 的。
我们也可以在类的内部把 inline 作为声明的一部分显式地声明成员函数。同样,也能在类的外部用 inline 关键字修饰函数的定义。
我们无须但是可以在声明和定义的地方同时说明 inline。
最好只在类外部定义的地方说明 inline,这样可以使类更容易理解。
和我们在头文件中定义 inline 函数的原因一样,inline 成员函数也应该与相应的类定义在同一个头文件中。
(4)重载成员函数
和非成员函数一样,成员函数也可以被重载,只要函数之间在参数的数量/类型上有所区别即可。
(5)可变数据成员
有时我们希望能在即使一个 const 成员函数内修改类的某个数据成员。可以通过在变量的声明中加入 mutable 关键字做到这一点。
一个可变数据成员永远不会是 const,即使它是 const 对象的成员。因此,一个 const 成员函数也可以改变一个可变成员的值。

class Screen
{
public:
	// 追踪每个成员函数的点击率(click-through rate)
	void some_member() const;
private:
	mutable size_t access_ctr;
};
void Screen::some_member() const
{
	++access_ctr;
}

(6)类数据成员的初始值

class Window_mgr
{
private:
	// 默认情况下,一个 Window_mgr 包含一个标准尺寸的空白 Screen
	std::vector<Screen> screens{Screen(24, 80, ' ')};
};

我们使用了一个单独的元素对 vector 成员执行了列表初始化,这个 Screen 的值被传递给 vector 的构造函数,从而创造了一个单元素的 vector 对象。
注意,类内初始值必须使用 = 的初始化形式或者花括号括起来的直接初始化形式。
2.返回 *this 的成员函数

class Screen
{
public:
	Screen &set(char);
	Screen &set(pos, pos, char);
};
inline Screen &Screen::set(char c)
{
	contents[cursor] = c;
	return *this;
}
inline Screen &Screen::set(pos r, pos col, char c)
{
	contents[r*width + col] = ch;
	return *this;
}

和之前出现的 move 操作一样,set 成员的返回值是调用 set 的对象的引用。返回引用的函数是左值的,意味着这些函数返回的是对象本身而非对象的副本。

myScreen.move(4,0).set('#');

等价于:

myScreen.move(4,0);
myScreen.set('#');

若 move 和 set 返回的是 Screen 而非 Screen&,则上述代码等价于:

Screen temp = myScreen.move(4,0);
temp.set('#'); // 不会改变 myScreen 的 contents

(1)从 const 成员函数返回 *this
一个 const 成员函数如果以引用的形式返回 *this,那么它的返回类型将是常量引用。
因此以下代码非法:

Screen myScreen;
// display 负责打印 Screen 的内容,令 display 为一个 const 成员,返回执行它的对象的引用
myScreen.display(cout).set('*');

(2)基于 const 的重载
通过区分成员函数是否是 const 的,我们可以对其进行重载。

class Screen
{
public:
	Screen &display(std::ostream &os)
		{do_display(os); return *this;}
	const Screen &display(std::ostream &os) const
		{do_display(os); return *this;}
private:
	void do_display(std::ostream &os) const {os << contents;}
};

当一个成员函数调用另外一个成员时,this 指针在其中隐式地传递。因此,当 display 调用do_display 时,它的 this 指针隐式地传递给 do_display。而当 display 的非常量版本调用do_display 时,它的 this 指针将隐式地从指向非常量的指针转换成指向常量的指针。
当 do_display 完成后,display 函数各自返回解引用 this 所得的对象。在非常量版本中,this 指向一个非常量对象,因此 display 返回一个普通的(非常量)引用;而 const 成员则返回一个常量引用。

Screen myScreen(5,3);
const Screen blank(5,3);
myScreen.set('#').display(cout); // 调用非常量版本
blank.display(cout);             // 调用常量版本

(3)对于公共代码使用私有功能函数
上述代码中 do_display 函数实现的操作十分简单,仍然被定义为了一个函数。原因如下:
①避免在多处使用同样的代码。
②随着类的规模发展,display 函数有可能变得更加复杂。
③我们很可能在开发过程中给 do_display 函数添加某些调试信息,而这些信息将在代码的最终产品版本中去掉。
④这个额外的函数不会增加任何开销。因为我们在类的内部定义类 do_display,所以它隐式地被声明成内联函数。
事实上,设计良好的 C++ 代码常常包含大量类似于 do_display 的小函数,通过调用这些函数,可以完成一组其他函数的“实际”工作。
3.类类型
即使两个类的成员列表完全一致,它们也是不同的类型。对于一个类来说,它的成员和其他任何类(或者任何其他作用域)的成员都不是一回事儿。
我们可以把类名作为类型的名字使用,从而直接指向类类型。也可以把类名放在 class 或 struct 之后。

Sales_data item1;
class Sales_data item1; // 一条等价的声明

(1)类的声明
我们可以像把函数的声明和定义分离开来一样,将类的声明和定义分开。

class Screen;

这种声明又成为前向声明,已声明而未定义的类是一个不完全类型。此时我们已知 Screen 是一个类类型,但是不清楚它到底包含哪些成员。
不完全类使用情景有限,因为编译器无法了解这样的对象需要多少存储空间。我们可以定义指向这种类型的指针或引用,也可以声明(但是不能定义)以不完全类型作为参数或者返回类型的函数。
直到类被定义之后数据成员才能被声明成这种类类型。
但是一旦一个类的名字出现后,它就被认为是声明过了,因此类允许包含指向它自身类型的引用或指针。

class Link_screen
{
	Screen window;
	Link_screen *next;
	Link_screen *prev;

4.友元再探
类除了可以把普通的非成员函数定义成友元外,还可以把其他的类定义成友元,也可以把其他类(之前自己定义过的)的成员函数定义成友元。此外,友元函数能定义在类的内部,这样的函数是隐式内联的。
(1)类之间的友元关系

class Screen
{
	friend class Window_mgr;
};

class Window_mgr
{
public:
	using ScreenIndex = std::vector<Screen>::size_type;
	// 将选定位置的 Screen 重置为空白
	void clear(ScreenIndex);
private:
	std::vector<Screen> screens{Screen(24, 80, '')};
};
void Window_mgr::clear(ScreenIndex i)
{
	Screen &s = screens[i];
	s.contents = string(s.height * s.width, ' ');
}

注意,友元关系不存在传递性。A 是 B 的友元,不意味着 B 是 A 的友元。每个类负责控制自己的友元类或友元函数。
(2)令成员函数作为友元
当把一个成员函数声明成友元时,我们必须明确指出该成员函数属于哪个类。

class Screen
{
	friend void Window_mgr::clear(ScreenIndex);
};

想要令某个成员函数作为友元,我们必须仔细组织程序的结构以满足声明和定义的彼此依赖关系。在这个例子中,我们必须按照如下方式设计程序:
①首先定义 Window_mgr 类,其中声明 clear 函数,但是不能定义它。
②其次定义 Screen,包括对于 clear 的友元声明。
③最后定义 clear。
(3)函数重载和友元
尽管重载函数的名字相同,但它们仍然是不同的函数。因此,如果一个类想把一组重载函数声明成它的友元,它需要对这组函数中的每一个分别声明。
(4)友元声明和作用域
将要被当作友元的类和非成员函数的声明不是必须在它们的友元声明之前。当一个名字第一次出现在一个友元声明中时,我们隐式地假定该名字在当前作用域中是可见的。然而,友元本身不一定真的声明在当前作用域中。
即使是在类内定义该函数,我们也必须在类的外部提供相应的声明从而使得函数可见。也就是说,即使我们仅仅是用声明友元的类的成员调用该友元函数,它也必须是被声明过的。
<上面讲的一大堆,意思就是友元声明不能当作函数声明>

struct X
{
	friend void f() { /*友元函数的定义*/ }
	X() {f();}            // 错误
	void g();
	void h();
};
void X::g() {return f();} // 错误
void f();                 
void X::h() {return f();} // 正确
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值