Getting started with C programming: a lightning-fast start for absolute beginners
Posted at , last edit:
!☕ This tutorial will guide you through writing the “Hello World” program in the C programming language. You’ll use Unix-style terminal emulators and command-line tools to execute commands, Linux-style package managers to install programs and libraries, the GNU nano text editor to write C code, the meson build system to build executable programs and the Gtk+ library to write portable, cross-platform graphical programs.

Instructions for three operating systems are provided: macOS, Ubuntu Linux and Windows. The selected tools are personal favorites and were hand picked to allow the fastest development and to allow identical development flows (and thus seamless environment switching) on the three operating systems. All tools used in this tutorial are free, open source software and can be downloaded and used without any issues. No programming experience is required, as this tutorial is for absolute beginners.
Before we start: terminals
There are a few notes for absolute beginners that will make our path easier and faster to understand: to stay short, the tutorial will only guide through the setup process, but will not teach the C language; references for next steps will be given at the end; some program downloads may be large and take some time.
Programming languages and textual commands are very different from spoken languages: programs must be written in a very strict way. Miss a comma and the program will stop working. The most important rule for a beginner is that the C language, like most other programming languages, is case sensitive, that is, upper case letters are considered distinct from lower case letters and one can’t be swapped for the other. Keep this in mind when typing the code and when in doubt copy and paste it to make sure all symbols are correctly written.

We’ll use the terminal emulator application to type textual commands. I’ll succintly explain what each command does and will show each command written after a dollar sign. The dollar sign represents the terminal emulator’s prompt symbol, and that’s the usual symbol that the command line input program, called the shell, displays when waiting for a command. For example:
$ ls -l
In the above example, we type after the prompt symbol: ls[space][minus]l
, then press
[enter]
to execute. In this example, we execute the ls
program (list directory) with
the -l
parameter (a specific parameter for the program, changes the output mode to long,
or more descriptive). Same example, but also showing the resulting output in my computer:
$ ls -l total 7344 -rw-r--r-- 1 hdante users 9999 set 19 19:12 coffee-34251.svg -rw-r--r-- 1 hdante users 347 set 19 19:12 coffee-34251.svg.license -rw-r--r-- 1 hdante users 16805 set 19 19:12 favicon.ico -rw-r--r-- 1 hdante users 3253 set 19 19:12 favicon.svg -rw-r--r-- 1 hdante users 11665 out 2 18:49 index.html -rw-r--r-- 1 hdante users 17833 set 19 19:12 not-coffee.svg -rw-r--r-- 1 hdante users 2372085 set 19 19:12 radio.caf -rw-r--r-- 1 hdante users 2656465 set 19 19:12 radio.mka -rw-r--r-- 1 hdante users 2401864 set 19 19:12 radio.opus -rw-r--r-- 1 hdante users 3263 out 2 16:48 site.css -rw-r--r-- 1 hdante users 2166 set 19 19:12 square.svg $
So, after running the ls
program with the -l
parameter, it shows the long
description of current directory and another dollar symbol appears at the end, it’s
waiting for a new command. Don’t worry about understanding this example, I’ll also add
references at the end for learning more about using Unix-style command-line tools.
Installation

