Variable Declaration Examples: extern, Initialization, Garbage Values and Addresses-Free Embedded C Programming Course

Need Help or More Information?

Questions about this free C programming course, embedded C training or career guidance? Get in touch.

MODULE 4 · LESSON 4.1 · EXAMPLES

Variable Declaration Examples: extern, Initialization, Garbage Values and Addresses

Eleven worked examples with real compiler output: shared variables across files, the two classic linker errors, every initializer form, uninitialized-variable warnings, zero-initialization proofs and the address-of operator.

11
Worked Examples
Beginner
Level
Real output
Compiled and run

Variable Declaration Examples: What This Page Covers

These variable declaration examples show declaration, definition, initialization, garbage values and addresses in real, compiled programs. Compiler and linker messages are the genuine output of GCC, so you will recognise them when they appear in your own STM32 projects.

#ExampleSkill
1One variable shared by three files (extern in a header)Declaration vs definition
2The two classic linker errorsReading linker output
3Every initialization form, with sizeofInitializers
4What GCC says about an uninitialized localWarnings
5Garbage values: two runs, two resultsIndeterminate values
6Global and static zero-initialization, verified.bss
7Initialization happens once for statics, every time for localsWhen initializers run
8The & operator: addresses, sizes and alignmentAddress of a variable
9Reading and writing through an addressPointer preview
10A realistic configuration structure with designated initializersEmbedded pattern
11Mini exercisesSelf-test

Example 1: One Variable Shared by Three Files

This is one of the variable declaration examples you should be able to reproduce yourself. Problem: Let main.c and board.c share the counter g_ticks without creating two copies.

board.h
#ifndef BOARD_H #define BOARD_H #include <stdint.h> extern uint32_t g_ticks; /* declaration */ extern const char g_name[]; /* declaration (array of unknown size) */ void tick(void); uint32_t uptime_seconds(void); #endif
board.c
#include “board.h” uint32_t g_ticks = 0; /* definition: memory allocated here */ const char g_name[] = “Discovery”; void tick(void) { g_ticks += 250; } uint32_t uptime_seconds(void) { return g_ticks / 1000; }
main.c
#include <stdio.h> #include “board.h” int main(void) { for (int i = 0; i < 8; i++) tick(); printf(“board : %s\n”, g_name); printf(“ticks : %u ms\n”, (unsigned)g_ticks); printf(“uptime : %u s\n”, (unsigned)uptime_seconds()); return 0; }
Output (verified by compiling and running)
board : Discovery ticks : 2000 ms uptime : 2 s
Who declares and who defines
board.h
extern uint32_t g_ticks;
declaration only
→
board.c
uint32_t g_ticks = 0;
the one definition: SRAM allocated
←
main.c
#include “board.h”
uses the same storage
Why board.c includes board.h: The compiler then compares the definition with the declaration. If the types ever disagree you get an error at compile time instead of memory corruption at run time.

Example 2: The Two Classic Linker Errors

Another of the variable declaration examples, this time with real output. Problem: Recognise the two linker messages that come from confusing declaration with definition.

Error A: multiple definition

