Concepto de la palabra clave estática desde la perspectiva de C incrustado

9
static volatile unsigned char   PORTB   @ 0x06;

Esta es una línea de código en un archivo de encabezado de microcontrolador PIC. El @operador se utiliza para almacenar el valor PORTB dentro de la dirección 0x06, que es un registro dentro del controlador PIC que representa PORTB. Hasta este punto, tengo una idea clara.

Esta línea se declara como una variable global dentro de un archivo de encabezado ( .h). Entonces, por lo que supe sobre el lenguaje C, una "variable global estática" no es visible para ningún otro archivo, o simplemente, las funciones / variables globales estáticas no se pueden usar fuera del archivo actual.

Entonces, ¿cómo puede PORTBser visible esta palabra clave para mi archivo fuente principal y muchos otros archivos de encabezado que creé manualmente?

En mi archivo fuente principal, solo agregué el archivo de encabezado #include pic.h¿Tiene esto algo que ver con mi pregunta?

Electro Voyager
fuente
2
no hay problema con la pregunta, pero la sección SE incorrecto temo
gommer
static normalmente se usa dentro de una función para especificar que la variable se crea una vez y mantiene su valor de una ejecución de una función a la siguiente. Una variable global es una creada fuera de cualquier función para que sea visible en todas partes. global estático realmente no tiene sentido.
Finbarr
8
@Finbarr Mal. staticlos globales son visibles dentro de la unidad de compilación individual completa, y no se exportan más allá de eso. Son muy parecidos a los privatemiembros de una clase en OOP. Es decir, todas las variables que deben compartirse entre diferentes funciones dentro de una unidad de compilación, pero no se supone que sean visibles fuera de ese cu realmente deberían ser static. Esto también reduce el "golpeteo" del espacio de nombres global del programa.
JimmyB
2
Re "El operador @ se utiliza para almacenar el valor PORTB dentro de la dirección 0x06" . De Verdad? ¿No es más como "El operador @ se usa para almacenar la variable" PORTB "en la dirección de memoria absoluta 0x06" ?
Peter Mortensen

Respuestas:

20

La palabra clave 'static' en C tiene dos significados fundamentalmente diferentes.

Alcance limitante

En este contexto, 'static' se empareja con 'extern' para controlar el alcance de una variable o nombre de función. Estático hace que el nombre de la variable o función esté disponible solo dentro de una sola unidad de compilación y solo disponible para el código que existe después de la declaración / definición dentro del texto de la unidad de compilación.

Esta limitación en sí misma solo significa algo si y solo si tiene más de una unidad de compilación en su proyecto. Si solo tiene una unidad de compilación, entonces todavía hace las cosas, pero esos efectos son en su mayoría inútiles (a menos que le guste excavar en archivos de objetos para leer lo que generó el compilador).

Como se señaló, esta palabra clave en este contexto se empareja con la palabra clave 'extern', que hace lo contrario, al hacer que la variable o el nombre de la función se puedan vincular con el mismo nombre que se encuentra en otras unidades de compilación. Por lo tanto, puede considerar que 'static' requiere que la variable o el nombre se encuentren dentro de la unidad de compilación actual, mientras que 'extern' permite el enlace de unidades de compilación cruzada.

Vida estática

La duración estática significa que la variable existe a lo largo de la duración del programa (por mucho tiempo que sea). Cuando usa 'estática' para declarar / definir una variable dentro de una función, significa que la variable se crea en algún momento antes de su primer uso ( lo que significa, cada vez que lo he experimentado, que la variable se crea antes de que comience main () y no se destruye después. Ni siquiera cuando la ejecución de la función se completa y vuelve a su llamador. Y al igual que las variables de vida útil estáticas declaradas fuera de las funciones, se inicializan en el mismo momento, antes de que main () comience, a un cero semántico (si no se proporciona ninguna inicialización) oa un valor explícito específico, si se proporciona.

Esto es diferente de las variables de función de tipo 'automático', que se crean nuevas (o, como si fueran nuevas) cada vez que se ingresa la función y luego se destruyen (o, como si se destruyeron) cuando la función sale.

A diferencia del impacto de aplicar 'estática' en una definición de variable fuera de una función, que afecta directamente su alcance, declarar una variable de función (dentro de un cuerpo de función, obviamente) como 'estática' no tiene impacto en su alcance. El alcance está determinado por el hecho de que se definió dentro de un cuerpo de función. Las variables de duración estática definidas dentro de las funciones tienen el mismo alcance que otras variables 'automáticas' definidas dentro de los cuerpos de las funciones: alcance de funciones.

Resumen

Entonces, la palabra clave 'estática' tiene contextos diferentes con lo que equivale a "significados muy diferentes". La razón por la que se usó de dos maneras, como esta, fue para evitar usar otra palabra clave. (Hubo una larga discusión al respecto). Se consideró que los programadores podían tolerar el uso y el valor de evitar otra palabra clave más en el lenguaje era más importante (que los argumentos de lo contrario).

(Todas las variables declaradas fuera de las funciones tienen una vida útil estática y no necesitan la palabra clave 'static' para que eso sea cierto. Por lo tanto, este tipo de palabra clave se liberó para usarse allí para significar algo completamente diferente: 'visible solo en una sola compilación unidad. 'Es un truco, de algún tipo.)

Nota específica

estático volátil sin signo char PORTB @ 0x06;

La palabra 'estática' aquí debe interpretarse en el sentido de que el vinculador no intentará hacer coincidir múltiples ocurrencias de PORTB que se pueden encontrar en más de una unidad de compilación (suponiendo que su código tenga más de una).

Utiliza una sintaxis especial (no portátil) para especificar la "ubicación" (o el valor numérico de la etiqueta, que generalmente es una dirección) de PORTB. Entonces, el enlazador recibe la dirección y no necesita encontrar una. Si tuviera dos unidades de compilación usando esta línea, cada una terminaría apuntando al mismo lugar, de todos modos. Entonces no hay necesidad de etiquetarlo como 'externo', aquí.

Si hubieran usado 'externo' podría plantear un problema. El enlazador entonces podría ver (e intentaría hacer coincidir) múltiples referencias a PORTB encontradas en múltiples compilaciones. Si todos ellos especifican una dirección como esta, y las direcciones NO son las mismas por alguna razón [¿error?], Entonces ¿qué se supone que debe hacer? ¿Quejar? ¿O? (Técnicamente, con 'externo' la regla general sería que solo UNA unidad de compilación especificaría el valor y las demás no deberían).

Es más fácil etiquetarlo como 'estático', evitando que el enlazador se preocupe por los conflictos, y simplemente culpe por cualquier error en las direcciones que no coinciden con quien cambió la dirección a algo que no debería ser.

De cualquier manera, la variable se trata como si tuviera una 'vida útil estática'. (Y 'volátil').

Una declaración no es una definición , pero todas las definiciones son declaraciones

En C, una definición crea un objeto. También lo declara. Pero una declaración generalmente no (vea la nota de viñeta a continuación) crea un objeto.

Las siguientes son definiciones y declaraciones:

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

Las siguientes no son definiciones, sino solo declaraciones:

extern int b;
extern int f();

Tenga en cuenta que las declaraciones no crean un objeto real. Solo declaran los detalles al respecto, que el compilador puede usar para ayudar a generar el código correcto y proporcionar mensajes de advertencia y error, según corresponda.

  • Arriba, digo "por lo general", aconsejado. En algunos casos, una declaración puede crear un objeto y, por lo tanto, el vinculador la promueve a una definición (nunca el compilador). Por lo tanto, incluso en este raro caso, el compilador C todavía piensa que la declaración es solo una declaración. Es la fase de enlace que hace las promociones necesarias de alguna declaración. Tenga esto en cuenta cuidadosamente.

    En los ejemplos anteriores, si resulta que solo hay declaraciones para un "extern int b;" en todas las unidades de compilación vinculadas, el vinculador tiene la responsabilidad de crear una definición. Tenga en cuenta que este es un evento de tiempo de enlace. El compilador es completamente inconsciente, durante la compilación. Solo se puede determinar en el momento del enlace, si se promueve una declaración de este tipo.

    El compilador es consciente de que "static int a;" no puede ser promovido por el enlazador durante el enlace, por lo que este hecho es una definición en tiempo de compilación .

jonk
fuente
3
Gran respuesta, +1! Solo un punto menor: podrían haber usado extern, y sería la forma más adecuada de hacerlo en C: declarar la variable externen un archivo de encabezado para incluirla varias veces en el programa y definirla en algún archivo sin encabezado para compilar y vinculado exactamente una vez. Después de todo, PORTB se supone que es exactamente una instancia de la variable a la que pueden referirse diferentes cu's. Por lo tanto, el uso de statichere es una especie de acceso directo que tomaron para evitar necesitar otro archivo .c además del archivo de encabezado.
JimmyB
También me gustaría señalar que una variable estática declarada dentro de una función no se cambia entre las llamadas a funciones, lo que puede ser útil para funciones que necesitan retener algún tipo de información de estado (la he usado específicamente para este propósito en el pasado).
Peter Smith
@ Peter Creo que dije eso. ¿Pero tal vez no tan bien como te hubiera gustado?
jonk
@JimmyB No, no podrían haber usado 'extern', en cambio, para las declaraciones de variables de función que se comportan como 'static'. 'extern' ya es una opción para declaraciones de variables (no definiciones) dentro de los cuerpos de funciones y tiene un propósito diferente: proporcionar acceso en tiempo de enlace a variables definidas fuera de cualquier función. Pero es posible que yo también esté malinterpretando tu punto.
jonk
1
@JimmyB La vinculación externa definitivamente sería posible, aunque no sé si es "más apropiado". Una consideración es que el compilador puede emitir código más optimizado si la información se encuentra en la unidad de traducción. Para escenarios integrados, guardar ciclos en cada declaración de E / S puede ser un gran problema.
Cort Ammon
9

statics no son visibles fuera de la unidad de compilación actual , o "unidad de traducción". Esto no es lo mismo que el mismo archivo .

Tenga en cuenta que incluye el archivo de encabezado en cualquier archivo fuente donde pueda necesitar las variables declaradas en el encabezado. Esta inclusión hace que el archivo de encabezado sea parte de la unidad de traducción actual y (una instancia de) la variable visible dentro de él.

JimmyB
fuente
Gracias por su respuesta. "Unidad de compilación", lo siento, no entiendo, ¿puede explicar ese término? Permítame hacerle una pregunta más, incluso si queremos usar variables y funciones escritas dentro de otro archivo, primero debemos INCLUIR ese archivo en nuestro ARCHIVO DE FUENTES principal. Entonces, ¿por qué la palabra clave "estática volátil" en ese archivo de encabezado?
Electro Voyager
1
Discusión bastante profunda en stackoverflow.com/questions/572547/what-does-static-mean-in-c
Peter Smith
3
@ElectroVoyager; si incluye el mismo encabezado que contiene una declaración estática en varios archivos fuente c, entonces cada uno de esos archivos tendrá una variable estática con el mismo nombre, pero no son la misma variable .
Peter Smith
2
Desde el enlace @JimmyB: Files included by using the #include preprocessor directive become part of the compilation unit.cuando incluya su archivo de encabezado (.h) en un archivo .c, piense en ello como insertar el contenido del encabezado en el archivo fuente, y ahora, esta es su unidad de compilación. Si declara esa variable o función estática en un archivo .c, puede usarlas solo en el mismo archivo, que al final, será otra unidad de compilación.
gustavovelascoh
5

Trataré de resumir los comentarios y la respuesta de @ JimmyB con un ejemplo explicativo:

Supongamos este conjunto de archivos:

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();
}

