C 与 C++ 的 static 关键字全解

编译器的行为以 gcc/g++ 9.4.0 为准。

目录

1 前置知识

2 C 的 static

2.1 静态存储期的存储类说明符

2.1.1 C 静态全局变量

2.1.2 C 静态局部变量

2.1.3 C 静态函数

2.2 函数参数声明中的静态数组索引

3 C++ 的 static

3.1 静态存储期的存储类说明符

3.2 类的静态成员

3.2.1 静态成员函数

3.2.2 静态数据成员

3.2.3 常量静态成员

4 References


1 前置知识

参考本系列的上一篇文章:

C 与 C++ 的 extern 关键字全解_yihuajack的博客-CSDN博客编译器的行为以 gcc/g++ 9.4.0 为准。1 前置知识1.1 声明、定义与初始化声明(声明式)= declarationextern int x;std::size_t numDigits(int number);class Widget;template<typename T>class GraphNode;定义(定义式)= definitionint x;std::size_t numDigits(int number) {}clashttps://blog.csdn.net/yihuajack/article/details/122305628?spm=1001.2014.3001.5501

2 C 的 static

2.1 静态存储期的存储类说明符

C 的 static 作为静态存储期的存储类说明符,在文件作用域内具有内部链接(即只能在所在文件内被使用,而不能在其它文件中使用),在块作用域内为无链接(即块作用域内的对象不管是否使用 static 声明都不能在块作用域外被使用)。

C 的静态变量分配在数据段上而非栈上,并会默认初始化为 0,但是不能被动态初始化,即如

int initializer() {
    return 5;
}
  
int main() {
    static int i = initializer();
    return 0;
}

是非法的,会错误 initializer element is not constant(称为 static initialization order fiasco)。C 的静态变量不能声明在结构体内部,因为整个结构体拥有一块连续的内存。

一个定义规则(One Definition Rule)的第 1 条指出:
每个翻译单元可以拥有每个具有内部链接( static 全局名称)的零或一个外部定义

extern 不更改标识符的链接,试探性定义可以与同一标识符另一声明的链接不一致,但同一标识符的二个声明均在作用域内不能拥有不同链接。例如,定义或试探性定义静态 i4 后

static int i4 = 2;

static int i4;

不能再试探性定义 i4:

int i4;

否则会错误 non-static declaration of 'x' follows static declaration,但是可以再用 extern 或 static 声明 i4,这时 i4 仍然具有内部链接。此外,拥有内部链接的试探性定义也不能为不完整类型,例如

static int i[];  // error!

是错误的。 

2.1.1 C 静态全局变量

作为访问修饰符(Access Modifier,或称“访问说明符” Access Specifier,类似于 C++ 的 private、protected 和 public),因为静态全局变量仅对所在文件可见,即所谓在文件作用域具有内部链接(区分于具有显式或隐式 extern 声明的外部链接会使得变量对其它文件可见)。

2.1.2 C 静态局部变量

C 静态局部变量具有静态存储期,不同于 auto 自动存储期的变量的内存会在函数结束后释放,静态存储期变量的内存到程序结束后才会释放。MWE:

int foo() {
    static int count = 0;
    count++;
    return count;
}
  
int main() {
    foo();
    foo();
    return 0;
}

这与将 count 的定义与初始化放在全局或将 count 的定义放在全局而在函数内使用前初始化效果相同,即多次调用 foo() 后尽管 count 看似每次进入函数都被初始化为 0,实际上它只在 main 函数之前初始化一次存储于对象的值,即只在执行过程第一次到达声明处被初始化一次

2.1.3 C 静态函数

这种用法较少见。和 C 静态全局变量一样,C 静态函数仅对当前文件内可见,即在文件作用域具有内部链接(区分于不加 static 关键字而默认为 extern 的函数对其它文件可见)。MWE:在源文件 1 中定义

static void foo(void) {
    puts("foo called");
}

在源文件 2 中不能调用 foo() 函数。

