数据的共享与保护
C++语言是适合于编写大型复杂程序的语言,数据的共享与保护机制是C++ 语言的
重要特性之一。本章介绍标识符的作用域、可见性和生存期的概念,以及类成员的共享
与保护问题。最后介绍程序的多文件结构和编译预处理命令,即如何用多个源代码文件
来组织大型程序。
5.1 标识符的作用域与可见性
作用域讨论的是标识符的有效范围,可见性是讨论标识符是否可以被引用。我们知道, 
在某个函数中声明的变量就只能在这个函数中起作用,这就是受变量的作用域与可见性的
限制。作用域与可见性既相互联系又存在着很大差异。
5.1.1 作用域
作用域是一个标识符在程序正文中有效的区域。C++ 语言中标识符的作用域有函数
原型作用域、局部作用域(块作用域)、类作用域、文件作用域、命名空间作用域和限定作用域
的enum 枚举类。
1.函数原型作用域
函数原型作用域是C++程序中最小的作用域。第3章中介绍过,在函数原型中一定要
包含型参的类型说明。在函数原型声明时形式参数的作用范围就是函数原型作用域。例
如,有如下函数声明: 
double area(double radius); 
标识符radius的作用(或称有效)范围就在函数area形参列表的左右括号之间,在程序
的其他地方不能引用这个标识符。因此标识符radius的作用域称作函数原型作用域。
注意 由于在函数原型的形参列表中起作用的只是形参类型,标识符并不起作用,因
此是允许省去的。但考虑到程序的可读性,通常还是要在函数原型声明时给出形参标
识符。
2.局部作用域
为了理解局部作用域,先来看一个例子。

void fun(int a) { 
int b=a; 
cin>>b; 
if(b>0) { 
int c; 
… } c 的作用域 
} 
} 
ü
t
y
.... 
.... 
b 的作用域
ü
t
y
..... 
..... 
a 的作用域
这里,在函数fun的形参列表中声明了形参a,在函数体内声明了变量b,并用a的值初
始化b。接下来,在if语句内,又声明了变量c。a、b和c都具有局部作用域,只是它们分别
属于不同的局部作用域。
函数形参列表中形参的作用域,从形参列表中的声明处开始,到整个函数体结束之处为
止。因此,形参a的作用域从a的声明处开始,直到fun函数的结束处为止。函数体内声明
的变量,其作用域从声明处开始,一直到声明所在的块结束的花括号为止。所谓块,就是一
对花括号括起来的一段程序。在这个例子中,函数体是一个块,if语句之后的分支体又是一
个较小的块,二者是包含关系。因此,变量b的作用域从声明处开始,到它所在的块(即整个
函数体)结束处为止;而变量c的作用域从声明处开始,到它所在的块,即分支体结束为止。
具有局部作用域的变量也称为局部变量。
3.类作用域
类可以被看成是一组有名成员的集合,类X的成员m 具有类作用域,对m 的访问方式
有如下3种。
(1)如果在X 的成员函数中没有声明同名的局部作用域标识符,那么在该函数内可以
直接访问成员m。也就是说m 在这样的函数中都起作用。
(2)通过表达式x.m 或者X::m。这正是程序中访问对象成员的最基本方法。X::m 
的方式用于访问类的静态成员,相关内容将在5.3节介绍。
(3)通过ptr->m 这样的表达式,其中ptr为指向X 类的一个对象的指针。关于指针将
在第6章详细介绍。
C++中,类及其对象还有其他特殊的访问和作用域规则,在后续章节中还会深入讨论。
4.文件作用域
不在前述各个作用域中出现的声明,就具有文件作用域,这样声明的标识符其作用域开
始于声明点,结束于文件尾。例5-1中所声明的全局变量就具有文件作用域,它们在整个文
件中都有效。
5.命名空间作用域
生活中存在重名现象,在C++应用程序中,也存在同名变量、函数和类等情况,为避免
重名冲突,使编译器能够区分来自不同库的同名实体,C++引入了命名空间的概念,它本质
上定义了实体所属的空间。命名空间定义使用namespace关键字,声明方式如下: 
namespace namespace_name{ 
//代码声明
} 
第5章 数据的共享与保护·151·

