Essential C++ 学习笔记 第四章

本书一共七章,读完这章就过半啦~这章的题目是基于对象的编程风格。介绍对象class的实现方法。

之前其实已经使用过一些class,比如说<string><vector>。使用方法是将string或者vector当做数据类型使用定义变量,而这个变量就被称为对象的实例,并且可以进行初始化。然后对实例进行函数操作。

函数的实现包括publicprivate两个部分。均包含操作函数和运算符,称为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进行初始化。那么matmat2的指针指向同一个地址,当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开放出去,允许程序在其他地方加以修改,所以程序编译时还是会出错。同时编译器也允许我们通过重载,实现constnon-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,在使用关键字之后,当前类的对象可以调用其他类中的成员函数,不分publicprivate。这种声明可以是针对某个函数的声明,也可以是针对整个类的声明。具体实例如下:

//声明某个函数为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

终于!连滚带爬的看完了!
下周见~

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值