c++primer第七章(类)【7.3】

7.3、类的其它特性

包括:类型成员、类的成员的类内初始值,可变数据成员、内敛成员函数、从成员函数返回*this、关于如何定义并且使用类类型以及友元类的更多知识。

7.3.1类成员再探

为了展示这些新的特性,我们需要定义一对相互关联的类,分别是Screen和Window_mgr。

定义一个类型成员

Screen表示显示器中的一个窗口,每个Screen包含一个用于保存Screen内容的string成员和三个string::size_type类型成员。分别表示光标的位置和屏幕的高和宽。

除了定义数据的函数成员之外,类还可以自定义某种类型在函数内的别名。这种别名也有访问限制,public or private。

class Screen{
public:
    typedef string::size_type pos;
private:
    pos curspr = 0;
    pos hegiht =0,width = 0;
    string contents;
};

两个需要注意的地方:

  1. Screen的public部分定义了pos,这样用户就可以使用这个名字。
  2. Screen的用户不知道Screen使用一个string对象来存放它的数据,因此通过把pos定义成public成员可以隐藏Screen实现细节。

关于pos的声明有两点需要注意:

  1. 首先我们使用typedef,也可以等价地使用类型别名。  //使用类型别名  using pos = string::size_type;
  2. 用来定义类型的成员必须先定义后使用,这一点与普通成员不同。(P254解释)。因此,类型成员通常出现在类开始的地方。

Screen类的成员函数

为了使类更加使用,还要添加一个构造函数令用户定义屏幕的尺寸和内容,以及其它两个成员,负责移动光标,读取该位置的字符。

class Screen {
public:
	typedef string::size_type pos;
	Screen() = default; //如果我们再提供构造函数之后仍然需要默认构造函数,就要显示的声明出来。
	Screen(pos ht, pos wd, char c) :hegiht(ht), width(wd), contents(ht*wd, c){} //cursor类内默认初始化了
	char get() const { return contents[cursor]; } //隐式内敛函数
	inline char get(pos ht, pos wd) const;//显示内敛函数
	Screen &move(pos r, pos c); //在之后被设置为内敛
private:
	pos cursor = 0;
	pos hegiht = 0, width = 0;
	string contents;
};

令成员作为内敛函数

在类中,常有一些规模较小的函数适合被声明成内敛函数。定义在类内部的成员函数自动inline。

我们可以在类的内部把inline作为声明的一部分显示的声明成员函数,同样的,也能在类的外部用inline关键字修饰函数的定义。

inline Screen &move(pos r, pos c){
    pos row = r*width; //计算行的位置?
    cursor = row + c;//在行内将光标移动到指定的列
    return *this;//以左值的形式返回对象
}

虽然我们无需在声明和定义同时说明inline,但其实这么做是合法的,不过,最好只在类外部定义的地方说明inline,这样可以使类更容易理解。

NOTE:和我们在头文件中定义inline函数的原因一样(P214),inline成员函数也应该与相应的类定义在同一头文件中。

重载成员函数

	Screen myScreen;
	char ch = myScreen.get();
	char cha = myScreen.get(3, 4);

可变数据成员

有时(但并不频繁)会发生这样一种情况,我们希望能修改类的某个数据成员。即使在一个const成员函数内,可以通过变量的声明中加入mutable关键字可以做到这一点。

一个可变数据成员(mutable data member )永远不会是const,即使它是一个const对象成员。因此一个const成员函数可以改变一个可变成员的值。举个例子,我们将给Screen添加一个名为access_ctr的可变成员,通过它我们可以追踪每个Screen的成员函数被调用了多少次:

class Screen{
public:
    void some_member() const;
private:
    mutable size_t access_ctr;//即使一个const对象内也能被修改  
};
void Screen::some_member() const
{
    ++access_str; //保存一个计数值,用来记录成员函数被调用的次数
}
  

尽管some_member()是一个const成员函数,它仍然能够改变access_ctr的值。access_ctr是一个可变成员,因此任何成员函数,包括const函数在内部都可以修改它的值。

TIPS:const函数之中无法修改变量的值。

类数据成员的初始值

在定义好Screen类之后,我们将继续定义一个窗口管理类Window_mgr 并用它表示显示器的一组Screen。这个类将包含一个Screen类型的vector,每个元素表示一个特定的Screen。默认情况下,我们希望Window_mgr类开始时总是拥有一个默认初始化Screen。在C++11中,最好的方式就是这个默认值声明成一个类内初始值。

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

