【C++】static与C++内存分布

01.目录

02.引言

自上次聊了聊const关键字之后就一直没有找到合适的时间来聊一聊static关键字,因为忘记了,哈哈哈,抱歉。
今天就C++内存模型和static一起来看看这个关键字吧。
我们主要分三个部分(待会展开):

  • 类外的static变量
  • 类内的static成员变量
  • 类内的static成员函数

03.C++内存布局

3.1 内存结构简介

C/C++的内存结构(推荐一篇写得很好的博客),主要分为5个区域:堆、栈、自由存储区、全局/静态存储区、常量存储区。内存区域划分如图:
内存图
下面我链接两个博客地址,你们感兴趣可以看看
C语言中内存栈和堆的区别
vs2015 设置栈大小

3.2 内存分布

编译器和操作系统自动完成内存空间的分配和释放。
优点:执行效率很高,使用方便【栈内存分配运算内置于处理器的指令集中】。
缺点:分配的内存容量有限,分配失败会提示栈错误。
注意,const局部变量也储存在栈区内,栈区向地址减小的方向增长。

程序员向系统申请分配空间,当系统收到程序的申请时,会遍历一个记录空闲内存地址的链表,寻找第一个空间大于所申请空间的堆结点,然后将该结点从空闲结点链表中删除,并将该结点的空间分配给程序。
缺点:分配速度慢,地址不连续,容易碎片化,容易造成内存泄漏。

补充一下内存泄漏:内存泄漏,同样是别人总结好的,感兴趣可以看,我就不单独拿出来了。

  • 自由存储区

1、管理机制和堆类似;
2、区别就是由malloc分配内存,由free来释放。

  • 全局/静态存储区

全局变量和静态变量被分配到同一块内存中,在以前的C语言中,全局变量又分为初始化的和未初始化的,
在C++里面没有这个区分了,他们共同占用同一块内存区。

  • 常量存储区

这是一块比较特殊的存储区,他们里面存放的是常量,不允许修改。

3.3 堆与栈(区别)
  1. 管理方式:栈由编译器自动管理;堆由用户控制,容易产生内存泄漏。
  2. 空间大小:一般来说,对于32位系统下,堆内存空间可以达到4G(2的32次方).从这个角度来看堆的空间是非常大的. 而对于栈空间来说,大小是有限的. 大概2MB.
  3. 碎片问题:频繁的new/delete会造成内存空间不连续,产生大量碎片,使程序效率降低。栈不会产生碎片,因为栈是先进后出的队列。
  4. 生长方向:栈的生长方向是向下,即内存地址减小的方向;堆的生长方向是向上,即内存地址增加的方向。
  5. 分配方式:栈有静态分配和动态分配两种方式:静态分配是编译器自动分配,比如局部变量;动态分配由alloca函数进行分配。栈分配的空间都由编译器释放。
  6. 分配效率:
    栈的效率很高:栈是机器系统提供的数据结构,计算机底层对栈提供支持:分配专门的寄存器存放栈的地址,压栈和出栈都有专门的指令集。

堆的效率很低:堆则是C/C++函数库提供的,它的机制是很复杂的,例如为了分配一块内存,库函数会按照一定的算法(具体的算法可以参考数据结构/操作系统)在堆内存中搜索可用的足够大小的空间,如果没有足够大小的空间(可能是由于内存碎片太多),就有可能调用系统功能去增加程序数据段的内存空间,这样就有机会分到足够大小的内存,然后进行返回。显然,堆的效率比栈要低得多。

Ps:无论堆栈都要防止产生越界,因为越界的结果很严重,要么程序崩溃,要么程序的堆栈结构被破坏,产生意想不到的结果。

04.static关键字

4.1 类外的static变量

类外的static变量我总结了下,有三个作用:
1.隐藏功能
2.保持变量内容的持久
3.默认初始化为0

4.1.1 隐藏功能

