首页 > 代码库 > C++基础学习教程(七)----类编写及类的两个特性解析--->多态&继承

C++基础学习教程(七)----类编写及类的两个特性解析--->多态&继承

类引入

到目前为止我们所写的自定义类型都是关键字struct,从现在起我们将采用class方式定义类,这种方式对于学习过其他高级语言包括脚本(Such as Python)的人来说再熟悉不过了.

但是在写之前我们还是需要比较一下用struct和class之间有什么区别.

首先对于struct,在C兼容性方面很重要,尽管C++是有别于C的另一门语言,但许多程序还是必须与C交互,C++有两个重要功能,可以方便的与C交互.其中之一的就是POD,即是Plain Old Data(简单旧式数据)的缩写.

POD类型就是没有其他功能仅用于存储数据的类型.如内置类型就是POD类型,该类型没有其他功能.具体的int类型.一个既无构造函数也没有重载的赋值操作函数而仅有共有的POD类型作为数据成员的类也是一个POD类型.

POD类型的重要性在于,那些在C++库/第三方库的/操作系统接口中遗留的C函数需要POD类型.

在编写类之前,我们可以直接看一下class的例子,即是rational的最新版本,部分代码如下:

/** @file rational_class.cpp */
/** The Latest Rewrite of the rational Class */
#include <cassert>
#include <cstdlib>
#include <istream>
#include <ostream>
#include <sstream>

using namespace std;

/// Compute the greatest common divisor of two integers, using Euclid’s algorithm.
int gcd(int n, int m)
{
  n = abs(n);
  while (m != 0) {
    int tmp(n % m);
    n = m;
    m = tmp;
  }
  return n;
}

/// Represent a rational number (fraction) as a numerator and denominator.
class rational
{
public:
  rational(): numerator_(0), denominator_(1)  {}
  rational(int num): numerator_(num), denominator_(1) {}

  rational(int num, int den)
  : numerator_(num), denominator_(den)
  {
    reduce();
  }

  rational(double r)
  : numerator_(static_cast<int>(r * 10000)), denominator_(10000)
  {
    reduce();
  }

  int numerator()   const { return numerator_; }
  int denominator() const { return denominator_; }
  float as_float()
  const
  {
    return static_cast<float>(numerator()) / denominator();
  }

  double as_double()
  const
  {
    return static_cast<double>(numerator()) / denominator();
  }

  long double as_long_double()
  const
  {
    return static_cast<long double>(numerator()) / 
           denominator();
  }

  /// Assign a numerator and a denominator, then reduce to normal form.
  void assign(int num, int den)
  {
    numerator_ = num;
    denominator_ = den;
    reduce();
  }
private:
  /// Reduce the numerator and denominator by their GCD.
  void reduce()
  {
    assert(denominator() != 0);
    if (denominator() < 0)
    {
      denominator_ = -denominator();
      numerator_ = -numerator();
    }
    int div(gcd(numerator(), denominator()));
    numerator_ = numerator() / div;
    denominator_ = denominator() / div;
  }

  int numerator_;
  int denominator_;
};

/// Absolute value of a rational number.
rational abs(rational const& r)
{
  return rational(abs(r.numerator()), r.denominator());
}

/// Unary negation of a rational number.
rational operator-(rational const& r)
{
  return rational(-r.numerator(), r.denominator());
}

/// Add rational numbers.
rational operator+(rational const& lhs, rational const& rhs)
{
  return rational(
          lhs.numerator() * rhs.denominator() + rhs.numerator() * lhs.denominator(),
          lhs.denominator() * rhs.denominator());
}

/// Subtraction of rational numbers.
rational operator-(rational const& lhs, rational const& rhs)
{
  return rational(
          lhs.numerator() * rhs.denominator() - rhs.numerator() * lhs.denominator(),
          lhs.denominator() * rhs.denominator());
}

/// Multiplication of rational numbers.
rational operator*(rational const& lhs, rational const& rhs)
{
  return rational(lhs.numerator() * rhs.numerator(),
                  lhs.denominator() * rhs.denominator());
}

/// Division of rational numbers.
/// TODO: check for division-by-zero
rational operator/(rational const& lhs, rational const& rhs)
{
  return rational(lhs.numerator() * rhs.denominator(),
                  lhs.denominator() * rhs.numerator());
}

/// Compare two rational numbers for equality.
bool operator==(rational const& a, rational const& b)
{
  return a.numerator() == b.numerator() and a.denominator() == b.denominator();
}

