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被其类内初始值初始化为0
      Screen (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关键字修饰函数的定义:

//可以在函数的定义处指定inline
inline 
Screen &Screen: :move (pos r, pos c)
{
  pos row = r * width;  //计算行的位置
  cursor = row + C;  //在行内将光标移动到指定的列
  return *this;  //以左值的形式返回对象
}
// 在类的内部声明成 inline
char 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::vector<Screen>screens{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的声明在作用域中了

关于这段代码最重要的是理解友元声明的作用是影响访问权限,它本身并非普通意义上的声明。

请注意:有的编译器并不强制执行上述关于友元的限定规则。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Cpp编程小茶馆

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值