12 Odds and Ends
This chapter covers a few remaining topics that do not fit neatly into the previous chapters but are important for writing real C programs and for working with C code from C++.
12.1 exit vs return
void exit(int status);You already know that return in main ends the program. The exit function from <stdlib.h> does the same thing, but it can be called from any function — not just main:
#include <stdio.h>
#include <stdlib.h>
void check_file(const char *path) {
FILE *f = fopen(path, "r");
if (f == NULL) {
fprintf(stderr, "Fatal: cannot open %s\n", path);
exit(1); // end the program immediately
}
// ... work with the file ...
fclose(f);
}exit is useful when an error deep inside a call chain is unrecoverable and there is no reasonable way to propagate the error back through multiple layers of callers. In C++, you would throw an exception; in C, exit is sometimes the pragmatic choice.
Tip: Use exit sparingly. It is a blunt instrument — it ends the entire program immediately, skipping any cleanup code in calling functions. If you can reasonably propagate an error code back to main and let main return, prefer that approach. Reserve exit for truly fatal errors.
12.1.1 atexit
A C program can end from many places — a return from main, or an exit call buried deep in a call chain. If some cleanup must always happen — removing a temporary file, saving state, restoring terminal settings — you would have to remember to do it at every one of those exit points. atexit solves this: register a cleanup function once, and exit calls it for you no matter where the program ends. exit runs the registered functions first and then flushes all open stdio streams before terminating the program:
int atexit(void (*func)(void));You can register several handlers; they run in reverse registration order:
void close_log(void) { printf("log closed\n"); }
void remove_temp(void) { printf("temp file removed\n"); }
int main(void) {
atexit(close_log); // registered first, runs last
atexit(remove_temp); // registered last, runs first
printf("working...\n");
exit(0);
}Reverse order is deliberate: it unwinds resources opposite to the order you set them up, the same way C++ destructors run.
atexit has real limitations. Handlers take no arguments and return nothing, so anything they clean up must be reachable through file-scope variables. Once registered, a handler cannot be unregistered. And handlers only run on normal termination — exit or a return from main.
Trap: A crash, abort, or a fatal signal (Ctrl-C sends one) skips atexit handlers entirely. Do not rely on them to protect critical data — write and flush important state as you go.
Tip: Register a handler as soon as the resource it cleans up exists, and keep handlers small and independent. Use atexit for process-lifetime cleanup: temporary files, terminal state, log files. For releasing resources on an error path inside one function, use the goto cleanup pattern covered later in this chapter instead.
12.2 extern "C" — Calling C from C++
If you are writing C++ code that needs to call functions from a C library, you need extern "C". The reason: C++ mangles function names to support overloading (so void foo(int) and void foo(double) have different symbol names), but C does not. Without extern "C", the C++ linker looks for the mangled name and cannot find the C function.
// In your C++ code:
extern "C" {
#include "my_c_library.h" // treat these declarations as C
}Or for a single function:
extern "C" void c_function(int x);Many C headers protect themselves with this pattern so they work from both C and C++:
#ifdef __cplusplus
extern "C" {
#endif
void some_function(int x);
int another_function(const char *s);
#ifdef __cplusplus
}
#endifThe __cplusplus macro is only defined when compiling with a C++ compiler, so the extern "C" wrapper only appears in C++ compilation.
12.3 Pointer Ownership
In C++, smart pointers make ownership clear: a std::unique_ptr owns the memory, and when it goes out of scope, the memory is freed. In C, there are no smart pointers. When a function returns a pointer, you must ask: who owns this memory?
There are three common patterns:
1. The caller owns it (you must free). The function allocates memory and hands ownership to you:
char *copy = strdup("Everybody Wants to Rule the World");
// You own this memory. You must free it.
free(copy);2. The library owns it (do not free). The function returns a pointer to memory it manages internally:
struct hostent *h = gethostbyname("example.com");
// The library owns this. Do NOT free it.
// (gethostbyname is obsolete — use getaddrinfo — but it's a classic)3. You own it (you passed it in). You allocated the memory and passed a pointer to the function. The function used it but did not take ownership:
char buf[100];
fgets(buf, sizeof(buf), stdin);
// You still own buf. Nothing to free (it's on the stack).
Trap: Always read the documentation of a C function that returns a pointer. Look for words like “the caller must free the returned pointer” or “the returned pointer points to a static buffer.” If the documentation does not say, look at the source code. Getting ownership wrong leads to either memory leaks (never freeing) or double-free bugs (freeing what you do not own).
12.4 Error Handling Without Exceptions
In C++, you can throw an exception and let a catch block handle it several call levels up. C has no exceptions. Error handling is done through return codes, and cleanup is your responsibility.
The simplest pattern is to check return values and bail out:
void perror(const char *s);FILE *f = fopen("La Isla Bonita.txt", "r");
if (!f) {
perror("fopen");
return -1;
} But what happens when a function acquires multiple resources? You need to release them in the correct order when something goes wrong. The idiomatic C pattern uses goto to jump to cleanup labels:
int process(const char *path) {
int status = -1;
FILE *f = fopen(path, "r");
if (!f) return -1;
char *buf = malloc(1024);
if (!buf) goto close_file;
char *line = malloc(256);
if (!line) goto free_buf;
/* do work with f, buf, and line ... */
status = 0;
free(line);
free_buf:
free(buf);
close_file:
fclose(f);
return status;
}Each resource acquired gets a corresponding cleanup label below it. If any allocation fails, control jumps to the label that releases everything acquired so far, in reverse order. This pattern is used extensively in real C code including the Linux kernel.
Wut: C++ programmers are taught “never use goto.” In C, goto for cleanup is an accepted and widely used idiom. It is the closest thing C has to RAII — a structured way to ensure resources are always released.
The other common strategy is to return an error code (often -1 or NULL) and let the caller decide what to do. Many C library functions set the special variable errno (declared in <errno.h>, and thread-local on modern systems) to indicate what went wrong. You can use perror (from <stdio.h>) or strerror(errno) (from <string.h>) to get a human-readable message:
char *strerror(int errnum);FILE *f = fopen("No Existe.txt", "r");
if (!f) {
perror("fopen"); // prints: fopen: No such file or directory
} 12.5 enum
C has enum just like C++, but there are a few differences. In C, enum constants are plain int values — there is no enum class and no scoping:
enum direction { NORTH, SOUTH, EAST, WEST };Unlike C++, you cannot use the enum name as a type directly without the enum keyword:
enum direction heading = NORTH; /* need the 'enum' keyword */You can avoid this with a typedef, just like with struct (see Chapter 2):
typedef enum { NORTH, SOUTH, EAST, WEST } Direction;
Direction heading = NORTH;Enum values are just integers. You can assign specific values and mix enums with integers freely — the compiler will not complain:
enum status { OK = 0, ERR_FILE = -1, ERR_MEM = -2 };
int code = OK; /* fine — enum to int */
enum status s = 42; /* also fine in C, no warning */
Wut: C++ enum class prevents implicit conversions and scopes the constants. C has neither protection. Enum constants are global and freely convert to int.
12.6 union
A union looks like a struct, but all members share the same memory. Only one member can hold a value at a time, and the size of the union is at least the size of its largest member (padding can round it up):
union value {
int i;
float f;
char s[20];
};
printf("sizeof(union value) = %zu\n", sizeof(union value));
/* prints 20 — the size of the largest member */Unions are useful for saving memory when a variable can hold one of several types. A common C pattern is a tagged union — a struct with an enum tag that tracks which member is active:
enum val_type { VAL_INT, VAL_FLOAT, VAL_STR };
struct tagged_value {
enum val_type type;
union {
int i;
float f;
char s[20];
} data;
};
void print_value(struct tagged_value v) {
switch (v.type) {
case VAL_INT: printf("%d\n", v.data.i); break;
case VAL_FLOAT: printf("%f\n", v.data.f); break;
case VAL_STR: printf("%s\n", v.data.s); break;
}
}
Tip: Tagged unions are C’s equivalent of std::variant. Always check the tag before accessing a union member — reading the wrong member is undefined behavior.
12.7 qsort — Function Pointers in Action
You learned about function pointers in Chapter 7 (Functions). The most common place you will encounter them in practice is the standard library function qsort from <stdlib.h>. In C++, you would use std::sort with a lambda or comparator. In C, qsort takes a comparison function pointer:
void qsort(void *base, size_t nel, size_t width,
int (*compar)(const void *, const void *));#include <stdio.h>
#include <stdlib.h>
#include <string.h>
int compare_ints(const void *a, const void *b) {
int ia = *(const int *)a;
int ib = *(const int *)b;
return (ia > ib) - (ia < ib);
}
int main(void) {
int years[] = {1989, 1982, 1985, 1980, 1987};
int n = sizeof(years) / sizeof(years[0]);
qsort(years, n, sizeof(int), compare_ints);
for (int i = 0; i < n; i++) {
printf("%d ", years[i]);
}
printf("\n");
// Output: 1980 1982 1985 1987 1989
return 0;
}qsort takes four arguments: the array, the number of elements, the size of each element, and a pointer to a comparison function. The comparison function receives const void * pointers — you must cast them to the correct type inside the function. It returns a negative value if the first argument is less than the second, zero if equal, and a positive value if greater.
You can sort anything with qsort by writing different comparison functions. Here is one that sorts strings:
int compare_strings(const void *a, const void *b) {
const char *sa = *(const char **)a;
const char *sb = *(const char **)b;
return strcmp(sa, sb);
}
int main(void) {
const char *songs[] = {
"Maniac", "Footloose", "Flashdance", "Fame"
};
int n = sizeof(songs) / sizeof(songs[0]);
qsort(songs, n, sizeof(char *), compare_strings);
for (int i = 0; i < n; i++) {
printf("%s\n", songs[i]);
}
// Output:
// Fame
// Flashdance
// Footloose
// Maniac
return 0;
}Notice the double cast in compare_strings: qsort passes a pointer to each array element, and each element is already a const char *, so you receive a const char ** disguised as const void *.
Wut: A common mistake is to write return a - b in integer comparison functions. This can overflow when a and b have very different signs (e.g., INT_MAX - (-1) overflows). The pattern (a > b) - (a < b) is safe and returns -1, 0, or 1.
12.8 Try It: Odds and Ends Starter
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
// A comparison function for qsort
// a normal numeric sort, but we want 1982 to always be first
// because we are big Thriller fans!
int compare_ints(const void *a, const void *b) {
int ia = *(const int *)a;
int ib = *(const int *)b;
// 1982 always appears first (Thriller came out in 1982)
if (ia == 1982 && ib == 1982) return 0;
if (ia == 1982) return -1;
if (ib == 1982) return 1;
// Overflow-safe alternative to (ia - ib). The three cases:
// ia > ib → (1) - (0) = 1
// ia == ib → (0) - (0) = 0
// ia < ib → (0) - (1) = -1
return (ia > ib) - (ia < ib);
}
void goodbye(void) {
printf("atexit: Adios!\n");
}
int main(void) {
atexit(goodbye);
// Function pointer
int (*cmp)(const void *, const void *) = compare_ints;
int x = 10, y = 20;
printf("compare(10, 20) = %d\n", cmp(&x, &y));
// qsort
int years[] = {1987, 1983, 1982, 1989, 1980, 1985};
int n = sizeof(years) / sizeof(years[0]);
qsort(years, n, sizeof(int), compare_ints);
printf("Sorted: ");
for (int i = 0; i < n; i++)
printf("%d ", years[i]);
printf("\n");
// Pointer ownership: strdup allocates, you must free
char *copy = strdup("Master of Puppets");
printf("strdup: '%s'\n", copy);
free(copy);
// goto cleanup pattern
printf("Demonstrating goto cleanup...\n");
char *buf = malloc(100);
if (!buf) return 1;
char *msg = malloc(50);
if (!msg) goto free_buf;
snprintf(buf, 100, "Resource 1 OK");
snprintf(msg, 50, "Resource 2 OK");
printf(" %s, %s\n", buf, msg);
free(msg);
free_buf:
free(buf);
return 0;
}12.9 Key Points
exitterminates the program from any function. Use it for unrecoverable errors.exitcallsatexithandlers and then flushesstdiostreams before terminating.extern "C"tells the C++ compiler not to mangle function names, so it can link to C libraries.- C headers often use
#ifdef __cplusplusto wrap declarations inextern "C"automatically. - When you receive a pointer from a function, always determine who owns the memory: you, the function, or a library.
- C has no exceptions. Use return codes for errors and
gotocleanup for releasing resources in the correct order. qsortis the most common use of function pointers — it takes a comparison callback to sort any type.- C
enumconstants are plainintvalues with no scoping — there is noenum class. - A
unionstores all members in the same memory. Use a tagged union (struct with an enum tag) to track which member is active.
12.10 Exercises
Think about it: In C++ you would use exceptions for error handling. In C there are no exceptions. What strategies can you use to handle errors in deeply nested function calls? When is
exitappropriate and when is it not?What happens here?
#include <stdlib.h> #include <stdio.h> void cleanup(void) { printf("Adios!\n"); } int main(void) { atexit(cleanup); printf("Starting...\n"); exit(0); }Where is the bug?
char *get_greeting(void) { char buf[50]; sprintf(buf, "Hola, mundo"); return buf; }Think about it: You call a function
char *get_name(int id)from a library. How would you determine whether you need tofreethe returned pointer?Where is the bug? (Hint: ownership)
char *name = strdup("Walking on Sunshine"); char *alias = name; free(name); printf("%s\n", alias);Calculation: Given
int nums[] = {5, 10, 15, 20};, what is the value ofsizeof(nums) / sizeof(nums[0])?What does this print?
int compare_desc(const void *a, const void *b) { int ia = *(const int *)a; int ib = *(const int *)b; return (ib > ia) - (ib < ia); } int main(void) { int vals[] = {3, 1, 4, 1, 5}; qsort(vals, 5, sizeof(int), compare_desc); printf("%d %d %d %d %d\n", vals[0], vals[1], vals[2], vals[3], vals[4]); return 0; }Write a program that uses
qsortto sort an array of strings in reverse alphabetical order. Write a custom comparison function that callsstrcmpwith the arguments swapped.Write a program in C++ that uses
extern "C"to call the C functionstrlenfrom<string.h>, passes it a string, and prints the result. Compile it withc++to verify it works.Calculation: What is
sizeof(union { int i; double d; char s[3]; })on a system whereintis 4 bytes anddoubleis 8 bytes?Where is the bug?
union value { int i; float f; }; union value v; v.i = 42; printf("%.2f\n", v.f);Write a program that defines a tagged union representing a shape (circle with a radius, or rectangle with width and height). Write a function that prints the area of the shape using the tag to determine which union member to read.