C++—19、C++ 中静态存储static

今天来介绍c++中的static关键字,根据上下文有两种意思,一种是在类class或者结构体外使用static;另一种是在类class或者结构体内使用。总结一下:类外的static修饰的符号在link阶段是局部的,也就是它只对定义它的编译单元(.obj)可见。而类和结构体里面的static,表示这部分内存是这个类的所有实例共享的,简单来说,就算你实例化了很多次这个类或结构体,但那个静态(static)变量只会有一个实例(就是只有一个),类里面的静态方法也是一样,静态方法里没有该实例的指针(this)。之后会更详细的讲static在类或结构体里的意思—作用域(scope)。

一、类或者结构体外的static

1、正确使用static的方式

我们在一个static.cpp文件里面,只定义一个static变量。

除了前面的static,它看起来就跟普通的变量一样。这个static什么意思呢?它表示这个变量在link的时候只在这个编译单元(.obj)里可见。

(如果你不清楚c++的编译和链接link的原理,这里简要叙述下。

第一步:预处理

C/C++语言最常见的预处理就是将所有的“#define”删除,并且展开所有的宏定义。而预处理其实还包括:处理所有的条件编译指令,比如“#if”、处理“#include”预编译指令、删除所有的注释、添加行号和文件名标识等。

第二步:编译

编译会将源代码由文本形式转换成机器语言,编译过程就是把预处理完的文件进行一系列词法分析、语法分析、语义分析以及优化后生成相应的汇编代码文件。

编译后的.s也是ascii码文件。因为汇编也是人能看懂的文字编码形式,所以.s汇编文件也是ASCII码文件。

第三步:汇编

汇编过程调用汇编器AS来完成,是用于将汇编代码转换成机器可以执行的指令,每一个汇编语句几乎都对应一条机器指令。(非底层的程序员不需要考虑)

汇编后的.o文件是纯二进制文件。因为.o中放的是纯二进制的机器指令,所以我们打开后看不懂。
ASCII的源码被汇编为能被CPU执行的机器指令,.o文件中放的就是机器指令。但是.o文件还无法运行,需要链接后才能运行。

第四步:链接

链接是将所有的.o文件和库(动态库、静态库)链接在一起,得到可以运行的可执行文件(Windows的.exe文件或Linux的.out文件)等。它的工作就是把一些指令对其他符号地址的引用加以修正。链接过程主要包括了地址和空间分配、符号决议和重定向。

static变量或者函数表示在link到它实际的定义时,linker不会在这个编译单元.obj外面找它的定义。我们来看另一个cpp文件(也就是另一个编译单元),这儿是main函数的cpp文件。我们创建一个跟前面静态变量名字一样的全局变量并赋值为10,然后输出它。

发现没有任何问题。

2、没使用static导致变量的重复定义

当我们把static.cpp文件里的static去掉。编译运行发现有一个重复定义的变量,如下图:

可以看到在linking阶段有个link错误。因为s_Variable已经在另一个编译单元里定义了,所以两个全局变量的名字不能一样。

3、如何使用另外cpp文件中的变量-extern

一种解决办法是把这个变成另一个的引用,就是把赋值去掉再加上extern关键字如下图:

加上extern关键字,它就会在另外的编译单元里找s_Variable的定义,这被称为external linkage或者external linking,这样运行代码就正常了。因为他是static.cpp文件中变量的引用。

但是加入我在变量声明前面加上static,那外部就找不到了,如下图:

这有点像在class里面声明私有(private)成员,其他的编译单元不能访问s_Variable。

linker在全局作用域下找不到它,所以就出现了上面的无法解析的外部符号,也就是因为linker在任何地方都找不到s_Variable的定义,因为我们把这个变量标记成了“私有”的。

4、函数和变量情况是一样的

和之前一样,我们来声明一个函数。

main里面也声明了个有同样签名、没有返回值的函数。

调试就会发现,多了一个错误,在链接阶段就出现错误重复定义。如下图:

根据变量的情况,我们只要在函数前面加上static,这个函数就变成所在编译单元私有的了,链接阶段外部就无法找到了,如下图:

运行结果如下:

可见,这时因为linker就看不到这个函数了,就没有错误了。

上面这几个实例充分说明了c++里在类和结构体外的static关键字的所有含义。当你在类或者结构体外定义一个静态函数或者静态变量,这意味着你定义的函数和变量只对它的声明所在的cpp文件(编译单元)是可见的。

假如你在头文件中定义了静态变量,然后在两个cpp文件里包含了该头文件,这样s_Variable在两个编译单元里都声明为静态变量,因为#include头文件,就是把头文件里的所有东西复制粘贴到cpp中。所以,其实你就是在两个编译单元都创造了static变量。什么时候用static,想想什么时候对class成员用private吧。

如果你不想变量是全局可见的,基本上static用的越多越好。因为你不带static定义全局变量的话,你会发现链接器会跨编译单元进行链接,因为你创建的是全局变量。

假如我有个全局变量variable,突然之间variable这个名字就全局可用了,就可能出现奇形怪状的bug。归根到底全局变量很糟糕。

重点:除非需要用到其他的编译单元里,否则尽量让全局函数和变量用static修饰。

二、类或者结构体内的static

现在我们来讨论下在class或者struct内部的static是什么意思?几乎在所有的面向对象的编程语言中,class内部的static代表一个特定的东西,如果你在类内定义了一个static变量,这意味着这个类的所有实例中,这个变量只有一个实例。

如果我新建一个叫Entity的类,然后我不断地创建这个Entity类的实例,但是那个static变量仍然只会有一个,这意味着如果一个实例改变了这个static变量,这个改变会体现在所有的类实例中,因为这个变量只有一个,即使我创建了很多个类的实例。正因为这样,通过类实例来引用静态变量是没有意义的,因为这就像是这个类的全局实例。

静态方法也是一样的,静态方法不能访问类的实例,静态方法不通过类的实例就可以调用(使用类名加范围解析运算符::就可以访问),在静态方法的内部也无法访问到类的实例,你不能写引用类的实例的代码。举例说明:

#include<iostream>
struct Entity//这里用struct的原因,是让变量下x,y默认是public的
{
	int x;
	int y;
	void print()
	{
		std::cout << x << y << std::endl;//用标准库的cout方法打印一些信息到控制台
	}

};
int main()
{
	Entity e;//类的实例化
	e.x = 2;
	e.y = 3;

	Entity f = { 4,5 };//类的实例化
	e.print();
	f.print();

		std::cin.get();
}

运行结果如下:

程序很简单,顺利打印出结果。

1、静态变量必须声明

如果我把变量改成静态的,如下所示:

#include<iostream>
struct Entity//这里用struct的原因,是让变量下x,y默认是public的
{
	static int x;
	static int y;
	void print()
	{
		std::cout << x << y << std::endl;//用标准库的cout方法打印一些信息到控制台
	}

};
int main()
{
	Entity e;//类的实例化
	e.x = 2;
	e.y = 3;

	Entity f = { 4,5 };//类的实例化
	e.print();
	f.print();

		std::cin.get();
}

把变量改成静态的,首先初始化这里就会报错。

因为x,y不再是类成员,所以我们换种写法:

#include<iostream>
struct Entity//这里用struct的原因,是让变量下x,y默认是public的
{
	static int x;
	static int y;
	void print()
	{
		std::cout << x << y << std::endl;//用标准库的cout方法打印一些信息到控制台
	}

};
int main()
{
	Entity e;//类的实例化
	e.x = 2;
	e.y = 3;

	Entity f;//类的实例化
	f.x = 5;
	f.y = 6;
	e.print();
	f.print();

		std::cin.get();
}

注意,这里还是有错误的,如下图:

这里我们需要对静态变量作用域进行声明

#include<iostream>
struct Entity//这里用struct的原因,是让变量下x,y默认是public的
{
	static int x;
	static int y;
	void print()
	{
		std::cout << x << y << std::endl;//用标准库的cout方法打印一些信息到控制台
	}

};
int Entity::x;
int Entity::y;
int main()
{
	Entity e;//类的实例化
	e.x = 2;
	e.y = 3;

	Entity f;//类的实例化
	f.x = 5;
	f.y = 6;
	e.print();
	f.print();

		std::cin.get();
}

这样再运行就不会出错了。

你会发现打印了2次5和6.我们在第一个实例处把下x,y分别设置为2,和3.第二个实例处设置成5和6.然而当我们把x,y设置为静态变量的时候,我们就把这两个变量变成在所有Entity类的实例中只有一个副本,这意味着我改变第二个Entity实例的x和y,第一个也会改变,它们指向的是同一个内存空间。可以想象下就是两个不同的实例的下x和y变量指向的是同一个共享空间。(但是类的成员变量加上static必须声明这一点,搞得变量好像是全局变量一样,但是它又是类的,静态成员变量是整个数据结构共享的,直到程序结束它的生命周期)。

2、静态变量的正确引用方式:

#include<iostream>
struct Entity//这里用struct的原因,是让变量下x,y默认是public的
{
	static int x;
	static int y;
	void print()
	{
		std::cout << x << y << std::endl;//用标准库的cout方法打印一些信息到控制台
	}

};
int Entity::x;
int Entity::y;
int main()
{
	Entity e;//类的实例化
	Entity::x = 2;
	Entity::y = 3;

	Entity f;//类的实例化
	Entity::x = 5;
	Entity::y= 6;
	e.print();
	f.print();

		std::cin.get();
}

静态变量需要这样引用,类名::变量名。就像是在名叫Entity的命名空间里创建了两个变量。它们实际上并不属于类,从这个意义上说,它们可以是private的,或者是public的,它们仍是类的一部分,而不是命名空间,但是这样来看,它们大致上和在命名空间里一样。当新建类的实例或者类似的东西时,它们并不会重新分配内存。

通过上面这种写法,你大概就容易理解为什么打印相同的数值了。当你想要跨类使用变量的时候,这会很有用。你可以只建一个全局变量,或者用一个静态全局变量代替全局变量。它会在单元内部进行链接,而不会变成在整个项目中都是全局可见的,这样做有相同的效果。

那为什么还要这么做(在类内部使用staic)?是因为把它们放在类内是有意义的。如果你有一些东西,比如说你有一条信息,你想要在所有的实例中共享,例如售票剩余票数,那么讲这条信息放在类内部是有意义的,因为它是和类相关的。

要组织好代码,你最好在这个类中创建一个静态变量,而不是到处创建静态或者全局变量。

静态方法也是一样的。

3、静态方法的正确使用方式

静态方法也是一样的,如果我把print函数改成static,它仍然可以工作,因为它引用的x,y也是静态变量。它的调用同样可以写成Entity::Print();事实上这才是正确的调用方式。因为我们使用了同样的方法Entity::Print();,所以两次打印的效果是相同的。

上面这2个实例中都用不到类的实例,直接类名::静态变量名或者类名::静态函数名。

4、静态函数的易错点

如果我们把x,y改成非静态的,print函数仍然是静态的,事情就不一样了。因为静态方法不能访问非静态变量。

运行程序,错误提示如下图:

因为静态方法不能访问非静态变量。

有时候我们会对静态方法能否访问非静态的东西感到很困惑,通过这个实例的错误输出,对非静态成员“Entity::x”的非法引用,想必你就直到答案了

你没法从静态方法访问到x,原因就是静态方法没有类实例。但本质上你在类里写的每个非静态方法都会获得当前的类实例作为参数(this指针),这就是类背后的实际工作方式。在类当中,它们通过隐藏参数发挥作用。

静态方法没有那个隐藏参数。静态方法和你在类外部编写的方法是一样的,因为类外部编写的方法根本就不认识类内的成员变量是什么。(这就是所谓的属于类但不属于实例),假设你另外有个print函数,但是它有个参数是Entity的实例。如下图:

上图中的方法,本质上就是一个类的非静态方法在编译时的真正样子。类的静态方法不知道要怎么访问Entity的x,y,因为你没有给它一个Entity的引用。而类的非静态的方法默认是有指向本实例的指针,而静态方法没有,因为静态的方法时不属于任何一个实例,和放在类外面定义一样的。

5、类的静态方法与非静态方法的区别

在面向对象编程中,类的方法可以分为静态方法(static methods)和非静态方法(non-static methods)。这两种方法在生命周期、调用方式、访问权限等方面存在显著差异。以下是它们的主要区别:

特性静态方法(Static Methods)非静态方法(Non-Static Methods)
生命周期与类的生命周期相同与类的实例生命周期相同
调用方式可以通过类名直接调用必须通过类的实例调用
访问权限只能访问静态成员可以访问静态和非静态成员
内存分配在类加载时分配在实例化对象时分配
线程安全可能存在线程安全问题通常不存在线程安全问题
效率通常效率更高效率相对较低
  1. 生命周期
    • 静态方法:静态方法的生命周期与类的生命周期相同。类加载时,静态方法会被加载到内存中,并且在整个程序运行期间都存在。
    • 非静态方法:非静态方法的生命周期与类的实例生命周期相同。只有当类被实例化时,非静态方法才会被创建,当实例被销毁时,非静态方法也会被销毁。。
  2. 调用方式
    • 静态方法:可以通过类名直接调用,例如 ClassName.staticMethod() 。也可以通过类的实例调用,但不推荐。
    • 非静态方法:必须通过类的实例调用,例如 instance.nonStaticMethod()
  3. 访问权限
    • 静态方法:只能访问类的静态成员(静态变量和静态方法),因为静态方法属于类级别,而不是实例级别。
    • 非静态方法:可以访问类的所有成员,包括静态成员和非静态成员(实例变量和实例方法)。
  4. 内存分配
    • 静态方法:在类加载时分配内存,属于类的静态成员,所有实例共享同一个静态方法。
    • 非静态方法:在实例化对象时分配内存,每个实例都有自己的非静态方法副本。
  5. 线程安全
    • 静态方法:由于静态方法和静态变量是共享的,可能会存在线程安全问题,需要特别注意并发访问。
    • 非静态方法:通常不存在线程安全问题,因为每个实例都有独立的非静态成员。
  6. 效率
    • 静态方法:通常效率更高,因为不需要实例化对象就可以调用。
    • 非静态方法:效率相对较低,因为需要先实例化对象。

  静态方法和非静态方法各有优缺点,选择使用哪种方法取决于具体的应用场景。如果方法不需要访问实例成员,并且希望提高效率,可以考虑使用静态方法。如果方法需要访问实例成员,则必须使用非静态方法。

三、局部作用域中的static

你可以在局部作用域中用static来声明变量,这和我们之前学过的另外两种static的用法不同,只要你理解了声明变量时要考虑的两点,很容易就能理解这种不同,这两点就是变量的生命周期和作用域。生命周期的意思是变量实际的存在时间,也就是变量在被删除之前在内存中停留多久。作用域就是我们可以访问这个变量的范围。比如说我们在一个函数的内部声明了一个变量,那我们就不能在其他函数里访问到这个变量,因为我们生命的变量相对于我们声明的函数是局部的。

而静态局部(local static)变量允许我们声明一个变量,它的声明周期是整个程序的生存期,但是作用域被限制在这个函数里,其实也不一定是函数,你可以在任何作用域中声明变量,我们只是用函数举例,不一定非要是函数,也可以是if语句,或者其他任何地方,这就是为什么函数作用域中的static和类作用域中的static之间没有太大的区别,因为它们的声明周期是一样的,唯一的区别就是类作用域中的静态变量,类内部的任何东西都能访问到它(静态变量),但是如果你在函数作用域中声明静态变量,那它就是函数的局部变量,就像类的静态变量对类也是“局部”的。

如下:

void Function()

{

static int i=0;//这句的意思是,当我们第一次调用这个函数时,它的值被初始化为0,后续调用不会再创建一个新的变量

}

我们来证明一下:

1、普通函数的调用实例

运行结果是1被输出了5次。因为每次调用时都会创建一个变量,先赋值为0,再增加1,然后输出到控制台。

2、普通函数中含有静态变量的实例

如果我们把变量设为静态(static)静态时:

3、全局变量在函数中使用的实例

和声明全局变量很相似,我们看看全局变量的情况

(补充:

什么是静态变量?

是储存在静态数据区的变量。静态变量会在程序开始运行时就完成数据初始化,这是唯一的一次初始化。共有两种变量存储在静态存储区:全局变量和 static 变量,只不过和全局变量比起来,static 可以控制变量的可见范围,说到底 static 还是用来隐藏的。虽然这种用法不常见。
static局部变量在函数内定义时, 它的生存周期是整个源程序,但是只能在定义该变量的函数内使用。退出函数后,static局部变量还会存在,但不能使用该变量。)

上面程序一开始i等于0,然后自增5次,会得到12345.

不好之处是:

但是这种情况的问题是,我能在任何地方访问到i,我可以在调用函数的中间位置,把i设为其它值,我们来看看结果。

通过上面实例比较可以看出,当你想要这种效果而又不希望其他人访问到它时,可以这么用函数中的静态变量。

4、函数静态变量的优点如下:

当i为函数内的静态局部变量时,外部是不允许访问的,是不可见的,编译结果如下:

也就是说我们第一次调用函数时,创建变量i,它的值被设置为0,接下来的调用,i还是指向原来的变量(初次调用创建的变量)。但是i不能在全局被访问到,只能在函数内的局部作用域被访问到。

四、总结

在C++中,静态变量可以声明在函数内部、类内部或文件范围内,具体取决于变量的作用域需求。

以下是C++静态变量的相关信息:

类型特点
函数内部静态变量在函数体内,静态变量具有记忆功能,即一个被声明为静态的变量只会被初始化一次,然后在这一函数被调用的过程中其值维持不变
文件内静态变量用来限制变量或函数的作用域为当前文件,即如果一个变量被声明为静态的,那么该变量可以被当前文件内所有函数访问,但不能被其他文件中的函数访问。它是一个本地的全局变量,且只会被初始化一次。如果一个函数被声明为静态的,那么该函数与普通函数作用域不同,其作用域仅在本文件中,它只可被当前文件内的其他函数调用,不能被其他文件的函数调用。也就是说,这个函数被限制在仅能被声明它的文件内使用
类内静态数据成员对于非静态数据成员,每个对象都有自己单独的一个副本。而静态数据成员被当作是类的成员,只会存在唯一的副本,且被所有对象共享。静态成员变量属于类而不属于对象。也就是说,即使没有实例化的对象,也可以使用静态变量,通常通过类名::静态成员变量来访问。static成员变量的初始化是在类外,初始化的时候不需要再使用static关键字。被private或protected修饰的static成员虽然可以在类外初始化,但是不能在类外被访问
类内静态成员函数被static修饰的函数是类的静态成员函数,静态成员函数也属于类,而不属于某一个特定对象,被所有对象共享。因此,它没有this指针。从这个意义上讲,类的静态成员函数无法访问对象成员,也无法访问普通成员函数,它只能访问静态成员函数或静态成员变量

需要注意的是,静态变量在使用时应当注意其作用域和生命周期,避免滥用并确保正确使用

五、单例类

C++单例类是一种设计模式,它确保一个类只有一个实例存在,并提供一个全局访问点来访问这个实例。单例类的实现方式有多种。在应用程序中,经常用于配置,日志等的处理。

如果我想不通过静态局部作用域(local static scope)来创建一个单例类,我得创建某种静态的单例实例。得有指针,还得有你想返回得一个引用,我得有返回引用类型的Get静态方法,返回解引用的实例。还得全局声明这个实例。

下面是单例类的一个写法:

#include<iostream>
class Singleton
{
private:
	static Singleton* s_Instance;//定义一个静态的Singleton类型的指针
public:
	static Singleton& Get()//使用引用,就是 Singleton类型变量的别称,
		//返回的是一个Singleton类的实例的引用
	{
		return *s_Instance;//返回的是指针地址所指的Singleton类型的变量
	}
	void Hello()
	{

	}
};
Singleton* Singleton::s_Instance = nullptr;//静态变量需要全局声明,注明变量的作用域
int main()
{
	Singleton::Get().Hello();//Singleton::Get()为类的静态方法,
		//返回的是一个Singleton类型的变量(就是实例的引用)
		//让这个实例调用类的方法,就像e.print();
		//Singleton::Get()得到一个可用的类的实例
		std::cin.get();
}

我们刚刚学的静态局部变量,看看它的实现方法:

#include<iostream>
class Singleton
{
public:
	static Singleton& Get()//使用引用,就是 Singleton类型变量的别称,
		//返回的是一个Singleton类的实例的引用
	{
		static Singleton instance;
		return instance;//返回的是指针地址所指的Singleton类型的变量
	}
	void Hello()
	{

	}
};
int main()
{
	Singleton::Get().Hello();//Singleton::Get()为类的静态方法,
		//返回的是一个Singleton类型的变量(就是实例的引用)
		//让这个实例调用类的方法,就像e.print();
		//Singleton::Get()得到一个可用的类的实例
		std::cin.get();
}

重点:static修饰的变量创建后地址是固定的,即该变量名访问的地址是唯一的,并且其生命周期与全局变量一致。

如果这里没有static关键字(static Singleton instance;)这个Singleton实例会在栈上创建,运行到这个花括号跳出函数作用域时就会被销毁,这将是一个严重的错误,特别是这里还是它的引用,如果返回一个复制,那就没什么问题。但是因为我们在这里返回一个引用,这就是一个大问题。但是加上static关键字后,这就把它的生命周期变成了永久。第一次我调用Get()将会创建一个Singleton类的实例,之后所有的调用都会返回这个存在的实例。

这是一个当你想用local static的很好实例,也不一定局限于单例,它们可以帮你替换初始化函数,比如你要调用一个静态初始化函数,在程序的某个位置来创建所有的对象,这样你可以用静态的Get()方法之类的东西,简化代码,不用初始化静态变量,直接调用GET方法相当于直接初始化了。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Growthofnotes

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值