Fork me on GitHub

【C/C++语法外功】Static详解

=========================================================================================================================================================

C++实例

/******************************************************************************************************

*Copyright(c) 2010,**

*All right reserved.

******************************************************************************************************

内    容:       static

作      者:     SkySeraph

完成时间:     2010-8-3

参考资料:    谭浩强C++ 课件 例9.11

******************************************************************************************************/

/*知识点

目  :引入静态数据成员是为了数据的沟通,实现数据的共享,实现某一个或几个数据成员为所有对象共享 & 全局变量的安全性得不到保证

        引入静态成员函数是为了能处理静态数据成员。因为静态成员函数没有this指针

特  :static成员变量不属于对象的一部分,而是类的一部分,所以程序还没诞生任何对象的时候就处理这种成员函数。即即使对象没有产生,static成员函数也已存在

           由于static不需要借助任何对象就可以被调用执行,所以编译器不会为它暗加上一个this指针,即static成员函数没有this指针。

              不随对象的建立而分配空间,也不随对象的撤销而释放。静态数据成员是在程序编译时被分配空间的,到程序结束时才释放空间。

初始化静态数据成员只能在类外初始化。(因为构造函数可能一再被调用,而变量的初值却只应设定一次,故不放在类的构造函数中;同时,由于可能会被载入许多地方,会被执行许多次,故不要安排在头文件中,可以放在main中、全局函数中、任何函数之外) 见下

引  :静态数据成员可以通过类名引用也可通过对象名来引用 见下

习  :只用静态成员函数引用静态数据成员,而不引用非静态数据成员 [引用本类的非静态成员,可以用对象名.非静态成员形式引用]

 

对于C【补充】 

声明变量在C语言的特点

1)、变量会被放在程序的全局存储区中,这样可以在下一次调用的时候还可以保持原来的赋值。这一点是它与堆栈变量和堆变量的区别。

2)、变量用static告知编译器,自己仅仅在变量的作用范围内可见。这一点是它与全局变量的区别。

Tips:   

A.若全局变量仅在单个C文件中访问,则可以将这个变量修改为静态全局变量,以降低模块间的耦合度;
B.若全局变量仅由单个函数访问,则可以将这个变量改为该函数的静态局部变量,以降低模块间的耦合度;
C.设计和使用访问动态全局变量、静态全局变量、静态局部变量的函数时,需要考虑重入问题;
D.如果我们需要一个可重入的函数,那么,我们一定要避免函数中使用static变量(这样的函数被称为: 带“  内部存储器”功能的的函数);

E.函数中必须要使用static变量情况:比如当某函数的返回值为指针类型时,则必须是static的局部变量的地址作为返回值,若为auto类型,则返回为错指针。

函数前加static使得函数成为静态函数。但此处“static”的含义不是指存储方式,而是指对函数的作用域仅局限于本文件(所以又称内部函数)。使用内部函数的好处是:不同的人编写不同的函数时,不用担心自己定义的函数,是否会与其它文件中的函数同名。

扩展分析[渊源]

术语static有着不寻常的历史.

起初,C中引入关键字static是为了表示退出一个块后仍然存在的局部变量

随后,static在C中有了第二种含义:用来表示不能被其它文件访问的全局变量和函数。为了避免引入新的关键字,所以仍使用static关键字来表示这第二种含义。

最后,C++重用了这个关键字,并赋予它与前面不同的第三种含义:表示属于一个类而不是属于此类的任何特定对象的变量和函数(与Java中此关键字的含义相同)。

*/

/******************************************************************************************************/

#include <iostream>

using namespace std;

class Student                   //定义Student类

{

public:

       Student(int n,int a,float s):num(n),age(a),score(s){ }      //定义构造函数

       void total( );

       static float average( );      //声明静态成员函数

private:

       int num;

       int age;

       float score;

       static float sum;            //静态数据成员

       //不属于某个对象成员,个对象成员共享的,其值是不断变化的,无论对哪个对象元素,都是相同,始终不释放内存空间

       static int count;            //静态数据成员

};

