1ms Faster
Home
  • Program

    • Lean in C++
  • Perfromance Engineering

    • [>>>>>]
  • Reading Note

    • [>>>>>]
  • ComputeArch

    • [_]
  • Compiler

    • [_]
  • System

    • [_]
Authoring
  • Categories
  • Tags
GitHub (opens new window)

Quincy Jet

We are.
Home
  • Program

    • Lean in C++
  • Perfromance Engineering

    • [>>>>>]
  • Reading Note

    • [>>>>>]
  • ComputeArch

    • [_]
  • Compiler

    • [_]
  • System

    • [_]
Authoring
  • Categories
  • Tags
GitHub (opens new window)
  • Lean in c++

    • Lean in C++
    • Phase_0:Glimpse

      • C++ Glimpse
      • type cast
      • typedef
      • class vs. typename
      • const
      • extern
      • pointer
      • static
        • C 语言中的 static:
          • static 全局变量
          • static 局部变量
          • static 函数
        • C++类的static成员变量:
          • 成员变量的几种情况:
          • 静态变量
          • 常量
          • 引用变量
          • 静态常量
          • static member var.:
          • 优点
        • C++ 类的Static成员函数
        • 拷贝构造函数的问题
      • volatile
      • inline
      • assert
      • void
      • __global__
  • Performance Engineering

  • Misc

  • Reading Notes

  • Wiki
  • Lean in c++
  • Phase_0:Glimpse
Quincy Jet
2021-08-05
Content

static

static关键字的含义, example以及程序底层的数据划分

这里是cpp reference上面static关键字的链接: https://en.cppreference.com/w/cpp/keyword/static

从汇编层面, 解释static变量是存在于什么地方, 存在于什么样代码段或者数据段上面? (这涉及到汇编和编译的更细节的知识)

# C 语言中的 static:

修饰函数内部变量,即函数内的静态变量。这种变量的生存期长于该函数,使得函数具有一定的“状态”。使用静态变量的函数一般是不可重入的,也不是线程安全的,比如strtok(3)。

在函数体之外,修饰全局变量或函数,表示该变量或函数只在本文件可见,其他文件看不到也访问不到该变量或函数。专业的说法叫“具有internal linkage”(不暴露给别的translation unit)。

# static 全局变量

//Example 1
#include <iostream.h> 

void fn();
static int n=5;    // 静态全局变量 
int j = 3;  // 默认为extern, 全局变量
void main()
{
	n=20;
	cout<<n<<endl;
	fn();
} 

void fn()
{
	n++;
	cout<<n<<endl;
} 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

static 修饰全局变量,限制该变量的作用域为当前文件,就是说这个变量能在当前文件中作为全局变量被访问,但不能再其他文件中被访问

全局变量 j 和静态变量 i 都存放于程序的全局数据区域,它们的生存周期都是程序的整个运行期,但是 j 在函数外定义, 是一个全局变量,可以通过extern在其他文件中使用,而 i 的使用的static修饰,只能在file A中使用,例如在file B中:

extern int j; //OK
extern int i; // error: i在文件B中不可见, 只在 file A 中可见
int a = j; // OK
int b = i; // error
1
2
3
4

也就是说,在声明全局的static变量时,static没有改变它的生存周期,也即存储位置(因为全局变量本来就存储在全局数据域),而是将变量的作用域限制在当前文件中。

静态全局变量有以下特点:

  1. 该变量在全局数据区分配内存;

    img

  2. 未经初始化的静态全局变量会被程序自动初始化为0(自动变量的自动初始化值是随机的);

  3. 静态全局变量在声明它的整个文件都是可见的,而在文件之外是不可见的;

  4. 静态变量都在全局数据区分配内存,包括静态局部/全局变量。对于一个完整的程序,在内存中的分布情况如上图,一般程序的由new产生的动态数据存放在堆区,函数内部的自动变量存放在栈区,静态数据(即使是函数内部的静态局部变量)存放在全局数据区。**自动变量一般会随着函数的退出而释放空间,**而全局数据区的数据并不会因为函数的退出而释放空间。

Example 1中的代码中将

static int n=5; // 定义静态全局变量
1

改为

int n=; // 定义全局变量
1

程序照样正常运行。 定义全局变量就可以实现变量在文件中的共享,但静态全局变量还有以下好处:

  1. 静态全局变量不能被其它文件所用;
  2. 其它文件中可以定义相同名字的变量,不会发生冲突;

将上述示例代码改为如下:

//Example 2
//File A
#include <iostream.h> 

void fn();
static int n; //定义静态全局变量 

void main()
{
	n=20;
	cout<<n<<endl;
	fn();
} 

//File B
#include <iostream.h> 
extern int n;
void fn()
{
	n++;
	cout<<n<<endl;
} 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22

编译并运行Example 2,会发现上述代码可以分别通过编译,但运行时出现错误。 这就是因为静态全局变量不能被其它文件所用,即使在其它文件中使用extern 进行声明也不行。

