背景
前段时间初步整理了C++中static的相关知识点,以此做个记录。
在C++中,static关键字是常见的修饰符。从大方向上static分为两类:
1.类或结构体外的static
2.类或结构体内的static
因此,本文内容的划分如下:
接下来会结合 静态全局变量/函数 、静态成员变量/函数、静态局部变量 这三个部分,分别通过代码说明static的特点,以及它们的使用场景,最后拿出几个问题进行讨论(代码可以自己实践,为了突出重点,本文不放入程序运行结果截图)。
一、静态全局变量/函数
1、主要特点
静态全局变量/函数的主要特点就在于内部链接。
那什么是“内部链接”属性呢?
“内部链接”就是一个名称对编译单元来说是局部的,在链接的时候其他编译单元无法链接到它。
(编译单元:源代码文件及其所包含的头文件的总和,经过预处理之后生成的文件。可以简单理解为一个.cpp文件)
通俗上说,就是静态全局变量/函数的的可见性被限制在定义它的文件中,程序中的其他文件无法访问。
用代码举例:
在Test1.cpp中我们定义一个全局变量value,并在MyClass.cpp中定义同名变量并打印它。
//Test1.cpp
#include <iostream>
using namespace std;
int value = 5;
//MyClass.cpp
#include <iostream>
using namespace std;
int value = 10;
int main()
{
cout << value << endl;
return 0;
}
以上代码会报重复定义的问题,这是因为我们不能定义同名的全局变量,因为全局变量的作用域是整个程序。
但如果我们在Test1.cpp中用static修饰value,这时候再运行,控制台会打印10。因为此时Test1.cpp中的value只在Test1.cpp可见,有点类似在类中定义了一个私有变量。(另一种解决全局变量命名冲突的方式,是将MyClass.cpp中的value定义更改为extern声明,即:extern int value;,意味着这里的value实际指向的是Test1.cpp中的value)
函数也是类似,可以用以上方式写个Function函数去验证。
2、静态全局变量的使用场景
静态全局变量是一种在文件范围内可见的变量。可以在以下场景中使用:
1、作用域控制。(如何限制全局变量的作用域,使其只在定义它的文件内可见?)
2、数据隐藏,实现模块私有数据。(如何在模块中存储数据而不影响其他模块?)
3、避免命名冲突。(如何避免在大型项目中全局变量名称冲突?)
二、静态成员变量/函数
1、主要特点
1.静态成员变量
静态成员变量的主要特点就是内存共享。
也就是说如果有一个类,类中有一个静态成员变量,当我们不断创建这个类的实例,实际上它们的这个静态成员变量指向相同的内存。当其中一个类实例改变了这个变量的值,其他类实例的这个变量也会更改。
进一步讲,静态成员变量是属于类的,不是类实例的。
所以在讨论到生命周期的时候,普通成员变量会在对象创建时被初始化,在对象销毁时被销毁,与对象的生命周期相同;静态成员变量不依赖对象的创建,它在程序启动时被初始化,在程序结束时被销毁,与程序的生命周期相同。
通过代码举例:
//MyClass.cpp
#include <iostream>
using namespace std;
class Entity {
public:
int x, y;
};
int main()
{
Entity e1;
e1.x = 2;
e1.y = 3;
Entity e2;
e2.x = 5;
e2.y = 6;
return 0;
}
这是一个普通的类Entity,有两个成员变量x和y,我们创建两个实例,分别对x、y赋值,如果分别打印x、y的数值,那么就如代码所写的,分别是2、3以及5、6。因为每个实例都有自己的x和y,互相不受影响。
当我们在x,y前增加static,那么它们就变成了静态成员变量。
这里有一个地方需要注意!
如果我们在这时直接运行代码,可以发现报错了:静态成员变量x、y未定义。这是因为我们在类内这样写:
class Entity {
public:
static int x, y;
};
这实际上只是声明,我们还需要在类外对静态成员变量进行定义。(静态成员变量是类内声明,类外定义。至于为什么要在类外定义,后续会说明。)
class Entity {
public:
static int x, y;
};
int Entity::x;
int Entity::y;
//定义的方式是: 变量类型 作用域::变量名;
//可以赋初值,也可以不用。
这时运行代码,打印出的两个实例的x、y都是5、6,因为静态成员变量x、y是所有对象共享的。
由于静态成员变量属于类,不属于类实例,我们可以不创建实例,直接通过类名去访问变量:
int main()
{
Entity::x = 5;
Entity::y = 6;
return 0;
}
以上是静态成员变量的内容,我们通过代码说明了它的共享内存的特点。
2.静态成员函数
静态成员函数的主要特点在于:静态成员函数不能直接访问非静态成员变量。
原因是普通的成员函数实际上是通过获取当前类的实例去访问变量的,也就是说有一个隐含的this指针,指向调用该函数的对象实例,但问题在于静态成员函数没有this指针,也就无法知道是哪个对象在访问变量。
普通成员函数在编译时的真实样子:
//假设Entity类有一个打印函数Print
void Print(Entity e)
{
cout << e.x << e.y << endl;
}
//可以看到这里会有一个隐藏参数Entity e,通过这个对象e,我们可以访问到这个类的成员变量。
//但是静态成员函数没有这个隐藏参数(因为静态成员函数不属于类实例),因此无法直接访问到成员变量。
这里还有一个地方主要注意:为什么要强调“直接访问”呢,因为我们实际上可以通过在静态成员函数中创建对象的方式去间接访问到非静态成员变量,如下所示:
class Entity {
public:
int x, y;
};
class A {
static void Test() {
Entity e;
e.x = 5;
e.y = 6;
}
};
2、静态成员变量的使用场景
静态成员变量是属于类的,而不是属于类的任何一个对象。可以在以下场景中使用:
1、数据共享。(如何让一个类的所有对象共享一个变量?)
2、类级别的常量。(如何在类内部定义一个常量,使其可供所有对象使用?)
3、计数器或唯一ID生成器。(如何为每个对象生成一个唯一的ID?)
4、配置或状态存储。(如何在类中存储一些全局配置或状态信息?)
三、静态局部变量
1、主要特点
静态局部变量与前面的两种static又有些区别,当我们定义一个静态局部变量时,我们需要考虑两个部分,一个是作用域,另一个生命周期。静态局部变量的作用域是局部(函数、if语句等等),但它的生命周期是整个程序。
生存周期延长到整个程序的执行周期,这意味着在函数调用结束后静态局部变量不会被销毁,而是保留其值供下一次调用使用。
用代码举例:
#include <iostream>
using namespace std;
void Function()
{
int i = 0;
i++;
cout << i << endl;
}
int main()
{
Function();
Function();
Function();
return 0;
}
我们在函数中定义了一个普通的局部变量,程序运行结果是 1 1 1。每次调用函数时,i总会被初始化为0,然后自增一次,最后打印,所以每次调用,这个i值最后总为1。
如果我们使用static修饰这个i,程序运行结果时 1 2 3。这是因为当第一次调用函数时,i会被初始化为0,而之后的每次调用,i的值不会再初始化,而是保留值进行下一次操作。
这里程序运行的效果和我们定义一个全局变量是相同的,但是由于程序的任何位置都可以访问全局变量,并对其进行更改,因此这极大增加了程序的可操作性。就如下代码所示,我们可以在函数调用之余对i值进行更改:
#include <iostream>
using namespace std;
int i = 0;
void Function()
{
i++;
cout << i << endl;
}
int main()
{
Function();
Function();
i = 10;
Function();
return 0;
}
当我们不希望其他人直接访问i,而只能通过函数调用的形式去访问,那么我们就可以将i值放入函数中,为了达到可以输出 1 2 3 这样的结果,就使用static将其变为静态的局部变量。
同样的,另一个常见的例子就是单例类(只存在一个实例的类):
#include <iostream>
using namespace std;
class Singleton {
public:
static Singleton& Get() {
static Singleton instance;
return instance;
}
void Print() {}
};
int main()
{
Singleton::Get().Print();
return 0;
}
可以看到我们定义了一个单例类,命名为Singleton。当我们第一次调用Get()函数,静态局部变量instance会被初始化一次,而之后Get()的每次调用,只会得到已经存在的instance,那么这样我们就能保证这个类只有一个实例。
2、静态局部变量的使用场景
静态局部变量的存在是为了提供一种既具有局部作用域、又具有全局生命周期的变量。可以在以下场景中使用:
1、单例模式。(如何确保一个类只有一个实例,并提供一个全局访问点?)
2、函数级别的状态保持。(如何在函数调用之间保持状态,而不使用全局变量?)
3、避免重复初始化。(如何避免在函数多次调用时反复初始化资源? 如何在函数内缓存计算结果,以提高性能?)
四、Q&A
问:
为什么不能在类的内部定义以及初始化static成员变量?
答:
因为如果在类的内部定义并初始化静态成员变量,意味着每个对象都包含该静态成员,这些同名的静态成员变量实际上是不同的变量,不是共享的。
我们将其声明在类的外部定义,可以确保静态成员变量在整个程序中只有一个实例,确保全局唯一性和正确的内存分配。
(需要注意的是,有个例外的情况:静态常量整型变量可以在类内定义,如:const static int value = 1; 除了int,还有char、bool、long long、枚举(enum 和enum class都可以)等。
静态常量整型成员可以在类内初始化,这是因为整型常量在编译时就可以确定其值,而且这些值通常很小,可以直接嵌入到代码中。这样做可以提高效率,因为不需要在运行时进行初始化。)
问:
为什么静态成员函数不能声明为const(const修饰成员函数)?
答:
const修饰的成员函数被用来表示函数不会修改该函数访问的目标对象的数据成员。
但是静态成员函数不与特定的类对象实例相关联,它不能访问对象的非静态成员变量。
所以,用const表示不修改对象状态的特性在静态成员函数中是多余的。
问:
为什么静态成员函数不能直接访问非静态成员变量?
答:
两个原因:
1.
静态成员函数只属于类本身,随着类的加载而存在,不属于任何对象,是独立存在的。
非静态成员只有在实例化对象之后才存在,静态成员函数产生在前,非静态成员产生在后,所以不能访问。
2.
静态成员函数没有this指针,无法直接访问类的非静态成员。
以上是所有内容,欢迎一起讨论!