Monday, May 5, 2008

解析static关键字

文章名:解析static 关键字
出 处:vc编程网http://vcprogram.6to23.com/
作 者:vcprogram

内容提要 文章详细讲解了static的两方面的含义:静态存储和控制连接。

关键词 static,静态存储,成员函数

文章正文

通常理解static只是指静态存储的概念,事实上在c++里面static包含了两方面的含义。
1) 在固定地址上的分配,这意味着对象是在一个特殊的静态区域上创建的,而不是每次函数调用的时候在堆栈上动态创建的,这是static的静态存储的概念。
2) 另一方面,static能够控制对象对于连接器的可见性。一个static对象,对于特定的编译单元来说总是本地范围的,这个范围包括本地文件或者本地的某一个类,超过这个范围的文件或者类是不可以看到static对象的,这同时也描述了连接的概念,它决定连接器能够看到哪些名字。 关于静态存储

对于一个完整的程序,他们在内存中的分布情况如下图:

程序内存空间

(code area)
代码区

(data area)
全局数据区

(heap area)
堆区

(stack area)
栈区

一般程序的由malloc,realloc产生的动态数据存放在堆区;程序的局部数据即各个函数内部的数据存放在栈区,局部数据对象一般会随着函数的退出而释放空间;对于static数据即使是函数内部的对象也存放在全局数据区,全局数据区的数据并不会因为函数的退出就将空间释放。

函数内部的静态变量

