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静态成员变量和真正的全局变量 又有什么不同呢?

  1. 访问权限不同:全局变量默认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; //通过接口就可以访问了
  1. 类中static的成员变量的初始化,注意:不可以直接在类内初始化,要在类外初始化,并且要去掉static修饰符,加上类名作用域::
class A{
private:
static int a; //位于private中,私有化
}
int A::a=0; //类外初始化
  1. 访问类中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静态成员函数:相当于外部的全局的函数它虽然在类中,但是和类没有半毛钱关系,它的内存位于全局区。


  1. 无法使用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静态变量,和其他静态函数。
在这里插入图片描述

  1. 静态成员函数不能是虚函数,不能使构造函数,不能是析构函数

在这里插入图片描述 在这里插入图片描述

  1. 声明和实现分离时,实现的时候不要加static修饰符。

单例设计模式

什么是单例设计模式?
在我们的程序运行中只允许存在唯一一个对象,不管我们调用了多少个创建对象,我们始终都是得到唯一一个。

三要素:

  1. 构造函数必须私有化
  2. 必须具有一个唯一的static静态变量成为那个单例
  3. 提供公共的访问对象的接口

我们来一步步实现,我们既然要创建唯一一个对象,则不允许对象随意利用构造函数和析构函数。

注意: 所有的类的函数都是static的静态成员函数,这样我们就可以通过类名直接调用

  1. 把构造和析构设置为私有,并且创建唯一的单例成为静态成员变量
class Res
{
public:
private:
static Res* p; //这就是我们的单例对象,是唯一的
Res(){}
~Res() {} //防止在外部使用delete
};
  1. 单例的对象必须由我们亲自初始化: 给他一个构造对象的共有方法:p为空的时候才构造,这就实现了我们如果调用了多次创建对象的函数,但是只能创建一个对象。
static Res* CreatePoint()
{
if (p == nullptr)
{
p = new Res(); //调用构造函数
}
return p;
}
  1. 单例的销毁类成员函数:
static void Destroy()
{
if (p != nullptr)
{
delete p;
p = nullptr; //注意此处重新置空,这样才能下一次创建对象
}
}
  1. 测试:
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枚举可以选取每一张特定的图片,大家仔细体会:

  1. 头文件
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() {}
};
  1. 实现文件,我们使用了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");
}
  1. 简单测试:
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;
}

在这里插入图片描述

posted @   hugeYlh  阅读(553)  评论(0编辑  收藏  举报  
编辑推荐:
· 从 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)
点击右上角即可分享
微信分享提示