What C Is NOT
1. NO OBJECT-ORIENTED PROGRAMMING
C is not an object-oriented programming language because its core design and feature set do not provide the fundamental mechanisms that define OOP. C’s type system centers on primitives and aggregates such as struct and union, and does not bind functions to types syntactically. Its designers intentionally left out higher-level constructs to keep the language lean and give programmers explicit control over memory and execution. Providing object-oriented features would have increased complexity and potentially reduced the control C targets — OS kernels, embedded systems, compilers. Quora
To understand what C lacks, you need to understand what OOP actually is. OOP has four core pillars:
Encapsulation — bundling data and the functions that operate on it into a single unit (a class), and hiding the internals from the outside world.
Inheritance — a new type can automatically acquire all the properties and behaviors of an existing type.
Polymorphism — the same function name can do different things depending on what type it’s operating on, resolved automatically.
Abstraction — exposing only what is necessary, hiding implementation details behind an interface.
C has none of these as language-level constructs. There is no class. No extends. No virtual. No method dispatch table built by the compiler. No this pointer.
What C does have is struct — a way to group data together. That’s it. The functions that operate on that data live separately, and you have to pass the struct to them explicitly every time.
/* OOP style (C++) */
centrifuge.spin(1200); /* method bound to object */
/* C style */
spin(¢rifuge, 1200); /* function takes struct as argument */
C was designed to be an imperative procedural language because that is the simplest form of structured programming. This enabled its designers to make C highly efficient because it’s relatively easy to map its constructs directly to assembly language. Quora
Every OOP feature has a runtime cost. Method dispatch requires a vtable lookup. Inheritance requires layout compatibility checking. Polymorphism requires type information to be carried at runtime. C refuses all of these costs. What you write is what executes. No hidden machinery.
What this means in practice
In C you write procedures — step-by-step instructions. You call functions. You pass data. There is no object that “owns” behavior. There is no hidden constructor that runs before your code. There is no destructor that cleans up after. You are responsible for the entire lifecycle of every piece of data yourself.
The Stuxnet angle
Stuxnet’s Windows-side components were written in C++ — and used OOP deliberately. The dropper, rootkit, update system, and payload delivery were separated into class hierarchies, making the codebase modular enough for 30 engineers across potentially two countries to work on without breaking each other’s code.
The PLC payload — the actual centrifuge-killing code — was written without OOP, in bare STL/MC7. Because the PLC has no runtime support for classes. No vtables. No memory allocator. You write procedures. You call them from OB1. That’s it. The same model C uses.
2. NO GARBAGE COLLECTION
C and C++ were designed for use with manual memory management. Garbage collection relieves the programmer from doing manual memory management, where the programmer specifies what objects to de-allocate and return to the memory system and when to do so. Wikipedia
Garbage collection is a background process that automatically finds memory your program no longer uses and frees it. Java, Python, Go, C# — they all have it. You allocate an object, stop referencing it, and eventually the GC reclaims it. You never call free(). You never think about it.
C has no GC. Period. Not optional, not disabled by default — it simply does not exist in the language.
In C, memory lives exactly as long as you say it does. No more, no less. If you call malloc() and forget to call free() — that memory is gone for the life of the process. That is a memory leak. It accumulates. On a long-running server or embedded system, it eventually consumes all available RAM and the process crashes or freezes.
If you call free() and then use the pointer again — use-after-free. Undefined behavior. You are reading or writing memory that has been reclaimed and potentially reallocated to something else. This is one of the most exploited vulnerability classes in systems software history.
If you call free() twice on the same pointer — double-free. The heap allocator’s internal bookkeeping is corrupted. Exploitable.
Why no GC is a feature, not a bug
Garbage collection may take a significant proportion of a program’s total processing time, and affect performance as a result. Wikipedia
GC has to periodically pause your program, scan all live memory, mark what is reachable, and sweep what is not. In high-performance systems — OS kernels, real-time embedded firmware, game engines, network drivers — those pauses are unacceptable. A PLC running centrifuge control logic cannot stop for 50 milliseconds while a GC runs. It has hard real-time deadlines. Miss one, a motor controller behaves unexpectedly.
C gives you total control over when allocations happen and when they don’t. You can write C programs that never call malloc at all — everything on the stack or in static memory. Completely deterministic. Zero GC pauses. This is why embedded firmware is almost always written in C.
The Stuxnet angle
Stuxnet’s Windows-side dropper used dynamic memory allocation to build its attack structures, store intercepted PLC packets, and assemble the injected MC7 bytecode before delivering it. All of this memory was manually managed. The authors had to carefully track every malloc with a corresponding free, because a memory leak in a rootkit that’s supposed to live invisibly for a year would eventually cause the infected system to become unstable — and trigger investigation.
3. NO BOUNDS CHECKING
This is the sharpest edge in C. It is directly responsible for the most exploited class of vulnerabilities in the history of computing.
C provides no built-in protection against accessing or overwriting data in any part of memory. More specifically, it does not check that data written to a buffer is within the boundaries of that buffer. Wikipedia
When you declare an array in C:
char username[20];
You get exactly 20 bytes. The compiler does not track this at runtime. If you write 100 bytes into it, C does not stop you. It does not warn you. It does not throw an exception. It writes the 100 bytes starting at the address of username[0], which means 80 bytes overflow into whatever memory comes after the array — other variables, function call data, the return address of the current function.
Writing outside the bounds of a block of allocated memory can corrupt data, crash the program, or cause the execution of malicious code. OWASP Foundation
The consequences form a spectrum:
Silent data corruption — you overwrite a neighboring variable. The program continues but produces wrong results. This is the hardest to debug because there is no crash — just wrong behavior, possibly hours later.
Segmentation fault / crash — you write into memory the OS has not allocated to your process. The OS kills the program with a segfault. Annoying, but at least visible.
Code execution — you overwrite the return address on the stack. When the current function returns, instead of going back to the caller, execution jumps to an address you control. If you wrote shellcode into the buffer, you now execute arbitrary code with the privileges of the compromised process. This is a stack smash — the foundation of exploitation for decades.
A particular weakness in C is the absence of automatic bounds-checking for array or pointer accesses. Languages that are strongly typed and do not allow direct memory access, such as COBOL, Java, Eiffel, Python, and others, prevent buffer overflow in most cases. Computer Security
Why no bounds checking is a feature, not a bug
Every bounds check is a comparison instruction — if (index >= size) trap(). On a tight inner loop processing a million elements, that’s a million extra comparisons. C trusts you to know your bounds. The assumption is that you, the programmer, wrote code that stays within bounds — so no check is needed at runtime.
This makes C fast. It also makes C dangerous in the hands of anyone who doesn’t fully understand what they’re doing. Or in the hands of anyone whose input comes from an attacker.
The Stuxnet angle
Two of Stuxnet’s four Windows zero-days were privilege escalation vulnerabilities — taking code running at user level and escalating it to kernel level. Both exploited memory corruption bugs in the Windows kernel itself, which is written in C. No bounds checking. A crafted input overflows a buffer, overwrites a critical kernel structure, and suddenly attacker code is running at ring 0 — the highest privilege level on the machine.
This is the direct consequence of C’s no-bounds-checking design applied to security-critical system code written by humans who made mistakes.
4. NO EXCEPTIONS
Since C does not provide built-in exception handling like other high-level languages such as try-catch in Java or Python, error handling relies heavily on function return values, global variables, and system calls. GeeksforGeeks
In Java or Python, when something goes wrong — file not found, division by zero, null pointer — the runtime throws an exception. Execution immediately jumps to the nearest catch block up the call stack. If no catch block handles it, the program terminates with a traceback. The error is impossible to silently ignore — it propagates automatically.
C has none of this. When something goes wrong in C, the function returns a value. Usually -1 for failure, 0 for success, or NULL for a failed pointer. That’s it.
C does not provide direct support for error handling. By convention, the programmer is expected to prevent errors from occurring in the first place, and test return values from functions. For example, -1 and NULL are used in several functions such as socket() or malloc() to indicate problems that the programmer should be aware about. Wikibooks
Instead of throwing exceptions, a function indicates success, partial work, or failure through a numeric or pointer result. Library writers pick a convention for each function, and callers read that result immediately to decide what to do next. Substack
The critical implication: you can ignore a return value in C. The compiler will not stop you. The runtime will not stop you. If you call malloc() and don’t check if it returned NULL, and then try to write to that pointer — you crash. If you call fopen() and don’t check if the file actually opened, and then try to read from it — undefined behavior.
Every error check in C is voluntary. Every one you skip is a potential bug or vulnerability.
/* In Java — impossible to ignore */
FileInputStream f = new FileInputStream("data.txt");
// throws FileNotFoundException automatically if missing
/* In C — completely silent failure if unchecked */
FILE *f = fopen("data.txt", "r");
// returns NULL silently if missing
// nothing happens unless YOU check
fread(buffer, 1, 100, f); // crash if f is NULL — undefined behavior
The errno system
For system calls and library functions, C uses a global variable called errno to communicate why something failed.
errno is a global variable defined in the errno.h header file that indicates the error that occurred during a function call in C. When a function fails, the errno variable is automatically set to a specific error code, which helps identify the type of error encountered. GeeksforGeeks
ENOENT — file not found. EACCES — permission denied. ENOMEM — out of memory. EBADF — bad file descriptor. You call perror() or strerror(errno) to turn these into human-readable messages.
But errno is crude. It’s global — in multithreaded programs, two threads can clobber each other’s errno. It only tells you the last error. It gives no stack trace. It tells you nothing about what your own code did wrong — only what the OS or library returned.
Why no exceptions is a feature, not a bug
C favors explicit error handling through return codes and errno so the programmer controls how and when errors are handled, avoiding hidden jumps and implicit stack unwinding. C targets environments that lack a standard runtime or OS support such as embedded systems and kernels. Quora
Exceptions require a runtime unwinding mechanism — the stack must be traversed, destructors called, catch blocks located. This requires the language runtime to maintain metadata about every stack frame. On a bare PLC or inside an OS kernel, there is no such runtime. Exceptions are literally impossible to implement without infrastructure that doesn’t exist in those environments.
C’s manual error handling maps exactly to what hardware can do — return a value, check it, branch. Every error check is a visible if statement in your code. There are no hidden control flow paths. What you see is exactly what executes. This predictability is essential for safety-critical and embedded systems.
The Stuxnet angle
Stuxnet’s DLL replacement exploited a missing error check in WinCC’s initialization code. When WinCC loaded s7otbxdx.dll, it did not verify the DLL’s integrity — it just loaded it. No exception was possible because loading a DLL in Windows is a C-style operation: it either succeeds or returns NULL. WinCC checked for NULL (total failure) but did not detect that the DLL, while loadable, had been replaced with a malicious version. The error that mattered — wrong binary — had no return code. No errno. No exception. Just a loaded pointer to a function that did something different than expected.
The Full Picture — Why These Absences Matter Together
NO OOP → No hidden constructors, no vtables, no runtime type info.
You see every byte that moves. Total transparency.
Cost: You manage complexity manually.
NO GC → No pauses. No background threads. Deterministic timing.
Cost: Every malloc needs a free. Miss one = leak.
Double-free = crash. Use-after-free = exploit.
NO BOUNDS CHECK → No overhead on every array access. Maximum speed.
Cost: Write one byte past your buffer and you corrupt
memory. Write to the right place and you own
the machine.
NO EXCEPTIONS → No hidden control flow. No runtime unwinding overhead.
Cost: Every error must be checked manually.
Miss one check = silent failure, data corruption,
or exploitable condition.
These four absences are not oversights. They are the same decision stated four different ways: C trusts the programmer completely and protects them from nothing.
In exchange, C gives you a language that runs on anything with a CPU — from a 256KB PLC with no OS to a supercomputer with 512 cores. It gives you code that does exactly what you wrote, no more, no less. It gives you the ability to understand every byte of what your program does.
That total trust is a double-edged weapon. It is why C remains the foundation of operating systems, embedded firmware, and compilers forty years after it was invented. It is also why buffer overflows are still the most common class of exploitable vulnerability in existence. And it is why the people who wrote Stuxnet chose it — because to attack hardware at the level they needed to, you cannot afford any layer between your code and the machine.