1. static关键字

C语言代码是以文件为单位来组织的,在一个源程序的所有源文件中,一个外部变量(注意不是局部变量)或者函数只能在一个源程序中定义一次,
如果有重复定义的话编译器就会报错。伴随着不同源文件变量和函数之间的相互引用以及相互独立的关系,产生了extern和static关键字。

1.1 C中的static

一个进程在内存中的布局如下表所示:

|栈区 |
|堆栈增长区|
|堆区 |
|其他段 |
|.bss段 |
|.data段 |
|.text段 |

其中
.text段保存进程所执行的程序二进制文件,
.data段保存进程所有的已初始化的全局变量,
.bss段保存进程未初始化的全局变量。
在进程的整个生命周期中,.data段和.bss段内的数据时跟整个进程同生共死的,也就是在进程结束之后这些数据内存才会被释放。

当一个进程的全局变量被声明为static之后,它的中文名叫静态全局变量。静态全局变量和其他的全局变量的存储地点并没有区别,都是在.data段(已初始化)或者.bss段(未初始化)内,但是它只在定义它的源文件内有效,其他源文件无法访问它。所以,普通全局变量加上static关键字修饰后,就只能被定义它的源文件中的变量或函数访问。

static局部变量

普通的局部变量在栈空间上分配,这个局部变量所在的函数被多次调用时,每次调用这个局部变量在栈上的位置都不一定相同。局部变量也可以在堆上动态分配,但是记得使用完这个堆空间后要释放之。

static局部变量中文名叫静态局部变量。它与普通的局部变量比起来有如下几个区别:

  1. 位置:静态局部变量被编译器放在全局存储区.data(注意:不在.bss段内,原因见3)),所以它虽然是局部的,但是在程序的整个生命周期中存在。

  2. 访问权限:静态局部变量只能被其作用域内的变量或函数访问。也就是说虽然它会在程序的整个生命周期中存在,由于它是static的,它不能被其他的函数和源文件访问。

  3. 值:静态局部变量如果没有被用户初始化,则会被编译器自动赋值为0,以后每次调用静态局部变量的时候都用上次调用后的值。这个比较好理解,每次函数调用静态局部变量的时候都修改它然后离开,下次读的时候从全局存储区读出的静态局部变量就是上次修改后的值。

static函数
相信大家还记得C++面向对象编程中的private函数,私有函数只有该类的成员变量或成员函数可以访问。在C语言中,也有“private函数”,它就是接下来要说的static函数,完成面向对象编程中private函数的功能。

当你的程序中有很多个源文件的时候,你肯定会让某个源文件只提供一些外界需要的接口,其他的函数可能是为了实现这些接口而编写,这些其他的函数你可能并不希望被外界(非本源文件)所看到,这时候就可以用static修饰这些“其他的函数”.所以static函数的作用域是本源文件,把它想象为面向对象中的private函数就可以了。

1.2 类中的static关键字

面向对象的static关键字

1.2.1 静态数据成员

在类内数据成员的声明前加上关键字static,该数据成员就是类内的静态数据成员。先举一个静态数据成员的例子。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
//头文件
#include <iostream.h>
class Myclass
{
public:
  Myclass(int a,int b,int c);
  void GetSum();
private:
  int a,b,c;
  static int Sum;//声明静态数据成员
};
int Myclass::Sum=0;//定义并初始化静态数据成员
//源文件
Myclass::Myclass(int a,int b,int c)
{
  this->a=a;
  this->b=b;
  this->c=c;
  Sum+=a+b+c;
}
void Myclass::GetSum()
{
  cout<<"Sum="<<Sum<<endl;
}
void main()
{
  Myclass M(1,2,3);
  M.GetSum();
   Myclass N(4,5,6);
  N.GetSum();
  M.GetSum();
}

可以看出,静态数据成员有以下特点:

  1. 对于非静态数据成员,每个类对象都有自己的拷贝。而静态数据成员被当作是类的成员,是类本身的一部分。无论这个类的对象被定义了多少个,静态数据成员在程序中也只有一份拷贝,被该类型的所有对象共享访问。也就是说,静态数据成员是该类的所有对象所共有的。对该类的多个对象来说,静态数据成员只分配一次内存,供所有对象共用。所以,静态数据成员的值对每个对象都是一样的,它的值可以更新.
  2. 静态数据成员存储在全局数据区。静态数据成员定义时要分配空间,所以不能在类的声明中定义。上例语句int Myclass::Sum=0;是定义静态数据成员,
    注意:这条语句是在整个类声明之外的,即static数据成员是类内声明,类外定义,static成员不通过类构造函数初始化,而是在定义时进行初始化.
    一个例外:初始化式为常量表达式,整型static const 数据成员(static const int) 可以在类的定义体内进行初始化.
  3. 静态数据成员和普通数据成员一样遵从public,protected,private访问规则.
  4. 因为静态数据成员在全局数据区分配内存,属于本类的所有对象共享,所以,它不属于特定的类对象,在没有产生类对象时其作用域就可见,即在没有产生类的实例时,我们就可以操作访问它.
  5. 静态数据成员初始化与一般数据成员初始化不同。静态数据成员初始化的格式为:
    <数据类型><类名>::<静态数据成员名>=<值>
  6. 类的静态数据成员有两种访问形式:
    <类对象名>.<静态数据成员名> 或 <类类型名>::<静态数据成员名>
    如果静态数据成员的访问权限允许的话(即public的成员),可在程序中,按上述格式来引用静态数据成员 ;
  7. 静态数据成员主要应用在各个对象都有相同的某项属性的时候.
    比如对于一个存款类,每个实例的利息都是相同的。所以,应该把利息设为存款类的静态数据成员。这有两个好处:
    第一,不管定义多少个存款类对象,利息数据成员都共享分配在全局数据区的内存,所以节省存储空间。
    第二,一旦利息需要改变时,只要改变一次,则所有存款类对象的利息全改变过来了;
  8. 同全局变量相比,使用静态数据成员有两个优势:
    • 静态数据成员没有进入程序的全局名字空间,因此不存在与程序中其它全局名字冲突的可能性
    • 可以实现信息隐藏。静态数据成员可以是private成员,而全局变量不能,不破坏类的封装

