类 (II)

运算符重载

C++ 允许使用标准运算符对类进行操作,就像对基本类型进行操作一样。例如:

1
2
int a, b, c;
a = b + c;

这是 C++ 中显然有效的代码,因为加法的不同变量都是基本类型。然而,如果我们想要执行类似下面的操作,就不那么明显了:

1
2
3
4
5
struct {
  string product;
  float price;
} a, b, c;
a = b + c;

实际上,这会导致编译错误,因为我们没有定义我们的类在加法运算中应该具有的行为。但是,得益于 C++ 的运算符重载特性,我们可以设计能够使用标准运算符执行操作的类。以下是可以重载的所有运算符的列表:

可重载的运算符
+    -    *    /    =    
    >    +=   -=   *=   /=   

   >>
= >>= == != = >= ++ -- % & ^ ! |
~ &= ^= |= && || %= [] () , ->* -> new
delete new[] delete[]

为了重载运算符以便在类中使用,我们声明运算符函数,它们是常规函数,其名称为运算符关键字 `operator` 后跟我们要重载的运算符符号。 格式是:

type operator sign (parameters) { /*...*/ }

这里有一个重载加法运算符 (+)的例子。 我们将创建一个类来存储二维向量,然后我们将添加其中的两个:a(3,1)b(1,2)。 两个二维向量的加法就像将两个x坐标相加得到结果x坐标,并将两个y坐标相加得到结果y坐标相加一样简单。 在这种情况下,结果将是(3+1,1+2) = (4,3).

// vectors: overloading operators example
#include <iostream>
using namespace std;

class CVector {
  public:
    int x,y;
    CVector () {};
    CVector (int,int);
    CVector operator + (CVector);
};

CVector::CVector (int a, int b) {
  x = a;
  y = b;
}

CVector CVector::operator+ (CVector param) {
  CVector temp;
  temp.x = x + param.x;
  temp.y = y + param.y;
  return (temp);
}

int main () {
  CVector a (3,1);
  CVector b (1,2);
  CVector c;
  c = a + b;
  cout << c.x << "," << c.y;
  return 0;
}
4,3

多次看到CVector标识符可能会有点令人困惑。 但是,请考虑其中一些指的是类名(类型)CVector而另一些是具有该名称的函数(构造函数必须与类具有相同的名称)。 不要混淆它们。

1
2
CVector (int, int);            // function name CVector (constructor)
CVector operator+ (CVector);   // function returns a CVector 

函数`operator+`的类CVector负责重载加法运算符 (+)。 此函数可以使用运算符隐式调用,也可以使用函数名称显式调用:

1
2
c = a + b;
c = a.operator+ (b);

两个表达式是等效的。

还要注意,我们包含了空构造函数(没有参数),并且我们用一个空块定义了它:

1
CVector () { };

这是必要的,因为我们已经显式声明了另一个构造函数:

1
CVector (int, int);

当我们显式声明任何具有任意数量参数的构造函数时,编译器可以自动声明的默认的无参数构造函数将不会被声明,因此我们需要自己声明它,以便能够构造没有参数的此类型的对象。 否则,声明:

1
CVector c;

包含在main()中将无效。

无论如何,我必须警告你,空块对于构造函数来说是一个糟糕的实现,因为它没有满足通常对构造函数期望的最低功能,即初始化其类中的所有成员变量。 在我们的例子中,此构造函数使变量xy未定义。 因此,更可取的定义应该类似于以下内容:

1
CVector () { x=0; y=0; };

为了简化并仅显示代码的要点,我没有将其包含在示例中。

就像一个类包含默认构造函数和复制构造函数(即使它们没有被声明)一样,它也包含赋值运算符 (=)的默认定义,其中类本身作为参数。 默认定义的行为是将作为参数传递的对象(运算符右侧的对象)的所有数据成员的内容复制到运算符左侧的对象。

1
2
3
CVector d (2,3);
CVector e;
e = d;           // copy assignment operator 

复制赋值运算符函数是默认实现的唯一运算符成员函数。 当然,您可以将其重新定义为你想要的任何其他功能,例如,仅复制某些类成员或执行其他初始化过程。

