Radio

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.

Ken Thompson (sitting) and Dennis Ritchie at PDP-11
Dennis Ritchie (standing), the creator of the C programming language, with Ken Thompson, the creator of Unix. Ken is using a teletypewriter (known as a tty on Unix) typewriter-style physical terminal. Behind is the DEC PDP-11 minicomputer.

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.

PDP-11 terminals
The DEC VT55 (display-style terminal, left side), the DECwriter (dot-matrix printer-style terminal) and the Teletype ASR-33 (typewriter-style terminal).

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

macOS Terminal App
The macOS Terminal.app is a terminal emulator. Modern terminals are software that emulate physical terminals.

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

gcc on Ubuntu
Installing gcc on Ubuntu 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

MSYS2 website
MSYS2 website. MSYS2 is a toolkit for Windows development that contains Unix-style tools.

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.msys2.org and follow the instructions to install the package. When installed, MSYS2 will provide three sets of programs, which are selected whenever opening the terminal emulator. They are called the MSYS2 shell, the MINGW64 shell (pronounced mingwee 64) and the MINGW32 shell. The MINGW64 shell is the appropriate one to develop Windows programs. The MSYS2 is appropriate for managing MSYS2 itself and the MINGW32 is the 32-bit version that shouldn’t be used anymore. After installing MSYS2 it’s necessary to immediatelly upgrade it. Open the MSYS2 shell and type:

$ 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 !

GNU nano on macOS
Writing the Hello World program using GNU nano on macOS.

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.

Woman yelling at a cat meme
On Unix, commands have short names and were created without much consistency. 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

Graphical Hello World
The cross-platform graphical Hello World running on Ubuntu Linux.

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

Meson build on Windows
Building the complete project on Windows with two simple commands: meson and ninja. With a build system, you can distribute your cross-platform source code so that other developers can easily build the project in their own machines, even if they have completely different operating systems and configurations. For free software projects, you can even place the project in a public repository to attract users and contributors.

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.