Welcome to Software Development on Codidact!
Will you help us build our independent community of developers helping developers? We're small and trying to grow. We welcome questions about all aspects of software development, from design to code to QA and more. Got questions? Got answers? Got code you'd like someone to review? Please join us.
Detailed explanation of "hello, world" in C?
I am trying to print "hello, world" on screen in in C using this code:
#include <stdio.h>
int main()
{
printf("hello, world\n");
return 0;
}
I typed this down in a text file named main.c and compiled with gcc main.c -O program (or program.exe on Windows) and then ran program - it works and I'm told not to worry about all the details yet.
Still I am looking for a more more in-depth explanation of this code. What does all of this mean, in great detail?
(This is a self-answered Q&A aimed to those who aren't satisfied with the explanation "just accept that this works for now". Accepting this for now would normally be a very sensible approach if you just picked up C or even programming overall, but there are always those who insist on learning all the details still.)
2 answers
NOTE: this answer contains a whole lot more information than a beginner reasonably needs to know at this point. It addresses various concepts that one normally encounters further down the road in the learning process.
History
In the 1970s, the book The C Programming Language (B. Kernighan, D. Ritchie) was released and became known as "K&R C" (Kernighan & Ritchie). Dennis Ritchie was the creator of the C language. The first code example from that book looked like this:
#include <stdio.h>
main()
{
printf("hello, world\n");
}
And since then it has become tradition to start every programming book (for C or any other language) with an example printing the text "hello, world". This historic example above is a bit problematic and not quite up to date, but that's another story.
Functions
Looking at the example from the question instead, main is the name of a function. The { and } mark the beginning and end of that function. Functions are the sections of code in a C program where the actual program execution takes place. The name main is a special function name reserved for the function where the program starts, so it can be regarded as the "mandatory bottom of the program" from where we may call other functions, which in turn may call other functions - but eventually the program execution returns to main. Because of that, it is also the function which can communicate a bit with the OS if desired.
A function may take parameters and it may return a value to the caller. All functions that return something must have (at least) one return statement containing the value to return. For example return 0; returns the integer with value 0 to the caller, since 0 in C code is a so-called integer constant (sometimes also referred to as "integer literal"). All such constants in C have a type, in this case int, which nicely matches the int main return type of the function.
The return statement
As soon as the return statement is executed, the function returns to the caller. Since C executes the source code corresponding to the program from the top to bottom like we read a book, any code we happened to have written below a return statement will not get executed. We can try to modify the example by adding a second printf and note that it will not print anything since it isn't executed:
#include <stdio.h>
int main()
{
printf("hello, world\n");
return 0;
printf("test\n");
}
The value 0 happens to be the value that main should return to the OS after successful program execution. If we return another number, then that number corresponds to some error code. In modern programming it is unlikely that the OS even cares about the return value though, other than displaying it for information. On some consoles you will get this number printed along the form of Program returned: 0 or similar.
Since main is a special function unlike any other, we are actually permitted (as per the C99 standard or later) to entirely omit return 0; from main() and that will have the same meaning as if we had written return 0; explicitly. So in modern/semi-modern C, we can actually safely leave out the return in the specific case of main, but then the program would not be portable to very old C compilers. Try this out:
#include <stdio.h>
int main()
{
printf("hello, world\n");
}
And that will work just as fine and return 0, unless you are using a very old compiler.
Parameters and the format of main()
The parenthesis in main() corresponds to the parameter list - an optional list of data that the function accepts as input from the caller. Until very recently, an empty parenthesis in a function actually meant "function accepting any parameter", because in the old days of K&R C we could declare function parameters in an alternative way. So main() is actually sloppy style - more correct would have been int main (void) where void means that we take no parameter. In the very recent "C23" version of the language, the old "accept any parameter" style got phased out of the language though, so now int main() and int main(void) are 100% equivalent.
It is important to recognize that the format of the function main() is set by the C standard and the compiler, never by the programmer. The C standard says that for a hosted system (one with an OS), the format must be int main (void) (or in C23, int main() is equivalent). Or in the special case where we wish to pass parameters along to the program when we call it, then the alternative standard form int main(int argc, char *argv[]) is used for that, but I won't go into that one here.
Furthermore, standard C allows compiler-specific forms of main(). For example the standard says that in freestanding systems (embedded), the form of main() is always compiler-specific. In case of hosted systems, compiler-specific forms may exist too. In that case the compiler will document in its manual which alternative form(s) of main() it supports. For example void main (void) is very common. Again, it is the compiler stating which forms that are allowed, never the programmer.
Function declarations versus definitions
Regarding the #include <stdio.h> and printf - the short story is that stdio.h is the library where printf is located and by including it we may use the printf function. The long and detailed story is that the #include makes printf visible to main, by providing a function declaration.
As noted earlier, functions are the part of the program where execution happens. But for a function to call another function, it needs to be made aware of its existence. Normally one makes other functions aware by writing a function declaration, a line of code looking something like this:
int func (char x);
This is a note regarding function usage, for the user of the function and the compiler both, stating this: "Somewhere in the program I have defined a function named func. It returns int and takes a char as parameter. I prefer to call the char parameter x (writing the name of the parameter in a function declaration is actually optional). You may call this function from your program, if you use it like I just showed you." The semicolon at the end signifies that this is a function declaration and that it is now done.
Whereas int main(){ ... } in our example comes with the { and } - a function body with executable code. That is called a function definition - the actual function.
printf, stdio.h and include pre-processing
Now what we like to do from main() is to call a function printf ("print formatted"), which got a function definition somewhere inside a C standard library file that the compiler automatically links to our program when asked for it. We don't actually get to see what the function definition of printf looks like, but for the compiler compiling main() to know about printf and how it is called, we should make a function declaration visible and that will be enough.
The function declaration for printf is located in the header file stdio.h. A header file in C (typically always ending with the extension .h) is a file that contains information of how to use a module or library, but rarely ever any executable code. It may contain constants, types and other things we can use too. stdio.h ("standard input/output") specifically is part of the pre-made standard library that the C standard requires to be present in advance. It is integrated into the C compiler itself.
#include is how we include a header file. The < and > signifies that we want a standard library header, and not a custom one, in which case we would have used " and " instead. Any line that begins with # in C are so-called pre-processing directives, which means that they execute and prepare things for the program before the rest of it is even compiled. What #include does is to grab the header file named stdio.h and between the lines pastes the whole contents of that file into our main source. On a compiler like gcc we can actually look at the file after preprocessing but before compilation, if we compile with the -E option. We will then see many hundred lines of function declarations and the like from stdio.h and then our main() program at the bottom. This state of a .c file after pre-processing, where all the header files it uses are silently "pasted" into it, is formally known as a translation unit, consisting of a single .c file and all the headers it includes.
Once #include <stdio.h> is in place, the compiler can now see the function definition of printf and therfore knows how to call it. As for how the programmer knows how that function works and what it expects, we have to look that up in a book or manual. Basically it is the standard function for formatted output, really a quite complicated function but we don't need to know everything about it just to print some text. The first parameter to printf is the format string where apart from stuff that we print out, like "hello, world", we may also include other commands that printf will parse, leading to special behavior. In this case there is a \n at the end - something beginning with a \ inside a string is known as an escape sequence - simply put a special token or command. \n means "line feed", meaning that after printing "hello, world", there should be a switch to a new line in the output. It is custom to do this so that the next function that wants to print something can assume that printing begins on a new line. Example: had we written "hello\nworld" instead, we would get "hello" and "world" printed on separate lines.
Instead of using printf, we could as well have used a much easier function puts ("put string"). It only knows how to print a string and it always ends the printing with a new line without us asking for it. But it can't do anything else, unlike printf which comes with a lot of various formatting options.
The printf("hello, world\n") part of the code is what does the important work. The rest is, essentially, setup that's needed to have a valid C program.
Functions
Practical programs are built up out of functions that each describe a small part of the work that the program has to do. Every function has a name, which lets the functions refer to each other. This way, we can describe the entire flow of the code — which steps happen when — and create complex flows (not just following one step after another in order).
Every function can have inputs and an output. The inputs are whatever is needed for the function to do its work, and the output is the result of that work. All of these are values — like numbers of varying kinds and sizes, or text (well, what passes for text in C). Which is to say, they are chunks of data that have some type (what kind of thing it is).
Our program contains one function of its own, main; and it uses one function provided elsewhere, printf. We use that function by "calling" it; and in turn, main gets called by some other startup code created by the compiler. (Many other languages allow you to write code outside of any function, which just runs top to bottom. But this is not so practical when working at a low level like in C.)
In more detail:
Defining functions
When we write int main() { } and then put more code (which can be spread across multiple lines) between the { }, we define a function. Between the (), we list parameters that explain what the inputs are. In this case, we do not require any input; the purpose is to print the hello, world message no matter what. The main part, of course, is the name.
The int at the front is a return type; it states the type of our output — an integer number (meaning: it can be negative, but does not have a fractional or decimal part). There are a few such integer types in C, that declare different sizes (more or less data used, allowing a wider or smaller range of possible numbers).
Between { } (we call this part the function's body) we have two statements: printf("hello, world\n"), and return 0. Each has a ; at the end, which lets the compiler split the code into statements. Other languages use different rules for this. C's rule requires some attention to detail, but it gives you more freedom in spacing out your code and spreading it across lines.
Notice that our printed message is not considered an "output". Displaying a message like this is just a "side effect". Instead, the statement return 0 gives the output: the int with a value of zero. When the code runs, and reaches (the compiled equivalent of) the return statement, that is the end of main's calculation: the result is known to be 0, and that is the result immediately given back to whatever called main (i.e., the real starting point of the code).
Calling functions
In our other statement, printf("hello, world\n"), we call the printf function. We do this by simply writing its name and then putting any needed arguments between (). The compiler understands this as a request to run the code in the function, matching up the arguments to the function's parameters, and then give us back the function's return value, if any.
The printf function does return a value (not all functions in C return a value; they may use void for the return type to say that there is nothing returned). Our code simply ignores that value, but it could use that value, for example by doing some math with it. A function call is a kind of expression, and so we can mix and match these with math operators. If we have some function square that gives us the square of an integer, then we can write square(3) + square(3), and we can write square(1 + 2), etc.
(Specifically, printf writes out some text, and then tells us how many "characters" were written. Decades ago, when C was new, that was a straightforward idea; now we understand that it's very complicated to have text that supports all the things we take for granted now, so explaining this part is beyond the scope of this answer. At any rate, we don't really care how many characters are in our hello, world text; we just want them to show up.)
Where we wrote "hello, world\n", the double-quotes are used to mark a piece of text, called a string (although C's concept of a "string" is quite limited compared to that in other languages). By having a double-quote on each side, it's clear to the compiler where the text begins and ends.
The \n does not actually mean a backslash and a letter n; this is part of a system of escape sequences that lets us describe difficult text. Specifically, this sequence means a "newline": a symbol in our text that means to go to the next line. This is just a normal part of text the same way that letters and spaces and punctuation are; but C doesn't let us split the string across multiple lines in our source code, so we use this system instead. (This system lets us put anything in the string that the string could legally contain. Including actual backslashes and double-quotes, of course.)
The printf function is specially designed and can be called in many ways, so that we can also display (for example) the numbers in our program and control their formatting (number of places after the decimal point, spacing before and after, etc.) You will learn about it in more detail in due course. For now, it's enough to understand that printf is used to print formatted text.
The standard library
To produce the code for our function, the compiler needs to produce code that calls printf. To do that, it needs to know what that function is and how it works. We can't realistically write that function ourself; making text show up on the screen involves a ton of details we don't want to worry about.
How that works:
Before the C compiler itself runs, it uses a preprocessor to fix up the text of the program. This is mostly copying and pasting other code; it can also be used for "macros" which you will learn about later. Where we wrote #include <stdio.h>, this tells the preprocessor to replace that with the contents of a stdio.h file in the standard library (files that come with the compiler — you don't generally need to worry about where they've been put; the compiler knows).
This stdio.h file is a header file; it contains declarations for functions, but not their actual code. This is enough information so that the compiler has a prototype of each function: its parameters (and their types) and return type, but not necessarily the body. With this, the compiler can produce the machine-level code actually needed to call functions.
(For the standard library, the compiler might skip all these formal steps, and use some built-in knowledge of the function prototypes. But formally it requires you to #include the headers. This way, it can be sure that you meant to type printf, and that you weren't looking for some other printf, for example, somewhere else in your own code.)
The linker and operating system
Just as our code calls printf which returns a value (that we ignore), it also defines a main which returns a value. You may be wondering by now: where does that value go?
You may also be wondering: if our #include <stdio.h> only told the compiler how to call printf, where does it actually get the printf code from?
To answer both of these questions, we first need to remember that many other programs are running on the computer besides the one that we wrote and compiled. In reality, in the modern age, our program doesn't actually make the text show up on the screen.
It's much more complicated than that...
In particular, an operating system (OS — like Windows or Linux) is always there; and we may also have a compiler, a text editor (or an IDE), a terminal (where we type the compiler commands, or the IDE types them for us), etc. The operating system knows how to start other programs running, and it also allows programs to request that other programs start running.
When our program runs, it merely sends our text data to a terminal program, which will figure out all the pixels that have to light up to make those letter-shapes in the right places. Then it just stores that information about its own window, and the operating system has to figure out where each window is and how they overlap.
Meanwhile, to get our program started, when we type program (or program.exe) and press Enter, the terminal program may turn it into an OS request; or that may be handled by a separate "shell" program running within the terminal. After our program is done with "printing" (communicating text data to the terminal), the value returned from main is given back to the OS, and the OS gives it back to the shell.
By convention, we use this 0 return value to mean that the program ran successfully (i.e. there was no error). Historically we chose this because it's very easy for hardware to check whether a number is zero, and because a program can report many different errors but has only one way to be successful.
To get at the printf code there are two options. For standard libraries like stdio, typically the compiler just already has the code ready and inserts it into the executable. This is static linking. More generally, this kind of linking might require compiling the other part of the code first. In these cases, the compiler leaves symbols in your compiled code that mark the missing pieces, and later uses a linker to connect up pieces of compiled-with-symbols code into a single executable.
Another thing that can happen is that symbols are left in your file, and the needed code is only found when the program actually runs. This is dynamic linking, and it requires support from the OS. Basically, when your program is loaded, something like the compiler's linker will run, except that instead of inserting more code, it can point your code at existing library code already in memory (or, if necessary, load up that library first). Explaining this in more detail is OS-specific, and requires more concepts that you won't have yet.

1 comment thread