DRAFT C++ for C-like Programmers 2/12/2016 5 Strings, References, Constants In this chapter, we revisit strings, and then do references (or “smart pointers”) and constants. String literals Recall that string literals is a character arrays: “Hi" {'H', 'I', '\0'} A length-n string literal is really a length-n+1 character array, because the null character '\0' is automatically appended. The following two declarations, therefore, have exactly the same effect: char am[] = "hi"; /* am is immutable */ char am[] = {'H', 'i', '\0'}; Here is another declaration, which, while close to the others, is subtly different: char *pm = "hi"; /* pm is mutable */ The biggest difference is that pm is a pointer, while am is an array. What this means is that pm is a variable that can take on any address; am, however, is permanently attached to the memory location where its contents begin. picture? Copying strings In the philosophy of language, there is the problem of sense and reference. Philosophers from Gottlob Frege to Saul Kripke noticed that it’s possible to talk using two names, such as Hesperus and Phosforus, or the Morning Star and the Evening Star, or London and Londre, without know that they refer to the same entity. As similar problem arises in C++. String literals 2 of 14 How do we copy the value of a character array from one to another? The following certainly won’t work: s = t; If s and t are arrays, then the line will not compile; the value of s cannot be changed. If s and t are pointers to character arrays, then the line will compile but it won’t do what we want. In this case, the value of t is simply a memory address. After this line is executed, the value of s is the same. The result is that s now refers to the same block of memory (containing characters) that t does. Modifying s’s content is the same as modifying t’s content, since they’re the same content. This is sometimes called a “shallow copy”, rather than the “deep copy” we want. Let’s write a function to do an actual, deep copy of a string. For now, we’ll assume that the destination has enough space for the data in the source. First, we’ll copy strings as arrays: void strcpy(char s[], char t[]) { int i = 0; while ((s[i] = t[i]) != '\0') i++; } The (counterintuitive) convention is that the second parameter, t, is the source and the first parameter, s, is the destination. Each time around the loop, we assign one char from t to the corresponding position in s. Because the assignment expression s[i] = t[i] evaluates to the new value of s[i], we can place it in the while test expression. Notice that the last character assigned to s will be the first null character encountered in t, which is the desired behavior. Here’s a second, pointer-style version of the function, which looks quite different but behaves the same way: void strcpy(char *s, char *t) { while ((*s = *t) != '\0') { s++; t++; } } In this version, both parameters are written as character pointers. The while test expression is a direct translation of the previous version: we assign the rvalue pointed to by t to the lvalue pointed to by s, and check to see whether that value is the '\0'. The larger difference is that we now advance forward not by incrementing an index (which was used for both arrays) but by incrementing the two pointers themselves. To demonstrate that the way the parameters are written is immaterial, we can mix and max the two previous versions of the function: void strcpy(char s[], char *t) { /* s arr, t ptr */ int i = 0; while ((*s = t[i]) != ‘\0’) { s++; /* s ptr */ i++; /* t arr */ } 2 Comparing strings 3 of 14 } This version isn’t very elegant, but once again, the behavior is completely unchanged. Returning to version two, though, we can compress the function further. We now turn back to the expressions of the form *p++, which we encountered earlier, when trying to increment a pointer’s referent. We found that incrementing the referent required parentheses: (*p)++, because *(p++) is also legal. (Both unary operators * and ++ have high precedence, but, unlike most operators, unaries associate right-to-left.) In this function, we finally have occasion to use the second expression: void strcpy(char *s, char *t) { while ((*s++ = *t++) != '\0') ; } Each subexpression in the assignment expression, of the form *p++, evaluates the ++ operator first. In the parsing of *t++, t++ evaluates to the current value of t and has the side effect of incrementing t (to point to the next member of the array). When * is applied to the resulting pointer (which points to the same place as t used to point to), we get the rvalue of the current position of t. Similarly, *s++ evaluates to the lvalue of the current position of s, which has the side effect of incrementing s. In summary, the current t member value is assigned to the current s member, both pointers are incremented in the process, and we check to see whether the value in question was '\0'. If so, we fall out of the loop. The other feature of C++ that we’re taking advantage of in this version, is the fact that the null statement ; is a legal statement, which allows us to create body-less loops. This is pretty good, the body’s only one line long, but we can make that line a little shorter, again taking advantage of the conversion between booleans and integers. What is the value of the null character '\0'? It’s just 0, and any other character is != 0. We can take advantage of this in an even more succinct strcpy function: void strcpy(char *s, char *t) { while ((*s++ = *t++)) ; } Each time the new value is *s, we continue. We fall out only when *s is 0, meaning we encountered t’s first null character. The real strcpy function lives in the <cstring> library. Comparing strings Perhaps the next most common operation for strings is comparing them. The same C++ library includes a function called strcmp, which takes two string parameters, s and t, and returns an integer indicating their relationship. 0 means they’re equal; a negative number means s is “smaller”; a positive means t is. (Notice that a function returning this information for every pair of strings is a well ordering. As will become important later, we can choose arbitrarily a well ordering for the types we create.) Here is one implementation of strcmp: int strcmp(char *s, char *t) { for (int i = 0; s[i] == t[i]; i++) 3 Pointers and I/O 4 of 14 if (s[i] == '\0') return 0; return s[i] – t[i]; } Notice that we can divide possible outcomes into two types. Either we traverse the entire string s without reaching a discrepancy. In this case, we fall out of the loop when we reach s’s null character, in which case we return 0. We’ve reached the end of s, but what if there are more characters of t remaining? In this case, wouldn’t we want to return a negative number in indicate that t comes after s? Actually, this case never obtains. If we reach the if statement, then the current s and t values are equal. If s[i] is NULL, therefore, so is t[i], and so the two strings really are equal. We get through all of s iff s equals t. The second possibility is that the strings are distinct. In this case, we must eventually reach the first character position at which s and t differ, and consequently fall out of the loop. We now immediately return s[i] – t[i], the amount by which the s character is larger than the t character. If (the ASCII value of) the s char is greater than (the ASCII value of) the t char, then this different is positive; if the s char is less than the t char, then this different is negative; if they were equal (which they can’t be), the result would still be correct: 0. Again, we can write the function with arrays: int strcmp(char *s, char *t) { for(; *s == *t; s++, t++) if (*s == '\0') return 0; return *s - *t; } This version omits the need for a variable i (thus the null statement in the for loop’s initialization slot), but it uses the , operator to combine two statements. In this case, , is used to combine to statements, s++ and t++, that don’t naturally combine with another operator. (Which is not to say they couldn’t. Putting s++ + t++ in the last slot would have the same effect, but the value of the addition expression would simply be thrown away.) Pointers and I/O Again, a pointer is just a variable that takes on memory addresses, or, more precisely, numbers that are interpreted as memory addresses. The implementation of pointer values as numbers is usually ignored, but it is possible to look at the numbers themselves. Here’s some code, from ptrio.cpp: int x = int *xp cout << cout << 10; = &x; "x == " << x << endl; "xp == " << xp << endl; When run, we get the following: $ g++ ptrio.cpp $ a.out 4 Multidimensional arrays 5 of 14 x == 10 xp == 0xffbffa54 s == hi s as int == 0xffbffa48 $ Notice that pointers are, by default, automatically printed in hex. Of course, we can print them in other bases if we like: inset other base example What about character pointers? By default, they’re printed as strings, which is what we usually want. To print the address of a character array, we must specifically request it. To do so, we cast the char pointer as a generic void pointer, using reinterpret_cast: (which program?) char s[] = "hi"; cout << "s == " << s << endl; cout << "s as int == " << reinterpret_cast<void*>(s) << endl; When we run this code, we get: (replace with real output) s == hi s as int == 0xffbffa90 Multidimensional arrays We saw that C++ arrays are a very simple technology. An array of ten ints is just a fixed pointer to a contiguous block of memory with room for ten ints, except that it dereferences to an int. An two-dimensional array is, very simply, an array of arrays. A 5-by-10 two-dimensional array is just a contiguous block with room for 5*10 == 50 ints Again, a two-dimensional array m of ints is an array of int[]s. When we dereference m, *m, we get an int[]. When we dereference m twice, **m, we get an int. If we remember what the [] notation reduces to, the use of the indirection operator here makes sense. Writing m[0][0] is equivalent to writing *(*(m+0)+0), which reduces to **m. The absence of non-zero subscripts obscures the fact that the two additions are different. Writing m[1][2] is equivalent to writing *(*(m+1)+2).The +1 advances from the beginning of m to member number 1 of m, which is row number 1. The +2 then advances from the beginning of m[1] to member number 2, which is the third character of the second row of m. Entry i of m[i][j] is conventionally interpreted as the row number, entry j as the column number. This is why we use syntax like m[1][2] rather than m[1,2]. There is no other structure than arrays of arrays. We would access a three-dimensional array in the analogous way: c[1][2][3]. There’s no fixed limit to the number of degrees, but higher-dimension arrays are rare. Here’s a two-dimensional array example, a Tic Tac Toe board: char board[3][3] = { {'X', 'X', 'O'}, {'X', 'O', 'X'}, {' ', 'X', 'X'} 5 Command-line arguments 6 of 14 }; We picture the memory as it’s written here: (fix all these, from slide 61 of lec 2) X X O X O X X X We picture the individual members as: b[0][0] b[0][1] b[0][2] b[1][0] b[1][1] b[1][2] b[2][0] b[2][1] b[2][2] The internal braces in the initialization of board are actually optional. This initialization: char board[3][3] = {'X', 'X', 'O', 'X', 'O', 'X', ' ', 'X', 'X'}; might be pictured as: 1000 1001 1002 1003 1004 1005 1006 1007 1008 b[0][0] b[0][1] b[0][2] b[1][0] b[1][1] b[1][2] b[2][0] b[2][1] b[2][2] but the result is exactly the same. Command-line arguments Perhaps the commonest two-dimensional arrays used are sets of command-line arguments. Technically, they’re just arrays of char pointers… The canonical main function begins like this: int main(int argc, char *argv[]) { argv ( “argument vector”) is an array of char* strings, each of which is an individual command-line argument. argc (“argument count”) is the number of arguments. Recall that without this, we have no idea how many elements are in argv. You can think of the contents of argv as the results of an automatic tokenization of what the user types at the command prompt. Each contiguous block of characters becomes an argv member, where blocks are separated by whitespace. (To include spaces in an argument, surround the entire argument by double quotes.) One way that C++’s command-line argument support differs from Java’s is that the executable name itself counts as one block. Here’s a little program, params.cpp, that we can experiment with: #include <iostream> using namespace std; 6 Command-line arguments 7 of 14 int main(int argc, char *argv[]) { for (int i = 0; i < argc; i++) cout << "argv[" << i << "] == " << argv[i] << endl; } If we run it with no parameters: $ g++ params.cpp $ a.out argv[0] == a.out $ With parameters, though, we get:: $ a.out argv[0] argv[1] argv[2] argv[3] argv[4] here are some params == a.out == here == are == some == params And in and out of quotes: $ a.out argv[0] argv[1] $ a.out argv[0] argv[1] argv[2] argv[3] argv[4] $ "inside quotes" == a.out == inside quotes "inside quotes" out of quotes == a.out == inside quotes == out == of == quotes Parsing command-line options One popular format for command-line options is to have each setting composed of a dash, a single character to indicate the setting name and a string for the setting value: -P<param> -Wall -Dmacro==def This is an elegant form convention for command-line options and quite easy to code. We can do it all with a single switch statement, inside a single loop; (where’s this from? Make into an exercise?) while (argc-- >= 0 && argv[argc-][0] == '-')) { char *val = argv[argc-]+2; switch (argv[argc-][1]) { case 'D': case 'd': cout << “macro def: “ << val << endl; break; case 'W': case 'w': cout << “warnings: “ << val << endl; break; 7 Types of constants 8 of 14 default: print_usage(); break; } } The switch statement used here is a complex control-flow statement that replaces runon if-else statements. A single integral value is examined (in this case, argv[argc1][1]) and compared to several constants. (The examined value must be some form of integer, and the compared values must be constant values, such as integer literals. Ranges cannot be compared.) In execution, the value is compared, linearly, to each of the case values until one matches. At this point, the body of case’s code is executed. What may be counter-intuitive, however, is that execution of code continues until we reach the end of the switch statement or we leave it. In general, each case ends with a break statement or, in some circumstances, a return. (Notice that the cases do not actually have bodies—no curly brackets are used.) There are times, though, when we omit a break or return because we want execution to continue past the end of the case that we entered. In this option-parsing code, we’re supporting two options: one indicated by 'D' or 'd' and one indicated by 'W' or 'w'. We can’t directly code cases for boolean expressions, but we can effectively do ors: the first cout will be executed if the value was either 'D' or 'd' and the second will be executed if it was either 'W' or 'w'. The reason is that, if the value is 'D', then we enter it’s case. It’s case is empty, without a break, so we continue to the 'd' case, where the code for both cases is. At the end of that case, we encounter the break. If the value is 'd', then the case we match is the 'd', but the result is the same. In addition to the limitations that the value be integral and the comparisons be constants, we’re not allowed to have a single case appear more than once. Finally, we can optionally implement a default case. If no regular case was matched, then the default case will be executed. (Of course, if the last case was matched but did end with a break, then the default will be executed here as well.) Types of constants Constants can be subtle when combined with pointers or arrays. Constant ptrs v. ptrs to constants We can make a variable constant (or immutable) with the const modifier: const double PI = 3.14; int ftn(const int param) { … } In the first case, any code later attempting the assign a value to PI will not compile (or, on some compilers, will at least generate a warning). In the second case, the local parameter param will be immutable for the duration of the function. (Of course, this has no effect, either way, on the argument passed to the function.) We can use const with a simple variable two ways: const int X = 10; 8 Types of constants 9 of 14 int const X = 10; Consequently, we also can create a pointer-to-const two ways: const int *cip = &x; int const *cip = &x; We can create a constant pointer: int *const IP = &x; Finally, we can create a constant pointer-to-const two ways: const int *const CIP; int const *const CIP; Here’s a large example (with parts of const.cpp) demonstrating these possibilities, but including many errors: 1 2 3 4 5 6 7 8 9 10 11 12 const int x = 10; int * ip = &x; const int * cip = &x; (*cip)++; cip++; int y = 11; int * const IP = &y; (*IP)++; IP++; const int * const CIP (*CIP)++; CIP++; //error/cancels const //val is const, ptr is not //not allowed //is allowed //ptr is const, val is not //is allowed //not allowed = &x; //not allowed //not allowed It doesn’t obviously contradict any rule stated so far, but line 2 is illegal. The reason is that x is a constant int, but ip has been defined as a pointer to ordinary, non-constant integers. So while we wouldn’t be allowed to modify x, *ip evaluates to a regular int lvalue, and so there’s nothing to remind the compiler not to let us modify it. Line 3 defines a pointer variable called cip. What is the referent type? The way to determine this is to imagine parentheses drawn around the pointer name and the indirection operator in the pointer declaration: const int (*cip) = &x; The referent of cip is *cip, which is of type const int. The total type of cip, therefore, is that it is a pointer to constant ints. We now know why line 4 is illegal. We’re attempting to modify the referent of cip, which must be a constant int. Modifying the pointer cip itself, though, in line 5, is perfectly legal. Line 7 goes the opposite direction, putting the const to the left of the *. How do we parse this? Again, imagine parentheses surrounding the * and the IP: int (* const IP) = &y; The expression from the * to the IP is the referent of IP. The const is, in effect, already gone by the time we look at the referent type, so what dos it mean? It means that IP itself is a constant. When we dereference that constant IP, what we get is an int. 9 References 10 of 14 Line 8, therefore, incrementing IP’s referent is allowed, but line 9, incrementing IP itself, is not. Finally, we combine the two cases. Line 10: const int (* const CIP( = &x; defines a constant pointer CIP that points to a constant integer. Neither line 11 nor line 12 is legal. Now look back at the names chosen for these variables. We follow the convention that constant variables (other name?) are named in all caps and variables are all lower-case. IP and CIP are constants, while cip is not. Constants and arrays The issues are considerably less complicated between constants and arrays. On the one hand, we can have arrays of constants: const int nums[10] = {1,2,3}; const int *xp = num[2]; // must be ptr to const *xp = 10; // not allowed We interpret array elements similarly, parenthesizing the array name and the brackets: const int (nums[10]) = {1,2,3}; When we choose a member of this array, what we get is a constant integer. (talk about constants and static initialization) What about a constant array variable? Every array variable is a constant. References C++ introduces the more modern concept of references or “smart pointers”. References variables are another language construct used for referring to other variables. They’re often more convenient than traditional pointers, though, in that they’re syntax is somewhat simplified and safer because the ways they can be used are more limited. Here is an example: int x = 10; int &y = x; y++; y in this example can be called an alias. y is a new identifier that refers to x. More precisely, when y named, it refers to the same lvalue as does x. It’s as though we had written the following: int x = 10; int *p = &x; (*p)++; After executing either block of code, we have x == 11. This behavior will be familiar from the use of objects in Java, where code like obj1 = obj2; 10 References 11 of 14 wouldn’t do any copying of the contents of the second object but would merely set the reference value of obj1 to that of obj2. It’s interesting to note that the reference-ness of a variable in C++ is something we can choose or not. Object variables and primitive variables can both be reference variables or non-reference (term?) variables. There’s no tight coupling between objects and reference variables like in Java. Reference parameters The more common use of reference variables is as parameters, which are chosen to yield the usual benefits of pointer variables but with added robustness. Here’s a simple example: void inc(int &num) { num++; } Because sum is a reference variable, the squaring of it is reflected in the original argument passed to the function. With a pointer variable, the function would be somewhat more messy. We would need to remember the parentheses to pick out the lvalue: void inc(int *num) { (*num)++; } What makes reference parameters convenient is that they can be used like a regular var. We don’t need to dereference, but the formal parameter nonetheless acts as an alias to the original argument. Now that we have reference parameters, we can write yet another version (solution 4: reference parameters) of our swap function: void swap(int &x, int &y) { int temp = x; x = y; y = temp; } … int a = 5, b = 10; swap(a,b); Following the execution of this code, we have a == 10 and b == 5. The only difference between this function, and the original, naive implementation—the one that had no effect on the arguments passed to it—is he insertion of the two &s. Both the body of the function, and the code for calling it, are completely unchanged. Benefits of references We’ve seen that references are simpler than pointers because we don’t need to dereference. In addition to being less error-prone, references are safer than pointers because there are more restrictions on their use. First, references must always be initialized as they’re defined. int &x; will not compile. Second, references can never be null. Third, references are immutable. To see why, consider the following code: 11 References 1 2 3 12 of 14 int x, y; int &z = x; z = y; We define z as an alias for x, and then we try to make z an alias for y. Of course, that’s not what happens. Because z is an alias for x, line 3 simply assigns a new value to z. (By the way, is the fact that x and y have not been defined a problem? Certainly we don’t want to base the output of anything on their current values, because they’re essentially random. And it would be even worse to interpret their values as memory addresses. But all we’re doing here is assigning their values—whatever they happen to be—to each other, which is perfectly harmless.) Well, what if we did the following? &z = &y; Would this work? No, it wouldn’t even compile. Although there is, in fact pointer arithmetic going on under the covers (confirm), the z in &z evaluates to the lvalue x. &z, therefore, evaluates to the address of x. Again, even if the x has not been initialized, it’s location is perfectly well defined. By the same token, though, x is where it is. It doesn’t make sense to try to modify the address itself, only the address stored in an address-storing variable. References & pointers Most compilers translate reference code int pointer code, just as array code is translated to pointer code. In both cases, we have two kinds of syntax for one underlying concept. Even if restrictive reference code is translated into happy-go-lucky pointer code, the safety-from-restrictiveness still applies. This is because the compiler checks for adherence to the restrictions before doing the translation. If the code doesn’t respect the rules, then the reference code will never graduate into the pointer code, by which point the rules no longer matter. (Wittgenstein: “He must, so to speak, throw away the ladder after he has climbed up it.”) References & constants The relationship references have with constants is similar to the one that arrays do. We can have references to constants: const int x = 10; const int &rx = x; // must be const rx = 10; // not allowed As to constant references—they all are. References, pointers, things Here’s a program to help keep the three straight: int x = 4; &x int *xp = &x; *xp = 10; // // // // thing address of thing ptr to thing thing 12 Exercises int &rx = x; rx = 10; x = 11; int *rxp = &rx; *rxp = 20; 13 of 14 // // // // /* reference to thing thing same thing: x == rx ptr to thing same thing: *rxp == rx == x */ Explain this line-by-line? Pointer and reference example: int x = 5; int y = 10; int& xr = x; int *yp = &y; rx *= 3; x == 15, xr == 15, y == 10, *yp == 10 *yp = rx; rx++; x == 16, xr == 16, y == 15, *yp == 15 yp = &rx; rx++; x == 17, xr == 17, y == 15, *yp == 17 explain the above more? One of the commonest uses of references in operator overloading. We’ll look at this soon… References & objects In Java, references and objects are tightly coupled: After obj1 = obj2; obj1.changeMe(); obj2 is changed as well, because obj1 is an alias for it. In C++, though, the question of reference-ness is orthogonal to the question of object-ness. We have both reference primitives: int x = 10; int &rx = x; rx++; // rx and x are both changed and non-reference objects: string s1 = "hi", s2 = "there"; s1 = s2; s1.append("abc"); // s1 is changed but se is not Q: How can an object be assigned (by value) to another, anyway? A: With a copy-constructor. We’ll see these in a chapter on classes. Exercises Exercise 5-1: … Exercise 5-2: … 13 Exercises 14 of 14 Exercise 5-3: … 14