今天我们来聊一聊C++中的static修饰符,并通过单例模式说明static的作用。

程序的内存结构

进入主题之前,我们首先看看一段C/C++程序的内存分配情况。

程序内存分配

图中,程序的二进制代码是存放在静态存储区。C/C++中的全局变量和静态变量都是存储在数据区中,需要强调的是不论static变量的作用域如何,变量的内存位置都在数据区中(而非函数栈上)。这也很容易解释为何函数中的局部静态变量在函数退出后依然存在,并不会随着函数退出消亡(析构)。

static修饰符

static的主要作用是限定变量或函数为静态存储。如果用static限定全局变量与函数,则可以将该对象的作用域限定为被编译源文件的剩余部分。通过static限定全局变量,可以达到隐藏外部对象的目的。因而,static限定的变量或函数不会和同一程序中其它文件中同名的相冲突。如果用static限定函数局部变量,则该变量从程序一开始就拥有内存,不会随其所在函数的调用和退出而分配和消失。在类中,静态成员可以实现多个对象之间的数据共享,并且使用静态数据成员还不会破坏隐藏的原则,即保证了安全性。

静态变量或者静态函数的好处:

  1. 对于类成员static变量,可以节省内存,所用对象共享该数据。
  2. 静态函数会被自动分配在一个一直使用的存储区,直到退出应用程序实例,避免了调用函数时压栈出栈,速度快很多。
  3. 关键字“static”,译成中文就是“静态的”,所以内部函数又称静态函数。但此处“static”的含义不是指存储方式,而是指对函数的作用域仅局限于本文件。 使用内部函数的好处是:不同的人编写不同的函数时,不用担心自己定义的函数,是否会与其它文件中的函数同名,因为同名也没有关系。

在类中使用static时,需要注意以下事项:

  1. 类的静态成员函数是属于整个类而非类的对象,所以它没有this指针,这就导致了它仅能访问类的静态数据和静态成员函数。
  2. 不能将静态成员函数定义为虚函数。
  3. 由于静态成员声明于类中,操作于其外,所以对其取地址操作,就多少有些特殊,变量地址是指向其数据类型的指针,函数地址类型是一个“nonmember函数指针”。
  4. 由于静态成员函数没有this指针,所以就差不多等同于nonmember函数,结果就产生了一个意想不到的好处:成为一个callback函数,使得我们得以将C++和C-based X Window系统结合,同时也成功的应用于线程函数身上。
  5. static并没有增加程序的时空开销,相反她还缩短了子类对父类静态成员的访问时间,节省了子类的内存空间。
  6. 静态数据成员在<定义或说明>时前面加关键字static。
  7. static变量作为类的成员时,类中只是声明,需要在类外对该变量进行初始化。并且,static成员不属于对象,而是属于对应的类。
  8. C++中局部静态对象在程序的执行路径第一次经过对象定义语句时初始化,并且直到程序终止才被销毁,类成员函数中亦是如此。

全局变量以及全局变量与静态变量的关系:

顾名思义,全局变量是指能够在全局引用的变量,相对于局部变量的概念,也叫外部变量;同静态变量一样,全局变量位于静态数据区,全局变量一处定义,多处引用,用关键字“extern”引用“外部”的变量。

全局变量也可以是静态的,在前面有过说明,静态全局变量的意义就是不让“外部”引用,是单个源文件里的全局变量,即是编译阶段的全局变量,而不是连接阶段的全局变量。

库之间static常见错误

C++保证所有用到的static或者全局遍历在main函数之前完成初始化,但不保证static变量的初始化顺序。这是一个显然的事情,同时也往往会被我们所忽视,从而造成一些奇怪的运行时错误。假设以下一种场景:1. liba中有一个静态变量var1,同时liba中提供了一个单利管理的资源handle1,单利中使用var1构造唯一资源;2. libb中有一个静态函数fun1,其中调用了liba的单利返回一个值;libb中同时有一个静态变量var2 = fun1()。这里就会产生运行时错误,由于static的初始化不存在顺序,故存在以下case:

  1. 初始时var2, 调用函数fun1()
  2. fun1()中使用单利返回的handle1,返回值。
  3. 单利第一次使用需要构造对象,构造过程中发现var1还没有构造,因此抛出异常。

static实现单例模式(懒汉)

单例模式是我们经常使用到的设计模式,例如:应用程序的日志应用、数据库连接池、线程池等场景。那么C++中如何实现单例模式呢?我们已经提到C++中成员函数的静态变量特点:静态局部变量在第一次经过定义语句时初始化,知道程序终止时才会销毁。这种特性使得C++能够轻松的实现单例,话不多说直接看单例模式代码:

class Singleton {
public:
	Singleton(const Singleton&) = delete;             //阻止拷贝
	Singleton &operator=(const Singleton&) = delete;  //阻止赋值

	static Singleton& getInstance() {
		static Singleton m_instance;
		return m_instance;
	}
private:
	Singleton() = default;
	~Singleton() = default;
}

int main() {
	Singleton &s1 = Singleton::GetInstance();
	Singleton &s2 = Singleton::GetInstance(); //s1与s2是同一对象的引用
	
	return 0;
}

这里需要注意的是,C++0X以后,要求编译器保证内部静态变量的线程安全性,可以不加锁。但C++0X以前,需要手动加锁。