重要的事情说三遍:
运算符重载
类本质上定义了在C++代码中使用的新类型。在C++中,类型不仅通过构造和赋值与代码交互,还通过运算符交互。例如,考虑以下对基本类型的操作:
int a, b, c;
a = b + c;
这里,不同的基本类型(int
)变量使用了加法运算符和赋值运算符。对于基本的算术类型,这些操作的含义通常是显而易见且明确的,但对于某些类类型可能并非如此。例如:
struct myclass {
string product;
float price;
} a, b, c;
a = b + c;
这里,加法运算b + c
的结果并不明显。事实上,这段代码会导致编译错误,因为myclass
类型没有定义加法的行为。然而,C++允许重载大多数运算符,以便为几乎任何类型(包括类)定义其行为。以下是可以重载的所有运算符列表:
可重载的运算符 |
---|
+ - * / = > += -= *= /= >>= >>= == != >= ++ -- % & ^ ! ~ &= ^= |= && %= [] () , ->* -> new delete new[] delete[] |
通过operator
函数重载运算符,这些函数是具有特殊名称的常规函数:它们的名称以operator
关键字开头,后跟要重载的运算符符号。语法如下:
type operator sign (parameters) { /*... body ...*/ }
例如,笛卡尔向量是两个坐标的集合:x
和y
。两个笛卡尔向量的加法操作定义为将两个x
坐标相加,并将两个y
坐标相加。例如,将笛卡尔向量(3,1)
和(1,2)
相加会得到(3+1,1+2) = (4,3)
。这可以用以下C++代码实现:
// 运算符重载示例
#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;
}
如果对频繁出现的CVector
感到困惑,请考虑其中一些指的是类名(即类型)CVector
,而另一些是函数名(即构造函数,必须与类名相同)。例如:
CVector (int, int) : x(a), y(b) {} // 构造函数名称为CVector
CVector operator+ (const CVector&); // 返回CVector的函数
类CVector
的函数operator+
重载了该类型的加法运算符(+
)。一旦声明,这个函数可以使用运算符隐式调用,或者使用其函数名显式调用:
c = a + b;
c = a.operator+ (b);
这两种表达式是等价的。
运算符重载只是常规函数,可以具有任何行为;实际上没有要求重载的操作必须与运算符的数学或常规含义有关,尽管强烈建议这样做。例如,一个类重载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) a | TYPE | A::operator TYPE() | - |
其中a
是类A
的对象,b
是类B
的对象,c
是类C
的对象。TYPE
只是任何类型(运算符重载为类型TYPE
的转换)。
注意,有些运算符可以有两种形式的重载:作为成员函数或作为非成员函数:上面的例子中使用了成员函数形式的operator+
。但有些运算符也可以作为非成员函数重载;在这种情况下,运算符函数将适当类的对象作为第一个参数。
例如:
// 非成员运算符重载
#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;
}
this关键字
关键字this
表示一个指向正在执行其成员函数的对象的指针。在类的成员函数内使用它来引用对象本身。
它的一个用途是检查传递给成员函数的参数是否是对象本身。例如:
// this示例
#include <iostream>
using namespace std;
class Dummy {
public:
bool isitme (Dummy& param);
};
bool Dummy::isitme (Dummy& param)
{
if (¶m == 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;
}
它也经常用于返回对象引用的operator=
成员函数。继续前面的笛卡尔向量例子,其operator=
函数可以定义为:
CVector& CVector::operator= (const CVector& param)
{
x = param.x;
y = param.y;
return *this;
}
实际上,这个函数非常类似于编译器为该类隐式生成的operator=
代码。
静态成员
类可以包含静态成员,无论是数据还是函数。
类的静态数据成员也被称为“类变量”,因为对于同一类的所有对象,只有一个共享相同值的变量:即其值对于该类的一个对象与另一个对象没有区别。
例如,它可以用于类中的一个变量,该变量可以包含当前分配的该类对象数量的计数器,如以下示例所示:
// 类中的静态成员
#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;
}
实际上,静态成员具有与非成员变量相同的属性,但它们享有类作用域。因此,为了避免它们被多次声明,它们不能直接在类中初始化,而需要在类外的某个地方初始化。如前例所示:
int Dummy::n = 0;
因为它是同一类的所有对象的公共变量值,所以可以作为该类的任何对象的成员引用,甚至可以直接通过类名引用(当然,这只对静态成员有效):
cout << a.n;
cout << Dummy::n;
上面这两个调用都引用相同的变量:类Dummy
中的静态变量n
,由该类的所有对象共享。
再次强调,它就像一个非成员变量,但其名称需要像类(或对象)的成员那样访问。
类也可以有静态成员函数。它们表示相同的东西:对该类的所有对象都通用的类成员,行为与非成员函数完全相同,但被访问时像类的成员一样。由于它们像非成员函数,因此不能访问类的非静态成员(既不能访问成员变量也不能访问成员函数)。它们也不能使用关键字this
。
常量成员函数
当类的对象被限定为常量对象时:
const MyClass myobject;
从类外部访问其数据成员的权限被限制为只读,好像所有数据成员对于从类外部访问它们的那些人来说都是常量一样。不过请注意,构造函数仍然会被调用,并且允许初始化和修改这些数据成员:
// 构造函数在常量对象上
#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; // 无效:x不能被修改
cout << foo.x << '\n'; // 可以:数据成员x可以读取
return 0;
}
常量对象的成员函数只有在它们本身也被指定为常量成员时才能被调用。在上面的例子中,成员函数get
(未指定为常量)不能从foo
调用。要指定成员是常量成员,const
关键字应在其参数的右括号后跟随:
int get() const { return x; }
注意,const
还可以用于限定成员函数返回的类型。这个const
与指定成员为常量的那个const
不同,两者是独立的,并位于函数原型中的不同位置:
int get() const { return x; } // 常量成员函数
const int& get() { return x; } // 返回常量引用的成员函数
const int& get() const { return x; } // 返回常量引用的常量成员函数
指定为常量的成员函数不能修改非静态数据成员,也不能调用其他非常量成员函数。本质上,常量成员不应修改对象的状态。
常量对象只能访问标记为常量的成员函数,但非常量对象没有限制,因此可以访问常量和非常量成员函数。
您可能认为无论如何您很少会声明常量对象,因此将所有不修改对象的成员标记为常量不值得,但常量对象实际上非常常见。大多数将类作为参数的函数实际上通过常量引用传递它们,因此,这些函数只能访问其常量成员:
// 常量对象
#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;
}
在这个例子中,如果get
没有被指定为常量成员,print
函数中的arg.get()
调用将不可能,因为常量对象只能访问常量成员函数。
成员函数可以根据其常量性进行重载:即,一个类可以有两个签名相同的成员函数,除了一个是常量,另一个不是:在这种情况下,仅当对象本身是常量时才调用常量版本,当对象本身是非常量时才调用非常量版本。
// 根据常量性重载成员
#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; // 可以:get()返回int&
// bar.get() = 25; // 无效:get()返回const int&
cout << foo.get() << '\n';
cout << bar.get() << '\n';
return 0;
}
类模板
就像我们可以创建函数模板一样,我们也可以创建类模板,允许类的成员使用模板参数作为类型。例如:
template <class T>
class mypair {
T values [2];
public:
mypair (T first, T second)
{
values[0]=first; values[1]=second;
}
};
我们刚定义的类用于存储任何有效类型的两个元素。例如,如果我们想声明一个该类的对象来存储两个int
类型的值,值为115和36,我们会这样写:
mypair<int> myobject (115, 36);
这个类也可以用于创建存储其他任何类型的对象,例如:
mypair<double> myfloats (3.0, 2.18);
构造函数是前一个类模板中的唯一成员函数,并且已在类定义本身中内联定义。如果在类模板定义外定义成员函数,则应在前面加上template <...>
前缀:
// 类模板
#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;
}
注意成员函数getmax
的定义语法:
template <class T>
T mypair<T>::getmax ()
被这么多T
搞糊涂了吗?在这个声明中有三个T
:第一个是模板参数。第二个T
指的是函数返回的类型。第三个T
(尖括号之间的那个)也是一个要求:它指定此函数的模板参数也是类模板参数。
模板特化(Template specialization)
当特定类型作为模板参数传递时,可以为模板定义不同的实现。这称为模板特化。
例如,假设我们有一个非常简单的类mycontainer
,它可以存储任何类型的一个元素,并且只有一个成员函数increase
,该函数增加其值。但我们发现,当它存储一个char
类型的元素时,更方便的是有一个完全不同的实现,带有一个成员函数uppercase
,因此我们决定为该类型声明一个类模板特化:
// 模板特化
#include <iostream>
using namespace std;
// 类模板:
template <class T>
class mycontainer {
T element;
public:
mycontainer (T arg) {element=arg;}
T increase () {return ++element;}
};
// 类模板特化:
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;
}
这是类模板特化的语法:
template <> class mycontainer <char> { ... };
首先,注意我们在类名前加上template<>
,包括一个空的参数列表。这是因为所有类型都是已知的,不需要模板参数,但它仍然是类模板的特化,因此需要注意。
但比这个前缀更重要的是类模板名后的<char>
特化参数。这个特化参数本身标识模板类被特化的类型(char
)。注意通用类模板和特化之间的区别:
template <class T> class mycontainer { ... };
template <> class mycontainer <char> { ... };
第一行是通用模板,第二行是特化。
当我们为模板类声明特化时,我们还必须定义其所有成员,即使那些与通用模板类相同的成员,因为没有从通用模板到特化的成员“继承”。