2.2 函数参数声明中的静态数组索引

允许编译器进行可能的编译时(对数组的)边界检查;允许编译器作预读取优化。MWE:

void fadd(double a[static 10], const double b[static 10]) { 
    for (int i = 0; i < 10; i++) {
        if (a[i] < 0.0)
            return;
        a[i] += b[i];
    }
}

调用 fadd 函数时传给它的两个参数只能是长度大于等于 10 的 double 数组,如果长度小于 10 会导致未定义行为。也可以和 restrict 限定符联合使用:

void fadd(double a[static restrict 10], const double b[static restrict 10]) {
    for (int i = 0; i < 10; i++) {
        if (a[i] < 0.0)
            break;
        a[i] += b[i];
    }
}

restrict 限定符的作用如何理解C语言关键字restrict? - 知乎要理解 restrict,先要知道什么是 Pointer aliasing。Pointer aliasing 是指两个或以上的指针指向同一数…https://www.zhihu.com/question/41653775/answer/92088248 已经讲的很清楚了,不再赘述。

3 C++ 的 static

Contribution: https://zh.cppreference.com/mwiki/index.php?title=cpp/keyword/static&oldid=73393.

3.1 静态存储期的存储类说明符

与 2.1 C 的 static 一致,可用于声明具有静态存储期和内部链接的命名空间成员(命名空间作用域即意味着函数或类外部,类比于 C 中的静态全局变量),也可用于定义具有静态存储期且仅初始化一次的块作用域变量(即静态局部变量)。不同于 C,static 变量的值也可通过 constexpr 常量初始化。

由于静态类实例(对象)具有静态存储期,其析构函数在 main() 函数结束时才被调用。

在 lambda 表达式内使用

int main(){
    static int i{12};
    []{std::cout << i;}();
}

如果 i 是非静态局部变量,则编译不通过;如果 i 是全局变量,则程序正常。

C++ 同样存在 static initialization order fiasco 的问题;作常量初始化(constexpr 指定的值或默认 0);静态局部变量在执行过程第一次到达声明处被初始化一次。因此,安全的做法是:所有非 constexpr 初始化的静态变量应为函数内的局部变量

C++ 的静态函数有以下 4 种用法:
(1) 保证函数不会被其它任何文件使用
(2) 放在头文件中使得每个包含该头文件的源文件获得该函数的一份拷贝(不实用,因为可以用 inline 代替)
(3) 减少链接器的工作量以减少链接时间
(4) 在不同翻译单元使用相同名字的函数做不同的事情(tricky)

3.2 类的静态成员

static 只用于静态成员在类定义中的声明(可以声明为不完整类型,除非使用 constexpr 或 inline 声明,见下文;对于类中的静态类成员,由于没有定义,自然也不会调用构造函数与析构函数),而不用于该静态成员的定义。

静态成员遵循类成员访问规则(私有、受保护、公开)。

类的静态成员被类的所有实例所共享,所以其构造函数和析构函数只会被调用一次。

3.2.1 静态成员函数

struct X {
    static void f();
};

(1) 静态成员函数不关联到任何对象,没有 this 指针。
(2) 静态成员函数不能是 virtual、const 或 volatile 的。
(3) 静态成员函数的地址可以存储在常规的函数指针中,但不能存储在成员函数指针中。

静态成员函数只能访问静态数据成员或其它静态成员函数。

3.2.2 静态数据成员

(1) 静态数据成员不关联到任何对象,整个程序中只有一个拥有静态存储期的静态数据成员实例(除非 thread_local)。
(2) 静态数据成员不能是 mutable 的。
(3) 在命名空间作用域中,如果类自身具有外部链接(即不是无名命名空间的成员),那么类的静态数据成员也具有外部链接。

局部类(定义于函数内部的类)、无名类、无名类的成员类不能拥有静态数据成员。

class X { static int n; };
struct S {
   static int a[];
   static Foo x;
   static S s;
};