运算符的重载不会强制其操作与运算符的数学或常用含义相关联,尽管建议这样做。 例如,如果您使用`operator +`来减去两个类或使用`operator==`来用零填充一个类,代码可能不是很直观,尽管这样做是完全可能的。

虽然函数的原型`operator+`看起来很明显,因为它将运算符右侧的内容作为运算符左侧对象的运算符成员函数的参数,但其他运算符可能不那么明显。 这里有一个表格,总结了如何声明不同的运算符函数(将 @ 替换为每个运算符):

表达式运算符成员函数全局函数
@a+ - * & ! ~ ++ --A::operator@()operator@(A)
a@++ --A::operator@(int)operator@(A,int)
a@b+ - * / % ^ & | < > == != <= >= << >> && || ,A::operator@ (B)operator@(A,B)
a@b= += -= *= /= %= ^= &= |= <<= >>= []A::operator@ (B)-
a(b, c...)()A::operator() (B, C...)-
a->x->A::operator->()-
其中a是类A, 和 b是类的物件B是类c.

您可以在此面板中看到,有两种方法可以重载某些类运算符:作为成员函数和作为全局函数。 它的使用是不同的,但是提醒您,不是类的成员的函数无法访问该类的私有或受保护的成员,除非全局函数是它的朋友(友元将在后面解释)。

关键字 this

关键字this表示指向正在执行其成员函数的对象的指针。 它是指向对象本身的指针。

它的用途之一是检查传递给成员函数的参数是否是对象本身。 例如:

// this
#include <iostream>
using namespace std;

class CDummy {
  public:
    int isitme (CDummy& param);
};

int CDummy::isitme (CDummy& param)
{
  if (&param == this) return true;
  else return false;
}

int main () {
  CDummy a;
  CDummy* b = &a;
  if ( b->isitme(a) )
    cout << "yes, &a is b";
  return 0;
}
yes, &a is b

它也经常用于`operator=`通过引用返回对象的成员函数(避免使用临时对象)。 继续使用之前看到的向量示例,我们可以编写一个`operator=`类似于这个的函数:

1
2
3
4
5
6
CVector& CVector::operator= (const CVector& param)
{
  x=param.x;
  y=param.y;
  return *this;
}

事实上,如果我们不包含一个`operator=`成员函数来复制此类的对象,则此函数与编译器为此类隐式生成的代码非常相似。

静态成员

一个类可以包含静态成员,包括数据或函数。

类的静态数据成员也称为“类变量”,因为对于同一类的所有对象,只有一个唯一值。它们的内容与该类的不同对象之间没有区别。

例如,它可能用于类中的一个变量,该变量可以包含一个计数器,其中包含当前分配的该类的对象的数量,如下例所示:

// static members in classes
#include <iostream>
using namespace std;

class CDummy {
  public:
    static int n;
    CDummy () { n++; };
    ~CDummy () { n--; };
};

int CDummy::n=0;

int main () {
  CDummy a;
  CDummy b[5];
  CDummy * c = new CDummy;
  cout << a.n << endl;
  delete c;
  cout << CDummy::n << endl;
  return 0;
}
7
6

事实上,静态成员具有与全局变量相同的属性,但它们具有类作用域。因此,为了避免多次声明它们,我们只能在类声明中包含原型(其声明),而不能包含其定义(其初始化)。为了初始化静态数据成员,我们必须在类的外部、全局作用域中包含一个正式定义,如前面的示例所示:

1
int CDummy::n=0;

因为它对于同一类的所有对象都是唯一的变量值,所以它可以被称为该类的任何对象的成员,甚至可以直接通过类名引用(当然,这仅对静态成员有效):

1
2
cout << a.n;
cout << CDummy::n;

前面示例中包含的这两个调用都引用同一个变量:类n中的静态变量CDummy,由该类的所有对象共享。

再次提醒您,事实上,它是一个全局变量。唯一的区别是它的名称以及可能的类外部访问限制。

正如我们可以在类中包含静态数据一样,我们也可以包含静态函数。它们代表相同的内容:它们是全局函数,就像给定类的对象成员一样被调用。它们只能引用静态数据,在任何情况下都不能引用类的非静态成员,并且不允许使用关键字this`this`,因为它引用一个对象指针,而这些函数实际上不是任何对象的成员,而是类的直接成员。
Index
目录