嵌入式C视角下的static关键字概念

电器工程 微控制器 图片 C 嵌入式 C++
2022-01-07 13:54:20
static volatile unsigned char   PORTB   @ 0x06;

这是 PIC 单片机头文件中的一行代码。@运算符用于将 PORTB 的值存储在 address中,address0x06是 PIC 控制器内部代表 PORTB 的寄存器。至此,我有一个清晰的想法。

此行在头文件 ( ) 中声明为全局变量.h因此,根据我对 C 语言的了解,“静态全局变量”对任何其他文件都不可见 - 或者简单地说,静态全局变量/函数不能在当前文件之外使用。

PORTB那么,我的主源文件和我手动创建的许多其他头文件如何才能看到这个关键字?

在我的主要源文件上,我只添加了头文件#include pic.h这与我的问题有关吗?

4个回答

C 中的关键字“static”有两个根本不同的含义。

限制范围

在这种情况下,“静态”与“外部”配对以控制变量或函数名称的范围。静态导致变量或函数名称仅在单个编译单元中可用,并且仅可用于编译单元文本中声明/定义之后存在的代码。

只有当且仅当您的项目中有多个编译单元时,此限制本身才真正有意义。如果您只有一个编译单元,那么它仍然可以做一些事情,但这些效果大多是没有意义的(除非您喜欢深入研究目标文件以读取编译器生成的内容。)

如前所述,此上下文中的此关键字与关键字“extern”配对,后者的作用相反——通过使变量或函数名称可与其他编译单元中的相同名称链接。因此,您可以将“静态”视为要求在当前编译单元中找到变量或名称,而“外部”允许交叉编译单元链接。

静态寿命

静态生命周期意味着变量在程序的整个持续时间内都存在(不管有多长)。当您使用“静态”在函数中声明/定义变量时,这意味着该变量是在第一次使用之前的某个时间创建的(这意味着,每次我经历它时,变量都是在 main() 开始之前创建的)并且之后不会被销毁。即使函数执行完成并返回给调用者,也不会。就像在函数外部声明的静态生命周期变量一样,它们在同一时刻被初始化——在 main() 开始之前——初始化为语义零(如果没有提供初始化)或指定的显式值(如果给定)。

这与 'auto' 类型的函数变量不同,后者在每次进入函数时创建新的(或者,好像是新的),然后在函数退出时被销毁(或者,好像它们被销毁)。

与将“静态”应用于函数外部的变量定义的影响不同,后者直接影响其范围,将函数变量(显然在函数体内)声明为“静态”对其范围没有影响。范围取决于它是在函数体中定义的事实。在函数中定义的静态生命周期变量与在函数体中定义的其他“自动”变量具有相同的范围——函数范围。

概括

因此,“静态”关键字具有不同的上下文,相当于“非常不同的含义”。像这样以两种方式使用它的原因是为了避免使用另一个关键字。(对此进行了长时间的讨论。)人们认为程序员可以容忍使用语言,并且避免在语言中使用另一个关键字的价值更重要(否则比争论。)

