C++教程翻译:类(2):Classes (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; |
在这个示例中,对于
b
和
c
之间的加法操作符,其结果应当是什么,就不那么明确了。事实上,以上这砣代码在编译时会出现错误,因为,
myclass
这个类型并未定义加法行为。然而,C++允许对大部分操作符进行重载,因此,可以针对包括类在内的任意类型定义这些操作符的行为。以下是所有可重载的操作符列表:
可重载的操作符 |
+ - * / = < > += -= *= /= << >> <<= >>= == != <= >= ++ -- % & ^ ! | ~ &= ^= |= && || %= [] () , ->* -> new delete new[] delete[] |
操作符的重载,是通过
operator
函数的方式来进行的,这些函数本身是普通函数,只是带有特殊的名字:它们的名字,由两部分组成,其中,以关键字
operator
开头,后面跟上要重载的
操作符符号
。语法是:
type operator sign (parameters) { /*... body ...*/ }
例如,笛卡尔向量(
cartesian vectors
),由两个座标成员组成:
x
和
y
。对于两个笛卡尔向量的加法,
其定义是,将二者的
x
坐标相加,并且将二者的
y
坐标相加。例如,将两个笛卡尔向量
(3,1)
和
(1,2)
相加,其结果是
(3+1,1+2) = (4,3)
。在C++中,可使用以下代码来实现这样的计算:
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 |
// 操作符重载示例 #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) {} // 函数 名 CVector (构造函数) CVector operator + ( const CVector&); // 函数 ,返回一个 CVector 对象 |
CVector
类中的
operator+
函数,对该个数据类型的加法操作符(
+
)进行了重载。在声明了这个函数之后,就可以隐式地使用操作符来调用它,或者显式地使用函数名来调用它:
1 2 |
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+
示例中使用了。而某些操作符还可以作为非成员函数来重载;在这种情况下,对应的操作符函数,会以适当的类的一个对象作为其第一个参数。
示例:
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 |
// 非成员函数形式的操作符重载 #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
,代表的是,指向那个其成员函数正在被执行的对象的指针。它的作用是,在类的成员函数中引用该对象本身。
其中的一个用法就是,检查被传入成员函数的某个参数是否是该对象本身。例如:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
// 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=
函数可能会是这样的:
1 2 3 4 5 6 |
CVector& CVector:: operator = ( const CVector& param) { x=param.x; y=param.y; return * this ; } |
事实上,以上写出的这个函数,与编译器隐式为这个类的
operator=
方法生成的代码非常类似。
类中可以包含静态成员,可以是数据也可以是函数。
类中的静态数据成员,也被称为“类变量”,因为,对于同一个类的所有对象,只存在一个共用的变量,它们共享同一个值:也就是说,对于这个类的不同对象,这个变量的值都是相同的。
例如,可使用这样的变量作为一个计数器,记录下当前分配了那个类的多少个对象,如下面代码所示:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
// 类的静态成员 #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; |
由于它是同一个类的所有对象共有的变量值,所以,它能够被那个类的任意对象当作成员来引用,或者,也可以直接通过类名来引用(显然,这种引用方法只对静态成员有效):
1 2 |
cout << a.n; cout << Dummy::n; |
上面两行代码,引用到的是同一个变量:类
Dummy
中的静态变量
n
,它被这个类的所有对象所共享。
再次说明,它与非成员变量类似,只是,必须按照类(或对象)的成员变量的方式来引用。
类中也可以拥有静态成员函数。它们的意义是类似的:它们是某个类的成员,并且被该个的所有对象所共用,其行为与非成员函数一致,只是要以类成员的方式来访问。因为它们的行为与非成员函数类似,所以,它们无法访问到该个类中的非静态成员(既不能访问到成员变量也不能访问到成员函数)。它们也不能使用
this
关键字。
如果某个类的某个对象被限定为常量( const )对象:
|
const MyClass myobject; |
那么,从外界看来,对于该个类的数据成员,只能以只读方式来访问,其效果就是,对于从该个类之外访问其数据成员的代码来说,那些数据成员全部是常量(
const
)。不过要注意,其构造函数仍然是会被调用的,因而允许在这个过程中对那些数据成员进行初始化及修改:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
// 常量对象 的构造函数 #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; } |
10 |
对于一个常量(
const
)对象的成员函数,只有当它本身也被指定为常量(
const
)成员时,才允许调用;在上面的示例中,无法通过
foo
来调用成员函数
get(它未被指定为常量(const))
。要将某个成员函数指定为常量(
const
)成员的话,则,应当在函数原型之后加上
const
关键字,也就是在参数列表的反括号之后加上关键字:
|
int get() const { return x;} |
注意,
const
可用来限定某个成员函数所返回的数据的类型。这里所说的
const
,与前面所说的用来将成员标记为常量的
const
是不同的。两者是互相独立的,它们在函数原型中所处的位置也不同。
1 2 3 |
int get() const { return x;} // 常量成员函数 const int & get() { return x;} // 成员函数 ,返回的是一个常量引用( const& ) const int & get() const { return x;} // 常量成员函数 ,返回 的是一个常量引用( const& ) |
被指定为常量(
const
)的成员函数,无法修改非静态数据成员,也无法调用非常量成员函数。实际上,常量(
const
)成员不应当修改对应对象的状态。
常量(
const
)对象,只允许访问那些被标记为常量(
const
)的成员函数,而非常量对象则没有这些限制,因而可以同时访问常量(
const
)和非常量成员函数。
妳可能会觉得,妳在日后几乎不会声明常量(
const
)对象,于是,将所有的那些不会修改该个对象的成员标记为常量是不值得的,但是,实际上,常量对象是狠常见的。大部分以某个类作为参数的函数,实际上都是以常量引用(
const
reference)的方式来使用其参数的,因此,这些函数就只能访问那些类的常量(
const
)成员了:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
// 常量 ( const )对象 #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
)成员函数。
成员函数可根据其常量性(constness)来进行重载:也就是说,在某个类中,可以有两个具有相同签名特征的成员函数,唯一的区别只在于其中一个是常量(
const
)函数,而另一个不是:在这种情况下,只有当该个对象本身是常量时,才会调用到常量(
const
)版本的那个函数,而非常量版本(non-
const
)的那个函数呢,只有当该个对象本身是非常量(non-
const
)时才会被调用。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
// 针对常量 性(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; // 正确 : get()返回 一个 int& // bar.get() = 25; // 无效:get()返回 一个常量(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,那么应当这样写:
|
mypair< int > myobject (115, 36); |
这个类同样可用来创建一个用于储存任意其它数据类型的对象,例如:
|
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 |
// 类模板 #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
(尖括号里面的那个),也是必需的:它表明,这个函数的模板参数就是该个类的模板参数。
可以做到,在将某个特定的数据类型传递作为模板参数时,定义出一个不一样的模板实现。这被叫做模
板特化(
template specialization
)。
例如,我们有一个狠简单的类,名为
mycontainer
。它能够存储单个任意数据类型的元素,并且只有一个成员函数
increase
,这个成员函数会将那个成员变量的值增加一步。但是,我们发现,当它存储的是
char
数据类型的元素时,采用另外一种不同的方式,实现出成员函数
uppercase
,会更方便。因此,我们决定针对那个数据类型声明一个类模板特化:
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 |
// 模板特化 #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; } |
8 J |
以下既是针对类模板特化的语法:
|
template <> class mycontainer < char > { ... }; |
首先注意一点,我们在类名之前加上了
template<>
,其中包含着一个空白的参数列表。这是因为,所有的数据类型都是已知的,对于这个特化不再需要提供模板参数,但是,它本身仍然是对于某个类模板的特化,所以,需要按照这种语法来表明这一点。
相比于这个前缀来说,类模板名字后面的那个
<char>
特化参数就更重要了。这个特化参数,其作用就是,指明,当前这个模板类是针对哪个数据类型(
char
)进行特化的。注意看泛型类模板和其特化之间的区别:
1 2 |
template < class T> class mycontainer { ... }; template <> class mycontainer < char > { ... }; |
第一行是泛型模板,第二行是特化。
当我们声明某个模板类的特化时,我们必须定义它的全部成员,甚至包括那些与泛型模板类中实现完全一致的成员。因为,在泛型模板与特化之间,并不存在成员之间的“继承”关系。
未知美人
Your opinionsHxLauncher: Launch Android applications by voice commands