Although I primarily write PHP, I do also use C. There are lots of things I like and dislike about the language.
Size
C is a very small language – the smallest I’ve ever worked with. Even if you include the standard library, C is easily within the grasp of a single developer, whereas no one is going to master the entire PHP standard library. It is very satisfying when you realise you can keep most of a language in your head.
Portability
C is the most portable language, or at least the language with the most opportunity for portability (it’s easy to write non-portable C). If you have any given piece of hardware which is capable of running software, it’s probably got a C compiler target – if for no other reason than its operating system will be written in C. Also, Doom was written in C and has been ported to almost everything, including a lawnmower and printers.
Documentation
There are man pages available for most (all?) of the functions in the standard library, which don’t require network access. They usually contain examples, information about thread safety, which standards apply etc. Unlike the PHP manual, they don’t contain lots of user comments, most of which are out of date or unhelpful.
Manual memory management
Memory management is a faff, and something the vast majority of other languages hide away from you, with various degrees of success and performance. It is easy to get memory management wrong and create memory leaks or data corruption, and this has been the source of endless bugs in programs written in C.
If your data is small enough, you might be able to keep everything on the stack, in which case you effectively have automatic memory management in C.
Pointers
Most languages I’ve worked with have something superficially similar to pointers – they may also be called references. However, you can often get away without using them, although sometimes they can spring surprises (e.g. objects passed to functions in PHP are references by default in most cases, and assigning a variable containing an object to another variable doesn’t usually clone the object).
In C, you really have to get to grips with pointers for anything more than the most basic of programs. Pointer arithmetic is also something that is unusual to find in other languages, whereas in C it’s a core feature. It’s not unusual to see code like:
while ((*dest++ = *src++) != '\0');
That is part of the strcpy function in the standard library, and it copies a ‘string’ from one area of memory to another. It’s full of traps, such as: what happens if dest and src point to overlapping blocks of memory?
I also think it’s difficult to read, and probably a holdover from the times where compilers couldn’t optimise code as well as they can today. It feels like the person who wrote it was thinking about performance over readability.
Oh and don’t forget that arrays and pointers are not the same thing, even though they often appear to be and are often described as such.
Multiple compilers
One thing which is unique to C out of the languages I use regularly is that there are several widely-used implementations / compilers, and no official or default compiler. PHP and Go on the other hand have a default implementation which most people use. C has three major compilers, with a range of licences:
- GCC – GPLv3 licence (with exceptions)
- Clang / LLVM – Apache 2.0 licence (with exceptions)
- MSVC – proprietary licence
This competition has really helped – I can remember when GCC error messages were fairly unhelpful, but they improved a lot once LLVM came onto the scene. If you really want to test your code, you can try using multiple compilers to see if they spot different problems, or produce faster / smaller binaries.
Backwards compatibility
C prizes backwards compatibility. C code written to C89 will still compile on the latest versions of GCC and LLVM, and you can still tell them to require C89 compliance when compiling code today. Even the move from pre-C89 was careful to avoid breaking existing code.
In Python, moving from 2 to 3 broke the print statement by changing it to a function. Who on earth breaks something as basic as print in a major version? printf on the other hand still works the same way as it always has.
PHP seems reasonably good in this regard for the most part, until you remember all the really important functions that were deprecated and then removed, such as all the mysql_ and ereg_ functions (I continue to patch legacy codebases which are still using those).
Slow but continued evolution
C continues to get a new standard every 5-10 years. Not so fast that you feel overwhelmed and unable to keep up, but not so slow that you think it’s dead. If you really want to live on the bleeding edge, the latest versions of compilers will often include support for features that are likely to make it into the next standard but haven’t been officially confirmed.
Closed standard
The one big problem I have with C is that it is a closed standard. The process is controlled by an ISO working group (JTC1/SC22/WG14), whose membership consists mainly of representatives from large organisations. There is a charge for the standard, which means it isn’t available to everyone, although you can usually get drafts for free.
Of course, you don’t need a copy of the standard to write C, and the language isn’t encumbered by patents. It would be nice though if ISO would at least make earlier standards available for free – especially as they receive funding from countries which raise revenue from taxation, so people like me have already given them money indirectly (I think the outputs of bodies funded by taxation should be freely available by default).
Ease of mistakes
It’s really easy to make mistakes in C, especially when it comes to that dreaded phrase: Undefined Behaviour (which basically means your compiler can do anything when it encounters it).
Having said this, if you compile with the following your life will be much easier:
- –std=c89 – or whatever standard you are targetting
- -pedantic – actually enforce standards compliance
- –Wall – enable ‘all’ warnings
- –Wextra – enable those ‘extra’ warnings that aren’t included in ‘all’ (but still not all available warnings…)
- –Werror – compiler warnings are errors, don’t ignore them
The above flags, combined with a modern compiler, will prevent you making a lot of mistakes, even if you are targetting old standards.
Ideally you’d use -Weverything instead of -Wall and -Wextra, but only the most basic programs will compile with every warning enabled (and the LLVM developers don’t recommend its use in production).
Understanding the machine
I still think if you want to understand what actually happens to your code when it executes, you need to learn C and assembly language. I’m sure there are people who think this is an outdated position, but I’m sticking to it.
Cross compilation
Cross compilation of operating system (e.g. targetting Windows from Linux) or architecture (e.g. targetting MIPS from x86) is a pain. There are some projects which attempt to make this easier, such as crosstool-ng, but it is hard to get right – I eventually gave up trying to get a GCC cross compiler to work (the original use case was a student at the university I was working at wanting to add new instructions to the MIPS architecture on Linux, but trying to compile their code under Windows on the x86_64 architecture).