工作三年,我才搞懂了这俩关键字

文摘   2024-12-11 12:03   新加坡  

你好,我是雨乐~

一直以来,对c/cpp中的两个关键字static 和 extern绕着走,尤其是在看某些第三方源码的时候,一看到这些关键字就头大。恰好跟斜对面的家伙扯淡的时候,他来了句:我工作三年才勉强弄懂这俩的用法。恰好,群里(群里氛围还是很不错的,可以找我加群哈)今天恰恰聊到了这个技术点,嗯,所以就想把这块整理下,以便系统的了解这俩的用法。

先看下面一个例子:

int x;
static int y;
extern int z;

对于static关键字,我一开始的认知是其生命周期随着所属进程的结束而结束,也就是说只要进程在运行,static变量就处于生效状态。直至我遇到了下面这种:

struct Obj{
staticint x;
};

static int fun() {
// do sth
return0;
}

int func() {
staticint x;
return0;
}

看到上面这种基本开始头大了~

如果说看到static开始头大的话,那么遇到extern就开始头疼了,后面的代码就看不下去了~

😭~

内外链接

为了能够更好的理解这俩概念,不得不提到一个概念内部链接和外部链接

在之前的文件中,我们多次提到TU这个概念,翻译成中文就是编译单元,也就是说由单个源文件(.cc/.cxx/.cpp等)经过编译器翻译后的文件,翻译后的源文件往往生成.o文件,称为目标文件,然后该.o与其它.o一起链接成可执行文件。

对于这块的内容可以参考文章:通透,从源码到可执行文件

内部链接和外部链接就是相对于.o文件来说的,也就是说链接主要有两种类型:

1.内部链接(Internal Linkage):符号只在同一个编译单元(文件)中可见。常见的例子有使用 static 修饰的变量和函数。2.外部链接(External Linkage):符号可以跨不同的编译单元(文件)可见。这是大多数变量和函数的默认行为,除非显式指定。

PS: C++中,一个变量或者函数可以是内部或者外部链接。

默认情况下,以下几种类型都具有内部链接(此内容来自于Translation units and linkage):

const objectsconstexpr objectstypedef objectsstatic objects in namespace scope

其它的非const对象和函数在默认情况下具有外部链接。

extern

先看下面这段代码:

// file1.cc
int x = 0;

// file2.cc
extern int x;
x = 2;

在上面这段代码中,file1文件中定义了一个变量x,在file2文件中,如果要使用file1中定义的变量x,必须要重新声明一遍extern int x;

当然了,在file2文件中也可以不加extern,这样引起的一个问题就是链接错误,因为存在多个x,链接器不知道使用哪个,对于这种问题,可以看看之前的文章inline: 我的理解还停留在20年前

对于extern,不得不提的一个点就是extern仅进行声明而不进行定义,举例如下:

int x; // 声明 & 定义
extern int y; // 声明

当然了,如果我们写出类似**extern int z = 3;其看起来像是声明+定义,实际上编译器会忽略掉后面的定义,其等同于extern int z;**。

就像它应用于变量一样,extern 关键字也可以用在函数上。然而,在函数的情况下,使用 extern 关键字是冗余的,因为函数的声明和定义有明显的语法区别,即:

// file1.cc
extern void Foo();  // 函数声明,`extern` 是冗余的

// file1.cc

void Foo(); // 这就是函数声明,`extern` 可以省略

效果完全一样。

extern "C"

当你在 C++ 中使用 extern "C" 时,你告诉编译器要使用 C 语言的链接方式 来处理接下来声明或定义的符号。这意味着编译器会按照 C 语言的规则处理符号,特别是在名称修饰(name mangling)方面。

在上一篇文章几种常见特性的编译器实现的重载(Overload)一节中,提到了在重载,这样就意味着编译器可以通过参数类型或者参数个数来区分同名函数,而这种方式在C中是行不通的,也就是说C只能通过不同的函数名来调用。

假如有两个函数,其参数可以是int和double,又可以被C进行调用,那么我们可以像如下这样写:

