14 Properties of The C Programming Language

14 Properties of The C Programming Language

What kind of language is C? What are the properties? Which paradigms does it belong to? How can we categorize it?

Any programming language can be categorized from different perspectives. These categories are also called paradigms. In this article, we will look at the properties of C and the paradigms to which C belongs.

As in real life, a property is not binary (0 or 1). Most of them lay on a scale. For feature x, one can say that C language is not as much x as language Y. Furthermore, in another resource, C can be said to belong to a different paradigm or category. Some of them are open to discussion. The following reflects my understanding.

Here there are 14 properties of C in alphabetical order. Of course, there may be other features besides these. But I think I've listed the fundamental ones. You can add your suggestions in the comments 👇.

🤖 artificial

C is an artificial language. You might say:

Hmm…, that is not very interesting, it should be artificial since it is a programming language, a language for computers not for humans, right?

Yes, this argument makes sense. However, some programming languages are closer to natural languages (human languages, like English or Turkish) than others. Consider our old friend COBOL, from the 60s.

ADD 1 TO x
ADD 1, a, b TO x ROUNDED, y, z ROUNDED

ADD a, b TO c
        DISPLAY "Error"

ADD a TO b
        DISPLAY "No error"
        DISPLAY "Error"

It sounds closer to English, right? Or consider this SQL statement:

SELECT * FROM users WHERE name = '' OR '1'='1';

It sounds like an English sentence. Now let’s look at some C code:


This looks more artificial than COBOL or SQL, only while sounds like English.

Almost all programming languages are artificial, but like many programming languages, C is closer to a machine than a human.


💚 efficient

C is an efficient language. If efficiency is mentioned in a context, we should question what kind of efficiency is being referred to. In this context, efficiency refers to the number of instructions generated when the code is compiled. Low-level languages like C and C++ generally perform more efficiently in terms of execution speed and memory consumption compared to high-level languages. For this reason, these languages are sometimes referred to as energy-efficient or 🌍 green languages.

From another criterion, high-level languages can also be more efficient. It depends on how easily a program that performs the same task can be written in the respective language. In other words, from a developer’s perspective, high-level languages like Python can be more efficient than languages like C. While writing code in high-level languages is more efficient (accomplishing more in less time), the execution of the code may not be as efficient as in low-level languages. As a result, it is incorrect to claim that any programming language is the best or the most efficient in every aspect.

Due to its efficiency and closeness to (medium-low) hardware both in terms of efficiency and level, the C language is one of the ideal languages for system programming or writing operating systems.

🎨 expressive

C is an expressive language. But what does it mean for a programming language to be expressive? We can define it as the ability to easily convey our intentions, thoughts, and what we want to achieve in our code to the compiler or other programmers. C’s strong language design, rules, syntax, and semantic approach have made it possible to write many algorithms or programs clearly and straightforwardly, making C a powerful language. Its influence can be seen in many programming languages. If you’re interested in this topic, you can also read this article. Here is a quote from the article:

A language is expressive when it allows a programmer to easily convey his/her intent, with errors detected early. In contrast to assembly languages, C allows one to constrain variables to hold values from specific types, so it is more expressive here. In contrast to C, Java allows to test if a value is within the length a.length of an array a, so it is more expressive here. Contrary to Java, Ada allows one to constrain scalar variables to hold values from specific ranges, so it is more expressive here.

On the other hand, newer programming languages (including C++) are considered more expressive than C, in general. This lies on a scale rather than a binary property.


⭕ general purpose

C is a general-purpose programming language. It can be used to solve different problems on many platforms (embedded, Windows/Linux, x86/ARM/RISC-V, etc.) efficiently. This is true because C is not designed and optimized for a specific problem type or architecture. Almost all known languages like C, C++, C#, Java, JavaScript, and Python are in that category.

However, some languages are created to solve some specific domain problems or to program some special pieces of hardware, like FPGAs. These languages are called Domain-specific languages (DSL). SQL and HDL languages like Verilog and VHDL are DSL examples.

👮 imperative

C is an imperative (buyurgan in Turkish) 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 (bildirimsel in Turkish). When we are programming with declarative languages, we describe what should be done, but not how. With declarative languages, we do not control the flow of the program explicitly. SQL and Make are typical examples of declarative languages. Let’s look at some examples.

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


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


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 the value of y. Depending on the result of the comparison of x by 20, the flow of the program takes a different direction. Each line tells the computer what to do explicitly.

A Declarative Example

The following SQL code (query) fetches data from a table named Users that are 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 the table Users where both username and password are admin and if multiple users are found, the user with the highest id is returned. Did we implement the sorting algorithm used to sort entries? No! We don’t care about the exact operations and flow executed by the database engine, we only care about the result. But while programming in C, we must give orders and control the flow.

Computer Architecture and Imperative Programming

The imperative programming paradigm is the natural way of computer programming. The basics of computer and processor architectures haven’t changed too much since the beginning of the microprocessor era. Almost all processors work by executing orders (instructions) sequentially. Some instructions (branch instructions) change the flow that the processor follows. From that perspective, programming in C is very parallel to programming in assembly language, only at a slightly higher level.

Programs coded in a declarative language run on the same processors. There is no different processor to run SQL code or a procedural language. The difference between these two paradigms comes from the perspective that a programmer sees the computer. Of course, the database engine executing SQL codes runs similar instructions on the processor as a C program. The engine itself could be programmed in an imperative language, like C. In general, if you use a declarative language, computer architecture becomes more abstract to you compared to programming in an imperative language. This doesn’t have to be necessarily a good or a bad thing. As always, it depends on your needs…

Functional Programming

The functional programming paradigm is a subset of the declarative programming paradigm. There are purely functional languages like Haskell. Nowadays, most modern programming languages like Python or C++ have some tools that allow us to program in a functional programming fashion (like lambda functions).

A programming language does not have to be in either category. Most of the programming languages are multi-paradigm languages supporting both paradigms more or less.

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

Unlike many modern languages, C does not support the declarative programming paradigm inherently. It is possible to add some declarative programming features to C with 3rd party libraries. However, extending a language with 3rd party libraries doesn’t change its paradigm or category. The key point is that the features should be native to that language.

⬇ middle-low level

The level of a programming language is a measure of its position between a computer and a human. Lower-level languages are closer to a computer than high-level languages. For example, an assembly language for a specific processor is one of the lowest-level languages available for that processor/system. On the other hand, GUI-based languages like Scratch are one of the highest-level ones. Higher-level languages create abstracted computer models. Programmers usually don’t think about CPU architecture, memory organization, etc. when programming with high-level languages. However, programming in low-level languages usually requires more insight into computers, otherwise one might not benefit from programming in a low-level language.

We can divide languages into 3 categories according to their levels: low level, middle level, high level. C is generally considered a middle-level language, though it is very close to the low end.

When a C program is compiled, the generated assembly program reflects the C source code line-by-line generally. Thus, sometimes C is called a portable assembly language. It is a compact and not an over-bloated language like high-level languages. Many high-level languages have different sophisticated mechanisms like garbage collection to ease the programmer’s job. However, the C programmer is responsible for everything!

The level of a language might change with technology. For example, as I mentioned in A Brief History of C Programming Language 📜 , rewriting UNIX in C was a revolutionary step back in 1973 because an operating system was written in a high-level language which was C!

Now, we have Python, Java, etc. or GUI-based languages like Scratch or Simulink, and they sit at far higher levels than C today. In the future, the positions could be changed and they could become middle-level language.


YouTube Link

🆓 non-proprietary

C is a non-proprietary language. Some languages are proprietary. These languages belong to a group, or they are controlled by companies to primarily satisfy their needs. For example, MATLAB (programming language) from MathWorks is in this category. Of course, being a proprietary language does not mean necessarily a bad thing. On the other hand, most of the well-known and widespread languages like C, C++, and Python are non-proprietary languages. Their development is guided by a committee, not by a single company.

🎊 popular

C is a popular language, there’s no doubt about that. Almost all operating systems kernels such as Linux are written in the C language. C is also widely used in embedded systems where microcontrollers are used. Even though C is 50 years old, C is still one of the most preferred languages for software that needs to be run close to the hardware, has performance requirements, and takes up little space. C and C++ are also preferred in applications such as system programming that run on the operating system but have high-performance requirements. When you write code in C, you can more easily understand exactly what the code you wrote does on the hardware.