void Student::total( )                      //定义非静态成员函数

//公有的成员函数可以引用本对象中的一般数据成员(非静态数据成员),也可以引用类中的静态数据成员。

{

       sum+=score;                            //累加总分

       count++;                               //累计已统计的人数

}

float  Student::average( )                  //定义静态成员函数

{

       return(sum/count);

}

float Student::sum=0;                     //对静态数据成员初始化

int Student::count=0;                     //对静态数据成员初始化

//知识点:static数据成员初始化一般形式及注意

//数据类型名::静态数据成员名=初值  不必在初始化语句中加static;不能用初始化表对静态数据成员初始化;未初始化,系统自动赋0

int main( )

{

       Student stud[3]=

       {                      //定义对象数组并初始化

              Student(1001,18,70),

              Student(1002,19,78),

              Student(1005,20,98)

       };

       int n;

       cout<<"please input the number of students:";

       cin>>n;                            //输入需要求前面多少名学生的平均成绩 n<3

       for(int i=0;i<n;i++)                  //调用3次total函数

              stud[i].total( );

       cout<<"the average score of "<<n<<" students is "<<Student::average( )<<endl;

       //调用静态成员函数

       return 0;

}

>概念基础

>>C程序存储空间布局

C程序一般由下列部分组成:   

1)正文段——CPU执行的机器指令部分; 一个程序只有一个副本; 只读, 防止程序由于意外事故而修改自身指令;

2)初始化数据段(数据段)——在程序中所有赋了初值的全局变量, 存放在这里。  

3)非初始化数据段(bss段)——在程序中没有初始化的全局变量; 内核将此段初始化为0。  

4)栈——增长方向: 自顶向下增长; 自动变量以及每次函数调用时所需要保存的信息( 返回地址; 环境信息) 。

5)堆——动态存储区

全局变量&堆变量&栈变量

int a ;

main()

{

int b ;

int c* = (int *)malloc(sizeof(int));

}

a是全局变量, b是栈变量, c是堆变量。

>>Static用法小结

static关键字是C,C++中都存在的关键字,它主要有三种使用方式,其中前两种只指在C语言中使用, 第三种在C++中使用(C,C++中具体细微操作不尽相同,此文以C++为准).  (1)局部静态变量  (2)外部静态变量/函数    (3)静态数据成员/成员函数  下面就这三种使用方式及注意事项分别说明  

一、局部静态变量  

在C/C++中,  局部变量按照存储形式可分为三种auto,static,register     【<C语言程序设计(第二版)>谭浩强,   第174-175页】 

与auto类型(普通)局部变量相比,static局部变量有三点不同  

1.存储空间分配不同  

auto类型分配在栈上,属于动态存储类别,占动态存储区空间,函数调用结束后自动释放,而static分配在静态存储区,在程序整个运行期间都不释放。两者之间的作用域相同,但生存期不同

2. static局部变量在所处模块在初次运行时进行初始化工作,且只操作一次

3 对于局部静态变量,如果不赋初值,编译期会自动赋初值0或空字符,而auto类型的初值是不确定的。( 对于C++中的class对象例外,class的对象实例如果不初始化,则会自动调用默认构造函数,不管是否是static类型)

特点:static局部变量的”记忆性”与生存期的”全局性”所谓”记忆性”是指在两次函数调用时,在第二次调用进入时,能保持第一次调用退出时的值.  

示例程序一  
  #include   <iostream>      
  using   namespace   std;      
  void   staticLocalVar()     {  
    static   int   a   =   0;   //运行期时初始化一次,下次再调用时,不进行初始化工作  
    cout<<"a="<<a<<endl;  
    ++a;  
  }      
  int   main()  
  {  
    staticLocalVar();   // 第一次调用, 输出a=0  
    staticLocalVar();   //第二次调用,记忆了第一次退出时的值, 输出a=1  

return   0;  
  }      
应用

利用”记忆性”,记录函数调用的次数(示例程序一)  

利用生存期的”全局性”,改善”return a pointer /reference to a local object”的问题。Local object的问题在于退出函数,生存期即结束,利用static的作用,延长变量的生存期。

