Prefer std::string to char*

This article is intended for programmers who are starting C++ programming and have a background of C knowledge. Yes, it is true that C++ inherits most of C, but there are many things that should be avoided, such as preferring new and delete to malloc and free, the C++ casts to the C ones or the use of STL containers to statically or dynamically allocated arrays. I will bring into the discussion a special case of the later: preferring std::string to char*. I will take you through a list of problems with the C-like character arrays and then show how much you benefit by avoiding it and using std::string.

Avoid Ill-Formed Declarations of char Arrays

When declaring a char array, many programmers do it like this:

char* name = “marius”;

That might look okay at first glance, but then you might decide you need to make the strings begin with a capital. The simplest way to implement it is:

name[0] = ‘M’;

And the code might build fine, but crash at run-time because that is undefined behaviour and depends on the compiler implementation (with VS2005 it compiles fine but crashes at run-time). The answer is obvious: “marius” is a string literal (stored in the data section of the program), and “name” is only a pointer to the array. Because that data area (of string literals) is read-only, you are not allowed to change it. The correct declaration should have been:

const char* name = “marius”;

In this case, an attempt to change a character is detected by the compiler that throws an error: cannot modify a constant variable.

Nasty C-Like Approach

What you can do is use a char[] to define a fixed-length array of chars:


char name[] = “marius”;
name[0] = ‘M’;

In this case, name will be an array of seven characters (including the null-terminator), initialized with the string literal “marius”. It is basically an im-memory copy of the literal, but one with read/write access.

Now, imagine that you want to append the first name to the last name. You could use strcat():


char name[] = “marius”;
strcat(name, ” bancila”);

But, as soon as you run the program you’ll crash because strcat does not verify whether the buffer (name in this case) is big enough to hold the appended string too. You write beyond the array bounds and corrupt the memory.

Of course, you can work around that by declaring a bigger array, just to make sure that it can hold all that you want. For instance, 50 chars should be enough to hold a name.


char name[50] = “marius”;
strcat(name, ” bancila”);

That works. But, you have to pray you’ll not have to deal with names like Carlos Marìa Eduardo García de la Cal Fernàndez Leal Luna Delgado Galván Sanz (yes, that is a single Spanish name). You also have to think that you might waste memory. If you declare the name as 100 chars, and use on average 20, with a list of 100,000 names, you waste 8,000,000 bytes in memory. This leads you to the next approach.

Allocating Memory Dynamically

The next step in finding the appropriate solution is to try allocating the memory dynamically.


char* name = new char[strlen(“marius”)+1];
strcpy(name, “marius”);

In this case, you can re-allocate as much memory as you need. This could be simply illustrated like below:


char* temp = new char[strlen(name) + strlen(” bancila”) + 1];
strcpy(temp, name);
strcat(temp, ” bancila”);

delete [] name;
name = temp;

A lot of code needs to be written and maintained. And, things become even worse when you deal with classes (after all, I am talking about C++) and need to make deep copies.

Ensure Correct Memory Handling in Classes

Consider the case of a Person class that needs to store the name of a person. Your first impulse is to write it like this:


class Person
{
char* name;
};

At first glance it could be okay, but this class should provide:

  • A constructor that takes a string to initialize the name
  • A custom copy constructor to ensure a deep copy (default copy constructor provided by the compiler makes a shallow copy; in other words, copies the value of all attributes from one object to the other, which leads to a copy of pointers, not the objects they point to)
  • A custom operator=
  • A destructor to clean up the dynamically allocated memory

After putting all that together, the Person class would look like this:


class Person
{
char* name;
public:
Person(const char* str)
{
name = new char [strlen(str)+1];
strcpy(name, str);
}

Person(const Person& p)
{
name = new char [strlen(p.name)+1];
}

Person& operator=(const Person& p)
{
if(this != &p)
{
delete [] name;

name = new char [strlen(p.name)+1];
strcpy(name, p.name);
}

return *this;
}

~Person()
{
delete [] name;
}
};

More by Author

Must Read