static关键字在C++中有两个意思,取决于上下文,其中之一是在类或者结构体外部使用static关键字;另一种是在类或结构体内部使用static。
类或结构体外面的static意味着你声明为static的符号,链接将只在内部,意味着它只对你定义它的编译单元可见(每个cpp文件被称为一个独立的编译单元)。
类或结构体内部的静态(static)变量意味着该变量实际上将与类的所有实例共享内存,意味着该静态(static)变量在你创建的所有实例中只有一个实例。这同样适用于类中的静态(static)方法。
静态变量或者函数意味着当需要将这些函数或者变量与实际定义的符号链接时,链接器(Linker)不会在这个编译单元的作用域外去寻找那个符号的定义。
在Visual Studio中新建一个C++项目。新建两个CPP文件,分别叫做Main.cpp和Static.cpp,其中Main.cpp代码如下:
#include
int s_Variable = 5;
int main()
{
std::cout << s_Variable << std::endl;
std::cin.get();
}
Static.cpp代码如下:
int s_Variable = 5;
由于不能出现两个同名的全局变量,所以无法通过编译,解决方法有两个:
extern
标识符,意味着链接器会在外部编译单元中去找s_Variable
变量,这种写法被称为“Extern Linking”。s_Variable
变量前加static
修饰符,有点像在类里面声明一个私有变量,其他所有编译单元都无法看到这个静态的s_Variable
变量。函数和变量的过程同理。如果你不需要把变量变成全局变量,就尽可能多地使用static
关键字,频繁使用全局变量是一种非常不好的习惯。
另外,需要注意,const声明的全局变量具有内部链接性。
在上面的例子中,在Main.cpp中添加一行代码:
const int a = 1;
在Static.cpp中也添加一行代码:
const int a = 2;
编译通过,在Main.cpp中打印a的值,输出a的值是1。
在几乎所有面向对象的编程语言中,static
在类/结构体中意味着特定的东西,若对某变量加上static
关键字,这意味着在这个类的所有实例中,该变量只有一个实例,如果改变了这个静态变量,它会在所有示例中反映这个变化,因为所有实例中的静态变量指向一个地址。因此,通过实例来引用静态变量是毫无意义的。
下面是个例子,来印证这个说法:
#include
class Entity {
public:
static int x, y;
void print()
{
std::cout << x << ',' << y << std::endl;
}
};
int main()
{
Entity e;
e.x = 1;
e.y = 2;
Entity e1;
e1.x = 3;
e1.y = 4;
e.print();
e1.print();
std::cin.get();
}
编译时无法通过,错误代码如下:
error LNK2001: 无法解析的外部符号 "public: static int Entity::x"
error LNK2001: 无法解析的外部符号 "public: static int Entity::y"
因为我们需要在某个地方先定义这些静态变量,先写作用域,再写变量名:
int Entity::x;
int Entity::y;
通过运行,发现输出结果为:
3,4
3,4
这意味着给e1的x和y赋值完全覆盖了e的x和y,说明e的x和y和e1的x和y是一个东西,它们指向同一个内存。所以用实例e和e1访问静态变量x和y是完全没有意义的,取而代之的引用方式如下:
Entity::x = 3;
Entity::y = 4;
这就像在命名空间Entity中创建了两个变量x和y,它们某种意义上说并不属于Entity类,却又是类的一部分。
如果想要某个类的所有实例共享一条消息,就可以用static来实现。
静态方法与静态变量同理。
静态方法不需要类的实例就可以被调用,而静态方法内部不能写引用到类实例的代码,静态方法不能访问非静态变量。
修改上面的例子,将变量x和y变为非静态,方法print()
变为静态:
class Entity {
public:
int x, y;
static void print()
{
std::cout << x << ',' << y << std::endl;
}
};
尝试编译代码,会得到如下错误:
error C2597: 对非静态成员“Entity::x”的非法引用
原因比较复杂,我们一般在类中写的非静态方法在背后的执行总是会获得当前类的一个实例作为参数**(隐藏参数)**,静态方法没有类的实例,得不到隐藏参数,就像在类的外部编写一个函数,它根本不知道x和y是啥,所以可以给静态方法一个Entity对象,作为参数传入,就可以正常执行:
static void print(Entity e)
{
std::cout << e.x << ',' << e.y << std::endl;
}
……
Entity e;
e.x = 1;
e.y = 2;
Entity::print(e);
……
输出结果为:
1,2
非静态方法在背后就是这么执行的。
来看以下代码:
#include
void Increase()
{
int i = 0;
i++;
std::cout << i << std::endl;
}
int main()
{
Increase();
Increase();
Increase();
Increase();
Increase();
std::cin.get();
}
很显然,控制台会打印五次1,这是因为每次调用Increase()
函数,都会创建一个变量i
默认为0,然后i++
,打印。
现在在int i = 0
前面加上static
关键字:
static int i = 0;
输出结果如下:
1
2
3
4
5
这种写法类似于将变量i
作为全局变量,每次修改都得到了保留,但是又不一样,变量i
不是全局的。当首次运行该函数时,首先创建变量i
赋值为0,然后后面每次调用该函数,变量i都是最开始的变量i
。
在局部作用域(比如函数)中使用static来声明一个变量,该变量的生存期基本相当于整个程序的生存期,但是它的作用域就被限制在这个局部作用域内,外部无法访问它。
看下面的例子:单例类
单例类是只存在一个实例的类,不使用局部静态作用域的写法如下:
#include
class Singleton
{
private:
static Singleton* s_Instance;
public:
static Singleton& Get() { return *s_Instance; }
void Hello() { std::cout << "Hello!" << std::endl; };
};
Singleton* Singleton::s_Instance = nullptr;
int main()
{
Singleton::Get().Hello();
std::cin.get();
}
上面代码中,可以通过Singleton::Get()
得到实例,从而做任何想做的事情( 比如Hello()
),使用局部静态可以把代码变得更加简洁,得到相同的效果:
#include
class Singleton
{
public:
static Singleton& Get() {
static Singleton s_Instance;
return s_Instance;
}
void Hello() { std::cout << "Hello!" << std::endl; };
};
int main()
{
Singleton::Get().Hello();
std::cin.get();
}
如果Singleton s_Instance
前面没有static
关键字,那么这个实例会在栈上创建,当代码运行到函数最后的花括号,即函数作用域结束时,就会被销毁。通过添加静态,将实例的生存期延长到永远。