类的成员函数
0. 类的成员函数
函数原型定义了所有和函数相关的类型信息:
函数返回类型是什么、函数的名字、应该给这个函数传递什么类型的实参。
类的成员函数,其原型必须在类中定义。 但是,函数体则既可以在类中也可以在类外定义。
以下是一个 Sales_item 类的定义。
1 class Sales_item { 2 // operations on Sales_item objects 3 public: 4 double avg_price() const; 5 bool same_isbn(const Sales_item &rhs) const 6 { return isbn == rhs.isbn; } 7 8 // private members 9 private: 10 std::string isbn; 11 unsigned units_sold; 12 double revenue; 13 };
1. 定义成员函数的函数体
类的所有成员都必须在类定义的花括号里面声明,此后,就不能再为类增加任何成员。
类的成员函数必须如声明的一般定义。类的成员函数既可以在类的定义内也可以在类的定义外定义。
编译器隐式地将在类内定义的成员函数当作内联函数。
再详细观察函数 same_isbn 的定义:
1 bool same_isbn(const Sales_item &rhs) const { 2 return isbn == rhs.isbn; 3 }
在这个函数中,块中只有一个语句,比较两个 Sales_item 对象的数据成员 isbn 的值,并返回比较结果。
首先要注意的是:尽管类的成员变量 isbn 是 private 的。上述语句却没有任何错误!But, Why?
( 因为,类的成员函数可以访问该类的 private 成员变量的。)
更有意思的是,函数从哪个 Sales_item 类对象得到这个用于比较的值呢?
函数涉及到 isbn 和 rhs.isbn。很明显,rhs.isbn 使用的是传递给此函数的实参的 isbn 成员。
而这个没有前缀的 isbn 指的是用于调用函数的对象的 isbn 成员。
2. 成员函数含有额外的、隐含的形参
调用成员函数时,实际上是使用对象来调用的。例如通过名为 total 的对象来执行 same_isbn 函数:
if (total.same_isbn(trans))
在这个调用中,传递了对象 trans。作为执行调用的一部分,使用对象 trans 初始化形参 rhs。于是,rhs.isbn 是 trans.isbn 的引用。
而没有前缀的 isbn 使用了相同的实参绑定过程,使之与名为 total 的对象绑定起来。
每个成员函数都有一个额外的、隐含的形参将该成员函数与调用该函数的类对象捆绑在一起。
当调用名为 total 的对象的 same_isbn 时,这个对象也传递给了函数。
而 same_isbn 函数使用 isbn 时,就隐式地使用了调用该函数的对象的 isbn 成员。
这个函数调用的效果是比较 total.isbn 和 trans.isbn 两个值。
3. 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。
4. 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 只能读取而不能修改调用它们的对象的数据成员。
(注意:const 对象、指向 const 对象的指针或引用只能用于调用其 const 成员函数,不能用它们来调用非 const 成员函数。)
5. this 指针的使用
在成员函数中,不必显式地使用 this 指针来访问被调用函数所属对象的成员。
对这个类的成员的任何没有前缀的引用,都被假定为通过指针 this 实现的引用:
1 bool same_isbn(const Sales_item &rhs) const { 2 return isbn == rhs.isbn; 3 }
在这个函数中 isbn 的用法与 this->units_sold 或 this->revenue 的用法一样。
由于 this 指针是隐式定义的,因此不需要在函数的形参表中包含 this 指针,实际上,这样做也是非法的。
但是,在函数体中可以显式地使用 this 指针。如下定义函数 same_isbn 尽管没有必要,但是却是合法的:
1 bool same_isbn(const Sales_item &rhs) const { 2 return this->isbn == rhs.isbn; 3 }
6. 在类外定义成员函数
在类的定义外面定义成员函数必须指明它们是类的成员:
1 double Sales_item::avg_price() const 2 { 3 if (units_sold) 4 return revenue/units_sold; 5 else 6 return 0; 7 }
该函数返回类型为 double,在函数名后面的圆括号起了一个空的形参表。
函数名: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. 类的构造函数
可以在定义类时不显示的初始化它的数据成员,而是通过构造函数来初始化其数据成员。
构造函数是特殊的成员函数,与其他成员函数不同,构造函数和类同名,而且没有返回类型。
而与其他成员函数相同的是,构造函数也有形参表(可能为空)和函数体。
一个类可以有多个构造函数,每个构造函数必须有与其他构造函数不同数目或类型的形参。
构造函数的形参指定了创建类类型对象时使用的初始化式。
通常,这些初始化式会用于初始化新创建对象的数据成员。构造函数通常应确保其每个数据成员都完成了初始化。
Sales_item 类只需要显式定义一个构造函数:没有形参的默认构造函数。
默认构造函数说明当定义对象却没有为它提供(显式的)初始化式时应该怎么办:
vector<int> vi; // default constructor: empty vector string s; // default constructor: empty string Sales_item item; // default constructor: ???
我们知道 string 和 vector 类默认构造函数的行为:这些构造函数会将对象初始化为合理的默认状态。
string 的默认构造函数会产生空字符串上,相当于 ""。vector 的默认构造函数则生成一个没有元素的 vector 向量对象。
同样地,我们希望类 Sales_items 的默认构造函数为它生成一个空的 Sales_item 对象。
这里的“空”意味着对象中的 isbn 是空字符串,units_sold 和 revenue 则初始化为 0。
8. 构造函数的定义
和其他成员函数一样,构造函数也必须在类中声明,但是可以在类中或类外定义。
1 class Sales_item { 2 // operations on Sales_item objects 3 public: 4 double avg_price() const; 5 bool same_isbn(const Sales_item &rhs) const{ 6 return isbn == rhs.isbn; 7 } 8 // default constructor needed to initialize members of built-in type 9 Sales_item(): units_sold(0), revenue(0.0) { } 10 11 // private members as before 12 private: 13 std::string isbn; 14 unsigned units_sold; 15 double revenue; 16 };
在解释任何构造函数的定义之前,注意到构造函数是放在类的 public 部分的。
通常构造函数会作为类的接口的一部分,这个例子也是这样。
毕竟,我们希望使用类 Sales_item 的代码可以定义和初始化类 Sales_item 的对象。
如果将构造函数定义为 private 的,则不能定义类 Sales_item 的对象,这样的话,这个类就没有什么用了。
对于定义本身:
// default constructor needed to initialize members of built-in type Sales_item(): units_sold(0), revenue(0.0) { }
在冒号和花括号之间的代码称为构造函数的初始化列表。
构造函数的初始化列表为类的一个或多个数据成员指定初值。它跟在构造函数的形参表之后,以冒号开关。
构造函数的初始化式是一系列成员名,每个成员后面是括在圆括号中的初始值。多个成员的初始化用逗号分隔。
上述例题的初始化列表表明 units_sold 和 revenue 成员都应初始化为 0。
每当创建 Sales_item 对象时,它的这两个成员都以初值 0 出现。而 isbn 成员可以不必准确指明其初值。
除非在初始化列表中有其他表述,否则具有类类型的成员皆被其默认构造函数自动初始化。
于是,isbn 由 string 类的默认构造函数初始化为空串。当然,如果有必要的话,也可以在初始化列表中指明 isbn 的默认初值。
解释了初始化列表后,就可以深入地了解这个构造函数了:
它的形参表和函数体都为空。形参表为空是因为正在定义的构造函数是默认调用的,无需提供任何初值。
函数体为空是因为除了初始化 units_sold 和 revenue 成员外没有其他工作可做了。
初始化列表显式地将 units_sold 和 revenue 初始化为 0,并隐式地将 isbn 初始化为空串。
当创建新 Sales_item 对象时,数据成员将以这些值出现。
注意:如果没有为一个类显式定义任何构造函数,编译器将自动为这个类生成默认构造函数。
由编译器创建的默认构造函数通常称为默认构造函数,它将依据如同变量初始化的规则初始化类中所有成员。
对于具有类类型的成员,如 isbn,则会调用该成员所属类自身的默认构造函数实现初始化。
内置类型成员的初值依赖于对象如何定义。
如果对象在全局作用域中定义(即不在任何函数中)或定义为静态局部对象,则这些成员将被初始化为 0。
如果对象在局部作用域中定义,则这些成员没有初始化。
除了给它们赋值之外,出于其他任何目的对未初始化成员的使用都没有定义。
由于合成的默认构造函数不会自动初始化内置类型的成员,所以此处必须明确定义 Sales_item 类的默认构造函数。