4-基于对象的编程风格

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,必须返回引用。

运算符的重载规则

  1. 不可引入新的运算符。
  2. 除了(点操作符 .)、(指向成员操作的操作符 ->*,.*)、(作用域操作符 :: ) 、(三元运算符 ?: )、(sizeof),其他运算符皆可被重载。
  3. 运算符的操作数(operand)个数不可改变。每个二元运算符都需要两个操作数,每个一元运算符都需要恰好一个操作数。
  4. 运算符函数的参数列表中,必须至少有一个参数为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_iteratorprivate 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];
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

itzyjr

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

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

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

打赏作者

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

抵扣说明:

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

余额充值