I have been using high-level OOP language (like C# and/or Java) for quite some time. I know patterns like the gang of four, can read and write code reasonably well and am considered a senior by his peers.

I am moving to low-level C development. As a result of my background I am unfamiliar with many of the technologies and skills inherent in lower level development/implementation.

What are the key differences in the technology and approach to development that I need to understand in order to be successful in C development when coming from a higher level OOP background?

  • 2
    That main thing you have to remember about C is that there's no safety net. In C# and Java you have garbage collection, object-orientation, a robust static type system, a robust packaging and build system, automatic variable initialization, etc. You have reference objects, which abstract pointers away from the machine. Commented Sep 24, 2015 at 15:48
  • 3
    I asked a meta question about this question: Improving “What should a programmer experienced in high-level OOP language learn when moving to low-level C development?”
    – user22815
    Commented Sep 24, 2015 at 16:28
  • 2
    Can we please not try to delete this until people have a chance to try to edit it into shape? Commented Sep 24, 2015 at 17:15
  • 3
    @KarlBielefeldt it's rather a blatant duplicate of How to think as a C programmer after biased with OOP language?, what would be the point to keep two identical questions
    – gnat
    Commented Sep 24, 2015 at 17:20
  • 2
    @Mandrill the other was closed as Primarily Opinion - which this one suffers from too. The core question there "What steps do I replace this with, when using a non-OOP Imperative Programming paradigm?" is entirely the answer's opinion. Furthermore, what works for one doesn't necessarily work for another resulting in answers of "For me, I did..." which leads to difficulty in actually finding the answer. This question suffers from the same underlying problem - the answers to the multitude of questions asked are going to be "try an arduino" and "I found the K&R helpful."
    – user40980
    Commented Sep 24, 2015 at 18:26

4 Answers 4


It will depend on how "close to the metal" you'll need to get. But a few common things you might find that may not be familiar to you:

  • Right off the bat, the fist thing you'll have to pay a lot of attention to is resource management. C doesn't run a garbage collector and doesn't have automatic finalizers (AKA destructors), so allocating a chunk of memory or opening a file/socket implies that you have to manually keep track of the lifetime of that resource and free it at some point in the program. That's probably the biggest shift in paradigm, coming from Java/C#, but once you get used to it, it's no big deal. We have very good tools nowadays that help tracking resource leaks, so you don't have to live in fear.

  • C doesn't provide a safety net. And by that I mean no array bounds checking, no memory guarding whatsoever and very little runtime protection against common programming mistakes. You might have heard that "C is a big gun, but it is very easy to shoot yourself in the foot with it" - and that is true, high execution performance requires making a few tradeoffs, and waving runtime checks is one of those. Any safety mechanisms that your program might need are implemented by you, the language doesn't offer much out-of-the-box.

  • C is very lightweight, and so is its Standard Library. The Standard Lib consists basically of a handful of IO functions and math functions, that's about it. If you're into embedded C, you might not even get that! No nice containers and generic algorithms. Again, in the spirit of the language, if your application needs a feature, it implements it. The language and library makes no assumptions for you.

  • Data typing is much more slack in C. This is debatable if good or bad, but it does allows for some interesting things like reinterpreting pointers and data read from files.

  • You have to be more aware about the hardware where your program is going to run. This is not particularly related to C, but to any flavour of low-level performance-oriented programming. The closer you get to the metal, the more you have to pay attention to things like CPU cache usage, memory footprint, device IO costs and algorithm time complexity. For instance, you might eventually find out after profiling that a plain array and linear search is faster than a binary search tree due to better cache access patterns in the target architecture.

  • You have to be more aware about the data, not just the code. The purpose of any program is to transform data, but it is easy to get carried away about the code and abstractions when writing high-level interfaces and forget that the code actually has to process some data and output some other data. The more you know your data, the easier it gets to optimize the code, that's data oriented design.

Those are some interesting aspects to consider. Certainly there are more, but that's what I can think of for the moment. I hope that's not too daunting. C is a great language to work with, even though I personally like C++ better ;). And low-level performance-oriented programming can also be a lot of fun. Once you enter it, you'll see programming in a whole new way.

  • Data typing isn't really more slack in the language described by C89, C99, or C11. While the C Standard's rules may be more sloppily written than those of some other languages, it allows compilers to behave in arbitrary fashion unless programmers abide restrictions that are even more confining than even those of Java. In Java, for example, if Cat derives from Animal, a function that expects to read items from an Animal[] could safely be passed a Cat[]. None of the C Standards provide any reasonable way of achieving comparable behavior, even when passing arrays of pointers.
    – supercat
    Commented Jul 2, 2018 at 21:27