// ====== foo.h ======
#ifdef __cplusplus
// function overloading is C++ only
int Foo(int a);
double Foo(double a);

extern"C"{
#endif // __cplusplus

int FooInt(int a);
double FooDouble(double a);

#ifdef __cplusplus
}
#endif // __cplusplus
// ====== foo.cpp ====== 
# include  "Foo.h" 
int  Foo( int a) {
return++a;
}

double  Foo ( double a) {
return a +0.01f;
}

extern"C"int  FooInt ( int a) {
returnFoo(a);
}

extern"C"double  FooDouble ( double a) {
returnFoo(a);
}

如果C调用,则可以使用FooInth和FooDouble,如果是cpp调用,则可以使用**Foo(int)和Foo(double)**。

static

static 关键字在 C++ 中用于指定符号的 静态存储持续时间(static storage duration),意味着该符号在程序的整个执行过程中都存在。换句话说,这意味着一旦被创建,它将一直存在于程序的整个生命周期内,直到程序结束。即便是局部变量,static 也不会在每次调用函数时被销毁,而是会保留其值。

源文件

如果将一个变量或者一个函数声明在源文件(.cc/.cxx/.cpp)等,那么其相当于该符号具有内部链接(internal linkage),即对其他编译单元不可见。

我们看下下面这个例子:

static int x;

static void Foo() {
    
}

如果在其它源文件同样定义了x或者Foo,不会有链接冲突,这是因为它们的符号仅仅对当前编译单元课件,当然了,如果没有加static,且在其它文件也有类似的定义,那么就会违反ODR原则,这块建议阅读文章inline,超乎你想想

特别特别需要注意的是,绝对不要在头文件中定义带有 static 的符号(除了 constexpr)。这样做会导致每个翻译单元都有自己独立的副本,这可能会引起混乱和潜在的问题。

class

我们也可以在类中声明一个static变量,这意味着所有类的实例将共享这个成员,而不管创建多少个类的对象,只有 一个副本 会存在。

#include <iostream>
usingnamespace std;

classObj{
public:
staticint counter;// 静态成员变量

Obj(){
        counter++;// 每创建一个实例,counter 会增加
}

void Counter() {
        cout <<"Counter: "<< counter << endl;
}
};

// 静态成员变量的定义
intObj::counter =0;

int main() {
Obj obj1;
    obj1.Counter();// 输出 Counter: 1

Obj obj2;
    obj2.Counter();// 输出 Counter: 2


    cout <<"Direct access to counter: "<<Obj::counter << endl;// 输出 Direct access to counter: 2

return0;
}

对于上面的示例,可能有人建议在类内使用inline static counter = 0;进行直接初始化,这样也可以,但不是本文的目的,所以不做过多解释~

函数

当一个静态变量声明在 函数作用域 内时,它会有一个特别的行为:这个静态变量 只会初始化一次,并且它的值会 在函数的多次调用之间保持不变,直到程序结束。

#include <iostream>
usingnamespace std;

void countCalls() {
staticint callCount =0;// 静态局部变量
    callCount++;
    cout <<"Function has been called "<< callCount <<" times."<< endl;
}

int main() {
countCalls();// 输出: Function has been called 1 times.
countCalls();// 输出: Function has been called 2 times.
countCalls();// 输出: Function has been called 3 times.
return0;
}

正因为这个特性,C++11及以后,甚至可以像如下这样生成单例模式:

Singleton* Singleton::GetSingleton() const {
    static Singleton instance; // Initialised during first call
    return &instance;
}


以上

如果对本文有疑问可以加笔者微信直接交流,笔者也建了C/C++相关的技术群,有兴趣的可以联系笔者加群。


推荐阅读  点击标题可跳转

1、几种常见特性的编译器实现

2、【Modern Cpp】constexpr、consteval傻傻分不清楚

3、【Modern cpp】常见面试题之move语义



雨乐聊编程
毕业于中国科学技术大学,现任某互联网公司高级技术专家一职。本公众号专注于技术交流,分享日常有意思的技术点、线上问题等,欢迎关注