Thursday, August 11, 2016

Transition from C to C++: Dynamic Memory Allocation

I have to admit that I am most comfortable with C among programming languages, but writing code in C sometimes gives me a headache. At the cost of its simple and elegant structure, C suffers from narrow features with no support for object-oriented programming paradigm. One of the most difficulties in programming with C is perhaps manual allocation of memory. For any dynamically varying-size object or data, one must manually allocate and free memory. If the programmer knows what he is doing, C allows him to create the most efficient program to perform the desired task. On the other hand, if the programmer does not know what he is doing, the resultant program will simply be very unstable, buggy, and inefficient.

When programming in C, there are simply too many thing that one needs to track down in terms of allocating and freeing resources in appropriate timing and space. Consider the very simple example below, which takes in user's input words and saves them.


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

struct Word {
  char *word;
};

void initWord(struct Word *ptr) {
  ptr->word = NULL;
}

void setWord(struct Word *ptr, char* word) {
  if( ptr->word != NULL) {
    ptr->word = (char*)realloc(ptr->word, strlen(word)*sizeof(*ptr->word));
  }
  else {
    ptr->word = (char*)malloc( strlen(word)*sizeof(*ptr->word));
  }
  strcpy( ptr->word, word);
}

void freeWord(struct Word *ptr) {
  if (ptr->word != NULL)
    free(ptr->word);
}

int main( int argc, char **argv ) {
  struct Word *wordArray = (struct Word*)malloc((argc-1)* sizeof(*wordArray));
  for (int i=1; i<argc; i++) {
    initWord(&wordArray[i-1]);
    setWord(&wordArray[i-1], argv[i]);
    printf("word %d: %s\n", i, wordArray[i-1].word);
  }

  for (int i=1; i<argc-1; i++) {
    freeWord(&wordArray[i-1]);
  }

  free(wordArray);

  return 0;
}
Because one does not know how many words will be there to save, one has to create an array of struct Word dynamically by calling malloc(), assign appropriate memory space for each word pointer in the struct, and finally call free() on all dynamically allocated resources when exiting. It is the programmer's role to take care of resources, and when the program grows big and complicated, one becomes more and more prone to mistakes and bugs.

Fortunately enough, there is C++ to help. Although many people, including Linus Torvalds, complain about C++, I happen to believe that C++ is a good choice for some applications, not all. There is a catch though. Somebody must do the hard work of taking care of all the memory allocation of classes.

Consider the C++ version of the same code.



#include <cstdio>
#include <cstring>
#include <vector>
#include <cstdlib>

class Word {
  char* word;
public:
  Word() {
    word = NULL;
  }
  ~Word() {
    if (word != NULL)
      free(word);
  }
  void setWord(char *word) {
    if (this->word != NULL) {
      this->word = (char*)realloc(this->word, strlen(word));
    }
    else {
      this->word = (char*)malloc(strlen(word));
    }
    strcpy(this->word, word);
  }
  char* getWord() {
    return word;
  }
};

int main (int argc, char **argv) {
  std::vector<Word> words(argc-1);
  for (int i=1; i<argc; i++) {
    words[i-1].setWord(argv[i]);
    printf("word %d: %s\n", i, words[i-1].getWord());
  }

  return 0;
}
Here, I am intentionally using char* and not string object to demonstrate how C++ class constructors and destructors can be designed to relieve the burden of memory allocation from the user. Note that in main() function, the programmer never has to call malloc() or free() to allocate resources manually. All these are performed by the constructors and destructors, so the programmer can simply create and make use of class Word as if it is a primitive data. To focus on resource management, I an using as much as C features as possible, except for object-oriented structure and C++'s vector class. The vector class allows one to assign memory dynamically without calling malloc() or free(), as vector's constructor, destructor, and methods will take care of all those.

It is now clear that the burden of memory management now lies in the hand of the class and library writer. If the class or library is not well written, the entire program will become unstable. From the perspective of a programmer who simply makes use of the class and library, it is apparent that writing programs is much simpler, given that the library is well-designed.

In a follow up post, I will go through the class design rule in C++, namely the rule of three. In fact, the above C++ example is an excellent case that needs to be modified to comply the design rule.

No comments:

Post a Comment