(在函数之外声明的所有变量都具有静态生命周期,并且不需要关键字“静态”来实现这一点。因此,这种释放了在那里使用的关键字来表示完全不同的含义:“仅在单个编译中可见单位。'这是一种黑客行为。)

具体说明

静态易失无符号字符 PORTB @ 0x06;

此处的“静态”一词应解释为意味着链接器不会尝试匹配可能在多个编译单元中找到的多次出现的PORTB(假设您的代码有多个)。

它使用一种特殊的(不可移植的)语法来指定 PORTB 的“位置”(或标签的数值,通常是地址)。所以链接器得到了地址,不需要为它找到一个。如果你有两个使用这条线的编译单元,无论如何它们都会指向同一个地方。因此,这里没有必要将其标记为“外部”。

如果他们使用'extern'可能会造成问题。然后,链接器将能够查看(并尝试匹配)在多个编译中找到的对PORTB的多个引用。如果他们都指定了这样的地址,并且由于某种原因地址不一样[错误?],那么它应该做什么?投诉?要么?(从技术上讲,使用“extern”的经验法则是只有一个编译单元会指定该值,而其他编译单元不应该。)

将其标记为“静态”更容易,避免让链接器担心冲突,并且只需将不匹配地址的任何错误归咎于将地址更改为不应该的地址的人。

无论哪种方式,变量都被视为具有“静态生命周期”。(和“易变的”。)

声明不是定义,但所有定义都是声明

在 C 中,定义创建了一个对象。它也声明了它。但是声明通常不会(参见下面的项目符号)创建对象。

以下是定义和声明:

static int a;
static int a = 7;
extern int b = 5;
extern int f() { return 10; }

以下不是定义,而只是声明:

extern int b;
extern int f();

请注意,声明不会创建实际对象。它们仅声明有关它的详细信息,然后编译器可以使用这些详细信息来帮助生成正确的代码并在适当的情况下提供警告和错误消息。

  • 上面,我建议说“通常”。在某些情况下,一个声明可以创建一个对象,因此被链接器提升为一个定义(从不被编译器提升)。所以即使在这种罕见的情况下,C 编译器仍然认为该声明只是一个声明。链接器阶段对某些声明进行了必要的提升。请牢记这一点。

    在上面的例子中,如果只有“extern int b;”的声明?在所有链接的编译单元中,链接器负责创建定义。请注意,这是一个链接时间事件。编译器在编译期间完全不知道。它只能在链接时确定,如果这种类型的声明最有可能被提升。

    编译器知道“static int a;” 链接器不能在链接时提升,所以这实际上是在 compile-time的定义。

statics 在当前编译单元或“翻译单元”之外不可见。这和同一个文件不一样。

请注意,您头文件包含在您可能需要在头文件中声明的变量的任何源文件中。这种包含使头文件成为当前翻译单元的一部分,并且变量(的实例)在其中可见。

我将尝试用一个解释性示例来总结评论和@JimmyB 的回答:

假设这组文件:

静态测试.c:

#include <stdio.h>
#if USE_STATIC == 1
    #include "static.h"
#else
    #include "no_static.h"
#endif

void var_add_one();

void main(){

    say_hello();
    printf("var is %d\n", var);
    var_add_one();
    printf("now var is %d\n", var);
}

静态.h:

static int var=64;
static void say_hello(){
    printf("Hello!!!\n");
};

no_static.h:

int var=64;
void say_hello(){
    printf("Hello!!!\n");
};

static_src.c:

#include <stdio.h>

#if USE_STATIC == 1
    #include "static.h"
#else
    #include "no_static.h"
#endif

void var_add_one(){
    var = var + 1;
    printf("Added 1 to var: %d\n", var);
    say_hello();
}

gcc -o static_test static_src.c static_test.c -DUSE_STATIC=1; ./static_test您可以使用静态标头或gcc -o static_test static_src.c static_test.c -DUSE_STATIC=0; ./static_test使用非静态标头来编译和运行代码。

请注意,这里有两个编译单元:static_src 和 static_test。使用-DUSE_STATIC=1静态版本,仍然是 64:varsay_hellovar_add_one() var var

$ gcc -o static_test static_src.c static_test.c -DUSE_STATIC=1; ./static_test                                                                                                                       14:33:12⌚
Hello!!!
var is 64
Added 1 to var: 65
Hello!!!
now var is 64

现在,如果您尝试使用非静态版本 ( -DUSE_STATIC=0) 编译和运行代码,则会由于重复的变量定义而引发链接错误:

$ gcc -o static_test static_src.c static_test.c -DUSE_STATIC=0; ./static_test                                                                                                                       14:35:30⌚
/tmp/ccLBy1s7.o:(.data+0x0): multiple definition of `var'
/tmp/ccV6izKJ.o:(.data+0x0): first defined here
/tmp/ccLBy1s7.o: In function `say_hello':
static_test.c:(.text+0x0): multiple definition of `say_hello'
/tmp/ccV6izKJ.o:static_src.c:(.text+0x0): first defined here
collect2: error: ld returned 1 exit status
zsh: no such file or directory: ./static_test

希望这可以帮助您澄清这个问题。

#include pic.h大致意思是“将 pic.h 的内容复制到当前文件中”。结果,每个包含的文件pic.h都有自己的本地定义PORTB.

也许您想知道为什么没有单一的全局定义PORTB. 原因很简单:你只能在一个C 文件中定义一个全局变量,所以如果你想在你的项目中的多个文件中使用PORTB你需要pic.h一个声明它的定义让每个 C 文件定义其自己的副本使代码构建更容易,因为您不必将非编写的项目文件包含在您的项目文件中。PORTBpic.cPORTB

静态变量与全局变量相比的另一个好处是您可以减少命名冲突。不使用任何 MCU 硬件功能(因此不包括pic.h)的 AC 文件可以将名称PORTB用于其自身目的。并不是故意这样做是个好主意,但是当您开发例如与 MCU 无关的数学库时,您会惊讶地发现意外重用一个由其中一个 MCU 使用的名称是多么容易。