Different procedures are required for each operating system. Pick the best one for you. After installing, writing and running code will work the same way. We’ll write a single cross-platform program that works on all three systems and build it using the same cross-platform build tools.
macOS
For macOS, only the terminal emulator comes installed with the operating system. The Apple provided C compiler is the LLVM clang compiler, contained in the “Xcode command line tools” package. To install it, open the Terminal application by opening the Spotlight search input, then typing Terminal (or find it in the Utilities folder inside Applications). In the terminal emulator, type:
$ xcode-select --install
The Xcode installation program will start. Follow the instructions that will appear and in
the end, the LLVM clang C compiler will be installed. You can check if it’s working by
executing it with the --version
parameter:
$ clang --version
For installing packages like build tools, text editors and libraries, we’ll install the Homebrew package manager, which is the main Linux-style package manager for macOS. The package manager allows single-command installing, configuring, upgrading and removing of packages from the command line. Go to brew.sh and access the instructions there, or simply execute this command to download and install it:
$ /bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/master/install.sh)"
Notice that this command is already more complex than the previous ones, downloading and
executing the install script from the Homebrew source code repository, and gives a hint on
the power and efficiency of the command line. Don’t worry trying to understand it, we’re
installing Homebrew exactly to make it easy to execute complex package installations.
[edit 20201214: It might be necessary to restart the Terminal App and configure the
search path after installing brew
to be able to use it. When in doubt, check the
Homebrew documentation.]
Now that Homebrew is installed, you may install packages with brew install
and search
packages with brew search
. Search and install the GNU nano text editor with:
$ brew search nano
$ brew install nano
Linux

We’ll use the apt package manager available in Ubuntu Linux from the Debian family, but other Linux distributions using, for example, yum, dnf, pacman, etc. will work in pretty much the same way. On Linux the GNU Compiler Collection usually comes preinstalled, so we’ll use it instead of clang, but they work the same way. Open a terminal by clicking the activities menu on the top left of the screen and writing terminal in the search input (or click on the terminal icon in the favorites bar). In the terminal emulator, write:
$ sudo apt-get install gcc
Ubuntu Linux and most other distributions require switching to administrator (root) mode
to install programs, so we use the sudo
program to execute apt-get
in root mode (you
may also use the su
program, if sudo
is not installed). Type your password and follow
the instructions. In the end, check gcc:
$ gcc --version
You may install packages with sudo apt-get install
and search packages with apt-cache
search
:
$ apt-cache search nano
$ sudo apt-get install nano
For other Linux distributions, use the appropriate package manager:
$ yum search nano # for RedHat Linux/CentOS
$ sudo yum install nano # for RedHat Linux/CentOS
Windows

For Windows we’ll use the
MSYS2 toolkit,
which provides a terminal emulator, the pacman package manager and a complete set of
Unix-style command line tools. Download the MSYS2 installer from
www.
$ pacman -Syu
Follow the instructions and if requested restart the terminal emulator. On Windows we’ll use the GNU Compiler Collection C compiler (gcc):
$ pacman -S mingw-w64-x86_64-toolchain
After installing, to test gcc, you must use a MINGW64 shell. If you’re running the MSYS2 shell for executing pacman, open another terminal running the MINGW64 shell and type:
$ gcc --version
From now on, we’ll assume that the MINGW64 shell is being used. Notice that pacman runs
normally both on MSYS2 and MINGW64 shells. You may install packages with pacman -S
and
search packages with pacman -Ss
:
$ pacman -Ss nano
$ pacman -S nano
Hello, World !

