C Language Limitations and How It Compares to Other Languages

In this course, we had to work around fundamental limitations in C on several occasions. Let’s take a look at some of C’s limitations and disadvantages and how does it compare to newer and more languages.

C Language Disadvantages and Limitations

The main disadvantages of C Language are:

  • C doesn’t have a garbage collector
  • C doesn’t support any kind of polymorphism
  • C doesn’t have exceptions
  • C doesn’t support object-oriented programming very well
  • C provides only limited support for avoiding namespace collisions

C doesn’t have a garbage collector

Many modern program languages will detect and free unreachable data for you automatically. C doesn’t, so the programmer has to spend a lot of time worrying about when and by whom data allocated with malloc will be passed to free. Not only does this create many possibilities for error, but it also means that certain kinds of data structures in which a single component of the data structure is pointed to by an unpredictable number of other components are difficult to write in C, since it’s hard to tell when it is safe to free a component. Garbage-collected languages avoid all of these problems at a slight cost in performance. Though there exists a garbage collector for C/C++ http://www.hboehm.info/gc/, it isn’t 100% portable and may not work as well as a built-in collector.

C doesn’t support any kind of polymorphism

Polymorphism is when a function can work on more than one data type. The closest C can do is either parameterized macros (see Macros), heavy use of void * and function pointers as in qsort, or various nasty hacks where code is automatically generated with type names filled in from a base template. Most modern programming languages have some sort of support for polymorphism, allowing you to write, for example, a generic sorting routine without resorting to void *-like departures from the type system.

C doesn’t have exceptions

Exceptions are a mechanism for doing non-standard returns from a function when something blows up, which get caught using an “exception handler” that is often separate from the code handling the normal return values and which can often be used to catch exceptions from a variety of sources. Instead, C requires function writers to invent and document an ad-hoc protocol for indicating bad outcomes for every function they write, and requires function users to remember to test for bad return values. Most programmers are too lazy to do this all the time, leading to undetected run-time errors. Most modern programming languages fix this.

C doesn’t support object-oriented programming very well

