类的其他特性

类的基本特性包括:类型成员、类的成员的类内初始值、可变数据成员、内联成员函数、从成员函数返回*this。

类成员再探

定义一个类型成员

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

除了定义数据和函数成员之外,类还可以自定义某种类型在类中的别名。由类定义的类型名字和其他成员一样存在访问限制,可以是public或者private中的一种:

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

我们在Screen的public部分定义了pos,这样用户就可以使用这个名字。Screen的用户不应该指针Screen使用一个string对象来存放它的数据,因此通过把pos定义成public成员可以隐藏Screen实现的细节。

关于pos的声明有两点需要注意。首先,我们使用了typedef,也可以等价地使用类型别名:

class Screen{
public:
    using pos=string::size_type;
};

其次,用来定义类型的成员必须先定义后使用。这一点与普通成员有所区别。因此,类型成员通常出现在类开始的地方。

 

Screen类的成员函数以及内联函数

class Screen{
public:
    typedef string::size_type pos;
    Screen()=default;
    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;
    string contents;
};

在类中,常有一些规模较小的函数适用于被声明成内联函数。如我们之前所见的,定义在类内部的成员函数时自动inline的。因此,Screen的构造函数和返回光标所指字符的get函数默认是inline函数。

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

inline Screen& Screen::move(pos r,pos c) //可以在函数的定义处指定inline
{
    pos row=r*width;
    cursor=row+r;
    return *this;
}

char Screen::get(pos r,pos c) const //在类的内部声明成inline
{
    pos row=r*width;
    return contents[row+c];
}

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

和我们在头文件中定义inline函数的原因一样,inline成员函数也应该与相应的类定义在同一个头文件中。(内联函数要和类的定义放在同一个文件中)

 

重载成员函数

和非成员函数一样,成员函数也可以被重载,只有函数之间在参数的数量或类型上有所区别就行。成员函数的函数匹配过程同样和非成员函数非常类似。

 

可变数据成员

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

一个可变数据成员永远不会是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函数在内都能改变它的值。

 

类数据成员的初始值

在C++11新标准中,可以给数据成员提供一个类内初始值:

class Window_mgr{
private:
    vector<Screen> screens{Screen(24,80,' ')};
};

如我们之前所知的,类内初始值必须使用=的初始化形式或者花括号括起来的直接初始化形式(初始化screens所用的)。

当我们提供一个类内初始值时,必须以符号=或者花括号表示

 

返回*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('#');

这些操作将在同一个对象上执行。在上面的表达式中,我们首先移动myScreen内的光标,然后设置myScreen的contents成员。也就是说,上述语句等价于

myScreen.move(4,0);

myScreen.set('#');

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

在此例中等价于:

//如果move返回Screen而非Screen&

Screem temp=myScreen.move(4,0);  //对返回值进行拷贝

temp.set('#');    //不会改变myScreen的contents

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

 

从const成员函数返回*this

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

从逻辑上来说,显示一个Screen并不需要改变他的内容,因此我们令display为一个const成员,此时,this将是一个指向const的指针而*this是const对象,由此推断,display的返回类型应该是const Screen&。然而,如果真的令display返回一个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(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指向一个非常量对象,因此display返回一个普通的(非常量)引用;而const成员则返回一个常量引用。

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

Screen myScreen(5,3);

const Screen blank(5,3);

myScreen.set('#').display(cout);  //调用非常量版本

blank.display(cout);   //调用常量版本

 

类类型

每个类定义了唯一的类型。对于两个类来说,即使他们的成员完全一样,这两个类也是两个不同的类型。

我们可以把类名作为类型的名字使用,从而直接指向类类型。或者,我们也可以把类名跟在关键字class或struct后面:

Sales_data item1;   //默认初始化Sales_data类型的对象

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

 

类的声明

就像可以把函数的声明和定义分离开来一样,我们也能仅仅声明类而暂时不定义它:

class Screen ; //声明Screen类

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

不完全类型只能在非常有限的情景下使用:可以定义指向这种类型的指针或引用,也可以声明(但是不能定义)以不完全类型作为参数或者返回类型的函数。

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

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

class Link_screen{

  Screen window;

  Link_screen *next;

  Link_screen *prev;

};

 

posted @ 2014-08-08 19:16  Jessica程序猿  阅读(528)  评论(0编辑  收藏  举报