Why the C programming language still rules

The C language has been a software development staple for five decades. Here’s how it stacks up against C++, Java, C#, Go, Rust, and Python in 2019

No technology sticks around for 50 years unless it does its job better than most anything else—especially a computer technology. The C programming language has been alive and kicking since 1972, and it still reigns as one of the fundamental building blocks of our software-defined world.

But sometimes a technology sticks around because people just haven’t gotten around to replacing it. Over the last few decades, dozens of other languages have appeared—some explicitly designed to challenge C’s dominance, some chipping away at C from the side as a byproduct of their popularity.

It isn’t hard to argue C needs replacing. Programming language research and software development practices all hint at how there are far better ways to do things than C’s way. But C persists all the same, with decades of research and development behind it. Few other languages can beat it for performance, for bare-metal compatibility, or for ubiquity. Still, it’s worth seeing how C stacks up against big-name language competition in 2018.

C vs. C++

Naturally, C is compared most commonly to C++, the language that—as the name itself indicates—was created as an extension of C. The differences between C++ and C could be characterized as extensive, or excessive, depending on whom you ask.

While still being C-like in its syntax and approach, C++ provides many genuinely useful features that aren’t available natively in C: namespaces, templates, exceptions, automatic memory management, and so on. Projects that demand top-tier performance—databases, machine learning systems—are frequently written in C++ using those features to wring every drop of performance out of the system.

Further, C++ continues to expand far more aggressively than C. The forthcoming C++ 20 brings even more to the table including modules, coroutines, a synchronization library, and concepts, which make templates easier to use. The latest revision to the C standard adds little and focuses on retaining backwards compatibility.

Thing is, all of the pluses in C++ can also work as minuses. Big ones. The more C++ features you use, the more complexity you introduce and the more difficult it becomes to tame the results. Developers who confine themselves to a subset of C++ can avoid many of its worst pitfalls and excesses. But some shops want to guard against C++ complexity all together. Sticking with C forces developers to confine themselves to that subset. The Linux kernel development team, for instance, eschews C++.

Picking C over C++ is a way for you—and any developers who maintain the code after you—to avoid having to tangle with C++ excesses, by embracing an enforced minimalism. Of course, C++ has a rich set of high-level features for good reason. But if minimalism is a better fit for current and future projects—and project teams—then C makes more sense.

C vs. Java

After decades, Java remains a staple of enterprise software development—and a staple of development generally. Many of the most significant enterprise software projects were written in Java—including the vast majority of Apache Software Foundation projects—and Java remains a viable language for developing new projects with enterprise-grade requirements.

Java syntax borrows a great deal from C and C++. Unlike C, though, Java doesn’t by default compile to native code. Instead, the Java runtime environment, the JVM, JIT (just-in-time) compiles Java code to run in the target environment. Under the right circumstances, JITted Java code can approach or even exceed the performance of C.

The “write once, run anywhere” philosophy behind Java also allows Java programs to run with relatively little tweaking for a target architecture. By contrast, although C has been ported to a great many architectures, any given C program may still need customization to run properly on, say, Windows versus Linux.

This combination of portability and strong performance, along with a massive ecosystem of software libraries and frameworks, make Java a go-to language and runtime for building enterprise applications.

Where Java falls short of C is an area where Java was never meant to compete: running close to the metal, or working directly with hardware. C code is compiled into machine code, which is executed by the process directly. Java is compiled into bytecode, which is intermediate code that the JVM interpreter then converts to machine code. Further, although Java’s automatic memory management is a blessing in most circumstances, C is better suited for programs that must make optimal use of limited memory resources.

That said, there are some areas where Java can come close to C in terms of speed. The JVM’s JIT engine optimizes routines at runtime based on program behavior, allowing for many classes of optimization that aren’t possible with ahead-of-time compiled C. And while the Java runtime automates memory management, some newer applications work around that. For example, Apache Spark optimizes in-memory processing in part by using custom memory management code that circumvents the JVM.

C vs. C# and .Net

Nearly two decades after their introduction, C# and the .Net Framework remain major parts of the enterprise software world. It has been said that C# and .Net were Microsoft’s response to Java—a managed code compiler system and universal runtime—and so many comparisons between C and Java also hold up for C and C#/.Net.

Like Java (and to some extent Python), .Net offers portability across a variety of platforms and a vast ecosystem of integrated software. These are no small advantages given how much enterprise-oriented development takes place in the .Net world. When you develop a program in C#, or any other .Net language, you are able to draw on a universe of tools and libraries written for the .Net runtime. 

Another Java-like .NET advantage is JIT optimization. C# and .Net programs can be compiled ahead of time as per C, but they’re mainly just-in-time compiled by the .Net runtime and optimized with runtime information. JIT compilation allows all sorts of in-place optimizations for a running .Net program that can’t be performed in C.

Like C, C# and .Net provide various mechanisms for accessing memory directly. Heap, stack, and unmanaged system memory are all accessible via .Net APIs and objects. And developers can use the unsafe mode in .Net to achieve even greater performance.