“Object-oriented” is a buzzword with many possible meanings (but see http://c2.com/cgi/wiki?HeInventedTheTerm). However, at minimum it means that in addition to supporting polymorphism (described above), your language should support strong encapsulation (controlling who can get at the internal data of an object) and inheritance (allowing one abstract data type to be defined by extending another). You can fake most of these things in C if you try hard enough (for example, using function pointers), but it is always possible to muck around with internal bits of things just because of the unlimited control C gives you over the environment. This can quickly become dangerous in large software projects.

C provides only limited support for avoiding namespace collisions

In a large C program, it’s impossible to guarantee that my eat_leftovers function exported from leftovers.c doesn’t conflict with your eat_leftovers function in cannibalism.c. A mediocre solution is to use longer names: leftovers_eat_leftovers vs cannibalism_eat_leftovers, and one can also play games with function pointers and global struct variables to allow something like leftovers.eat_leftovers vs cannibalism.eat_leftovers. Most modern programming languages provide an explicit package or namespace mechanism to allow the programmer to control who sees what names where.

What does C++ fix?

On the above list, C++ fixes everything except the missing garbage collector. If you want to learn C++, you should get a copy of The C++ Programming Language, by Bjarne Stroustrup, which is the definitive reference manual. But you can get a taste of it from several on-line tutorials:

Other C-like languages

C syntax has become the default for new programming languages targeted at a general audience. Some noteworthy examples of C-like languages are Java (used in Android), Objective-C (used in OSX and iOS), and C# (used in Windows).

Each of these fix some of the misfeatures of C (including the lack of a garbage collector and bounds checks on arrays) while retaining much of the flavor of C. Which to choose probably depends on what platform you are interested in developing for.

Scripting languages

Much current programming is done in so-called scripting languages like Python, Perl, PHP, JavaScript, Visual Basic, Tcl, etc. These are generally interpreted languages similar to Lisp or Scheme under the hood, with dynamic typing (type information is carried along with values, so type errors are detected only at runtime but polymorphism is provided automatically), garbage collectors, and support for many advanced programming features like objects and anonymous functions. What distinguishes scripting languages from the Lisp-like languages is that the syntax is generally more accessible to newcomers and the language runtime usually comes with very large libraries providing built-in tools for doing practical programming tasks like parsing odd input formats and interfacing to databases and network services. The result is that common programming tasks can be implemented using very few lines of code, at a cost in performance that ranges from slight to horrendous depending on what you are doing.

Let’s look at an example in two common scripting languages, Perl and Python.

Here are some solutions to an old assignment, which find all the palindromes on stdin and report the first non-matching character for any non-palindrome.

The original C version looks like this:

#include <stdio.h>
#include <stdlib.h>
#include <string.h>

/* Palindrome detector.
 *
 * For each line of the input, prints PALINDROME if it is a palindrome
 * or the index of the first non-matching character otherwise.
 *
 * Note: does not handle lines containing nulls.
 */

/* read a line of text from stdin
 * and return it (without terminating newline) as a freshly-malloc'd block.
 * Caller is responsible for freeing this block.
 * Returns 0 on error or EOF.
 */
char *
getLine(void)
{
    char *line;		/* line buffer */
    int n;		/* characters read */
    int size; 		/* size of line buffer */
    int c;

    size = 1;
    line = malloc(size);
    if(line == 0) return 0;
    
    n = 0;

    while((c = getchar()) != '\n' && c != EOF) {
	while(n >= size - 1) {
	    size *= 2;
	    line = realloc(line, size);
	    if(line == 0) return 0;
	}
	line[n++] = c;
    }

    if(c == EOF && n == 0) {
	/* got nothing */
	free(line);
	return 0;
    } else {
	line[n++] = '\0';
	return line;
    }
}

#define IS_PALINDROME (-1)

/* returns IS_PALINDROME if s is a palindrome,
 * or index of first unmatched character otherwise. */
int 
testPalindrome(const char *s)
{
    int n;	/* length of s */
    int i;

    n = strlen(s);

    /* we only have to check up to floor(n/2) */
    for(i = 0; i < n/2; i++) {
	if(s[i] != s[n-1-i]) {
	    return i;
	}
    }
    /* else */
    return IS_PALINDROME;
}

int
main(int argc, char **argv)
{
    char *line;
    int mismatch;

    while((line = getLine()) != 0) {
	mismatch = testPalindrome(line);
	if(mismatch == IS_PALINDROME) {
	    puts("PALINDROME");
	} else {
	    printf("%d\n", mismatch);
	}

	free(line);
    }

    return 0;
}

examples/scripting/palindrome.c

This version is written in Perl (http://www.perl.org):

#!/usr/bin/perl

# For each line in stdin, print PALINDROME if it is a palindrome, or index of
# the first non-matching character otherwise.

while(<>) {
    chomp;              # remove trailing newline
    if($_ eq reverse $_) {
        print "PALINDROME\n";
    } else {
        for $i (0..length($_) - 1) {
            if(substr($_, $i, 1) ne substr($_, length($_) - $i - 1, 1)) {
                print $i, "\n";
                last;
            }
        }
    }
}

examples/scripting/palindrome.pl

The things to notice about Perl is that the syntax is deliberately very close to C (with some idiosyncratic extensions like putting $ on the front of all variable names), and that common tasks like reading all input lines get hidden inside default constructions like while(<>) and the $_ variable that functions with no arguments like chomp operate on by default. This can allow for very compact but sometimes very incomprehensible code.

Here’s a version in Python (https://www.python.org/):

#!/usr/bin/python

"""For each line in stdin, print PALINDROME if it is a palindrome, or index of
the first non-matching character otherwise."""

import sys

for line in sys.stdin:
    line = line.rstrip('\n')         # remove trailing newline
    if line == line[::-1]:
        print("PALINDROME")
    else:
        mismatches = [ i for i in range(len(line)) if line[i] != line[-(i+1)] ]
        print(min(mismatches))

examples/scripting/palindrome.py

Here the syntax is a little more alien if you are used to C: Python doesn’t use curly braces for block structure, using indentation instead. The code above uses some other odd features of the language, such as the ability to take “slices” of sequence variables like strings (the expression line[::-1] means “take all elements of line starting from the obvious default starting point (the empty string before the first colon) to the obvious default ending point (the empty string before the second colon) stepping backwards one character at a time (the -1)), a feature the language adopted from array-processing languages like MatLab; and the ability to do list comprehensions (the large expression assigned to mismatches), a feature that Python adopted from Haskell and that Haskell adopted from set theory.

What these gain in short code length they lose in speed; run times on /usr/share/dict/words in the Zoo are:

C = 0.107s

Perl =0.580s

Python = 2.052s

Note that for Perl and Python some of the cost is the time to start the interpreter and parse the script, but factors of 10–100 are not unusual slowdowns when moving from C to a scripting language. The selling point of these languages is that in many applications run time is not as critical as ease and speed of implementation.

As an even shorter example, if you just want to print all the palindromes in a file, you can do that from the command line in one line of Perl, e.g:

$ perl -ne 'chomp; print $_, "\n" if($_ eq reverse $_)' < /usr/share/dict/words

Licenses and Attributions


Speak Your Mind

-->