示例程序二:  

  //   IP   address   to   string   format  
  //   Used   in   Ethernet   Frame   and   IP   Header   analysis  
  const char  * IpToStr(UINT32 IpAddr)  
  {  
    static char strBuff[16];   //   static局部变量,用于返回地址有效  
    const unsigned char  *pChIP   =   (const  unsigned  char  *)&IpAddr;  
    sprintf(strBuff,"%u.%u.%u.%u",pChIP[0],pChIP[1],pChIP[2],pChIP[3]);  
    return strBuff;  
  }  
注意事项:  
  1  “记忆性” 程序运行很重要的一点就是可重复性,而static变量的”记忆性”破坏了这种可重复性,造成不同时刻至运行的结果可能不同.  
  2  “生存期” 全局性和唯一性。普通的local变量的存储空间分配在stack上,因此每次调用函数时,分配的空间都可能不一样,而static具有全局唯一性的特点,每次调用时,都指向同一块内存,这就造成一个很重要的问题   ----不可重入性!!!  

这样在多线程程序设计或递归程序设计中,要特别注意这个问题.  (不可重入性的例子可以参见<effective   C++   (2nd)>(影印版)第103-105页)  

下面针对示例程序二,分析在多线程情况下的不安全性.(为方便描述,标上行号)  
  ①   const   char   *   IpToStr(UINT32   IpAddr)  
  ②   {  
  ③     static   char   strBuff[16];   //   static局部变量,   用于返回地址有效  
  ④     const   unsigned   char   *pChIP   =   (const   unsigned   char   *)&IpAddr;  
  ⑤     sprintf(strBuff,   "%u.%u.%u.%u",     pChIP[0],   pChIP[1],   pChIP[2],   pChIP[3]);  
  ⑥     return   strBuff;  
  ⑦   }  

假设现在有两个线程A,B运行期间都需要调用IpToStr()函数,将32位的IP地址转换成点分10进制的字符串形式.   现A先获得执行机会,执行IpToStr(),传入的参数是0x0B090A0A,顺序执行完应该返回的指针存储区内容是:”10.10.9.11”,现执行到⑥时,失去执行权,调度到B线程执行,B线程传入的参数是0xA8A8A8C0,执行至⑦,静态存储区的内容是192.168.168.168. 当再调度到A执行时,从⑥继续执行,由于strBuff的全局唯一性,内容已经被B线程冲掉,此时返回的将是192.168.168.168字符串,不再是10.10.9.11字符串.

 

二、外部静态变量/函数  

在C中static有了第二种含义:用来表示不能被其它文件访问的全局变量和函数。但为了限制全局变量/函数的作用域,函数或变量前加static使得函数成为静态函数 但此处“static”的含义不是指存储方式,而是指对函数的作用域仅局限于本文件(所以又称内部函数)注意此时,对于外部(全局)变量,不论是否有static限制,它的存储区域都是在静态存储区,生存期都是全局的. 此时的static只是起限制作用域作用,限定作用域在本模块(文件)内部.

好处:

[1]不会被其他文件访问、修改

[2]其他文件可以使用相同的名字,不会冲突,即不同的人编写不同的函数时,不用担心自己定义的函数,是否会与其它文件中的函数同名

示例程序三:        
  //file1.cpp      
  static   int   varA;  
  int   varB;  
  extern   void   funA()  
  {  
  ……  
  }      
  static   void   funB()  
  {  
  ……  
  }      
  //file2.cpp      
  extern   int   varB;   //   使用file1.cpp中定义的全局变量  
  extern   int   varA;   //   错误!   varA是static类型,   无法在其他文件中使用  
  extern   vod   funA();   //   使用file1.cpp中定义的函数  
  extern   void   funB();   //   错误!   无法使用file1.cpp文件中static函数  

 三、静态数据成员/成员函数(C++特有)  

