The C language (or any programming language) can be characterized from different perspectives. Here, I listed three fundamental properties (or paradigms) of the language.

imperative

C is an imperative language. As programmers, we explicitly control the state or flow of the program by writing statements 1. Writing in C is similar to giving orders to a computer. Codes in C++ and Python are also usually written imperatively. The opposite is declarative. When we are programming with declarative languages, we describe what should be done, but not how. We do not control the flow of the program explicitly. SQL and Make are typical examples for declarative languages. Let’s look at some examples.

The following code may be a part of a C program.

//...

if (x > 20) {
    y = 5;
    foo();
} else
    bar();

//...

As you can see, we explicitly control the flow of the program by an if statement and give orders by calling functions foo() and bar() and somehow modify the program state by changing value of y.

The following SQL code (query) fetches data from a table named Users stored in a database.

SELECT * FROM Users WHERE username='admin' AND PASSWORD='admin' ORDER BY id DESC LIMIT 1;

Unlike C, in that case we describe what we want but not how the database engine should work to satisfy our query. We want a single row from table Users where both username and password are admin and if multiple users are found, user with the highest id is returned. We don’t care about the exact operations and flow executed by database engine, we only care about the result. But while programming in C, we must give orders and control the flow.

Notice that a language may support programming in both imperative and declarative fashion. For example in Python or in C++, one can program in both paradigms 2. But of course C is an imperative language.

procedural

Procedural programming paradigm is derived from imperative programming paradigm and C is a very good example of procedural programming 3. Actually procedural programming is a paradigm based on functional decomposition idea. Procedural programming reflects divide and conquer approach. As programmers programming in C, we take the problem and break it down to smaller problems. Then we write C functions to solve each smaller problem. Finally these C functions which solve a smaller part of the problem are combined together to solve the given original problem. Depending on programming language, part of the program that solves the smaller problem can be called function (as in C), procedure, routine, subroutine, method, etc. Procedural programming paradigm is very close to imperative programming paradigm. One can say that procedural programming languages are subsets of imperative languages 3.

For example, let’s say that our problem is getting an integer from the user and printing true if the given value is divisible by 3 and false otherwise. Although this is a very simple example, we can divide it into sub-problems and write a C function for each sub-problem.

#include <stdio.h>

int get_input(){
    int x;
    printf("What is your input: ");
    scanf("%d",&x);
    return x;
}

int isDivisibleby3(int a){
    return a%3;
}

int main(void) {
    int y;
    y = get_input();
    if (isDivisibleby3(y))
        printf("true\n");
    else
        printf("false\n");
}

As you can see, we write C functions (procedures) to get an integer from user and check divisibility by 3. Then we call the functions from main flow to solve the original problem. Calling procedures by controlling flow is the fundamental idea behind procedural programming.

Along with Procedural Programming there are many many programming paradigms like Object Oriented Programming (kind of imperative programming), Functional Programming (declarative programming), etc. Notice that a programming language can support coding in multiple paradigms. For example, one can write C programming style C++ codes following procedural programming practices. However C++ is a very well-known object oriented language. Furthermore C++ supports some functional programming features 4. There is even a separate book for that. Most of the programming languages like C++, Python, PHP are multiparadigm languages. But we can say that some languages like Smalltalk are intended to use with a single paradigm. For example, Smalltalk is a pure object oriented programming language.

It is the language designer’s choice that a programming language will or will not support a certain paradigm. For a given language, programming in a supported paradigm is easier because the language is designed to support that paradigm. But this doesn’t mean that one can’t write a program in not natively supported paradigm. For example, C is not an object oriented programming language. But one can write C programs with object oriented approach. There is even a book for that!

Object-Oriented Programming With ANSI-C by Axel Schreiner

Of course if the main aim is programming with object oriented approach then C isn’t a good choice because this approach is not native in C. But you can make it work…

So, C is a programming language that supports procedural programming paradigm.

statically typed