After installing the compilers, remember installing the GNU nano text editor:
$ brew install nano # (or apt-get, or pacman)
Now, let’s create a new root directory to store this project and all future code:
$ mkdir code
$ cd code
The first command above creates a new directory called code
. If it already exists, it
will complain. Change the name if you prefer something else. The second command changes
the current directory to the code
directory (the current directory is the directory used
when file operations specify file names without specifying their directory locations). Now
create the hello
directory:
$ mkdir hello
$ cd hello
This creates the code/hello
directory (the hello
subdirectory in the code
directory)
and switches to it. Now let’s create the hello.c
file with the nano editor:
$ nano hello.c
C source code names have appended the .c
extension, like in hello.c
. This way, both
users and automated tools will know that this is a C program. The nano text editor will
open and you can type the program in it. Write your first program, like the following.
Remember: programming languages are very strict, so type it exactly. Upper case letters
are distinct from lower case letters and can’t be swapped.
#include <stdio.h>
int main(void)
{
printf("Hello, World!\n");
return 0;
}
Just for a quick explanation, the first line loads the stdio.h
function library, which
provides the printf()
function, used to write a message on the terminal. C programs are
always wrapped into functions. The function called when execution starts is called
main()
. Functions, like mathematical functions may return a value, like a result, and
they might also accept input parameters, like the two numeric values of a mathematical
sum, or in case of the main function, the input parameters given in the command line (like
-l
in ls -l
). C requires that we declare beforehand the input and output data types,
so our main function that starts the hello program receives zero (void
) parameters as
input and returns an integer value (int
) to the shell that executed the program. Braces
are used to delimit the instructions executed by the functions, called statements and
statements are executed in sequence. A complete statement in C does not finish with a new
line. Instead, we place a semicolon after each statement at the point where the statement
ends. In this case there are two single-line statements: one that calls a second function,
printf()
, and another that exits the main function, finishing the program and returning
the integer zero to the shell (return 0
). The message to be written is enclosed in
double quotes, "Hello, World!\n"
. The two final characters (\n
) are not garbage, they
are understood by the C compiler to send a newline command (like the [enter]
key does)
to the terminal after printing the greeting. We have also written the function’s contents
with indentation, where the contents do not start at the first column of text because we
added a [tab]
character before writing the statements. This is how professional
programmers do: brace-enclosed blocks of statements can be nested and each new block is
indented to make it quickly distinguishable from the rest of the code. The code structure
then becomes visually apparent.

ls
means list, cat
means concatenate and
grep
means globally search for a regular expression and print
matching lines.
After writing the program, save the file by typing [ctrl]+o
and [enter]
, then exit
nano by typing [ctrl]+x
. You can see the file the way you saved it with the cat
command:
$ cat hello.c
#include <stdio.h>
int main(void)
{
printf("Hello, World!\n");
return 0;
}
$
Now let’s compile it to generate a new executable:
$ cc -o hello hello.c
I’ll use the cc
command instead of gcc
or clang
because both create the symbolic
link from cc
to themselves and their behavior is the same. The cc
command creates the
hello
executable (or hello.exe
on Windows), given immediatelly after the -o
parameter, using the source code file names given, in this case only hello.c
. If any
error message appears related to the program, go back to the source code and check if you
have mistyped anything. When in doubt, copy and paste the code above. To execute the
program, type:
$ ./hello
Hello, World!
$
And there we go, our first C program ! Note: differently from ls
, cat
, cc
, etc.
commands, which are preconfigured in the shell’s directory search path, to be able to
execute hello, an explicit directory must be typed. In this case, the hello
file is in
the current directory (the current directory is always accessible with a single dot
character without being explicitly named). So to execute it, prepend its directory path:
./hello
.
Graphical Hello World

So, we’ve written a program that targets the terminal to display a message, which
basically only interacts with the user with input and output text messages. What about
creating a graphical window with buttons, mouse interaction and a larger greeting ? The
answer is that C only has a really small built-in library, called the
standard C library
(stdio.h
is part of it), that does not provide any way to creating a graphical user
interface. The C language, as standardized by international groups, only has a complete
support for user interaction using terminals, and the language designers have opted to
remain neutral regarding more complex functionalities, leaving the implementation
responsibility to each organization. This is intrinsic part of the language philosophy and
it’s what allows C to scale from devices with a few kilobytes of random access memory used
in embedded devices, like inside drones, credit cards, reactors, etc. to the largest
computers in the world that might occupy whole buildings and predict earthquakes and
storms and research new proteins.
To create the graphical Hello World, we’ll use the Gtk+ library, which is a portable library: it’s programmed in a way that it can work with different operating systems, in each case using different functionality provided by the vendors to achieve the same result. This is a recurrent theme in C programming: the language allows, with more or less effort, to write portable or cross-platform programs that work on heterogeneous environments. In our case, the additional effort to make our code portable is zero: we already cherry-picked great libraries and build tools that work this way. Our code will run on Linux, macOS and Windows.
So, let’s first install gtk:
$ pacman -S mingw-w64-x86_64-gtk3 # Windows
$ brew install gtk+3 # macOS
$ sudo apt-get install libgtk-3-dev # Ubuntu
Check if gtk is installed with the pkg-config
command:
$ pkg-config --print-variables gtk+-3.0
Now let’s create a new directory for our graphical hello world program:
$ cd
$ mkdir code/graphical_hello
$ cd code/graphical_hello
In the commands above, the cd
command without parameter changes to your home directory.
The others create and change to the code/graphical_hello
directory. Start nano to edit
graphical_hello.c
:
$ nano graphical_hello.c
Write the following program. As always, don’t worry about the complexity of the program by now. An additional hint is to read the code bottom to top, because the functions are written from the most detailed to the most general parts.
/* graphical_hello - A cross-platform graphical hello world
**********************************************************
* Works in macOS, linux and Windows.
* The program contains a text area where the greeting will
* appear and two buttons, one to display the greeting and
* another to clear the message.
*
* It uses the Gtk+ 3 graphical toolkit.
* See: https://www.gtk.org/
*
* Developed by: <put your name here>.
*/
/* Include the gtk header file to be able to use the functions. */
#include <gtk/gtk.h>
/* Function called when the user clicks the show button. */
void on_show_button_clicked(GtkWidget *show_button, GtkWidget *hello_display)
{
/* Display the greeting. */
gtk_label_set_markup(GTK_LABEL(hello_display),
"<span font='Italic 30'>Hello, World!</span>");
}
/* Function called when the user clicks the clear button */
void on_clear_button_clicked(GtkWidget *clear_button, GtkWidget *hello_display)
{
/* Display an empty message (clear the greeting). */
gtk_label_set_text(GTK_LABEL(hello_display), "");
}
/* Build the application window with all its components */
void build_window(void)
{
/* Declares the list of variables. Each one will represent one
* graphical element (called a widget). */
GtkWidget *hello_window, *layout;
GtkWidget *hello_display, *show_button, *clear_button;
/* Creates the text box (label) and two buttons. */
hello_display = gtk_label_new(NULL);
show_button = gtk_button_new();
gtk_button_set_label(GTK_BUTTON(show_button), "Display hello");
clear_button = gtk_button_new();
gtk_button_set_label(GTK_BUTTON(clear_button), "Clear");
/* Connects the "clicked" input events happening at the buttons to
* their callback functions (on_show_button_clicked() and
* on_clear_button_clicked()). */
g_signal_connect(show_button, "clicked",
G_CALLBACK(on_show_button_clicked),
hello_display);
g_signal_connect(clear_button, "clicked",
G_CALLBACK(on_clear_button_clicked),
hello_display);
/* Create a layout element to visually organize widgets. */
layout = gtk_grid_new();
gtk_grid_attach(GTK_GRID(layout), hello_display, 0, 0, 2, 1);
gtk_grid_attach(GTK_GRID(layout), show_button, 0, 1, 1, 1);
gtk_grid_attach(GTK_GRID(layout), clear_button, 1, 1, 1, 1);
gtk_grid_set_row_homogeneous(GTK_GRID(layout), TRUE);
gtk_grid_set_column_homogeneous(GTK_GRID(layout), TRUE);
gtk_grid_set_row_spacing(GTK_GRID(layout), 8);
gtk_grid_set_column_spacing(GTK_GRID(layout), 32);
gtk_widget_set_margin_start(layout, 20);
gtk_widget_set_margin_end(layout, 20);
gtk_widget_set_margin_top(layout, 20);
gtk_widget_set_margin_bottom(layout, 20);
/* Finally, create the main window and insert the whole layout
* inside. */
hello_window = gtk_window_new(GTK_WINDOW_TOPLEVEL);
gtk_window_set_title(GTK_WINDOW(hello_window), "Graphical Hello");
gtk_window_set_default_size(GTK_WINDOW(hello_window), 400, 300);
gtk_container_add(GTK_CONTAINER(hello_window), layout);
gtk_widget_show_all(hello_window);
/* Connects the "destroy" event, generated when closing the window
* to the gtk_main_quit() function, which asks to stop the event
* loop, so that the program can finish. */
g_signal_connect(hello_window, "destroy", G_CALLBACK(gtk_main_quit),
NULL);
}
/* main() function called when program starts. */
int main(int argc, char *argv[]) {
/* Call the gtk initialization function */
gtk_init(&argc, &argv);
/* Call our function to build the complete program window. */
build_window();
/* Enter Gtk+ main event loop (sleeps waiting for input events,
* like mouse or keyboard input, then wakes up calling the registered
* event callbacks and goes back to sleep until exit is requested). */
gtk_main();
/* After the main loop quits just return zero to the operating
* system. */
return 0;
}
That’s a lot of code. Now to compile it, we need a way to tell the C compiler that we’re
using external libraries beyond the C standard library. Since the amount of parameters may
be large, use pkg-config
to return all the parameters required. For example, in my
system it gives:
$ pkg-config --cflags --libs gtk+-3.0
-I/usr/include/gtk-3.0 -I/usr/include/pango-1.0 -I/usr/include/glib-2.0 -I/usr/lib/glib-2.0/include -I/usr/include/harfbuzz -I/usr/include/freetype2 -I/usr/include/libpng16 -I/usr/include/libmount -I/usr/include/blkid -I/usr/include/fribidi -I/usr/include/cairo -I/usr/include/pixman-1 -I/usr/include/gdk-pixbuf-2.0 -I/usr/include/gio-unix-2.0 -I/usr/include/atk-1.0 -I/usr/include/at-spi2-atk/2.0 -I/usr/include/dbus-1.0 -I/usr/lib/dbus-1.0/include -I/usr/include/at-spi-2.0 -pthread -lgtk-3 -lgdk-3 -lz -lpangocairo-1.0 -lpango-1.0 -lharfbuzz -latk-1.0 -lcairo-gobject -lcairo -lgdk_pixbuf-2.0 -lgio-2.0 -lgobject-2.0 -lglib-2.0
$
As you can see, building with gtk requires a lot of extra parameters, all of them
composing a set of directory search paths (parameters starting with -I
) and a collection
of library names (parameters starting with -l
) that describe the libraries locations and
interdependencies. Now either go ahead and add all of these parameters to your cc
command line, or embed the call to pkg-config
using the backtick operator:
$ cc -o graphical_hello -Wall -Werror -pedantic -std=c11 graphical_hello.c `pkg-config --cflags --libs gtk+-3.0`
I’ve also added a couple of more parameters to the compilation command that makes the C
compiler less forgiving about possible programmer mistakes: ‑Wall
enables extra analysis to look for warnings in the code, ‑Werror
converts all warnings to errors that force the compilation to stop and
‑pedantic ‑std=c11
makes the compiler behave in a way conforming
to the
ISO C 2011 standard,
blocking most non-standard extensions to the C language dialect used (that would probably
break code portability and lock you in a specific operating system).
As always check for any errors while typing to make it compile cleanly. Let’s run the program:
$ ./graphical_hello
And there you go ! We have a graphical hello world program that is portable to three operating systems, using external libraries with a single cross-platform code ! Actually it’s much more than three operating systems, since gtk will run on many Unix-like operating systems.
Using a build tool

Have you noticed how large the command line has become after we started using the external library ? The whole line returned by pkg-config is enormous. Imagine when you have a dozen of external libraries, multiple source files, maybe even mixing different programming languages, bundling data files, multimedia assets and so on. Building the whole program could become a giant set of giant commands. That’s where the build tools come in. They are specialized in finding the correct build tools for your systems, ordering the source files based on their dependencies, supporting partial rebuilding and parallelizing build commands. Here we’ll use the meson build system and write a build script in its own build language to make the project ready for extending the hello world program. Install both the meson and ninja build systems:
$ brew install meson ninja # macOS
$ sudo apt-get install meson ninja-build # Ubuntu
$ pacman -S meson ninja # Windows
Note that there are two separate programs. Modern cross-platform build systems are always
split in two: one part, called the meta build system contains all the “reasoning” about
the current build and target environments, but does not execute the build commands, only
generates an intermediate script. The second, on the other hand, knows nothing about the
different compilers and operating systems and only mechanically executes the commands in
the most efficient way, possibly parallelizing the build commands and skipping files that
didn’t change since last build. In our case, meson and ninja make up the dynamic duo.
Change to the graphical_hello
subdirectory and create the meson.build
file:
$ cd ~/code/graphical_hello
$ nano meson.build
Write the following build script:
project('graphical_hello', 'c', default_options: ['c_std=c11',
'warning_level=1', 'werror=true', 'buildtype=debugoptimized'],
version: '0.1.0')
srcs = ['graphical_hello.c']
exe = 'graphical_hello'
gtk = dependency('gtk+-3.0')
executable(exe, srcs, dependencies: [gtk], install: true)
Now execute meson:
$ meson build
In the command, build
is a directory name. Meson will create a subdirectory called
build
to place its build files, so that they don’t pollute your source directory. When
run, meson collects and displays information about the build environment, including the
detected C compiler, CPU type, checking if pkg-config exists and searching for Gtk+. If
the build requirements described in the build script file cannot be met, meson stops with
an error message. This would be the case if Gtk+ were not installed, for example. On
success, meson finishes by writing the build.ninja
script, which is the final script for
executing the build. After meson is run for the first time, it’s not necessary to run it
again, unless the build script changes. Just run ninja
:
$ ninja -C build -v
In the command, the -C
parameter describes the build directory, in this case, build
,
and -v
activates the verbose output. Take a look at the output to see how complex the
command line might become. To avoid showing too much information, remove the -v
parameter. Now, if you change the source code, just execute ninja
again and it will
recompile. Pretty easily huge command lines are dealt with. You can even install your new
program to the system directory:
$ ninja -C build install
Prepend the command with sudo
if your operating system requires it.
Conclusion
Congratulations, you now have everything set up to start programming in C and have already created two cross-platform programs, one terminal based and the other, graphical. You already integrated third-party libraries with your program and used a build system to deal with the build environment details. The next steps include actually learning about the C language, the standard C library, other portable third-party libraries, the Unix command line tools and replacing the GNU nano text editor with a more advanced programmer’s editor. Good luck !
References
- The Harvard University has a Youtube channel called CS50, a course of introductory computer science and programming, with multiple lectures, including many about C programming. You may either watch the whole playlist, or jump straight to C programming.
- The book Modern C, by Jens Gustedt, is a pretty dense book about C, full of tips, covering the ISO C17 standard and is available online and in printed form.
- There are many short C tutorials online. For learning quickly, check tutorialspoint or cprogramming, for example.
- The Debian Administrator’s Handbook is a great resource to learn about the Debian family of Linux distributions and contains an appendix with a short Unix course. Similar manuals are available for Red Hat, Fedora, Arch Linux, etc. See also the Ubuntu installation guide for help with installation.
- There’s a full Unix and programming course in coursera, provided by the Johns Hopkins University.
- There are zillions of books about Unix, including about overall usage, system administration, shell scripting and programming. See for example, a list of the 50 best Unix books of all time.
- The Arch Linux Wiki is an excellent community maintained reference site that can be used when searching about specific programs or doing specific configuration tasks.
- The GNU C library manual is an excellent manual for the standard C library (including Unix and GNU/Linux extensions).
- The ISO/IEC 9899:202x N2310 document is the latest (C2X) international draft document of the C standard. It’s the most technical and complete manual of the C language, called the language reference (very hard to read). It covers only the standardized dialect, without extensions, plus the standard C library, without extensions.
- The GTK Project documentation site contains user guides and references for the whole Gtk+ family of libraries, including Gtk proper, the Glib data structures and utilities libraries and many others. A more complete Gtk tutorial is available.
- The Meson build system website contains a great tutorial and a reference manual.