C++重用了这个关键字,并赋予它与前面不同的第三种含义:表示属于一个类而不是属于此类的任何特定对象的变量和函数.这是与普通成员函数的最大区别,也是其应用所在,比如在对某一个类的对象进行计数时,计数生成多少个类的实例,就可以用到静态数据成员.在这里面,static既不是限定作用域的,也不是扩展生存期的作用,而是指示变量/函数在此类中的唯一性。这也是”属于一个类而不是属于此类的任何特定对象的变量和函数”的含义。因为它是对整个类来说是唯一的,因此不可能属于某一个实例对象的。(针对静态数据成员而言,成员函数不管是否是static,在内存中只有一个副本,普通成员函数调用时,需要传入this指针,static成员函数调用时,没有this指针。)  

请看示例程序四  (<effective   c++   (2nd)>(影印版)第59页)  

class   EnemyTarget   {  
  public:  
      EnemyTarget()   {   ++numTargets;   }  
      EnemyTarget(const   EnemyTarget&)   {   ++numTargets;   }  
      ~EnemyTarget()   {   --numTargets;   }  
      static   size_t   numberOfTargets()   {   return   numTargets;   }  
      bool   destroy();       //   returns   success   of   attempt   to   destroy   EnemyTarget   object  
  private:  
      static   size_t   numTargets;                               //   object   counter  
  };  
  //   class   statics   must   be   defined   outside   the   class;  
  //   initialization   is   to   0   by   default  
  size_t   EnemyTarget::numTargets;  
  在这个例子中,静态数据成员numTargets就是用来计数产生的对象个数的.  

  另外,在设计类的多线程操作时,由于POSIX库下的线程函数pthread_create()要求是全局的,普通成员函数无法直接做为线程函数,可以考虑用Static成员函数做线程函数.

使用静态函数的好处

1.静态函数会被自动分配在一个一直使用的存储区, 直到退出应用程序实例, 避免了调用函数时压栈出栈, 速度快很多。

2.关键字“static”,译成中文就是“静态的”,所以内部函数又称静态函数。但此处“static”的含义不是指存储方式, 而是指对函数的作用域仅局限于本文件。 使用内部函数的好处是: 不同的人编写不同的函数时, 不用担心自己定义的函数, 是否会与其它文件中的函数同名, 因为同名也没有关系。

 

 

static修饰之后的变量和函数与之前的比较

 

存储方式/内存中的位置

初始化

作用域

特殊说明

全局变量+static

未变,仍为静态存储

位于静态存储区

未经初始化全局静态变量会被程序自动初始化为0

变了,由在多个文件的全局有效,变成了:仅在当前文件有效

 

局部变量+static

变了,由函数被调用时候在栈上分配且函数调用结束,返回时,被销毁变成了:静态存储区

位于静态存储区

未经初始化局部静态变量会被程序自动初始化为0

未变,仍是函数内

1.静态局部变量也像其他静态变量一样,仅被初始化一次,下一次调用该函数,而使用该变量时,其值以上一次为准。简单说就是:仅被初始化一次,下一次值依据上一次结果

2.正常情况,函数内的局部变量,在函数返回时,被销毁,所以不能返回局部变量的指针,但是由于声明为static后,此局部变量生命期为整个程序运行期,所以,可以返回局部静态变量的指针

函数+static

变了,由每次调用就重新有一份拷贝变成了:在内存中仅有一份拷贝

未变

其实static对函数的影响,类似于static对局部变量的影响,主要是对存储方式变了,即存储区域变了,由每次调用新申请,变成常驻内存静态区了。

>>全局变量、静态全局变量、静态局部变量和局部变量的区别

变量可以分为:全局变量、静态全局变量、静态局部变量和局部变量。

按存储区域分,全局变量、静态全局变量和静态局部变量都存放在内存的静态存储区域(全局数据区)局部变量存放在内存的栈区

按作用域分,全局变量在整个工程文件内都有效; 静态全局变量只在定义它的文件内有效;静态局部变量只在定义它的函数内有效,只是程序仅分配一次内存,函数返回后,该变量不会消失;局部变量在定义它的函数内有效,但是函数返回后失效