使用某个命名空间中的函数、变量等实体,需要命名空间∷ 实体名称或通过using 
namespacenamespace_name的方式。例5-1中usingnamespacestd使得标准命名空间中
实体调用无须加空间前缀,而my_space中的func通过∷方式调用。
6.限定作用域的enum 枚举类
我们在第4章介绍了enum 枚举类,枚举类分为限定作用域和不限定作用域两种,由于
之前未涉及作用域的概念,因此第4章只给了不限定作用域的例子,在这里对限定作用域的
enum 枚举类型做更深入的讨论。
定义限定作用域的枚举类型的方式是enumclass{...},即多了class或struct限定符, 
此时枚举元素的名字遵循常规的作用域准则,即类作用域,在枚举类型的作用域外是不可访
问的。相反,在不限定作用域的枚举类型中,枚举元素的作用域与枚举类型本身的作用域
相同: 
enum color {red, yellow, green}; //不限定作用域的枚举类型
enum color1 {red, yellow, green}; //错误,枚举元素重复定义
enum class color2 {red, yellow, green}; //正确,限定作用域的枚举元素被隐藏了
color c=red; //正确,color 的枚举元素在有效的作用域中
color2 c1=red; //错误,color2 的枚举元素不在有效的作用域中
color c2=color::red; //正确,允许显式地访问枚举元素
color2 c3=color2::red; //正确,使用了color2 的枚举元素
例5-1 作用域实例。 
//5_1.cpp 
#include <iostream> 
using namespace std; 
int i; //全局变量,文件作用域
int main() { 
i=5; //为全局变量i 赋值 
{ //子块1 
int i; //局部变量,局部作用域 
i=7; 
cout<<"i="<<i<<endl; //输出7 
} 
cout<<"i="<<i<<endl; //输出5 
return 0; 
}
运行结果: 
i=7 
i=5 
在这个例子中,在主函数之外声明的变量i具有文件作用域,它的有效作用范围到文件
尾才结束。在主函数开始处给这个具有文件作用域的变量赋初值5,接下来在块1中又声
·152· C++语言程序设计(第5版)

明了同名变量并赋初值7。第一次输出的结果是7,这是因为具有局部作用域的变量把具有
文件作用域的变量隐藏了,也就是具有文件作用域的变量变得不可见(这是下面要讨论的可
见性问题)。当程序运行到块1结束后,进行第二次输出时,输出的就是具有文件作用域的
变量的值5。
具有文件作用域的变量也称为全局变量。
5.1.2 可见性
现在,让我们从标识符引用的角度,来看标识符的有效范围,即标识符的可见性。程序
运行到某一点,能够引用到的标识符,就是该处可见的标识符。为了理解可见性,先来看一
图5-1 作用域关系图
看不同作用域之间的关系。文件作用域最大,接下
来依次是类作用域和局部作用域。图5-1描述了
作用域的一般关系。可见性表示从内层作用域向
外层作用域“看”时能看到什么。因此,可见性和作
用域之间有着密切的关系。
作用域可见性的一般规则是: 
● 标识符要声明在前,引用在后。
● 在同一作用域中,不能声明同名的标识符。
● 在没有互相包含关系的不同的作用域中声明的同名标识符,互不影响。
● 如果在两个或多个具有包含关系的作用域中声明了同名标识符,则外层标识符在内
层不可见。
再看一下例5-1,这是文件作用域与块作用域相互包含的实例,在主函数内块1之外,可
以引用具有文件作用域的变量,也就是说它是可见的。当程序运行进入块1后,就只能引用
具有局部作用域的同名变量,具有文件作用域的同名变量被隐藏了。
提示 作用域和可见性的原则不只适用于变量名,也适用于其他各种标识符,包括常量
名、用户定义的类型名、函数名、枚举类型的取值等。
5.2 对象的生存期
对象(包括简单变量)都有诞生和消失的时刻。对象从诞生到结束的这段时间就是它的
生存期。在生存期内,对象将保持它状态(即数据成员的值),变量也将保持它的值不变,直
到它们被更新为止。本节,使用对象来统一表示类的对象和一般的变量。对象的生存期可
以分为静态生存期和动态生存期两种。
5.2.1 静态生存期
如果对象的生存期与程序的运行期相同,我们称它具有静态生存期。在文件作用域中
声明的对象都是具有静态生存期的。如果要在函数内部的局部作用域中声明具有静态生存
期的对象,则要使用关键字static,例如下列语句定义的变量i便是具有静态生存期的变量, 
也称为静态变量: 
static int i; 
第5章 数据的共享与保护·153·

