Skip to main content

Welcome to a Grasp lesson

Create a free account to keep studying and create your own study path.

Lesson 9

Storage Classes in Multi-File Projects

Recommended resources

Hello! Welcome to the first lesson of Module 2: Data Representation and Low-Level Manipulation.

In our previous module, we concluded by mastering the strict aliasing rule and safe type-punning—topics centered on how the compiler makes assumptions to optimize code. Now, we shift our focus to the other side of the coin: how we, as programmers, explicitly control the properties of our variables and functions to manage their lifetime, visibility, and interaction with the hardware.

This lesson directly addresses the learning outcome: Apply storage classes (static, extern, register) with correct linkage and lifetime semantics in multi-file projects.

For an experienced embedded engineer, a deep, practical understanding of storage classes is non-negotiable. It's fundamental to creating modular, maintainable, and efficient firmware. In interviews at companies like Qualcomm, AMD, or NVIDIA, you'll be expected to articulate precisely how these keywords influence the build process and program structure.

We will cover:

  1. The foundational concepts: Storage Duration, Linkage, and Scope.
  2. How the keywords static and extern are used to control these properties.
  3. The role and relevance of register and auto.
  4. A practical, multi-file project example to see these concepts in action.
  5. How these C concepts translate to symbols in an ELF object file.

1. The Three Pillars: Scope, Storage Duration, and Linkage

Before we dive into the keywords themselves, it's crucial to understand the three distinct properties they control. Confusing them is a common source of bugs and misunderstandings. The keywords static and extern are merely tools to manipulate these underlying attributes.

Storage Duration and Linkage in C and C++ - Dan Saks

To build a precise mental model, we'll watch a few key segments from a talk by Dan Saks, a renowned expert on C/C++. He does an exceptional job of separating these concepts, which is the key to true understanding.

Please watch the following segments from the video "Storage Duration and Linkage in C and C++": Introduction (0:32 - 1:28): The speaker introduces the core attributes of any named entity in C: type, scope, storage duration, and linkage. Storage Duration (31:06 - 35:02): Focus on the definitions of static, automatic, and dynamic storage duration. This answers the question: "How long does this object exist in memory?" Linkage (35:02 - 40:56): This is the most critical part. Pay close attention to the explanation of external, internal, and no linkage. This answers the question: "Can this name be accessed from other files?"

To summarize the key ideas from the video:

  • Scope: Where a name is visible (e.g., block scope, file scope). This is a compile-time concept, confined to a single file.
  • Storage Duration (Lifetime): When an object's memory is allocated and deallocated.
    • Automatic: The object's lifetime is tied to its scope (e.g., a non-static local variable). Typically allocated on the stack.
    • Static: The object exists for the entire duration of the program's execution. It's allocated once when the program starts.
  • Linkage: Determines if a name in one translation unit can refer to the same entity in another translation unit. This is what the linker resolves.
    • External Linkage: The name is made available to all other translation units. Functions and file-scope variables have this by default.
    • Internal Linkage: The name is restricted to the current translation unit. It's "private" to its file.
    • No Linkage: The name is unique to its scope and cannot be accessed from elsewhere (e.g., local variables).

With these formal definitions, let's see how the C keywords map onto them.

The Five C Storage Classes Summary
This table provides a concise summary of how different declaration methods in C map to the properties of scope, duration, and linkage.

2. The Keywords: static, extern, register, and auto

The static Keyword: A Tale of Two Meanings

The static keyword is notoriously context-dependent. Its meaning changes completely based on where you use it.

1. At File Scope (Internal Linkage)

When you declare a global variable or a function as static, you are changing its linkage from external (the default) to internal. The storage duration remains static. This effectively makes the variable or function private to that specific .c file.

// In module.c

// This variable is private to module.c. No other file can access it.
static int private_counter = 0;

// This function is private to module.c. No other file can call it directly.
static void private_helper_function() {
    private_counter++;
}

// This function has external linkage (the default) and can be called from other files.
void public_api_function() {
    private_helper_function();
}

This is a cornerstone of modular design in C, allowing you to hide implementation details and prevent naming collisions between different parts of a large project.

2. At Block Scope (Static Storage Duration)

When you declare a local variable inside a function as static, you are changing its storage duration from automatic (the default) to static. Its linkage remains none. The variable is initialized only once, before the program begins, and it retains its value across subsequent calls to the function.

Understanding Static in C++

Let's watch a practical demonstration of a static local variable. This short clip clearly illustrates how the variable persists across function calls.

Watch the segment from 3:39 to 4:47. Notice the difference in output when the count variable is declared with and without the static keyword. The static version behaves like a global variable but with its scope limited to the function.

This is extremely useful for things like counters, state variables in a simple state machine, or ensuring a one-time initialization within a function without using global variables.

The extern Keyword: Declaring, Not Defining

The extern keyword is used to create a declaration for a global variable or function that has been defined in another translation unit. It's a promise to the compiler that the name exists and has external linkage, and that the linker will find it.

  • Definition: Allocates storage for the variable (e.g., int g_shared_data = 100;). There must be exactly one definition in the entire program.
  • Declaration: Introduces the name and its type without allocating storage (e.g., extern int g_shared_data;). There can be many declarations.