在同时编译多个文件时,所有未加static关键字的全局变量和函数都具有全局可见性,其他源文件也可以访问。如果加了static关键字,就会对其他源文件隐藏。利用这一特性,就可以在其他文件中定义同名函数和变量了,不用担心命名冲突。

4.1.2 保持变量内容的持久

存储在静态数据区内的变量会在程序刚开始运行的时候就完成初始化,也是唯一的一次初始化。如果作为static局部变量在函数内定义,它的生存期为整个源程序,但其作用域与自动变量相同,只能在定义该变量的函数内部使用该变量,退出函数后,变量继续存在,但是不能使用它。

4.1.3 默认初始化为0

在静态数据区,内存中所有的字节默认值都是0x00,某些时候这一特点可以减少程序员的工作量。因为static变量存储在静态数据区,所以其默认初始化值为0.

Ps:可能类外static变量的一些特性,类内成员变量也会有,我会再强调一遍。

4.2 类内的static成员变量

在类内的成员变量前面加上static关键字之后,他就是静态成员变量,例子的话,最好的例子就是单例模式中,到时候有本事跟大家聊设计模式了再说,不懂可以去搜一下单例模式,这里我们用简单的例子看看就好。

4.2.1 定义与栗子
  1. 例1
class A
{
public:
    A(int a, int b):m_a(a),m_b(b)  //这里是初始化列表,我记得以前好像有说过
    {
        num += m_a + m_b;
    }
    
    ~A(){ }
    
    void Fun();             // 普通成员函数
    static void PrintNum()  // 静态成员函数
    {
        // 在静态成员函数中,不能访问非静态成员变量,也不能调用非静态成员函数
        std::cout << num << std::endl; 
    }
    
private:
    int m_a;         // 普通成员变量
    int m_b;         // 普通成员变量
    static int num;  // 静态成员变量
};

// 静态成员必须在定义类的文件中对静态成员变量进行初始化,否则会编译出错。
int A::num = 0;

int main()
{
    A a1(1,1);
    A::PrintNum(); // 访问静态函数  result:2
    A a2(1,1);
    A::PrintNum(); // 访问静态函数  result:4
    
    return 0;
}

普通成员变量每个对象有各自的一份,而静态成员变量一共就一份,为所有对象共享。
这里需要注意的是sizeof运算符不会计算静态成员变量的大小,如下栗子:

class CTest
{
    int n;
    static int s;
};

则sizeof(CTest)等于4

  • 普通成员函数必须具体作用于某个对象,而静态成员函数并不具体作用于某个对象。
  • 因此静态成员不需要通过对象就能访问,因为他是共享的。

==========================================================================

  1. 例2
#include <iostream.h>
using namespace std;

class Myclass
{
public:
	Myclass(int a,int b,int c);
	void GetSum();
private:
	int a,b,c;
	static int Sum;//声明静态数据成员
};

int Myclass::Sum=0;    //定义并初始化静态数据成员

Myclass::Myclass(int a,int b,int c)
{
	this->a=a;
	this->b=b;
	this->c=c;
	Sum+=a+b+c;
}

void Myclass::GetSum()
{
	cout<<"Sum="<<Sum<<endl;
}

void main()
{
	Myclass M(1,2,3);
	M.GetSum();
	Myclass N(4,5,6);
	N.GetSum();
	M.GetSum();
}

