类本质上定义了在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++允许重载大多数操作符, 这样它们的行为就可以定义为任何类型,包括类。下面列出了所有可以重载的操作符:
可重载运算符 |
Overloadable operators + - * / = < > += -= *= /= << >> <<= >>= == != <= >= ++ -- % & ^ ! | ~ &= ^= |= && || %= [] () , ->* -> new delete new[] delete[] |
操作符通过operator函数来重载,operator函数是具有特殊名称的常规函数: 它们的名称以operator关键字开头,后面跟着重载的操作符符号。的语法是:
type operator sign (parameters) { /*... body ...*/ }例如,笛卡尔向量是两个坐标的集合:x和y,两个笛卡尔向量的加法运算被定义为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, 其他一些是具有该名称的函数(即构造函数,必须与类同名)。例如:
CVector (int, int) : x(a), y(b) {} // function name CVector (constructor) CVector operator+ (const CVector&); // function that returns a CVector |
类CVector的函数operator+重载该类型的加法操作符(+)。声明后,可以使用操作符隐式调用该函数, 也可以使用函数名显式调用该函数:
c = a + b; c = a.operator+ (b); |
操作符重载只是普通函数,可以有任何行为;实际上并不要求该重载执行的操作与运算符的数学意义或通常意义有关, 尽管强烈建议这样做。例如,重载operator+以实际减去或重载operator==以用0填充对象的类是完全有效的, 尽管使用这样的类可能很有挑战性。
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+的第一种情况。但是有些操作符也可以重载为非成员函数; 在这种情况下,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表示一个指向其成员函数正在执行的对象的指针。它在类的成员函数中用于引用对象本身。
它的用途之一是检查传递给成员函数的形参是否是对象本身。例如:
// example on 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; } |
yes, &a is b |
它也经常用于通过引用返回对象的operator=成员函数中。按照前面笛卡尔向量的例子, 它的operator=函数可以定义为:
CVector& CVector::operator= (const CVector& param) { x=param.x; y=param.y; return *this; } |
类可以包含静态成员: 数据或函数。
一个类的静态数据成员也被称为“类变量”,因为这个类的所有对象只有一个公共变量, 共享相同的值:也就是说,它的值在这个类的一个对象和另一个对象之间不变。
例如,它可以用于类中的一个变量,该变量可以包含一个计数器,该计数器包含当前分配的该类对象的数量,如下所示:
// 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 |
事实上,静态成员具有与非成员变量相同的属性,但它们具有类作用域。 因此,为了避免多次声明它们,它们不能在类中直接初始化,而需要在类外的某个地方初始化。正如前面的例子:
int Dummy::n=0; |
因为它是同一个类的所有对象的公共变量值,它可以作为该类的任何对象的成员来引用, 甚至可以直接通过类名来引用(当然这只对静态成员有效):
cout << a.n; cout << Dummy::n; |
上述两个调用引用同一个变量:Dummy类内的静态变量n,由该类的所有对象共享。
同样,它就像一个非成员变量,但是它的名称需要像类(或对象)的成员一样被访问。
类也可以有静态成员函数。它们表示的是相同的:类的所有对象共同的成员,完全作为非成员函数, 但像类的成员一样被访问。因为它们类似于非成员函数, 它们不能访问类的非静态成员(既不是成员变量也不是成员函数)。他们都不能使用关键字this。
const MyClass myobject; |
从类外部对其数据成员的访问被限制为只读,就好像从类外部访问其数据成员的所有数据成员都是const一样。 但是请注意,构造函数仍然被调用,并且允许初始化和修改这些数据成员:
// 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关键字必须紧跟在函数原型形参括号之后:
int get() const {return x;} |
注意,const可用于限定成员函数返回的类型。这个const与指定成员为const的方法不同。 两者都是独立的,并且位于函数原型的不同位置:
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成员:
// 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版本,
n当对象本身是非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 |
就像我们可以创建函数模板一样,我们也可以创建类模板,允许类拥有使用模板形参作为类型的成员。例如:
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); |
构造函数是前一个类模板中唯一的成员函数,它在类定义本身内嵌定义。 如果成员函数是在类模板定义之外定义的,那么它的前面应该有模板<…>前缀:
// 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 |
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 |
template <> class mycontainer <char> { ... }; |
首先,请注意,我们在类名的前面加上模板<>,包括一个空的形参列表。 这是因为所有类型都是已知的,并且这个特化不需要模板实参,但它仍然是类模板的特化, 因此需要对此加以注意。
首先,请注意,我们在类名的前面加上模板<>, 包括一个空的形参列表。这是因为所有类型都是已知的, 并且这个特化不需要模板实参,但它仍然是类模板的特化,因此需要对此加以注意。
template <class T> class mycontainer { ... }; template <> class mycontainer <char> { ... }; |
第一行是泛型模板,第二行是特化。
在为模板类声明特化时,还必须定义它的所有成员,甚至是与泛型模板类相同的成员, 因为从泛型模板到特化的成员没有“继承”。