1.2.2 静态成员函数

与静态数据成员一样,我们也可以创建一个静态成员函数,它为类的全部服务而不是为某一个类的具体对象服务。
静态成员函数与静态数据成员一样,都是类的内部实现,属于类定义的一部分。
普通的成员函数一般都隐含了一个this指针,this指针指向类的对象本身,因为普通成员函数总是具体的属于某个类的具体对象的。
通常情况下,this是缺省的。如函数fn()实际上是this->fn()。但是与普通函数相比,静态成员函数由于不是与任何的对象相联系,
因此它不具有this指针。从这个意义上讲,它无法访问属于类对象的非静态数据成员,也无法访问非静态成员函数,它只能调用其余的静态成员函数。
下面举个静态成员函数的例子。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
//头文件
#include <iostream.h>
class Myclass
{
public:
  Myclass(int a,int b,int c);
  static void GetSum(); //声明静态成员函数
private:
  int a,b,c;
   static int Sum; //声明静态数据成员
};
int Myclass::Sum=0; //定义并初始化静态数据成员
//源文件
Myclass::Myclass(int a,int b,int c)
{
  this->a=a;
  this->b=b;
  this->c=c;
  Sum+=a+b+c; //非静态成员函数可以访问静态数据成员
}
void Myclass::GetSum() //静态成员函数的实现
{
  // cout<<a<<endl; //错误代码,a是非静态数据成员
  cout<<"Sum="<<Sum<<endl;
}
void main()
{
  Myclass M(1,2,3);
  M.GetSum();
  Myclass N(4,5,6);
  N.GetSum();
  Myclass::GetSum();
}

关于类的静态成员函数,可以总结为以下几点:

  1. 出现在类体外的函数定义不能指定关键字static;
  2. 静态成员之间可以相互访问,包括静态成员函数访问静态数据成员和访问静态成员函数;
  3. 非静态成员函数可以任意地访问静态成员函数和静态数据成员;
  4. 静态成员函数不能访问非静态成员函数和非静态数据成员;
  5. 由于没有this指针的额外开销,因此静态成员函数与类的全局函数相比速度上会有少许的增长;
  6. 调用静态成员函数,可以用成员访问操作符(.)和(->)为一个类的对象或指向类对象的指针调用静态成员函数,也可以直接使用如下格式:
    <类名>::<静态成员函数名>(<参数表>)调用类的静态成员函数。

成员函数声明为const说明该成员函数不会修改该成员函数所属的对象,所以static成员函数不能声明为const。
static成员通过作用域操作符(直接调用)、对象、引用、指向该类类型对象的指针(间接调用)

1
2
3
4
5
6
7
8
9
10
11
12
13
class Person
{
static double getHeight();
static const age = 30;
string school;
};
Person xiaoming;
Person *someone = &xiaoming;
double height;
height = Person::getHeight(); //static成员通过作用域操作符(直接调用)
height = xiaoming.getHeight();   //static成员通过对象(间接调用)
height = someone->getHeight();   //static成员通过指向该类类型对象的指针(间接调用)

static数据成员定义:

  1. 一般情况下,static数据成员是类内声明,类外定义;
  2. static成员不通过类构造函数初始化,而是在定义时进行初始化;
  3. 一个例外:初始化式为常量表达式,整型static const 数据成员(static const int) 可以在类的定义体内进行初始化:
    值得注意的是:const static数据成员在类的定义体中出始化时,该数据成员仍必须在类的定义体外定义,只是不再指定初始值:const int Person::age;
    常实型 static const数据成员不可在类内初始化。一个好的解决方法是使用宏定义: #define age 30
    常整型静态数据成员可以在类中直接初始化,而常实型静态数据成员不可以
1
2
3
4
5
6
7
8
9
class circle
{
int a;                      // 普通变量,不能在类中初始化
static int b;                   // 静态变量,不能在类中初始化
static const int c=2;               // 静态常整型变量,可以在类中初始化
static const double PI=3.1416;//error C2864: //只有静态常量整型数据成员才可以在类中初始化
} ;
const int cicle::c ; //const static数据成员在类的定义体中出始化时,该数据成员仍必须在类的定义体外定义,只是不再指定初始值

b可以在类外进行初始化,且所有对象共享一个b的值:
int circle::b = 2;
double circle::PI = 3.1416;

知乎问答:static函数在头文件中定义有什么好处么?

  1. 没有好处,不要这么做。除非该头文件只会被一个翻译单元(translation unit)所使用,那么static是可用作表示内部链接(internal linkage)。不过这种头文件和一般所指的头文件不同,通常会使用.inc文件后缀.
  2. 为了内联. 一般定义成static inline.
    隐患: 头文件中的 static 函数会在每个文件中生成一份代码,这造成代码冗余倒不是最大的问题,最大的问题是可能带来库文件与工程文件同一函数的代码的不一致性,这有风险。