类的静态成员
实现数据共享有很多方法,设置全局性的变量或者对象是一种方法。但是全局变量和对象是有局限性的。类的静态成员的提出是为了解决同类对象之间的数据共享问题。
考虑如下学生类中,如果要统计当前学生总数totalNumber,这个数据应该存放在什么地方合适?如果将该数据存放在类外部的全局变量,则无法实现数据的隐藏;若在类中增加一个数据成员存放学生总数的信息,则每个对象的存储空间都会有这样一个数据副本,不仅冗余,还会造成数据的不一致性,给数据维护带来不便。
class Student{
private:
int StuNo;
char *name;
......
int totalNumber;
.....
};
实际上学生总数totalNumber应该是学生类Student的所有对象所共享的,比较理想的方案就是类的所有对象共享这一数据,程序运行时内存是允许存在一个拷贝!
静态数据成员
首先要明确一个原则:类的什么样的数据成员可以定义为静态数据成员?通过前面的学习,我们了解到类的数据成员描述的是该类所有对象共有的特征,比如学生类中的数据成员学号,姓名等。每个学生对象都具有这样的属性,这些数据成员在每个学生对象的内存空间都有一个副本,用于区分每个不同学生对象的状态。但对象学生总数totalNumber这样的数据成员,其描述的特征不属于某个具体的学生对象,而是描述整个学生类别的特征。对于某个学生对象来说学生总数是无意义的,totalNumber这一成员只是用来描述整个学生类的所有对象的个数,即学生总数。**这样的属性是专门用于描述类别而非用于描述具体类对象的。
类中用于描述类的属性和数据成员,定义时通过使用static关键字区分,表示这是一个类的静态成员。
使用静态数据成员可以节省内存,因为它是所有对象所公有的,因此对多个对象来说,静态数据成员只存储在一处,并供所有对象使用。静态数据成员的值对每个对象都是一样的,但它的值是可以更新的。只要对静态数据成员的值更新一次,保证所有对象存取更新后的相同的值,这样就可以提高时间效率。**由于静态数据成员不属于任何一个对象,因此可以通过类名对它进行访问,一般用法是:
类名::标识符
在类的定义中仅仅对静态数据成员进行了引用性声明,还必须在类外部进行初始化。
静态数据成员的使用方法和注意事项如下:
(1) 静态数据成员在定义或说明时前面加关键字static.
(2) 静态成员初始化与一般数据成员初始化不同。静态数据成员初始化需要在全局作用域范围使用如下格式进行:
<数据类型><类名>::<静态数据成员名> = <值>
(3) 静态数据成员是静态存储的,它是静态生存期,必须对它进行初始化。
(4) 引用静态数据成员时,采用如下格式:
<类名>::<静态成员名>
下面介绍一个具有静态成员的Point类
#include <iostream>
using namespace std;
class Point{
private:
int X,Y;
static int countP;//此处不可以赋值
public:
Point(int xx = 0,int yy = 0){
X = xx;
Y = yy;
countP++;
}
~Point(){
countP--;
}
Point(Point &p);
int GetX(){
return X;
}
int GetY(){
return Y;
}
void GetCount(){
cout<<"countP = "<<countP<<endl;
}
};
Point::Point(Point &p){
X = p.X;
Y = p.Y;
countP++;
}
int Point::countP = 0; // 在文件作用域内初始化
int main()
{
Point A(4,5);
cout<<"Point A,"<<A.GetX()<<","<<A.GetY();
A.GetCount();
Point B(A);
cout<<"Point B,"<<B.GetX()<<","<<B.GetY();
B.GetCount();
return 0;
}
运行结果如下:
上例中Point类有一个静态数据成员名为countP,表示是当前系统中当前点对象的个数。因此在Point类的构造函数与拷贝构造函数中都将该静态成员值+1.因为构造函数和拷贝构造函数在调用时,系统中点对象个数会增加,相应的在Point类的析构函数中将该静态成员值-1,表示系统中点对象的个数-1.注意在主程序中,不管构造多少点对象,系统中只有一个countP的拷贝,Point类的所有对象共享这一静态数据。
静态函数成员
在上节静态数据成员的代码案例中存在一个小小的BUG。当我们需要知道程序运行时系统有多少个点对象时,通过调用成员函数GetCount()可以获取。但该方法是一个普通的函数成员,必须通过对象名才能调用。如果当前系统中点对象的个数是0,即还没有构造出任何点对象的时候,则无法通过对象名来访问该方法以获取点对象的个数。那么应该如何解决这个问题呢?
实际上,静态成员countP是属于类的属性,与具体的某个对象无关,因此即使系统没有初始化任何对象的时候,也是可以访问静态属性countP的。问题在于访问countP的程序代码应该放在什么位置合适?由于普通的成员函数必须通过对象名才能访问,那么是否存在一种类的成员函数可以不通过对象名直接访问呢?答案是肯定的,与静态数据成员对应的一种类成员是静态函数成员。静态函数成员在定义的时候加上static关键字即可。与静态数据成员一样,静态函数成员也是属于某个类的,可以通过类名直接访问静态函数成员。因此可以将访问静态数据成员的代码写在类的静态成员函数中,再通过类名直接访问就可以了。
我们通过下面的例子来进行分析:
#include <iostream>
using namespace std;
class Point{
private:
int X,Y;
static int countP;
public:
Point(int xx = 0,int yy = 0){
X = xx;
Y = yy;
countP++;
}
~Point(){
countP--;
}
Point(Point &p);
int GetX(){
return X;
}
int GetY(){
return Y;
}
static void GetCount(){
cout<<"countP = "<<countP<<endl;
}
};
Point::Point(Point &p){
X = p.X;
Y = p.Y;
countP++;
}
int Point::countP = 0; //在文件作用域初始化
int main(void)
{
Point::GetCount();
Point A(4,5);
cout<<"Point A:"<<A.GetX()<<','<<A.GetY()<<endl;
Point B(A);
cout<<"Point B:"<<B.GetX()<<','<<B.GetY()<<endl;
Point::GetCount();
return 0;
}
运行结果如下:
与之前静态成员变量案例中的代码相比较,这个案例仅仅是在定义成员函数GetCount的时候添加了关键字static,使其成为类的静态函数成员,这样就可以通过类名直接访问静态成员函数,并可以不用依赖任何对象直接访问类的静态数据成员,并可以不用依赖任何对象直接访问类的静态数据成员。
下面列出几个有关类的静态数据成员与静态成员函数比较容易出错的地方!
(1) 不能通过类名来调用类的非静态成员函数,例如:
void main()
{
Point::GetCount();
Point::GetX();
}
会编译出错:
[Error] cannot call member function ‘int Point::GetX()’ without object
(2) 通过类的对象可以调用静态成员函数和非静态成员函数,例如:
void main()
{
Point p1(1,2);
p1.GetCount();
p1.GetX();
}
(3) 静态成员函数中不能引用非静态成员。因为静态成员函数属于整个类,在类实例化之前就已经分配空间了,而类的非静态成员必须在类实例化之后才有内存空间。
(4) 类的非静态成员函数可以调用静态成员函数,但反之不能。
(5) 类的静态成员变量必须先初始化再使用。