Master C Preprocessor Tricks, Static/Const/Volatile Keywords, and Common Embedded Pitfalls
This article walks through essential C concepts for embedded development, including macro definitions for constants and MIN, the use of #error, infinite loops, variable declaration patterns, the roles of static, const, and volatile, bit‑mask operations, absolute address access, ISR constraints, unsigned‑int quirks, safe typedef usage, and tricky expressions like a+++b.
1. Defining Constants with Macros
Use #define SEC_YEAR (365*24*60*60)UL to define the number of seconds in a year. The UL suffix forces the constant to be an unsigned long, preventing overflow on 16‑bit targets. The macro demonstrates basic #define syntax, constant‑expression evaluation by the preprocessor, and the importance of parentheses.
2. Safe Minimum Macro
The naïve macro #define MIN(a,b) ((a)<=(b)?(a):(b)) suffers from multiple evaluation and type‑mismatch problems. Safer alternatives evaluate each argument exactly once and preserve type safety:
#define min_i(x,y) ((x)<=(y)?(x):(y))
#define min_t(type,x,y) ({ type _x = (x); type _y = (y); _x < _y ? _x : _y; })
#define min(x,y) ({ const typeof(x) _x = (x); const typeof(y) _y = (y); (void)(&_x == &_y); _x < _y ? _x : _y; })These macros use a temporary variable (or a GNU statement expression) so that side‑effects such as p++ are performed only once.
3. Compile‑Time Checks with #error
Embedding #error "XXX has been defined" inside an #ifdef / #else block forces a compilation failure when an unexpected macro is defined. This guarantees that the build environment matches the programmer’s assumptions.
4. Writing Infinite Loops in Embedded C
Three idiomatic ways to create a dead‑loop that never returns:
while(1) {} for(;;) {} loop: … goto loop;5. Variable and Pointer Declarations
Common declaration patterns used in interview questions:
int a; // integer
int *a; // pointer to int
int **a; // pointer to pointer to int
int a[10]; // array of 10 ints
int *a[10]; // array of 10 pointers to int
int (*a)[10]; // pointer to an array of 10 ints
int (*a)(int); // pointer to a function taking int and returning int
int (*a[10])(int); // array of 10 function pointers with the same signature6. The static Keyword
Three distinct uses in C:
Static local variables retain their value between function calls.
Static global variables have internal linkage, making them invisible to other translation units.
Static functions are limited to the defining source file.
In C++ the keyword can also qualify class data members and member functions, providing shared storage and file‑level access.
7. The const Keyword
constmakes an object read‑only. It can qualify variables, function parameters, and return values, enabling:
Compile‑time type safety (preventing accidental modification).
Potential compiler optimisations because the value cannot change.
Self‑documenting code that expresses intent.
8. The volatile Keyword
volatiletells the compiler that a variable may be changed by external factors (hardware, ISR, another thread). The optimizer must always reload the value from memory. Typical uses:
Memory‑mapped hardware registers.
Variables accessed from an interrupt service routine.
Shared variables in a multithreaded environment.
9. Bit‑Mask Operations
Define a mask and manipulate a static integer without affecting other bits:
#define BIT3 (0x1 << 3)
static int a;
void set_bit3(void) { a |= BIT3; }
void clear_bit3(void) { a &= ~BIT3; }10. Accessing an Absolute Memory Address
Cast the address to a pointer and write the desired value. This is legal in a strictly conforming ANSI C compiler:
int *ptr = (int *)0x67A9;
*ptr = 0xAA66;11. Pitfalls of Using __interrupt
Interrupt Service Routines (ISRs) must obey the following rules:
They must have a void return type (no return value).
They must not take parameters.
Avoid floating‑point operations and non‑reentrant library calls such as printf, because many embedded compilers do not provide a re‑entrant floating‑point context.
12. Unsigned‑Int Comparison Quirk
In the expression
unsigned int a = 6;
int b = -20;
(a + b > 6) ? puts("> 6") : puts("<= 6");the signed operand b is converted to unsigned, yielding a large positive value. Consequently the condition is true and the program prints > 6.
13. Complement of Zero
For portable code, obtain a bitwise complement of zero with unsigned int compzero = ~0;. Hard‑coding 0xFFFF assumes a 16‑bit int and is not portable.
14. Behaviour of malloc(0)
Calling malloc(0) may return a non‑NULL pointer. The pointer can safely be passed to free. A typical test prints “Got a valid pointer”.
15. typedef vs Macro for Pointer Types
Using typedef struct s *tPS; is safer than #define dPS struct s *. The typedef binds the pointer to the type, preventing accidental variable definitions such as struct s *p1, p2; where p2 would be a plain struct, not a pointer.
16. The Curious Expression a+++b
The token sequence is parsed as a++ + b. After evaluation a becomes 6, b remains 7, and c receives 12.
Signed-in readers can open the original source through BestHub's protected redirect.
This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactand we will review it promptly.
Liangxu Linux
Liangxu, a self‑taught IT professional now working as a Linux development engineer at a Fortune 500 multinational, shares extensive Linux knowledge—fundamentals, applications, tools, plus Git, databases, Raspberry Pi, etc. (Reply “Linux” to receive essential resources.)
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.