我们将

static int n; //定义静态全局变量
1

改为

int n; //定义全局变量
1

再次编译运行程序,程序可正常运行。

因此,在一个文件中,静态全局变量和全局变量功能相同;而在两个文件中,要使用同一个变量,则只能使用全局变量而不能使用静态全局变量。

# static 局部变量

//Example 3
#include <iostream.h>
void fn();  // forward declaration
void main()
{
	fn(); //10
	fn(); //11
	fn(); //12
}

void fn()
{
	static n=10; // 函数体内部的变量叫局部变量,加上关键字static,就成为一个静态局部变量。
	cout<<n<<endl;
	n++;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

在函数体内的变量,每当程序运行到该语句时都会给该局部变量分配栈内存。但随着程序退出函数体,系统就会收回栈内存,局部变量也相应失效。

但有时候我们需要在两次调用同一个函数的时候对变量的值进行保存和处理。通常的想法是定义一个全局变量来实现。但这样一来,变量已经不再属于函数本身了,不再仅受函数的控制。

静态局部变量可以解决这个问题。静态局部变量保存在全局数据区,而不是保存在栈中,每次的值保持到下一次调用,直到下次赋新值。

static 局部变量特点:

  1. 静态局部变量在全局数据区分配内存;
  2. 静态局部变量在程序执行到该对象的声明处时被首次初始化,即以后的函数调用不再进行初始化;
  3. 静态局部变量一般在声明处初始化,如果没有显式初始化,会被程序自动初始化为0;
  4. 静态局部变量始终驻留在全局数据区,直到程序运行结束。但其作用域为局部作用域,当定义它的函数或语句块结束时,其作用域随之结束;

# static 函数

在函数的返回类型前加上static关键字, 函数即被定义为静态函数。静态函数与普通函数不同,它只能在声明它的文件当中可见,不能被其它文件使用。

//Example 4
#include <iostream.h>
static void fn();//声明静态函数

void main()
{
    fn();
}

void fn()//定义静态函数
{
    int n=10;
    cout<<n<<endl;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14

static 函数特点:(类似于静态全局变量)

  1. 静态函数不能被其它文件所用;
  2. 其它文件中可以定义相同名字的函数,不会发生冲突;

# C++类的static成员变量:

在类内成员变量的声明前加上关键字static,该数据成员就成为类内的静态数据成员。

//Example 5
#include <iostream.h>
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();
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35

何时采用静态数据成员?

设置静态成员(变量和函数)这种机制的目的是将某些和类紧密相关的全局变量和函数写到类里面,看上去像一个整体,易于理解和维护。**如果想在同类的多个对象之间实现数据共享,又不要用全局变量,那么就可以使用静态成员变量。**也即,静态数据成员主要用在各个对象都有相同的某项属性的时候。比如对于一个存款类,每个实例的利息都是相同的。所以,应该把利息设为存款类的静态数据成员。这有两个好处:

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

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

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

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

用于修饰类的数据成员,即所谓“静态成员”。这种数据成员的生存期大于class的对象(实例/instance)。静态数据成员是每个class有一份,普通数据成员是每个instance 有一份。

class CTypeInit{
    public:
    	CTypeInit(int c) : m_c(c),m_ra(c) { }
    private:
        int m_a;          //通过初始化列表初始化,或者构造函数初始化
        /*引用*/
        int &m_ra;        //只能通过初始化列表初始化
        /*静态变量*/
        static int m_b;            
        /*常量*/
        const int m_c;            

        /*静态整型常量*/
        static const int m_d;    
        /*静态非整型常量*/
        static const double m_e;
};

//静态成员变量,必须在类外初始化,且要去掉static关键字
int CTypeInit::m_b = 6;
const int CTypeInit::m_d = 6;
const double CTypeInit::m_e = 3.1415926;

int main()
{
    CTypeInit obT(2);
    return 0;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28

# 成员变量的几种情况:

# 静态变量

static int m_b;
1

static成员变量需要在类定义体外进行初始化与定义,因为static数据成员独立该类的任意对象存在,它是与类关联的对象,不与类对象关联。例如:上述程序中的c变量的初始化。

  1. 只能在类外初始化
  2. 不能通过初始化列表初始化,
  3. 不能在类内进行初始化,
  4. 不能在构造函数中初始化,

# 常量

const int m_c;
1
  1. 只能通过初始化列表初始化

  2. 不能在类内进行初始化

  3. 不能在构造函数中初始化

  4. 不能在类外初始化

# 引用变量

int &m_ra;
1

只能通过初始化列表初始化且必须用变量初始化,不能在类的定义外(内)初始化,不能通过构造函数初始化。

# 静态常量

static const int m_d;
1
  1. 能否在类中初始化,取决于编译器
  2. 能在在类外初始化,但不能带static

类的静态成员属于类作用域,但不属于类对象,它的生命周期和普通的全局静态变量一样,程序运行时进行分配内存和初始化,程序结束时则被释放。所以不能在类的构造函数中进行初始化。

# static member var.:

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

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

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

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

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

  6. 类的静态成员变量访问形式1:<类对象名>.<静态数据成员名>

  7. 类的静态成员变量访问形式2:<类类型名>::<静态数据成员名>,也即,静态成员不需要通过对象就能访问。

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

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

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

    class CMyclass{
        int n;
        static int s;
    };    //则sizeof(CMyclass)等于4
    
    1
    2
    3
    4

# 优点

  1. static成员的名字是在类的作用域中,因此可以避免与其它类成员或全局对象名字冲突。
  2. 可以实施封装,static成员可以是私有的,而全局对象不可以。
  3. 阅读程序容易看出static成员与某个类相关联,这种可见性可以清晰地反映程序员的意图。

# C++ 类的Static成员函数

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

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

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

//Example 6
#include <iostream>
using namespace std;

class Student{
private:
   char *name;
   int age;
   float score;
   static int num;  	//学生人数
   static float total;  //总分
public:
   Student(char *, int, float);
   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()
{
   (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;
}

运行结果:
小明的年龄是 15,成绩是 90(当前共1名学生)
李磊的年龄是 16,成绩是 80(当前共2名学生)
张华的年龄是 16,成绩是 99(当前共3名学生)
王康的年龄是 14,成绩是 60(当前共4名学生)
平均成绩为 82.25
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55

静态成员函数的特点:

  1. 出现在类外的函数定义不能指定关键字static;
  2. 在类中的static成员函数属于整个类所拥有,这个函数不接收this指针,因而静态成员函数(仅)可以访问static成员变量、static成员函数。
  3. 因为static成员函数没有this指针,所以静态成员函数不可以访问非静态成员。
  4. 静态数据成员与类的大小无关,因为静态成员只是作用在类的范围而已。
  5. 非静态成员函数可以任意地访问静态成员函数和静态数据成员;
  6. 由于没有this指针的额外开销,静态成员函数与类的全局函数相比速度上会稍快;
  7. 调用静态成员函数,两种方式:
  • 通过成员访问操作符(.)和(->),也即通过类对象或指向类对象的指针调用静态成员函数。
  • 直接通过类来调用静态成员函数。<类名>::<静态成员函数名>(<参数表>)。也即,静态成员不需要通过对象就能访问。

成员函数特点:

  1. 函数体内作用范围为该函数体,该变量内存只被分配一次,具有记忆能力(内存分配在静态区,在第一次调用的时候分配内存,函数调用结束内存并不释放)
  2. 在模块内的static全局变量可以被模块内所有函数访问,但不能被模块外其它函数访问;(模块,{}括起来的语句块都是,不同的文件也是不同的模块)
  3. 在模块内的static函数只可被这一模块内的其它函数调用,这个函数的使用范围被限制在声明它的模块内;
  4. 在类中的static成员变量属于整个类所拥有,对类的所有实例只有一份拷贝;

# 拷贝构造函数的问题

在使用包含静态成员的类时,有时候会调用拷贝构造函数生成临时的隐藏的类对象,而这个临时对象在消亡时会调用析构函数有可能会对静态变量做操作(例如total_num--),可是这些对象在生成时却没有执行构造函数中的total_num++的操作。解决方案是为这个类写一个拷贝构造函数,在该拷贝构造函数中完成total_num++的操作。

#include <iostream>
using namespace std;
class test2
{
    public:
        test2(int num) : y(num){}
        ~test2(){}

        static void testStaticFun()
        {
            cout << "y = " << y << endl;     // Error:静态成员函数不能访问非静态成员, static成员函数没有this指针
        }

        void testFun()
        {
            cout << "x = " << x << endl; 
        }
    private:
        static int x;   // 静态成员变量的引用性说明
        int y;
};

int test2::x = 10;      // 静态成员变量的定义性说明
int main(void)
{
    test2 t(100);
    t.testFun();

    return 0;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30

静态整型常量和静态非整形常量在类定义内部初始化时,在VC6.0中都不能编译通过,而在GCC中都可以编译通过,在不同编译器下有不同的结果,但前三个是确定的。当然,如果不习惯类内初始化,可以将静态常量和静态变量的初始化统一起来,将静态常量和静态变量的初始化全部都移动类定义之外初始化(推荐使用这种方式)。另外,如果编译器不支持类内初始化,而此时类在编译期又恰恰需要定义的成员常量的值,身出如此左右为难的境地,我们应该考虑使用enum!因为enum 本质也是一个整型常量。

#C++
Last updated: 2023/06/18, 16:23:56
pointer
volatile

← pointer volatile→

Copyright © 2017-2023 Quincy Jet | MIT License
  • Auto
  • Light
  • Dark
  • Read