通常,在函数体内定义一个变量的时候,编译器使得每次函数调用时候堆栈的指针向下移动一个合适的位置,为这些内部变量分配内存。如果这个变量是一个初始化表达式,那么每当程序运行到这儿的时候程序都需要对表达式进行初始化。这种情况下变量的值在两次调用之间则不能进行保存。(局部变量每次在函数调用的时候都有压栈和出栈的过程

有的时候我们却需要在两次调用之间对变量的值进行保存,通常的想法是定义一个全局变量来实现。但这样一来,变量已经不再属于函数本身了,不再仅受函数的控制。因此我们有必要将变量声明为static对象,这样对象将保存在静态存储区域,而不是保存在堆栈中。对象的初始化仅仅在函数第一个被调用的时候进行初始化,每次的值保持到下一次调用,直到新的值覆盖它。下面的例子解释了这一点。

//****************************************
//1.cpp
//****************************************
1 #include
2 void add_n(void);
3 void main(){
4 int n=0;
5 add_n();
6 add_n();
7 add_n();
8 }
9 void add_n(void){
10 static n=50;
11 cout<<”n=”<static变量必须被初始化,但是零值初始化仅仅只对系统预定义类型有效,比如int,char,bool等等。事实上我们用到的不仅仅是这些预定义类型,大多数情况下可能用到结构,联合或者类等各种用户自定义的类型,对于这些类型用户必须使用构造函数进行初始化。如果我们在定义一个静态对象的时候没有指定构造函数的参数,那就必须使用缺省的构造函数,如果缺省的构造函数也没有的话则会出错。看下面的例子。
//**************************************************
// 2.cpp
//**************************************************
1 #include
2 class x{
3 int i;
4 public:
5 x(int i=0):i(i) {
6 cout<<”i=”<类中的静态成员

static静态存储的概念可以进一步引用到类中来。c++中的每一个类的对象都是该类的成员的拷贝,一般情况下它们彼此之间没有什么联系,但有时候我们需要让它们之间共享一些数据,我们可以通过全局变量来实现,但是这样的结果是导致程序的不安全性,因为任何函数都可以对这个变量进行访问和修改,而且容易与项目中的其余的名字产生冲突,因此我们需要一种两全其美的方法,既可以当成全局数据变量那样存储,又可以隐藏在类的内部,与类本身联系起来,这样只有类的对象才可以操纵这个变量,从而增加了变量的安全性。

这种变量我们称之为类的静态成员,静态成员包括静态数据成员和静态成员函数。类的所有的静态数据成员有着单一的存储空间而不管类的对象由多少,这些对象共享这块存储区域。因此每一个类的对象的静态成员发生改变都会对其余的对象产生影响。先看下面的例子。
//**************************************
// student.cpp
//**************************************
1. #include
2. #include
3. class student{
4. public:
5. student(char* pname=”no name”){
6. cout<<”create one student”静态数据成员

在代码中我们可以看出,number既不是对象s1也不是对象s2的一部分,它是属于student 这个类的。每一个student对象都有name成员,但是number成员却只有一个,所有的student对象共享使用这个成员。s1.number与s2.number是等值的。在student的对象空间中没有为number成员保留空间,它的空间分配不在student的构造函数里完成,空间回收也不再析构函数里面完成。因此与name成员不一样,它不会随着对象的产生或者消失而产生或消失。

由于静态数据成员的空间分配在全局数据区,因此在程序一开始运行的时候就必须存在,所以静态成员的空间的分配和初始化不可能在函数包括main主函数中完成,因为函数在程序中被调用的时候才为内部的对象分配空间。这样,静态成员的空间分配和初始化只能由下面的三种途径。一是类的外部接口的头文件,那里声明了类定义。二是类定义的内部实现,那里有类成员函数的定义和具体实现。三是应用程序的main()函数前的全局数据声明和定义处。由于静态数据成员必须实际的分配空间,因此不可能在类定义头文件中分配内存。另一方面也不能在头文件中类声明的外部定义,因为那会造成多个使用该类的源程序重复出现定义。静态数据成员也不能在main()函数的全局数据声明处定义。如果那样的话每一个使用该类的程序都必须在程序的main()函数的全局数据声明处定义一下该类的静态成员。这是不现实的。唯一的办法就是将静态数据成员的定义放在类的实现中。定义时候用类名引导,引用时包含头文件即可

例如 class a{
static int i;
public:
//
}
在类定义文件中 int a::i=1;

有两点需要注意的是

1. 静态数据成员必须且只能定义一次,如果重复定义,连接器会报告错误。同时它们的初始化必须在定义的时候完成。对于预定义类型的静态数据成员,如果没有赋值则赋零值。对于用户自定义类型必须通过构造函数赋值,如果没有构造函数包括缺省构造函数,编译无法通过。

2. 局部类中不允许出现静态数据成员。因为静态数据成员必须在程序运行时候就存在这导致程序无法为局部类中的静态数据成员分配空间。下面的代码是不允许的。

void fn(){
class foo{
static int i;//定义非法。
public:
//
}
}

静态函数成员

与静态数据成员一样,我们也可以创建一个静态成员函数,它为类的全部服务而不是为某一个类的具体对象服务。静态函数成员与静态成员一样,都是类的内部实现,属于类定义的一部分。程序student.cpp中的number()就是静态成员函数,它的定义位置与一般成员函数相同。

普通的成员函数一般都隐含了一个this指针,this指针指向类的对象本身,因为普通成员函数总是具体的属于某个类的具体对象的。通常情况下this是缺省的。如函数add()实际上是写成this.add().但是与普通函数相比,静态成员函数由于不是与任何的对象相联系因此它不具有this指针,从这个意义上讲,它无法访问属于具体类对象的非静态数据成员,也无法访问非静态成员函数,它只能调用其余的静态成员函数。如下面的代码

1 class x {
2 int i;
3 static int j;
4 public:
5 x(int i=0):i(i){
6 j=i
7 }//非静态成员函数可以访问静态函数成员和静态数据成员。
8 int val() const {return i;}
9 static int incr(){
10 i++;//错误的代码,因为i是非静态成员,因此incr()作为静态成员函数无
11   //法访问它。
12 return ++j;
13 }
14 static int fn(){
15 return incr();//合法的访问,因为fn()和incr()作为静态成员函数可以互相访
16 //问。
17 }
18 };
19 int x::j=0;
20 main(){……}

根据上面的程序可以总结几点:
1. 静态成员之间可以相互的访问,包括静态成员函数访问静态数据成员和访问静态成员函数。
2. 非静态成员函数可以任意的访问静态成员函数和静态数据成员。
3. 静态成员函数不能访问非静态成员函数和非静态数据成员。

由于没有this指针的额外开销,因此静态成员函数与全局函数相比速度上会有少许的增长。

理解控制连接

理解控制连接之前我们先了解一下外部存储类型的概念。一般的在程序的规模很小的情况下我们用一个源程序既可以表达完整。但事实上稍微有价值的程序都不可能只用一个程序来表达的,而是分割成很多的小的模块,每一个模块完成特定的功能,构成一个源文件。所有的源文件共享一个main函数。在不同的源文件之间为了能够相互进行数据或者函数的沟通,这时候通常声明数据或者函数为extern,这样用extern声明的数据或者函数就是全局变量或者函数默认情况下的函数的声明总是extern的。在文件范围内定义的全局数据和函数对程序中的所有的编译单元来说都是可见的。这就是通常我们所说的外部连接

但有时候我们可能想限制一个名字对象的可见性,想让一个对象在本地文件范围内是可见的,这样这个文件中的所有的函数都可以利用这个对象,但是不想让本地文件之外的其余文件看到或者访问该对象,或者外部已经定义了一个全局对象,防止内部的名字对象与之冲突。

通过在全局变量的前面加上static,我们可以做到这一点。在文件的范围内,一个被声明为static的对象或者函数的名字对于编译单元来说是局部变量,我们称之为静态全部变量。这些名字不使用默认情况下的外部连接,而是使用内部连接。从下面的例子可以看出static是如何控制连接的。 工程文件为first.prj由两个源文件组成。
exam1.cpp
exam2.cpp
//***************************************
// exam1.cpp
//***************************************
1 #include
2 int n;
3 void print_n();
4 void main()
5 {
6 n=20;
7 cout<。(static int n 是作为局部变量的)

改造二: 我们在exam1.cpp的第二行和第三行之间增加void staticfn();同时在第八行和第九行之间增加staticfn()的调用。再看执行结果。vc会产生一个找不到函数staticfn的错误。这说明exam1.cpp无法共享exam2.cpp中的staticfn()。

从上面的结论可以看出下面几点:
1. static解决了名字的冲突问题。使得可以在源文件中建立并使用与其它源文件甚至全局变量一样的名字而不会导致冲突的产生。这一点在很大的项目中是很有用处的

2. 声明为静态的函数不能被其他的源文件所调用,因为它的名字只对本地文件可见,其余的文件无法获取它的名字,因此不可能进行连接。

在文件作用域下声明的inline函数默认情况下认为是static类型。在文件作用域下声明的const的常量默认情况下也是static存储类型的。

No comments: