In C programming, function calls introduce a small overhead due to stack operations, parameter passing, and control transfer. While this overhead is negligible in most applications. But it can become significant in performance-critical systems such as embedded firmware, device drivers, and real-time applications.
To reduce this overhead, the C language provides the inline keyword, which suggests to the compiler that the function body should be expanded directly at the call site instead of performing a normal function call.
In this article, you will learn:
- What an inline function in C is
- How the inline specifier works
- The difference between inline, static inline, and extern inline
- When inline functions should be used
- Practical examples used in embedded systems
What is the inline function in C?
A function declared with the inline specifier is known as an inline function.
The C language introduced the inline keyword in the C99 standard. The inline specifier is a hint given to the compiler to perform an optimization. Its purpose is to suggest that the compiler expand the function body at the point where the function is called, instead of performing a normal function call. However, the compiler is free to ignore this suggestion.
In other words, when you create an inline function, the compiler may replace the function call with the actual body of the function. However, it is important to understand that inline is only a hint to the compiler. The compiler is free to ignore this request depending on optimization settings and other conditions.
Let’s see an example.
inline void Swap(int *a, int *b)
{
int tmp= *a;
*a= *b;
*b = tmp;
}
If the compiler decides to inline this function, every call to Swap() may be replaced with the function body.
Swap(&x, &y);
may internally become:
int tmp = *(&x); *(&x) = *(&y); *(&y) = tmp;
This avoids extra overhead created by the function call such as:
- Stack operations
- Parameter passing
- Jump to function
- Return from function
However, excessive inlining may increase the code size, because the function body is duplicated at every call site.
For example, If Swap() were a large 50-line function and it was called in 100 different places, the compiler might duplicate those 50 lines 100 times when inlining.
This can significantly increase the size of the final compiled binary. In performance-critical or embedded systems, excessive binary size may lead to Instruction Cache (I-Cache) thrashing. If the expanded code no longer fits in the CPU’s ultra-fast L1 instruction cache, the CPU must frequently fetch instructions from slower levels of memory. Ironically, this can make the “optimized” program run slower instead of faster.
Why Use Inline Functions?
An inline function is a function where the compiler may replace the function call with the actual function body. Instead of performing a normal function call, the function’s code is inserted directly at the call site. Inline functions are typically used for small, frequently called functions to reduce the overhead of function calls.
Let’s see a few reasons to use inline functions in your code.
1. Reduced Function Call Overhead:
In a normal function call, several steps occur behind the scenes:
- Pushing arguments onto the stack: Each argument passed to the function is stored temporarily on the call stack.
- Jumping to the function location: The program counter moves to the function’s code.
- Executing the function and returning: After execution, the program returns to the point where the function was called.
Each of these steps adds extra time to the execution, which can become significant in functions that are called frequently, especially in loops.
Inline functions tell the compiler to replace the function call with the actual code of the function. This eliminates:
- Stack operations for arguments
- Jumping to the function location
- Return instructions
As a result, execution becomes faster because the overhead of the call is removed.
Example,
inline int square(int x)
{
return x * x;
}
int main()
{
int result = square(5); // replaced by: int result = 5 * 5;
}
Here, the compiler replaces square(5) with 5 * 5 directly, saving the overhead of a normal function call.
2. Faster Execution for Small Functions:
Inline functions are particularly beneficial for short, frequently used functions. Since the compiler replaces the function call with the actual code, it avoids repeated function call overhead, leading to faster execution.
These small functions often include:
- Mathematical helpers: Functions like square(), max(), or min()
- Bit manipulation functions: For setting, clearing, or toggling bits
- Getter/setter functions: Accessor methods in classes that just return or set a value
Example: Getter function
class Point
{
int x;
public:
inline int
getX ()
{
return x;
}
};
Instead of performing a function call every time getX() is used, the compiler inserts the code return x; directly, reducing execution time.
This is why inline functions are most effective for small, frequently called functions. Large functions, when inlined, can actually increase code size and may slow down caching, so they are usually left as normal functions.
3. Better Readability and Safety Than Macros
Inline functions are often used as a safer alternative to macros. Macros are processed by the preprocessor, which simply performs text substitution without understanding the code structure. This can lead to unexpected results.
Let’s understand it with an example code.
#define SQUARE(x) x*x
If we use it like this:
int result = SQUARE(3+2);
The macro expands to:
3+2*3+2
Because of operator precedence, the result becomes:
3 + 6 + 2 = 11 // Wrong result
The expected result should be 25. But this produces the wrong result because the macro does not automatically add parentheses.
With inline functions, you will not face such macro expansion issues. They also provide better readability, type checking, and safer code execution compared to macros. Let’s understand it with same example.
inline int square (int x)
{
return x * x;
}
Usage:
int result = square(3 + 2);
Now the compiler evaluates it correctly:
(3 + 2) * (3 + 2) = 25
4. Type Safety
One major advantage of inline functions over macros is type safety.
Macros do not perform type checking because they are handled by the preprocessor before compilation. They simply replace text in the program, which may lead to errors if incorrect data types are passed.
However, inline functions are checked by the compiler, just like normal functions. The compiler verifies that the correct data types are used when calling the function.
Example,
inline int add(int a, int b)
{
return a + b;
}
Usage:
int result = add(5, 10);
Here, the compiler checks that both arguments (a and b) are integers. If you pass the wrong type of value, the compiler will show an error or warning, helping you avoid mistakes.
5. Argument Evaluation:
Macros can sometimes cause unexpected behavior because they may evaluate arguments multiple times.
Since macros work by text substitution, the same argument can be used more than once in the expanded code. If the argument contains an expression that changes value, it may produce incorrect results.
Example Using Macro
#define SQUARE(x) x * x
Usage:
int a = 3; int result = SQUARE(a++);
After expansion, it becomes:
a++ * a++
Here, a++ is executed twice, which changes the value of a two times and may give an unexpected result.
Inline Function Version
inline int square (int x)
{
return x * x;
}
Usage:
int a = 3;
int result = square(a++);
In this case, the argument a++ is evaluated only once, because inline functions behave like normal functions.
Inline Function vs Macro:
Both inline functions and macros are used to reduce function call overhead by expanding code at the point where it is used.
In early C programming (before the C99 standard introduced the inline keyword), developers relied heavily on preprocessor macros (#define) to avoid the overhead associated with function calls. Macros achieve this by performing simple text substitution before the compilation phase.
However, macros can introduce subtle and hard-to-detect errors because the preprocessor performs substitution without type checking, scope awareness, or proper evaluation of expressions.
To address these limitations, the C99 standard introduced inline functions, which allow the compiler to expand function code at the call site while still preserving the semantics, type safety, and debugging capabilities of normal C functions.
Although both aim to improve performance by reducing function call overhead, they differ significantly in terms of safety, maintainability, debugging support, and behavior.
Let’s see few differences between the macro and inline function.
Advantages of Inline Functions over Macros:
Here are few advantages of inline function over the Macros. You must know before using them.
Type Safety: Inline functions are checked by the compiler for correct types, whereas macros are simple text substitutions and can lead to type-related errors.
Arguments Evaluated Only Once: Inline functions evaluate each argument a single time, preventing unexpected side effects that can occur with macros where arguments may be evaluated multiple times.
Better Debugging Support: Since inline functions are actual functions, debuggers can step through them easily, unlike macros which are expanded as code text.
Follows Scope Rules: Inline functions respect normal scope and visibility rules, while macros do not have scope and can unintentionally affect code outside their intended area.
More Readable and Maintainable: Inline functions have clear syntax and behave like regular functions, making the code easier to read and maintain compared to macros.
Compiler Performs Syntax Checking: Inline functions are compiled like regular functions, so the compiler can catch syntax errors, unlike macros which may silently expand into invalid code.
When Macros Are Still Used:
Although inline functions are often preferred, macros remain useful in specific situations:
1. Header Guards:
Macros prevent multiple inclusions of the same header file, avoiding redefinition errors:
#ifndef FILE_H
#define FILE_H
// header file content
#endif
2. Conditional Compilation
Macros allow code to be included or excluded depending on conditions, useful for debugging or platform-specific code:
#ifdef DEBUG
printf("Debug Mode\n");
#endif
3. Constant Definitions:
Macros can define constants that are replaced at compile time:
#define PI 3.14159
But nowadays, constexpr in C/C++ is preferred over macros for defining constants because it is type-safe, scoped, and evaluated at compile-time.
When the Compiler May Ignore inline:
Declaring a function with the inline keyword does not guarantee that the function will actually be inlined. The compiler decides whether inlining is appropriate. Therefore, the inline keyword acts as a suggestion to the compiler, not a strict instruction.
Let’s look at some situations where the compiler may ignore the inline keyword. This will help you decide whether you need to use inline or not.
1. Function is Too Large:
If the function body is very large, inlining it everywhere may increase the program size (code bloat). So, the compiler may keep it as a normal function call.
2. Function is Recursive:
Recursive functions usually cannot be inlined because they call themselves. Inlining such functions would require the compiler to expand the function body repeatedly without limit, which is not practical.
Example,
inline int factorial (int n)
{
if (n <= 1)
return 1;
return n * factorial (n - 1);
}
Inlining this would require the compiler to expand the function an unknown number of times, which is not practical. So, compiler may ignore inline here.
3. Function has complex control flow:
If a function contains complex control flow, the compiler may decide not to inline it. Inlining such functions can make the generated code very large and difficult to optimize.
Examples include functions that contain:
- Large loops
- switch statements
- Multiple conditional branches (if-else)
- goto statements
- Exception-like control flow
4. The Function Is Virtual
Virtual functions usually cannot be inlined when called through a base-class pointer or reference, because the compiler doesn’t know which version of the function will run until the program is running.
Example,
Base *obj = new Derived ();
obj->display (); // Compiler may inline this in some cases, depending on
// optimization settings
5. Function address is taken
If the address of a function is used (for example, assigned to a function pointer), the compiler may avoid inlining because the function must exist as a callable entity.
Example,
inline void display() {}
void (*funcPtr)() = display;
6. Compiler Optimization Decisions
Modern compilers use their own optimization strategies. Even if a function is marked inline, the compiler may ignore it if it determines that inlining would not improve performance.
Difference Between inline, static inline, and extern inline in C:
Understanding how inline functions work in C can be confusing because their behavior is influenced not only by compiler optimization but also by the linkage rules defined in the C99 standard.
Unlike C++, where inline mainly enables multiple definitions across translation units while suggesting inlining to the compiler, C uses inline as part of its linkage model. As a result, the meaning of inline in C is tied to how the function is defined, where it is visible, and whether an external definition exists.
Modern C provides three commonly used forms of inline declarations:
- static inline
- inline
- extern inline
Each of these forms behaves differently in terms of linkage, visibility, and how the compiler and linker treat the function definition. Before discussing these variants, it is important to understand the concept of a translation unit.
What is a “Translation Unit”?
A translation unit is the result of a source file after the preprocessor has processed all #include directives and macros.
For example:
main.c + included headers → one translation unit driver.c + included headers → another translation unit
Each translation unit is compiled independently by the compiler before the linker combines them into the final program. This concept is important because inline functions behave differently depending on their linkage across translation units.
1. static inline:
When a function is declared as static inline, it has internal linkage, meaning the function is visible only within the translation unit that includes the definition. Because each translation unit receives its own private definition, static inline is the most common and safest way to define inline functions in header files.
How It Works:
When a header containing a static inline function is included:
- The compiler may replace function calls with the function body (inline expansion).
- If the compiler decides not to inline the function (due to optimization limits or function size), it generates a local callable copy of the function.
- If the compiler does not inline the function:
- It generates a local (internal) callable copy of the function.
- This copy has internal linkage, so it is visible only inside that translation unit.
So, in summary:
- Inline → no function call
- Not inlined → private function generated.
Example,
/* math_utils.h */
static inline int square(int x)
{
return x * x;
}
Now its usage,
#include "math_utils.h"
int main(void)
{
int result = square(5);
}
Why static inline function is Safe in Header Files:
The safety of static inline functions in header files comes from their internal linkage semantics.
When a function is declared as static inline:
- No external symbol is emitted: The function does not produce a globally visible symbol, so the linker never sees it.
- Each translation unit gets its own definition: Every .c file that includes the header has its own private copy of the function.
- No multiple definition conflicts: Since the definitions are local to each translation unit, the One Definition Rule at the linker level is never violated.
Example,
static inline void gpioSetPin(GPIO_TypeDef *port, uint16_t pin)
{
port->BSRR = pin;
}
In addition, the compiler is free to inline the function at call sites. If it chooses not to inline, it still generates a local (internal) function, avoiding any linkage issues.
2. Inline (Inline Definition with External Linkage):
If you have ever added the inline keyword to a function in a header file and then been surprised by a linker error like “undefined reference”. Do not worry, you are not alone. This is one of the most common pitfalls developers encounter when working with modern C (C99 and later), especially if coming from C++.
The root of the confusion lies in how C defines the behavior of inline. Unlike C++, where inline also helps manage multiple definitions across translation units, in C it does not create a complete, externally linkable function definition.
In C, declaring a function with inline alone provides an inline definition but does not create an external (linkable) definition. This is one of the most misunderstood aspects of C99 inline semantics.
How It Works
When an inline function appears in a header file:
- The compiler may use it for inline expansion within that translation unit.
- If the function cannot be inlined, the compiler expects that a separate external definition exists in another source file. If no external definition exists, the linker will produce an undefined reference error.
Example,
//math_utils.h
inline int add(int a, int b)
{
return a + b;
}
In this situation, one source file must provide the external definition.
// math_utils.c
int add(int a, int b)
{
return a + b;
}
This external definition acts as the fallback implementation when the compiler does not inline the function.
3. extern inline (External Inline Definition):
The extern inline keyword is used when you want a function to be available globally, while still allowing the compiler to inline it.
How It Works
When a function is declared as extern inline:
- The compiler creates a global function that can be called from other files
- At the same time, it may still inline the function where possible
Example,
extern inline int multiply(int a, int b)
{
return a * b;
}
Important Points for inline function in C:
Any function with internal linkage can be an inline function. See the below example in which I have used the swap function that has internal linkage.
static inline void Swap(int *a, int *b)
{
int tmp= *a;
*a= *b;
*b = tmp;
}
If a non-static function is declared inline, then it must be defined in the same translation unit. The inline definition that does not use extern is not externally visible and does not prevent other translation units from defining the same function. Let’s see an example ( Compiler: -std=C99).
Example 1:
/*main.c*/
#include<stdio.h>
inline void ok();
int main()
{
ok();
return 0;
}
/*test.c*/
inline void ok()
{
//function body
}
Output: Error
Example 2:
/*main.c*/
#include<stdio.h>
void ok();
int main()
{
ok();
return 0;
}
/*test.c*/
#include<stdio.h>
extern inline void ok()
{
printf("%d",10);
}
Output: 10
Now I am going to explain a very important concept. As we know that an inline definition does not provide an external definition for the function and does not forbid an external definition in another translation unit.
So, you can say that an inline definition provides an alternative to an external definition, which a translator may use to implement any call to the function in the same translation unit. It is unspecified whether a call to the function uses the inline definition or the external definition.
Let’s see an example (Compiler: -std=C99).
Example 1:
Creating an inline function name and the definition is an “inline definition”. Also, I am not creating any external definition of the name
#include<stdio.h>
inline const char *name()
{
return "Aticle";
}
int main()
{
printf("%s", name());
return 0;
}
Output:

Note: According to the C standard, If an identifier declared with internal linkage is used in an expression (other than as a part of the operand of a sizeof or _Alignof operator whose result is an integer constant), there shall be exactly one external definition for the identifier in the translation unit.
Example 2:
Now creating an external definition to the function “name”.
/*main.c*/
#include<stdio.h>
inline const char *name()
{
return "Aticle";
}
int main()
{
printf("%s", name());
return 0;
}
/*test.c*/
extern const char *name()
{
return "world";
}
Output: Unspecified (Could call external or internal definition).
You also need to remember that a non-static inline function (function with external linkage) must not contain a definition of a non-const object with static or thread storage duration. Also, it must not contain a reference to an identifier with internal linkage. Consider the below example,
static int data;
inline void fun()
{
static int n = 1; // error: non-const static in a
// non-static inline function
int value = data; // error: non-static inline
// function accesses a static variable
}
Recommended Post:
To strengthen your understanding of C programming concepts, explore the following articles:
- Function Specifiers in C (inline, static, extern).
- Type Qualifiers in C (const, volatile, restrict) Explained C.
- Punctuators in C with Examples.
- Basic Elements of C Programming Language.
- C String Literals and Their Types with Examples.
- C Identifiers: Rules, Naming Conventions, and Best Practices.
- Stringizing Operator (#) in C Preprocessor
- Token Pasting Operator in C/C++ programming.
- How to Use typedef in C (Examples and Best Practices).
- Macros in C: Complete Guide with Examples.
- enum in C: Definition, Examples, and Practical Applications
- Understanding the volatile Qualifier in Embedded C
- C Format Specifiers: Complete Guide with Examples.