深入学习C++——21~23静态
static在C++中有两种含义,分为类和结构体外的静态和类和结构体内的静态
21.类和结构体外的静态
类和结构体外部的static,意味着你声明为static的符号只在当前文件内部链接,它只对它所在的翻译单元可见。有关翻译单元请看深入学习C++——6~7编译器和链接器
新建一个源文件命名为CStatic.cpp
,在main.cpp
和Cstatic.cpp
中同时定义一个变量和一个函数,编译器在链接阶段会报错multiple definition of XXX
,这是因为这个变量/函数已经在另一个编译单元中定义了。所以我们不能有两个同名的全局变量/函数。其中一种修改方式是使用extern
关键字,extern意味着它会在外部翻译单元中寻找这个变量/函数。我们把main.cpp
中的定义加上static,再次编译无报错。
/*CStatic.cpp*/
int Variable = 5;
void Function()
{
std::cout << "Function in CStatic" << std::endl;
}
/*main.cpp*/
//int Variable = 5; //multiple definition of Variable
//void Function() //multiple definition of `Function()'
//{
// std::cout << "Variable" << std::endl;
//}
extern int Variable;
extern void Function(); //函数默认都是extern,这里删掉extern也可以编译通过
int main()
{
Function();
std::cout << Variable << std::endl;
}
另一种解决方法是使用static
关键字。static的意思是这个变量/函数只会在这个翻译单元内部链接。这有点像在类中定义私有变量/函数,其他所有的翻译单元都不能看到这个变量/函数。链接器在全局作用域下将不会看到这个变量/函数。把CStatic.cpp
中的变量与函数均改成静态的,编译会报错,因为他们在main中不可见,跨翻译单元是找不到他的。将main.cpp中的变量和函数修改为全局的,此时编译通过,且输出的是main中定义的变量值和函数。
/*CStatic.cpp*/
static int Variable = 5;
static void Function()
{
std::cout << "Function in CStatic" << std::endl;
}
/*main.cpp*/
//extern int Variable; //undefined reference to `Function()'
//extern void Function(); //undefined reference to `Variable'
int Variable = 10;
void Function()
{
std::cout << "Function in main" << std::endl;
}
int main()
{
Function();
std::cout << Variable << std::endl;
}
在类和结构体外使用静态,意味着当你声明静态函数和静态变量时,它只会在它被声明的C++文件中被“看到”。如果在一个头文件中声明静态变量并将该头文件包含在两个不同的C++文件中,就相当于在两个翻译单元中都声明了那个变量为静态变量。(包含头文件时,编译器会复制所有头文件中的内容到C++文件中)
为什么要用static?可以类比为什么要在类中用private。当不需要变量是全局变量时,尽可能地用静态变量。因为一旦在全局作用域下声明东西的时候,编译器会跨编译单元进行链接,这个变量在任何地方都可以被使用,可能会导致一些很严重的bug。
综上,尽可能地标记函数或变量为静态的,除非你真的需要他们跨翻译单元链接。
22.类和结构体内的静态
在类和结构体内部定义的静态变量,在类的所有实例中这个变量只有一个实例,这意味着该变量实际上将与类的所有实例共享内存。对于静态函数来说,没有实例会传递给一个静态函数。
下面用结构体举例子(类也一样,只是因为结构体默认是public的)
struct Entity
{
int num;
void Print()
{
std::cout << num << std::endl;
}
};
int main()
{
Entity e1;
e1.num = 2;
Entity e2;
e2.num = 5;
e1.Print();
e2.Print();
}
很显然,这样会输出2和5。如果将结构体内的x和y变成静态的,会报错undefined reference to ‘Entity::num’
,因为x和y要在某个地方被定义,加上int Entity::num;
,程序修改为:
struct Entity
{
static int num;
void Print()
{
std::cout << num << std::endl;
}
};
int Entity::num;
int main()
{
Entity e1;
e1.num = 2;
Entity e2;
e2.num = 5;
e1.Print();
e2.Print();
}
这时运行会输出5和5.这是因为num变量在Entity类的所有实例中只有一个实例,这意味着e1.num和e2.num指向的是相同的内存,所以e1.num
和e2.num
这样写是没有意义的。可以直接写成Entity::num = 5
。这就像是在一个名为Entity的命名空间中创建了一个变量,他们实际上并不属于类,但是他们可以是private的也可以是public的,所以他们仍是类的一部分。但是在应用上来说他们其实和在命名空间中一样。
静态方法跟静态变量一样,如果将Print也改为静态的,调用时同样需要使用Entity::Print();
但静态方法不能访问非静态变量,将num改为非静态变量,Print保持为静态方法,这时编译会报错error: invalid use of member 'Entity::num' in static member function
,原因是静态方法没有类实例。每个非静态方法总是会获得当前类的一个实例作为参数,这我们是看不见的,在底层通过隐藏参数发挥作用,而静态方法不会得到那个隐藏参数。类中的静态方法拿到类外面在编译的时候实际上是这个样子的,实际上传进去了一个实例参数,这样就不会报错:
struct Entity
{
int num;
};
static void Print(Entity e)
{
std::cout << e.num << std::endl;
}
int main()
{
Entity e;
e.num = 5;
Print(e);
}
综上所述,当你需要跨类使用变量时,类内静态变量将会派上用场。那这么说创建一个全局变量或者静态变量不也一样吗?NO!如果你有一条消息,想要在所有实例之间共享数据,把这个消息变量放在类中是有意义的,因为它在逻辑上跟这个类有关。要想组织好你的代码,最好在类中创建一个静态变量,而不是将全局或者静态变量到处乱写。
23.局部静态
局部静态允许我们创建一个变量,它的生存周期基本相当于整个程序的生存期,但是作用范围被限制在这个域中。来看一段程序:
void Function()
{
int i = 0;
i++;
std::cout << i << std::endl;
}
int main()
{
Function();
Function();
Function();
Function();
Function();
}
显而易见输出为11111。要是想将i每次递增实现输出12345,你的第一反应可能是将i改为全局变量。但是这样做会使每个人都能访问这个变量,如果要避免这个问题,可以在局部作用域下将i声明为static。这样程序也可以输出12345,与全局变量效果相同,但是此时i只是函数作用域下的局部变量。
使用局部静态的主要作用是可以使代码更干净,我们来看另一个例子。创建一个单例类(单例类是只存在一个实例的类),如果不使用静态局部作用域来创建单例类,就需要创建静态的单例实例,可能是一个指针,并返回一个引用,即为创建的实例:
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();
}
如果使用局部静态来创建,代码会变得干净很多:
class Singleton
{
public:
static Singleton& Get()
{
static Singleton instance;
return instance;
};
void Hello()
{
std::cout << "Hello" << std::endl;
}
};
int main()
{
Singleton::Get().Hello();
}
这段代码里如果没有static,当代码运行到函数右花括号处,即函数作用域结束时,instance就会被销毁。通过添加static,它的生存周期被延长到永远,在第一次调用Get的时候,实际上会构造一个单例实例,接下来它只会返回这个已存在的实例。