// In shared_data.h
// This declares that g_shared_data exists somewhere.
// Any file including this header can use g_shared_data.
extern int g_shared_data;

// In shared_data.c



#include "shared_data.h"
// This is the one and only definition. It allocates the storage.
int g_shared_data = 100;

// In main.c



#include <stdio.h>
#include "shared_data.h"
int main() {
    printf("Shared data: %d\n", g_shared_data); // Accesses the variable defined in shared_data.c
    return 0;
}

For functions, extern is optional in declarations as they have external linkage by default. void my_func(); at file scope is implicitly extern void my_func();.

register and auto

  • register: This is a hint to the compiler to store a variable in a CPU register for faster access. Modern compilers are extremely good at register allocation, making this keyword largely obsolete. The main constraint it imposes is that you cannot take the address of a variable declared with register (e.g., &my_reg_var). It's good interview trivia but rarely used in modern code.
  • auto: In C, this keyword explicitly specifies automatic storage duration for a local variable. Since this is the default, it is redundant and never used. (Note: C++11 repurposed auto for type inference, giving it a completely new and very useful meaning).

3. Tying It All Together: A Multi-File Project

Let's build a small project to see how these concepts prevent common errors in multi-file setups.

Static and Extern in C - Lei Mao's Log Book

This blog post provides a perfect, self-contained example of a multi-file project that uses static and extern. We will use it to understand how the linker behaves.

Read the sections "Static and Extern" and "Example". Pay close attention to the code in lib.c and main.c and the comments explaining what would cause a linking error. For example, main.c cannot call hello_heaven() because it is static to lib.c.

Let's analyze the key interactions from that example:

  • Two i variables: Both main.c and lib.c define static int i = 0;. Because static gives them internal linkage, they are completely separate variables, private to their respective files. The linker sees them as distinct, and there is no conflict.
  • One j variable: main.c defines int j = 0; (external linkage by default). lib.c declares extern int j;. The linker connects the usage of j in lib.c to the single definition in main.c. Both files modify the same variable.
  • static function call: main.c cannot call hello_heaven() directly because it is static in lib.c. The linker would produce an "undefined reference" error. The only way to call it is indirectly via the public function hello_heaven_shortcut().

4. Under the Hood: Storage Classes and ELF Symbols

How does the linker know the difference between a static global and a non-static global? It comes down to the symbol table in the compiled object file (e.g., an ELF file).

Chapter 2 Basics: C language

This resource provides a more advanced look, connecting C-level concepts to the underlying object file format. This is precisely the kind of deep knowledge that distinguishes candidates in semiconductor interviews.

Read the section "2.6 Symbols". Focus on how C object properties translate to symbol bindings in ELF files. Note the distinction between LOCAL and GLOBAL bindings in the readelf output, which corresponds directly to internal and external linkage.

Let's put this into practice. If you were to compile the files from our earlier example, you could inspect the symbols with nm (arm-none-eabi-nm for cross-compilation).

nm main.o might show:

...
00000000 D j          // 'D' indicates a GLOBAL symbol in the initialized data section (.data)
00000004 d i          // 'd' indicates a LOCAL symbol in the .data section
...

nm lib.o might show:

...
         U j          // 'U' indicates an UNDEFINED (external) symbol the linker must find
00000000 d i          // 'd' indicates a different LOCAL symbol in this file's .data section
...
  • A variable with external linkage (j) becomes a GLOBAL symbol (D). The linker will ensure there's only one global symbol with that name across all object files.
  • A variable with internal linkage (static i) becomes a LOCAL symbol (d). The linker doesn't try to connect it with symbols in other files, even if they have the same name.
  • An extern declaration (extern j) becomes an UNDEFINED symbol (U), which is a request for the linker to resolve.

Conclusion

You have now explored the critical role of storage class specifiers in C. By understanding and correctly applying them, you gain precise control over your program's structure, which is essential for writing robust and modular embedded software.

Key Takeaways:

  • Separate the Concepts: Always think in terms of storage duration (lifetime) and linkage (visibility across files) first. The keywords are just the syntax to control these properties.
  • static is Two-Faced: At file scope, static means internal linkage. Inside a function, static means static storage duration.
  • extern is a Promise: It declares a variable or function with external linkage, telling the compiler to trust that the linker will find its single definition elsewhere.
  • Linker's View: These keywords directly control whether a symbol in an object file is marked as LOCAL or GLOBAL, which dictates how the linker treats it when creating the final executable.

Preview of the Next Lesson:

In this lesson, we saw how to manage object lifetime and visibility. In the next lesson, we will tackle the volatile keyword. While static and extern communicate properties to the linker, volatile communicates a crucial instruction to the compiler: "Do not optimize away accesses to this memory location." This is fundamental for interacting with hardware registers, shared memory in multi-threaded applications, and ISRs, making it one of the most important keywords in embedded C. We'll also cover const correctness, another form of communication with the compiler.