11.3 静态成员类实际上就是一种用户定义的数据类型。每当生成一个某一类的对象时,系统就为该对象分配一块内存来存放其所有成员
(从理论上讲,包括所有成员函数)。然而,在实际应用中,
常常希望程序中所有同一类的对象共享某一成员(通常指数据成员,比如,同一学校中所以学生对象中的学校名称成员),
以保证数据的一致性及简化操作。解决这一问题的一个办法就是将需要共享的数据说明成全局变量,但这样作将破坏数据的封装性。更好的解决办法就是将需要共享的成员说明成类中的静态成员。
11.3.1 静态数据成员静态数据成员是指在类中说明的、用关键字 static 修饰的数据成员。例:
class X {
int d;
static int s; // 引用性说明
//…
};
int X,,s; // 定义性说明在类中说明的静态成员属于引用性说明,必须在类外对其进行定义性说明(说明时还可以对其进行初始化,定义性说明时不得带关键字 static)。 所以需要这样做是因为静态成员不属于任何一个对象,而为所有同类对象所共享。
// SDCLS.H
class X {
int d;
static int nCount;
public:
X(int a = 0),d(a) { nCount ++; }
~X() { nCount --; }
void Set(int a) { d = a; }
int Get() { return d; }
int Count() { return nCount; }
};
int X,,nCount = 0;
// TSDCLS.CPP
#include <iostream.h>
#include "sdcls.h"
void main()
{
X xArr[3],aX(100);
int i;
for(i = 0; i < 3; i ++)
xArr[i].Set(i * i);
for(i = 0; i < 3; i ++)
cout << "xArr[" << i << "].d = " << xArr[i].Get() << '\t';
cout << "xArr[" << i << "].nCount = " << xArr[i].Count()
<< endl;
}
cout << "aX.d = " << aX.Get() << '\t';
cout << "aX.nCount = " << aX.Count() << endl;
}
该程序的输出为:
xArr[0].d = 0 xArr[0].nCount = 4
xArr[1].d = 1 xArr[1].nCount = 4
xArr[2].d = 4 xArr[2].nCount = 4
aX.d = 100 aX.nCount = 4
从上例的输出可以看出:无论哪个对象,其成员 nCount 的值均是相同的。实际上,所有对象中的该成员根本就是同一个变量。
本例中该变量的值为 4 是类定义和所创建的对象个数所决定的:这里将 nCount 设计成了一个对象计数器,其初值为 0。
每创建一个对象其值就加一(参见类 X 的构造函数);而每当一个对象被撤消其值就减一(参见类 X 的析构函数)。
由于 nCount 是一个所有 X 类对象共享的数据成员,从理论上讲它不属于任何一个对象。因此,对该成员的访问以如下的形式进行将显得更为合理:
n = X,,Count();
cout << X,,Count() << endl;
11.3.2 静态成员函数与静态数据成员相同,静态成员函数是类中说明的、用关键字
static 修饰的成员函数。
一般讲,若类中存在静态数据成员,则访问该数据成员的成员函数应当说明成静态的。以便为共享的数据提供共享的接口。
应当说明的是:
1,由于关键字 static 不是数据类型的组成部分,所以在类外定义静态成员函数时不需要该关键字。
2,由于静态成员函数没有 this 指针,所以对于那些需要访问类中其它非静态成员的静态成员函数,必须带有一个同类对象或引用参数。
3,静态成员函数只能访问类中的静态数据成员(除非函数带有相应的参数),而非静态成员函数则可以访问类中的任何成员。
// SFCLS.H
class X {
int d;
static int nCount;
public:
X(int a = 0),d(a) { nCount ++; }
~X() { nCount --; }
void Set(int a) { d = a; }
int Get() { return d; }
static int Count() { return nCount; }
};
int X,,nCount = 0;
// TSFCLS.CPP
#include <iostream.h>
#include "sfcls.h"
void main()
{
cout << X,,Count() << endl;
X aX(100);
cout << "aX.d = " << aX.Get() << '\t' << X,,Count() << endl;
}
程序的输出为:
0
aX.d = 100 1
11.4 const,volatile对象和成员函数对象
key_word class_name obj_name;
函数
type func_name(<arg_list>) key_word;
其中,key_word 或者是 const,或者是 volatile。 用前者修饰的对象或成员函数叫做 const(常)对象或成员函数;用后者修饰的对象或函数叫做 volatile(易变)对象或成员函数。通过常对象只能访问类中的常成员函数;通过易变对象只能访问类中的易变成员函数。
由于易变对象和成员函数很少使用,下而仅举例说明常对象和成员函数的作用。
// STRCLS.H
#if !defined _STRCLS_H_
#define _STRCLS_H_
class String {
private:
char* pStr;
int nLen;
char* CopyStr(char*);
public:
String(),pStr(0),nLen(0) {}
String(String&);
String(char*);
~String() { delete []pStr; }
char* IsIn(char) const;
const char* GetContent() { return (const char*)pStr; }
int Length() { return nLen; }
void Show() const;
};
#endif
// STRCLS.CPP
#include <iostream.h>
#include <string.h>
#include "strcls.h"
char* String,,CopyStr(char* s)
{
nLen = strlen(s);
pStr = new char[nLen + 1];
strcpy(pStr,s);
}
String,,String(String& rs)
{
CopyStr(rs.pStr);
}
String,,String(char* s)
{
CopyStr(s);
}
char* String,,IsIn(char ch) const
{
char *cp = pStr;
while(*cp && *cp != ch)
cp ++;
if(*cp == ch)
return cp;
return 0;
}
void String,,Show()
{
cout << pStr << endl;
}
// TESTSTR.CPP
#include <iostream.h>
#include "strcls.h"
void main()
{
const String cStr("A constant object");
String Str("A normal object");
cout << cStr.Length() << endl; // 错误
cout << Str.Length() << endl; // 输出 13
cStr.Show();
Str.Show();
}
从上例可以看出:通过常对象只能调用类中公有的常成员函数,
而通过普通对象则可以调用类中的任何公有成员函数。类中只所以说明常成员函数就是为了方便常对象来调用它。
在许多场合下,需要使用常对象。比如,将一个对象用作一个函数的参数,为了防止函数中由于偶然的因素修改了对象的内容,就将该参数说明成 const 的。但是,若类中没有说明常成员函数,则函数将无法通过该参数来访问对象中的任何成员。
从另一个角度来看,使用常对象常常也就是不希望修改对象的内容。因此,常成员函数通常要设计成不改动类中数据成员的形式。比如,本例中的两个常成员函数一个中用来输出字符串的内容( Show( ) 函数),而另一个是测试一 个字符是否存在于串中( IsIn( ) 函数)。
注意成员函数 GetContent( ),这是一个返回常指针的函数,
而不是常成员函数。
11.5 指向类成员的指针指向类成员的指针也叫成员指针,它是用来存放类中成员的地址的。说明成员指针的一般形式为:
type class_name,,*pointer;
例:
char String,,*cPtr;
成员指针不是类中的成员,因此它必须在类外进行说明。另外,
成员指针是一个受一定限制的普通指针,它仅能指向指定类对象中类型与其相同的公有成员。例如,设 String 类中的 pStr
成员被说明成公有的,则可以用以下方式使用 cPtr:
String Str("A String");
cPtr = &Str.pStr;
cout << cPtr << endl;
第 12章 运算符重载
12.1 运算符重载运算符重载可能是 C++ 语言中最有趣的内容之一。所谓运算符重载,就是在类中说明的一种具有特殊格式的非静态成员函数。
12.1.1 重载运算符
type class_name,,operator @ (<arg_list>)
{
func_body;
}
其中,@ 为除以下 4 个之外的所有合法运算符:
:,,*(递引用运算符)?:
// 在 类 String( STRCLS.H )中添加两个公有的成员函数:
String operator + (String&);
String& operator = (Strint&);
// 在 STRCLS.CPP 中添加这两个函数的定义:
String String,,operator + (String& rs)
{
String temp;
temp.nLen = nLen + rs.nLen;
temp.pStr = new char[nLen + 1];
strcpy(temp.pStr,pStr);
strcat(temp.pStr,rs.pStr);
return temp;
}
String& String,,operator = (String& rs)
{
delete []pStr;
nLen = rs.nLen;
pStr = new char[nLen + 1];
strcpy(pStr,rs.pStr);
return *this;
}
说明:赋值运算符重载完全可以定义成一个 void 型函数,所以定义成一个返回同类对象或引用的函数是为了使重载的赋值运算符保留预定义的赋值运算符的性质,即“串连”赋值功能,
比如:
s3 = s2 = s1;
12.1.2 使用运算符重载
String s1("The C++"),s2("language"),s3;
s1 = s1 + " "; // 调用串类中的两个运算符重载
s3 = s1 + s2;
cout << s3.GetContent() << endl; // 也可以用 s3.Show();
// 输出为,The C++ language
当编译器遇到类似 s1 + s2 这样的表达式时,就将它解释为:
s1.operator + (s2)
即调用类中重载的加法运算符。而当遇到类似 s1 = s2 这样的表达式时,就将它解释为:
s1.operator = (s2)
即调用类中重载的赋值运算符。
观察上例,对于 s1 + "Constent" 这样的表达式,编译器又如何解释呢? String 类中并没有定义 operator + (char*) 这样的运算符重载呀!执行这样的表达式,仍然调用的是类中所定义的加法运算符重载。
当编译器遇到上述一个对象加一个串常量的表达式时,就试图找到一个 operator + (char*) 这样的运算符重载或者与表达式最为匹配的运算符重载。若找不到,它并不急于报错,而是试图利用类中已有的运算符重载 operator + (String&) 来匹配这一表达式。然而,该函数中的参数是一个串对象的引用,而不是字符串常量,类型不匹配。这时,编译器就会查找类的构造函数,看是否存在一个 String(char*) 这样用 C 串来创建串对象的构造函数。若存在,则通过该构造函数用常串创建一个临时的对象,以满足加法运算符重载的要求。
实用中,再重载一个 operator + (char*) 将可以避免许多麻烦。
但最佳选择还是使用友元运算符。
12.1.3 友元运算符除了以下 5 种运算符,所有的运算符重载都可以是类的友元函数:
= ( ) [ ] -> type
其中,= 包括所有的复合赋值运算符; ( ) 为函数调用运算符;
[ ] 为下标运算符; -> 为用于指针的成员访问运算符;而 type
则是任一合法的数据类型,这样的运算符重载叫做转换函数
(后面介绍)。
通常,将说明成类中成员函数的运算符重载称为成员(或类)
运算符;将说明成类的友元的运算符重载称为友元运算符。
// 将 String 类中的加法运算符重载改为友元
friend String operator + (String&,String&);
// 改写加法运算符重载函数
String operator + (String& rs1,String& rs2)
{
String temp;
temp.nLen = rs1.nLen + rs2.nLen;
temp.pStr = new char[temp.nLen + 1];
strcpy(temp.pStr,rs1.pStr);
strcat(temp.pStr,rs2.pStr);
return temp;
}
有了上述的定义,编译器在遇到 s1 + s2 这样的表达式时,就将它解释为:
operator + (s1,s2);
即调用前面定义的友元运算符。
对类似于 "Constant" + s 这样的表达式,编译器就将其解释为:
operator + ("Constant",s);
而成员运算符将对此表达式无能为力。这是因此字符串常量不是一个对象,它不存在加法运算符重载。
对比成员运算符和友元运算符可以发现:若重载的运算符是一个一元运算符,则成员运算符不能有参数,而友元运算符必须有一个类对象或引用的参数;若重载的运算符是一个二元运算符,则成员运算符必须有一个参数,而友元运算符必须有两个类对象或引用的参数。另外,二元运算符重载为友元使用起来比较方便。
12.1.4 转换函数转换函数是指类中定义的非静态成员函数,其一般形式为:
class_name,,operator type ( );
其中,type 为任一合法的数据类型,也就是转换函数返回值的类型。
例:为 String 类增加一个转换函数:
operator const char* () { return (const char*)pStr; }
operator int () { return nLen; }
前者可以替代 String 类中的 GetContent() 成员函数,后者可以替代其 Length() 成员函数 。例:
cout << const char*(s) << endl;
cout << int(s) << endl;
习题:
27,28