views:

537

answers:

4

Are there any known design principles, best-practices and design patterns that one can follow while designing a C project? Or useful design principles for procedural (imperative) programming in general?

(I'm child of the 'object-oriented generation' and have to design a large C project for the first time)

+5  A: 

Information hiding - as espoused by Parnas (Software Fundamentals).

Careful management of headers and visibility:

  • Everything in a source file that can be hidden from the outside world should be; only the documented external interface should be exposed.
  • Everything that is exposed is declared in a header.
  • That header is used where the functionality is needed (and where it is defined).
  • The header is self-contained - when you need it, you use it, and you don't have to fret about 'what other headers do I also have to include' because the header ensures it works by including anything it needs to make it work.
  • The header is self-protected - so it does not matter if it is included multiple times.

    #ifndef HEADER_H_INCLUDED
    #define HEADER_H_INCLUDED
    ...rest of header contents, including other #include lines if necessary
    #endif /* HEADER_H_INCLUDED */
    
  • Design sets of functions to work on 'objects' (usually structures) - and use those functions rather than poking around the innards of the structure in the code that is using it. Think of it as self-imposed encapsulation.

Jonathan Leffler
Good point, thank you, Jonathan. Abstract data types are another good example of information hiding with a clean separation of usage and implementation (known external interface and unknown internal implementation).
Dimi
+3  A: 

There is a good, free, online book, titled Object-Oriented Programming With ANSI-C, which covers the topic of writing object-oriented code in C. A google search for "object-oriented C" also yields a number of other good examples and resources.

If your project is safety-critical, MISRA-C is a good set of rules. It is intended mostly for embedded c, but it can be useful in other areas as well.

I consider myself an OO coder, and I do a lot of work with embedded-C. The best advice I can give, especially for large projects, is not to overdo it. Creating a complete OO framework on top of ANSI C can be very tempting, but it takes a great deal of time and effort to get it right. The fancier you get, the more time you will spend debugging your framework instead of working on the real project. Approach the task with a clear head, and a good, solid grasp of YAGNI. Best of luck!

e.James
Thank you, e.James. I do not want to create an object-oriented framework on top of ANSI C, but look for special and appropriate procedural programming design principles. The MISRA-C hint is very useful, especially because it actually is an embedded project. I am going to take a closer look at it.
Dimi
Ah, the joys of embedded C. Don't forget that you have to declare your variables at the top of your function (or at the top of any `{ }` block). That one always bites me once or twice `:)`
e.James
+1  A: 

OOP is a methodology not a technology. So my first bit of advice is stop thinking of it as procedural programming.

To e.James's point, you don't want to try and re-create an object-oriented language or pretend that you have the capabilities thereof. You can still do all the right things by clinging to a few simple principles:

  1. Test drive everything.
  2. Find what varies and encapsulate it.
  3. Design to interfaces.
+2  A: 

My three advices:

  • Write unit tests. They will help you zero in on a design that suites your problem as you go along. Much better than relying (solely) on pre-meditated thinking.
  • Have a memory leak detector (there are all sort of libraries out there) installed and running from day one. Have this library print out all leaks as soon as the program/tests exits. This will allow you to catch a leak as soon as you introduce it, thereby making its fixing much less painful.
  • Write OOP code in C. Not that difficult. While it is possible to emulate method overriding, I suggest that you start with emulation of simple objects. Even this simple mechanism can give you great mileage.

Here's an example:

struct Vector {
  int size;
  int limit;
  int* buf; 
}

Vector* Vector_new() {
  Vector* res = (Vector*) malloc(sizeof(Vector));
  res.limit = 10;
  res.size = 0;
  res.ints = (int*) malloc(sizeof(int) * res.limit);

  return res;
}


void Vector_destroy(Vector* v) {
  free(v->ints);
  free(v);
}

void Vector_add(Vector* v, int n) {
  if(v->size == v->limit) {
    v->limit = v->limit * 2 + 10;
    v->ints = realloc(v->ints, v->limit);     
  }

  v->ints[v->size] = n;
  ++v->size;
}

int Vector_get(Vector* v, int index) {
  if(index => 0 && index < v->size())
    return v->ints[index];

  assert false;
}
Itay
Thank you, Itay. I'll follow your advices.
Dimi