从嵌入式C角度看static关键字的概念


9
static volatile unsigned char   PORTB   @ 0x06;

这是PIC微控制器头文件中的一行代码。的@操作者用于存储地址内的值PORTB 0x06,这是PIC控制器表示PORTB内部的寄存器。到目前为止,我有一个明确的想法。

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

然后,该关键字如何PORTB对我的主源文件和我手动创建的许多其他头文件可见?

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


2
这个问题没有问题,但我担心的是SE部分错误
gommer

通常在函数内部使用static来指定一次创建该变量,并将其值保留在函数执行到下一次执行之间。全局变量是在任何函数外部创建的变量,因此它在任何地方都可见。静态全局并没有任何意义。
Finbarr

8
@Finbarr错误。static全局变量在整个单个编译单元中可见,并且不会在该范围之外导出。他们很像privateOOP中的班级成员。也就是说,每个变量都需要在编译单元内的不同函数之间共享,但在cu 应该真正存在的外部不可见static。这也减少了程序的全局名称空间的“混乱”。
JimmyB

2
关于“ @运算符用于将PORTB值存储在地址0x06内”。真?是不是更像“ @运算符用于将变量“ PORTB”存储在绝对存储地址0x06上”
彼得·莫滕森

Answers:


20

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

限制范围

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

仅当并且仅当您的项目中有多个编译单元时,此限制本身才真正有意义。如果您只有一个编译单元,那么它仍然可以执行操作,但是这些效果几乎没有意义(除非您喜欢挖掘目标文件以读取编译器生成的内容。)

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

静态寿命

静态生存期意味着该变量在程序的整个过程中都存在(无论多长时间。)当您使用“静态”在函数内声明/定义变量时,这意味着该变量是在首次使用之前创建的(这意味着,每次我经历过,该变量都是在main()开始之前创建的,并且之后不会销毁。甚至在函数执行完成并返回到其调用方时也没有。就像在函数外部声明的静态生存期变量一样,它们在main()开始之前同时初始化为语义零(如果未提供初始化)或指定的显式值(如果有的话)。

这与“自动”类型的函数变量不同,后者在每次输入函数时都会被创建(或者,如果是新的话),然后在函数退出时被销毁(或者被销毁)。

与在函数外部对变量定义应用“静态”的影响会直接影响其范围不同,将函数变量(显然在函数体内)声明为“静态” 不会影响其范围。范围由在功能体内定义的事实确定。在函数中定义的静态生存期变量与在函数主体中定义的其他“自动”变量具有相同的作用域-函数作用域。

摘要

因此,“静态”关键字具有不同的上下文,其含义是“非常不同的含义”。之所以以两种方式使用它,是为了避免使用另一个关键字。(对此进行了长时间的讨论。)人们认为程序员可以忍受这种用法,避免在该语言中使用另一个关键字的价值比其他参数更重要。

(在函数外部声明的所有变量均具有静态生存期,不需要使用关键字“ static”即可实现。因此,这种释放关键字的方式将在那里使用,表示完全不同的含义:“仅在单个编译中可见单位。”这是一种hack。)

具体说明

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

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

它使用特殊的(非便携式)语法来指定PORTB的“位置”(或标签的数字值,通常是地址)。因此,链接器将获得一个地址,而无需为此找到一个。如果您有两个使用此行的编译单元,则无论如何它们都会指向相同的位置。因此,无需在此处将其标记为“外部”。

如果他们使用“外部”,可能会造成问题。然后,链接器将能够看到(并尝试匹配)在多个编译中找到的对PORTB的多个引用。如果所有人都指定这样的地址,并且由于某种原因[错误?]这些地址不同,那么该怎么办?抱怨?要么?(从技术上讲,凭经验,经验法则是只有一个编译单元可以指定该值,而其他编译单元则不能。)

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

无论哪种方式,该变量都被视为具有“静态寿命”。(以及“易失性”。)

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

在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”。链接器不能在链接时对其进行升级,因此实际上这是在编译时的定义。


3
好答案,+ 1!仅有一点要点:他们可以使用extern,这将是更正确的C方式:声明extern头文件中的变量在程序中多次包含,并在要编译的某些非头文件中定义并链接一次。毕竟,PORTB 应该恰好不同cu可以引用的变量的一个实例。因此,static这里使用的是一种快捷方式,他们为了避免在头文件之外还需要另一个.c文件而采用了这种快捷方式。
JimmyB

我还要指出,在函数内声明的静态变量不会在函数调用之间更改,这对于需要保留某种状态信息的函数很有用(我过去曾专门为此目的使用过它)。
彼得·史密斯,

@Peter我想我是这么说的。但也许不如您所愿?
jonk

@JimmyB不,他们不能使用'extern'来代替功能变量声明,就像'static'一样。'extern'已经是函数体内变量声明(不是定义)的一个选项,并且具有不同的用途-提供对任何函数外部定义的变量的链接时访问。但是我也可能会误解你的意思。
jonk

1
@JimmyB确实可以进行外部链接,尽管我不知道它是否“更合适”。一个考虑因素是,如果在翻译单元中找到该信息,则编译器可能能够发出更多优化的代码。对于嵌入式方案,在每个IO语句上节省周期可能很重要。
科特·阿蒙

9

static在当前的编译单元或“翻译单元” 之外看不到。这与同一文件不同

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


感谢您的回复。对不起,我不明白,您能解释一下这个术语吗?让我再问一个问题,即使我们要使用写在另一个文件中的变量和函数,我们也必须首先将该文件包含到我们的主源文件中。然后为什么在该头文件中使用关键字“ static volatile”。
Electro Voyager



3
@ElectroVoyager; 如果在多个c源文件中包含包含静态声明的相同标头,则这些文件中的每个文件都将具有相同名称的静态变量,但它们不是相同的变量
彼得·史密斯,

2
从@JimmyB链接:Files included by using the #include preprocessor directive become part of the compilation unit.当您将头文件(.h)包含在.c文件中时,请将其视为在源文件中插入头文件的内容,现在,这是您的编译单元。如果在.c文件中声明了该静态变量或函数,则只能在同一文件中使用它们,最后,该文件将是另一个编译单元。
gustavovelascoh

5

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

假设这组文件:

static_test.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);
}

