1.1. 类与对象

面向对象编程,是一种通过将算法与其所操作的数据结合,成为“对象”实体来组织程序的方法。大多数面向对象的语言(包括 C++)都基于类。类是对象的定义 ——它描述了算法、数据、数据格式,以及与其他类的关系。对象是类的具体实例,也就是变量。对象具有地址,即内存中的一个位置。类是一种用户定义的类型。通常,可以根据类提供的定义创建任意数量的对象(某些类会限制可创建对象的数量,但这属于例外情况,而非一般规则)。

在C++中,类所包含的数据可组织为不同类型的数据成员(或称变量)的集合。算法则通过函数来实现,即类的“方法”。尽管语言本身并不要求类的数据成员必须与其实现方法相关,但良好的设计通常会将数据在类中很好地封装起来,并限制方法与外部数据的交互。

封装在C++的类中处于核心地位 —— 控制类的哪些数据成员和方法是公开的(在类外部可见),哪些是内部的(对类私有)。有设计良好的类通常(或仅)拥有私有数据成员,其公开的方法仅限于表达类的公共接口所需的部分,即类对外提供的功能。这个公共接口就像一份约定:类的设计者承诺该类提供某些特性与操作。而类的私有数据和方法属于实现细节,只要保持公共接口(即承诺的约定)不变,这些实现细节就可以修改。例如,以下类表示一个有理数,并通过其公共接口进行自增操作:

class Rational { public:
  Rational& operator+=(const Rational& rhs);
}

设计良好的类,不会通过其公共接口暴露超出必要范围的实现细节。实现本身不属于约定,尽管已记录的接口可能会对其施加某些限制。例如,承诺所有有理数的分子和分母不含公因数,那么加法操作就应该包含约去公因数的步骤。这正是使用私有成员函数的恰当场景 —— 其他多个操作的实现可能需要调用它,但类的使用者永远不用调用,因为每个有理数在暴露给调用者之前,都已经为最简形式了:

class Rational {
public:
  Rational& operator+=(const Rational& rhs);
private:
  long n_; // 分子
  long d_; // 分母
  void reduce();
};

Rational& Rational::operator+=(const Rational& rhs) {
  n_ = n_*rhs.d_ + rhs.n_*d_;
  d_ = d_*rhs.d_; reduce();
  return *this;
}

Rational a, b;
a += b;

类方法具有特殊权限来访问类的数据成员 —— 可以访问类的私有数据。注意这里类和对象的区别 —— operator+=() 是 Rational 类的一个方法,对象 a对其进行调用。然而,它也能访问对象 b 的私有数据,因为 a 和 b 是同一类的对象。如果一个成员函数在引用类成员时,仅使用名称而没有其他限定符,那么访问的就是所调用对象的成员(可以通过显式地写成 this→n_ 和 this→d_ 来表明)。访问同一类的另一个对象的成员需要该对象的指针或引用,除此之外没有其他限制,这与我们试图从一个非成员函数中访问私有数据成员的情况不同。

同样,C++ 也支持 C 风格的 struct。但 C++ 中,struct 并不仅限于数据成员的集合 —— 可以拥有方法、公有和私有的访问修饰符,以及类所具备的其他特性。从语言角度看,class 和 struct 的唯一区别在于默认访问权限:在 class 中,所有成员和方法默认为私有,而在 struct 中它们默认为公有。除此之外,使用 struct 还是 class 取决于约定 —— 传统上,struct 用于 C 风格的结构体,以及几乎类似 C 风格的结构体,例如仅添加了一个构造函数的结构体。当然,这种界限并不精确,具体取决于每个项目或团队的编码风格和实践。

除了已经看到的方法和数据成员外,C++ 还支持静态数据和静态方法。静态方法与普通的非成员函数非常相似 —— 它不是在某个特定对象上调用的,其访问类型对象的唯一方式是通过其参数。然而,与非成员函数不同的是,静态方法保留了对类私有数据的访问权限。

类本身是一种有用的方式,用于将算法及其操作的数据组合(封装)在一起,并限制对某些数据的访问。然而,C++ 最强大的面向对象特性是继承,以及由此产生的类型层次结构。