C / STANDARD LIBRARY TOUR
stdlib.h: exit, atoi and the conversions to prefer
Shut a C program down cleanly with exit and its atexit handlers, and convert strings to numbers with strtol instead of atoi so bad input is detected.
What you will learn
- Call exit(EXIT_FAILURE) knowing it runs atexit handlers LIFO, then flushes stdio
- Reject bad input with the four strtol checks: end==s, *end, ERANGE, int range
- Chain strtol calls through endptr to read several numbers out of one string
- Explain why atoi cannot report failure and why overflow in atoi is undefined
Understanding stdlib.h: exit, atoi and the conversions to prefer
exit ends a program through the normal shutdown path defined in stdlib.h: it calls every function registered with atexit in reverse order of registration, flushes and closes all open streams, removes files created by tmpfile, and then hands the status back to the host. Returning a value from main is defined to do the same thing, so inside main, return EXIT_FAILURE; and exit(EXIT_FAILURE); are interchangeable; the difference is that exit works from any depth of the call stack, which is what makes it useful in a validation helper that has to abandon the run. Only 0, EXIT_SUCCESS and EXIT_FAILURE are portable status values, and _Exit and abort deliberately skip that cleanup, so anything still sitting in a stdio buffer is simply lost.
atoi has exactly one channel for its answer, an int, and no room left over to say "that was not a number". atoi("abc"), atoi("") and atoi("0") all return 0, so the moment the string comes from a user you can no longer tell a genuine zero from a failed parse. Worse, when the digits do not fit in an int the standard calls the behaviour undefined rather than promising a clamped result, so an oversized argument is not merely wrong, it is outside what the language guarantees. atol and atoll inherit both defects.
strtol keeps the two facts atoi throws away: it stores in your char *end the first character it did not consume, and it sets errno to ERANGE while returning LONG_MAX or LONG_MIN when the value does not fit. That is why the usable idiom is four checks: end == s means nothing was converted, *end != '\0' means trailing junk, ERANGE means out of range for long, and a final comparison against INT_MIN and INT_MAX before casting down to int. You must write errno = 0 yourself before the call, because strtol is not required to reset errno on success. The base argument picks the numeral system, base 0 means "decide from the prefix", and strtod, strtoul and strtoll offer the same contract for other target types.
<errno.h>
<limits.h>
<stdio.h>
<stdlib.h>
/* 0 on success, -1 if s is not entirely a valid int */
static int parse_int(const char *s, int *out)
{
char *end;
long v;
errno = 0;
v = strtol(s, &end, 10);
if (end == s) return -1; /* nothing converted */
if (*end != '\0') return -1; /* trailing junk */
if (errno == ERANGE) return -1; /* too big for long */
if (v < INT_MIN || v > INT_MAX) return -1; /* too big for int */
*out = (int)v;
return 0;
}
int main(void)
{
const char *inputs[] = { "42", " -7", "0", "12abc", "abc", "" };
size_t i;
char *end;
long big;
int v;
for (i = 0; i < sizeof inputs / sizeof inputs[0]; i++) {
if (parse_int(inputs[i], &v) == 0)
printf("\"%s\": atoi=%d strtol=%d\n", inputs[i], atoi(inputs[i]), v);
else
printf("\"%s\": atoi=%d strtol=rejected\n", inputs[i], atoi(inputs[i]));
}
/* atoi on this string would be undefined behaviour, so only strtol is asked */
errno = 0;
big = strtol("99999999999999999999", &end, 10);
printf("overflow: strtol=%ld ERANGE=%d\n", big, errno == ERANGE);
if (fflush(stdout) != 0) {
perror("stdout");
exit(EXIT_FAILURE);
}
exit(EXIT_SUCCESS);
}
Prefer a conversion that reports both where it stopped and whether the value fit, because atoi's lone int return cannot distinguish a valid 0 from garbage or from overflow.
Worked examples
What exit does before the process dies
Shows atexit handlers running in reverse registration order and stdio being flushed by exit.
<stdio.h>
<stdlib.h>
static void say_bye(void) { puts("atexit: say_bye"); }
static void close_db(void) { puts("atexit: close_db"); }
int main(void)
{
atexit(say_bye);
atexit(close_db);
printf("no newline here, so this sits in the buffer");
exit(EXIT_SUCCESS);
}
Example explained
Line 1say_bye is registered first and close_db second, so exit calls them last-in-first-out and close_db prints first.
Line 2The printf has no newline, so on a terminal that text is still in stdout's buffer when exit is reached.
Line 3exit flushes stdout as part of its shutdown, which is why the buffered text appears at all; _Exit would discard it.
Line 4return EXIT_SUCCESS; here would print exactly the same thing, because returning from main is defined to call exit.
Walking a string with endptr and base 0
Uses the end pointer to read several numbers from one string and to detect where parsing became impossible.
<stdio.h>
<stdlib.h>
int main(void)
{
const char *p = "10, 0x1f, 077, -3, oops, 8";
long sum = 0;
int count = 0;
while (*p != '\0') {
char *end;
long v = strtol(p, &end, 0); /* base 0: 0x is hex, leading 0 is octal */
if (end == p) {
printf("stuck at \"%s\"\n", p);
break;
}
printf("value %ld, rest \"%s\"\n", v, end);
sum += v;
count++;
p = end;
while (*p == ',' || *p == ' ')
p++;
}
printf("%d values, sum = %ld\n", count, sum);
return 0;
}
Example explained
Line 1Base 0 makes 0x1f convert as hexadecimal 31 and 077 as octal 63; with base 10 the same text would yield 0 and 77.
Line 2end always points at the first character not consumed, so p = end resumes exactly where the previous conversion stopped.
Line 3end == p is the only signal that no digits were found; it fires on "oops" and stops the loop instead of adding a bogus 0.
Line 4The small skip loop, not strtol, is what steps over the commas and spaces between numbers.
strtoul quietly accepts a negative number
Demonstrates that a minus sign is converted and negated in the unsigned return type rather than reported as an error.
<errno.h>
<stdio.h>
<stdlib.h>
int main(void)
{
char *end;
unsigned long u;
long l;
errno = 0;
u = strtoul("-1", &end, 10);
printf("strtoul(\"-1\") = %lu ERANGE=%d rest=\"%s\"\n",
u, errno == ERANGE, end);
errno = 0;
l = strtol("-1", &end, 10);
printf("strtol (\"-1\") = %ld ERANGE=%d rest=\"%s\"\n",
l, errno == ERANGE, end);
return 0;
}
Example explained
Line 1strtoul converts the digits and then negates the result in unsigned long, so "-1" becomes ULONG_MAX.
Line 2errno stays 0 because that wrap is a defined conversion, not a range error, so an ERANGE check cannot catch it.
Line 3end is at the terminator in both calls, so the "whole string consumed" test passes too and hides nothing.
Line 4For a size or a count, parse with strtol into a long and reject negatives yourself before converting to unsigned.
Important notes
_Exit and abort skip both the atexit handlers and the stdio flush, so unterminated printf output can disappear; calling exit from inside an atexit handler is undefined behaviour.
The host keeps only the low 8 bits of the status, so exit(256) looks like success and exit(-1) shows up as 255; stick to 0, EXIT_SUCCESS and EXIT_FAILURE. The numbers above assume a 64-bit long, which is why the int range check is separate from the ERANGE check.
Common mistakes
Feeding user input to atoi and treating the returned 0 as a number: "abc", "" and "0" are indistinguishable, so a typo silently becomes a zero size, zero port or zero timeout.
Calling strtol without errno = 0 first, or not testing ERANGE at all: a stale ERANGE from an earlier library call makes a valid number look overflowed, and skipping the test lets LONG_MAX be used as if the user had typed it.
Using strtol's return value without checking *end: "8 GB" and "12abc" convert to 8 and 12 with no complaint, so the unit or the typo is dropped without anyone noticing.
Try it yourself
Change, predict, then run
Write parse_port(const char *s, int *out) that uses strtol and accepts only strings that convert entirely to a value in 1..65535. Run it over "80", "0", "65536", "8080x", " 443" and "", print accept or reject for each, and call exit(EXIT_FAILURE) at the end if anything was rejected.
Open the C workspaceCheck your understanding
A parser calls long v = strtol(s, &end, 10); and checks only errno == ERANGE. Which input is accepted with a wrong value?
- " 42", because strtol refuses to skip leading whitespace and returns 0
- "99999999999999999999", because strtol overflows silently
- "abc", because strtol returns 0 without touching errno
- "-7", because strtol only converts unsigned digit sequences
Show answer
With "abc" no conversion happens at all: strtol returns 0, leaves errno alone, and reports the failure only by setting end == s, so an ERANGE-only check accepts a fabricated zero. The overflow option is tempting but wrong, because a value that does not fit is exactly what ERANGE reports (alongside LONG_MAX or LONG_MIN); and " 42" and "-7" are legitimately converted, since strtol skips leading whitespace and honours a sign.