/// Compare two rational numbers for inequality.
inline bool operator!=(rational const& a, rational const& b)
{
  return not (a == b);
}
/// Compare two rational numbers for less-than.
bool operator<(rational const& a, rational const& b)
{
  return a.numerator() * b.denominator() < b.numerator() * a.denominator();
}

/// Compare two rational numbers for less-than-or-equal.
inline bool operator<=(rational const& a, rational const& b)
{
  return not (b < a);
}
/// Compare two rational numbers for greater-than.
inline bool operator>(rational const& a, rational const& b)
{
  return b < a;
}

/// Compare two rational numbers for greater-than-or-equal.
inline bool operator>=(rational const& a, rational const& b)
{
  return not (b > a);
}

/// Read a rational number.
/// Format is @em integer @c / @em integer.
istream& operator>>(istream& in, rational& rat)
{
  int n(0), d(0);
  char sep('\0');
  if (not (in >> n >> sep))
    // Error reading the numerator or the separator character.
    in.setstate(in.failbit);
  else if (sep != '/')
  {
    // Push sep back into the input stream, so the next input operation
    // will read it.
    in.unget();
    rat.assign(n, 1);
  }
  else if (in >> d)
    // Successfully read numerator, separator, and denominator.
    rat.assign(n, d);
  else
    // Error reading denominator.
    in.setstate(in.failbit);

  return in;
}

/// Write a rational numbers.
/// Format is @em numerator @c / @em denominator.
ostream& operator<<(ostream& out, rational const& rat)
{
  ostringstream tmp;
  tmp << rat.numerator() << '/' << rat.denominator();
  out << tmp.str();

  return out;
}

类特性

在上面的代码中,有两个之前没有见过的关键字:public和private.其实学习过其他高级语言的话很容易看出这个是访问级别的限制声名符.顾名思义,public就是公共的访问级别,对外开放的,而private则不是,他不对用户开放,用户仅仅能通过public的方法或接口来访问或修改

既然介绍了class,就应该说一下面向对象的有关特征.

类主要包含动作和属性这两样东西.他们的区别在于,属性对于单个对象是独特的,而动作是属于同一类的所有对象多共享的.动作有时也被称为行为.在C++中,类描述了所有对象的行为或动作,以及属性类型.每个对象都有其自己的属性,并在类中枚举出来.在C++中,成员函数实现动作,并提供堆属性的访问机制,而数据成员存储属性.

面向对象编程其他的特点就是继承和多态.

关于继承

即是存在父类和子类(派生类),派生类继承了他的父类的非私有属性和行为.

Liskov置换原则.当派生类特化了一个基类的行为或者属性的时候,则代码中任何使用基类的地方代之以继承类的对象是等效的.简单的描述是:如果基类B和派生类D,则在任何调用B类型的一个对象环境里,可以无副作用的使用一个D类型对象置换之.

关于多态,即是变量的类型决定了它所包含的对象的类型.多态的变量可以包含众多的不同类型的对象的一个.特别的,一个基类的变量既可以指代一个该基类的对象,也可以指代由该基类派生的任意类型的一个对象.根据置换原则,可以使用基类变量编写代码,调用基类的任意成员函数,而改代码均会正常工作,无论该对象真正的/派生的类型.

现在就实际说明一下关于继承的编程方法.首先还是看代码,下面我们构造了一个work类,以及两个派生类book和periodical.

/** @file calss_inh.cpp */
/** Defining a Derived Class */

using namespace std;
class work
{
public:
  // 构造函数
  work() 
  : id_(), title_() 
  {}
  // 构造函数
  work(string const& id, string const& title) 
  : id_(id), title_(title) 
  {}
  // 内置方法
  string const& id()    
  const 
  { 
    return id_; 
  }

  string const& title()
  const 
  { 
    return title_; 
  }
// 数据成员
private:
  string id_;
  string title_;
};

// 子类 book
class book : public work
{
public:
  // 构造函数
  book() 
  : author_(), pubyear_(0) 
  {}
  // 构造函数
  book(string const& id, string const& title, string const& author,int pubyear)
  : work(id, title), author_(author), pubyear_(pubyear)
  {}

  // 内置方法 
  string const& author() 
  const 
  { 
    return author_;
  }

  int pubyear()
  const 
  { 
    return pubyear_; 
  }
// 数据成员
private:
  string author_;
  int pubyear_; ///< year of publication
};