Puede compilar y ejecutar el código usando gcc -o static_test static_src.c static_test.c -DUSE_STATIC=1; ./static_testpara usar el encabezado estático o gcc -o static_test static_src.c static_test.c -DUSE_STATIC=0; ./static_testpara usar el encabezado no estático.

Tenga en cuenta que aquí hay dos unidades de compilación: static_src y static_test. Cuando usa la versión estática del encabezado ( -DUSE_STATIC=1), habrá una versión de vary say_hellodisponible para cada unidad de compilación, es decir, ambas unidades pueden usarlas, pero compruebe que aunque la var_add_one()función incremente su var variable, cuando la función principal imprime su var variable , sigue siendo 64:

$ 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

Ahora, si intenta compilar y ejecutar el código, utilizando una versión no estática ( -DUSE_STATIC=0), arrojará un error de enlace debido a la definición de variable duplicada:

$ 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

Espero que esto pueda ayudarte a aclarar este asunto.

gustavovelascoh
fuente
4

#include pic.hmás o menos significa "copiar el contenido de pic.h en el archivo actual". Como resultado, cada archivo que incluye pic.hobtiene su propia definición local de PORTB.

Quizás se pregunte por qué no existe una definición global única de PORTB. La razón es bastante simple: solo puede definir una variable global en una fichero C, por lo que si desea utilizar PORTBen múltiples archivos en su proyecto, lo que se necesita pic.hcon una declaración de PORTBy pic.ccon su definición . Permitir que cada archivo C defina su propia copia PORTBfacilita la creación de código, ya que no tiene que incluir en los archivos de su proyecto que no escribió.