Of course, C is not a language that solves all problems most optimally. If there was such a programming language, it would be used for all solutions and there would not be so many languages. But as I mentioned, there are areas where C shines and there is not much of an alternative. If you work close to these areas, you can feel how popular C is. Still, people rightfully feel the need to represent such subjective feelings numerically. So how can we measure how popular a language is?

One of the measures that numerically shows the popularity of a programming language is the TIOBE Index. There are various debates regarding the method and accuracy of this measurement, which I will address in later sections. Let’s continue with this measurement for now.

The TIOBE Index is calculated monthly and measures how popular programming languages are around the world. As of November 2023, when I wrote this article, the C programming language is the 2nd most popular language. Python ranks 1st and the C++ language ranks 3rd. When we look at the 35-year history, we can see that the C language has always been among the top 2 most popular languages according to this index.

TIOBE Index uses a controversial measurement method. They measure the number of results indexed by various search engines for a programming language. This method is quite controversial and there are many opinions about the meaninglessness of this measurement method. I wrote a detailed article on this topic, in Turkish.

At the end of the day, is C a popular language or not? So is this important to us? When we look at such measurements, we can see that languages such as Verilog and VHDL used in the field of FPGA and ASIC design are not popular and widespread at all, so we certainly shouldn’t waste time learning these things, right? NO! Those languages are mandatory for people working in these fields to learn. Whether a language is popular or not, we should learn that language if we need it. As I mentioned at the beginning, C is a superstar in some areas. If you are going to work in these fields or if you are curious, you should learn C. Other than that, I would say don’t worry too much about the popularity numbers.


👜 portable

Portability is a measure of the easiness of porting a program written for an architecture/platform to another architecture/platform. One of the least portable languages is assembly language. Assembly programs are considered to be non-portable since the Instruction Set Architecture (ISA) of each processor family is different. C is different. In general, most C programs can be ported easily (with relatively low effort) to another platform/processor, as long as there is a compiler for the target system. This is why rewriting UNIX in C was a very crucial step for UNIX history back in the 70s because it made porting the operating system to other architectures much easier (compared to porting an operating system purely written in assembly).

C is a source-level portable language. When a C program is compiled, it is compiled for a specific platform or architecture. In general, the final executable binary file is only compatible with the target system. For example, a binary compiled for an x86/Windows system won’t run x86/Linux or any ARM architecture. The same source code has to be compiled for all target systems separately with little or no modification. This is why C is portable at the source code level. Since C is a relatively low-level language, sometimes it is called a portable assembly language.

Keep in mind that, in theory, C is portable, but in practice due to the different natures of operating systems, architectural details and implementation-defined behaviors of compilers, porting a fairly complex C program is not as easy as it sounds.

Source code level portability was a miracle for assembly programmers. However, the industry has invented other portability levels. For example, programs written in C# and Java are called binary portable languages. Compilers of those languages create machine-independent intermediate code. Then, it is run by a sort of runtime software (like Java Runtime Environment) on the target platform. This makes running the same binary file on different platforms possible. Of course, binary portable languages need different runtime software for each target platform, just as different source-level portable languages need different compilers for portability.


〽 procedural

Procedural programming (prosedürel programlama in Turkish) paradigm is a subset of the imperative programming paradigm. Another famous paradigm under imperative programming is object-oriented programming (OOP) (nesne yönelimli programlama in Turkish) [1]. C is a very good example of procedural programming [3].

Procedural programming is a paradigm based on the functional decomposition idea. Procedural programming reflects the divide-and-conquer approach.

Philip II of Macedon: “Did somebody say divide and conquer?” Origin

As programmers programming in C, we take the problem and break it down into smaller problems. Then we write C functions to solve each smaller problem. Finally, these C functions which solve a smaller part of the original problem are combined to solve the given original problem. Depending on the programming language, part of the program that solves the smaller problem can be called function (as in C), procedure, routine, subroutine, method, etc.

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("Please enter an integer: ");
    return x;

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