当我们初始化类类型的成员时,需要为构造函数传递一个符合成员类型的实参。如我们之前所知,类内初始值必须使用=初始化形式或者使用花括号括起来的直接初始化形式。

NOTE:当我们提供一个类内初始值时,必须使用=或者花括号表示。

7.3.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 ch) {
	contents[r * width + col] = ch;
	return *this;
}

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

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

如果我们令move和set返回Screen而非Screen&,则上述语句的行为将大为不同。等价于:

//如果move返回Screen而不是Screen&
Screen temp = myScreen.move(4,0);
temp.set('#');

假如我们定义返回类型不引用,则move的返回值将是*this的副本(P201),因此调用set只能改变临时副本,不能改变myScreen的值。

从const成员函数返回*this(~~~~~重要~~~~)

接下来,我们将继续添加一个名为display的操作,负责打印Screen的内容。我们希望这个函数能和move以及set出现在同一序列中,因此类似于move和set,display函数也应该返回执行它的对象的引用。

从逻辑上将,显示一个Screen并不需要改变它的内容,因此我们令display为一个const成员,此时this将是一个指向const的指针而*this是const对象。display的返回类型应该是const Sales_data&。然而如果令display返回一个const的引用,则我们将不能把display嵌入到一组动作的序列中去

Screen myScreen;
//如果display返回常量引用,则调用set将引发错误
myScreen.display(cout).set('*');

即使myScreen是个非常量对象,对set的调用也无法通过编译。问题在于display的const版本返回的是常量引用,而我们显然无法set一个常量对象。

NOTE:一个const成员函数如果以引用的形式返回*this,那么它的返回类型将是一个常量引用。

基于const的重载

通过区分成员函数是否是const的,我们可以对其进行重载,因为非常量版本的函数对于常量对象是不可用的,所以我们只能在一个常量对象上调用const成员函数;另一方便,虽然在非常量对象调用常量版本或非常量版本,但显然是非常量版本是一个更好的匹配。

根据指针参数是否指向const(P208)而重载函数。

在下面的例子中,我们将定义一个名为do_display的私有成员,有它否则打印Screen的实际工作。所有display操作都将调用这个函数,然后返回执行操作的对象:

class Screen {
public:
	//根据对象是否是const重载了display函数
	Screen& display(ostream &os) {
		do_display(os);
		return *this;
	}
	const Screen& display(ostream &os) const {
		do_display(os);
		return *this;
	}
private:
	void do_display(ostream &os) const {
		os << contents;
	}
};

当一个成员调用另一个成员时,this指针在其中隐式传递。因此当display调用do_display时候,它的this指针隐式地传递给do_display。而当display的非常量版本调用do_display,它将this指针隐式从非常量的指针转换成指向常量的指针

当do_display完成后,display函数各自返回解引用this所得的对象。在非常量版本中,this指向一个非常量的对象,在const成员返回一个常量引用。

当我们在某个对象上调用display时,该对象是否是const决定了应该调用display的哪个版本:

Screen myScreen(5,4);
const Screen blank(5,3);
myScreen.set('#').dispaly(cout);
blank.display(cout);

建议:对于公共代码使用私有功能函数

为什么我们要费力定义一个单独的do_display函数。毕竟对于do_display的调用并不比do_display函数内部所做的操作简单多少。为什么还要这么做呢?实际上我们处于一下原因:

  • 一个基本的愿望是避免多处使用同样的代码
  • 预期随着类的规模发展,display函数有可能变得更加复杂。此时,把相应的代码写在一处作用就比较明显。
  • 我们很可能在开发过程中给do_display函数添加某些调试信息,而这些信息将在代码的最终产品版本去掉。显然,只在do_display一处添加或删除这些信息要容易一些。
  • 这个额外的函数调用不会增加任何开销,因为我们在类内部定义do_display,所以它隐式地被声明成内敛函数。这样的话,调用do_display不会带来任何额外的运行开销。

7.3.3类类型

每个类定义了唯一的类型。

NOTE:即使两个类的成员列表完全一致,它们也是不同的类型。

类的声明:

我们也能仅仅声明类而暂时不定义它。

这种声明被称作前向声明(forward declaration),它向程序中引入名字Screen并且指明Screen是一种类类型。对于类Screen来说,在它声明之后定义之前是一个不完全类型,此时我们知道Screen是一个类类型,但是不清楚它到底包含哪些成员。

