Arrays & Strings · beginner · ~8 min
- Explain what `argc` and `argv` are and how the operating system hands them to your program at startup. - Read individual arguments from `argv` safely, knowing that `argv[0]` is the program name and real arguments begin at index 1. - Loop over arguments correctly using the `argc` count and the `NULL` terminator at `argv[argc]`. - Compare arguments to known options (like `--help` or `-o`) with `strcmp`, and read an option's value only after a bounds check. - Convert string arguments to numbers with `strtol` and detect invalid input. - Recognize and avoid the most common out-of-bounds bug: reading `argv[i+1]` without confirming it exists.
Almost every command-line program you have ever used — ls -l, gcc main.c -o app, grep error log.txt — receives instructions from the words you type after the program's name. Those words are the program's command-line arguments. In C, your program receives them through the two parameters of main: argc and argv.
This builds directly on two things you already know. From The main function you know that main is where execution begins; here you learn the full signature int main(int argc, char **argv) and what those parameters carry. From C strings you know that text in C is a char * pointing at a sequence of characters ending in a '\0' byte; argv is simply an array of such strings, one per word the user typed.
When you run ./greet Alice 42, the shell splits your line into separate words, then launches your program and delivers those words to main. Your program sees three arguments: "./greet", "Alice", and "42". Reading them correctly — and never reading one that isn't there — is the foundation of every configurable, scriptable C tool.
In terminology: argc is the argument count (an int), and argv is the argument vector (an array of C strings). "Vector" here is the old C word for a contiguous array. Together they describe both how many arguments arrived and what each one says.
Command-line arguments are how programs become reusable instead of hard-coded. A backup script that always copies the same folder is useless; one that takes the folder as an argument works for everyone. Compilers, package managers, container runtimes, build systems, and the entire Unix toolchain are configured through argv.
There is also a robustness and safety angle. Arguments come from outside your program — from a user, a shell script, or another program. That makes them untrusted input. A program that reads argv[i+1] without checking whether it exists will read past the end of the array, causing undefined behavior and possibly a crash or a security bug. Treating argv as input you must validate is the same defensive mindset you will apply to files, network data, and environment variables later. Getting argv right teaches the habit of bounds-checking before you touch memory you do not control.
argc — the argument countDefinition. argc is an int telling you how many command-line arguments were passed, including the program name itself.
Plain language. If you run ./greet Alice 42, then argc is 3: the program name plus two arguments. If you run ./greet with nothing after it, argc is 1.
How it works internally. When the shell launches your program, the operating system loader counts the words it was given and stores that count where main can find it. argc is always at least 1 on a normal launch, because the program name is always present.
When to use / when not. Use argc as the upper bound of every loop over arguments and as the guard before any argv[k] access. Never assume a specific count — always check.
Pitfall. Beginners often expect argc to count only user arguments. It counts the program name too, so ./greet alone gives argc == 1, not 0.
argv — the argument vectorDefinition. argv is an array of pointers to C strings. Each element argv[i] is a char * pointing at one null-terminated argument string.
Plain language. Think of a row of labeled boxes. Box 0 holds the program name; box 1, box 2, ... hold the user's words. Each box points to text stored elsewhere.
How it works internally — memory layout. argv itself is a pointer to the first box. The boxes are laid out contiguously, and the slot just past the last argument holds NULL.
Command: ./greet Alice 42
argc = 3
argv ──▶ +----------+
| argv[0] | ─────▶ "./greet\0"
+----------+
| argv[1] | ─────▶ "Alice\0"
+----------+
| argv[2] | ─────▶ "42\0"
+----------+
| argv[3] | ─────▶ NULL (always, by convention)
+----------+
When to use / when not. Read from argv[1] upward to get user input. Do not write into argv strings unless you know you may modify them; treat them as input.
Pitfall. Forgetting that argv[0] is the program name and accidentally processing it as data.
Knowledge check. For the command
./tool -v file.txt, what areargc,argv[0],argv[1], andargv[2]?
char **argv vs char *argv[]Definition. char **argv and char *argv[] mean exactly the same thing in a function parameter: a pointer to a pointer to char.
Plain language. argv points to the first element of an array, and each element is itself a pointer to characters. The [] form looks like an array but, as a parameter, it decays to a pointer — so both spellings compile to identical code.
When to use. Pick whichever reads more clearly to you and your team. char *argv[] signals "array of strings" to many readers; char **argv is more literal.
Pitfall. Believing char *argv[] lets you use sizeof to find the array length. It cannot — inside main, argv is a pointer, so sizeof argv is the size of a pointer, not the array. Use argc for the length.
NULL terminator at argv[argc]Definition. The standard guarantees argv[argc] is a null pointer.
Plain language. Just as a C string ends with '\0', the argv array ends with NULL. This gives you a second, equivalent way to know where the arguments stop.
How it works. You can loop using the count (for i in 1..argc) or by walking pointers until you hit NULL:
Walk until NULL:
p = argv; // p points at argv[0]
while (*p != NULL) // stop when the slot holds NULL
p++;
When to use / when not. The count-based loop is clearest and is what most code uses. The NULL-walk is handy when you pass argv-style arrays to other functions (like the exec family) that expect a NULL-terminated list.
Pitfall. Relying on NULL while also miscounting — both methods must agree; do not mix a manual index past argc with a NULL check halfway.
Knowledge check (predict the output). If
argc == 1, what does the loopfor (int i = 1; i < argc; i++) printf("%s\n", argv[i]);print?
Definition. Reading the value that follows a flag (e.g. the filename after -o) means accessing argv[i+1], which only exists if i + 1 < argc.
Plain language. If the user types -o as the very last word and forgets the filename, then argv[i+1] is actually argv[argc], which is NULL — and dereferencing it (or going further) is a bug. Always check first.
./copy -o <-- value missing!
i = 1, argc = 2
argv[i+1] == argv[2] == NULL
=> must detect, must not dereference
Knowledge check (find the bug). What is wrong with
if (strcmp(argv[i], "-o") == 0) out = argv[i + 1];and how would you fix it?
The full, standard signature of main for an argument-aware program:
#include <stdio.h> /* printf */
#include <string.h> /* strcmp */
/* argc: how many arguments (program name + user args)
argv: array of C strings; argv[argc] == NULL */
int main(int argc, char **argv) {
/* argv[0] -> program name
argv[1..argc-1] -> user arguments
Loop from 1 to skip the program name. */
for (int i = 1; i < argc; i++) {
if (strcmp(argv[i], "--help") == 0) { /* exact-match a flag */
puts("usage: prog [--help]");
}
}
return 0;
}
Key points: compare strings with strcmp (it returns 0 on a match — not == on the pointers, which compares addresses). The empty-argument case (argc == 1) is handled automatically because the loop body never runs. char **argv and char *argv[] are interchangeable here.
When your program starts, C passes the command-line arguments to main:
int main(int argc, char **argv)
argc ("argument count") is how many arguments were given, including the program name itself.argv ("argument vector") is an array of C strings (one string per argument).argv[0] is the program name (the path used to run it), not a user argument.argv[1], argv[2], ... are the actual arguments the user typed.argv[argc] is always NULL by convention, marking the end of the array.Because argv[0] is the program name, start your loop at i = 1 to process only user arguments.
For options such as -v or --help, you have two choices:
getopt (POSIX), which parses options for you.#include <stdio.h>
#include <stdlib.h> /* strtol, EXIT_FAILURE */
#include <string.h> /* strcmp */
#include <errno.h> /* errno */
#include <limits.h> /* INT_MIN, INT_MAX */
/* A tiny CLI: ./greet [--help] [-n COUNT] NAME
Prints "Hello, NAME!" COUNT times (default 1). */
int main(int argc, char **argv) {
long count = 1; /* default repeat count */
const char *name = NULL; /* the required NAME argument */
for (int i = 1; i < argc; i++) { /* start at 1: skip program name */
if (strcmp(argv[i], "--help") == 0) {
printf("usage: %s [--help] [-n COUNT] NAME\n", argv[0]);
return 0;
} else if (strcmp(argv[i], "-n") == 0) {
if (i + 1 >= argc) { /* bounds check: is there a value? */
fprintf(stderr, "error: -n needs a number\n");
return EXIT_FAILURE;
}
char *end;
errno = 0;
count = strtol(argv[i + 1], &end, 10); /* parse base-10 int */
if (*end != '\0' || errno != 0 || count < 1) {
fprintf(stderr, "error: invalid count '%s'\n", argv[i + 1]);
return EXIT_FAILURE;
}
i++; /* consume the value we just read */
} else if (argv[i][0] == '-') { /* unknown option */
fprintf(stderr, "error: unknown option '%s'\n", argv[i]);
return EXIT_FAILURE;
} else {
name = argv[i]; /* a non-option word: the NAME */
}
}
if (name == NULL) { /* required argument missing */
fprintf(stderr, "error: NAME is required\n");
return EXIT_FAILURE;
}
for (long k = 0; k < count; k++) {
printf("Hello, %s!\n", name);
}
return 0;
}
What it does. This program parses three kinds of input: a --help flag that prints usage and exits, a -n COUNT option that takes a number, and one positional NAME. It validates everything before using it.
Expected output. Running ./greet -n 2 Alice prints:
Hello, Alice!
Hello, Alice!
Running ./greet (no arguments) prints error: NAME is required to standard error and exits with a non-zero status. Running ./greet -n (value missing) prints error: -n needs a number.
Edge cases handled. Missing value after -n (bounds check), non-numeric or out-of-range count (strtol + errno + the end pointer), zero/negative counts (count < 1), unknown flags, and a missing required NAME. No memory is allocated, so there is nothing to free; argv strings are owned by the runtime and we only read them.
stdio.h), strtol/EXIT_FAILURE (stdlib.h), strcmp (string.h), and the tools for safe numeric parsing (errno.h, limits.h).count and name hold the parsed configuration, initialized to sane defaults so the program behaves predictably even before parsing.i = 1, skipping argv[0] (the program name).strcmp(argv[i], "--help") == 0 matches the literal flag; strcmp returns 0 only when the strings are equal character-for-character.-n, the guard i + 1 >= argc runs before touching argv[i + 1]. If the value is missing, we report an error and return rather than read out of bounds.strtol(argv[i + 1], &end, 10) converts the next argument to a long. end is set to the first character it could not convert.*end != '\0' catches trailing junk like 12x; errno != 0 catches overflow; count < 1 rejects zero and negatives.i++ consumes the value so the loop does not try to parse it again as a separate argument.argv[i][0] == '-' treats any other dash-prefixed word as an unknown option — a defensive default that rejects typos instead of silently ignoring them.else branch captures the first non-option word as name.name == NULL means no positional argument was given, which is an error.count times. return 0 signals success.Trace for ./greet -n 2 Alice (argc = 4):
i argv[i] action count name
1 "-n" see -n; check i+1<argc (ok) 1 NULL
"2" strtol -> 2, valid; i++ to 2 2 NULL
3 "Alice" not an option -> name = "Alice" 2 "Alice"
-- end name set, count=2 -> print x2
Mistake 1 — Reading an option's value without a bounds check.
/* WRONG: if -o is the last argument, argv[i+1] is NULL/out of range */
if (strcmp(argv[i], "-o") == 0) out = argv[i + 1];
Why it is wrong: when the user runs ./prog -o with nothing after, argv[i + 1] is argv[argc], which is NULL. Using it as a string is undefined behavior. Corrected:
if (strcmp(argv[i], "-o") == 0) {
if (i + 1 >= argc) { fprintf(stderr, "error: -o needs a value\n"); return 1; }
out = argv[++i]; /* read and consume the value */
}
Recognize it by running your program with the flag as the final word; prevent it by always guarding argv[i+1].
Mistake 2 — Comparing strings with ==.
if (argv[i] == "--help") { ... } /* WRONG: compares pointers, not text */
This compares two addresses, which are almost never equal, so the branch silently never runs. Use strcmp(argv[i], "--help") == 0.
Mistake 3 — Starting the loop at i = 0.
Starting at 0 processes the program name as if it were user data, so ./greet greet style bugs appear. Start at i = 1.
Mistake 4 — Trusting atoi for numeric arguments.
int n = atoi(argv[1]); /* WRONG: returns 0 for "abc" with no error signal */
atoi cannot distinguish the input "0" from invalid text and silently ignores overflow. Use strtol with the end pointer and errno, as in the lesson's example.
Compiler errors.
implicit declaration of function 'strcmp' / 'strtol' → you forgot #include <string.h> / #include <stdlib.h>.comparison between pointer and integer near a string compare → you wrote argv[i] == "..." instead of strcmp(...) == 0.Runtime errors.
argv[i+1] past the end; add the i + 1 < argc guard. Reproduce it deliberately by running ./prog -o (value omitted).argv[1] without checking argc >= 2 first.Logic errors.
== instead of strcmp, or there is a typo / different case in the literal.0 → you used atoi; switch to strtol and inspect the end pointer.Concrete steps. Print what you actually received at the top of main:
for (int i = 0; i < argc; i++) printf("argv[%d] = '%s'\n", i, argv[i]);
Then run under a memory checker — gcc -g -fsanitize=address prog.c or valgrind ./prog ... — which pinpoints the exact out-of-bounds read.
Questions to ask when it doesn't work. Did I start at index 1? Is argc what I expect for this command line? Did I guard every argv[i+1]? Am I comparing strings with strcmp, not ==? Did I validate numbers with strtol?
argv is the first untrusted input most C programs touch, so the same discipline you will use for files and network data starts here.
argv[k] is valid only for 0 <= k <= argc (and argv[argc] is NULL, which you may compare but must not dereference). The classic bug is argv[i+1] after a flag; guard it with i + 1 < argc.argv live for the entire run of the program, so it is safe to store an argv[i] pointer (like name = argv[i]) and use it later in main. Do not, however, assume they exist after main returns.count = 1, name = NULL) so that a missing argument leaves a known value you can test, rather than reading an indeterminate one.strtol sets errno to ERANGE on overflow and clamps to LONG_MIN/LONG_MAX; check errno and range before using the value (e.g. before assigning to an int).free to do — but the habit of pairing every resource with cleanup carries into the next lessons.Concrete uses. Every Unix command parses argv: cp src dst, gcc -O2 -o app main.c, curl -X POST https://localhost/api. Build tools, test runners, container CLIs (docker run -p 8080:80 image), and git itself (git commit -m "msg") are all argv parsers underneath. Scripts chain these together, so robust argument handling is what makes a C program scriptable and automatable.
Professional best practices.
Beginner rules.
i = 1; never treat argv[0] as data.strcmp(...) == 0.argv[i+1] before every option-value read.strtol, not atoi.stderr and return a non-zero status on failure, so callers and scripts can detect problems.Advanced habits.
getopt/getopt_long (POSIX) for non-trivial option sets; it handles -abc bundling, --key=value, and -- end-of-options correctly so you don't reinvent it.--help/usage message and exit 0 for it.Beginner 1 — Echo the arguments. Write a program that prints each user argument on its own line, numbered starting at 1, and skips the program name. For ./echoer a b c, print:
1: a
2: b
3: c
Requirements: loop from i = 1 to argc - 1; use printf. Constraint: no fixed-size assumptions about how many arguments there are. Hint: the loop body never runs when argc == 1. Concepts: argc, argv, looping.
Beginner 2 — Require exactly one argument. Write a program that expects exactly one user argument (a name) and prints Hello, NAME!. If the user gives zero or more than one, print a usage message to stderr and return a non-zero status. Hint: check argc != 2. Concepts: argc validation, stderr, exit status.
Intermediate 1 — Count flags. Implement logic that counts how many user arguments begin with '-' (treat these as flags) and prints the total. For ./prog -a file -b, print 2. Requirements: skip argv[0]; inspect argv[i][0]. Edge case: a lone "-" counts as starting with '-'. Concepts: indexing into a string, the relationship between argc and the loop bound.
Intermediate 2 — Read a flag's value safely. Implement a search that finds -o among the arguments and prints the value that follows it; if -o is missing or has no value, print an error to stderr and return non-zero. For ./prog -o out.txt, print out.txt. Requirements: guard i + 1 < argc before reading the value. Hint: return as soon as you find it. Concepts: strcmp, bounds-checking, option values.
Challenge — Mini argument parser. Build a program ./calc -n COUNT --label TEXT VALUE that accepts a repeat count (-n, an integer), a label (--label, a string), and one positional integer VALUE. Print LABEL: VALUE exactly COUNT times. Validate that COUNT and VALUE are valid integers with strtol, that required option values are present, and reject unknown flags. Provide a --help message. Constraints: default COUNT to 1 and label to "value" if omitted; exit non-zero on any error. Hint: model it on the lesson's example and consume each option's value with i++. Concepts: full argv loop, strcmp, bounds-checking, strtol validation, defaults, error reporting.
int main(int argc, char **argv) receives the argument count and an array of argument strings; char **argv and char *argv[] mean the same thing.argc counts everything including the program name, so ./prog alone gives argc == 1.argv[0] is the program name; real arguments start at argv[1]; and argv[argc] is always NULL. Loop from i = 1.strcmp(argv[i], "...") == 0, never with == (which compares pointers).i + 1 < argc before reading an option's value at argv[i+1] — the most common out-of-bounds bug in argv code.strtol (checking the end pointer and errno), not atoi.stderr, and return a non-zero status on failure.