int main(void) {
    int y;
    y = get_input();
    if (isDivisibleby3(y))

As you can see, we write C functions (procedures) to get an integer from the user and check divisibility by 3. Then we call the functions from the main flow to solve the original problem. Calling procedures by controlling flow is the fundamental idea behind procedural programming. For example, get_input() asks the user to enter a number and returns that number. This is a part of the problem, getting a number from the user. The other function, isDivisibleby3(), solves another part of the problem: checking the divisibility of a number by 3. These two functions are completely independent of each other, and they are not aware of each other’s existence. In main(), they are called in a logical order. This is the main motivation behind procedural programming: divide, conquer and build the solution.

Along with Procedural Programming, there are 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, and PHP are multi-paradigm languages. But we can say that some languages like Smalltalk are intended to be used 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. One can write C programs with an object-oriented approach. There is even a book for that!

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

Computer Architecture and Procedural Programming

Previously, I said that the imperative programming paradigm is a natural way of programming if we consider processor architectures.

This is also true for procedural programming. All processors (including ancient ones) natively support routines (functions in C). Almost all processors have instructions like GOSUB (go to subroutine) or RET (return from subroutine). If we write an assembly code, that program will consist of (imperative) instructions and some subroutines (procedures). At the end of the day, programming in C is similar to programming in assembly but of course much easier.

Compared to object-oriented programming, procedural programming are more native way of programming considering computer architecture. However, as I said previously, this doesn’t mean that one is a better way of programming than the other. It solely depends on your needs.

In summary, C is a programming language that supports the procedural programming paradigm.

📓 standard

C is a standardized programming language. It has formal standard documents published by ISO. Widespread languages like Python and Perl are not formally standardized unlike C, instead, they have some documents that serve as a de facto standard. Remember that The C Programming Language book was de-facto C standard between 1978 and 1989 (until the first standardization of C).

Names of the official C standards are in ISO/IEC 9899:<YEAR> format. For example, the official name of the C11 is ISO/IEC 9899:2011.

There are high-quality and both community-driven and company-driven C compilers like GCC, Clang/LLVM, MSVC, etc. thanks to the standards. Compilers that implement rules defined in the standards are called conforming compilers.

For further information, I recommend reading the Standards section of the following article:


statically typed

Almost all programming languages have a type system (tür in Turkish). For example, variables and constants have types associated with them. Most of the languages have rules regarding type systems, like type conversion rules. These rules can be checked at different times. In general, types of objects are determined, and type rules are checked at the compilation phase of compiled languages. On the other hand, these steps are done at runtime for interpreted languages. If type-related steps (like determining types of objects) are done at compile time, we call these languages statically typed languages. If those steps are done at runtime, then the language becomes an example of dynamically typed language.

In statically typed languages, types of objects (like variables) are determined at compile time and types of variables don’t change during runtime. Typically, in dynamically typed languages, types of variables are determined with the first assignment. The variable gets the type of the first assigned value, but this doesn’t mean that the variable’s type can’t change. During execution, if another value with a different type is assigned to the same variable, then the type of the variable changes. This is not true for statically typed languages. In statically typed languages, the type of variable always stays unchanged regardless of the type of the assigned value.

Since types of objects stay constant in statically typed languages, compilers can check the type rules of the language during compilation. By doing so, most of the type-related errors can be caught at compilation time, before running the program. In contrast, most of the dynamically typed languages can check their rules at runtime. These languages can throw exceptions at runtime when a rule violation is observed.

C is a statically typed language.

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() can be used to get an object’s type.

x = 3
print (type(x))
x = "alper"
y = x + " yazar"
print (type(x))
x = 3.5
print (type(x))
y = x + " yazar"

The following output is produced when the program runs:

<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, the type of x is set and changed implicitly (by assigning values with different types) during execution, in other words, at runtime. 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 the execution of the last line, the 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 the type of x at runtime. Notice that TypeError is caught when the program is run and hits the last line, but not at the beginning of the execution. Why? Because Python is a dynamically typed language and checks the type-related rules at runtime.

Now let’s look at a statically typed language, C. Getting the type of variable is not as 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 the 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 the types of variables at compile time: type of x is int and type of y is float. These types will stay constant during the whole life of the program. Notice that in the middle of the flow, we assign a 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? According to rules of C, during this assignment, an implicit type conversion occurs and 3.4 is converted to a hidden and temporary int object with value 3. This doesn’t violate any rule and is not related to the static type-checking concept. Notice that the type of x is always int regardless of the assigned values.

Let’s see another C example. Compilation of the following codes fails. We can’t even 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 the 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 the compilation phase. C is a statically typed language and the compiler can check type-related rules during compilation, no need to wait until execution.


The following video is about JavaScript but explains the concepts well.

🏗️ structured

C is a structured programming language. The opposite of structured programming is non-structured programming or unstructured programming. Here there are 5 typical characteristics of a structured language:

  1. Control Structures: Enables clear flow control using loops and conditional statements, improving readability and maintainability. C has control structures like if, switch..case, for, while.

  2. Modularity: Supports dividing code into reusable, independent functions or modules for better organization and testing. In C, we use mainly functions to obtain modularity. C includes a standard library that provides a set of predefined functions for performing various tasks, further supporting structured and modular programming.

  3. Local Variable Scope: Encourages variables to be declared within their usage scope, reducing side effects and enhancing code safety.

  4. Sequential Execution: Maintains a predictable and straightforward flow of program execution within each structure.

  5. Limited Use of GOTO Statements: Discourages the use of GOTO statements, preventing complex and unmanageable "spaghetti code." Although C allows GOTO statements, it should be used with caution.

While C is a structured programming language, it's also flexible and allows for different programming styles. This flexibility means that it's possible to write unstructured code in C, but the language itself provides all the necessary features to support structured programming practices.

〰 weakly typed

Almost all languages do some type-checking and apply type-related rules. Some languages are more "paranoid" in type. They have strict rules and want programmers to explicitly indicate type conversions and do minimal implicit type conversions. They are strongly typed languages. On the other hand, weakly typed languages are more permissive towards type rules, they do not hesitate to do an implicit conversion wherever it is needed. This is related to a concept called type safety concept.

Unlike most of the categories and paradigms, the strength of the type system is a continuum. Some languages have stronger type rules than others. IMHO, this is one of the most controversial categories. Considering C, you can find many resources on the Internet on this topic. Some of them say that C is weakly typed and others say the opposite. Interesting, right? Let’s see what the authors of The C Programming Language book (K&R C) about this topic:

C is not a strongly-typed language in the sense of Pascal or Algol 68. It is relatively permissive about data conversion, although it will not automatically convert data types with the wild abandon of PL/I. Existing compilers provide no run-time checking of array subscripts, argument types, etc.

This quote is from the 3rd page of the first edition (1978) of the book.

As we can see the bosses didn’t put C in the strongly typed languages. But also, they didn’t tag the C language as a weakly typed language explicitly. In the book, they suggest the usage of linter tools to make the type system and type-checking rules stronger.

The second edition of the book was published in 1988 and this edition almost covers ANSI C. In that book, the authors say that

C is not a strongly-typed language, but as it has evolved, its type-checking has been strengthened.

After that, improvements taken to make the language type system stronger are explained.

The C language sets the programmer free as much as possible. The programmer has the highest responsibility. The language itself doesn’t check what the programmer is doing too much. Since it is designed to write an operating system and very low-level tools, the programmer should be able to do some memory tricks. This is the philosophy behind the language. The book follows as:

Nevertheless, C retains the basic philosophy that programmers know what they are doing; it only requires that they state their intentions explicitly.

As you see, if you program in C you should know what are you doing.

If we think of a line with two ends, the C language is positioned close to the weak end. So, C is a weakly typed language.

Please keep in mind that this topic isn’t related to being a statically typed or dynamically typed language. Python is a dynamically and strongly typed language, whereas C is a statically and weakly typed language.

C is considered a weakly typed language because it does some implicit type conversions to help the programmer. From a memory safety perspective, it almost does not check anything regarding memory access. For example, one can easily access beyond array bounds.

JavaScript is a pretty weakly typed language. Let’s look at the following code. Your mind may blow off!

// https://www.programiz.com/javascript/online-compiler/

console.log(4 + '7');
console.log(4 * '7');
console.log(2 + true);
console.log(false - 3);

The output is:


This is very interesting. In the first line, the addition does a string concatenation. In the second line, the character 7 is interpreted as an integer and a multiplication is performed.

You will never think of the C language as a weakly typed language again once you see JavaScript.

📚 Resources

  • Personal notes from Necati Ergin’s C course

  • Personal notes from Kaan Aslan’s Unix/Linux System Programming Course

🔗 References