全局变量(外部变量)的说明之前再冠以static 就构成了静态的全局变量。全局变量本身就是静态存储方式,静态全局变量当然也是静态存储方式。 这两者在存储方式上并无不同。这两者的区别在于非静态全局变量的作用域是整个源程序,当一个源程序由多个源文件组成时,非静态的全局变量在各个源文件中 都是有效的。 而静态全局变量则限制了其作用域, 即只在定义该变量的源文件内有效,在同一源程序的其它源文件中不能使用它。由于静态全局变量的作用域局限于一个源文件内,只能为该源文件内的函数公用,因此可以避免在其它源文件中引起错误。

从以上分析可以看出,把局部变量改变为静态变量后是改变了它的存储方式即改变了它的生存期。把全局变量改变为静态变量后是改变了它的作用域, 限制了它的使用范围。

static函数与普通函数作用域不同。仅在本文件。只在当前源文件中使用的函数应该说明为内部函数(static),内部函数应该在当前源文件中说明和定义。对于可在当前源文件以外使用的函数,应该在一个头文件中说明,要使用这些函数的源文件要包含这个头文件。

static全局变量与普通的全局变量区别:static全局变量只初使化一次,防止在其他文件单元中被引用;

static局部变量和普通局部变量区别:static局部变量只被初始化一次,下一次依据上一次结果值;

static函数与普通函数区别:static函数在内存中只有一份,普通函数在每个被调用中维持一份拷贝全局变量和静态变量如果没有手工初始化,则由编译器初始化为0。局部变量的值不可知。


>>Static的作用

1.先来介绍它的第一条也是最重要的一条:隐藏

当我们同时编译多个文件时,所有未加static前缀的全局变量和函数都具有全局可见性。为理解这句话,我举例来说明。我们要同时编译两个源文件,一个是a.c,另一个是main.c. 下面是a.c的内容:

char a = 'A'; // global variable

void msg()

{

printf("Hello\n");

}

下面是main.c的内容:

 int main(void)

{

extern char a; // extern variable must be declared before use

printf("%c ", a);

(void)msg();

return 0;

}

程序的运行结果是:

A Hello

你可能会问:为什么在a.c中定义的全局变量a和函数msg能在main.c中使用?前面说过,所有未加static前缀的全局变量和函数都具有全局可见性,其它的源文件也能访问。此例中,a是全局变量,msg是函数,并且都没有加static前缀,因此对于另外的源文件main.c是可见的。

如果加了static,就会对其它源文件隐藏。例如在a和msg的定义前加上static,main.c就看不到它们了。利用这一特性可以在不同的文件中定义同名函数和同名变量,而不必担心命名冲突。Static可以用作函数和变量的前缀,对于函数来讲,static的作用仅限于隐藏,而对于变量,static还有下面两个作用

2.static的第二个作用是保持变量内容的持久  即上文所述的记忆性

存储在静态数据区的变量会在程序刚开始运行时就完成初始化,也是唯一的一次初始化。共有两种变量存储在静态存储区:全局变量和static变量,只不过和全局变量比起来,static可以控制变量的可见范围,说到底static还是用来隐藏的。虽然这种用法不常见,但我还是举一个例子。

 #include <stdio.h>

int fun(void){

static int count = 10; // 事实上此赋值语句从来没有执行过

return count--;

}

int count = 1;

int main(void)

{

printf("global\t\tlocal static\n");

for(; count <= 10; ++count)

printf("%d\t\t%d\n", count, fun());

return 0;

}

程序的运行结果是:

global local static
1  10
2   9
3   8
4   7
5   6
6   5
7   4
8   3
9   2
10  1

3.static的第三个作用是默认初始化为0.其实全局变量也具备这一属性,因为全局变量也存储在静态数据区

在静态数据区,内存中所有的字节默认值都是0x00,某些时候这一特点可以减少程序员的工作量。比如初始化一个稀疏矩阵,我们可以一个一个地把所有元素都置0,然后把不是0的几个元素赋值。如果定义成静态的,就省去了一开始置0的操作。再比如要把一个字符数组当字符串来用,但又觉得每次在字符数组末尾加‘\0’太麻烦。如果把字符串定义成静态的,就省去了这个麻烦,因为那里本来就是‘\0’。不妨做个小实验验证一下。