C is a statically typed programming language. This means that types of variables created in C are determined and checked at compile time. Variables have static types, they do not change during execution of the program. The opposite is dynamically typed. If a language is dynamically typed then types of variables are determined and checked at runtime or execution time. We can say that C, C++ and Java are statically typed languages whereas JavaScript, Python and PHP are dynamically typed 5 6.

Let’s look at some examples and start with a dynamically typed language, Python. In Python, type() function can be used to get type of an object.

1
2
3
4
5
6
7
8
x = 3
print (type(x))
x = "alper"
y = x + " yazar"
print (type(x))
x = 3.5
print (type(x))
y = x + " yazar"

The output is:

<class 'int'>
<class 'str'>
<class 'float'>
Traceback (most recent call last):
  File "<string>", line 8, in <module>
TypeError: unsupported operand type(s) for +: 'float' and 'str'

As you can see type of x is changed implicitly (by assigning values with different types) during execution. There are identical expressions at different lines: y = x + " yazar" on line 4 and 8. Although they are the same, the first one is OK since at that point type of x is str and we can add two strings together. However during execution of the last line, type of x is float and we can’t add a str with a float value. Since Python is a dynamically typed language, we were able to alter type of x at runtime. Notice that TypeError is caught when the program is run and hit the last line but not at the beginning of execution.

Now let’s look at a statically typed language, C. Getting type of a variable is not easy as in Python because C doesn’t have a built-in function like type(). However with the help of some macro definitions and generic selection expression support starting from C11, we can do something similar 7 8.

Let’s look at the following example C program. You don’t need to understand all lines.

// See: https://stackoverflow.com/a/17290414/1766391
// Tested with x86-64 gcc 12.2 with default flags on https://godbolt.org/
#include <stdio.h>
#include <stddef.h>
#include <stdint.h>

#define typename(x) _Generic((x),        /* Get the name of a type */             \
                                                                                  \
        _Bool: "_Bool",                  unsigned char: "unsigned char",          \
         char: "char",                     signed char: "signed char",            \
    short int: "short int",         unsigned short int: "unsigned short int",     \
          int: "int",                     unsigned int: "unsigned int",           \
     long int: "long int",           unsigned long int: "unsigned long int",      \
long long int: "long long int", unsigned long long int: "unsigned long long int", \
        float: "float",                         double: "double",                 \
  long double: "long double",                   char *: "pointer to char",        \
       void *: "pointer to void",                int *: "pointer to int",         \
      default: "other")

int main(void){
    int x;
    double y = 3.4;
    printf("x is %s\n",typename(x));
    printf("y is %s\n",typename(y));
    x = y; //Why not error?
    printf("x is still %s\n",typename(x));
    printf("x = %d\n",x);
}

The output is:

x is int
y is double
x is still int
x = 3

In this program, two variables are defined: x and y. Notice that the definitions explicitly indicate types of variables at compile time: type of x is int and type of y is float. Notice that in the middle of the flow we assign value (3.4) of y which is a float type variable to variable x which is a int type variable. But why this doesn’t cause an error at compilation or runtime? Because according to rules of C, during this assignment, an implicit type conversion occurs and 3.4 is converted to an int with value 3. This doesn’t violate and is not related to static type checking rules. Notice that type of x is always int regardless of assigned values.

Let’s see another C example. Compilation of the following codes fails. We can’t event get an executable file to test the code.

// Tested with x86-64 gcc 12.2 with default flags on https://godbolt.org/
int main(void){
    int *p1, *p2, *p3;
    p3 = p1 + p2;
}

Output from the compiler (not the output of the program) follows:

<source>: In function 'main':
<source>:3:13: error: invalid operands to binary + (have 'int *' and 'int *')
    3 |     p3 = p1 + p2;
      |             ^
ASM generation compiler returned: 1

But why this program is invalid? Because addition of two pointers in C is not a valid operation. The reason behind this is out of scope for now but the important point is that the violation is caught during compilation phase. C is a statically typed language and compiler can check type related rules during compilation, no need to wait until execution.

Summary

C is an imperative, procedural and statically typed language. In the upcoming posts, I will try to explain other programming language categorization perspectives.

Further Read

References