static.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)的静态版本时,每个编译单元都可以使用var和的版本say_hello,这就是说,这两个单元都可以使用它们,但是当主函数打印变量时,请检查即使var_add_one()函数增加了 var变量,,仍然是64: 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

希望这可以帮助您澄清此事。


4

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

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

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


“您会意外地意外重用其中一个MCU使用的名称是多么容易” –我敢于希望所有数学库仅使用小写名称,并且所有MCU环境仅使用大写进行注册名称。
vsz

@vsz LAPACK充满了所有大写字母的历史名称。
德米特里·格里戈里耶夫

3

已经有一些好的答案,但是我认为需要简单直接地解决造成混乱的原因:

PORTB声明不是标准的C。它是C编程语言的扩展,仅适用于PIC编译器。需要扩展是因为PIC并非旨在支持C。

在此使用static关键字是令人困惑的,因为您永远不会static在普通代码中使用该方式。对于全局变量,您将使用extern在标题中,而不要在中static。但是PORTB 不是正常变量。这是一种黑客,它告诉编译器对寄存器IO使用特殊的汇编指令。声明PORTB static有助于诱使编译器执行正确的操作。

在文件范围内使用时, static,将变量或函数的范围限制为该文件。“文件”是指C文件以及预处理器复制到其中的所有文件使用#include时,您正在将代码复制到C文件中。这就是为什么static在标头中使用没有意义-#include标头的每个文件都将获得一个单独的变量副本,而不是一个全局变量。

与普遍的看法相反,static总是意味着同一件事:有限范围内的静态分配。这是变量在声明前后发生的事情static

+------------------------+-------------------+--------------------+
| Variable type/location |    Allocation     |       Scope        |
+------------------------+-------------------+--------------------+
| Normal in file         | static            | global             |
| Normal in function     | automatic (stack) | limited (function) |
| Static in file         | static            | limited (file)     |
| Static in function     | static            | limited (function) |
+------------------------+-------------------+--------------------+

令人困惑的是,变量的默认行为取决于定义它们的位置。


2

主文件可以看到“静态”端口定义的原因是由于#include指令。该指令等效于将整个头文件插入到源代码中,与指令本身在同一行。

微芯片XC8编译器将.c和.h文件完全相同,因此您可以将变量定义放在任一文件中。

通常,头文件包含对其他地方定义的变量(通常是.c文件)的“外部”引用。

需要在与实际硬件匹配的特定内存地址上指定端口变量。因此,实际的(非外部)定义需要存在于某处。

我只能猜测为什么Microchip公司选择将实际定义放入.h文件中。一个可能的猜测是,他们只想要一个文件(.h)而不是2个文件(.h和.c)(以便于用户使用)。

但是,如果将实际的变量定义放在头文件中,然后将该头文件包含到多个源文件中,则链接器将抱怨变量已定义多次。

解决方案是将变量声明为静态变量,然后将每个定义视为该目标文件的本地变量,并且链接程序不会抱怨。

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.