C++ 类中的static成员的使用及单例设计示例
静态成员:被static修饰的成员变量\函数
可以通过对象(对象.静态成员)、对象指针(对象指针->静态成员)、类访问(类名::静态成员)
static 静态成员变量
静态成员变量
- 存储在
数据段
(全局区,类似于全局变量),整个程序运行过程中只有一份内存
。 - 对比全局变量,它可以设定
访问权限
(public、protected、private),达到局部共享的目的。 - 必须初始化,
必须在类外面初始化
,初始化时不能带static,如果类的声明和实现分离(在实现.cpp中初始化)。
class Foo { private: //static静态变量无法在类中初始化 static int ms_age; //与普通的全局变量的区别是 作用域 的权限不同,static类成员变量受到类的权限控制 public: void test() { cout << ms_age << endl; } //提供接口以用来访问与修改静态成员变量 int& getMS_age() { return ms_age; } }; int Foo::ms_age = 1; //在类外使用::作用域解析初始化 int main() { Foo a; a.test(); //两个对象共享同一个static静态对象,static位于全局区,整个程序运行时只存在这一个 Foo b; b.test(); /* mov dword ptr [Foo::age (07FF768C2C000h)],64h mov dword ptr [Foo::age (07FF768C2C000h)],0C8h */ //调用的是同一个static静态成员变量 a.getMS_age() = 100; b.getMS_age() = 200; a.test(); b.test(); return 0; }
注意点:
- 在类中用static声明的变量是 静态变量,它存储在
数据段
(全局区,类似于全局变量),在整个程序运行时期只会有一份内存。 - 我们在一个类对象中修改的static静态成员变量会保持修改,则下一个类打印会发现static变量就不是以前的数据了,因为他类似于全局变量,相当于对全局变量进行修改。
两个类是否真的共用同一个static静态变量?我们来试验一下:
int main() { Foo a; Foo b; //ms_age为类中的static静态成员变量 a.ms_age=100; b.ms_age=200; return 0; }
进入反汇编:
mov dword ptr [Foo::age (07FF768C2C000h)],64h mov dword ptr [Foo::age (07FF768C2C000h)],0C8h
可以看到: 07FF768C2C000h 即是static变量的内存空间,a,b两个类的此变量的地址一样,说明他们共享同一个static变量,更加证明了类中的static变量其实就相当于全局变量,在整个程序运行期间不变且只有唯一的一个
。
那么? 类中的static静态成员变量和真正的全局变量 又有什么不同呢?
访问权限不同
:全局变量默认public,类中的static受到类的权限限定符的控制
class A{ private: static int a; //位于private中,私有化 public: int& getA() { return a; //返回此static变量 } } int abc; //全局 int main() { cout<<abc <<endl; //随意访问全局变量 cout<<A::a<<endl; //无法直接访问类中的static //提供一个接口来访问 getA()=100; //通过接口就可以访问了
类中static的成员变量的初始化
,注意:不可以直接在类内初始化,要在类外初始化,并且要去掉static修饰符,加上类名作用域::
class A{ private: static int a; //位于private中,私有化 } int A::a=0; //类外初始化
访问类中static变量的方法
: 我们要尽量使用类名直接访问static,因为他们本来就相当于全局的,否则容易产生混淆
int main() { Foo* p = new Foo; p->ms_age = 100; //OK:指针访问 a.ms_age = 200; //OK:栈对象访问 Foo::ms_age = 300; //OK(推荐):类名直接访问 return 0; }
static静态成员函数
静态成员函数
- 内部
不能
使用this指针(this指针只能用在非静态成员函数内部) 不能是虚函数
(虚函数只能是非静态成员函数)- 内部
不能
访问非静态成员变量\函数,只能
访问静态成员变量\函数 - 非静态成员函数内部可以访问静态成员变量\函数
- 构造函数、析构函数不能是静态
- 当声明和实现分离时,实现部分不能带static
class Foo { public: //静态函数内存不位于类中,它在外部定义,与类无关 static void test() { //静态成员变量可以随便访问 cout << "<static> test():" << count << endl; //testTemp(); //cout << "age:" << age << endl; //非静态成员函数无法访问age属性,因为此age不属于任何对象,也无法访问非静态成员函数,同样它需要一个对象,但是此静态函数位于外部的全局区,它不属于类中,因此没有对象的概念,无法直接使用非静态成员函数。 Foo a; a.age = 50; //但是可以创建一个临时的对象,利用对象来使用age成员变量 a.testTemp(); } void testTemp() { cout << "testTemp()\n"; } void test3() { //可以随意访问静态成员函数与变量 count = 220; test(); testTemp(); } private: static int count; int age = 10; }; int Foo::count = 0; Foo g; int main() { Foo a; Foo* b = new Foo; //任意类型创建的对象都可以正常访问静态函数 a.test(); b->test(); g.test(); //可以用类名直接指定此静态函数 Foo::test(); return 0; }
static静态成员函数:相当于外部的全局的函数
,它虽然在类中,但是和类没有半毛钱关系,它的内存位于全局区。
- 无法使用this 指针,并且不能访问非静态成员变量\函数,只能访问静态static成员变量\函数。
class A{ private: int a=20; static int b; public: static void test(){ this->a=50; //ERROR:this指针 a=100; //ERROR: 相当于this指针 A temp; temp.a=500; //OK:通过临时对象来访问temp的变量(闲的没事干) b=500; //OK:可以访问类的static静态成员函数,相当于全局变量 fun(); //ERROR:无法访问非静态成员函数 } void fun(); //一个非静态成员函数 void fun2() { test(); //OK:可以使用非静态成员函数访问static成员函数 } } int A::b=20;
Error: 因为this指针表示某个类对象把它所在的地址传给了this指针,然后可以通过this访问其所有的变量,但是static静态成员函数位于全局区,所以根据this获取不到某个对象的地址,并且不使用this,想要获取类成员函数也是不可能的(除非你创建一个临时的对象),并且也无法访问非静态成员函数,相反则可以。
但可以访问类的static静态变量,和其他静态函数。
- 静态成员函数不能是虚函数,不能使构造函数,不能是析构函数
- 声明和实现分离时,实现的时候不要加static修饰符。
单例设计模式
什么是单例设计模式?
在我们的程序运行中只允许存在唯一一个对象,不管我们调用了多少个创建对象,我们始终都是得到唯一一个。
三要素:
构造函数必须私有化
必须具有一个唯一的static静态变量成为那个单例
提供公共的访问对象的接口
我们来一步步实现,我们既然要创建唯一一个对象,则不允许对象随意利用构造函数和析构函数。
注意: 所有的类的函数都是static的静态成员函数,这样我们就可以通过类名直接调用
- 把构造和析构设置为私有,并且创建唯一的单例成为静态成员变量
class Res { public: private: static Res* p; //这就是我们的单例对象,是唯一的 Res(){} ~Res() {} //防止在外部使用delete };
- 单例的对象必须由我们亲自初始化: 给他一个构造对象的共有方法:p为空的时候才构造,这就实现了我们如果调用了多次创建对象的函数,但是只能创建一个对象。
static Res* CreatePoint() { if (p == nullptr) { p = new Res(); //调用构造函数 } return p; }
- 单例的销毁类成员函数:
static void Destroy() { if (p != nullptr) { delete p; p = nullptr; //注意此处重新置空,这样才能下一次创建对象 } }
- 测试:
int main() { //不管你创建多少次指针变量,他们都是同一个对象 Res* p = Res::CreatePoint(); //只会创建这一个 p->test(); Res* p1 = Res::CreatePoint(); p1->test(); Res* p2 = Res::CreatePoint(); p2->test(); Res* p3 = Res::CreatePoint(); p3->test(); p->Destroy(); //只会销毁这一次 p1->Destroy(); p2->Destroy(); p3->Destroy(); //delete p; //析构私有,防止使用delete return 0; }
单例实现图片资源的封装(很实用)
在我们游戏制作中很好用,比如逆向做一个飞机大战,你可以封装这样一个单例,把所有的资源都加载进map容器中
(键–值对),根据类的enum枚举可以选取每一张特定的图片,大家仔细体会:
- 头文件
class Res { public: enum class TYPE { plane, mm, bullet,往左走 }; static Res* GetRes(); static void Draw(int x,int y,TYPE name); static void Draw动画(int x, int y, TYPE name, int frame); static void Destroy(); private: static Res* res; //单例对象 static map<TYPE, IMAGE*> image; //私有构造函数和析构函数 Res(); ~Res() {} };
- 实现文件,我们使用了map集合。
/* 注意:首先在类的外部定义类的static成员变量 */ map<Res::TYPE, IMAGE*> Res::image{}; Res* Res::res = nullptr; //初始化单例对象 /* 注意: static成员函数的声明和实现分离时,实现要去掉static的修饰符 */ Res* Res::GetRes(){ if (res == nullptr) { res = new Res; //创建单例对象 } return res; } void Res::Draw(int x,int y,TYPE name){ //绘制静态图片 putimage(x, y,GetRes()->image[name]); } void Res::Draw动画(int x, int y, TYPE name, int frame){ //绘制连环画 :一系列动画的集合 /* 连环画,每个图片有八张连续的动作图片,每一帧换一张图片。即用帧数乘以宽度可以得到下一张图片, 利用putimage的裁剪图片的功能。 */ putimage(x, y, 400, 300, GetRes()->image[name]+0, frame * 400, 0, SRCAND); //掩码图 putimage(x, y, 400, 300, GetRes()->image[name]+1, frame * 400, 0, SRCPAINT); //原图 } //单例的销毁 void Res::Destroy(){ if (res != nullptr) { delete res; res = nullptr; } } Res::Res(){ /* 存储图片资源 */ image[Res::TYPE::plane] = new IMAGE; loadimage(image[Res::TYPE::plane], "./Res/plane.jpg"); image[Res::TYPE::bullet] = new IMAGE; loadimage(image[Res::TYPE::bullet], "./Res/bullet.jpg"); image[Res::TYPE::mm] = new IMAGE; loadimage(image[Res::TYPE::mm], "./Res/mm.jpg"); image[Res::TYPE::往左走] = new IMAGE[2]; loadimage(image[Res::TYPE::往左走] + 0, "./Res/lefty.bmp"); loadimage(image[Res::TYPE::往左走] + 1, "./Res/left.bmp"); }
- 简单测试:
int main() { initgraph(800, 600,SHOWCONSOLE); setbkcolor(WHITE); cleardevice(); //你想画什么,直接Draw即可,在Draw的时候,会创建单例对象,且只创建一次 Res::Draw(50, 100, Res::TYPE::plane); Res::Draw(50, 30, Res::TYPE::bullet); Res::Draw(200, 200, Res::TYPE::mm); Res::Destroy(); //销毁 system("pause"); closegraph(); return 0; }
本文来自博客园,作者:hugeYlh,转载请注明原文链接:https://www.cnblogs.com/helloylh/p/17209701.html
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 从 HTTP 原因短语缺失研究 HTTP/2 和 HTTP/3 的设计差异
· AI与.NET技术实操系列:向量存储与相似性搜索在 .NET 中的实现
· 基于Microsoft.Extensions.AI核心库实现RAG应用
· Linux系列:如何用heaptrack跟踪.NET程序的非托管内存泄露
· 开发者必知的日志记录最佳实践
· TypeScript + Deepseek 打造卜卦网站:技术与玄学的结合
· Manus的开源复刻OpenManus初探
· 写一个简单的SQL生成工具
· AI 智能体引爆开源社区「GitHub 热点速览」
· C#/.NET/.NET Core技术前沿周刊 | 第 29 期(2025年3.1-3.9)