// 子类periodical
class periodical : public work
{
public:
  periodical() 
  : volume_(0), number_(0), date_() 
  {}
  periodical(string const& id, string const& title, int volume,
             int number,string const& date)
  : work(id, title), volume_(volume), number_(number), date_(date)
  {}

  // 内置方法
  int volume()              
  const 
  { 
    return volume_; 
  }

  int number()              
  const 
  { 
    return number_; 
  }

  string const& date() 
  const 
  { 
    return date_; 
  }

  // 数据成员
private:
  int volume_;       ///< volume number
  int number_;       ///< issue number
  string date_;      ///< publication date
};

义类时候如果使用struct,则默认访问级别是public,若使用class,则默认访问级别是private.这些关键字也会影响到类.

上面的代码中每个类都有自己的构造函数,每个类也可以有自己的析构函数,所谓析构函数是指执行类的清理工作的函数.这个函数一样没有返回值,名字是~加上类名字.

如下的代码中,加入了析构函数,运行之后我们可以看出构造函数和析构函数在继承中的执行顺序.代码如下:

/** @file Destructors.cpp */
/** Order of Calling Destructors */
#include <iostream>
#include <ostream>

class base
{
public:
  base()  { std::cout << "base::base()\n"; }
  ~base() { std::cout << "base::~base()\n"; }
};

class middle : public base
{
public:
  middle()  { std::cout << "middle::middle()\n"; }
  ~middle() { std::cout << "middle::~middle()\n"; }
};

class derived : public middle
{
public:
  derived()  { std::cout << "derived::derived()\n"; }
  ~derived() { std::cout << "derived::~derived()\n"; }
};

int main()
{
  derived d;
}


结果如下:



如果没有手动编写析构函数,则编译器一样会自动生成一个短小的默认析构函数.在执行完析构函数体后,编译器会调用每个成员函数的析构函数,然后从最后派生的类开始调用所有基类的析构函数.下面一个示例代码,你能猜到输出是什么么?

/** @file Constructors_Destructors.cpp */
/** Constructors and Destructors */
#include <iostream>
#include <ostream>
#include <vector>

using namespace std;

class base
{
public:
    // 构造函数
    base(int value) 
    : value_(value) 
    { 
        cout << "base(" << value << ")\n"; 
    }
    // 构造函数
    base() 
    : value_(0) 
    { 
      cout << "base()\n"; 
    }
    // 构造函数 
    base(base const& copy)
    : value_(copy.value_)
    { 
      cout << "copy base(" << value_ << ")\n"; 
    }
    // 析构函数
    ~base() { cout << "~base(" << value_ << ")\n"; }
    // 
    int value() 
    const 
    { 
      return value_; 
    }

    base& operator++()
    {
      ++value_;
      return *this;
    }
    // 私有数据成员
private:
    int value_;
};

// 子类
class derived : public base
{
public:
    // 构造函数
    derived(int value)
    : base(value) 
    { 
      cout << "derived(" << value << ")\n"; 
    }
    // 构造函数
    derived() 
    : base() 
    { 
      cout << "derived()\n"; 
    }
    // 构造函数
    derived(derived const& copy)
    : base(copy)
    { 
        cout << "copy derived(" << value() << "\n"; 
    }
    // 析构函数
    ~derived() 
    { 
      cout << "~derived(" << value() << ")\n"; 
    }
};

// 方法
derived make_derived()
{
  return derived(42);
}

base increment(base b)
{
  ++b;
  return b;
}

void increment_reference(base& b)
{
  ++b;
}

int main()
{
  derived d(make_derived());
  base b(increment(d));
  increment_reference(d);
  increment_reference(b);
  derived a(d.value() + b.value());
}

结果如下(图片为反,希望可以自己调试自己思考一下结果).


tips:


具体错误如下:


说完继承,我们继续说类的另一个特性:

多态.

要实现多态只需要一个关键字,这个关键字会告诉编译器你需要多态,则编译器会神奇的实现多态.仅用一个派生类型的对象去初始化一个基类引用类型的变量,则编译后的代码会检查该对象的真实类型,并调用相应的函数,这个关键字是virtual.

下面一个例子示例了一个virtual的print函数,代码:

/** @file virtual_fun.cpp */
/** Calling the print Function */
#include <iostream>
#include <ostream>
#include <string>

using namespace std;

