notes / Systems Programming
Implementing Formatted Output in C
Field notes from rebuilding a minimal printf-style function in C to understand variadic arguments, format parsing, character output, conversion handling, and low-level edge cases.
Why This Note Exists
This note documents the process of implementing a minimal printf-style function in C.
The value of this project is not copying the standard library. The useful part is understanding what formatted output actually does behind a simple call like:
printf("Hello %s, you have %d messages\n", name, count);
A normal user sees text.
A printf implementation needs to:
read the format string
detect conversion markers
pull the correct argument type
convert values into printable characters
write the result to output
track the number of printed characters
handle errors and edge cases
That makes it a useful low-level C and parsing project.
Project Context
The project was built in C as a fundamentals project focused on output formatting and variadic functions.
It belongs in the portfolio as a systems-programming note, not as a generic training assignment.
The value is in showing understanding of:
- C function interfaces
- variadic arguments
- format parsing
- conversion dispatching
- numeric conversion
- character output
- return values
- edge-case behavior
- small-library design
It complements the minimal Unix shell note because both projects deal with C, Linux behavior, and careful handling of low-level details.
What This Project Is Meant To Prove
- simple library functions hide real parsing logic
- variadic functions require strict type discipline
- format strings are small languages
- every conversion specifier needs clear behavior
- integer output is a conversion problem, not only printing digits
- return values matter
- edge cases define whether the implementation is reliable
- C projects need careful separation between parsing, conversion, and output
Stack and Tools Used
Language and Runtime
- C
- GCC
- standard C library direction
- POSIX-style output direction
- Linux terminal output
C Concepts
stdarg.hva_listva_startva_argva_end- pointers
- strings
- integer conversion
- recursion or iterative digit output
- function pointers or dispatch tables direction
- error handling
Formatting Concepts
- conversion specifiers
- literal characters
- percent escaping
- return count
- signed integers
- unsigned integers
- characters
- strings
- hexadecimal direction if implemented
- flags/width/precision direction if extended
Intended Build
The intended build is a minimal printf-style function that can print common formatted values.
A first strong version should support:
%c character
%s string
%d signed decimal integer
%i signed decimal integer
%u unsigned decimal integer
%% literal percent sign
Possible extended conversions:
%x lowercase hexadecimal
%X uppercase hexadecimal
%o octal
%p pointer address
A more advanced version can support:
field width
precision
left alignment
zero padding
plus sign
space flag
hash flag
length modifiers
The first version should prioritize correctness over too many features.
Basic Mental Model
A printf-style function is mostly a loop over the format string.
Simplified flow:
for each character in format string:
if normal character:
print it
increment count
if '%' is found:
read next format specifier
fetch matching argument
convert argument to output
increment count
The hard part is making this reliable when input is incomplete, invalid, or uses different argument types.
Variadic Arguments
printf accepts a variable number of arguments.
That means the function cannot know argument types from the function signature alone.
A custom implementation uses stdarg.h.
Conceptual flow:
va_list args;
va_start(args, format);
value = va_arg(args, expected_type);
va_end(args);
The format string tells the function which type to pull next.
This creates an important rule:
The parser and argument extraction must agree.
If the format says %d, the implementation should pull an int.
If the format says %s, it should pull a char *.
Pulling the wrong type causes undefined behavior.
Format String Parsing
The format string is the instruction set.
Example:
"Name: %s | Score: %d%%"
The parser sees:
literal text: "Name: "
specifier: %s
literal text: " | Score: "
specifier: %d
specifier: %%
The parser should distinguish:
- normal characters
- valid conversion specifiers
- literal percent signs
- invalid or incomplete specifiers
- optional flags/width/precision if implemented
Even a small parser needs predictable behavior.
Conversion Dispatch
A clean implementation should avoid putting every conversion into one massive function.
A better structure:
format parser
↓
specifier dispatcher
↓
conversion handler
↓
output function
Example handlers:
print_char
print_string
print_signed_int
print_unsigned_int
print_hex
print_pointer
This makes the code easier to extend and debug.
Character Output
At the lowest level, formatted output becomes character output.
A minimal implementation might write using:
write(1, &c, 1);
or another output helper.
A helper function can track the printed count:
print one character
if success:
count += 1
else:
mark error
This is useful because printf returns the number of characters printed.
Return Value
The return value matters.
Standard printf returns the number of characters printed, or a negative value on error.
A custom implementation should track printed characters carefully.
Example:
len = _printf("Hi %s", "Ochy");
Expected count:
length of "Hi Ochy"
The count should include:
- literal characters
- converted characters
- percent signs printed by
%% - newlines
- spaces
It should not count characters that were not successfully written.
String Conversion
The %s conversion prints a string.
Important cases:
- normal string
- empty string
NULLpointer direction- long string
- precision if implemented
A common safe behavior is to print something like:
(null)
when the string pointer is NULL, depending on the chosen requirements.
The key is to define the behavior clearly and test it.
Character Conversion
The %c conversion prints a single character.
Even though it prints a char, variadic argument promotion means it is usually retrieved as an int.
Important detail:
char arguments are promoted to int in variadic functions
So the implementation should use:
va_arg(args, int)
then cast or output as a character.
Signed Integer Conversion
The %d and %i conversions print signed integers.
Important cases:
- zero
- positive numbers
- negative numbers
- minimum integer value
- large numbers
- return count
The hardest edge case is often:
INT_MIN
because its absolute value may not fit in a signed int.
A safe implementation needs to handle that without overflow.
Unsigned Integer Conversion
The %u conversion prints unsigned decimal integers.
This avoids negative signs, but still needs digit extraction.
Conceptual flow:
if value is 0:
print '0'
else:
repeatedly divide by 10
collect digits
print digits in correct order
The implementation can use recursion, a temporary buffer, or reverse digit storage.
Hexadecimal Conversion
If %x and %X are implemented, the function converts numbers to base 16.
Digits:
0123456789abcdef
0123456789ABCDEF
This teaches a general concept:
number conversion = repeated division by base
Decimal, octal, and hexadecimal are all variations of the same logic.
Pointer Conversion
If %p is implemented, the function prints an address-like value.
Usually the output direction is:
0x...
This requires:
- retrieving a pointer
- casting to an integer type suitable for addresses
- converting to hexadecimal
- handling null pointer direction
Pointer formatting is a good extension but not necessary for the first version.
Percent Escaping
The %% sequence prints a literal percent sign.
Example:
_printf("Progress: 100%%\n");
Output:
Progress: 100%
This matters because % is normally the beginning of a conversion specifier.
The parser needs to recognize the special escape.
Invalid Specifiers
A format string may contain unsupported conversions.
Example:
_printf("Value: %q\n", value);
A minimal implementation needs a defined behavior.
Possible choices:
print '%' and the unknown specifier
ignore it
return an error
The important thing is consistency.
Undefined or random behavior makes debugging harder.
Incomplete Percent At End
A common edge case:
_printf("hello %");
The format string ends after %.
The implementation needs to decide what happens.
Possible behavior:
- return error
- print
% - ignore incomplete specifier
For a custom implementation, this should be documented and tested.
Flags, Width, and Precision Direction
A basic implementation can skip advanced formatting.
A stronger extended version can support:
- left align
+ force sign
0 zero padding
# alternate form
space sign
width
precision
Example:
printf("%08d", 42);
Output direction:
00000042
These features turn the parser from a simple specifier detector into a more serious formatting engine.
For a first version, it is better to implement fewer features correctly.
Buffer vs Direct Write
There are two common output strategies.
Direct Write
Print each character as soon as it is ready.
Advantages:
- simpler
- less memory management
- easy to understand
Disadvantages:
- many write calls
- harder to roll back on error
Buffer First
Build output in memory, then write.
Advantages:
- fewer writes
- easier to manage final output
- closer to performance direction
Disadvantages:
- more memory management
- buffer resizing
- error handling complexity
A minimal version can use direct writes.
Memory Management
A minimal printf may not need heavy dynamic allocation if it prints directly.
But memory may appear in:
- temporary digit buffers
- copied format data
- conversion helper buffers
- dynamic string handling
- advanced width/precision formatting
In C, every allocation needs a cleanup path.
For a small implementation, avoid unnecessary allocation where simple stack buffers or recursive output are enough.
Testing Checklist
Basic Output
- plain string
- empty string
- newline
- spaces
- multiple literal characters
Character
%c- null character direction if tested
- multiple characters
String
%s- empty string
NULLstring if behavior defined- long string
Integers
%d%i- zero
- positive number
- negative number
INT_MAXINT_MIN
Unsigned
%u- zero
- large unsigned value
Percent
%%- percent beside text
- multiple percent signs
Mixed Format
%s %d %c- text before and after conversions
- repeated conversions
Invalid Cases
- unsupported specifier
- trailing
% - missing arguments direction
- format string is
NULLif behavior defined
Comparing With Standard printf
A useful test strategy is comparing output and return values against standard printf for supported conversions.
Example test direction:
custom output == printf output
custom return == printf return
Only compare features that the custom implementation claims to support.
If flags or precision are not implemented, they should not be tested as supported behavior.
Common Bugs
Wrong va_arg Type
Cause:
format parser expects one type but va_arg retrieves another
Result:
undefined behavior
Fix:
match each specifier to the correct promoted type
Missing NULL Terminator Assumption
Cause:
string handler assumes every pointer is valid
Result:
segmentation fault
Fix:
define and handle NULL string behavior
Incorrect Return Count
Cause:
converted characters are printed but not counted correctly
Fix:
centralize output/count logic
INT_MIN Overflow
Cause:
trying to convert INT_MIN with normal negative-to-positive logic
Fix:
handle minimum value safely using wider type or unsigned logic
Percent Handling Broken
Cause:
parser treats %% like invalid format
Fix:
special-case %% as literal percent
Massive Function
Cause:
all parsing and conversion lives in one function
Fix:
split parser, dispatcher, and handlers
Practical Decisions
Implement fewer specifiers first
A correct %c, %s, %d, %i, %u, and %% is better than many broken conversions.
Track count in one place
Character count bugs are easier to avoid when all output goes through one helper.
Keep parser predictable
Unsupported specifiers should behave consistently.
Watch variadic promotions
char and short are promoted to int in variadic calls.
Test edge integers
0, negative values, INT_MAX, and INT_MIN reveal many bugs.
Avoid unnecessary allocation
Simple direct output can keep the first version safer.
What A Finished Version Should Show
A strong finished version should show:
- custom
_printffunction - format string parser
- variadic argument handling
- conversion dispatcher
- character output helper
- printed character count
%c%s%d%i%u%%- optional
%x,%X,%o,%p - consistent error behavior
- tests against standard
printf - README with supported specifiers
- no memory leaks for supported paths
Evidence Worth Capturing
Useful evidence for this note would include:
- source tree
_printffunction interface- parser/dispatcher code excerpt
- integer conversion helper
- test output compared with
printf - edge case tests
- return value tests
- Valgrind output if available
- README supported specifier table
- examples of unsupported behavior
Technical Assumptions
This note assumes the implementation is written in C.
It assumes the goal is to build a minimal printf-style function for learning and systems-programming fundamentals.
It also assumes the implementation supports a documented subset of standard printf, not the full standard library behavior.
Key Risks
- pulling wrong types from
va_arg - returning the wrong character count
- crashing on
NULLstrings - mishandling
INT_MIN - broken percent escaping
- unsupported format behavior not documented
- too many features added before the core works
- memory leaks from temporary buffers
- inconsistent output helpers
- comparing against standard
printffor features not implemented
Current State
This note represents a low-level C project focused on formatted output.
It is older/fundamentals-based compared with the infrastructure and business-system notes, but it still adds value because it shows:
parsing
type handling
conversion logic
output control
edge-case discipline
That makes it a good supporting systems-programming note.
What This Note Does Not Claim
This note does not claim to replace the standard C library.
It does not claim to fully implement every printf flag, width, precision, length modifier, locale behavior, or platform-specific detail.
It documents a minimal custom implementation used to understand how formatted output works internally.
Practical Takeaway
The useful lesson is:
printflooks simple because the difficult parsing and conversion work is hidden.
To rebuild even a small version, you need to understand:
- variadic arguments
- format strings
- type-specific extraction
- number-to-text conversion
- output counting
- error handling
- edge cases
- memory discipline
Framed this way, the project becomes a C systems-programming note instead of a basic training exercise.