Una ventaja adicional de las variables estáticas frente a las globales es que obtienes menos conflictos de nombres. El archivo AC que no usa ninguna característica de hardware MCU (y por lo tanto no incluye pic.h) puede usar el nombre PORTBpara su propio propósito. No es que sea una buena idea hacerlo a propósito, pero cuando desarrolle, por ejemplo, una biblioteca matemática independiente de MCU, se sorprenderá de lo fácil que es reutilizar accidentalmente un nombre que es utilizado por una de las MCU.

Dmitry Grigoryev
fuente
"se sorprendería de lo fácil que es reutilizar accidentalmente un nombre que es utilizado por una de las MCU" . Me atrevo a esperar que todas las bibliotecas de matemáticas usen solo nombres en minúsculas y que todos los entornos de MCU solo usen mayúsculas para el registro. nombres
vsz
@vsz LAPACK para uno está lleno de nombres históricos con mayúsculas.
Dmitry Grigoryev
3

Ya hay algunas buenas respuestas, pero creo que la causa de la confusión debe abordarse de manera simple y directa:

La declaración PORTB no es el estándar C. Es una extensión del lenguaje de programación C que solo funciona con el compilador PIC. La extensión es necesaria porque los PIC no fueron diseñados para admitir C.

El uso de la staticpalabra clave aquí es confuso porque nunca se usaría de staticesa manera en el código normal. Para una variable global, usaría externen el encabezado, no static. Pero PORTB no es una variable normal . Es un truco que le dice al compilador que use instrucciones de ensamblaje especiales para registrar IO. Declarar PORTB staticayuda a engañar al compilador para que haga lo correcto.

Cuando se usa en el alcance del archivo, staticlimita el alcance de la variable o función a ese archivo. "Archivo" significa el archivo C y cualquier cosa copiada por el preprocesador. Cuando usa #include, está copiando el código en su archivo C. Es por eso que usar staticun encabezado no tiene sentido: en lugar de una variable global, cada archivo que incluye el encabezado obtendría una copia separada de la variable.

Contrariamente a la creencia popular, staticsiempre significa lo mismo: asignación estática con alcance limitado. Esto es lo que sucede con las variables antes y después de ser declaradas 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) |
+------------------------+-------------------+--------------------+

Lo que lo hace confuso es que el comportamiento predeterminado de las variables depende de dónde estén definidas.

Adam Haun
fuente
2

La razón por la que el archivo principal puede ver la definición de puerto "estático" se debe a la directiva #include. Esa directiva es equivalente a insertar el archivo de encabezado completo en su código fuente en la misma línea que la directiva misma.

El compilador microchip XC8 trata los archivos .c y .h exactamente igual para que pueda colocar sus definiciones de variables en cualquiera de los dos.

Normalmente, un archivo de encabezado contiene referencias "externas" a variables que se definen en otro lugar (generalmente un archivo .c).

Las variables de puerto debían especificarse en direcciones de memoria específicas que coinciden con el hardware real. Por lo tanto, una definición real (no externa) debía existir en alguna parte.

Solo puedo adivinar por qué Microchip Corporation eligió poner las definiciones reales en el archivo .h. Una suposición probable es que solo querían un archivo (.h) en lugar de 2 (.h y .c) (para facilitar las cosas al usuario).

Pero si coloca las definiciones de variables reales en un archivo de encabezado y luego incluye ese encabezado en varios archivos de origen, el vinculador se quejará de que las variables se definen varias veces.

La solución es declarar las variables como estáticas, luego cada definición se trata como local para ese archivo de objeto y el vinculador no se quejará.

usuario4574
fuente