局部作用域中静态变量的特点是,它并不会随着每次函数调用而产生一个副本,也不会
随着函数返回而失效,也就是说,当一个函数返回后,下一次再调用时,该变量还会保持上一
回的值,即使发生了递归调用,也不会为该变量建立新的副本,该变量会在各次调用间共享。
在定义静态变量的同时也可以为它赋初值,例如: 
static int i=5; 
这表示i会被以5初始化,而非每次执行函数时都将i赋值为5。
类的数据成员也可以用static修饰,本章5.3节将专门讨论类的静态成员。
细节 定义时未指定初值的基本类型静态生存期变量,会被以0值初始化,而对于动态
生存期变量,不指定初值意味着初值不确定。
5.2.2 动态生存期
除了上述两种情况,其余的对象都具有动态生存期。在局部作用域中声明的具有动态
生存期的对象,习惯上也被称为局部生存期对象。局部生存期对象诞生于声明点,结束于声
明所在的块执行完毕之时。
提示 类的成员对象也有各自的生存期。不用static修饰的成员对象,其生存期都与
它们所属对象的生存期保持一致。
例5-2 变量的生存期与可见性。 
//5_2.cpp 
#include <iostream> 
using namespace std; 
int i=1; //i 为全局变量,具有静态生存期
void other() { 
//a, b 为静态局部变量,具有全局寿命,局部可见,只第一次进入函数时被初始化 
static int a=2; 
static int b; 
//c 为局部变量,具有动态生存期,每次进入函数时都初始化 
int c=10; 
a+=2; 
i+=32; 
c+=5; 
cout<<"---OTHER---"<<endl; 
cout<<" i: "<<i<<" a: "<<a<<" b: "<<b<<" c: "<<c<<endl; 
b=a; 
}
int main() { 
//a 为静态局部变量,具有全局寿命,局部可见 
static int a; 
//b, c 为局部变量,具有动态生存期 
int b=-10; 
·154· C++语言程序设计(第5版)

int c=0; 
cout<<"---MAIN---"<<endl; 
cout<<" i: "<<i<<" a: "<<a<<" b: "<<b<<" c: "<<c<<endl; 
c+=8; 
other(); 
cout<<"---MAIN---"<<endl; 
cout<<" i: "<<i<<" a: "<<a<<" b: "<<b<<" c: "<<c<<endl; 
i+=10; 
other(); 
return 0; 
}
运行结果: 
---MAIN--- 
i: 1 a: 0 b: -10 c: 0 
---OTHER--- 
i: 33 a: 4 b: 0 c: 15 
---MAIN--- 
i: 33 a: 0 b: -10 c: 8 
---OTHER--- 
i: 75 a: 6 b: 4 c: 15 
例5-3 具有静态、动态生存期对象的时钟程序。
这里仍以时钟类的为例,在这个实例中,声明了具有函数原型作用域、局部作用域、类作
用域和文件作用域的多个对象,我们来具体分析它们各自的可见性和生存期。 
//5_3.cpp 
#include<iostream> 
using namespace std; 
class Clock { //时钟类定义
public: //外部接口 
Clock(); 
void setTime(int newH, int newM, int newS); //3 个形参均具有函数原型作用域 
void showTime(); 
private: //私有数据成员 
int hour, minute, second; 
}; 
//时钟类成员函数实现
Clock::Clock() : hour(0), minute(0), second(0) {} //构造函数
void Clock::setTime(int newH, int newM, int newS) { //3 个形参均具有局部作用域 
hour=newH; 
minute=newM; 
第5章 数据的共享与保护·155·

second=newS; 
}
void Clock::showTime() { 
cout<<hour<<":"<<minute<<":"<<second<<endl; 
}
Clock globClock; //声明对象globClock,具有静态生存期,文件作用域
//由默认构造函数初始化为0:0:0 
int main() { //主函数 
cout<<"First time output:"<<endl; 
//引用具有文件作用域的对象globClock: 
globClock.showTime(); //对象的成员函数具有类作用域 
//显示0:0:0 
globClock.setTime(8,30,30); //将时间设置为8:30:30 
Clock myClock(globClock); //声明具有块作用域的对象myClock 
//调用复制构造函数,以globClock 为初始值 
cout<<"Second time output:"<<endl; 
myClock.showTime(); //引用具有块作用域的对象myClock 
//输出8:30:30 
return 0; 
}
运行结果: 
First time output: 
0:0:0 
First time output: 
8:30:30 
在这个程序中,包含了具有各种作用域类型的变量和对象,其中时钟类定义中函数成员
setTime的3个形参具有函数原型作用域;setTime函数定义中的3个参数、对象myClock 
具有局部作用域;时钟类的数据、函数成员具有类作用域;对象globClock具有文件作用域。
在主函数中,这些变量、对象及公有其成员都是可见的。就生存期而言,除了具有文件作用
域的对象globClock具有静态生存期,与程序的运行期相同外,其余都具有动态生存期。
5.3 类的静态成员
在结构化程序设计中程序模块的基本单位是函数,因此模块间对内存中数据的共享是
通过函数与函数之间的数据共享来实现的,其中包括两个途径———参数传递和全局变量。
面向对象的程序设计方法兼顾数据的共享与保护,将数据与操作数据的函数封装在一
起,构成集成度更高的模块。类中的数据成员可以被同一类中的任何一个函数访问。这样
一方面在类内部的函数之间实现了数据的共享,另一方面这种共享是受限制的,可以设置适
·156· C++语言程序设计(第5版)

