C / STANDARD LIBRARY TOUR
math.h and linking with -lm
Link C programs that use math.h correctly: why sqrt needs -lm, where the library goes on the command line, and how math errors are reported.
What you will learn
- Put -lm after the .c and .o files on the link line, never before
- Read "undefined reference to sqrt" as a linking problem, not a missing include
- Match the call to the type: sqrtf for float, sqrt for double, sqrtl for long double
- Detect domain errors with isnan/isinf or errno instead of trusting the return value
Understanding math.h and linking with -lm
#include <math.h> hands the compiler declarations and macros; it contains no machine code. The compiled bodies of sqrt, pow, sin and the rest live in a separate library, libm (libm.so or libm.a on Linux), which the linker searches only when you ask for it with -lm. That split dates back to early Unix, where floating point was slow or emulated and many programs never touched it, and it survives as a platform convention. The practical consequence is that leaving out -lm gives a clean compile followed by a link failure: undefined reference to sqrt.
The order of the link line matters because the traditional Unix linker makes a single left-to-right pass while carrying a set of still-unresolved symbols. When it reaches a library it takes only what resolves something already pending, so cc -lm prog.c presents libm at a moment when nothing needs sqrt yet; the library is discarded and the reference from prog.c fails seconds later. Most Linux distributions also default to --as-needed, which turns this into a hard rule instead of an accident that sometimes works. Libraries belong to the right of the objects that use them, and only the link step wants them: cc -c prog.c never needs -lm.
math.h works in double by default, so sqrt takes and returns double and sqrtf/sqrtl are the float and long double versions. Errors are reported in-band rather than by trapping: sqrt(-1.0) returns a NaN and normally sets errno to EDOM, log(0.0) returns -HUGE_VAL and sets ERANGE, and math_errhandling tells you whether the implementation uses errno, floating-point exception flags, or both. Because NaN and infinity propagate silently through further arithmetic, one unchecked domain error usually surfaces much later as a screenful of nan.
<math.h>
<stdio.h>
/* build: cc -std=c17 -Wall prog.c -o prog -lm (the -lm comes last) */
int main(void)
{
double x = 2.0;
printf("sqrt(x) = %.6f\n", sqrt(x));
printf("pow(x, 10) = %.1f\n", pow(x, 10.0));
printf("hypot(3, 4) = %.1f\n", hypot(3.0, 4.0));
printf("fmod(7, 3) = %.1f\n", fmod(7.0, 3.0));
printf("fabs(-3.75) = %.2f\n", fabs(-3.75));
printf("floor(-2.5) = %.1f\n", floor(-2.5));
printf("round(-2.5) = %.1f\n", round(-2.5));
return 0;
}
A header only declares a function; -lm is what supplies the actual machine code for math.h's functions at link time.
Worked examples
Constant folding hides a missing -lm
Shows the difference between a math call the compiler evaluates itself and one that must reach libm at run time.
<math.h>
<stdio.h>
/* taking the address forces a real call into libm */
static double (*root)(double) = sqrt;
int main(void)
{
volatile double v = 2.0; /* volatile blocks the folding */
printf("literal: %.5f\n", sqrt(2.0));
printf("runtime: %.5f\n", sqrt(v));
printf("pointer: %.5f\n", root(2.0));
return 0;
}
Example explained
Line 1sqrt(2.0) has a constant argument, so the compiler computes the result itself and emits no call at all.
Line 2volatile double v must be read from memory on every use, so sqrt(v) becomes a genuine call and the link now needs -lm.
Line 3static double (*root)(double) = sqrt; stores the function's address, which no optimisation level can fold away.
Line 4All three lines print the same number, which is why testing with literals proves nothing about your link line.
Picking the right width, and a domain error
Compares the float and double square roots and shows what a call outside the domain returns.
<math.h>
<stdio.h>
int main(void)
{
printf("sqrtf(2.0f) = %.9f\n", sqrtf(2.0f));
printf("sqrt(2.0) = %.9f\n", sqrt(2.0));
printf("pow(2, 10) = %.0f\n", pow(2, 10));
printf("sqrt(-1.0) is NaN: %s\n", isnan(sqrt(-1.0)) ? "yes" : "no");
return 0;
}
Example explained
Line 1sqrtf computes in float, so its answer parts company with sqrt at the eighth significant digit.
Line 2pow(2, 10) compiles because the prototype converts both int arguments to double; the standard library has no integer pow.
Line 3sqrt(-1.0) does not abort the program, it returns a NaN, and isnan is the portable test for that.
Line 4sqrtf and sqrtl sit in libm alongside sqrt, so the same -lm covers all three.
Important notes
M_PI, M_E and the other M_ constants are not ISO C; glibc hides them when you compile with -std=c17, so use acos(-1.0), define your own constant, or switch to -std=gnu17.
On macOS and the BSDs the math functions live inside the system C library, so -lm does nothing there; keep it in your build files anyway so the same command works on Linux.
Common mistakes
Writing cc -lm prog.c: the linker meets libm before any symbol is pending, drops it, and still reports undefined reference to sqrt, so the reader starts re-checking the #include instead of the argument order.
Testing only with literals such as sqrt(2.0) or pow(2.0, 3.0): the compiler folds them at compile time, the program links without -lm, and the build breaks the day the argument comes from input.
Reaching for abs() on a double: stdlib.h's abs takes an int, so abs(-3.75) quietly converts to -3 and yields 3, while fabs(-3.75) gives the 3.75 you wanted.
Try it yourself
Change, predict, then run
Print hypot(1e200, 1e200) next to sqrt(1e200 * 1e200 + 1e200 * 1e200) and explain why only one of them survives, then remove -lm from your build command and read the exact error the linker gives you.
Open the C workspaceCheck your understanding
A file that calls sqrt on a variable compiles cleanly, but cc -lm calc.c -o calc fails with "undefined reference to sqrt". What is going on?
- The linker read libm before anything referenced sqrt, so it dropped the library; -lm must come after calc.c
- math.h was not included, so sqrt has no prototype and cannot be resolved
- libm is missing from the system and has to be installed separately
- sqrt expects a double and the variable is an int, so no matching symbol exists
Show answer
The linker makes one left-to-right pass and considers a library only for symbols that are already unresolved when it gets there, so a library listed first satisfies nothing and is discarded, especially with --as-needed enabled by default. A missing prototype is tempting but wrong: that is a compile-stage diagnostic about an implicit declaration, and here compilation succeeded and the complaint came from the linker.