C++Primer 学习(类 三)类的其他特性

这一小节的内容有点杂而多,简单做个记录吧 。
类成员再探
为了展示这些新的特性,首先定义一对相互关联的类,它们分别是Screen和Window_mgro 。
定义一个类型成员Screen表示显示器中的一个窗口 。每个Screen包含一个用于保存Screen内容的string成员和三个string::size_type类型的成员,它们分别表示光标的位置以及屏幕的高和宽,除了定义数据和函数成员之外,类还可以自定义某种类型在类中的别名 。由类定义的类型名字和其他成员一样存在访问限制,可以是public或者private中的一种:
class Screen{public:typedef std::string::size_type pos;private:pos cursor =0;pos height = 0, width 0;std: :string contents;}; 关于pos的声明有两点需要注意 。首先,我们使用了typedef,也可以等价地使用类型别名:
class Screen{public://使用类型别名等价地声明一个类型名字using pos = std::string::size_type;//其他成员与之前的版本一致} 其次,用来定义类型的成员必须先定义后使用,这一点与普通成员有所区别 。
因此,,型成员通常出现在类开始的地方 。
Screen类的成员函数
要使我们的类更加实用,还需要添加一个构造函数令用户能够定义屏幕的尺寸和内容,以及其他两个成员,分别负责移动光标和读取给定位置的字符:
class Screen{public:typedef std::string::size_type pos;Screen() = default; //因为Screen有另一个构造函数,所以本函数是必需的// cursor被其类内初始值初始化为0Screen (pos ht, pos wd, char c): height (ht), width (wd),contents (ht * wd, c){}//读取光标处的字符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 height = 0, width =0;std::string contents;}; 因为我们已经提供了一个构造函数,所以编译器将不会自动生成默认的构造函数 。如果我们的类需要默认构造函数,必须显式地把它声明出来 。我们使用**=default**告诉编译器为我们合成默认的构造函数 。
令成员作为内联函数
在类中,常有一些规模较小的函数适合于被声明成内联函数 。如我们之前所见的,定义在类内部的成员函数是自动inline的 。我们可以在类的内部把inline作为声明的一部分显式地声明成员函数,同样的,也能在类的外部用inline关键字修饰函数的定义:
//可以在函数的定义处指定inlineinline Screen &Screen: :move (pos r, pos c){pos row = r * width;//计算行的位置cursor = row + C;//在行内将光标移动到指定的列return *this;//以左值的形式返回对象}// 在类的内部声明成 inlinechar Screen: :get (pos r, pos c) const{pos row = r * width;//计算行的位置return contents [row + C] ; //返回给定列的字符} 可变数据成员
有时(但并不频繁)会发生这样一种情况,我们希望能修改类的某个数据成员,即使是在一个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_ctr; //保存一个计数值,用于记录成员函数被调用的次数//该成员需要完成的其他工作} 尽管some member是一个const成员函数,它仍然能够改变access ctr的值 。该成员是个可变成员,因此任何成员函数,包括const函数在内都能改变它的值 。
返回*this的成员函数
我们先看一个函数:
inline Screen &Screen::set (char ch){contents [cursor] = ch;//设置给定位置的新值return *this;//将this对象作为左值返回} 和move操作一样,我们的set成员的返回值是调用set的对象的引用 。返回引用的函数是左值的,意味着这些函数返回的是对象本身而非对象的副本 。如果我们把一系列这样的操作连接在一条表达式中的话:
//把光标移动到一个指定的位置,然后设置该位置的字符值myScreen.move(4, 0).set('#'); 这些操作将在同一个对象上执行 。在上面的表达式中,我们首先移动myScreen内的光标,然后设置myScreen的contents成员 。也就是说,上述语句等价于
myScreen.move(4, 0);myScreen.set('#'); 如果我们令move 和set返回Screen而非Screen&,则上述语句的行为将大不相同 。在此例中等价于:
//如果move 返回Screen 而非Screen&Screen temp = myScreen.move(4,0); // 对返回值进行拷贝temp.set ('#'); //不会改变myScreen的contents 假如当初我们定义的返回类型不是引用,则move的返回值将是*this的副本,因此调用set只能改变临时副本,而不能改变myScreen的值 。
从const成员函数返回*this
接下来,我们继续添加一个名为diplay的操作,它负责打印Screen的内容 。我们希望这个函数能和move以及set出现在同一序列中,因此类似于move和set,diplay函数也应该返回执行它的对象的引用 。从逻辑上来说,显示一个Screen并不需要改变它的内容,因此我们令diplay为个const成员,此时,this将是一个指向const的指针而*this是const对象 。由此推断,display的返回类型应该是const Sales_data& 。然而,如果真的令diplay返回一个const的引用,则我们将不能把display嵌入到一组动作的序列中去:
Screen myScreen;//如果display返回常量引用,则调用set将引发错误myScreen.display(cout).set('*'); 即使myScreen是个非常量对象,对set的调用也无法通过编译 。问题在于display的const版本返回的是常量引用,而我们显然无权set一个常量对象 。
一个const成员函数如果以引用的形式返回*this,那么它的返回类型将是常量引用 。
基于const的重载
通过区分成员函数是否是const的,我们可以对其进行重载,其原因与我们之前根据指针参数是否指向const而重载函数的原因差不多 。具体说来,因为非常量版本的函数对于常量对象是不可用的,所以我们只能在一个常量对象上调用const成员函数 。另一方面,虽然可以在非常量对象上调用常量版本或非常量版本,但显然此时非常量版本是一个更好的匹配 。在下面的这个例子中,我们将定义一个名为do display的私有成员,由它负责打印Screen的实际工作 。所有的display操作都将调用这个函数,然后返回执行操作的对象:
class Screen{public://根据对象是否是const重载了display函数Screen &display (std::ostream &os){ do_display (os); return *this; }const Screen &display (std: :ostream &os) const{do_display (os); return *this; }private://该函数负责显示Screen的内容void do_display (std::ostream &os) const {os << contents;}//其他成员与之前的版本一致} 当do display完成后,display函数各自返回解引用this所得的对象 。在非常量版本中,this指向一个非常量对象,因此display返回一个普通的(非常量)引用;而const成员则返回一个常量引用 。当我们在某个对象上调用display时,该对象是否是const决定了应该调用display的哪个版本:
Screen myScreen (5, 3) ;const Screen blank (5, 3) ;myScreen.set ('#').display(cout);//调用非常量版本blank.display (cout);//调用常量版本 建议:
对于公共代码使用私有功能函数
为什么要费力定义一个单独的dodisplay函数呢?作者给出是出于以下原因的:

  1. 避免在多处使用同样的代码 。
  2. 我们预期随着类的规模发展, display函数有可能变得更加复杂,此时,把相应的操作写在一处而非两处的作用就比较明显了 。
  3. 我们很可能在开发过程中给do_display函数添加某些调试信息,而这些信息将在代码的最终产品版本中去掉 。显然,只在do_display一处添加或删除这些信息要更容易一些 。
  4. 这个额外的函数调用不会增加任何开销 。因为我们在类内部定义了do_display,所以它隐式地被声明成内联函数 。这样的话,调用do display就不会带来任何额外的运行时开销 。
在实践中,设计良好的C++代码常常包含大量类似于do display的小函数,通过调用这些函数,可以完成一组其他函数的“实际”工作 。
类类型
每个类定义了唯一的类型 。对于两个类来说,即使它们的成员完全一样,这两个类也是两个不同的类型 。
例如:
struct First{int memi;int getMem();} struct Second {int memi;int getMem();}First objl;Second obj2 = obj1;//错误: objl和obj2的类型不同 **注意:
**
即使两个类的成员列表完全一致,它们也是不同的类型 。对于一个类来说,它的成员和其他任何类(或者任何其他作用域)的成员都不是一回事儿 。
我们可以把类名作为类型的名字使用,从而直接指向类类型 。或者,我们也可以把类名跟在关键字class或struct后面:
Sales_data item1;//默认初始化Sales data类型的对象class Sales_data item1;//一条等价的声明 类的声明
就像可以把函数的声明和定义分离开来一样,我们也能仅仅声明类而暂时不定义它:
class Screen;// Screen类的声明 这种声明有时被称作前向声明(forward declaration),它向程序中引入了名字Screen并且指明Screen是一种类类型 。对于类型Screen来说,在它声明之后定义之前是一个不完全类型(incomplete type),也就是说,此时我们已知Screen是一个类类型,但是不清楚它到底包含哪些成员 。
不完全类型只能在非常有限的情景下使用:**可以定义指向这种类型的指针或引用,****也可以声明(但是不能定义)以不完全类型作为参数或者返回类型的函数 。**对于一个类来说,在我们创建它的对象之前该类必须被定义过,而不能仅仅被声明 。否则,编译器就无法了解这样的对象需要多少存储空间 。
类似的,类也必须首先被定义,然后才能用引用或者指针访问其成员 。毕竞,如果类尚未定义,编译器也就不清楚该类到底有哪些成员 。
在后面会看到一种例外的情况:直到类被定义之后数据成员才能被声明成这种类类型 。换句话说,我们必须首先完成类的定义,然后编译器才能知道存储该数据成员需要多少空间 。因为只有当类全部完成后类才算被定义,所以一个类的成员类型不能是该类自己 。然而,一旦一个类的名字出现后,它就被认为是声明过了(但尚未定义),因此类允许包含指向它自身类型的引用或指针:
class Link_screen{Screen window;Link_screen *next;Link_screen *prev;}; 友元再探
我们的Sales data类把三个普通的非成员函数定义成了友元 。类还可以把其他的类定义成友元,也可以把其他类(之前已定义过的)的成员函数定义成友元 。此外,友元函数能定义在类的内部,这样的函数是隐式内联的 。
举个友元类的例子,我们的window mgr类的某些成员可能需要访问它管理的Screen类的内部数据 。例如,假设我们需要为window mgr添加一个名为clear的成员,它负责把一个指定的Screen的内容都设为空白 。为了完成这一任务,clear需要访问Screen的私有成员;而要想令这种访问合法, Screen需要把window mgr指定成它的友元:
class Screen{// Window_mgr的成员可以访问Screen类的私有部分friend class Window_mgr;// Screen类的剩余部分}; 如果一个类指定了友元类,则友元类的成员函数可以访问此类包括非公有成员在内的所有成员 。通过上面的声明,window mgr被指定为Screen的友元,因此我们可以将Window mgr的clear成员写成如下的形式:
class window_mgr{public://窗口中每个屏幕的编号using ScreenIndex = std::vector::size_type;//按照编号将指定的Screen重置为空白void clear (ScreenIndex);private:std::vectorscreens{Screen(24, 80, ' ')};}; void window_mgr::clear (ScreenIndex i) {//s是一个Screen的引用,指向我们想清空的那个屏幕Screen &s = screens[i] ;//将那个选定的Screen重置为空白s.contents = string (s.height * s.width, ' '); } 必须要注意的一点是,友元关系不存在传递性 。也就是说,如果window mgr有它自己的友元,则这些友元并不能理所当然地具有访问Screen的特权 。
注意:每个类负责控制自己的友元类或友元函数 。
令成员函数作为友元
除了令整个Window mgr作为友元之外,Screen还可以只为clear提供访问权限 。当把一个成员函数声明成友元时,我们必须明确指出该成员函数属于哪个类:
class Screen{// Window mgr::clear 必须在Screen类之前被声明friend void window_mgr::clear(ScreenIndex);// Screen类的剩余部分} 要想令某个成员函数作为友元,我们必须仔细组织程序的结构以满足声明和定义的彼此依赖关系 。在这个例子中,必须按照如下方式设计程序:
  1. 首先定义Window mgr类,其中声明clear函数,但是不能定义它 。在clear使用Screen的成员之前必须先声明Screen 。
  2. 接下来定义Screen,包括对于clear的友元声明 。
  3. 最后定义clear,此时它才可以使用Screen的成员 。
函数重载和友元尽管重载函数的名字相同,但它们仍然是不同的函数 。因此,如果一个类想把一组重载函数声明成它的友元,它需要对这组函数中的每一个分别声明:
//重载的storeOn函数extern std::ostream& storeOn(std: :ostream &, Screen &);extern BitMap& storeon (BitMap &, Screen &);class Screen{// storeon的ostream版本能访问Screen对象的私有部分friend std::ostream& storeOn(std::ostream &, Screen &);}; Screen类把接受ostream&的 storeon函数声明成它的友元,但是接受BitMap&作为参数的版本仍然不能访问Screen 。
友元声明和作用域
类和非成员函数的声明不是必须在它们的友元声明之前 。当一个名字第一次出现在一个友元声明中时,我们隐式地假定该名字在当前作用域中是可见的 。然而,友元本身不一定真的声明在当前作用域中 。甚至就算在类的内部定义该函数,我们也必须在类的外部提供相应的声明从而使得函数可见 。换句话说,即使我们仅仅是用声明友元的类的成员调用该友元函数,它也必须是被声明过的:
struct X{friend void f() {/*友元函数可以定义在类的内部*/}X(){ f();} //错误: f还没有被声明void g();void h();};void X::g(){ return f(); }//错误: f还没有被声明void f();//声明那个定义在x中的函数void x::h() {return f() ;} //正确:现在f的声明在作用域中了 关于这段代码最重要的是理解友元声明的作用是影响访问权限,它本身并非普通意义上的声明 。
【C++Primer 学习(类 三)类的其他特性】请注意:有的编译器并不强制执行上述关于友元的限定规则 。