0. IntroductionProgramming is hard. Programming in C++ is even harder. Unfortunately, it is often made unnecessarily hard by programmers' resistance to adopt modern, safer methods and idioms. Bring up the topic of C++ at the lunch table and -- especially if there are Java programmers present -- you will be greeted with the customary horror stories of buffer overruns, memory leaks, and wild pointer errors that led to caffeine binges and marathon programming sessions. Sadly, these kinds of errors occur far too often in C++ programs. Not because the language is inherently unsafe, but because many C++ programmers don't know how to use it safely. If you are tired of these kind of errors in your C++ programs, you've come to the right place. Relax, put down that Java compiler before it goes off, and follow the simple rules outlined in this article. 1. Use std::string instead of char * or char []Character arrays are the only way to encapsulate string data in C. They're quick and easy to use, but unfortunately their use can be fraught with peril. Let's look at some of the more common errors that occur with character pointers and arrays. Keep in mind that most if not all of these problems will go undetected by the compiler. Ex. 1 - Forgetting to allocate enough space for string terminator char myName[4] = "Dave"; // Oops! No room for the '\0' terminator! strcpy(anotherName, myName); // Might copy four characters or 4000 Ex. 2 - Forgetting to allocate memory for a char * char * errorString; ... strcpy(errorString, "SomeValueDeterminedAtRuntime"); Usually this error is caught rather quickly with a segmentation violation. Ex. 3 - Returning a pointer to space allocated on the stack char * getName() { char name[256]; strcpy(name, "SomeStaticValue"); ... strcat(name, "SomeValueDeterminedAtRuntime"); return name; } char * myName = getName(); Once the function returns, the space allocated to name is returned to the program. This means myName might point to something unexpected later. Ex. 4 - The dread function sprintf() char buf[128]; sprintf(buf, "%s%d", someValueGottenAtRuntime, someInteger); Unless you are absolutely sure of how much space you need, it's all too easy to overrun a buffer with sprintf(). Now, let's revisit each example and show how a std::string eliminates the aforementioned problems: Ex. 1a std::string myName = "Dave"; std::string anotherName = myName; Ex. 2 std::string errorString; ... errorString = "SomeValueDeterminedAtRuntime"; Ex. 3 std::string getName() { std::string name; name = "SomeStaticValue"; ... name += "SomeValueDeterminedAtRuntime"; return name; } std::string myName = getName(); Ex. 4 std::string buf; std::ostringstream ss; ss << someValueGottenAtRuntime << someInteger; buf = ss.str(); This one's a no-brainer, folks. Avoid the headaches associated with character arrays and pointers and use std::string. For legacy functions that expect a character pointer, you can use std::string's c_str() member function. 2. Use standard containers instead of homegrown containersBesides std::string, the standard library provides the following container classes that you should prefer over your homegrown alternatives: vector, deque, list, set, multiset, map, multimap, stack, queue, and priority_queue. It is beyond the scope of this article to describe these in detail, however you can probably ascertain what most of them are by their names. For a proper treatment of the subject, I highly recommend the book by Josuttis listed in my references. An important feature of the standard containers is that they are all template classes. This is a powerful concept. Templates let you define lists (or stacks or vectors) of *any* data type. The compiler generates type-safe code for each type of list you create. With C, you either needed a list for each type of data it would hold (e.g. IntList, MonsterList, StringList) or the list would hold a void * that pointed to data in each node; somewhat the antithesis of type-safety. Let's look at a simple example with the commonly used std::vector. You'll want to use std::vector (or std::deque) instead of variable length arrays. #include <vector> #include <iostream> using namespace std; int main() { vector<int> v; // Add elements to the end of the vector. // The vector class handles resizing v.push_back(1); v.push_back(2); v.push_back(3); v.push_back(4); // Careful - bounds-checking not performed cout << v[2] << endl; // iterate like you would with arrays for (int i = 0; i < v.size(); i++) { cout << v[i] << endl; } // iterate with an iterator vector<int>::iterator p; for (p = v.begin(); p != v.end(); p++) { cout << *p << endl; } } In addition to providing generic, type-safe containers for any data type, these classes also provide multiple ways to search and iterate, and like std::string, they manage their own memory - a huge win over rolling these things yourself. I can't stress enough how important it is to familiarize yourself with the standard containers. Josuttis' book is an invaluable reference that is always within arm's reach of my keyboard.
|
|