本书一共七章,读完这章就过半啦~这章的题目是基于对象的编程风格。介绍对象class
的实现方法。
之前其实已经使用过一些class
,比如说<string>
和<vector>
。使用方法是将string
或者vector
当做数据类型使用定义变量,而这个变量就被称为对象的实例,并且可以进行初始化。然后对实例进行函数操作。
函数的实现包括public
和private
两个部分。均包含操作函数和运算符,称为member function
(成员函数)。用户只能调用class中的public
成员函数,并且常常只知道其原型声明,而不知道实现细节。这种函数被称为公开接口。面向对象的核心思想就是,在接口固定后,用户按照接口规则调用。当class
中的具体实现改变,而接口维持不变时,就不影响用户的程序的运行。从而能够将复杂的程序分解成多个模块编程,大大提高效率。
如何实现一个class
直接给出一个实现的例子:
//头文件Stack.h
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;
};
inline bool Stack::full()
{return _stack.size()==_stack.max_size();}
...
//Stack.cpp文件
bool Stack::pop(string &elem)
{
if(empty())
return false;
elem = _stack.back();
_stack.pop_back();
return true;
}
...
//main.cpp文件
void full_stack(Stack &stack,istream &is=cin)
{
string str;
while (is>>str && ! stack.full() )
stack.push(str);
cout<<"Read in"<<stack.size()<<"elements\n";
}
void main()
{
...
full_stack(stack);
}
具体用法其实在看这本书之前就已经学过很多次了,这里讲一些书中提到的细节。首先是inline
函数,如果在class
中直接实现函数,则默认为inline
函数。如果在函数外部实现inline
函数,则类似其他inline
函数,也必须放在头文件中。而其他函数的实现则可以放在.cpp文件中。在声明函数时,要用到::
号,这是class scope resolution
类作用域解析运算符,其实就是声明这个函数是属于这个类的。
之后是编程习惯的建议,首先是头文件,建议写成和类名称一致,即Stack.h
。同理,函数的实现文件,用Stack.cpp
。而data member
建议使用前下划线_stack
,避免和外部变量混淆。
构造函数和析构函数
这也是C++里面老生常谈的内容,构造函数在class实例初始化时运行,析构函数在free某个class的实例时运行。其中构造函数返回值为void
,并且支持重载。这里给出一种用法
class Triangular{
punlic:
Triangular();
Triangular(int len);
Triangular(int len,int beg_pos);
};
但注意调用的时候不能使用Triangular t5();
,因为C++必须兼容C,此时t5后面的小括号,会使t5被视为函数。正确的使用方法是Triangular t5
。而构造函数的具体实现形式如下:
Triangular :: Triangular()
{
_length = 1;
_beg_pos = 1;
_next = 0;
}
class Triangular{
public:
//也可以这样选择默认值
Triangular(int len = 1,int bp=1);
};
Triangular::Triangular(int len, int bp)
{
_length = len>0? len:1;
_beg_pos = bp>0? bp:1;
_next = _beg_pos-1;
}
另外一种初始化方式是使用成员初始化列表(Member Initialization List),具体使用方法如下:
Triangular::Triangular(const Triangular &rhs)
:_length(rhs._length),
_beg_pos(rhs._beg_pos),_next(rhs._beg_pos-1)
{}
这里的程序实体为空,初始化也并没有使用默认参数,而是通过冒号:
后的写法赋值,函数体为要赋值的变量名,括号内为要赋的数值。这一用法在字符串时比较常用,例如
class Triangular{
public:
//
private:
string _name;
int _next,_length,_beg_pos;
};
Triangular:Triangular(int len,int bp)
: _name("Triangular")
{
_length = len>0? len:1;
_beg_pos = bp>0? bp:1;
_next = _beg_pos-1;
}
当class的某个object
结束生命时,便会自动调用析构函数destructor
处理善后。主要用来释放在对象中分配的资源。析构函数的名称是class名称外加~
,同样没有返回值,但参数列表也必须是空的,并且不能重载。这里给出一个例子
class Matrix{
public:
Matrix(int row, int col)
: _row(row),_col(col)
{ //这里分配了一个数组的内存空间
_pmat = new double[row*col];
}
~Matrix()
{
delete [] _pmat;
}
private:
int _row,_pol;
double *_pmat;
};
//调用
Matrix mat(4,4);
//程序结束后自动调用析构函数
同时书中强调了,前面使用使用的以储值方式(by value)存放的变量_length
等,在程序运行结束之后会自动释放内存,从而不需要使用析构函数释放,用户需要了解什么时候需要使用析构函数释放。
成员逐一初始化
这里讨论的是对象object
快捷初始化的技巧,首先是默认的成员逐一初始化操作default memberwise intialization
,
Triangular tril(8);
Triangular tri2 = tril;
这里将tril
的所有成员变量全部分别赋值给了tri2
。这种赋值在正常情况下是非常方便的,但也有例外的时候,比如说如下:
Matrix mat(4,4);
//mat的构造函数作用
{
Matrix mat2 = mat;
//成员逐一初始化
//对mat2操作
//mat2的析构函数发挥作用
}
//mat的析构函数发挥作用
此时的成员逐一初始化,是对动态内存的指针_pmat
进行初始化。那么mat
和mat2
的指针指向同一个地址,当mat2
的析构函数生效时,matrix的数组空间已经被释放,在后面对mat
的数组进行使用时就会发生错误。为了避免这一问题,我们需要重写一个等效赋值的构造函数。具体实现如下:
Matrix::Matrix(const Matrix &rhs)
: _row(rhs._row), _col(rhs._col)
{
int elem_cnt = _row*_col;
_pmat = new double[elem_cnt];
for (int ix=0;ix<elem_cnt;++ix)
_pmat[ix] = rhs._pmat[ix];
}
就是说此时另外开辟了一块内存空间,将原始对象的数组中的值赋值了过来。用户在写程序时,需要自己判断,默认的成员逐一初始化是否会出现类似如上的问题。
何谓mutable(可变)和const(不变)
const
变量在结构化编程里面非常重要,但是对于const class
会出现更复杂的情形,比如:
int sum(const Triangular &trian)
{
int beg_pos = trian.beg_pos();
int length = trian.length();
int sum = 0;
for (int ix = 0;ix<length;++ix)
sum += triam.elem(beg_pos+ix);
return sum;
}
我们看到class作为const
被传入函数,其成员变量语法上不允许修改。但是此时无法保证class的内置函数不会修改trian
的值,为了保证const
的概念的统一性,需要在member function
上也标注const
,如下:
class Triangular{
public:
//const函数
int length() const {return _length;}
int beg_pos() const {return _beg_pos;}
int elem(int pos) const;
//非const函数
bool next(int &val)
void next_reset() {_next=_beg_pos-1;}
private:
int _length;
int _beg_pos;
int _next;
static vector<int> _elems;
};
成员函数的声明中,将const
关键字写在参数列表后面,另外const
也必须出现在实现部分:
int Triangular::elem(int pos) const
{ return _elems[pos-1];}
如果声明const
后,函数更改了输入参数的值,就会在编译时出错。而如下这种属于比较特殊的情况。
class val_class{
public:
val_class(const BigClass &v)
: _val(v) {}
BigClass &val() const {return _val;}
private:
BigClass _val;
};
上述问题看起来并没有问题,val()
并没有改变参数值。但是会返回一个non-const reference
指向val
,实际上相当于把_val
开放出去,允许程序在其他地方加以修改,所以程序编译时还是会出错。同时编译器也允许我们通过重载,实现const
和non-const
两个版本:
const BigClass &val() const {return _val;}
BigClass &val() {return _val;}
//调用时
void example(const BigClass *pbc, BigClass &rbc)
{
pbc->val();
rbc.val();
}
一段没看明白的说明"设计class时,鉴定其const member function是一件很重要的事情。如果你忘记这么做,要知道,没有一个const reference class参数可以调用公开接口中的non-const成分(但目前许多编译器对此情况都只给警告)。用户也许会大声咒骂。将const加到class内并非易事,特别是如果某个member function被广泛使用后。"
Mutable Data Member可变的数据成员
在声明输入的对象为const后,有时在逻辑上也希望一部分变量是可变的。那么就要用到mutable
这个关键字,具体使用场景如下:
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;
};
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;
}
int main()
{
Triangular tri(4);
cout << tri << "-- sum of elements:"
<< sum(tri) << endl;
}
我们可以看到函数next()
和next_reset()
被设置成为了const,但是实际上函数的调用过程中改变了成员变量_next
的值,所以我们将这个成员变量设计成mutable
之后,程序才能正常运行。
什么是this指针
首先直接给出用法:
Triangular tr1(8);
Triangular tr2(8,9);
tri.copy(tr2);
//程序实现
Triangular& Triangular::
copy( const Triangular &rhs)
{
if (this!=&rhs)
{
_length = rhs._length;
_beg_pos = rhs._beg_pos;
_next = _rhs._beg_pos-1;
}
return *this;
}
注意这里的return,返回了一个this的指针,注意到copy()
函数其实是类中的一个成员函数,这时的this
代表当前调用成员函数的对象,所以能够和返回值类型对应。在如上程序编译后,相当于如下:
Triangular& Triangular::
copy(Triangular *this, const Triangular &rhs)
{
this->_length = rhs._length;
this->_beg_pos = rhs._beg_pos;
this->_next = _rhs._beg_pos-1;
};
这里强调一个理解,在对象调用它的成员变量的时候,直接调用即可,编译器会自动将代码翻译成this->成员变量
的形式。
静态类成员
static
这个关键字之前就使用过,函数中标记这个关键字的变量在函数运行结束之后不会被销毁,在下次调用这个函数时,可以保留之前的值。对于class来说,这个关键字意味着所有的对象共享同一个数据static data member
。另外,我们必须在程序代码文件中提供其清楚的定义。
//头文件中
class Triangular{
public:
//
private:
static vector<int> _elems;
};
//定义文件.cpp中
vector<int> Triangular::_elems;
int Triangular::_initial_size = 8;
在类内访问含有static
的成员变量的方式,和访问一般的数据相同。同时,如果需要,我们也可以在声明时就给静态成员变量指定初值:
class intBuffer{
public:
//
private:
static const int _buf_size = 1024;
int _buffer[ _buf_size];
};
Static Member Function(静态成员函数)
除了class中的成员变量,成员函数也可以使用static
这个关键字,但是含义又不同。此处的含义是即使不将类给定对象,也可以直接调用类中的函数。这里给出了一个例子:
//声明部分,只需要添加static的关键字
class Triangular{
public:
static gen_elems_to_value(int value);
...
private:
static const int _max_elems = 1024; //部分编译器不允许这样赋初值
static vector<int> _elems;
}
//定义部分,static的关键字可以不写,这个规则也适用于静态成员变量
void Triangular::
gen_elems_to_value(int value)
{
...
}
//主程序
#include <iostream>
#include "Triangular.h"
using namespace std;
int main()
{
char ch;
bool more = true;
//没有声明对象
...
bool is_elem = Triangular::is_elem(ival);
//直接调用,只不过为了指定是Triangular类中的成员函数需要用::指定
}
运算符重载操作
4.6节打造一个Iterator Class的实际目的是介绍运算符重载。这里先给出用法:
class Triangular_iterator
{
public:
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++();
Triangular_iterator operator++(int);
private:
void check_intergrity() const;
int _index;
其中的operator==
就代表了==
的运算符重载,另外这里强调一个编程习惯,如果一个运算符合另外一个性质相反,通常会由后者实现前者。比如用==
实现!=
。另外运算符重载也是有规则的:
. .* :: ?:
四个不能重载- 运算符的操作数个数不能变,
==
只能有两个操作数 - 运算符的优先级不能变
- 运算符函数的参数列表中,至少有一个参数为class类型。我理解为写在class中,自然是针对类本身的操作符,所以必须包含,这是人为定义的。
运算符的声明如上,定义方式可以为member function
的,也可以是non-memeber function
的。如下:
inline int Triangular_iterator::
operator*() const
{
check_intergrity();
return Triangular::_elems[_index];
}
inline int
operator*(const Triangular_iterator &rhs)
{
rhs.check_intergrity();
return Triangular::_elems[_index];
}
区别在于non-member
的话,需要将对象作为输入参数,另外const
没有写在参数列表之后而是输入参数中。
另外一个知识点,前置和后置的++
运算符,想要实现重载,需要输入参数不同,为了加以区别,人为规定如下:
//前置++
inline Triangular_iterator& Triangular_iterator::
operator++()
{...
}
//后置++
inline Triangular_iterator& Triangular_iterator::
operator++(int)
{...
}
而后置++的输入参数,编译器会自动为其产生一个参数0,不需要额外的处理。
嵌套类型
其实是为了实现迭代器顺便介绍的,其实就是给变量类型起一个别名,类似如下:
class Triangular{
public:
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;
...
};
//调用
Triangular::iterator it = train.begin();
//最终想要实现的迭代器
Triangular trian(1,8);
Triangular::iterator
it = trian.begin();
end_it = trian.end();
while (it != end_it)
{
cout<<*it<<' ';
++it;
}
合作关系必须建立在友谊的基础上
神题目…总之就是讲友类的一节。使用的是关键字friend
,在使用关键字之后,当前类的对象可以调用其他类中的成员函数,不分public
和private
。这种声明可以是针对某个函数的声明,也可以是针对整个类的声明。具体实例如下:
//声明某个函数为friend
class Triangular{
friend int Triangular_iterator::operator*();
friend void Triangular_iterator::check_integrity();
...
};
//声明整个class为友类
class Triangular{
friend class Triangular_iterator;
...
};
同时书中还强调,友类的功能可以利用将函数公开到public
代替,具体使用哪种给用户提供了比较灵活的选择空间。
实现一个copy assignment operator
还是这个例子
Triangular tri1(8),tri2(8,9);
tri1 = tri2;
这里的等号,其实是对对象中所有的成员变量进行分别赋值,成为默认的成员逐一复制操作(default membewise copy)。类似前面讨论的,这种复制方式,对于class中有动态内存,只存储指针的情形,简单的复制值并不合适。解决办法是写了一个Matrix
类的构造函数。而这还不够方便,现在我们来将等号copy assignment operator
重载成为可以复制数组的形式。
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 ix=0; ix<elem_cnt; ++ix)
_pmat[ix] = rhs._pmat[ix];
}
return *this;
}
这样矩阵的复制也就可以直接使用等号操作了,会方便很多。
实现一个function object
function object
在Page85已经出现过,通过添加库#include <functional>
之后,可以调用plus<type>(),less<type>()
等函数,其实相当于对运算符的重载。而这些是C++中内置的,本文要自己实现一个function object
,具体实现如下:
class LessThan
{
public:
LessThan(int val) : _val(val) {} //构造函数,初始化为val
int comp_val() const {return _val;}
void comp_val(int nval) {_val=nval;}
bool operator() (int _value) const;
private:
int _val;
}
inline bool LessThan::
operator() (int value) const {return value<_val;}
这里的operator
进行了运算符重载,对应的是对象函数,具体使用方法如下:
int count_less_than(const vector<int> &vec, int comp)
{
LessThan lt(comp);
int count = 0;
for (int ix=0;ix<vec.size();++ix)
if(lt(vec[ix]))
++count;
return count;
}
这里的lt(vec[ix])
就是我们使用operator
重新定义的运算符的使用方式,除此之外,类LessThan
的使用方式和普通的类并无不同。另外,通常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;
}
}
这里用到了前一章的迭代器。
重载iostream运算符
是为了方便的执行如下命令:
cout<<trian<<endl;
这里trian
是一个类的对象,显然这并不能直接输出。需要人为对输出运算符进行重载
ostream& operator<<(ostream &os, const Triangular &rhs)
{
os<<"("<<rhs.beg_pos()<<", "
<<rhs.length() <<")";
rhs.display(rhs.length(),rhs.beg_pos(),os);
return os;
}
类似的对于输入,我们也可以进行重载,
istream&
operator >> (istream &is, Triangular &rhs)
{
char ch1,ch2;
int bp,len;
is >> ch1 >> bp;
>> ch2 >> len;
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;
}
文中强调了几点细节,首先,传入函数ostream
对象又被原封不动地返回,如此我们就可以串联多个>>
运算符。两个参数均以传址的方式传入,后者是为了提高传输效率,而不准备改变值,从而使用了const
。而前者,每个output
操作实际上会改变ostream
对象的状态,所以不能给成const
。
另外,注意到这里的重载并没有写入类中。这是因为作为一个member function
,其左操作数必须是隶属于同一个class对象。如果重载写在类内,output
运算符只能写成如下形式,这不符合我们平时的习惯。
tri << cout << '\n';
指针,指向Class Member Function
这段看得心态崩了一次。。。
首先函数指针在前面就已经出现过,在子函数中进行的运算希望用户能够控制时,就将运算封装成为一个函数,然后用函数指针作为输入值进行控制。这种技巧在class中也存在,但是指向class中的成员函数的指针有着额外的要求。
num_sequence ns;
num_seuqence *pns = &ns;
PtrType pm = &num_sequence::fibonacci;
(ns.*pm)(pos)
(pns->*pm)(pos)
其中num_sequence
是一个类,而fibonacci
为类中的成员函数。pm
保存了这个成员函数的指针,但是调用该指针时,并不能直接使用*pm
,而是必须指明这个函数指针指向哪个对象的成员函数。这不难理解,因为函数指针的赋值时,只声明了是类num_sequence
中的成员函数。
有了基础的用法,书中将本章中关于Triangular class
实现的内容泛化到所有的Fibonacci、Pell、Lucas、Square、Pentagonal几种数列。具体的类实现内容如下:
//.h头文件
class num_sequence{
public:
//void类型被重新定义了
typedef void (num_sequence::*PtrType)(int);
//准备让函数指针指向的成员函数
void fibonacci(int);
void pell(int);
void lucas(int);
void triangular(int);
void sequare(int);
void pentagonal(int);
//...
private:
vector<int>* _elem; //指向当前所用的数组,指向seq中的某个数列
PtrType _pmf; //指向目前所用的函数
static const int num_seq = 7; //数列总数
static PtrType func_tbl[num_seq]; //作为一个数列存储所有的函数指针
static vector<vector<int> > seq; //所有的数列的元素全部存储下来
};
//.c实现文件中
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
};
//给定数列中的某个位置后,返回对应的值,如果数组中还未存储,就先进行计算
int num_sequence:elem(int pos)
{
if ( ! check_integrity(pos))
return 0;
if ( pos>_elem->size())
(this->*_pmf)(pos);
return (*_elem)[pos-1];
}
//main函数
int main()
{
num_sequence ns;
const int pos=8;
//循环了所有的数列,并且都输出第8个数的大小
for (int ix=1;ix<num_sequence::num_of_sequence();++ix)
{
ns.set_sequence(num_sequence::ns_type(ix));
int elem_val = ns.elem(pos);
display(cout,ns,pos,elem_val);
}
}
其中能够同时处理多个数组的关键就是函数指针this->*_pmf
。
maximal munch编译原则
规则要求,每个符号序列symbol sequence
总是以“合法符号序列”中最长的解释。比如说
static vector<vector<int>> seq;
会在编译中出现错误,因为>>
也是一个合法的符号序列,这里编译器并没有识别成两个分别的右括号。又如
a+++p
实际上编译成
a++ + p
终于!连滚带爬的看完了!
下周见~