The key difference is that:

  • When doing high level development, you are mainly dealing with how to solve a real world problem. Your "problem domain" is a real world situation, your entities are real world entities, and your logic is application logic. (The term "real world" is somewhat flexible, here, since for example you may be developing an MMORPG, like World of Warcraft, in which case the world is anything but real, but still, within that context, that's the world you are treating as real.)

  • When doing low level development, you are mainly dealing with how to get the machine to perform a specific task as efficiently as possible. The entities that you are dealing with are most of the time highly technical in nature, like memory blocks, block chains, hash values, etc. and your logic has nothing to do with the real world, and everything to do with the machine. You are often dealing with concepts that are completely machine-specific, like the endianness of machine words, or the I/O ports through which you can communicate with a specific device, and the sequences of byes that you have to send to it to get it to do its job.

As a matter of fact, when interfacing between high level code and low level code, it is often beneficial to transform the high-level entities into low-level entities (or simply view the former as the latter, without a need for an actual transformation) so that the high level code never needs to deal with machine concerns, and the low level code never needs to deal with application concerns.

So, in your application you may have a collection of, say, customers, and be doing things with it of the kind that we do with customers, like charging them for goods purchased, or sending them newsletters, without caring about the internal representation of the collection. In your low level code, however, you will be seeing the collection as a fixed length array of pointers to objects, and you may know nothing about these objects other than, perhaps, that they can be invoked to compare themselves to each other or to compute their own hash values. You are not supposed to know that they are customers, and you are not supposed to send newsletters to them in low-level code.

So, if you find yourself using a low-level language like C, and having to deal with application entities like "customers", you are doing it wrong. You have been asked to use the wrong language for the job.


Being an experienced C programmer and less experienced with C++ (but I have coded in projects started by other persons, just never built up a C++ project from scratch, myself), I might be traveling the same path in the opposite direction. We might be meeting in the middle somewhere and I can tell you a little of where I have come from.

Certainly there are OOPsie features of C++ that C doesn't have. There are namespace issues, templates, etc, that I have not made use of. All I have done is used and created classes and instantiated objects within the classes. And modified code in classes that other folks had created.

That said, any C programmer can write structured code that is similar, in many key respects, to fully OOP code. You have struct, typedef, pointers to struct, the ability to cast the pointer to one kinda struct to another where there are fields in common. There is also union in which you can explicitly overlay fields that are in common and other fields that are not in common. This looks a lot like inheritance that you'll find in an OOP language.

Personally, I dislike many specific syntax of C++ that I would rather just program in C where I know precisely what the "right" way to do it is. I don't know Java or C# or whatever variant.


Major differences:

  • You are responsible for all resource management. There is no garbage collection, nor are any buffers automatically extended if you need more space. This includes string management. C does not have a string type as such; strings are stored as arrays of char. If you want to append strings, you have to make sure the target buffer is large enough before doing so.

  • There is no structured exception handling. Errors and exceptional conditions are indicated by function return codes or writing to a parameter (or by setting errno). You can kind-of-sort-of fake exceptions using the setjmp/longjmp library functions, but it's a hack at best.

  • C does no bounds checking on array accesses. There are no built-in exceptions for accessing elements outside of an array. It is ridiculously easy to clobber memory you don't own (as can be seen by innumerable exploits over the years).

  • Data types are not smart - arrays don't know how big they are (they lose their "array-ness" in most contexts), struct types do not have any sort of methods (you can kind-of-sort-of fake it using function pointers, but there's no implicit this pointer; you explicitly have to tell each function which struct instance to operate on).

  • Code is organized into modules, rather than classes, where each module consists of type definitions, object definitions, and function definitions. There's no concept of inheritance between modules. Visibility for individual data items and functions is an all-or-nothing proposition - there's no equivalent for a "protected" visibility.

  • There are no container types beyond arrays. You want anything more sophisticated than that, you will either have to roll your own or find a container library. Similarly, there are no standard algorithms (such as std::copy or std::foreach - again, you'll have to roll your own).

  • There are no templates, generics, or other dynamic typing mechanisms. There's no overloading of functions or operators. All type information must be known at compile time.

  • There are no user-definable name spaces. C supports a grand total of four namespaces - labels, tag names, structure and union member names, and everything else.

It is possible to write OO code in C; it's just a lot of work.

Think of C as a nasty-looking table saw with exposed sharp edges, no blade guards, and visibly frayed wiring, and you'll be in the right mindset.

Not the answer you're looking for? Browse other questions tagged or ask your own question.