int main(void)Using argc and argv[] in C for Program Arguments
What is Using argc and argv[] in C for Program Arguments? Core Concept & Direct Summary
How do `argc` and `argv[]` parse command-line parameters passed to `int main(int argc, char *argv[])` from the terminal shell?
Every C program you've written starts the same way:
But main has a second form, and it's the one that makes your programs tools rather than exercises:
With it, your program can be told what to do when it starts — which file to read, which mode to run in, how many iterations to perform — instead of hardcoding those decisions or asking for them interactively. Every command-line utility you've ever used works this way, and so does every build script that runs yours.
(Suggested image: the hero graphic — a console showing fstats -l -s Error report.txt with arrows from each token to argv[0] through argv[4], and argc = 5 labeled)
How Does Using argc and argv[] in C for Program Arguments Work? In-Depth Mechanics & Architecture
What argc and argv Actually Are
argc= argument count — how many arguments there are, including the program name. It's always at least 1 in any environment that supports this form.argv= argument vector — an array of pointers to strings, one per argument.
Read the type carefully:char *argv[] is an array of char * — an array of strings. Because it's an array parameter, it decays to char **argv. The two declarations are identical, and you'll see both:
There's also a third parameter you'll occasionally encounter for environment variables:
envp is a POSIX/Windows extension rather than standard C — standard C gives you getenv() instead. Prefer getenv("PATH") for portability.
The layout in memory:
Two structural facts worth fixing in your mind:
argv[0]exists even though it's not "your" argument. It's the program name, so a program with one real argument hasargc == 2.argv[argc]is a NULL pointer — guaranteed by the C standard. That's not an accident of implementation; it's a specified property, and it's what makes pointer-based iteration safe.
(Suggested image: the argv array drawn as a row of pointers, each pointing to a string elsewhere in memory, ending with an explicit NULL box)
The Smallest Complete Example
Run as:showargs hello "two words" 42
Output (verified):
Four things this output teaches:
argcis 4, but only three arguments were given —argv[0]is the program name. This is the single most common source of off-by-one bugs for beginners, and it's why loops start ati = 1when processing your arguments."two words"arrived as ONE argument. The shell grouped it because of the quotes, and the quotes were removed before your program saw it.argv[2]is the stringtwo words, with no quote characters in it.argv[3]is the string"42", not the number 42. Everything from the command line is text. Converting it to a number is your job (covered below).argv[argc]isNULL— printed here as the all-zero address. Sofor (char **p = argv; *p; p++)is a valid, standard-guaranteed loop, and you can passargv + 1, argc - 1to a helper to shift the view past the program name.
One caution on argv[0]: it's conventionally the program name, but it's not guaranteed to be non-NULL, and it may be a full path (C:\tools\fstats.exe or /usr/bin/fstats) rather than a bare name. Never parse argv[0] to extract the filename — and if you use it in error messages, that's fine, but don't assume it's a plain name. (Extracting a basename portably is itself non-trivial; the simplest cross-platform habit is to keep a string literal like #define PROG "fstats" for diagnostics.)
(Suggested image: the console invocation annotated — quotes highlighted and crossed out with "shell strips these", the spaces inside quotes marked "stays one argument", and argv[3]="42" flagged as text not a number)
How the Shell Decides What's an Argument
Your program never sees the raw command line. The shell (or cmd.exe, or a launcher) splits it into arguments before your main runs, and that splitting follows rules you must know to predict argc:
Three platform gotchas worth knowing before they cost you an afternoon:
1. Wildcards are expanded by the shell on POSIX, but NOT on Windows.
On Linux/macOS, prog *.txt reaches your program with the wildcard already expanded — argc might be 7 because there are 6 matching files. Your program just loops over them.
On Windows, cmd.exe and PowerShell do not expand wildcards for argument passing the same way. Your program receives the literal string *.txt and must expand it itself with FindFirstFile/FindNextFile (Windows API), because standard C has no directory-listing facility at all.
So a portable tool needs to handle both: iterate argv for files that exist, and expand glob patterns itself when it sees them.
2. Quoting rules differ. POSIX shells use backslash escapes and single/double quotes. Windows uses its own backslash-and-quote rules (and CommandLineToArgvW for the real parsing). A command line that quotes correctly in one shell may not in the other — most commonly when paths contain spaces.
3. There's an OS-imposed length limit. POSIX systems cap total arguments and environment around ARG_MAX (often ~2 MB); Windows caps the command line near 32,767 characters. Very long argument lists silently fail at the shell before your program starts.
(Suggested image: three console panels — POSIX wildcard expansion yielding many argv entries, Windows passing the literal pattern, and a quoted path with a space arriving as one argument)
Converting Arguments to Numbers — Safely
Arguments are strings. To use one as a number, you must convert it — and this is where most command-line programs have bugs.
The tempting-but-wrong way: atoi
atoi is a trap for three reasons:
atoi("abc")returns0— indistinguishable fromatoi("0"). Garbage silently becomes a valid-looking number.atoi("42abc")returns42— trailing junk is ignored, silently accepting typo'd input.- Overflow is undefined behavior —
atoi("99999999999999999999")doesn't fail; it's UB.
Use atoi only in throwaway code with input you fully control. For anything a user can type, use strtol/strtod.
The correct way: strtol with endptr
Why each check is necessary:
end == s—strtolsetsendto where parsing stopped. If it equals the start, nothing was converted.*end != '\0'— if parsing stopped before the end of the string, there's trailing garbage. This is the check that rejects"42abc"and also"0x1F"when base is 10.errno == ERANGE— the value didn't fit.strtolreturnsLONG_MAXorLONG_MINand setsERANGE. You must seterrno = 0first, becausestrtolonly ever sets it, never clears it.
Use it at the call site like this:
The behaviors worth knowing
Note the third row: strtol skips leading whitespace, so " 42" is accepted. That's usually what you want (users type stray spaces), but it's worth knowing it's happening rather than assuming strictness.
For floating point, the same pattern with strtod:
And if you'd rather not hand-roll it, sscanf works for simple cases — its return value tells you whether the conversion happened:
That's more compact than strtol, though it's stricter about trailing junk in some cases and less informative about why it failed. Either is far better than atoi.
(Suggested image: the strtol check sequence as a flow — three rejection gates ("any digits?", "trailing junk?", "in range?") leading to accept)
The Parse Loop: Options, Values, and Files
Most tools have the same shape: some options (flags and options-with-values) plus some operands (usually filenames). The classic structure is one loop over argv, starting at 1:
Four decisions embedded in that loop:
1. arg[0] == '-' && arg[1] != '\0' is the option test. The second condition matters: a bare - is not treated as an option, which is the universal convention for "read from stdin." A lone dash should fall through to the operand branch.
2. Options-with-values consume the next argument.searchTerm = argv[++i] — the ++i both reads the value and advances the loop, so the value isn't re-examined as an option. Forgetting the increment is a classic bug: the value gets treated as an operand, or as an unknown option.
3. Missing values must be caught before the increment.if (i + 1 >= argc) guards against argv[i + 1] being out of bounds — that's argv[argc], which is the guaranteed NULL, and reading a string from it would crash.
4. Unknown options are an error, not a silent skip. Reporting and exiting non-zero is what makes a tool scriptable: a typo'd flag fails loudly instead of quietly doing the wrong thing.
Adding the -- convention
Standard practice: -- means "everything after this is an operand, even if it starts with a dash." That's how you pass a file literally named -weird-name.txt:
Verified behavior:fstats -- -weird-name.txt treats -weird-name.txt as a filename and reports -weird-name.txt: No such file or directory — rather than "unknown option."
Clustered short flags (-lw)
POSIX tools commonly accept -lw as shorthand for -l -w. Hand-rolling it looks like this:
The honest alternative: on Linux and macOS there's getopt/getopt_long in <unistd.h> / <getopt.h>, which handles clustering, --long options, and option arguments for you. It is not standard C and isn't available on MSVC by default — so using it makes your tool POSIX-only. For a portable tool, the hand-rolled loop is the price of portability. (There are small third-party getopt implementations you can bundle if you want both.)
(Suggested image: the parse loop as a decision diagram — "is it --? → everything after is an operand", "does it start with -? → option", "else → operand", with the "-s consumes next arg" branch marked)
Subcommands: The git-Style Pattern
Larger tools dispatch on the first argument:
The clean way to implement it: shift the view once, then hand each handler the remainder.
The key technique: argc - 1, argv + 1. This passes a shifted slice of the arguments — so inside cmdCount, argc excludes the subcommand and argv[0] is "count". Every handler can be written exactly like a standalone main, and it never needs to know it was dispatched. (argv + 1 is plain pointer arithmetic — argv is a char **, so advancing it moves one pointer forward. That's the array-pointer equivalence from the pointers cluster, applied to argv itself.)
Each subcommand gets its own usage text and its own options — which is how git, docker, and dotnet keep their interfaces manageable.
(Suggested image: the dispatch diagram — argv split into a command token and a remaining slice, with arrows showing (argc-1, argv+1) passed to each handler)
usage(): A Function You Should Always Write
Every tool with arguments needs one, and it should take a stream so help and errors go to the right places:
Why the FILE *out parameter matters:
Help requested is not an error, so it belongs on stdout with a success exit code — that way mytool --help | less works, and scripts that check exit codes don't see a false failure. Usage shown because of a mistake belongs on stderr — so it appears even when stdout is redirected to a file, and so mytool -x > out.txt still prints the error to the console instead of burying it in out.txt.
Using argv[0] for prog in the message means the usage text always names the program the way the user invoked it.
(Suggested image: the two usage paths — help to stdout (pipeable) and errors to stderr (always visible), with exit codes 0 and 2 labeled)
Exit Codes: How Your Program Reports Success
main's return value becomes the process exit status, and that's how shells and scripts decide what to do next:
The convention that's worth following:
Why it matters practically:
Two details:EXIT_SUCCESS and EXIT_FAILURE (from <stdlib.h>) are the portable macros — they're usually 0 and 1, but using the names documents intent. And a program that reaches the end of main without a return implicitly returns 0. Write the return explicitly anyway — a function that falls off the end is a habit that causes real bugs elsewhere, and it's better not to train it.
And a design rule: if one of several files fails, still process the rest, and return non-zero at the end. Reporting partial failure is what lets a script react correctly. (In the verified run below, fstats report.txt missing.txt prints the stats for the good file, the error for the bad one, and exits 1.)
(Suggested image: the exit-code decision chart, plus the shell operators && and || shown responding to 0 vs non-zero)
📋 Complete Program: fstats — A File Statistics Tool
Everything on this page in one working tool.
Verified Behavior
fstats report.txt — no options, so all counts are shown:
(exit 0)
fstats -l -w -s Error report.txt — two flags plus a search:
(exit 0 — note only lines and words are printed; -c wasn't requested)
fstats — no operands at all:
(exit 2)
fstats -x report.txt — unknown option:
(exit 2)
fstats -s report.txt — the trap case:
(exit 2)
This one is genuinely instructive. -sconsumedreport.txt as its search term, so no files remained — and the tool reported exactly that. It's the correct behavior for the design, and it's why many real tools accept -s=TERM or require the value to be attached: with a separate-argument option, a forgotten value silently eats your filename.
fstats -- -weird-name.txt — the -- convention:
(exit 1)
Without --, that filename would have been rejected as an unknown option. With it, the leading dash is just a filename character.
fstats report.txt missing.txt — partial failure:
(exit 1)
The readable file was still processed, the unreadable one produced a perror message, and the exit code signalled that something failed — exactly the behavior a script needs.
(Suggested image: the five invocations laid out in a console panel with their exit codes annotated on the right, highlighting the "-s report.txt" trap)
Patterns Cheat Sheet
(Suggested image: the cheat sheet as a shareable two-column reference card)
Common Mistakes
- Starting the parse loop at
i = 0—argv[0]is the program name, so you'd try to parse the executable as an option or filename. - Assuming
argccounts only your arguments — it includes the program name; a one-argument program hasargc == 2. - Using
atoion user input — errors are undetectable;"abc"becomes0and overflow is undefined. Usestrtolwithendptr. - Forgetting
errno = 0beforestrtol— the function only setserrno, never clears it, so stale values cause falseERANGErejections. - Checking only
end == s(no digits) and not*end != '\0'(trailing junk) —"42abc"slips through as 42. - Reading
argv[i + 1]without bounds-checking — for the last argument that'sargv[argc], which is NULL; using it crashes. - Forgetting the
++when consuming an option's value — the value gets reprocessed as an option, or treated as an operand. - Treating a bare
-as an option — thearg[1] != '\0'test prevents this;-conventionally means stdin. - Accessing
argv[argc]as a real argument — it exists only as the NULL terminator. It's the loop bound, not an element. - Dereferencing
argv[0]assuming it's non-NULL — legal to assume a name, not legal to assume a pointer. Guard it if you use it. - Parsing
argv[0]to get the program's filename — it may be a full path or an alias; keep a literal for messages instead. - Not validating the number of arguments before use — dereferencing
argv[1]whenargc == 1is out of bounds. - Returning 0 on failure — scripts check exit codes; reporting success when a file couldn't be read breaks automation.
- Sending help text to
stderr—--helpshould go tostdoutso it can be piped, with exit code 0. - Sending error text to
stdout— it vanishes into a redirect; errors belong onstderr. - Assuming wildcards are expanded — POSIX shells expand
*.txt; Windows passes the literal pattern and you must expand it yourself. - Expecting one
argventry per word — quoting and escaping change the split before your program ever sees it. - Modifying
argvstrings while also using them asconst char *keys — legal in C (they're modifiable), but confusing if the same string is stored in several places. - Using
getoptand expecting portability — it's POSIX, not standard C, and absent from MSVC by default. - Forgetting the OS argument-length limit — huge argument lists fail at the shell, before
mainruns.
(Suggested image: "20 argc/argv Mistakes" checklist — with #1, #3, and #6 highlighted as the most common, and #6 flagged as a crash)
Final Thoughts
argc and argv are the smallest possible interface between your program and everything that runs it — and they're deceptively simple. The whole mechanism is:
argccounts the arguments, including the program nameargvis an array of strings,argv[0]is the program name, andargv[argc] == NULLis guaranteed- Every argument is text, so converting and validating them is your job
- The shell splits the line first, so quoting and wildcard rules (and their platform differences) determine what you actually receive
The habits that turn a script-friendly tool from an exercise:
- Loop from
i = 1, and usearg[0] == '-' && arg[1] != '\0'to spot options - Validate
argcbefore indexing — and bounds-check before consuming an option's value - Convert with
strtol/strtodand all three checks, neveratoi - Report errors on
stderrwith a non-zero exit code; send--helptostdoutwith 0 - Support
--so filenames can't be mistaken for options - Keep processing what you can, and let the exit code report partial failure
Follow those and your C programs stop being things you run and start being things you use — composable with pipes, usable in build scripts, and consistent with the conventions every developer already expects from a command-line tool.
Runnable C Code Example & Practical Implementation
#include <stdio.h>
#include <stdlib.h>
int main(int argc, char *argv[]) {
printf("Total arguments passed (argc): %d\
", argc);
printf("Program binary name (argv[0]): %s\
", argv[0]);
for (int i = 1; i < argc; i++) {
printf(" Argument [%d]: %s\
", i, argv[i]);
}
return 0;
}Key Takeaways, Best Practices & Memory Safety Rules
argc >= expected_count before accessing argv[1], argv[2] to prevent segmentation faults from missing arguments.Frequently Asked Questions & Technical Interview Insights
Technical Interview & Viva Questions
Frequently asked C programming interview questionsQ: What is guaranteed to be stored in `argv[argc]` in standard C?
Q: Why should `strtol()` be preferred over `atoi()` for parsing numbers from `argv`?
Related Tools, Tutorials & Authoritative References
Recommended Interactive Tools
- Online C Compiler & Runner — Interactive online GCC sandbox to execute C code in real time.
- Online C Code Editor & IDE — Multi-file C programming environment with code formatting.
- C++ Programming Tutorial — Transition from procedural C to Object-Oriented C++ with STL.
- JavaScript Programming Tutorial — Master modern JavaScript fundamentals, DOM APIs, and async patterns.
Authoritative Standards & Citations
- ISO/IEC 9899 (C Language Standard) — Official international C specification by ISO/IEC JTC 1/SC 22/WG 14.
- GNU GCC Documentation — Official reference manual for the GNU Compiler Collection and C libraries.
- The C Programming Language (K&R) — Definitive classic work by Brian Kernighan & Dennis Ritchie at Bell Labs.
- C Reference Documentation (cppreference.com) — Verified reference covering C89 through C23 language features.