用static修饰数据成员
有时候,我们希望某些特定的数据在内存中只有一份,而且能够被一个类的所有对象共享,比如设计学生类时,可以定义一个属性用于统计学生的总人数,由于总人数只应该有一个有效值,因此完全不必在每个学生对象所占用的内存空间中都定义一个变量表示学生总人数,而可以在对象以外的空间定义一个表示总人数的变量让所有对象共享,具体内存中的分配情况如图1所示。
图1 学生总人数变量与普通数据成员的内存分布图
使用静态数据成员可以实现类中多个对象的数据共享和交互,静态数据成员的值对每个对象都一样,并且可以更新。只要对静态数据成员的值进行过更新,所有对象都会取到更新后的值。
C++中将使用static修饰的数据成员称为静态成员,具体定义形式如下所示:
static 类型标识符 静态数据成员名称;
对于静态数据成员来说,若其被声明具有public属性,则与普通的public数据成员类似,可以通过对象在类外完成访问,访问形式如下所示:
对象.公有静态数据成员 = xx;
然而由于静态数据成员不属于任何对象,访问静态数据成员有其独特的方式,可以通过类名直接对它进行访问,而无需通过类对象,并且通常采用这种方式。访问形式如下所示:
类名::静态数据成员名
若想对静态数据成员进行初始化,需要在类外通过“类名::静态数据成员 = 初值”的方法提供初值,初始化形式如下所示:
类名::静态数据成员 = 初值;
静态数据成员和C语言中的静态变量相同,由于静态数据位于数据段上,因此它们的生命期从编译阶段开始,到程序运行结束才终止。通常类的定义形式会放在头文件中,类中成员函数的实现会独立保存在一个.cpp文件中,由于静态数据成员只有一份,因此静态数据成员在类的定义形式中只有声明,没有初始化语句,对静态数据成员的初始化应该在.cpp中完成。
下面定义Student类,其中包含一个静态成员,代码如下所示:
class Student //定义学生类
{
public:
Student();
~Student();
static int s_nTotalNum; //静态数据成员
private:
char m_gName[20];
int m_nID;
};
int Student::s_nTotalNum = 0; //类外对静态数据成员初始化
上面代码中定义了Student类的静态数据成员s_nTotalNum用于记录学生总人数。
接下来通过一个案例来学习操作学生类中静态数据成员的用法,如例1所示。
例1
1 #include <iostream>
2 #include <cstring>
3 using namespace std;
4
5 class Date //日期类定义
6 {
7 public:
8 Date(int y, int m, int d); //声明带参数的构造函数
9 Date(Date &con_date); //声明拷贝构造函数
10 private:
11 int m_nYear, m_nMonth, m_nDay;
12 };
13 Date::Date(int y, int m, int d) //定义Date类带参数的构造函数
14 {
15 cout << "Date constructor!" << endl;
16 m_nYear = y;
17 m_nMonth = m;
18 m_nDay = d;
19 }
20
21 Date::Date(Date &con_date) //定义Date类的拷贝构造函数
22 {
23 m_nYear = con_date.m_nYear;
24 m_nMonth = con_date.m_nMonth;
25 m_nDay = con_date.m_nDay;
26 }
27 class Student //定义Student类
28 {
29 public:
30 static int s_nTotalNum; //静态数据成员
31 Student(char *con_name, int con_id, Date &con_birthday);
32 ~Student();
33 private:
34 char m_gName[20];
35 int m_nID;
36 Date m_iBirthday;
37 };
38
39 //定义Student类带参数的构造函数
40 Student::Student(char* con_name, int con_id,
41 Date &con_birthday):m_iBirthday(con_birthday)
42 {
43 int namelen = strlen(con_name) + 1;
44 strcpy_s(m_gName, namelen, con_name);
45 m_nID = con_id;
46 s_nTotalNum++; //通过构造函数每增加一个对象s_nTotalNum变量增1
47 }
48 Student::~Student()
49 {
50 s_nTotalNum--; //析构一个对象,s_nTotalNum变量减1
51 cout << "destructor, totalnum = " << s_nTotalNum << endl;
52 if (s_nTotalNum == 0)
53 system("pause");
54 }
55
56 int Student::s_nTotalNum = 0; //静态数据的初始化
57 int main()
58 {
59 Date tombirthday(1998, 5, 20);
60 //创建一个Student对象
61 Student std_tom("Tom", 1, tombirthday);
62 cout << "Tom, the totalnum = " << std_tom.s_nTotalNum << endl;
63
64 Date paulbirthday(1998, 4, 12);
65 Student std_paul("paul", 2, tombirthday);//创建第二个Student对象
66 cout << "Paul, the totalnum = " << std_paul.s_nTotalNum << endl;
67 return 0;
68 }
运行结果如图2所示。
图2 例1运行结果
例1中,Student类定义了一个静态数据成员s_nTotalNum用于记录学生总人数,该数据可以被多个对象共享。定义静态数据初始值为0,如代码第56行。Student类的构造函数用于对创建学生对象,每创建一个对象s_nTotalNum应该增加1,对象消失时通过析构函数将s_nTotalNum减1。
main()函数中创建了两个对象,运行结果显示每创建一个对象s_nTotalNum增1,若有一个对象的生命期结束则s_nTotalNum值减1,由此可知,静态数据成员是多个对象的共享数据,在内存中只有一份拷贝。
通过前面的学习,我们知道具有不同属性的数据成员,其定义及操作方式也有很大差异,导致这种差异的原因就在于不同属性的数据成员位于进程地址空间的不同区域。在C语言中已经介绍过不同变量在内存中的分布情况,C++中的数据成员与C语言中的变量一样,也被分配在不同的内存区间上,数据所处内存区间的不同,将影响数据的属性及操作方法。
接下来结合典型的进程地址空间内存分布图,来分析例2-16类中不同数据成员的存储特点。例1中类Date、Student数据成员的内存分布如图3所示。
图3 数据内存分布图
由图3可知,不同属性的数据会分布在不同的内存区间,与C语言中的数据布局相同,全局、静态变量会存储在数据区中,在程序运行期间常驻内存,例2-16中定义了静态数据成员s_nTotalNum,该成员被分配在数据区中,该成员在程序运行期间常驻内存,不会因为对象的产生和消亡而有任何影响,因此静态数据成员一般通过“类名::静态成员”形式进行访问。普通的局部变量保存在栈中,类中的普通数据成员也被分配在栈中,Date类的m_nYear、m_nMonth、m_nDay以及Student类中的m_gName、m_nID、m_iBirthday均被分配在栈中,这些数据成员随着对象被定义而分配空间,对象消亡则空间释放。此外,new和delete操作的是堆区空间,在程序运行期间根据需要进行空间的动态申请及释放。