Constructing a string from a C String

Now that we've disposed of the default constructor, let's take a look at the line in our string interface definition (Figure 7.1 on page 405): string(char* p);.[7] This is the declaration for another constructor; unlike the default constructor we've already examined, this one has an argument, namely, char* p.[8]

[7] I know we've skipped the copy constructor, the assignment operator, and the destructor. Don't worry, we'll get to them later.

[8] There's nothing magical about the name p for a pointer. You could call it George if you wanted to, but it would just confuse people. The letter p is often used for pointers, especially by programmers who can't type, which unfortunately is fairly common.

As we saw in Chapter 6, the combination of the function name and argument types is called the signature of a function. Two functions that have the same name but differ in the type of at least one argument are distinct functions, and the compiler will use the difference(s) in the type(s) of the argument(s) to figure out which function with a given name should be called in any particular case. Of course, this leads to the question of why we would need more than one string constructor; they all make strings, don't they?

Yes, they do, but not from the same "raw material". It's true that every constructor in our string class makes a string, but each constructor has a unique argument list that determines exactly how the new string will be constructed. The default constructor always makes an empty string (like the C string literal ""), whereas the constructor string(char* p) takes a C string as an argument and makes a string that has the same value as that argument.

Susan wasn't going to accept this without a struggle.

Susan: I don't get "whereas the string(char* p) constructor takes a C string and makes a string that has the same value as the C string does."

Steve: Well, when the compiler looks at the statement string n("Test"); it has to follow some steps to figure it out.

  1. The compiler knows that you want to create a string because you've defined a variable called n with the type string; that's what string n means.

  2. Therefore, since string is not a native data type, the compiler looks for a function called string::string, which would create a string.

  3. However, there can be several functions named string::string, with different argument lists, because there are several possible ways to get the initial data for the string you're creating. In this case, you are supplying data in the form of a C string literal, whose type is char*; therefore, a constructor with the signature string::string(char*) will match.

  4. Since a function with the signature string::string(char*) has been declared in the header file, the line string n("Test"); is translated to a call to that function.

Susan: So string(char* p) is just there in case you need it for "any given situation"; what situation is this?

Steve: It depends on what kind of data (if any) we're supplying to the constructor. If we don't supply any data, then the default constructor is used. If we supply a C string (such as a C string literal), then the constructor that takes a char* is used, because the type of a C string is char*.

Susan: So string s; is the default constructor in case you need something that uses uninitialized objects?

Steve: Not quite; that line calls the default constructor for the string class, string::string(), which doesn't need any arguments, because it constructs an empty string.

Susan: And the string n ("Test"); is a constructor that finally gets around to telling us what we are trying to accomplish here?

Steve: Again, not quite. That line calls the constructor string::string(char* p); to create a string with the value "Test".

Susan: See, you are talking first about string n("Test"); in Figure 7.5 on page 419 and then you get all excited that you just happen to have string::string(char* p) hanging around which is way over in Figure 7.1 on page 405.

Steve: Now that you know that a C string literal such as "Test" has the data type char*, does this make sense?

Susan: OK, I think this helped. I understand it better. Only now that I do, it raises other questions that I accepted before but now don't make sense due to what I do understand. Does that make sense to you? I didn't think so.

Steve: Sure, why not? You've reached a higher level of understanding, so you can now see confusions that were obscured before.

Susan: So this is just the constructor part? What about the default constructor, what happened to it?

Steve: We can't use it with the statement string n("Test"), because we have some data to assign to the string when the string is created. A default constructor is used only when there is no initial value for a variable.

Susan: So was the whole point of discussion about default constructors just to let us know that they exist even though you aren't really using them here?

Steve: We are using it in the statement string s; to create a string with no initial value, as discussed before.

Susan wasn't clear on why the C string "Test" would be of type char*, which is understandable because that's anything but obvious. Here's the discussion we entered into on this point.

Susan: When you say "Test" is a C string literal of type char* and that the compiler happily finds that declaration, that is fine. But see, it is not obvious to me that it is type char*; I can see char but not char*. Something is missing here so that I would be able to follow the jump from char to char*.

Steve: A C string literal isn't a single char, but a bunch of chars. Therefore, we need to get the address of the first one; that gives us the addresses of the ones after it.

