类 (II)

重载运算符

从本质上讲,类定义了在 C++ 代码中使用的新类型。C++ 中的类型不仅通过构造和赋值与代码交互,还通过运算符进行交互。例如,看下面对基本类型的操作:

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

这里,一个基本类型(int)的不同变量被应用了加法运算符,然后是赋值运算符。对于基本的算术类型,这类操作的含义通常是显而易见且无歧义的,但对于某些类类型可能并非如此。例如:

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

这里,对 bc 进行加法运算的结果并不明显。实际上,仅这段代码就会导致编译错误,因为 myclass 类型没有为加法定义任何行为。然而,C++ 允许大多数运算符被重载,以便可以为几乎任何类型(包括类)定义其行为。以下是可以被重载的所有运算符的列表:

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

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

运算符通过 operator 函数进行重载,这些函数是具有特殊名称的常规函数:它们的名称以 operator 关键字开头,后跟被重载的*运算符符号*。语法是:

type operator sign (parameters) { /*... 函数体 ...*/ }
例如,*笛卡尔向量*是两个坐标的集合:xy。两个*笛卡尔向量*的加法运算定义为它们的 x 坐标相加,以及它们的 y 坐标相加。例如,将*笛卡尔向量* (3,1)(1,2) 相加会得到 (3+1,1+2) = (4,3)。这可以用 C++ 实现如下代码:

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

class CVector {
  public:
    int x,y;
    CVector () {};
    CVector (int a,int b) : x(a), y(b) {}
    CVector operator + (const CVector&);
};

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

int main () {
  CVector foo (3,1);
  CVector bar (1,2);
  CVector result;
  result = foo + bar;
  cout << result.x << ',' << result.y << '\n';
  return 0;
}
4,3

如果对这么多 CVector 的出现感到困惑,请注意其中一些指的是类名(即类型)CVector,而另一些是同名函数(即构造函数,其名称必须与类名相同)。例如:

1
2
CVector (int, int) : x(a), y(b) {}  // function name CVector (constructor)
CVector operator+ (const CVector&); // function that returns a CVector  

CVector 类的函数 operator+ 重载了该类型的加法运算符(+)。一旦声明,这个函数就可以通过运算符隐式调用,或者通过其函数名显式调用:

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

两种表达式是等价的。

运算符重载只是常规函数,可以有任何行为;实际上,并没有要求重载执行的操作与该运算符的数学或通常意义有任何关系,尽管我们强烈推荐这样做。例如,一个类重载 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->b->A::operator->()-
(TYPE) aTYPEA::operator TYPE()-
其中 aA 类的对象,bB 类的对象,cC 类的对象。TYPE 是任意类型(该运算符重载了到 TYPE 类型的转换)。

请注意,某些运算符可以以两种形式重载:作为成员函数或作为非成员函数。第一种情况已在上面的 operator+ 示例中使用。但某些运算符也可以作为非成员函数重载;在这种情况下,运算符函数将相应类的对象作为其第一个参数。

例如:
// non-member operator overloads
#include <iostream>
using namespace std;

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


CVector operator+ (const CVector& lhs, const CVector& rhs) {
  CVector temp;
  temp.x = lhs.x + rhs.x;
  temp.y = lhs.y + rhs.y;
  return temp;
}

int main () {
  CVector foo (3,1);
  CVector bar (1,2);
  CVector result;
  result = foo + bar;
  cout << result.x << ',' << result.y << '\n';
  return 0;
}
4,3

关键字 this

关键字 this 代表一个指向其成员函数正在被执行的对象的指针。它在类的成员函数内部使用,以引用对象本身。

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

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

class Dummy {
  public:
    bool isitme (Dummy& param);
};

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

