跳转至

C++关键字static总结

C++的static关键字有两种用法,分别是面向过程和面向对象。面向过程有静态全局变量,静态局部变量,静态函数;面向对象有静态成员变量,静态成员函数。

面向过程

静态全局变量

在全局变量前,加上关键字static,该变量就被定义成为一个静态全局变量。例如:

#include<iostream>

using namespace std;

static int globalValue1;

int main() {    
    cout << globalValue1 << endl;
}

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

  1. 该变量在全局数据区分配内存,其实静态变量都在全局数据区分配内存,包括静态局部变量
  2. 未经初始化的静态全局变量会被程序自动初始化为0(自动变量的自动初始化值是随机的)
  3. 静态全局变量在声明它的整个文件都是可见的,而在文件之外是不可见的,即静态全局变量不能被其它文件所用。因此,其它文件中可以定义相同名字的变量,不会发生冲突,这是静态全局变量的优点。

例如

//main.cpp
#include<iostream>

using namespace std;

static int globalValue1;

int main() {    
    cout << globalValue1 << endl;
}

//source.cpp
#include<iostream>

using namespace std;

int globalValue1 = 1;

void f1() {
    globalValue1 += 1;
    cout << globalValue1 << endl;

}

如果两个文件里的globalValue1都没有static修饰,这回引发重定义的错误;只要有一个gobalValue1有修饰都可以正常运行。

如果把source.cpp的globalValue1改成extern int globalValue1,也会报错。

静态局部变量

在局部变量前,加上关键字static,该变量就被定义成为一个静态局部变量。它有以下特点

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

应用场景

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

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

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

例如

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

void fn()
{
    static n=10;
    cout<<n<<endl;
    n++;
}

静态函数

在函数的返回类型前加上static关键字,函数即被定义为静态函数。静态函数与普通函数不同,它只能在声明它的文件当中可见,不能被其它文件使用。定义静态函数的好处:(类似于静态全局变量)

  1. 静态函数不能被其它文件所用;
  2. 其它文件中可以定义相同名字的函数,不会发生冲突;
  3. 可以把静态函数的定义放到.h文件,多个.cpp文件include时,不会引起重定义的错误。

例如

// main.cpp
#include<iostream>
// #include"source.h",即使include了void f1();的声明,也不会报错

using namespace std;

static void f1() {
    cout << "main.f1()" << endl;
}

int main() {    
    f1();
}

// source.cpp
#include<iostream>
#include "source.h"


void f1() {
    std::cout << "source.f1" << std::endl;
}

面向对象

静态数据成员

在类内数据成员的声明前加上关键字static,该数据成员就是类内的静态数据成员。例如

// main.cpp
#include<iostream>
#include"source.h"

using namespace std;

int main() {
    MyClass a(1, 2, 3);
    a.getSum();
    MyClass b(2, 3, 4);
    b.getSum();
    a.getSum();
    cout << MyClass::Num << endl;
}

// source.h
#pragma once

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

// source.cpp
#include<iostream>
#include "source.h"

using namespace std;

int MyClass::Num = 3;
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 << endl;
}

它有以下特点

  1. 静态成员变量是该类的所有对象所共有的。对于普通成员变量,每个类对象都有自己的一份拷贝。而静态成员变量一共就一份,无论这个类的对象被定义了多少个,静态成员变量只分配一次内存,由该类的所有对象共享访问。所以,静态数据成员的值对每个对象都是一样的,它的值可以更新;
  2. 因为静态数据成员在全局数据区分配内存,由本类的所有对象共享,所以,它不属于特定的类对象,不占用对象的内存,而是在所有对象之外开辟内存,在没有产生类对象时其作用域就可见。因此,在没有类的实例存在时,静态成员变量就已经存在,我们就可以操作它;
  3. 静态成员变量存储在全局数据区。static 成员变量的内存空间既不是在声明类时分配,也不是在创建对象时分配,而是在初始化时分配。静态成员变量必须初始化,而且只能在类体外进行。否则,编译能通过,链接不能通过。在source.cpp中,语句int Myclass::Sum=0;是定义并初始化静态成员变量。初始化时可以赋初值,也可以不赋值。如果不赋值,那么会被默认初始化,一般是 0。静态数据区的变量都有默认的初始值,而动态数据区(堆区、栈区)的变量默认是垃圾值。并且,一般不再.h文件里定义并初始化静态成员变量,否则如果多个源文件include该头文件是,会导致重定义错误。
  4. static 成员变量和普通 static 变量一样,编译时在静态数据区分配内存,到程序结束时才释放。这就意味着,static 成员变量不随对象的创建而分配内存,也不随对象的销毁而释放内存。而普通成员变量在对象创建时分配内存,在对象销毁时释放内存。
  5. 静态数据成员初始化与一般数据成员初始化不同。初始化时可以不加 static,但必须要有数据类型。被 private、protected、public 修饰的 static 成员变量都可以用这种方式初始化。静态数据成员初始化的格式为:<数据类型><类名>::<静态数据成员名>=<值>
  6. 类的静态成员变量访问形式1:<类对象名>.<静态数据成员名>
  7. 类的静态成员变量访问形式2:<类类型名>::<静态数据成员名>,也即,静态成员不需要通过对象就能访问。
  8. 静态数据成员和普通数据成员一样遵从public,protected,private访问规则;
  9. 如果静态数据成员的访问权限允许的话(即public的成员),可在程序中,按上述格式来引用静态数据成员 ;
  10. sizeof 运算符不会计算 静态成员变量。

静态成员函数

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

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

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

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

静态成员函数的特点:

  1. 出现在类体外的函数定义不能指定关键字static;
  2. 静态成员之间可以相互访问,即静态成员函数(仅)可以访问静态成员变量、静态成员函数;
  3. 静态成员函数不能访问非静态成员函数和非静态成员变量;
  4. 非静态成员函数可以任意地访问静态成员函数和静态数据成员;
  5. 由于没有this指针的额外开销,静态成员函数与类的全局函数相比速度上会稍快;
  6. 调用静态成员函数,和普通成员函数一样遵守private、protected、public访问原则,两种方式:

  7. 通过成员访问操作符(.)和(->),也即通过类对象或指向类对象的指针调用静态成员函数。

  8. 直接通过类来调用静态成员函数。<类名>::<静态成员函数名>(<参数表>)。也即,静态成员不需要通过对象就能访问。

拷贝构造函数的问题

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

REF

https://zhuanlan.zhihu.com/p/37439983