board.h
int g_mode; /* WRONG: definition in a header */
a.c
#include “board.h” int get_a(void) { return g_mode; }
b.c
#include “board.h” int get_b(void) { return g_mode; } int main(void) { return 0; }
Real GCC/ld output
/usr/bin/ld: file1.o:(.bss+0x0): multiple definition of `g_mode’; file2.o:(.bss+0x0): first defined here collect2: error: ld returned 1 exit status

Both a.c and b.c include the header, so each file defines its own g_mode. The linker finds two definitions of the same name. Fix: put extern int g_mode; in the header and int g_mode; in one source file.

Error B: undefined reference

main.c
extern int g_mode; /* declared … */ int main(void) { return g_mode; /* … but never defined anywhere */ }
Real GCC/ld output
/usr/bin/ld: file1.o: in function `main’: main.c:(.text+0xa): undefined reference to `g_mode’ collect2: error: ld returned 1 exit status

The compiler accepted extern int g_mode; because it trusts that a definition exists elsewhere. The linker looked for it and failed. Fix: add the definition (int g_mode;) to exactly one .c file.

Linker messageMeaningCure
multiple definition of ‘x’Two or more definitionsKeep one; turn the others into extern declarations
undefined reference to ‘x’Declared, never definedAdd the definition, or link the file that has it

Example 3: Every Initialization Form With sizeof

Among the variable declaration examples, this one is the most common in firmware. Problem: Predict the contents and the size of each initialized object, then compare with the real output.

init_forms.c
#include <stdio.h> #include <stdint.h> typedef struct { uint8_t id; uint16_t period_ms; uint8_t enabled; } Task; int main(void) { int a[5] = { 1, 2, 3 }; /* two elements left over -> 0 */ int b[] = { 4, 5, 6, 7 }; /* size deduced: 4 */ uint8_t buf[16] = { 0 }; /* all zero */ char name1[] = “STM32”; /* 6 bytes: 5 + terminating zero */ char name2[10] = “STM32”; /* 10 bytes: rest are zero */ int grid[2][3] = { { 1, 2, 3 }, { 4 } };/* 2 rows, second row partly zero */ Task t = { .id = 7, .period_ms = 100 }; /* enabled left = 0 */ printf(“a : %d %d %d %d %d (sizeof %zu)\n”, a[0], a[1], a[2], a[3], a[4], sizeof(a)); printf(“b : %zu elements\n”, sizeof(b) / sizeof(b[0])); printf(“buf : sizeof %zu, buf[15] = %u\n”, sizeof(buf), buf[15]); printf(“name1 : %s sizeof %zu\n”, name1, sizeof(name1)); printf(“name2 : %s sizeof %zu name2[9] = %d\n”, name2, sizeof(name2), name2[9]); printf(“grid : row0 = %d %d %d, row1 = %d %d %d\n”, grid[0][0], grid[0][1], grid[0][2], grid[1][0], grid[1][1], grid[1][2]); printf(“task : id=%u period=%u enabled=%u\n”, t.id, t.period_ms, t.enabled); return 0; }
Output (verified by compiling and running)
a : 1 2 3 0 0 (sizeof 20) b : 4 elements buf : sizeof 16, buf[15] = 0 name1 : STM32 sizeof 6 name2 : STM32 sizeof 10 name2[9] = 0 grid : row0 = 1 2 3, row1 = 4 0 0 task : id=7 period=100 enabled=0
char name2[10] = “STM32”: the rest of the array is filled with zeros
S
T
M
3
2
\0
\0
\0
\0
\0
blue = characters, red = terminating zero, grey = zero padding

Example 4: What GCC Says About an Uninitialized Local

The next of the variable declaration examples shows a typical bug. Problem: The classic accumulator bug. Does the compiler notice?

sum.c
#include <stdio.h> int main(void) { int sum; /* forgot “= 0” */ for (int i = 1; i <= 4; i++) sum += i; printf(“sum = %d\n”, sum); return 0; }
GCC output with -Wall -Wextra -O2 (warnings need optimisation enabled)
sum.c: In function ‘main’: sum.c:7:13: warning: ‘sum’ is used uninitialized [-Wuninitialized] 7 | sum += i; | ~~~~^~~~ sum.c:5:9: note: ‘sum’ was declared here 5 | int sum; /* forgot “= 0” */ | ^~~

Without -O1 or higher GCC often stays silent because it does not run the data-flow analysis needed to detect the problem. That is why embedded projects should always build release and debug configurations with -Wall -Wextra and look at the release-build warnings. Fix: int sum = 0;

Example 5: Garbage Values: Same Program, Different Results

Problem: Show that the contents of an uninitialized variable are unpredictable by running the same compiled-from-scratch program three times.

garbage.c
#include <stdio.h> static void dirty(void) { volatile int scratch[16]; for (int i = 0; i < 16; i++) scratch[i] = 0x5A5A5A5A; } static void victim(void) { int x, y; /* uninitialised locals */ printf(“x = %d, y = %d\n”, x, y); } int main(void) { dirty(); victim(); return 0; }
Three real runs on a PC (the numbers change; on a microcontroller they would depend on earlier code and on power-up SRAM)
run 1: x = 1624929536, y = -2127088740 run 2: x = -1637329664, y = -1066913477 run 3: x = 138118400, y = -783110678
  • Even though dirty() wrote a known pattern into the stack, victim() did not reliably see it: the compiler may use different slots, and run-time randomisation changes the surrounding data.
  • This is undefined behaviour, not just an unspecified value: an optimising compiler may do anything, including removing the code.
  • The remedy is trivial and free: int x = 0, y = 0; (one per line in MISRA style).

Example 6: Verifying Global and Static Zero-Initialization

Problem: Prove that objects with static storage duration start at zero, for every kind: integer arrays, a matrix, a double, a pointer, a file-scope static and a local static.

zero_check.c
#include <stdio.h> #include <stdint.h> uint8_t g_big[1024]; /* global, no initializer */ int g_matrix[4][4]; double g_value; char *g_ptr; static uint16_t s_hist[32]; /* file-scope static */ static unsigned count_nonzero(const uint8_t *p, size_t n) { unsigned c = 0; for (size_t i = 0; i < n; i++) if (p[i] != 0) c++; return c; } int main(void) { static int visits; /* local static */ int nz_matrix = 0; for (int r = 0; r < 4; r++) for (int c = 0; c < 4; c++) if (g_matrix[r][c]) nz_matrix++; printf(“non-zero bytes in g_big[1024] : %u\n”, count_nonzero(g_big, sizeof g_big)); printf(“non-zero entries in g_matrix : %d\n”, nz_matrix); printf(“g_value = %.1f, g_ptr is %s\n”, g_value, g_ptr == NULL ? “NULL” : “not NULL”); printf(“non-zero bytes in s_hist : %u\n”, count_nonzero((const uint8_t *)s_hist, sizeof s_hist)); printf(“visits (static local) : %d\n”, visits); return 0; }
Output (verified by compiling and running)
non-zero bytes in g_big[1024] : 0 non-zero entries in g_matrix : 0 g_value = 0.0, g_ptr is NULL non-zero bytes in s_hist : 0 visits (static local) : 0
ObjectInitial valueSection on the STM32F407
uint8_t g_big[1024]all zero (1 KB).bss
int g_matrix[4][4]all zero.bss
double g_value0.0.bss
char *g_ptrNULL.bss
static uint16_t s_hist[32]all zero.bss
static int visits (local)0.bss
Embedded caution: This guarantee depends on the start-up code clearing .bss. A bootloader or a custom linker script that forgets to do so produces globals full of random data after a warm reset.

Example 7: Statics Initialize Once, Automatic Locals Every Time

Problem: Two functions both write id = 100;, yet one counts up and the other does not. Why?

init_when.c
#include <stdio.h> static int next_id_static(void) { static int id = 100; /* initialised ONCE, before main() runs */ return id++; } static int next_id_auto(void) { int id = 100; /* initialised EVERY call */ return id++; } int main(void) { printf(“call static auto\n”); for (int i = 1; i <= 4; i++) printf(“%4d %6d %4d\n”, i, next_id_static(), next_id_auto()); return 0; }
Output (verified by compiling and running)
call static auto 1 100 100 2 101 100 3 102 100 4 103 100
When does the initializer run?
static int id = 100;
  • Storage exists for the whole program (.data)
  • Value placed there before main()
  • Function calls just change it: 100, 101, 102 …
int id = 100;
  • New stack slot on every call
  • The initializer is a real instruction executed each time
  • Always returns 100

Lesson 4.2 builds on exactly this difference when it explains lifetime and the static keyword.

Example 8: The & Operator: Addresses, Sizes and Alignment

Problem: Use the address-of operator & to learn how variables are laid out. Absolute addresses differ on every system, so the program prints only differences and alignment, which are reproducible.

address_of.c
#include <stdio.h> #include <stdint.h> #include <stddef.h> typedef struct { uint8_t a; uint32_t b; uint16_t c; } Mix; int g_global; int main(void) { int local = 5; uint16_t arr[4] = { 10, 20, 30, 40 }; Mix m; static int s_static; printf(“sizeof(local) = %zu, sizeof(arr) = %zu\n”, sizeof(local), sizeof(arr)); printf(“&arr[1] – &arr[0] = %td elements = %td bytes\n”, &arr[1] – &arr[0], (char *)&arr[1] – (char *)&arr[0]); printf(“&arr[3] – &arr[0] = %td bytes\n”, (char *)&arr[3] – (char *)&arr[0]); printf(“offsets in Mix : a=%zu b=%zu c=%zu (size %zu)\n”, offsetof(Mix, a), offsetof(Mix, b), offsetof(Mix, c), sizeof(Mix)); printf(“(char *)&m.b – (char *)&m = %td\n”, (char *)&m.b – (char *)&m); printf(“&local %% 4 = %lu (int is 4-byte aligned)\n”, (unsigned long)(uintptr_t)&local % 4); printf(“&arr %% 2 = %lu (uint16_t is 2-byte aligned)\n”, (unsigned long)(uintptr_t)&arr % 2); printf(“&m.b %% 4 = %lu\n”, (unsigned long)(uintptr_t)&m.b % 4); printf(“regions: global=%s static=%s local=%s\n”, (uintptr_t)&g_global < (uintptr_t)&local ? “below stack” : “above stack”, (uintptr_t)&s_static < (uintptr_t)&local ? “below stack” : “above stack”, “stack”); return 0; }
Output (verified by compiling and running)
sizeof(local) = 4, sizeof(arr) = 8 &arr[1] – &arr[0] = 1 elements = 2 bytes &arr[3] – &arr[0] = 6 bytes offsets in Mix : a=0 b=4 c=8 (size 12) (char *)&m.b – (char *)&m = 4 &local % 4 = 0 (int is 4-byte aligned) &arr % 2 = 0 (uint16_t is 2-byte aligned) &m.b % 4 = 0 regions: global=below stack static=below stack local=stack
uint16_t arr[4]: neighbouring addresses differ by sizeof(uint16_t) = 2
base+0
arr[0] = 10
base+2
arr[1] = 20
base+4
arr[2] = 30
base+6
arr[3] = 40
struct Mix { uint8_t a; uint32_t b; uint16_t c; } — padding makes it 12 bytes
Mix (12 bytes)
a
pad
pad
pad
b0
b1
b2
b3
c0
c1
pad
pad
On the STM32F407: Global and static variables sit at 0x2000xxxx in SRAM (.data/.bss), while locals are on the stack near the top of SRAM, close to 0x2001FFFF. The relationships you see here (neighbouring elements, alignment) hold on the target as well.

Example 9: Reading and Writing Through an Address

Problem: An address is itself a value you can store. Store the address of x in a pointer and use it to read and change x.

through_address.c
#include <stdio.h> #include <stdint.h> int main(void) { uint32_t x = 10; uint32_t *p = &x; /* p holds the address of x */ printf(“x = %u\n”, x); printf(“*p = %u (read through the address)\n”, *p); *p = 99; /* write through the address */ printf(“after *p=99: x = %u\n”, x); printf(“p == &x : %d\n”, p == &x); uint8_t *bytes = (uint8_t *)&x; printf(“x as bytes : %02X %02X %02X %02X\n”, bytes[0], bytes[1], bytes[2], bytes[3]); return 0; }
Output (verified by compiling and running)
x = 10 *p = 10 (read through the address) after *p=99: x = 99 p == &x : 1 x as bytes : 63 00 00 00
x and p in memory
address A
x = 99
uint32_t x
←
address B
p = A
uint32_t *p

This is the same mechanism that lets *(volatile uint32_t *)0x40020C14 write to a hardware register: there the address is not &x but a number from the reference manual. Module 9 covers pointers in depth.

Example 10: A Realistic Configuration Structure With Designated Initializers

Problem: Describe the configuration of two UARTs. Designated initializers make each setting readable and guarantee that members you leave out are zero.

uart_config.c
#include <stdio.h> #include <stdint.h> typedef enum { PARITY_NONE, PARITY_EVEN, PARITY_ODD } Parity; typedef struct { uint32_t baud; uint8_t data_bits; uint8_t stop_bits; Parity parity; uint8_t use_dma; } UartConfig; /* Global with designated initializers: unlisted members are zero */ static const UartConfig k_debug_uart = { .baud = 115200, .data_bits = 8, .stop_bits = 1 }; int main(void) { UartConfig gps = { .baud = 9600, .data_bits = 8, .stop_bits = 1, .parity = PARITY_NONE, .use_dma = 1 }; UartConfig unset; /* oops: garbage */ (void)unset; printf(“debug uart : %u baud, %u-%u, parity %d, dma %u\n”, (unsigned)k_debug_uart.baud, k_debug_uart.data_bits, k_debug_uart.stop_bits, k_debug_uart.parity, k_debug_uart.use_dma); printf(“gps uart : %u baud, %u-%u, parity %d, dma %u\n”, (unsigned)gps.baud, gps.data_bits, gps.stop_bits, gps.parity, gps.use_dma); printf(“sizeof(UartConfig) = %zu\n”, sizeof(UartConfig)); return 0; }
Output (verified by compiling and running)
debug uart : 115200 baud, 8-1, parity 0, dma 0 gps uart : 9600 baud, 8-1, parity 0, dma 1 sizeof(UartConfig) = 16
  • .baud = 115200 documents itself; positional {115200, 8, 1, 0, 0} does not.
  • The global const object lives in flash on the STM32F407 and costs no SRAM.
  • The local unset object would contain garbage if it were ever read: always initialize with = {0} or designated initializers.

Example 11: Mini Exercises

Exercise 1: Is extern int count; a declaration, a definition, or both?
A declaration only. It allocates no memory.
Exercise 2: How many bytes does char s[] = "ARM"; occupy?
4 (three letters plus the terminating zero).
Exercise 3: What are the contents of int a[4] = {5};?
5, 0, 0, 0.
Exercise 4: Which values do a, b, c have after int a = 1, b, c = 3; inside main?
a = 1, c = 3, b is indeterminate (garbage).
Exercise 5: A global int g; is read before any assignment. What does it contain?
0, guaranteed (it lives in .bss).
Exercise 6: Why does a function that declares static int n = 0; keep counting while int n = 0; does not?
The static is initialized once and its storage lasts for the whole program; the automatic variable is recreated and re-initialized on each call.
Exercise 7: Which build error appears if two files both contain int flag; at file scope?
multiple definition of ‘flag’ from the linker.

Key Takeaways from the Variable Declaration Examples

  • The variable declaration examples show one definition plus many extern declarations: the pattern behind every shared variable.
  • Multiple definition and undefined reference are the two linker messages that signal a declaration/definition mix-up.
  • Partial initializers zero the remaining elements; char s[] = "ARM" includes the terminator.
  • GCC detects uninitialized locals only with optimisation on: always read release-build warnings.
  • Uninitialized locals give different garbage on different runs; statics and globals are zero.
  • Statics are initialized once before main(); automatic variables on every entry.
  • &x yields the address; element and member addresses show sizes, padding and alignment.

Ready for the quiz?

Next: multiple choice, garbage-value identification, fill-in-the-blank, code challenges and interview questions on declaration and definition.

Need Help or More Information?

Questions about this free C programming course, embedded C training or career guidance? Get in touch.

2 Comments

Leave a Reply

Your email address will not be published. Required fields are marked *