静态变量

发布时间 2023-11-26 16:51:13作者: ZTer

代表「静态」的关键字是 static ,它拥有多种含义。

  1. 用于声明某个变量是持久存储的(类似于全局变量)。
  2. 用于限制某个变量/函数不能从其他文件中访问。
  3. 用于声明一个成员变量/函数是一个全局变量/函数(所有对象共有)。

接下来我们分别对这几种情况来做讨论。

用于持久存储的声明与全局变量

  • 将一个本地变量声明为 static 类型以达到持久存储的目的。

实际上,静态本地变量就是全局变量,它与全局变量的使用方法相似,它们都在链接的时候被分配了内存空间,它们的作用域都是从它们的定义到 程序结束。

唯一不同的是初始化的时间——全局变量是在 main() 函数执行之前,静态变量是在程序运行到定义它之时。

class X 
{
public:
  X(int a, int b);
  ~X();
};

void f()
{
  static X my_X(10, 20);
}

譬如上面这段代码,当程序运行到 static X my_X(10, 20); 一句的时候,my_X 才会被初始化。当 f 之后再次被调用时,不会重复初始化对象 my_X,也就是说,当一个静态变量/对象被初始化之后,它就从各种意义上成为了一个全局变量。

Q:我们如何做到记住某个对象已经被初始化了而不至于在重复调用的函数中不再重复初始化它呢?
A:最简单的一种方案就是创建一个隐藏起来的标志来证明某个对象已经被初始化过了,实际上,C++ 上也确实是这么做的,这里不再展开讲。

需要注意的是:多文件程序的各个文件中的全局变量初始化顺序是不一定的——即使是同一个编译器的两次编译运行之间都有可能出现差异。这提醒我们,要特别注意初始化时的依赖关系。

解决办法只有两个:要么不做这件事,要么就把所有的全局变量放到同一个文件里。

用于限制单文件访问

  • static 修饰的变量只能在定义它的文件中访问,而不能在其他文件中访问(即使它在其他文件中做过声明)。
  • 需要注意的是,使用 static 来限制访问权限的做法在 C++ 中已经被弃用了,因为我们有更好的方法(比如 class 中的 public,private,protected 等关键字)来做这件事。

例如下面这个程序会编译错误:

/* in flie1.cpp */

#include <iostream>

using namespace std;

extern int gGlobal;
extern int sLocal;
//声明两个变量

int f();
//声明 f 函数,因为 f 函数的实体在 file2 中,所以 f 可以正常访问 file2 中的 static 变量 sLocal
int g()
{
  return gGlobal + sLocal;
}
//定义 g 函数,g 不能访问 file2 中的 static 变量,报错

int main()
{
  cout << f() << ' ' << g() << endl;
}

/* in file2.cpp */

int gGlobal = 10;
//gGlobal 是所有文件共享的
static int sLocal = 20;
//sLocal 只能在 file2 中访问

int f()
{
  return gGlobal + sLocal;
}

报错如下,显示声明变量 sLocal 但是未定义。

在上面这段代码中,sLocal 变量是一个 static 变量,它只能在 file2 中访问。即使 file1 中声明了它,仍然是不可以在 file1 中调用的。

静态成员

关于静态成员/函数我很早之前总结过一次,但是不太准确,这里再总结一次。

静态成员变量

前面第一个大标题下我们说明了 static 修饰的变量可以视为全局变量。

如果类里面有一个 static 修饰的成员,那么这个成员实际上就不存在于任何一个成员里了,而是归属于这个类的一个全局变量。

class A
{
public:
  A() : i(0), j(0) {}
  ~A(){}
  void Set(int ii){ i = ii; }
  static int i;
  int j;
}

int main()
{
  A::i = 10;
}

这段代码是不能运行的。

  • 在很久之前说过,类中的变量全部都是声明而不是定义。它们直到属于这个类的一个对象被定义,才算有了定义,并且调用它们时必须指定是「哪个对象的变量」。
    例如:A::j = 10; 是无意义的,因为没有指定对象。

  • static 修饰的成员又是特殊的,它们的存储空间不属于任何一个对象(它们是全局变 量),因此就算定义了某个对象,这些全局变量也不会被定义。
    既然 static int i; 一句是声明,那么我们就必须在别的地方给它一个定义,否则就会报错 undefined symbol(未定义的变量)。

所以说,我们必须在别的地方写上 int A::i; 这样 i 才有了定义。

我们来看一份完整的代码:

#include <iostream>

using namespace std;

class A
{
public:
  A(){ i = 0; }
  void Print() { cout << i << endl; }
  void Set(int ii){ i = ii; }
private:
  static int i;
};

int A::i;

int main()
{
  A a, b;
  a.Set(10);
  b.Print();
  return 0;
}

这份代码才可以正常运行,并且输出为 10。这充分的证明了变量 i 是一个全局变量,而不是某一个对象私有的。

有趣的是,全局变量不属于任意一个对象,因此它不可以在构造函数中初始化;但是它可以在构造函数中被赋值

换句话说:A() : i(0){} 是不可以的,而 A(){ i = 0; } 是可以的。

事实上,静态成员只能在它被定义的地方初始化,也就是 int A::i = 10; 这样就可以把 i 的值初始化为 10(尽管它是一个 private 的变量,看似不可以从外面访问)。

此外,虽然静态成员不属于任意一个对象,但是对象的 this 指针仍然指向 i。就像一个公用电话机,所有人都可以通过打电话访问到 i,但是所有人都不能说「i 是我的」。

以下三种方式都可以在有权限的情况下访问到静态变量 i: a.i, A::i, this -> i

静态成员函数

类似于静态成员变量,静态成员函数不属于任意一个对象,所以它不可以调用非静态成员变量

class A
{
public:
  A() : j(0) { i = 0; }
  void Print() { cout << i << endl; }
  static void Say(){ cout << i << ' ' << j << endl; }
private:
  static int i;
  int j;
};

这时无法通过编译,因为 Say 函数中调用 j,实际是调用 this -> j,但是 Say 函数不属于任意一个对象,它没有 this 指针;而 j 是一个非静态成员变量,j 的引用必须指定对象,因而引发了错误。

调用静态成员函数,也有三种方法:a.Say(), A::Say(), this -> Say();

Q:为什么 a.Say() 不会输出 a 的 j 呢?我已经指明了 a 去调用 Say 函数啊?
A:还是之前那个公用电话的例子:一个公用电话,我可以用这个电话打给 Say 函数,这样我可以找到 Say;但是 Say 打这个电话却找不到我。
也就是说,Say 不能反过来调用调用它的对象里的东西,因为所有对象(甚至不需要指定对象)都可以调用它。非静态函数则不同,它们每次被调用都必定是来自它们所归属的对象的调用操作,因此它们可以确定这次调用来自谁,因此也就可以访问归属于调用者的成员变量。