Now that the reason why a C string literal is of type char* is a bit clearer, Figure 7.6 shows the implementation for the constructor that takes a char* argument.

Figure 7.6. The char* constructor for our string class (from codestring1.cpp)
string::string(char* p)
: m_Length(strlen(p) + 1),
  m_Data(new char [m_Length])
{
   memcpy(m_Data,p,m_Length);
}

You should be able to decode the header string::string(char* p). This function is a constructor for class string (because its class is string and its name is also string) and its argument, named p, is of type char*. The first member initialization expression is m_Length(strlen(p) + 1). This is obviously initializing the string's length (m_Length) to something, but what?

As you may recall, C strings are stored as a series of characters terminated by a null byte (i.e., one with a 0 value). Therefore, unlike the case with our strings, where the length is available by looking at a member variable (m_Length), the only way to find the length of a C string is to search from the beginning of the C string until you get to a null byte. Since this is such a common operation in C, the C standard library (which is a subset of the C++ standard library) provides the function strlen (short for "string length") for this purpose; it returns a result indicating the number of characters in the C string, not including the null byte. So the member initialization expression m_Length(strlen(p) + 1) initializes our member variable m_Length to the length of the C string p, which we compute as the length reported by strlen (which doesn't include the terminating null byte) + 1 for the terminating null byte. We need this information because we've decided to store the length explicitly in our string class rather than relying solely on a null byte to mark the end of the string, as is done in C.[9]

[9] This is probably a good place to clear up any confusion you might have about whether there are native and user defined functions; there is no such distinction. Functions are never native in the way that variables are: built into the language. Quite a few functions such as strlen and memcpy come with the language; that is, they are supplied in the standard libraries that you get when you buy the compiler. However, these functions are not privileged relative to the functions you can write yourself, unlike the case with native variables in C. In other words, you can write a function in C or C++ that looks and behaves exactly like one in the library, whereas it's impossible in C to add a type of variable that has the same appearance and behavior as the native types; the knowledge of the native variable types is built into the C compiler and cannot be changed or added to by the programmer.

But why aren't there any native functions? Because the language was designed to be easy to move (or port) from one machine to another. This is easier if the compiler is simpler; hence, most of the functionality of the language is provided by functions that can be written in the "base language" the compiler knows about. This includes basic functions such as strlen and memcpy, which can be written in C. For purposes of performance, they are often written in assembly language instead, but that's not necessary to get the language running on a new machine.

Susan had some questions about the implementation of this function, and I supplied some answers.

Susan: What is strlen?

Steve: A function left over from C; it tells us how long a C string is.

Susan: Where did it come from?

Steve: It's from the C standard library, which is part of the C++ standard library.

Susan: What are you using it for here?

Steve: Finding out how long the C string is that we're supposed to copy into our string.

Susan: Is this C or C++?

Steve: Both.

Susan: Why is char* so special that it deserves a pointer? What makes it different?

Steve: The * means "pointer". In C++, char* means "pointer to a char".

Susan: I just don't understand the need for the pointer in char. See when we were using it (char) before, it didn't have a pointer, so why now? Well, I guess it was because I thought it was native back then when I didn't know that there was any other way. So why don't you have a pointer to strings then? Are all variables in classes going to have to be pointed to? I guess that is what I am asking.

Steve: We need pointers whenever we want to allocate an amount of memory that isn't known until the program is executing. If we wanted to have a rule that all strings were 10 characters in length (for example), then we could allocate the space for those characters in the string. However, we want to be able to handle strings of any length, so we have to decide how much space to allocate for the data when the constructor string::string(char* p) is called; the only way to do that is to use a pointer to memory that is allocated at run time, namely m_Data. Then we can use that memory to hold a copy of the C string pointed to by the parameter p.

Susan: Oh, no! Here we go again. Is m_Data a pointer? I thought it was just a variable that held an address.

Steve: Those are equivalent statements.

Susan: Why does it point? (Do you know how much I am beginning to hate that word?) I think you are going to have to clarify this.

Steve: It "points" in a metaphorical sense, but one that is second nature to programmers in languages like C. In fact, it merely holds the address of some memory location. Is that clearer?

Susan: So the purpose of m_Data is just a starting off point in memory?

Steve: Right. It's the address of the first char used to store the value of the string.

Susan: So the purpose of m_Length is to allot the length of memory that starts at the location where m_Data is?

Steve: Close; actually, it's to keep track of the amount of memory that has been allocated for the data.

Susan: But I see here that you are setting m_Length to strlen, so that in effect makes m_Length do the same thing?

Steve: Right; m_Length is the length of the string because it is set to the result returned by strlen (after adding 1 for the null byte at the end of the C string).

Susan: Why would you want a string with no data, anyway? What purpose does that serve?

Steve: So you can define a string before knowing what value it will eventually have. For example, the statement string s; defines a string with no data; the value can be assigned to the string later.

Susan: Oh yeah, just as you would have short x;. I forgot.

Steve: Yep.

Susan: Anyway, the first thing that helped me understand the need for pointers is variable-length data. I am sure that you mentioned this somewhere, but I certainly missed it. So this is a highlight. Once the need for it is understood then the rest falls in place. Well, almost; it is still hard to visualize, but I can.

Steve: I'll make sure to stress that point until it screams for mercy.

Susan: I think you might be able to take this information and draw a schematic for it. That would help. And show the code next to each of the steps involved.

Steve: Don't worry, we'll see lots of diagrams later.

Susan: So strlen is a function like any member function?

Steve: Yes, except that it is a global function rather than a member function belonging to a particular class. That's because it's a leftover from C, which doesn't have classes.

Susan: So it is what I can consider as a native function? Now I am getting confused again. I thought that just variables can be either made up (classes) or native. Why are we talking about functions in the same way? But then I remember that, in a backward way, functions belong to variables in classes rather than the other way around. This is just so crazy.

Steve: Functions are never native in the way that variables are; that is, built into the language. A lot of functions come with the language, in the form of the libraries, but they have no special characteristics that make them "better" than ones you write yourself. However, this is not true of variables in C, because C doesn't provide the necessary facilities for the programmer to add variable types that have the appearance and behavior of the native types.

Susan: You see I think it is hard for me to imagine a function as one word, because I am so used to main() with a bunch of code following it and I think of that as the whole function; see where I am getting confused?

Steve: When we call a function like strlen, that's not the whole function, it's just the name of the function. This is exactly like the situation where we wrote Average and then called it later to average some numbers.

Susan: A function has to "do something", so you will have to define what the function does; then when we use the function, we just call the name and that sets the action in gear?

Steve: Exactly.

Susan: Now, about this char* thing. . . (don't go ballistic, please) . . . exactly what is it? I mean it is a pointer to char, so what is *? Is it like an assignment operator? How is it classified?

Steve: After a type name, * means "pointer to the type preceding". So char* means "pointer to char", short* means "pointer to short", and so on.

Susan: So that would be for a short with variable-length data? And that would be a different kind of short than a native short?

Steve: Almost but not quite correct. It would be for variable-length data consisting of one or more shorts, just as a C string literal is variable-length data consisting of one or more chars.

Susan: So it would be variable by virtue of the number of shorts?

Steve: Actually, by virtue of the possibility of having a number of shorts other than exactly one. If you might need two or three (or 100, for that matter) shorts (or any other type), and you don't know how many when the program is compiled, then you pretty much have to use a pointer.

Susan: OK, yes, you said that about * and what it means to use a char*, but I thought it would work only with char so I didn't know I would be seeing it again with other variable types. I can't wait.

Steve: When we use pointers to other types, they will be to user-defined types, and we'll be using them for a different purpose than we are using char*. That won't be necessary until Chapter 10, so you have a reprieve for the time being.

The next member initialization expression in the constructor, m_Data(new char [m_Length]), is the same as the corresponding expression in the default constructor. In this case, of course, the amount of memory being allocated is equal to the length of the input C string (including its terminating null byte), rather than the fixed value 1.

Now that we have the address of some memory that belongs to us, we can use it to hold the characters that make up the value of our string. The literal value that our test program uses to call this constructor is "Test", which is four characters long, not counting the null byte at the end. Since we have to make room for that null byte, the total is 5 bytes, so that's what we'd ask new to give us. Assuming that the return value from new was 1234febc, Figure 7.7 illustrates what our new string looks like at this point.

Figure 7.7. string n during construction


The reason for the ???? is that we haven't copied the character data for our string to that location yet, so we don't know what that location contains. Actually, this brings up a point we've skipped so far: where new gets the memory it allocates. The answer is that much, if not all, of the "free" memory in your machine (i.e., memory that isn't used to store the operating system, the code for your program, statically allocated variables, and the stack) is lumped into a large area called the free store, which is where dynamically allocated memory "lives".[10] When you call new, it cordons off part of the free store as being "in use" and returns a pointer to that portion.

[10] I'm assuming that you are using an operating system that can access all of the memory in your computer. If not, the free store may be much smaller than this suggests.

It's possible that the idea of a variable that holds a memory address but which is itself stored in memory isn't that obvious. It wasn't to Susan:

Susan: I don't get this stuff about a pointer being stored in a memory address and having a memory address in it. So what's the deal?

Steve: Here's an analogy that might help. What happens when there is something too large to fit into a post office box? One solution is to put the larger object into one of a few large mailboxes, and leave the key to the larger mailbox in your regular mailbox. In this analogy, the small mailbox is like a pointer variable and the key is like the contents of that pointer. The large mailbox corresponds to the memory dynamically allocated by new.

So at this point, we have allocated m_Length bytes of memory, which start at the address in the pointer variable m_Data. Now we need to copy the current value of the input C string (pointed to by p) into that newly allocated area of memory. This is the job of the sole statement inside the brackets of the constructor proper,

memcpy(m_Data, p, m_Length);

which copies the data from the C string pointed to by p to our newly allocated memory.The final result is that we have made (constructed) a string variable and set it to a value specified by a C string. Figure 7.8 shows what that string might look like in memory.

But how would this string::string(char* p) constructor operate in a program? To answer that question, Figure 7.9 gives us another look at our sample program.

Figure 7.9. A simple test program for the string class (codestrtst1.cpp)
#include "string1.h"

int main()
{
   string s;
   string n("Test");
   string x;

   s = n;
   n = "My name is Susan";

   x = n;
   return 0;
}

Calling the string(char*) Constructor

How does the compiler interpret the line string n("Test");? First, it determines that string is the name of a class. A function with the name of a class, as we have already seen, is always a constructor for that class. The question is which constructor to call; the answer is determined by the type(s) of the argument(s). In this case, the argument is a C string literal, which has the type char*; therefore, the compiler looks for a constructor for class string that has an argument of type char*. Since there is such a constructor, the one we have just examined, the compiler generates a call to it. Figure 7.10 shows this constructor again for reference, while we analyze it.

Figure 7.10. The char* constructor for the string class, again (from codestring1.cpp)
string::string(char* p)
: m_Length(strlen(p) + 1),
  m_Data(new char [m_Length])
{
   memcpy(m_Data,p,m_Length);
}

When the program executes, string::string(char* p) is called with the argument "Test". Let's trace the execution of the constructor, remembering that member initialization expressions are executed in the order in which the member variables being initialized are listed in the class interface, not necessarily in the order in which they are written in the member initialization list.

  1. The first member initialization expression to be executed is m_Length(strlen(p) + 1). This initializes the member variable m_Length to the length of the C string whose address is in p, with 1 added for the null byte that terminates the string. In this case, the C string is "Test", and its length, including the null byte, is 5.

  2. Next, the member initialization expression m_Data(new char [m_Length]) is executed. This allocates m_Length (5, in this case) bytes of memory from the free store and initializes the variable m_Data to the address of that memory.

  3. Finally, the statement memcpy(m_Data,p,m_Length); copies m_Length bytes (5, in this case) of data from the C string pointed to by p to the memory pointed to by m_Data.

When the constructor is finished, the string variable n has a length, 5, and contents, "Test", as shown in Figure 7.8. It's now ready for use in the rest of the program. After all the discussion, Susan provided this rendition of the char* constructor for the string class.

Susan: So first we define a class. This means that we will have to have one or more constructors, which are functions with the same name as the class, used to create objects of that class. The char* constructor we're dealing with here goes through three steps, as follows: Step 1 sets the length of the string; step 2 gets the memory to store the data, and provides the address of that memory; step 3 does the work; it copies what you want.

Steve: Right.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset