the standard streams (stdin, stdout, stderr)
Picture every program as a little machine on a workbench with three pipes attached: one pipe feeds material in, one pipe carries the finished product out, and a third small pipe is just for warning lights and error messages. Crucially, the machine does not care whether the input pipe comes from your keyboard or from a hose connected to another machine - it just reads whatever flows in. Those three pipes are the standard streams that every Unix program starts with.
Concretely, when a process starts it is handed three already-open connections by convention, identified by small integers called file descriptors. Standard input (stdin) is file descriptor 0 - where the program reads its input. Standard output (stdout) is file descriptor 1 - where it writes its normal results. Standard error (stderr) is file descriptor 2 - where it writes diagnostics and error messages, kept separate so that error text does not get mixed into and corrupt the real output. By default 0, 1, and 2 are connected to your terminal, but the shell can redirect any of them to a file or to another program without the program knowing or caring: that is exactly how ./prog < in.txt > out.txt and pipelines like a | b work. The C library wraps these descriptors as the convenient stream objects stdin, stdout, and stderr that you use with printf() and friends.
Why it matters: this tiny convention - three numbered streams, output separate from errors - is what makes Unix programs composable. Because a program reads from descriptor 0 and writes to descriptor 1 without caring what is on the other end, the shell can wire the output of one program into the input of the next, building powerful pipelines out of small tools. The discipline of sending real output to stdout and errors to stderr is what lets you capture results in a file while still seeing error messages on screen.
Run ./prog > out.txt and the shell connects descriptor 1 (stdout) to the file, so normal output lands in out.txt - but anything the program writes to stderr (descriptor 2) still shows up on your terminal, because you only redirected 1, not 2.
Output and errors travel on different descriptors, so they can be routed separately.
Send results to stdout (1) and diagnostics to stderr (2), not the reverse. Mixing error text into stdout corrupts data that someone may be piping into another program; that is why printf() goes to stdout but error reporting should go to stderr.