对于 a[],去掉 static 是不合法的,错误 flexible array member 'S::a' in an otherwise empty 'struct S'

class X {
public:
	int n;
	X() {};
};

int main() {
	X x;
	X y;
	x.n = 1;
	y.n = 2;
	std::cout << x.n << y.n << std::endl;
}

如果在类 X 中使用了

static int n;

声明, 那么这段代码就会错误,因为编译器找不到 n 的定义,需要额外在命名空间作用域(即类外)写出 n 的定义:

int X::n = 1;

(结果为 x.n = 2,y.n = 2)。但是如果使用(C++ 17 及以后)

inline static int n;

这段代码又可以编译了(x.n = 2,y.n = 2),因为 inline 静态数据成员不需要类外定义

同样,在类内不能直接初始化静态成员变量:

static int n = 5;  // error!

错误 ISO C++ forbids in-class initialization of non-const static member 'X::n',但是可以使用 inline 直接初始化静态成员变量:

inline static n = 5;

(结果为 x.n = 2,y.n = 2)

此外,const 限定符也可以直接初始化静态成员变量(见 3.2.3)。

对于 (1),不关联意味着一个类的静态类成员可以在不存在类实例的情况下直接使用:

class A {
    int x;
public:
    A() { std::cout << "A's constructor called" << std::endl; }
};
 
class B {
    static A a;
public:
    B() { }
    static A getA() { return a; }
};
 
A B::a;
 
int main() {
    A a = B::getA();
}

在上述例子中,根本没有类 B 的实例化对象,不存在 B 的构造函数和析构函数调用,但是 A 的构造函数会被调用。

3.2.3 常量静态成员

struct X {
    const static int n = 1;
    const static int m{2};
    const static int k;
    constexpr static int arr[] = { 1, 2, 3 };
    constexpr static std::complex<double> n = {1,2};
};
const int X::k = 3;

不能使用 constexpr static 而不初始化。

4 References

[1] Wikipedia Contributors. Static (keyword) — Wikipedia, The Free Encyclopedia. [Online; accessed 5-January-2022]. Sept. 4, 2021. URL: https://en.wikipedia.org/wiki/Static_(keyword).
[2] GeeksforGeeks. Static Variables in C. GeeksforGeeks, July 19, 2021. URL: https://www.geeksforgeeks.org/static-variables-in-c/.
[3] GeeksforGeeks. Static Functions in C. GeeksforGeeks, Aug. 24, 2020. URL: https://www.geeksforgeeks.org/what-are-static-functions-in-c/.
[4] Cppreference Contributors. C keywords: static — Cppreference. [Online; accessed 5-January-2022]. Jan. 14, 2015. URL: https://en.cppreference.com/w/c/keyword/static.
[5] Cppreference Contributors. Storage-class specifiers — Cppreference. [Online; accessed 5-January-2022]. Sept. 30, 2021. URL: https://en.cppreference.com/w/c/language/storage_duration.
[6] Cppreference Contributors. C++ keywords: static — Cppreference. [Online; accessed 5-January-2022]. Apr. 10, 2020. URL: https://en.cppreference.com/w/cpp/keyword/static.
[7] Cppreference Contributors. static members — Cppreference. [Online; accessed 6-January-2022]. Dec. 9, 2021. URL: https://en.cppreference.com/w/cpp/language/static.
[8] Harsh Agarwal. Static Keyword in C++. GeeksforGeeks, June 5, 2018. URL:https://www.geeksforgeeks.org/static-keyword-cpp/.
[9] GeeksforGeeks. Static data members in C++. GeeksforGeeks, Dec. 5, 2021. URL: https://www.geeksforgeeks.org/static-data-members-c/.
[10] Tony the Lion et al. The static keyword and its various uses in C++. Apr. 7, 2021. URL: https://stackoverflow.com/questions/15235526/the-static-keyword-and-its-various-uses-in-c.

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

Ayka

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

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

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

打赏作者

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

抵扣说明:

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

余额充值