/** Adding a Polymorphic print Function to Every Class Derived from work */
class work
{
public:
  work() : id_(), title_() {}
  work(string const& id, string const& title) : id_(id), title_(title) {}
  virtual ~work() {}
  string const& id()    const { return id_; }
  string const& title() const { return title_; }
  virtual void print(ostream& out) const {}
private:
  string id_;
  string title_;
};

class book : public work
{
public:
  book() : author_(), pubyear_(0) {}
  book(string const& id, string const& title, string const& author,
       int pubyear)
  : work(id, title), author_(author), pubyear_(pubyear)
  {}
  string const& author() const { return author_; }
  int pubyear()               const { return pubyear_; }
  virtual void print(ostream& out) const
  {
    out << author() << ", " << title() << ", " << pubyear() << ".";
  }
private:
  string author_;
  int pubyear_; ///< year of publication
};

class periodical : public work
{
public:
  periodical() : volume_(0), number_(0), date_() {}
  periodical(string const& id, string const& title, int volume,
             int number,
 string const& date)
  : work(id, title), volume_(volume), number_(number), date_(date)
  {}
  int volume()              const { return volume_; }
  int number()              const { return number_; }
  string const& date() const { return date_; }
  virtual void print(ostream& out) const
  {
    out << title() << ", " << volume() << '(' << number() << "), " << date() << ".";
  }
private:
  int volume_;       ///< volume number
  int number_;       ///< issue number
  string date_; ///< publication date
};

void showoff(work const& w)
{
  w.print(cout);
  cout << '\n';
}

int main()
{
  book sc("1", "The Sun Also Crashes", "Ernest Lemmingway", 2000);
  book ecpp("2", "Exploring C++", "Ray Lischner", 2008);
  periodical pop("3", "Popular C++", 13, 42, "January 1, 2000");
  periodical today("4", "C++ Today", 1, 1, "January 13, 1984");

  showoff(sc);
  showoff(ecpp);
  showoff(pop);
  showoff(today);
}

结果如下:


上面的代码中,showoff函数无需知道book和periodical,只需关心w是work的一个引用.此处能够调用的函数必须在work类声明过.尽管如此,当showoff调用print时,他会根据该对象的真实类型是book还是periodical来调用相应的函数.

因为关键字virtual的原因,C++也把多态函数称作虚函数(virtual function).当某个函数定义为虚函数时候,它在其派生类中也保持虚函数的特性,因此不需要再派生类中使用virtual,但是依然推荐使用,这样可以方便阅读和识别.在每个派生类中,相应的虚函数必须是有相同的名字\相同的返回类型,并且参数的个数及类型也要相等.

派生类也可不实现某个虚函数,此时他会把基类的函数像非虚函数一样继承下来.而如果派生类实现了虚函数,则称为覆盖(override)函该数,因为派生类的行为覆盖了本应该继承于基类的行为.



在上面的图片代码中,说明了showoff函数的参数是引用传递类型的,而不是按值传递,因为如果是按值传递参数,或将一个派生类的对象赋值给基类变量,则会失去多态特性.

上面的基类work虽然定义了print函数,但是该函数没有用处,为了让他有用,每个派生类必须覆盖print.而诸如work类的编写者为了确保每个派生类都会正确的覆盖虚函数,可以省略函数体,而用=0代替之.该符号将函数标记为纯虚函数,表明此函数没有可继承的实现,派生类必须实现该函数(有点类似Java的抽象(类)函数).而对于纯虚函数,编译器加入了一些规则,至少有一个纯虚函数的类称为抽象类.不允许定义抽象类的对象.

比如讲work类修改成纯虚函数:

 

/** Defining work as an Abstract Class. */
class work
{
public:
  work() : id_(), title_() {}
  work(std::string const& id, std::string const& title) : id_(id), title_(title) {}
  virtual ~work() {}
  std::string const& id()    const { return id_; }
  std::string const& title() const { return title_; }
  virtual void print(std::ostream& out) const = 0;
private:
  std::string id_;
  std::string title_;
};

尽管大部分类不需要手动编写析构函数,但是有一个规则,如果一个类有虚函数,那么该类一定要将析构函数也声明为虚函数.这个仅仅是编程建议,不是语法要求,编译器也不会提示你应该写,但是你要代替编译器,推荐你写.


That is it.

Next :

几个概念:

声明与定义

自动类型

静态变量

静态数据成员