None of this comes for free, though. Managed objects and unsafe objects cannot be arbitrarily exchanged, and marshaling between them comes at a performance cost. Therefore, maximizing performance of .Net applications means keeping movement between managed and unmanaged objects to a minimum.

When you can’t afford to pay the penalty for managed vs. unmanaged memory, or when the .Net runtime is a poor choice for the target environment (e.g., kernel space) or may not be available at all, then C is what you need. And unlike C# and .Net, C unlocks direct memory access by default. 

C vs. Go

Go syntax owes much to C—curly braces as delimiters, statements terminated with semicolons, and so on. Developers proficient in C can typically leap right into Go without much difficulty, even taking into account new Go features like namespaces and package management.

Readable code was one of Go’s guiding design goals: Make it easy for developers to get up to speed with any Go project and become proficient with the codebase in short order. C codebases can be hard to grok, as they are prone to turning into a rat’s nest of macros and #ifdefs specific to both a project and a given team. Go’s syntax, and its built-in code formatting and project management tools, are meant to keep those kinds of institutional problems at bay.

Go also features extras like goroutines and channels, language-level tools for handling concurrency and message passing between components. C would require such things to be hand-rolled or supplied by an external library, but Go provides them right out of the box, making it far easier to construct software that needs them.

Where Go differs most from C under the hood is in memory management. Go objects are automatically managed and garbage-collected by default. For most programming jobs, this is tremendously convenient. But it also means that any program that requires deterministic handling of memory will be harder to write.

Go does include the unsafe package for circumventing some of Go’s type handling safeties, such as reading and writing arbitrary memory with a Pointer type. But unsafe comes with a warning that programs written with it “may be non-portable and are not protected by the Go 1 compatibility guidelines.”

Go is well-suited for building programs like command-line utilities and network services, because they rarely need such fine-grained manipulations. But low-level device drivers, kernel-space operating system components, and other tasks that demand exacting control over memory layout and management are best created in C.

C vs. Rust

In some ways, Rust is a response to the memory management conundrums created by C and C++, and to many other shortcomings of these languages as well. Rust compiles to native machine code, so it’s considered on a par with C as far as performance. Memory safety by default, though, is Rust’s main selling point.

Rust’s syntax and compilation rules help developers avoid common memory management blunders. If a program has a memory management issue that crosses Rust syntax, it simply won’t compile. Newcomers to the language, especially from a language like C that provides plenty of room for such bugs, spend the first phase of their Rust education learning how to appease the compiler. But Rust proponents argue that this near-term pain has a long-term payoff: safer code that doesn’t sacrifice speed.

Rust also improves on C with its tooling. Project and component management are part of the toolchain supplied with Rust by default, same as with Go. There is a default, recommended way to manage packages, organize project folders, and handle a great many other things that in C are ad-hoc at best, with each project and team handling them differently.

Still, what is touted as an advantage in Rust may not seem like one to a C developer. Rust’s compile-time safety features can’t be disabled, so even the most trivial Rust program must conform to Rust’s memory safety strictures. C may be less safe by default, but it is much more flexible and forgiving when necessary.

Another possible drawback is the size of the Rust language. C has relatively few features, even when taking into account the standard library. The Rust feature set is sprawling and continues to grow. As with C++, the larger Rust feature set means more power, but also more complexity. C is a smaller language, but that much easier to model mentally, so perhaps better suited to projects where Rust would be overkill.

C vs. Python

These days, whenever the talk is about software development, Python always seems to enter the conversation. After all, Python is “the second best language for everything,” and unquestionably one of the most versatile, with thousands of third-party libraries available.

What Python emphasizes, and where it differs most from C, is favoring speed of development over speed of execution. A program that might take an hour to put together in another language—like C—might be assembled in Python in minutes. On the flip side, that program might take seconds to execute in C, but a minute to run in Python. (A good rule of thumb: Python programs generally run an order of magnitude slower than their C counterparts.) But for many jobs on modern hardware, Python is fast enough, and that has been key to its uptake.

Another major difference is memory management. Python programs are fully memory-managed by the Python runtime, so developers don’t have to worry about the nitty-gritty of allocating and freeing memory. But here again, developer ease comes at the cost of runtime performance. Writing C programs requires scrupulous attention to memory management, but the resulting programs are often the gold standard for pure machine speed.

Under the skin, though, Python and C share a deep connection: the reference Python runtime is written in C. This allows Python programs to wrap libraries written in C and C++. Significant chunks of the Python ecosystem of third-party libraries, such as for machine learning, have C code at their core.

If speed of development matters more than speed of execution, and if most of the performant parts of the program can be isolated into standalone components (as opposed to being spread throughout the code), either pure Python or a mix of Python and C libraries make a better choice than C alone. Otherwise, C still rules. 

Copyright © 2019 IDG Communications, Inc.