当的访问控制属性,把共享只限制在类的范围之内,对类外来说,类的数据成员仍是隐藏的, 
达到了共享与隐藏两全。
然而这些还不是数据共享的全部。对象与对象之间也需要共享数据。
静态成员是解决同一个类的不同对象之间数据和函数共享问题的。例如,我们可以抽
象出某公司全体雇员的共性,设计如下雇员类: 
class Employee { 
private: 
int empNo; 
int id; 
string name; //字符串对象,第6 章详细介绍 
… 
//其他数据成员与函数成员略
}
如果需要统计雇员总数,这个数据存放在什么地方呢? 若以类外的变量来存储总数,不
能实现数据的隐藏。若在类中增加一个数据成员用以存放总数,必然在每个对象中都存储
一副本,不仅冗余,而且每个对象分别维护一个“总数”,容易造成数据的不一致性。由于这
个数据应该是为Employee类的所有对象所共享的,比较理想的方案是类的所有对象共同
拥有一个用于存放总数的数据成员,这就是下面要介绍的静态数据成员。
5.3.1 静态数据成员
我们说“一个类的所有对象具有相同的属性”,是指属性的个数、名称、数据类型相同,各
个对象的属性值则可以各不相同,这样的属性在面向对象方法中称为“实例属性”,在C++ 
程序中以类的非静态数据成员表示。例如上述Employee类中的empNo、id、name都是以
非静态数据成员表示的实例属性,它们在类的每个对象中都有,这样的实例属性正是每个对
象区别于其他对象的特征。
面向对象方法中还有“类属性”的概念。如果某个属性为整个类所共有,不属于任何一
个具体对象,则采用static关键字来声明为静态成员。静态成员在每个类只有一份,由该类
的所有对象共同维护和使用,从而实现了同一类的不同对象之间的数据共享。类属性是描
述类的所有对象共同特征的一个数据项,对于任何对象实例,它的属性值是相同的。简单地
说,如果将“类”比作一个工厂,对象是工厂生产出的产品,那么静态成员是存放在工厂中、属
于工厂的,而不是属于每个产品的。
静态数据成员具有静态生存期。由于静态数据成员不属于任何一个对象,因此可以通
过类名对它进行访问,一般的用法是“类名::标识符”。在类的定义中仅仅对静态数据成员
进行引用性声明,必须在文件作用域的某个地方使用类名限定进行定义性声明,这时也可以
进行初始化。C++11标准支持常量表达式类型修饰(constexpr或const)的静态常量在类
内初始化,此时仍可在类外定义该静态成员,但不能做再次初始化操作。在UML中,静态
数据成员是通过在数据成员下方添加下画线来表示。从下面的例子中可以看到静态数据成
员的作用。
提示 之所以类的静态数据成员需要在类定义之外再加以定义,是因为需要以这种方
第5章 数据的共享与保护·157·

