Ch. 5

advertisement
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 = ℞
*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++;
 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
Download