目录
- 4.1如何实现一个Class(How to Implement a Class)
- 4.2什么是构造函数和析构函数(What Are Class Constructors and the Class Destructor?)
- Member Initialization List(成员初始化列表)
- Memberwise Initialization(成员逐一初始化)
- 复制构造函数(copy constructor)
- 4.3何谓mutable(可变)和const(不变)(What Are mutable and const?)
- Mutable Data Member(可变的数据成员)
- 4.4什么是this指针(What Is the this Pointer?)
- 4.5静态类成员(Static Class Members)
- Static Member Function(静态成员函数)
- 4.6打造一个Iterator Class(Building an Iterator Class)[运算符重载]
- 为什么++前置版本返回对象引用,而++后置版本返回对象?
- 嵌套类型(Nested Type)[typedef]
- 4.7合作关系必须建立在友谊的基础上(Collaboration Sometimes Requires Friendship)[friend-友元函数和友元类]
- 4.8实现一个copy assignment operator(Implementing a Copy Assignment Operator)[复制赋值运算符]
- 4.9实现一个function object(Implementing a Function Object)
- 4.10重载iostream运算符(Providing Class Instances of the iostream Operators)
- 为什么不把output运算符设计为一个成员函数呢?
- 4.11指针、指向Class Member Function(Pointers to Class Member Functions)
4.1如何实现一个Class(How to Implement a Class)
public
member可以在程序的任何地方被访问,private
member只能在member function或class friend内被访问。
class Stack {
public:
bool push(const string&);
bool pop(string &elem);
bool peek(string &elem);
bool empty();
bool full();
int size() {
return _stack.size();
}
private:
vector<string> _stack;
};
如果要在class主体内直接给出函数定义,这个member function会自动地被视为inline
函数。例如,上例中的size()即是Stack的一个inline member。
如何在class主体之外定义member function呢?
必须使用特殊的语法,目的在于分辨该函数究竟属于哪一个class。
如果希望该函数为inline,就在最前面指定关键字inline。
inline bool Stack::empty() {
return _stack.empty();
}
bool Stack::pop(string &elem) {
if (empty())
return false;
elem = _stack.back();
_stack.pop_back();
return true;
}
Stack::empty()
告诉编译器,empty()是Stack class(而非vector或string)的一个member。
class名称之后的两个冒号(Stack::)即是class scope resolution(类作用域解析)运算符。
class定义及其inline member function通常都会放在与class同名的头文件中。
non-inline member function应该在程序代码文件中定义,该文件通常和class同名。
以下是Stack member function的定义:
inline bool Stack::full() {
return _stack.size() == _stack.max_size();
}
bool Stack::peek(string &elem) {
if (empty())
return false;
elem = _stack.back();
return true;
}
bool Stack::push(const string &elem) {
if (full())
return false;
_stack.push_back(elem);
return true;
}
4.2什么是构造函数和析构函数(What Are Class Constructors and the Class Destructor?)
先定义一个数列类:
class Triangular {
public:
// ...
private:
int _length;// 元素个数
int _beg_pos;// 起始位置
int _next;// 下一个迭代目标
};
要初始一个Triangular对象,可以使用构造函数用于初始化目的。
语法规定,Constructor函数名必须与class同名,不能指定返回类型,亦不用返回任何值。
class Triangular {
public:
Triangular();// 默认构造函数
Triangular(int len);
Triangular(int len, int beg_pos);
// ...
};
Triangular t;// 调用default constructor
Triangular t2(10, 3);// 调用带有两个参数的constructor
Triangular t3 = 8;// 调用带有单一参数的constructor
对于default constructor(默认构造函数),它不需要任何参数(argument),这就意味着两种情况:1.它不接受任何参数;2.它为每个参数都提供了默认值。
// 1.
Triangular::Triangular() {
_length = 1;
_beg_pos = 1;
_next = 0;
}
// 2.
Triangular(int len = 1, int bp = 1);
再定义一个non-default constructor:
Triangular::Triangular(int len, int bp) {
_length = len > 0 ? len : 1;
_beg_pos = bp > 0 ? bp : 1;
_next = _beg_pos - 1;
}
初始化:
Triangular tri1; // Triangular::Triangular(1, 1);
Triangular tri2(12); // Triangular::Triangular(12, 1);
Triangular tri3(8, 3); // Triangular::Triangular(8, 3);
Member Initialization List(成员初始化列表)
Triangular::Triangular(const Triangular &rhs)
: _length(rhs._length), _beg_pos(rhs._beg_pos), _next(rhs._beg_pos - 1)
{}// 是的,空的。
假设我们重新定义Triangular,令它包含一个string member:
class Triangular {
public:
// ...
private:
string _name;
int _next, _length, _beg_pos;
};
为了将_name的初值传给string constructor,我们必须以member initialization list完成:
Triangular::Triangular(int len, int bp) : _name("Triangular") {
_length = len > 0 ? len : 1;
_beg_pos = bp >0 ? bp : 1;
_next = _beg_pos - 1;
}
和constuctor对立的是destructor。
destructor是用户自定义的一个class member。
一旦某个class提供有destructor,当其object结束生命时,便会自动调用destructor处理善后。
Destructor主要用来释放在constructor中或对象生命周期中分配的资源。
Destructor的名称有严格规定:class名称加上’~'前缀。没有返回值,没有任何参数。所以也绝不可能被重载(overloaded)。
class Matrix {
public:
Matrix(int row, int col): _row(row), _col(col) {
// constructor进行资源的分配
// 此处未检查成功与否
_pmat = new double[row * col];
}
~Matrix() {// destructor进行资源的释放
delete[] _pmat;
}
private:
int _row, _col;
double* _pmat;
};
于是,我们通过Matrix本身的constructor和destructor,完成了heap内存的自动管理。
例如下面这个语句块:
{
Matrix mat(4, 4);// 引处应用constructor
// ...
// 此处应用destructor
}
Memberwise Initialization(成员逐一初始化)
以某个class object作为另一个object的初值:
Triangular tri1(8);
Triangular tri2 = tri1;
class data member会被依次复制。本例中的_length、_beg_pos、_next都会依次从tri1复制到tri2。此即default memberwise initialization(默认的成员逐一初始化操作)。
对于Triangular,default memberwise initialization会正确复制所有data member,我们不必特意做其他事。
但对Matrix class而言,default memberwise initialization并不适当:
{
Matrix mat(4, 4);
{
Matrix mat2 = mat;// 进行default memberwise initialization
// ... 在这里使用mat2
// mat2的destructor发生作用
}
// ... 在这里使用mat
// mat的destructor发生作用
}
其中,default memberwise initialization会将mat2的_pmat设为mat的_pmat值:
mat2._pmat = mat._pmat;
这会使得两个对象的_pmat都指向heap内的同一个数组。当Matrix destructor应用于mat2上,该数组空间便被释放。不幸的是,此时mat的_pmat仍指向那个数组,而对空间已经释放的数组进行操作,是非常严重的错误行为。
复制构造函数(copy constructor)
如果Matrix设计者提供了一个copy constructor,它就可以改变“成员逐一初始化”的默认行为模式。
其唯一参数是一个const reference,指向(代表)一个Matrix object:
Matrix::Matrix(const Matrix &rhs) {
// ...
}
其内容又该如何实现呢?我们可以产生一个独立的数组复本,这样便可以使某个对象的析构操作不致于影响到另一个对象:
Matrix::Matrix(const Matrix &rhs): _row(rhs._row), _col(rhs._col) {
int elem_cnt = _row * _col;
_pmat = new double[elem_cnt];
for (int i = 0; i < elem_cnt; ++i)
_pmat[i] = rhs._pmat[i];
}
当我们设计class时,必须搞清楚在此class之上进行“成员逐一初始化”的行为模式是否适当?如果答案是肯定的,我们就不需要另外提供copy constructor;如果答案是否定的,我们就必须另行定义copy constructor,并在其中编写正确的初始化操作。
注:如果有必要为某个class编写copy constructor,那么同样有必要为它编写copy assignment operator。
4.3何谓mutable(可变)和const(不变)(What Are mutable and const?)
int sum(const Triangular &trian) {
int beg_pos = trian.beg_pos();
int length = trian.length();
int sum = 0;
for (int i = 0; i < length; ++i)
sum += trian.elem(beg_pos + i);
return sum;
}
trian是个const reference参数
,因此编译器必须保证参数trian在sum()之中不会被修改。
对象不能被修改,也就是说它的data member不能被修改。
但是,上面代码中sum()所调用的任何一个member function都有可能更改trian的值(即可能更改trian对象的data member)。
为了确保trian对象不被更改,编译器必须确保beg_pos()、length()、elem()都不会更改其调用者。(在这里,它们的调用都都是trian。)
class设计必须在member function身上标注const,以成为const member function
,以此告诉编译器:这个member function不会更改class object的内容:
class Triangular {
public:
// 以下是const member function
int length() const { return _length; }
int beg_pos() const { return _beg_pos; }
int elem(int pos) const;
//以下是non-const member function
bool next(int &val);
void next_reset() { _next = _beg_pos - 1; }
// ...
private:
int _length;
int _beg_pos;
int _next;
static vector<int> _elems;// 稍后会说明static data member
};
如果是一个const member function,那就必须同时在声明与定义中指定const。例如:
int Triangular::elem(int pos) const {
return _elems[pos - 1];
}
编译器会检查每个声明为const的member function,看看它们是否真的没有更改class object内容。
如下next()声明为const member function,会产生编译错误:
bool Triangular::next(int &value) const {
if (_next < _beg_pos + _length - 1) {
value = _elems[_next++];// 错误:更改了_next这个data member
return true;
}
return false;
};
很显然,它会更改其调用者。所以编译错误!
看下面这段代码:
class val_class {
public:
val_class(const BigClass &v): _val(v) {}
BigClass& val() const { return _val;}
private:
BigClass _val;
};
val()没有直接修改_val,但它返回的是一个non-const reference指向_val,实际上等于将_val开放出去,允许程序在其他地方加以修改。所以以上代码会产生问题!
解决这个问题的方法:提供两份定义,➀const版本;➁non-const版本。
class val_class {
public:
const BigClass& val() const { return _val; }// 注意:两个const
BigClass& val() { return _val; }
// ...
};
non-const class object会调用non-const版的val(),于是对象内容被改变也没关系;
const class object则会调用const版的val(),那就不可能改变对象内容。
例子:
void example(const BigClass *pbc, BigClass &rbc) {
pbc->val();// 这会调用const版本
rbc.val();// 这会调用non-const版本
}
设计class时,鉴定其const member function是一件很重要的事。
要知道,没有一个const reference class参数可以调用公开接口的non-const成分(但目前许多编译器对此情况都只给警告)。
Mutable Data Member(可变的数据成员)
int sum(const Triangular &trian) {
if (!trian.length())
return 0;
int val, sum = 0;
trian.next_reset();
while (trian.next(val))
sum += val;
return sum;
}
注意到参数有const
修饰,说明trian是个const object
,它的的data member不可变!
然而,next_reset()和next()都会更改data member——_next的值,它们都不是const member function
。但它们却被trian调用,于是造成错误!
关于object的data member和const修饰:
如_length和beg_pos提供了数列的抽象属性。如果改变trian的长度或起始位置,形同改变其性质,将和未改变前的状态不再相同。所以const修饰的object的data member是不可更改的。
对于上面代码,_next只是用来让我们得以实现出iterator机制,它本身不属于数列抽象概念的一环。改变_next的值,从意义上来说,不能视为改变class object的状态,或者说不算是破坏了对象的常量性(constness)。处于这种情况,关键字mutable
就发明出来了。只要将_next标示为mutable
,就是宣称:对_next所做的改变并不会破坏class object的常量性。
class Triangular {
public:
bool next(int &val) const;
void next_reset() const {
_next = _beg_pos - 1;
}
// ...
private:
mutable int _next;
int _beg_pos;
int _length;
};
现在,next()和next_reset()既可以修改_next的值,又可以被声明为const member function。这样一来,前面的sum()实现就没有问题了,否则就是代码有错误。
4.4什么是this指针(What Is the this Pointer?)
假设有以下两个object:
Triangular tr1(8);
Triangular tr2(8, 9);
设计copy()函数,调用如下,它的返回值为被复制出来的对象:
tr1.copy(tr2);
将tr2的长度及起始位置赋值给tr1。
以下是copy()的一份实现:
Triangular& Triangular::copy(const Triangular &rhs) {
_length = rhs._length;
_beg_pos = rhs._beg_pos;
_next = rhs._beg_pos - 1;
return ???; //应该返回什么呢
}
对于tr1.copy(tr2);而言,就是rhs绑定至tr2。_length指向tr1内的相应成员。
我们还需要一种可以指向tr1整个对象的方法,this
指针就扮演了这样的角色。
this指针系在member function内用来指向其调用者(一个对象)。
本例中,this指向tr1。这是怎么办到的呢?
内部工作过程如下:
// 伪码(Pseudo Code): member function被转换后的结果
Triangular& Triangular::copy(Triangular *this, const Triangular &rhs) {
this->_length = rhs._length;
this->_beg_pos = rhs._beg_pos;
this->_next = rhs._beg_pos - 1;
}
tr1.copy(tr2);// 原始调用
copy(&tr1, tr2);// 内部的程序代码转换,tr1变成由this指针所指的对象
欲以一个对象复制出另一个对象,先确定两个对象是否相同是个好习惯。
Triangular& Triangular::copy(const Triangular &rhs) {
if (this != &rhs) {// 检查两个对象是否相同(比较的是地址是否相同)
_length = rhs._length;
_beg_pos = rhs._beg_pos;
_next = rhs._beg_pos - 1;
}
return *this;// 返回复制的对象,也即是被复制后的调用者
}
4.5静态类成员(Static Class Members)
如果我们的class只需要唯一的容器来储存数列元素。关键字static
为我们解决了问题。
static data member用来表示唯一的、可共享的member。它可以在同一类的所有对象中被访问。
class Triangular {
public:
// ...
private:
static vector<int> _elems;
};
对class而言,static data member只有唯一的一份实体。这种定义看起来很像全局对象(global object)的定义。
由于_elems属于class而非某个对象,所以这样访问它:
vector<int> Triangular::_elems;
// 指定初值
int Triangular::_initial_size = 8;
在class member function内访问static data member,和一般data member一样访问:
Triangular::Triangular(int len, int beg_pos):
_length(len>0?len:1),_beg_pos(beg_pos>0?beg_pos:1) {
_next = _beg_pos - 1;
int elem_cnt = _beg_pos + _length - 1;
if (_elems.size() < elem_cnt)
gen_elements(elem_cnt);
}
像buf_size这类的const static int data member
,可以在声明时为其明确指定初值:
class intBuffer {
public:
// ...
private:
static const int _buf_size = 1024;// ok
int _buffer[_buf_size];// ok
};
Static Member Function(静态成员函数)
// 给定某值,它会依据该值是否在Triangular数列内而返回true或false
bool Triangular::is_elem(int value) {
if (!_elems.size() || _elems[_elems.size() - 1] < value)
gen_elems_to_value(value);
vector<int>::iterator found_it;
vector<int>::iterator end_it = _elems.end();
found_it = find(_elems.begin(), end_it, value);
return found_it != end_it;
}
一般情形下,member function必须通过其类的某个对象来调用。这个对象会被绑定至该member function的this指针。通过储存于每个对象中的this指针,member function才能够访问储存于每个对象中的non-static data member。这个this指针隐式代表左操作数。
然而,上述is_elem()并未访问任何non-static data member。它的工作和任何对象都没有任何关联,因而应该声明该函数为static
的。static member function
便可以在这种“与任何对象都无瓜葛”的情形之下被调用。
if (Triangular::is_elem(8)) ...
注意,member function只有在“不访问任何non-static member”的条件下才能够被声明为static。声明方式是在声明之前加上关键字static:
class Triangular {
public:
static bool is_elem(int);
static void gen_elements(int length);
static void gen_elems_to_value(int value);
static void display(int length, int beg_pos, ostream &os = cout);
// ...
private:
static const int _max_elems = 1024;// Visual C++不允许在此直接指定初值
static vector<int> _elems;
};
在class主体外部进行member function的定义时,要加static吗?答:无须重复加上关键字static,并且这个规则也适用于static data member。
void Triangular::gen_elems_to_value(int value) {// 最前方无须再加关键字static
int ix = _elems.size();
if (!ix) {// Triangular数列:1,3,6,10,15,21,28,36…
_elems.push_back(1);
ix = 1;
}
while (_elems[ix - 1] < value && ix < _max_elems) {
++ix;
_elems.push_back(ix * (ix + 1) / 2);
}
if (ix == _max_elems)
cerr << "Triangular Sequence: opps: value too large "
<< value << " -- exceeds max size of "
<< _max_elems << endl;
}
看一个完整例子:
#include <iostream>
#include "Triangular.h"
using namespace std;
int main() {
char ch;
bool more = true;
while (more) {
cout << "Enter value: ";
int ival;
cin >> ival;
bool is_elem = Triangular::is_elem(ival);
cout << ival
<< (is_elem ? " is" : " is not ")
<< "an element in the Triangular series.\n"
<< "Another value?(y/n)";
cin >> ch;
if (ch == 'n' || ch == 'N')
more = false;
}
};
4.6打造一个Iterator Class(Building an Iterator Class)[运算符重载]
我们体验一下如何实现一个iterator class。我们必须提供以下操作方式:
Triangular trian(1, 8);
Triangular::iterator it = trian.begin(), end_it = trian.end();
while (it != end_it) {
cout << *it << ' ';
++it;
}
为了让上述程序代码得以工作,我们必须为此iterator class定义!=、*、++等运算符。
我们可以像定义member function那样来定义运算符。唯一差别是:它不用指定名称,只需在运算符前面加上关键字operator
即可。例如:
class Triangular_iterator {
public:
// 为了不要在每次访问元素时都执行-1操作,此处将_index的值设为index-1
Triangular_iterator(int index): _index(index - 1){}
bool operator==(const Triangular_iterator&) const;
bool operator!=(const Triangular_iterator&) const;
int operator*() const;
Triangular_iterator& operator++();// 前置(prefix)版
Triangular_iterator operator++(int);// 后置(postfix)版
private:
void check_integrity() const;
int _index;
};
Triangular_iterator维护一个索引值,用以索引Triangular中用来储存数列元素的那个static data member,也就是_elems。为了达到这个目的,Triangular必须赋予Triangular_iterator的member function特殊的访问权限。(friend机制就能给予这种特殊权限。)如果两个Triangular_iterator对象的_index相等,我们便说这两个对象相等:
inline bool Triangular_iterator::operator==(const Triangular_iterator &rhs) const {
return _index == rhs._index;
}
inline bool Triangular_iterator::operator!=(const Triangular_iterator &rhs) const {
return !(*this == rhs)
}
运算符嘛,可以直接作用于其class object:
if (trian1 == trian2) ...
if (train1 != trian2) ...
如果将运算符作用于指针所指的对象,就得先提领该指针,取出其所指对象:
if (*ptri1 == *ptri2) ...
if (*ptri1 != *ptri2) ...
运算符定义方式,就像member function一样:
inline int Triangular_iterator::operator*() const {
check_integrity();// private member
return Triangular::_elems[_index];
}
也可以像non-member function一样:
inline int operator*(const Triangular_iterator &rhs) {
rhs.check_integrity();
return Triangular::_elems[_index];
}
注意:由于以上是一个non-member function,所以不具有访问non-public member的权力。
non-member运算符
的参数列表中,一定会比相应的member运算符
多出一个参数,也就是this指针
。对member运算符
而言,这个this指针隐式代表左操作数。
关于成员函数(member function)与非成员函数(non-member function):
class A { public: void f1(){ }; // 这个就是成员函数。 int f2(); // 这个也是成员函数声明,其实现在类的外部。 };// 此分号不可省略 int A::f2() { return 2; } // 这个是成员函数的实现。 void f3() { } // 这个就是非成员函数,它不属于A,也不属于任何其他的类。
(注:以上代码在VS2019编辑器里是不报错没有任何问题的!)
member——>类成员——>class{}体里面定义的。
non-member——>非类成员——>class{}体之外定义的。
下面check_integrity()成员函数可以确保_index不大于_max_elems,并确保_elems储存了必要的元素:
inline void Triangular_iterator::check_integrity() const {
if (_index >= Triangular::_max_elems)
throw iterator_overflow();
if (_index >= Triangular::_elems.size())
Triangular::gen_elements(_index + 1);
}
接下拉必须提供increment(递增)运算符的前置(++trian)和后置(trian++)两个版本:
// 前置版本。由于可以作为运算符的左值,所以一般返回引用
inline Triangular_iterator& Triangular_iterator::operator++() {
++_index;
check_integrity();
return *this;
}
后置版本参数列表原本也是空的,但重载规则要求参数列表必须独一无二。因此,C++语言要求后置版本有一个int参数:
// 后置版本。运算符返回的是临时变量,所以它不能返回引用,也是防止如a++++这样的语句产生
inline Triangular_iterator Triangular_iterator::operator++(int) {
Triangular_iterator tmp = *this;
++_index;
check_integrity();
return tmp;// 返回原对象,只是数据成员更新了数据
}
对于后置版本,其唯一的那个int参数从何发生,又到哪去了呢?事实是,编译器会自动为后置版本产生一个int参数,并且其值必为0。所以用户不必为此烦恼。
increment(递增)或decrement(递减)运算符的前置及后置版本都可直接作用于其class object:
++it;// 前置版
it++;// 后置版
为什么++前置版本返回对象引用,而++后置版本返回对象?
先看一个完整的例子(代码都在同一个xyz.cpp文件中):
#include <iostream>
using namespace std;
class Clock {// 时钟类定义
public:
Clock(int hour = 0, int minute = 0, int second = 0);
void showTime() const;
Clock& operator++();// 前置单目运算符重载
Clock operator++(int);// 后置单目运算符重载
private:
int hour, minute, second;
};
Clock::Clock(int hour, int minute, int second) {
if (0 <= hour && hour < 24 && 0 <= minute && minute < 60
&& 0 <= second && second < 60) {
this->hour = hour;
this->minute = minute;
this->second = second;
} else
cout << "Time error!" << endl;
}
void Clock::showTime() const {// 显示时间
cout << hour << ":" << minute << ":" << second << endl;
}
// 前置++
Clock& Clock::operator++() {
second++;
if (second >= 60) {
second -= 60;
minute++;
if (minute >= 60) {
minute -= 60;
hour = (hour + 1) % 24;
}
}
return *this;
}
// 后置++
Clock Clock::operator++(int) {
Clock old = *this;
++(*this);// 调用前置“++”运算符
return old;
}
int main() {
Clock myClock(23, 59, 0);
cout << "First time output: ";
myClock.showTime();
cout << "Show myClock++: ";
(myClock++).showTime();
/*(myClock++++++).showTime();*/
cout << "Show ++myClock: ";
(++myClock).showTime();
/*(++++++myClock).showTime();*/
return 0;
}
First time output: 23:59:0
Show myClock++: 23:59:0
Show ++myClock: 23:59:2
解注释并注释相应上一行的代码,运行结果是:
First time output: 23:59:0
Show myClock++: 23:59:0
Show ++myClock: 23:59:4
对于第一次运行结果,由于我们知道,i++是在下一次被调用时才自增+1,以上运行结果与原义是相符的。相当于i++,++i,所以时钟增加了2秒。
对于第二次运行结果,myClock++++++;最终等同于myClock++;即时钟增加1秒,所以这里就有问题了,其实后置版本应该返回"const Clock",这可以防止形如"a++++"的用法,因为这种用法本身就不提倡甚至有些编译器不通过。对于++++++myClock;使时钟增加了3秒,共增加4秒,如结果所示。
Clock Clock::operator++(int)这个函数表示后置++,函数内部定义了一个局部变量old,在其生存期结束时会被释放掉,故不能使用返回引用的定义,否则会指向被释放掉的内存空间中的不确定的值。无论重复使用多少次后置++运算,也只会改变一次原有myClock对象的值(例如myClock++++++;因为只有第一次++时*this表示myClock对象,后面都是old对象,就算使用引用也是一样的,更何况不使用引用就无法连续赋值)。
仔细看下后置++代码:
Clock Clock::operator++(int) {
Clock old = *this;
++(*this);// 调用前置“++”运算符
return old;
}
第一次myClock++;函数体内++(*this);这行代码调用了前置++,即{second++;} 这时时钟秒+1,正确。此时,Clock c0 == old == *this,紧接着继续调用这时的Clock对象是c0,这时this就是调用者c0,但返回对象是一个复制操作,c0只是一个拷贝对象,它里面当然是有second这样的成员变量,但它已经不是以前的那个对象了,它是一个副本,所以再次紧接着调用就是副本的调用,影响不到原来那个对象的data member了。
Clock& Clock::operator++()这个函数表示前置++,返回的是当前对象的引用,故多次调用前置++运算符时,会一直对对象myClock进行赋值。如果取消引用符号,(++++++myClock).show还是会显示三次自增后的结果,究其原因是保存的一个临时变量副本,但myClock依然是只有一次自增的结果,这种情况充分说明了引用&能使返回对象为左值。要想能连续自增或比如重载运算符<<:cout<<obj1<<obj2,必须返回引用。
运算符的重载规则:
- 不可引入新的运算符。
- 除了(点操作符 .)、(指向成员操作的操作符 ->*,.*)、(作用域操作符 :: ) 、(三元运算符 ?: )、(sizeof),其他运算符皆可被重载。
- 运算符的操作数(operand)个数不可改变。每个二元运算符都需要两个操作数,每个一元运算符都需要恰好一个操作数。
- 运算符函数的参数列表中,必须至少有一个参数为class类型。也就是说,无法为诸如指针之类的non-class类型重新定义其原已存在的运算符。
接下来,为Triangular提供一组begin()/end() member function,并支持前述iterator定义。
首先看看我们必须对Triangular做的修正:
#include "Triangular_iterator.h"
class Triangular {
public:
// 以下这么做,可以让用户不必知晓iterator class的实际名称
typedef Triangular_iterator iterator;
Triangular_iterator begin() const {
return Triangular_iterator(_beg_pos);
}
Triangular_iterator end() const {
return Triangular_iterator(_beg_pos + _length);
}
// ...
private:
int _beg_pos;
int _length;
// ...
};
嵌套类型(Nested Type)[typedef]
typedef
可以为某个类型设定另一个不同的名称。其通用形式为:
typedef existing_type new_name;
其中existing_type可以是任何一个内置类型、复合类型或class类型。
根据上面的代码,以下是定义一个iterator object的语法:
Triangular::iterator it = trian.begin();
下面代码的使用对不对?
iterator it = trian.begin();
答:错误!因为我们得使用class scope(类作用域)运算符(::)来指引编译器,让它在面对iterator这个字眼时,查看Triangular内部提供的定义。
4.7合作关系必须建立在友谊的基础上(Collaboration Sometimes Requires Friendship)[friend-友元函数和友元类]
inline int operator*(const Triangular_iterator &rhs) {
rhs.check_integrity();// 直接访问private member
return Triangular::_elems[rhs.index()];// 直接访问private member
}
以上的non-member operator*()会直接访问Triangular的private
_elems以及Triangular_iterator的private
check_integrity()。
以上能访问private
成员,是因为具备了与class member function相同的访问权限,这样就可以访问class的private member了。怎么具备呢?就是friend
关键字的作用。
为了让operater*()通过编译,应该在class Triangular和Triangular_iterator中将operator*()声明为“朋友”:
class Triangular {
friend int operator*(const Triangular_iterator &rhs);
// ...
};
class Triangular_iterator {
friend int operator*(const Triangular_iterator &rhs);
// ...
};
只要在某个函数原型(prototype)前加上关键字friend
,就可以将它声明为某个class的friend。这份声明可以出现在class定义的任意位置上,不受private或public的影响,如:
Triangular_iterator内的operator*()和check_integrity()都需要访问Triangular_iterator的private member,因此我们将两者都声明为Triangular的friend。
class Triangular {
// 可以不放在private或public下
friend int Triangular_iterator::operator*();// 要访问private,故friend
friend void Triangular_iterator::check_integrity();// 要访问private,故friend
//...
public:
//...
private:
//...
};
为了让上述定义成功通过编译,我们必须在上述两行之前,先提供Triangular_iterator的定义让Triangular知道。否则编译器就没有足够的信息可以确定上述两个函数原型是否正确,也无法确定它们是否的确是Triangular_iterator的成员函数。
class之间的友谊
我们也可以令class A与class B建立friend关系,借此让classA的所有成员函数都成为class B的friend。
例如:
class Triangular {
friend class Triangular_iterator;
// ...
};
以上造成Triangular_iterator的所有成员函数都成为Triangular的friend。上例的两行代码就可以省略了。
如果以这种形式来声明class间的友谊,就不需要在友谊声明之前先显现class的定义。
我们也并非一定得以friend方式达到目的。如果Triangular提供一个公有成员函数来访问_max_elems,以及另一个公有成员函数来返回_elems的当前大小,那么check_integrity()就不再需要主动建立友谊。像这样:
class Triangular {
public:
static int elem_size() { return _elems.size(); }
static int max_elems() { return _max_elems; }
// ...
// 友谊不再必要
inline void Triangular_iterator::check_integrity() {
if (_index >= Triangular::max_elems())
throw iterator_overflow();
if (_index >= Triangular::elems.size())
Triangular::gen_elements(_index + 1);
}
};
友谊的建立,通常是为了效率考虑。例如在某个非成员运算符函数中进行Point和Matrix的乘法运算。
如果我们只是希望进行某个数据成员的读取和写入,那么,为它提供具有public访问权限的inline函数,这就是建立友谊之外的一个替代方案。
以下程序用来练习先前的iterator class:
int main() {
Triangular tri(20);
Triangular::iterator it = tri.begin();
Triangular::iterator end_it = tri.end();
cout << "Triangular Series of " << tri.length() << " elements\n";
cout << tri << endl;
while (it != end_it) {
cout << *it << ' ';
++it;
}
cout << endl;
}
Triangular Series of 20 elements
1 3 6 10 15 21 28 36 45 55 66 78 91 105 120 136 153 171 190 210
4.8实现一个copy assignment operator(Implementing a Copy Assignment Operator)[复制赋值运算符]
默认情况下,将某个class对象赋值给另一个,如下:
Triangular tri1(8), tri2(8, 9);
tri1 = tri2;
class的数据成员会被依次复制过去。在我们的例子中,_length、_beg_pos、_next都会从tri2被复制到tri1。这称为default memberwise copy
(默认的成员逐一复制操作)。
对于之前定义的Matrix类,这种default memberwise copy
行为便不正确。
如下是Matrix类,之前用复制构造函数解决了逐一赋值的问题,但没有解决通过赋值运算符的解决方案。
class Matrix {
public:
Matrix(int row, int col): _row(row), _col(col) {
// constructor进行资源的分配
// 此处未检查成功与否
_pmat = new double[row * col];
}
~Matrix() {// destructor进行资源的释放
delete[] _pmat;
}
private:
int _row, _col;
double* _pmat;
};
Matrix需要一个copy constructor(上述已讨论过)和一个copy assignment operator。以下便是为Matrix的copy assignment operator所做的定义:(基本和copy constructor类同)
Matrix& Matrix::operator=(const Matrix &rhs) {
if (this != &rhs) {
_row = rhs._row;
_col = rhs._col;
int elem_cnt = _row * _col;
delete[] _pmat;
_pmat = new double[elem_cnt];
for (int i = 0; i < elem_cnt; ++i)
_pmat[i] = rhs._pmat[i];
}
return *this;
}
4.9实现一个function object(Implementing a Function Object)
上一章节已经看到了标准库事先定义的函数对象
。
所谓函数对象
就是一个“提供有函数调用运算符
”的类。
当编译器在编译过程中遇到函数调用,例如:
lt(ival);
lt可能是函数名称,可能是函数指针,也可能是一个提供了函数调用(function call)运算符的函数对象(function object)。
如果lt是个类对象,编译器便会在内部将此语句转换为:
lt.operator(ival);// 内部转换结果
函数调用运算符
可接受任意个数的参数。例如,它可被用来支持Matrix的多维下标(subscripting)操作,因为语言所提供的subscript运算符仅能接受一个参数。
下面实现一个function call运算符,测试传入值是否小于某指定值。此class的每个对象都必须被初始化为某基值。此外,也提供该基值的读取及写入操作。实现如下:
class LessThan {
public:
LessThan(int val): _val(val) { }
int comp_val() const { return _val; }// 基值的读取
void comp_val(int nval) { _val = nval; }// 基值的写入
bool operator()(int _val) const;
private:
int _val;
};
其中的function call运算符
实现如下:
inline bool LessThan::operator()(int value) const {
return value < _val;
}
定义LessThan object的方式和定义一般对象是一样的:
LessThan lt10(10);
将function call运算符
应用于对象身上,便可以调用function call运算符
:
int count_less_than(const vector<int> &vec, int comp) {
LessThan lt(comp);
int count = 0;
for (int i = 0; i < vec.size(); ++i)
if (lt(vec[i]))
++count;
return count;
}
通常把函数对象(function object)当作参数传给泛型算法,例如:
void print_less_than(const vector<int> &vec, int comp, ostream &os = cout) {
LessThan lt(comp);
vector<int>::const_iterator iter = vec.begin();
vector<int>::const_iterator it_end = vec.end();
os << "elements less than " << lt.comp_val() << endl;
while ((iter = find_if(iter, it_end, lt)) != it_end) {
os << *iter << ' ';
++iter;
}
}
4.10重载iostream运算符(Providing Class Instances of the iostream Operators)
如果我们想显示trian对象的内容,可能会希望这样写:
cout << trian << endl;
为支持上述形式,我们必须另外提供一份重载的output运算符:
ostream& operator<<(ostream &os, const Triangular &rhs) {
os << "(" << rhs.beg_pos() << ", " << rhs.length() << " )";
rhs.display(rhs.length(), rhs.beg_pos(), os);
return os;
}
传入函数的ostream对象又被原封不动地返回,如此一来便可以串接多个output运算符。
参数ostream对象并未声明为const,因为每个output操作都会更改ostream对象的内部状态。
至于rhs这种被输出的对象,被声明为const——因为这里之所以使用传址方式,是基于效率考虑而非为了修改其对象内容。
为什么不把output运算符设计为一个成员函数呢?
因为作为一个成员函数,其左操作数必须是隶属于同一个class的对象。如果output运算符被设计为tri 类成员函数,那么tri 对象就必须被放在output运算符的左侧:
tri << cout << '\n';
这种奇怪的形式必定对class用户造成困惑!
再来看input运算符重载:
istream& operator>>(istream &is, Triangular &rhs) {
char ch1, ch2;
int bp, len;
// 假设输入为(3,6) 6 10 15 21 28 36
// 那么ch1=='(', bp==3, ch2==',', len==6
is >> ch1 >> bp >> ch2 >> len;
// 设定rhs的三个data member
rhs.beg_pos(bp);
rhs.length(len);
rhs.next_reset();
return is;
}
void main() {
Triangular tri(6, 3);
cout << tri << '\n';
Triangular tri2;
cin >> tri2;
cout << tri2;
(3,6) 6 10 15 21 28 36
|(4,10)
(4,10) 10 15 21 28 36 45 55 66 78 91
4.11指针、指向Class Member Function(Pointers to Class Member Functions)
实现出一个通用的数列类num_sequence,使其对象可同时支持Fibonacci、Pell、Lucas、Square、Pentagonal这六种数列。
以下是测试用main():
int main() {
num_sequence ns;
const int pos = 8;
for (int i = 1; i < num_sequence::num_of_sequence(); ++i) {
ns.set_sequence(num_sequence::ns_type(i));
int elem_val = ns.elem(pos);
display(cout, ns, pos, elem_val);
}
}
The element at position 8 for fibonacci sequence is 21
The element at position 8 for pell sequence is 408
The element at position 8 for lucas sequence is 47
The element at position 8 for triangular sequence is 36
The element at position 8 for square sequence is 64
The element at position 8 for pentagonal sequence is 92
ns是一个通用型数列对象。在for循环迭代过程中,利用set_sequence(),根据ns_type()返回的不同数列的代码,将ns重新设置。num_of_sequence()返回目前支持的数列种类个数。num_of_sequence()和ns_type()皆为inline static member function。elem()返回特定位置的元素值。
num_sequence的设计关键在于,pointer to member function
(指向成员函数的指针)机制的运用。这种指针看起来和pointer to non-member function极为相似。不过,pointer to member function
还得指定它所指的究竟是哪一个class。例如:
void (num_sequence::*pm)(int) = 0;
以上便是将pm声明为一个指针,指向num_sequence的member function。
它的返回类型必须是void,并只接受单一参数,参数类型为int。
pm的初始值为0,表示它目前并不指向任何member function。
我们也可以通过typedef
加以简化:
typedef void (num_sequence::*PtrType)(int);
PtrType pm = 0;
注意,六个数列,除了元素的算法不同外,其余都相同。
num_sequence提供以下六个member function,每一个都可由PtrType指针加以定位:
class num_sequence {
public:
typedef void (num_sequence::*PtrType)(int);
// _pmf可指向下列任何一个函数
void fibonacci(int);
void pell(int);
void lucas(int);
void triangular(int);
void sequare(int);
void pentagonal(int);
// ...
private:
PtrType _pmf;
};
要取得某个member function的地址,对函数名应用address-of(取址)运算符。
注意,函数名称前必须先以class scope运算符加以限定,至于返回类型和参数列表皆不必指明。
如果要定义一个指针,并令它指向member function fibonacci(),我们可以这么写:
PtrType pm = &num_sequence::fibonacci;
每当调用set_sequence(),我们便指定_pmf值,令其指向前述六个member function之一。为求简化,我们可以将这六个member function的地址储存在一个static array中。为了避免重复计算每个数列元素,我们还维护一个static vector,内放六个vector,分别储存各个数列:
class num_sequence {
public:
typedef void (num_sequence::*PtrType)(int);
// ...
private:
vector<int>* _elem;// 指向目前所用的vector
PtrType _pmf;// 指向目前所有的算法(用以计算数列元素)
static const int num_seq = 7;
static PtrType func_tbl[num_seq];
static vector<vector<int> > seq;// 注:两个>符之间有个空格
};
static vector<vector<int> > seq;
seq是个vector,其中每个元素又是一个vector(代表一个数列),用来存放int元素。
注意:以上最后两个反尖括号之间有个空格,否则无法编译成功。
maximal munch编译规则:
每个符号序列(symbol sequence)总是以“合法符号序列”中最长的那个解释。因为>>是个合法的运算符序列,因此如果两个>之间没有空白,这两个符号必定会被合在一起看待。同样道理,如果写下a+++p,在maximal munch规则之下,它必定会被解释为a++ + p
接下来,我们必须提供每个static data member的定义。由于PtrType是个嵌套类型,所以在num_sequence之外对它所做的任何访问操作,都必须以class scope运算符加以限定。至于num_seq的值,已经在class定义中指定好了,这里不需要重复指定。
const int num_sequence::num_seq;
vector<vector<int> > num_sequence::seq(num_seq);
num_sequence::PtrType num_sequence::func_tbl[num_seq] = {
0,
&num_sequence::fibonacci,
&num_sequence::pell,
&num_sequence::lucas,
&num_sequence::triangular,
&num_sequence::square,
&num_sequence::pentagonal
};
_elem和_pmf在set_sequence()内一起被设定。设定好后,_elem指向存有数列元素的vector,_pfm则指向产生数列元素的member function。
Pointer to member function和pointer to function的一个不同点是,前者必须通过同一类的对象加以调用,而该对象便是些member function内的this指针所指之物。
假设有以下定义:
num_sequence ns;
num_sequence *pns = &ns;
PtrType pm = &num_sequence::fibonacci;
为了通过ns调用_pmf,可以这样写:
// 以下写法和ns.fibonacci(pos);相同
(ns.*pm)(pos);
// 以下写法和pns->fibonacci(pos);相同
(pns->*pm)(pos);
下面就是elem()的实现内容。如果用户所指定的位置是个合理值,而目前所储存的元素并未包含这个位置,那么就调用_pmf所指函数,产生新元素。
int num_sequence::elem(int pos) {
if (!check_integrity(pos))
return 0;
if (pos > _elem->size())
(this->*_pmf)(pos);
return (*_elem)[pos - 1];
}