类中static成员的使用
static成员变量
定义
被static关键字修饰的成员变量。即使定义在类内,静态成员变量存储在全局区,相当于全局变量,整个程序的运行过程中只有一份内存。
class Car
{
public/private/protected:
int m_age;
static int m_price;// 静态成员变量相当于全局变量,跟类创建的对象没有关系,只跟类本身有关系
};
static成员变量的访问方式
首先我们回顾一下普通成员变量(非静态成员变量)的访问方式。
- 对象访问
Car car;
car.m_age = 10;
- 指向对象的指针访问
Car *p = new Car();
p->m_age = 10;
- 用类直接访问时
提示:非静态成员引用必须与特定的对象相对
我们再来看看静态成员变量的访问方式
- 对象访问
Car car;
car.m_price = 300;
- 指向对象的指针访问
Car *p = new Car();
p->m_price = 300;
- 用类直接访问时
Car::m_price = 300;
我们可以看到,静态成员变量的访问方式,比非静态成员变量多了一个类访问。
静态成员变量必须初始化
我们在类中定义了静态成员变量。必须要在类外对该静态成员变量初始化,否则会报错。
(错误 LNK2001 无法解析的外部符号 “public: static int Car::m_price” )
初始化的格式,为:数据类型 类名::静态成员变量 = 初始化值。其中类名::别忘记书写。
int Car::m_price = 0;
静态成员变量的意义
我们已经知道静态成员变量相当于全局变量,在程序运行过程中只有一块内存存储。但是,相对于全局变量,静态成员变量又有什么不同呢?
意义:它可以设定访问权限,达到局部共享的目的。 这又是什么意思?
假设我们有这样一个应用场景:想搞一个整个程序运行阶段都存在的内存,这个变量要一直存在,但是只允许某个范围内访问。
- 设置成private,只能让类内访问,外部就不能访问了
- 设置成protected,只可以让类内和子类访问,外部就不能访问了
- 设置成public,所有地方都可以访问,那就跟全局变量没有区别了
貌似一个限制作用域的全局变量。使得该全局变量更加的灵活,且不会被任意更改。
静态成员变量需要注意的地方
- 必须初始化,且必须在类外面初始化,初始化时不能带有static关键字
- 如果,类的声明.h和实现.cpp写在不同的文件中,即分离。那初始化必须在实现中,即.cpp中
- 静态成员变量跟对象没有关系
Car *p = new Car();
p->m_price = 300; //static成员
p->m_age = 10; //非static成员
delete p; // 将堆空间上的对象内存区域销毁,普通成员变量会销毁,但是静态成员变量不会销毁
为什么说静态成员变量跟对象没关系呢?我创建一个堆空间,用来存储对象的内存。此时,依然可以通过对象指针访问static成员m_price。但这并不代表m_price在这块堆空间,因为m_price是存储在数据段(全局区)的。所以当我delete p时,m_price的值依然不受影响。这就是静态成员跟对象没有关系的一个最大例证。
具体来说,static成员与对象没有关系,我们在static成员函数中还会论证。
static成员函数
定义
被static关键字修饰的成员函数。同静态成员变量一样,即使定义在类内,静态成员函数依然存储在全局区(数据段),相当于全局函数,整个程序的运行过程中只有一份内存。
class Car
{
public/private/protected:
static void run() // 静态成员函数,跟类创建的对象没有关系,只跟类本身有关系
{
}
void test() // 非静态成员函数
{
}
};
static成员函数的访问方式
首先我们回顾一下普通成员函数(非静态成员函数)的访问方式。
- 对象访问
Car car;
car.test();
- 指向对象的指针访问
Car *p = new Car();
p->test();
- 用类直接访问时
提示:非静态成员引用必须与特定的对象相对
我们可以看到,这与访问非静态成员变量的方式相同。都是无法通过类名直接访问。
我们再来看看静态成员变量的访问方式
- 对象访问
Car car;
car.run();
- 指向对象的指针访问
Car *p = new Car();
p->run();
- 用类直接访问时
Car::run();
静态成员函数体内部没有this指针
this指针,默认存于类中的成员函数内部(以前的博文有介绍)。这里我们在回顾一下:假设我们在类中创建了一个成员函数,函数内部对成员变量做了一定的操作。
class Car
{
int m_age;
public:
void test()
{
m_age = 10;
}
};
我在外部创建对象,然后调用这个函数
int main()
{
Car car;
car.test();
return 0;
}
但实际上,成员函数test内部执行了什么操作呢?执行这样的操作:将调用该成员函数的对象地址值传递给this指针,然后成员变量就通过this指针调用。
void test()
{
this = &car;
this->m_age = 10;
}
但是,我们是看不见这些操作的,因为编译器帮助我们省略了这些操作。到此,我们了解到了this指针的作用,就是用于接受调用者对象的地址值。
现在我们来回答标题:静态成员函数内部没有this指针。虽然静态成员函数可以通过对象调用(car.run()),但是却跟对象一点关系没有。也就意味着不会将对象的地址值传递给this指针。这是独立于对象,但是存于类中的函数。
静态成员函数内部不可以访问非静态成员
内部也不可以直接访问类中的非静态成员变量。(这块其实跟Python中的静态方法一样), 因为非静态的成员变量是随着对象存在而存在的。如果不创建对象,内存中是不会有成员变量的。由于静态成员函数没有this指针什么事,也不会默认this指针指向成员变量,所以直接访问成员变量是不可以的。
static void run() // 静态成员函数,跟类创建的对象没有关系,只跟类本身有关系
{
m_age = 0; //静态函数的内部,不可以直接访问类中的非静态成员变量访问
//Car::m_age = 0;//这样也是不行的 (会报错:非静态成员的引用必须与特定的对象相对)
}
如果非要访问非静态成员,必须通过对象访问:
static void run()
{
Car car;
car.m_age = 0; // 这样就可以了
}
想想为什么不可以访问非静态成员呢?
有人会问了:用对象调用静态成员函数(car.run()),不就有对象了吗,为什么内部还是不能访问普通的成员变量呢?
因为,即使是对象调用,也不会把对象值传递给this指针,所以里面的普通非静态成员变量根本不知道是谁在调用,因此就出错。
这里tips一个不成文的规定:以后开发中,调用静态成员函数,统统采用类::函数的调用方式。这样也能一定程度上表明该函数是静态成员函数。
我们知道静态成员函数内部没有this指针,那我们总结一下this指针只能存在于什么地方。
this指针只能在非静态成员函数的内部(一个是非静态,一个是成员函数,缺一不可)。不是成员函数不行,不是非静态不行。总之:this指针只跟对象有关系
静态成员函数不可以是虚函数
这个也很好理解,因为虚函数只用于多态,而多态要求父类指针指向子类对象,牵扯到对象,就不能是静态成员函数。
静态内部不可以访问非静态,而非静态内部可以访问静态
静态成员函数内部不可以访问非静态成员变量、函数,只能访问静态成员变量、函数。反过来,非静态成员函数内部,是可以访问静态成员变量和函数的。
这个也好理解,因为静态成员函数就相当于全局函数只要给定作用域,都可以访问。
构造函数和析构函数不可以是静态的
这个也很好理解,因为构造函数和析构函数都只跟对象有关:
- 当一个对象创建时,默认调用构造函数
- 当一个对象销毁时,默认调用析构函数
当静态成员函数的声明和实现是分离的时候,声明处要用static关键字修饰,实现不可以用static关键字修饰
因为我们在后续开发中,基本都是声明和实现分离。即类中成员的声明在.h文件中,类中成员的实现在.cpp文件中。我们在类中定义静态成员时,要用static修饰。在cpp文件中实现时,不可以加static关键字修饰。
static成员和普通成员的背后汇编原理
首先我们得类,是这样定义的:
class Car
{
public:
int m_age; //非静态
static int m_price1; //静态
static void run() //静态
{
cout << "Car::run()" << endl;
}
void test() //非静态
{
}
};
我们在外部调用这些成员:(这里为了演示方便,都选择对象调用)
int main()
{
Car car;
car.run();
car.test();
car.m_age = 1;
car.m_price1 = 10;
getchar();
return 0;
}
汇编代码如下:
car.m_age = 1;
mov dword ptr [rbp+4],1 //rbp很明显是栈空间中存储对象的地址寄存器
car.m_price1 = 10;
mov dword ptr [00007FF6C8DDF324h],0Ah // 00007FF6C8DDF324h就是全局区中的地址了
可以看到,调用非静态成员变量m_age得时候,将数值1赋值给[rbp+4],我们知道rbp明显是存储在栈空间的地址,也就是car对象所在的内存区域。
当我们调用静态成员变量m_price1得时候,直接将0Ah(10)赋值给数据段中的地址,也就是全局区的地址。
但这里我们也可以看出来,普通的非静态成员变量存储在对象内存里,而静态成员变量则存储在全局区。
car.run();
call 00007FF6F8411398
car.test();
lea rcx,[rbp+4]
call 00007FF6F84114D8
调用成员函数也是如此。当调用静态成员函数,直接call全局区的地址。调用非静态成员函数,则是通过对象调用。
继承关系下的静态成员函数
声明一个benz类,继承Car类。我们知道,继承就是把父类所有的东西都拿到子类里
class benz : public Car
{
};
这里我们打印不同作用域下,静态成员变量的地址值:
cout << &Car::m_price1 << endl; //00007FF7AD2BF324
cout << &benz::m_price1 << endl; //00007FF7AD2BF324
结果是一样的。因为只要是静态成员变量,全世界就只要一块内存存储。不像普通的成员变量,创建一个对象,就有一块内存存储。
但我我非要在子类中,写一个一模一样的静态成员变量,名字都一样,这时打印结果还会一致吗?
class benz : public Car
{
public:
static int m_price1;
};
结果:打印的地址不一样,因为编译器会认为这是两个不同的全局变量,编译器内部直接改名。
static应用:单例模式
定义
设计模式的一种,目的是:保证某个类永远只创建一个对象。注意我这里写的是一个对象,而不是一次对象。这也就意味着我可以创建多个对象,但是多个对象其实是相同的,即一个对象。
应用场景
比如电脑管家中的那个加速火箭,无论我执行电脑管家里的什么功能(杀毒、清理等等)。 那个加速火箭都在。这就要求设计者把这个火箭对象在内存中只有一块内存,即只创建一次对象。
static成员实现单例模式
我们首先创建一个火箭Rocket类
class Rocket
{
};
为了避免随意创建对象,我们只需要把构造函数设为私有化
class Rocket
{
private:
Rocket() {};
public:
};
因为创建对象就要调用构造函数,一旦放在私有的里面就无法在类外创建对象了。有人会有疑问了,都无法创建对象了,我们怎么保证只创建一个对象。不要着急,我这一步的作用只是保证无法在类外随意的创建对象。
我的想法不是不能创建对象,而是只创建一个对象,整个程序运行过程中只有一个对象。那怎么搞?
我们可以这么做:我们定义一个静态的成员函数,用来获取创建的对象(构造函数私有化,即无法在类外创建对象,但是可以在类内创建对象),即我们定义一个公共的接口,供外部访问(类似于get方法)
函数的内部是这样的:判断指针ms_rocket是否为空,如果是空的,就创建对象,让这个指针指向对象内存空间,然后返回这个对象指针。
class Rocket
{
private:
Rocket() {};
static Rocket *ms_rocket;
public:
static Rocket *sharedRocket()
{
if (ms_rocket == NULL)
{
ms_rocket = new Rocket();
}
return ms_rocket;
}
void run()
{
cout << "Rocket::run()" << endl;
}
};
紧接着,我们就可以在外部通过调用这个接口sharedRocket(),来返回在类内创建的对象。
Rocket* Rocket::ms_rocket = NULL;
int main()
{
Rocket *p = Rocket::sharedRocket();
p->run();
getchar();
return 0;
}
通过程序我们也可以看出来,返回的指针p就是ms_rocket,也就是指向对象内存的指针,自然可以通过这个指针访问类内的public成员。
怎么证明我们确实做到了单例模式呢?我再多创建几个对象看看
int main()
{
Rocket *p = Rocket::sharedRocket();
p->run();
Rocket *p2 = Rocket::sharedRocket();
Rocket *p3 = Rocket::sharedRocket();
Rocket *p4 = Rocket::sharedRocket();
cout << p << endl; //000001BAB3A56270
cout << p2 << endl; //000001BAB3A56270
cout << p3 << endl; //000001BAB3A56270
cout << p4 << endl; //000001BAB3A56270
getchar();
return 0;
}
结果是这几个对象,都是相同的。我们确实做到了,整个程序运行期间,只存在一个对象。
但有时候,我们也想要销毁那个单例对象,就像那个火箭,我想要关闭他。这时候就要提供公共的接口让用户删除这个对象并销毁这个对象的内存。所以完整的单例模式实现代码如下:
class Rocket
{
private:
Rocket() {};
static Rocket *ms_rocket;
public:
static Rocket *sharedRocket()
{
// 需要考虑多线程安全问题,大家同时进入这里,意味着拿到的ms_rocket都是NULL,那就会创建多个对象,这是我们不想要的
if (ms_rocket == NULL)
{
ms_rocket = new Rocket();
}
return ms_rocket;
}
void run()
{
cout << "Rocket::run()" << endl;
}
static void deleteRocket()
{
if (ms_rocket != NULL)
{
//delete指针的意思是,清空指针指向的内存区域,但是指针本身还在,他还是指向那块内存区域
delete ms_rocket;
ms_rocket = NULL; //!!!!!!!!!!!!!!!!!!!!!这句很重要
}
}
};
main函数,首先创建对象,然后销毁对象,然后再创建对象。
int main()
{
Rocket *p = Rocket::sharedRocket();
p->run();
Rocket::deleteRocket(); //销毁火箭对象
Rocket *p5 = Rocket::sharedRocket(); //再次创建火箭对象
getchar();
return 0;
}
调用函数deleteRocket(),执行delete ms_rocket; 然后清空指针指向的内存区域,但是指针本身还在,依然指向那块内存。(也就是指针本身是有值的,只是指针指向的内存区域没有东西了)
Rocket::deleteRocket();
当我下面再创建对象时
Rocket *p5 = Rocket::sharedRocket();
由于指针不为NULL,所以会直接return ms_rocket作为对象指针(就是刚才清空内存区域的对象指针),但是这个对象指向的是空的堆空间,是我们已经回收掉的堆空间。对象指针指向已经回收掉的空间,这就叫野指针。虽说不一定报错,但是开发是存在风险的,所以我们在销毁对象时,要把指针清空:
if (ms_rocket != NULL)
{
//delete指针的意思是,清空指针指向的内存区域,但是指针本身还在,他还是指向那块内存区域
delete ms_rocket;
ms_rocket = NULL; //!!!!!!!!!这句很重要!!!!!!!!!!!!
}
单例模式总结
想要实现单例模式,需要遵循几个步骤:
- 构造函数私有化
- 定义一个私有的成员变量指向唯一的那个单例对象
- 提供一个公共的访问单例对象的接口