这里小结一下静态成员变量的特点:

  • 静态成员变量是该类的所有对象所共有的。对于普通成员变量,每个类对象都有自己的一份拷贝。而静态成员变量一共就一份,无论这个类的对象被定义了多少个,静态成员变量只分配一次内存,由该类的所有对象共享访问。所以,静态数据成员的值对每个对象都是一样的,它的值可以更新;

  • 因为静态数据成员在全局数据区分配内存,由本类的所有对象共享,所以,它不属于特定的类对象,不占用对象的内存,而是在所有对象之外开辟内存,在没有产生类对象时其作用域就可见。因此,在没有类的实例存在时,静态成员变量就已经存在,我们就可以操作它;

  • 静态成员变量存储在全局数据区。static 成员变量的内存空间既不是在声明类时分配,也不是在创建对象时分配,而是在初始化时分配。静态成员变量必须初始化,而且只能在类体外进行。否则,编译能通过,链接不能通过。初始化时可以赋初值,也可以不赋值。如果不赋值,那么会被默认初始化,一般是 0。静态数据区的变量都有默认的初始值,而动态数据区(堆区、栈区)的变量默认是垃圾值。

  • static 成员变量和普通 static 变量一样,编译时在静态数据区分配内存,到程序结束时才释放。这就意味着,static 成员变量不随对象的创建而分配内存,也不随对象的销毁而释放内存。而普通成员变量在对象创建时分配内存,在对象销毁时释放内存。

  • 静态数据成员初始化与一般数据成员初始化不同。初始化时可以不加 static,但必须要有数据类型。被 private、protected、public 修饰的 static 成员变量都可以用这种方式初始化。静态数据成员初始化的格式为:<数据类型><类名>::<静态数据成员名>=<值>

  • 静态数据成员和普通数据成员一样遵从public,protected,private访问规则;

  • 如果静态数据成员的访问权限允许的话(即public的成员),可在程序中,按上述格式来引用静态数据成员 ;

  • sizeof 运算符不会计算 静态成员变量。

=============================================================================

4.2.2 静态成员变量的访问
  • 类名::成员名
A::PrintNum();
  • 对象名.成员名
A a;
a.PrintNum();
  • 指针->成员名
A *p = new A();
p->PrintNum();
  • 引用.成员名
A a;
A & ref = a;
ref.PrintNum();
4.2.3 什么情况使用静态成员变量

设置静态成员(变量和函数)这种机制的目的是将某些和类紧密相关的全局变量和函数写到类里面,看上去像一个整体,易于理解和维护。

如果想在同类的多个对象之间实现数据共享,又不要用全局变量,那么就可以使用静态成员变量。也即,静态数据成员主要用在各个对象都有相同的某项属性的时候。比如对于一个存款类,每个实例的利息都是相同的。所以,应该把利息设为存款类的静态数据成员。这有两个好处:

  1. 不管定义多少个存款类对象,利息数据成员都共享分配在全局数据区的内存,节省存储空间。
  2. 一旦利息需要改变时,只要改变一次,则所有存款类对象的利息全改变过来了。

你也许会问,用全局变量不是也可以达到这个效果吗?

同全局变量相比,使用静态数据成员有两个优势:

  • 静态成员变量没有进入程序的全局命名空间,因此不存在与程序中其它全局命名冲突的可能。
  • 可以实现信息隐藏。静态成员变量可以是private成员,而全局变量不能。
4.3 类内的静态成员函数

与静态成员变量类似,我们也可以声明一个静态成员函数。

  • 静态成员函数为类服务而不是为某一个类的具体对象服务。
  • 静态成员函数与静态成员变量一样,都是类的内部实现,属于类定义的一部分。普通成员函数必须具体作用于某个对象,而静态成员函数并不具体作用于某个对象。

普通的成员函数一般都隐含了一个this指针,this指针指向类的对象本身,因为普通成员函数总是具体地属于类的某个具体对象的。当函数被调用时,系统会把当前对象的起始地址赋给 this 指针。通常情况下,this是缺省的。如函数fn()实际上是this->fn()。

  • 与普通函数相比,静态成员函数属于类本身,而不作用于对象,因此它不具有this指针。

正因为它没有指向某一个对象,所以它无法访问属于类对象的非静态成员变量和非静态成员函数,它只能调用其余的静态成员函数和静态成员变量。从另一个角度来看,由于静态成员函数和静态成员变量在类实例化之前就已经存在可以访问,而此时非静态成员还是不存在的,因此静态成员不能访问非静态成员。

4.3.1 举个栗子
#include <iostream>
using namespace std;