对于一个类来说,在我们创建它的对象之前该类必须被定义过,而不能仅仅被声明。否则编译器就不能了解这样的对象需要多少存储空间,类似的,类必须首先被定义,然后才能引用或者指针访问其成员。如果类没有定义,编译器就不清楚该类到底有多少成员。

在7.6节(P268)中我们将描述一种例外的情况:直到类被定义之后数据成员才能被声明成这种类类型。换句话说,我们必须首先完成类的定义,然后编译器才能知道存储该数据成员需要多少空间。因为只有当类全部完成后类才算被定义,所以一个类的成员类型不能是该类自己。然而,一旦一个类的名字出现过后,它就被认为是声明过了(但是尚未定义),因此类允许包含指向它自身类型的引用和指针:

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

总结类类型:我佛了这么多书上写的!大多数都是解释!我看实际操作有用的就一句“一个类中的成员类型不能是它自己,但是可以是指向它的引用或指针”。

7.3.4、友元再探

  1. 非成员函数定义成友元(p241)
  2. 类把其他类定义成友元
  3. 类把已经定义的类的成员函数定义成友元

类之间的友元关系

举个友元类的例子,Window_mgr类的某些成员可能需要访问它管理的Screen类的内部数据。如我们需要Window_mgr添加一个clear成员,把指定Screen的内容设为空白。为了完成这操作,clear需要访问Screen的私有成员;而这种访问如果合法,Screen需要把Window_mgr指定成它的友元。

class Screen{
    friend class Window_mgr;
    //Window_mgr的成员可以访问Screen类的私有成分
};

如果一个类指定了友元类,则友元类的成员函数可以访问此类包括非公有成员在内的所有成员。通过上面的声明,Window_mgr被指定为Screen的友元,因此我们可以将Window_mgr的clear成员写成如下的形式:

class Window_mgr{
public:
    using ScreenIndex = vector<Screen>::size_type;
    void clear(ScreenIndex );//按照编号将指定的Screen重置为空白
private:
    vector<Screen> screens{Screen(24,80,' ')};
};

void Window_mgr::clear(ScreenIndex i){
    Screen& s = screen[i];
    s.contents = string(s.height * s.width ,' ');
};

如果clear不是Screen的友元,上面的代码将无法通过编译。clear()不能访问Screen的私有成员:height、width和contents。

而当Screen声明Window_mgr为其友元之后,则Screen所有成员对Wind_mgr就都变成可见的了。

NOTE:每个类负责控制字的友元类或友元函数,友元关系不具备传递性。

令成员函数作为友元

除了上述的把整个Window_mgr声明成友元类之外,还可以专门的给clear()函数提供访问权限,只需要把这个成员函数声明成友元,并且我们必须明确指出成员函数属于哪个类!

class Screen{
    //Window_mgr::clear必须在Screen类之前被声明!!!!
    friend void Window_mgr::clear(ScreenIndex);
};

要想令某个成员函数作为友元:

  1. 定义Window_mgr类,只能声明clear()函数,但是不可以定义它。
  2. 定义Screen类,对clear()进行友元声明。
  3. 定义clear(),此时它才可以访问Screen成员。

函数重载和友元

如果一个类想把一组重载函数声明成它的友元,它需要对这组函数的每一个分别声明:

//extern关键字表示变量或函数定义在别的文件之中
extern ostream& storeOn(ostream&, Screen&);
extern BitMap& storeOn(BitMap&, Screen&);
class Screen {
	friend ostream& storeOn(ostream&, Screen&);//只把ostream& storeOn声明成了友元。
};

友元声明和作用域

就算在类的内部定义该函数,我们也必须在类的外部提供相应的声明从而使得函数可见,即使是用成员函数调用该友元函数,它也必须声明过。

struct X {
	friend void f(){}
	X() { f(); } //错误 f还没有被声明
	void g();
	void h();
};
void X::g() { return f(); }
void f();
void X::h() { return f(); }

总结:额。。书上说的我没太理解,感觉不就是友元函数的声明和该函数的声明是两种,必须都要有,如果要调用它必须对函数进行声明,即使它在类内定义过了。[就算在类的内部定义该函数,我们也必须在类的外部提供相应的声明从而使得函数可见]

猜你喜欢

转载自blog.csdn.net/qq_34269988/article/details/86507814