int main () {
  Dummy a;
  Dummy* b = &a;
  if ( b->isitme(a) )
    cout << "yes, &a is b\n";
  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 Dummy {
  public:
    static int n;
    Dummy () { n++; };
};

int Dummy::n=0;

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

实际上,静态成员具有与非成员变量相同的属性,但它们享有类的作用域。因此,为了避免它们被多次声明,它们不能直接在类中初始化,而需要在类的外部某处进行初始化。如上一个例子所示:

1
int Dummy::n=0;

因为它是同一类的所有对象的公共变量值,所以它可以作为该类任何对象的成员来引用,甚至可以直接通过类名来引用(当然这只对静态成员有效):

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

上面这两个调用引用的是同一个变量:Dummy 类中所有对象共享的静态变量 n

再次强调,它就像一个非成员变量,只是其名称需要像访问类(或对象)的成员一样来访问。

类也可以有静态成员函数。它们代表的意义相同:是类的成员,对该类的所有对象都是公共的,其行为与非成员函数完全一样,但访问方式如同类的成员。因为它们像非成员函数,所以它们不能访问类的非静态成员(无论是成员变量还是成员函数)。它们也不能使用关键字 this

const 成员函数

当一个类的对象被限定为 const 对象时:

1
const MyClass myobject;

从类外部访问其数据成员被限制为只读,就好像对于从外部访问它们的人来说,其所有数据成员都是 const。但请注意,构造函数仍然会被调用,并被允许初始化和修改这些数据成员:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// constructor on const object
#include <iostream>
using namespace std;

class MyClass {
  public:
    int x;
    MyClass(int val) : x(val) {}
    int get() {return x;}
};

int main() {
  const MyClass foo(10);
// foo.x = 20;            // not valid: x cannot be modified
  cout << foo.x << '\n';  // ok: data member x can be read
  return 0;
}
10

一个 const 对象的成员函数只有在它们本身被指定为 const 成员时才能被调用;在上面的例子中,成员函数 get(没有被指定为 const)不能从 foo 调用。要将一个成员函数指定为 const 成员,const 关键字应紧跟在函数原型参数的右括号之后:

1
int get() const {return x;}

请注意,const 可以用来限定成员函数返回的类型。这个 const 与指定成员为 const 的那个不同。两者是独立的,并位于函数原型中的不同位置:

1
2
3
int get() const {return x;}        // const member function
const int& get() {return x;}       // member function returning a const&
const int& get() const {return x;} // const member function returning a const& 

被指定为 const 的成员函数不能修改非静态数据成员,也不能调用其他非 const 成员函数。本质上,const 成员不应修改对象的状态。

const 对象只能访问标记为 const 的成员函数,但非 const 对象不受限制,因此可以访问 const 和非 const 成员函数。

你可能认为反正你很少会声明 const 对象,因此把所有不修改对象的成员标记为 const 是不值得的,但实际上 const 对象非常普遍。大多数接受类作为参数的函数实际上是通过 const 引用来接受它们的,因此,这些函数只能访问它们的 const 成员:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// const objects
#include <iostream>
using namespace std;

class MyClass {
    int x;
  public:
    MyClass(int val) : x(val) {}
    const int& get() const {return x;}
};

void print (const MyClass& arg) {
  cout << arg.get() << '\n';
}

int main() {
  MyClass foo (10);
  print(foo);

  return 0;
}
10

如果在这个例子中,get 没有被指定为 const 成员,那么在 print 函数中调用 arg.get() 是不可能的,因为 const 对象只能访问 const 成员函数。

成员函数可以根据其 const 性进行重载:也就是说,一个类可以有两个成员函数,它们的签名完全相同,只有一个是 const 而另一个不是。在这种情况下,当对象本身是 const 时,调用 const 版本;当对象本身是非 const 时,调用非 const 版本。

// overloading members on constness
#include <iostream>
using namespace std;

class MyClass {
    int x;
  public:
    MyClass(int val) : x(val) {}
    const int& get() const {return x;}
    int& get() {return x;}
};

int main() {
  MyClass foo (10);
  const MyClass bar (20);
  foo.get() = 15;         // ok: get() returns int&
// bar.get() = 25;        // not valid: get() returns const int&
  cout << foo.get() << '\n';
  cout << bar.get() << '\n';

  return 0;
}
15
20

类模板

就像我们可以创建函数模板一样,我们也可以创建类模板,允许类拥有使用模板参数作为类型的成员。例如:

1
2
3
4
5
6
7
8
9
template <class T>
class mypair {
    T values [2];
  public:
    mypair (T first, T second)
    {
      values[0]=first; values[1]=second;
    }
};

我们刚刚定义的类用于存储任意有效类型的两个元素。例如,如果我们想声明这个类的一个对象来存储两个类型为 int 的整数值 115 和 36,我们会这样写:

1
mypair<int> myobject (115, 36);

这个相同的类也可以用来创建一个存储任何其他类型的对象,例如:

1
mypair<double> myfloats (3.0, 2.18);

在前面的类模板中,构造函数是唯一的成员函数,它在类定义内部被内联定义了。如果成员函数在类模板的定义之外定义,它必须以 template <...> 前缀开头:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
// class templates
#include <iostream>
using namespace std;

template <class T>
class mypair {
    T a, b;
  public:
    mypair (T first, T second)
      {a=first; b=second;}
    T getmax ();
};

template <class T>
T mypair<T>::getmax ()
{
  T retval;
  retval = a>b? a : b;
  return retval;
}

int main () {
  mypair <int> myobject (100, 75);
  cout << myobject.getmax();
  return 0;
}
100

注意成员函数 getmax 定义的语法:

1
2
template <class T>
T mypair<T>::getmax ()

被这么多的 T 搞糊涂了吗?这个声明中有三个 T:第一个是模板参数。第二个 T 指的是函数返回的类型。第三个 T(尖括号中的那个)也是必需的:它指定了这个函数的模板参数也是类模板的参数。

模板特化

当一个特定的类型作为模板参数传递时,可以为模板定义一个不同的实现。这被称为*模板特化*。

例如,假设我们有一个非常简单的类叫做 mycontainer,它可以存储任何类型的一个元素,并且只有一个名为 increase 的成员函数,该函数会增加其值。但我们发现,当它存储一个 char 类型的元素时,拥有一个带有 uppercase 成员函数的完全不同的实现会更方便,所以我们决定为该类型声明一个类模板特化:

// template specialization
#include <iostream>
using namespace std;

// class template:
template <class T>
class mycontainer {
    T element;
  public:
    mycontainer (T arg) {element=arg;}
    T increase () {return ++element;}
};

// class template specialization:
template <>
class mycontainer <char> {
    char element;
  public:
    mycontainer (char arg) {element=arg;}
    char uppercase ()
    {
      if ((element>='a')&&(element<='z'))
      element+='A'-'a';
      return element;
    }
};

int main () {
  mycontainer<int> myint (7);
  mycontainer<char> mychar ('j');
  cout << myint.increase() << endl;
  cout << mychar.uppercase() << endl;
  return 0;
}
8
J

这是用于类模板特化的语法:

1
template <> class mycontainer <char> { ... };

首先,请注意我们在类名前面加上了 template<>,包含一个空的参数列表。这是因为所有类型都是已知的,这个特化不需要模板参数,但它仍然是类模板的一个特化,因此需要这样标记。

但比这个前缀更重要的是类模板名称后面的 <char> 特化参数。这个特化参数本身标识了模板类被特化的类型(char)。注意通用类模板和特化之间的区别:

1
2
template <class T> class mycontainer { ... };
template <> class mycontainer <char> { ... };

第一行是通用模板,第二行是特化。

当我们为一个模板类声明特化时,我们还必须定义它的所有成员,即使是那些与通用模板类相同的成员,因为成员不会从通用模板“继承”到特化版本。
Index
目录