式专门为它们分配空间。非静态数据成员无须以此方式定义,是因为它们的空间是与它们
图5-2 包含静态数据成员的
Point类的UML图
所属对象的空间同时分配的。
例5-4 具有静态数据成员的Point类。
这个程序是由第4章的Point类修改而来,引入静态
数据成员count用于统计Point类的对象个数。包含静态
数据成员count的Point类的UML 图形表示如图5-2 
所示。 
//5_4.cpp 
#include <iostream> 
using namespace std; 
class Point { //Point 类定义
public: //外部接口 
Point(int x=0, int y=0) : x(x), y(y) {//构造函数 
//在构造函数中对count 累加,所有对象共同维护同一个count 
count++; 
} 
Point(Point &p) { //复制构造函数 
x=p.x; 
y=p.y; 
count++; 
} 
~Point() { count--;} 
int getX() {return x;} 
int getY() {return y;} 
void showCount() { //输出静态数据成员 
cout<<" Object count="<<count<<endl; 
} 
private: //私有数据成员 
int x, y; 
static int count; //静态数据成员声明,用于记录点的个数 
constexpr static int origin=0; //常量静态成员类内初始化
}; 
int Point::count=0; //静态数据成员定义和初始化,使用类名限定
constexpr int Point::origin; //类外定义常量静态成员,但不可二次初始化
int main() { //主函数 
Point a(4, 5); //定义对象a,其构造函数会使count 增1 
cout<<"Point A: "<<a.getX()<<", "<<a.getY(); 
a.showCount(); //输出对象个数 
Point b(a); //定义对象b,其构造函数会使count 增1 
cout<<"Point B: "<<b.getX()<<", "<<b.getY(); 
·158· C++语言程序设计(第5版)

b.showCount(); //输出对象个数 
return 0; 
}
运行结果: 
Point A: 4, 5 Object count=1 
Point B: 4, 5 Object count=2 
上面的例子中,类Point的数据成员count被声明为静态,用来给Point类的对象计数, 
每定义一个新对象,count的值就相应加1。静态数据成员count的定义和初始化在类外进
行,初始化时引用的方式也很值得注意,首先应该注意的是要利用类名来引用,其次,虽然这
个静态数据成员是私有类型,在这里却可以直接初始化。除了这种特殊场合,在其他地方, 
例如主函数中就不允许直接访问了。count的值是在类的构造函数中计算的,a对象生成
时,调用有缺省参数的构造函数,b对象生成时,调用复制构造函数,两次调用构造函数访问
的均是同一个静态成员count。通过对象a和对象b分别调用showCount函数输出的也是
同一个count在不同时刻的数值。这样,就实现了a、b两个直接的数据共享。
提示 在对类的静态私有数据成员初始化的同时,还可以引用类的其他私有成员。例
如,如果一个类T存在类型为T的静态私有对象,那么可以引用该类的私有构造函数将其
初始化。
5.3.2 静态函数成员
在例5-4中,函数showCount是专门用来输出静态成员count的。要输出count只能
通过Point类的某个对象来调用函数showCount。在所有对象声明之前count的值是初始
值0。如何输出这个初始值呢? 显然由于尚未声明任何对象,无法通过对象来调用
showCount。由于count是为整个类所共有的,不属于任何对象,因此我们自然会希望对
count的访问也不要通过对象。现在尝试将例5-4中的主函数改写如下: 
int main() { 
Point::showCount(); //直接通过类名调用函数输出对象个数的初始值 
Point a(4,5); 
cout<<"Point A, "<<a.getX()<<", "<<a.getY(); 
a.showCount(); 
Point b(a); 
cout<<"Point B, "<<b.getX()<<", "<<b.getY(); 
b.showCount(); 
}
但是不幸得很,编译时出错了,对普通函数成员的调用必须通过对象名。
尽管如此C++中还是可以有办法实现我们上述期望的,这就是使用静态成员函数。所
谓静态成员函数就是使用static关键字声明的函数成员,同静态数据成员一样静态成员函
数也属于整个类,由同一个类的所有对象共同拥有,为这些对象所共享。
静态成员函数可以通过类名或对象名来调用,而非静态成员函数只能通过对象名来
第5章 数据的共享与保护·159·