#include <stdio.h>

int a;

int main(void)

{

int i;

static char str[10];

printf("integer: %d; string: (begin)%s(end)", a, str);

return 0;

}

程序的运行结果如下integer: 0; string: (begin)(end)

最后对static的三条作用做一句话总结。首先static的最主要功能是隐藏,其次因为static变量存放在静态存储区,所以它具备持久性和默认值0.

>>const & static const

·对于C/C++语言来讲,const就是只读的意思,只在声明中使用;  static一般有2个作用,规定作用域和存储方式.对于局部变量,static规定其为静态存储方式,每次调用的初始值为上一次调用的值,调用结束后存储空间不释放;对于全局变量,如果以文件划分作用域的话,此变量只在当前文件可见;对于static函数也是在当前模块内函数可见. static const 应该就是上面两者的合集.

下面分别说明:
全局:
const,只读的全局变量,其值不可修改.
static,规定此全局变量只在当前模块(文件)中可见.
static const,既是只读的,又是只在当前模块中可见的.
文件:
文件指针可当作一个变量来看,与上面所说类似.
函数:
const,返回只读变量的函数.
static,规定此函数只在当前模块可见.
:
const,一般不修饰类,(在VC6.0中试了一下,修饰类没啥作用)
static,C++中似乎没有静态类这个说法,一般还是拿类当特殊的变量来看。  C#中有静态类的详细说明,且用法与普通类大不相同.

>>Static理解

问题:

关于static变量,请选择下面所有说法正确的内容:

  A、若全局变量仅在单个C文件中访问,则可以将这个变量修改为静态全局变量,以降低模块间的耦合度;

  B、若全局变量仅由单个函数访问,则可以将这个变量改为该函数的静态局部变量,以降低模块间的耦合度;

  C、设计和使用访问动态全局变量、静态全局变量、静态局部变量的函数时,需要考虑重入问题;

  D、静态全局变量过大,可那会导致堆栈溢出。

答案与分析:

  对于A,B:根据本篇概述部分的说明b),我们知道,A,B都是正确的。

  对于C:根据本篇概述部分的说明a),我们知道,C是正确的(所谓的函数重入问题,下面会具体阐述)。

  对于D:静态变量放在程序的全局数据区,而不是在堆栈中分配,所以不可能导致堆栈溢出,D是错误的。

因此,答案是A、B、C。

问题:不可重入函数

曾经设计过如下一个函数,在代码检视的时候被提醒有bug,因为这个函数是不可重入的,为什么?

unsigned int sum_int( unsigned int base )

{

 unsigned int index;

 static unsigned int sum = 0; // 注意,是static类型的。

 for (index = 1; index <= base; index++)

 {

  sum += index;

 }

 return sum;

}

答案与分析:

所谓的函数是可重入的(也可以说是可猜测的),即:只要输入数据相同就应产生相同的输出

这个函数之所以是不可猜测的,就是因为函数中使用了static变量,因为static变量的特征,这样的函数被称为:带“内部存储器”功能的的函数。因此假如我们需要一个可重入的函数,那么,我们一定要避免函数中使用static变量,这种函数中的static变量,使用原则是,能不用尽量不用。

将上面的函数修改为可重入的函数很简单,只要将声明sum变量中的static要害字去掉,变量sum即变为一个auto 类型的变量,函数即变为一个可重入的函数。

当然,有些时候,在函数中是必须要使用static变量的,比如当某函数的返回值为指针类型时,则必须是static的局部变量的地址作为返回值,若为auto类型,则返回为错指针

参考资料

http://www.cnblogs.com/yxmx/articles/1598396.html  static详解

http://www.knowsky.com/392856.html  水滴石穿C语言之static辨析

SkySeraph     Oct.1st,2010 HQU XiaMen China

Email:zgzhaobo@gmail.com 

posted @ 2010-10-11 17:59  SkySeraph  阅读(1708)  评论(1编辑  收藏  举报