C Basics · beginner · ~10 min
By the end of this lesson you will be able to: - Print text and values to standard output with `printf`, controlling exactly how each value appears. - Match the correct **format specifier** (`%d`, `%f`, `%s`, `%c`, `%x`, `%u`, `%ld`, and more) to the type of each argument. - Use **flags, width, and precision** (for example `%-10s`, `%08d`, `%.3f`) to align and round output. - Read the return value of `printf` to detect output errors. - Explain why `printf` is **variadic** and why a format/argument mismatch is undefined behavior. - Compile with warnings enabled (`-Wall`) so the compiler catches mismatches for you.
printf is how a C program talks. It writes formatted text to standard output (stdout) — normally your terminal, but it can also be redirected to a file or piped into another program. Almost every C program you write, from a one-line "hello" to a full application, uses printf (or its relatives) to report results, show progress, or produce data for another tool.
In the prerequisite lesson The main function you saw a program structured around int main(void) and learned that execution starts there. printf is usually the very first library function you call inside main, because it lets you see what your program is doing. To use it you must #include <stdio.h>, the standard input/output header that declares printf and the rest of the I/O family.
The key idea is the format string: the first argument to printf. It is ordinary text plus placeholders called conversion specifiers that begin with %. Each specifier says "print the next argument, formatted like this." The remaining arguments supply the values, consumed left to right. So printf("x=%d\n", 7) prints x=7 followed by a newline.
Two pieces of terminology you will see throughout C documentation:
%... construct (e.g. %.2f).printf is the classic example: it figures out how many arguments to read, and their types, only by parsing the format string at runtime.That runtime-only contract is what makes printf flexible — and also what makes mismatches dangerous, which is the theme that runs through this lesson.
Output is not a minor detail; it is often the interface of a program. Command-line tools, build systems, servers, and embedded firmware all communicate through formatted text. If the formatting is wrong, logs are unreadable, reports are misaligned, and numbers are subtly incorrect.
More importantly, printf is a place where the compiler's normal type-checking does not fully protect you. Because the function is variadic, passing the wrong type for a specifier (or forgetting an argument entirely) compiles cleanly yet produces undefined behavior at runtime: garbage numbers, crashes, or — in the worst case for the printf family — security holes. Learning to keep specifiers and arguments in lock-step, and to enable compiler warnings, is one of the earliest defensive habits a C programmer builds. The quiz example printf("%d\n", 1.5) is exactly this trap: %d expects an int, but a double was passed.
Getting printf right early pays off forever, because the same format-string rules reappear in fprintf (write to any file), sprintf/snprintf (write into a buffer), and the logging functions of nearly every C codebase.
Definition. The format string is a const char * that mixes literal text with conversion specifiers. A conversion specifier is % followed by an optional set of fields and a final conversion character that names the type.
How it works internally. printf scans the format string one character at a time. Literal characters are copied to stdout unchanged. When it hits %, it reads the rest of the specifier, pulls the next argument off the argument list, converts that argument to text according to the specifier, and writes the text out. It then resumes scanning.
Structure of a full specifier:
% [flags] [width] [.precision] [length] conversion
| | | | | |
| | | | | +-- d,i,u,f,s,c,x,p,...
| | | | +------------- l, ll, h, z (type size)
| | | +------------------------- digits after '.'
| | +------------------------------------ minimum field width
| +--------------------------------------------- -, +, 0, space, #
+--------------------------------------------------- starts every specifier
When to use which conversion character (the common ones):
| Specifier | Argument type | Prints |
|---|---|---|
%d / %i |
int |
signed decimal |
%u |
unsigned int |
unsigned decimal |
%ld |
long |
signed decimal |
%f |
double |
decimal fraction |
%e |
double |
scientific notation |
%c |
int (a char value) |
one character |
%s |
char * to a NUL-terminated string |
the string |
%x / %X |
unsigned int |
hexadecimal |
%p |
void * |
a pointer address |
%% |
(none) | a literal % |
Pitfall. To print a literal percent sign you must write %%, not %. A lone % at the end of a format string starts a specifier that has no conversion character — undefined behavior.
Knowledge check: What does
printf("50%% done\n")print, and why are there two percent signs?
Definition. A variadic function takes a fixed prefix of arguments followed by ..., meaning "zero or more additional arguments of unspecified type." printf's real prototype is int printf(const char *format, ...);.
Plain language. Normally the compiler checks every argument against the parameter's declared type. With ..., there is no declared type to check against. printf trusts you completely: it believes the format string and reads arguments of exactly the types the specifiers imply.
How it works internally. Variadic arguments are passed on the stack/registers with default argument promotions applied: char and short become int, and float becomes double. That is why you write %f for a float argument — by the time printf sees it, the value is already a double. printf then walks the argument area using the sizes the specifiers tell it to expect. If a specifier says "read 4 bytes as an int" but you actually passed an 8-byte double, printf reads the wrong bytes and everything after it is misaligned.
You wrote: printf("%d\n", 1.5);
Argument area in memory: [ 8 bytes: the double 1.5 ]
%d tells printf to read: [4 bytes][ ignored / misread ]
Result: a meaningless integer, not 1 or 2.
When NOT to trust it. Never assume the compiler will catch a mismatch on its own. It only does so when you enable format checking (e.g. -Wall, which turns on -Wformat).
Knowledge check (predict the output): Will
printf("%d\n", 1.5);reliably print1,2, or something unpredictable? Explain in one sentence.
Definition. These optional fields between % and the conversion character control alignment, padding, sign, and rounding.
%5d prints 42.- flag — left-justify within the width. %-5d prints 42 .0 flag — pad numbers with leading zeros instead of spaces. %05d prints 00042.+ flag — always show a sign for numbers. %+d prints +42..n) — for %f, the number of digits after the decimal point (%.2f → 3.14); for %s, the maximum characters to print; for integers, the minimum digits.Why it matters. Width and precision turn raw values into readable, aligned tables, and %.2f is how you avoid printing 3.1415926535 when you wanted a price.
Pitfall. %.2f rounds, it does not truncate. %.2f of 2.005 may print 2.00 or 2.01 depending on the exact binary value — floating point is not exact decimal.
Knowledge check (find the bug): A learner wants leading zeros and writes
printf("%5.0d\n", 42);expecting00042. Which field should they have used instead?
Always include the header and end interactive output lines with \n so the line is flushed and visible:
#include <stdio.h> /* declares printf */
/* literal text + specifiers, then matching arguments in order */
printf("format text %d and %s\n", an_int, a_string);
/* | |
| +-- a char* to a NUL-terminated string
+---------- an int */
printf("%-8s|%6.2f|\n", "name", 3.14159);
/* | | | |
| | | +-- precision: 2 digits after the decimal point
| | +----- width: at least 6 characters total
| +-------- width 8 for the string, '-' = left-justify
+------------ %s consumes the char* "name" */
int n = printf("hi\n"); /* return value = number of characters written, or <0 on error */
Key rules:
%% prints a literal %.printf returns an int: the count of characters printed, or a negative value if an output error occurred.printf doesprintf writes formatted text to stdout (standard output, normally your terminal).
Its first argument is a format string. This string contains placeholders that say how to print each value. Common placeholders are:
%d — an int%f — a double%s — a string (a char *)%c — a single char%x — an integer in hexadecimal (base 16)The remaining arguments fill those placeholders, in order from left to right.
printf is variadic: it accepts a variable number of arguments. It learns how many to expect, and their types, only by reading the format string at runtime.
This means the compiler does not force the arguments to match the placeholders. A wrong format/argument pair is a classic source of bugs.
Always compile with -Wall. With this flag, gcc checks the format string against the arguments you pass and warns you about mismatches.
#include <stdio.h>
int main(void) {
int age = 30;
long views = 1234567L; /* needs %ld, not %d */
double pi = 3.14159;
char grade = 'A';
const char *name = "ada"; /* a NUL-terminated C string */
unsigned int color = 0xFF8800; /* will print in hexadecimal */
/* Basic, in-order substitution */
printf("name=%s age=%d grade=%c\n", name, age, grade);
/* Precision: round pi to 2 decimal places */
printf("pi rounded = %.2f\n", pi);
/* Long integer needs the 'l' length modifier */
printf("views = %ld\n", views);
/* Width + zero padding + left/right alignment in a small table */
printf("|%-8s|%6d|\n", name, age); /* left-justify name, right-justify age */
printf("|%-8s|%06d|\n", "bob", 7); /* zero-pad the number */
/* Hexadecimal and a literal percent sign */
printf("color = #%06X (%u in decimal)\n", color, color);
printf("progress: 50%% complete\n");
/* Check the return value: number of characters written */
int written = printf("done\n");
if (written < 0) {
/* Output error (e.g. closed pipe). Report and signal failure. */
return 1;
}
return 0;
}
What it does. The program declares one value of several common types and prints each with the matching specifier, then demonstrates precision, the l length modifier for long, width/alignment for a tiny table, hexadecimal, a literal %, and reading printf's return value.
Expected output:
name=ada age=30 grade=A
pi rounded = 3.14
views = 1234567
|ada | 30|
|bob |000007|
color = #FF8800 (16746496 in decimal)
progress: 50% complete
done
Edge cases to notice. %.2f rounds 3.14159 to 3.14. %06X zero-pads the hex value to six digits and uses uppercase letters. %ld is required for long; using %d here would be a mismatch. The final printf returns 5 (four letters plus the newline), so written is positive and the program returns 0.
Walking through the key lines of the example:
#include <stdio.h> brings in the declaration of printf. Without it the compiler does not know printf's prototype and cannot apply format checking.printf("name=%s age=%d grade=%c\n", name, age, grade); — printf copies name= literally, hits %s, consumes name (a char *) and prints ada; copies age=, hits %d, consumes age and prints 30; copies grade=, hits %c, consumes grade and prints the single character A; finally \n ends the line.printf("pi rounded = %.2f\n", pi); — %.2f reads the double pi and formats it with two digits after the decimal point, rounding 3.14159 to 3.14.printf("views = %ld\n", views); — the l length modifier tells printf the argument is a long (typically 8 bytes), so it reads the right number of bytes and prints 1234567.printf("|%-8s|%6d|\n", name, age); — %-8s prints ada left-justified in a field 8 wide, padding with 5 trailing spaces; %6d prints 30 right-justified in a field 6 wide with 4 leading spaces. The | characters make the field boundaries visible.printf("|%-8s|%06d|\n", "bob", 7); — %06d pads the number 7 with leading zeros to width 6, giving 000007.printf("color = #%06X ...", color, color); — the same value is consumed twice because it appears as two arguments: first %06X prints it as six uppercase hex digits, then %u prints it as unsigned decimal.printf("progress: 50%% complete\n"); — %% emits a single literal %; there are no value arguments.int written = printf("done\n"); — printf returns the number of characters it wrote. done\n is 5 characters, so written becomes 5. The if (written < 0) guard would catch a rare output error (such as writing to a closed pipe) and return a non-zero exit code.A small trace of how printf consumes line 3's arguments:
format cursor action output so far
------------- ---------------------------- -------------
"name=" copy literal name=
"%s" take arg1 (name) -> "ada" name=ada
" age=" copy literal name=ada age=
"%d" take arg2 (age) -> 30 name=ada age=30
" grade=" copy literal name=ada age=30 grade=
"%c" take arg3 (grade)-> 'A' name=ada age=30 grade=A
"\n" copy newline (line ends)
Mistake 1 — wrong specifier for the type.
long views = 1234567L;
printf("%d\n", views); /* WRONG: %d expects int, views is long */
Why it is wrong: %d makes printf read an int-sized chunk of the argument, but a long may be larger. On many systems this prints garbage or shifts every later argument. Fix:
printf("%ld\n", views); /* correct length modifier */
Recognize it: build with -Wall and you get format '%d' expects argument of type 'int', but argument 2 has type 'long'.
Mistake 2 — type mismatch with %d and a floating value (the quiz trap).
printf("%d\n", 1.5); /* WRONG: %d expects int, 1.5 is a double */
Why it is wrong: undefined behavior. %d reads the wrong bytes; the result is unpredictable, not 1 or 2. Fix — match the specifier to the value:
printf("%f\n", 1.5); /* print as a double */
printf("%d\n", 1); /* or pass an int if that's what you meant */
Mistake 3 — too few arguments.
printf("%s scored %d\n", name); /* WRONG: %d has no matching argument */
Why it is wrong: printf still tries to read a second argument that was never passed, producing garbage or a crash. Fix: supply every argument the specifiers require.
printf("%s scored %d\n", name, score);
Mistake 4 — %s on something that is not a NUL-terminated string.
char c = 'A';
printf("%s\n", c); /* WRONG: %s wants char*, c is a single char */
Why it is wrong: printf treats the character value as an address and dereferences it, almost certainly crashing. Use %c for a single character, and reserve %s for valid char * strings.
Mistake 5 — forgetting %% for a literal percent.
printf("100% safe\n"); /* WRONG: '% ' is an invalid specifier */
printf("100%% safe\n"); /* CORRECT */
Prevent all of these by compiling with warnings on and reading the -Wformat messages before running.
Compiler warnings (your first line of defense).
gcc -Wall -Wextra prog.c -o prog. The -Wformat check (included in -Wall) reports mismatches like format '%d' expects argument of type 'int', but argument 2 has type 'double'. Treat every format warning as a bug to fix, not noise.implicit declaration of function 'printf' means you forgot #include <stdio.h>.Runtime symptoms and likely causes.
| Symptom | Likely cause |
|---|---|
| Huge or random integers | wrong specifier (e.g. %d for long or double) |
| Segmentation fault on a print | %s given a non-string / invalid pointer, or a missing argument |
% text disappears or output looks truncated |
a lone % instead of %% |
| Output appears late or out of order | line not terminated with \n; stdout is line-buffered |
| Numbers off by a tiny amount | floating-point rounding with %f precision |
Concrete steps when output is wrong:
long -> %ld, double -> %f, char* -> %s).-Wall -Wextra and resolve every format warning.\n or call fflush(stdout); to force the buffer out so you can see how far execution got.Questions to ask yourself: Did I match every specifier to a value of the right type? Did I pass exactly as many arguments as specifiers? Did I forget %%? Did I end the line with \n?
Even though this is not a security lesson, printf touches several undefined-behavior pitfalls worth treating seriously:
double where %d is expected, or a long to %d, makes printf misread the argument area. The standard places no limits on what happens — wrong values today, a crash tomorrow.%s requires a valid, NUL-terminated string. If the pointer is uninitialized, freed, or points at bytes with no \0, printf reads out of bounds until it happens to find a zero byte — an out-of-bounds read that can crash or leak adjacent memory.printf read whatever is next in the call frame. Always keep the count exact.printf("%s", user_text);, not printf(user_text);. If user_text contains % specifiers, printf will try to read arguments that do not exist — this is the classic format-string vulnerability, an undefined-behavior read (and historically an exploitable bug). The format string should always be a literal you wrote, with user data passed as %s arguments.Defensive habits: compile with -Wall -Wextra (turns on format checking), keep format strings as literals, initialize pointers before printing them, and prefer length-aware variants (snprintf) when writing into a fixed buffer instead of stdout.
Where it shows up. Command-line utilities (ls, grep, your own tools) format their results with printf. Servers and daemons write structured log lines. Embedded firmware sends diagnostic text over a serial port using a printf-style routine. Build tools and test runners print pass/fail tables aligned with width specifiers. The same format grammar drives fprintf (to a file or stderr), snprintf (into a buffer — the safe way to build strings), and the logging macros in nearly every C project.
Professional best practices:
Beginner rules
#include <stdio.h> and compile with -Wall.%.2f-style precision for money and measurements rather than dumping raw doubles.\n.Advanced habits
stderr with fprintf(stderr, ...) so they do not pollute the program's real data on stdout.snprintf (which returns the length that would have been written) to build strings safely into fixed buffers, and check that return value.%s.printf's return value in programs where a broken pipe or full disk must be detected.<inttypes.h> (e.g. PRId64) when printing fixed-width types portably.Beginner 1 — Personal card. Declare an int age, a double height in meters, and a char * name. Print one line: NAME is AGE years old and HEIGHT m tall. with the height shown to 2 decimal places. Concepts: %s, %d, %.2f. Hint: one printf call, three arguments in order.
Beginner 2 — Specifier match-up. Given long population = 8000000000L; and char initial = 'Q'; and unsigned int flags = 0x2A;, print the population in decimal, the initial as a character, and the flags in lowercase hexadecimal. Concepts: %ld, %c, %x. Hint: the population needs the l length modifier; expected hex output is 2a.
Intermediate 1 — Aligned table. Print a three-row table of product names and prices so the names are left-justified in an 12-character column and the prices are right-justified in an 8-character column with 2 decimals and a leading $. Example row: Keyboard | $19.99|. Concepts: width, - flag, %.2f. Hint: use a format like "%-12s|$%6.2f|\n".
Intermediate 2 — Return-value reporter. For each of three printf calls that print a label and a number, capture the return value and afterwards print how many characters each call wrote. Concepts: printf return value, %d. Hint: int n = printf(...); then print n later; remember the \n counts.
Challenge — Safe formatter. Write a function void show_message(const char *user_text) that prints a user-supplied string safely and then prints its length. The function must not allow user_text to act as a format string, must handle a NULL pointer by printing (no message), and must use %zu for the length from strlen. Constraints: include <string.h>; do not call printf(user_text) directly. Concepts: format-string safety, %s, %zu, NULL checks. Hint: the safe pattern is printf("%s\n", user_text);.
printf writes formatted text to stdout using a format string of literal text plus % conversion specifiers; remember #include <stdio.h>.%d (int), %ld (long), %u (unsigned), %f / %.2f (double), %c (char), %s (NUL-terminated char *), %x / %X (hex), %p (pointer), and %% for a literal percent.%-10s, %08d, %.3f) control alignment, padding, sign, and rounding.printf is variadic: it learns argument types only from the format string at runtime, so a mismatched specifier/argument is undefined behavior (the printf("%d\n", 1.5) trap).%s on a non-string, and forgetting %%.-Wall, keep specifier and argument counts in lock-step, never use untrusted data as the format string, and check the return value when output reliability matters.