第七章 函数(part5) 类的成员函数

来源:互联网 发布:水乳推荐知乎 编辑:程序博客网 时间:2024/06/05 00:41

7.7. 类的成员函数

成员函数的定义与普通函数的定义类似。和任何函数一样,成员函数也包含下面四个部分:

  • 函数返回类型。

  • 函数名。

  • 用逗号隔开的形参表(也可能是空的)。

  • 包含在一对花括号里面的函数体。

正如我们知道的,前面三部分组成函数原型。函数原型定义了所有和函数相关的类型信息:函数返回类型是什么、函数的名字、应该给这个函数传递什么类型的实参。

函数原型必须在类中定义。但是,函数体则既可以在类中也可以在类外定义。

知道这些后,观察下面扩展的类定义,我们为这个类增加了两个新成员:成员函数 avg_pricesame_isbn。其中 avg_price 函数的形参表是空的,返回 double 类型的值。
same_isbn 函数则返回 bool 对象,有一个 const Sales_item 类型的引用形参。

     class Sales_item {     public:         // operations on Sales_item objects         double avg_price() const;         bool same_isbn(const Sales_item &rhs) const              { return isbn == rhs.isbn; }     // private members as before     private:         std::string isbn;         unsigned units_sold;         double revenue;     };
在解释跟在形参表后面的 const 之前,必须先说明成员函数是如何定义的。

7.7.1. 定义成员函数的函数体

类的所有成员都必须在类定义的花括号里面声明,此后,就不能再为类增加任何成员。类的成员函数必须如声明的一般定义。

类的成员函数既可以在类的定义内也可以在类的定义外定义。在类 Sales_item 中,这两种情况各有一例说明:函数same_isbn 在类内定义,而函数avg_price 则在类内声明,在类外定义。

编译器隐式地将在类内定义的成员函数当作内联函数。

再详细观察函数 same_isbn 的定义:

     bool same_isbn(const Sales_item &rhs) const         { return isbn == rhs.isbn; }

与任何函数一样,该函数的函数体也是一个块。在这个函数中,块中只有一个语句,比较两个 Sales_item 对象的数据成员isbn 的值,并返回比较结果。

首先要注意的是:成员 isbnprivate 的。尽管如此,上述语句却没有任何错误。

<Note>:

类的成员函数可以访问该类的 private 成员。

更有意思的是,函数从哪个 Sales_item 类对象得到这个用于比较的值?函数涉及到 isbnrhs.isbn。很明显,rhs.isbn 使用的是传递给此函数的实参的isbn 成员。
没有前缀的 isbn 的用法更加有意思。正如我们所看见的,这个没有前缀的 isbn 指的是用于调用函数的对象的 isbn 成员。

成员函数含有额外的、隐含的形参

调用成员函数时,实际上是使用对象来调用的。例如,调用书店程序中的函数 same_isbn,是通过名为 total 的对象来执行 same_isbn 函数的:

     if (total.same_isbn(trans))

在这个调用中,传递了对象 trans。作为执行调用的一部分,使用对象 trans 初始化形参rhs。于是,rhs.isbntrans.isbn 的引用。

而没有前缀的 isbn 使用了相同的实参绑定过程,使之与名为 total 的对象绑定起来。每个成员函数都有一个额外的、隐含的形参将该成员函数与调用该函数的类对象捆绑在一起。当调用名为total 的对象的same_isbn 时,这个对象也传递给了函数。而same_isbn 函数使用 isbn 时,就隐式地使用了调用该函数的对象的isbn 成员。

这个函数调用的效果是比较 total.isbntrans.isbn 两个值。


this 指针的引入

每个成员函数(除了static 成员函数外)都有一个额外的、隐含的形参 this。在调用成员函数时,形参 this 初始化为调用函数的对象的地址。

为了理解成员函数的调用,可考虑下面的语句:

     total.same_isbn(trans);

就如编译器这样重写这个函数调用:

     // pseudo-code illustration of how a call to a member function is translated     Sales_item::same_isbn(&total, trans);
在这个调用中,函数 same_isbn 中的数据成员 isbn 属于对象 total

const 成员函数的引入

现在,可以理解跟在 Sales_item 成员函数声明的形参表后面的 const 所起的作用了:const 改变了隐含的this 形参的类型。

在调用 total.same_isbn(trans) 时,隐含的 this 形参将是一个指向total 对象的const Sales_Item* 类型的指针。就像如下编写same_isbn 的函数体一样:

     // pseudo-code illustration of how the implicit this pointer is used     // This code is illegal: We may not explicitly define the this pointer ourselves     // Note that this is a pointer to const because same_isbn is a const member     bool Sales_item::same_isbn(const Sales_item *const this,                               const Sales_item &rhs) const     { return (this->isbn == rhs.isbn); }
用这种方式使用 const 的函数称为常量成员函数。由于this 是指向const 对象的指针,const 成员函数不能修改调用该函数的对象。
因此,函数 avg_price 和函数 same_isbn 只能读取而不能修改调用它们的对象的数据成员。
<Note>:

const 对象、指向 const 对象的指针或引用只能用于调用其 const 成员函数,如果尝试用它们来调用非 const 成员函数,则是错误的。


this 指针的使用

在成员函数中,不必显式地使用 this 指针来访问被调用函数所属对象的成员。对这个类的成员的任何没有前缀的引用,都被假定为通过指针this 实现的引用:

     bool same_isbn(const Sales_item &rhs) const         { return isbn == rhs.isbn; }
在这个函数中 isbn 的用法与 this->units_soldthis->revenue 的用法一样。

由于 this 指针是隐式定义的,因此不需要在函数的形参表中包含 this 指针,实际上,这样做也是非法的。但是,在函数体中可以显式地使用this 指针。如下定义函数same_isbn 尽管没有必要,但是却是合法的:

     bool same_isbn(const Sales_item &rhs) const         { return this->isbn == rhs.isbn; }

7.7.2. 在类外定义成员函数

在类的定义外面定义成员函数必须指明它们是类的成员:

     double Sales_item::avg_price() const     {         if (units_sold)             return revenue/units_sold;         else             return 0;     }

上述定义和其他函数一样:该函数返回类型为 double,在函数名后面的圆括号起了一个空的形参表。新的内容则包括跟在形参表后面的const 和函数名的形式。函数名:

     Sales_item::avg_price
使用作用域操作符指明函数 avg_price 是在类 Sales_item 的作用域范围内定义的。

形参表后面的 const 则反映了在类 Sales_item 中声明成员函数的形式。在任何函数定义中,返回类型和形参表必须和函数声明(如果有的话)一致。
对于成员函数,函数声明必须与其定义一致。如果函数被声明为 const 成员函数,那么函数定义时形参表后面也必须有 const

现在可以完全理解第一行代码了:这行代码说明现在正在定义类 Sales_item 的函数 avg_price,而且这是一个const 成员函数,这个函数没有(显式的)形参,返回double 类型的值。

函数体更加容易理解:检查 units_sold 是否为 0,如果不为 0,返回 revenue 除以 units_sold 的结果;
如果 units_sold 是 0,不能安全地进行除法运算——除以 0 是未定义的行为。此时程序返回 0,表示没有任何销售时平均售价为 0。
根据异常错误处理策略,也可以抛出异常来代替刚才的处理。

7.7.3. 编写 Sales_item 类的构造函数

还必须编写一个成员,那就是构造函数。正如在所学习的,在定义类时没有初始化它的数据成员,而是通过构造函数来初始化其数据成员。
构造函数是特殊的成员函数
构造函数是特殊的成员函数,与其他成员函数不同,构造函数和类同名,而且没有返回类型。
而与其他成员函数相同的是,构造函数也有形参表(可能为空)和函数体。
一个类可以有多个构造函数,每个构造函数必须有与其他构造函数不同数目或类型的形参。

构造函数的形参指定了创建类类型对象时使用的初始化式。通常,这些初始化式会用于初始化新创建对象的数据成员。

构造函数通常应确保其每个数据成员都完成了初始化。

Sales_item 类只需要显式定义一个构造函数:没有形参的默认构造函数。默认构造函数说明当定义对象却没有为它提供(显式的)初始化式时应该怎么办:

     vector<int> vi;       // default constructor: empty vector     string s;             // default constructor: empty string     Sales_item item;      // default constructor: ???

我们知道 stringvector 类默认构造函数的行为:这些构造函数会将对象初始化为合理的默认状态。string 的默认构造函数会产生空字符串上,相当于""vector 的默认构造函数则生成一个没有元素的vector 向量对象。

同样地,我们希望类 Sales_items 的默认构造函数为它生成一个空的 Sales_item 对象。这里的“空”意味着对象中的isbn 是空字符串,units_soldrevenue 则初始化为 0。


构造函数的定义

和其他成员函数一样,构造函数也必须在类中声明,但是可以在类中或类外定义。由于我们的构造函数很简单,因此在类中定义它:

     class Sales_item {     public:         // operations on Sales_item objects         double avg_price() const;         bool same_isbn(const Sales_item &rhs) const             { return isbn == rhs.isbn; }         // default constructor needed to initialize members of built-in type         Sales_item(): units_sold(0), revenue(0.0) { }     // private members as before     private:         std::string isbn;         unsigned units_sold;         double revenue;     };

在解释任何构造函数的定义之前,注意到构造函数是放在类的 public 部分的。通常构造函数会作为类的接口的一部分,这个例子也是这样。

毕竟,我们希望使用类 Sales_item 的代码可以定义和初始化类 Sales_item 的对象。如果将构造函数定义为private 的,则不能定义类Sales_item 的对象,这样的话,这个类就没有什么用了。

As to the definition itself

对于定义本身:

     // default constructor needed to initialize members of built-in type     Sales_item(): units_sold(0), revenue(0.0) { }

上述语句说明现在正在定义类 Sales_item 的构造函数,这个构造函数的形参表和函数体都为空。令人感兴趣的是冒号和冒号与定义(空)函数体的花括号之间的代码。


构造函数和初始化列表
在冒号和花括号之间的代码称为构造函数的初始化列表。构造函数的初始化列表为类的一个或多个数据成员指定初值。
它跟在构造函数的形参表之后,以冒号开关。构造函数的初始化式是一系列成员名,每个成员后面是括在圆括号中的初始值。多个成员的初始化用逗号分隔。

上述例题的初始化列表表明 units_soldrevenue 成员都应初始化为 0。每当创建 Sales_item 对象时,它的这两个成员都以初值 0 出现。而isbn 成员可以不必准确指明其初值。除非在初始化列表中有其他表述,否则具有类类型的成员皆被其默认构造函数自动初始化。于是,isbnstring 类的默认构造函数初始化为空串。当然,如果有必要的话,也可以在初始化列表中指明isbn 的默认初值。

解释了初始化列表后,就可以深入地了解这个构造函数了:它的形参表和函数体都为空。形参表为空是因为正在定义的构造函数是默认调用的,无需提供任何初值。函数体为空是因为除了初始化units_soldrevenue 成员外没有其他工作可做了。初始化列表显式地将units_soldrevenue 初始化为 0,并隐式地将isbn 初始化为空串。当创建新 Sales_item 对象时,数据成员将以这些值出现。

合成的默认构造函数
<Note>:

如果没有为一个类显式定义任何构造函数,编译器将自动为这个类生成默认构造函数。


由编译器创建的默认构造函数通常称为默认构造函数,它将依据如同变量初始化的规则初始化类中所有成员。
对于具有类类型的成员,如 isbn,则会调用该成员所属类自身的默认构造函数实现初始化。内置类型成员的初值依赖于对象如何定义。
如果对象在全局作用域中定义(即不在任何函数中)或定义为静态局部对象,则这些成员将被初始化为 0。
如果对象在局部作用域中定义,则这些成员没有初始化。除了给它们赋值之外,出于其他任何目的对未初始化成员的使用都没有定义。
<Beware>:

合成的默认构造函数一般适用于仅包含类类型成员的类。而对于含有内置类型或复合类型成员的类,则通常应该定义他们自己的默认构造函数初始化这些成员。

7.7.4. 类代码文件的组织

通常将类的声明放置在头文件中。大多数情况下,在类外定义的成员函数则置于源文件中。
C++ 程序员习惯使用一些简单的规则给头文件及其关联的类定义代码命名。类定义应置于名为 type.htype.H 的文件中,type 指在该文件中定义的类的名字。
成员函数的定义则一般存储在与类同名的源文件中。依照这些规则,我们将类 Sales_item 放在名为 Sales_item.h 的文件中定义。
任何需使用这个类的程序,都必须包含这个头文件。而 Sales_item 的成员函数的定义则应该放在名为 Sales_item.cc 的文件中。这个文件同样也必须包含Sales_item.h 头文件。


































原创粉丝点击