class Student{
private:
   char *name;
   int age;
   float score;
   static int num;  	//学生人数
   static float total;  //总分
public:
   Student(char *name, int age, float score);
   void say();
   static float getAverage();  //静态成员函数,用来获得平均成绩
};

int Student::num = 0;
float Student::total = 0;

Student::Student(char *name, int age, float score)
{
   this->name = name;
   this->age = age;
   this->score = score;
   num++;
   total += score;
}

void Student::say()
{
   cout<<name<<"的年龄是 "<<age<<",成绩是 "<<score<<"(当前共"<<num<<"名学生)"<<endl;
}

float Student::getAverage()
{
   return total / num;
}

int main()
{
	/*
	下面这种写法,编译器会做优化
	Student * p = new Student("xxx",xx,xx);
	所以相当于:
	p->say();  应该是可以理解的吧?
	*/
   (new Student("小明", 15, 90))->say();
   (new Student("李磊", 16, 80))->say();
   (new Student("张华", 16, 99))->say();
   (new Student("王康", 14, 60))->say();
   cout<<"平均成绩为 "<<Student::getAverage()<<endl;
   return 0;
}
4.3.2 静态成员函数的特点
  1. 出现在类体外的函数定义不能指定关键字static;
  2. 静态成员之间可以相互访问,即静态成员函数(仅)可以访问静态成员变量、静态成员函数;
  3. 静态成员函数不能访问非静态成员函数和非静态成员变量;
  4. 非静态成员函数可以任意地访问静态成员函数和静态数据成员;(因为非静态成员ok了之后,静态成员是200%ok了的,所以不会说访问不到)
  5. 由于没有this指针的额外开销,静态成员函数与类的全局函数相比速度上会稍快;
  6. 调用静态成员函数,两种方式:

1、通过对象操作符(.)或者(->),也即通过类对象或指向类对象的指针调用静态成员函数。
2、直接通过类来调用静态成员函数。<类名>::<静态成员函数名>(<参数表>)。也即,静态成员函数不需要通过对象就能访问。

4.3.3 拷贝构造函数的问题

在使用包含静态成员的类时,有时候会调用拷贝构造函数生成临时的隐藏的类对象,而这个临时对象在消亡时会调用析构函数有可能会对静态变量做操作(例如total_num–),可是这些对象在生成时却没有执行构造函数中的total_num++的操作。解决方案是为这个类写一个拷贝构造函数,在该拷贝构造函数中完成total_num++的操作,意思就是自己重新实现以下拷贝构造来覆盖类自带的拷贝构造。

这里普及一下类自带的函数:
1.默认构造函数
2.默认拷贝构造函数
3.赋值函数
4.析构函数
这里我就不举例演示了,这个是C++类的特征,可以自己实现以下
ps:如果实现了有参构造函数,则系统不再创建默认构造函数。

05.本文总结

内存那一块儿,可能有些地方说的不是很详细,水平有限,感兴趣可以去看看链接的那几篇文章,我看了下,讲的非常好。

  • 静态成员变量本质上是全局变量,哪怕一个对象都不存在,类的静态成员变量也存在。
  • 静态成员函数本质上是全局函数。
  • 设置静态成员这种机制的目的是将和某些紧密相关的全局变量和函数写在类里面,看上去像是一个整体,易于维护和理解。
  • 在静态成员函数中,不能访问非静态成员变量,也不能调用非静态成员函数。
  • 静态成员必须在定义类的文件中对静态成员变量进行初始化,否则会编译出错。

上面的文章我贴出来,懒得去引用他们里面的东西了,感兴趣可以看下,强调很多遍了。哈哈哈。
如果有任何问题,都可以再下面评论区讨论,能解决就一起解决。
版权声明:转载请注明出处,谢谢支持!

  • 1
    点赞
  • 12
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

Cain Xcy

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

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

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

打赏作者

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

抵扣说明:

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

余额充值