Review of IO and strings
Strings: Work through the middle/first/last name example.
String literals and escapes:
"\n"
– The equivalent toendl
, but can be placed anywhere in the string."\\"
– An actual backslash"\t"
– A tab"\a"
– A “bell” (if printed, either a tone will sound or something else will be done to get your attention)."\b"
– Backspace, deletes the character before it."\""
– An actual double quote"\'"
– A single-quote
Characters:
char x = 'A';
s.at(n)
,s.at(n) = 'c';
Characters as numbers:
'A' - 'a' // distance between upper and lower case d - '0' // Convert to numerical digit 'a' < 'A' 'a' < 'b' '0' != 0 'A' == 101
Converting numbers to characters:
int x = 101; char a = x; // Works cout << a; cout << (char)(x); // Also works
Converting strings to ints:
int x = stoi("123"); // x == 123
The Conditional Operator
The conditional operator lets us choose between two expressions to evaluate, depending on a Boolean condition. For example, in our BMI program, we wanted to print a different message if the BMI was greater than 30 (indicating that the user is overweight) We can do this via
int bmi = ...
cout << bmi > 30 ?
"You are overweight!" :
"Doing good.";
cout << endl;
The conditional operator has the structure
boolean_condition ? true_expression : false_expression
First, boolean-condition is evaluated. If it evaluates to true, then
true-expression is evaluated and becomes the value of the whole structure.
If the condition evaluates to false, then false-expression is evaluated
and becomes the value. Note that one and only one of true/false-expression is
ever evaluated. (I.e., the conditional operator does short-circuiting, just
like &&
and ||
.) This means you can do things like this:
x != 0 ? 1 / x : 0
The true-side would be undefined if x was 0, but if x is 0 it will not be evaluated.
The ternary operator has lower precedence than all binary operators, except for the assignment operators.
It’s possible to “chain” ?:
together to express an if-else-if-else structure:
if_condition1 ? true-branch :
else_condition2 ? else_true_branch : else_branch
For example, we could check against too-high and too-low BMI with
cout << bmi < 18 ? "You are underweight" :
bmi > 30 ? "You are overweight" :
"You are fine";
It’s also possible to write this:
a ? b ? c : d : e
But that’s confusing. Better to write it as
a ? (b ? c : d) : e
or even better yet, think about when c, d, and e are going to be produced:
a && b ? c :
a && !b ? d :
e
One interesting trick to note is that in the case of something like
condition ? a : b
where a and b are variables, the conditional operator will evaluate to the box named by a or b, rather than the value in it. (In general, C++ saves pulling the value out of a box until the very last moment, to give you opportunities to use it as a box, rather than as a simple value.) This means you can do something like this:
(which == 1 ? a : b) = 12;
Which variable will assigned to depends on whether the condition works out to true or false (and, because of short-circuiting, the other won’t be touched).
Note, also, that negating the condition is exactly equivalent to swapping the true and false expressions:
a ? b : c // is equivalent to
!a ? c : b
If you find yourself writing a negated condition, you might ask whether it would be clearer if you removed the negation, and just swapped the expressions.
Some examples:
a < b ? a : b // gives the larger of a and b
width == height ? "square" : "rectangle"
It’s important to note the difference between the conditional operator and
the if-else
statement. You should use the conditional operator if all
you need to do is choose between two values, while if-else
allows you
to choose between two “courses of action”, two different sequences of statements.
Note that these two things are equivalent:
if(x < y)
y = 1;
else
y = 2;
and
y = x < y ? 1 : 2;
The second is more concise, but you might find the first easier to understand.
Conditional statements: branching
We continue our adventure into mixing up the flow of control (from the straight line top-to-bottom process it used before). We’re going to add branching, the ability for our code to take different paths, depending on the true/false value of some condition
The if-else
control structure
An if-else
structure allows our code to go one of two ways, depending on
whether a condition is true or false. While the conditional operator
allowed us to switch between values, the if-else
control structure allows
us to switch between statements (or blocks, of course). The most basic
if-else
looks like this:
if(condition)
body;
If the condition evaluates to true
, then the body (a single statement, or
block) is executed next. Otherwise, execution continues with the first statement
immediately after the body. For example:
int i;
cin >> i
if(i < 0)
cout << "(negative) ";
cout << "i = " << i << endl;
Depending on the sign of the user’s input, this will either print
(negative) i = ...
or
i = ...
This kind of if
structure is useful when you want something to happen
only if a condition is true, but you don’t want anything in particular to
happen when it is false. (Note how, above, the next line after the if
is
printed in both the true and false cases.)
If you want to do something specifically both when the condition is true,
and when it is false, you can add an else
clause:
if(condition)
true-body;
else
false-body;
If the condition evaluates to true, then the true-body will be executed, otherwise the false-body will be executed. After one of the two bodies has been executed, the flow of control continues with the next statement immediately after the false-body. For example:
int i;
cin >> i
if(i >= 0)
cout << "(positive) ";
else
cout << "(negative) ";
cout << "i = " << i << endl;
Depending on the sign of the user’s input, this will print either
(positive) i = ...
or
(negative) i = ...
Note that one, and only one, of true-body or false-body is always run.
Let’s look at some examples:
Testing whether a string (e.g. input from the user) is empty:
if(input.empty()) cout << "You didn't type anything!" << endl;
Testing whether a string is empty:
if(s.empty()) cout << "No text found!" << endl;
Testing whether a user entered a non-zero value:
if(score == 0) cout << "Score cannot be zero!" << endl;
Testing whether a value is positive or negative:
if(value < 0) cout << "Negative"; else cout << "Positive";
if-else
chains
Suppose we want to check whether the user’s input is 0, positive, or negative. We can start by checking for 0-or-not:
int i;
cin >> i;
if(i == 0)
cout << "Zero" << endl;
else
...
What do we put in the else
part? Well, we still need to check whether the
input is positive or negative, so why not another if
? Remember that although
control structures are not statements, they can be used anywhere where a
statement is required, so we can replace the false-body with another
if-else
:
int i;
cin >> i;
if(i == 0)
cout << "Zero" << endl;
else
if(i > 0)
cout << "Positive" << endl;
else
cout << "Negative" << endl;
Let’s analyze what we’re doing here:
If
i
is zero, then we print “Zero”. The false-body (including the otherif-else
) is skipped completely.If
i
is not zero, then we continue into the false-body, which means we now test the conditioni > 0
.If
i
is greater than 0, we print “Positive”.Else, we print “Negative”. This case can only happen if
i < 0
; can you see why?
When we write a “chain” of if-else if-else
like this, it’s traditional to
write it like this:
int i;
cin >> i;
if(i == 0)
cout << "Zero" << endl;
else if(i > 0)
cout << "Positive" << endl;
else
cout << "Negative" << endl;
This makes it clear that we are testing a sequence of conditions, looking for the first one that is true; that is the body that will be executed, and all the others will be skipped.
Remember that an else if
will only be tried if none of the conditions
before it succeeded. For example, consider this:
if(score < 60)
grade = 'f';
else if(score < 70)
grade = 'd';
else if(score < 80)
grade = 'c';
else if(score < 90)
grade = 'b';
else if(score <= 100)
grade = 'a';
What would happen if we reversed the order of the conditions and bodies:
if(score <= 100)
grade = 'a';
else if(score < 90)
grade = 'b';
else if(score < 80)
grade = 'c';
else if(score < 70)
grade = 'd';
else if(score < 60)
grade = 'f';
Now, everyone gets an A! The order in which the tests occur matters, if the conditions “overlap” at all. If we want to make it so the order doesn’t matter, we have to make it so the conditions don’t overlap:
if(score <= 100 && score >= 90)
grade = 'a';
else if(score < 90 && score >= 80)
grade = 'b';
else if(score < 80 && score >= 70)
grade = 'c';
else if(score < 70 && score >= 60)
grade = 'd';
else if(score < 60)
grade = 'f';
Some other examples:
Suppose we have two characters,
start
andend
, and we know thatstart
contains one of< ( { [
and end contains one of> ) } ]
and we want to know if they “match”. That is, ifstart == '<'
thenend
must be>
. This looks likebool matches = false; if(start == '<' && end == '>') matches = true; else if(start == '{' && end == '}') matches = true; else if(start == '(' && end == ')') matches = true; else if(start == '[' && end == ']') matches = true;
But note that we can simplify this if we look at the ASCII character table:
- If
start == '('
thenend == start + 1
- For all the other bracket types, matching means
end == start + 2
.
So we can write:
bool matches = false; if(start == '(') matches = (end == start + 1); else if(start == '[' || start == '{' || start == '<') matches = (end == start + 2);
- If
Nesting if-else
structures
As the above example shows, we can put one if-else
“inside” (i.e., in
place of true-body or false-body) another. This is called nesting, and
it’s the conditional analogue to nested loops. In the case of nested if
structures, it means something like the logical AND. For example:
if(y != 0) {
if(x > 0)
cout << "Positive";
else
cout << "Negative";
}
How can we “flatten” this into a single-level if
? By examining the situations
in which the different bodies are run:
The code will only print “positive” if
y != 0
ANDx > 0
.The code will only print “negative” if
y != 0
ANDx <= 0
.
So we can rewrite this as
if(y != 0 && x > 0)
cout << "Positive";
else if(y != 0 && x <= 0)
cout << "Negative";
We could simplify the second condition to just
if(y != 0 && x > 0)
cout << "Positive";
else if(y != 0)
cout << "Negative";
But can we simplify it even further, and use just an else
:
if(y != 0 && x > 0)
cout << "Positive";
else
cout << "Negative";
No! Now the second body will run if either x <= 0
OR x == 0
. It’s important,
when looking at a bare else
, to consider when exactly it will run. For
example:
if(a > 10 && a < 20)
...
else
...
When does the else
branch run? When the condition is not true, in other
words, when
!(a > 10 && a < 20)
!(a > 10) || !(a < 20)
a <= 10 || a >= 20
We can use DeMorgan’s laws to negate compound conditions, to find out when
the else
side will apply.
Suppose we have
if(a > 10)
if(b > 20)
cout << "Yes";
else
cout << "no";
To which if
is the else
attached? Is it
if(a > 10)
if(b > 20)
cout << "Yes";
else
cout << "no";
or is it
if(a > 10)
if(b > 20)
cout << "Yes";
else
cout << "no";
The first; the rule is that an else
is attached to the closest preceding
if
. But note that in general, it’s clearer to use a block to make it
explicit:
if(a > 10) {
if(b > 20)
cout << "Yes";
else
cout << "no";
}
Now there’s no possibility of confusion.
Transforming logical operators into nested if-else
(and vice versa)
If we have
if(A && B)
...
we can transform this into
if(A)
if(B)
...
(and vice versa)
If we have
if(A)
true-body
else
false-body
we can transform this into
if(!A)
false-body
else
true-body
These two can be applied to nested structures. E.g.,
if(a > 1)
if(b < 0)
if(name.empty())
if(c == 'y')
...
We can simplify this into
if(a > 1 && b < 0 && name.empty() && c == 'y')
...
Finally, we can always simplify:
if(true)
true-body
else
false-body
to just
true-body
and likewise,
if(false)
true_body
else
false_body
simplifies to just
false_body
Combine with DeMorgan’s laws, this gives us a lot of leeway for rewriting
if-else
structures to make things clear.
Examples:
if(!(x != y || y != z || x == z)
cout << x;
else
cout << "nope"
DeMorganifying the condition gives:
if(x == y && y == z && x != z)
cout << x;
else
cout << "nope";
But if we examine the condition, we see that it is always false
(because if
x == y
and y == z
it must also be the case that x == z
), so we can
simplify this to just
cout << "nope";
Suppose we have
if(a < b && b < c)
cout << "Hello";
else if(c > a)
cout << "Goodbye";
else
cout << "OK";
How can we simplify this? Note that c > a
is equivalent to a < c
. And if
a < b
and b < c
then it must be the case that a < c
. So the second
if
is redundant, it’s covered by the first, and is thus unreachable. We
can remove it.
The switch-case
control structure
Fairly often we find ourselves writing a long if-else-if-… chain that looks like this:
if(a == 1)
// a1 case
else if(a == 2)
// a2 case
else if(a == 3)
// a3 case
else
// otherwise case
where every condition tests the same variable for equality with something.
An if
chain like this is better written as a switch statement. A switch
statement tests a single variable for equality against a set of cases.
The structure of a switch
statement is quite flexible (and easy to get wrong)
so we’ll look at the most useful version, corresponding to the above version
switch(a) {
case 1:
// a1 case
break;
case 2:
// a2 case
break;
case 3:
// a3 case
break;
default:
// otherwise case
}
Note that the “body” of each case
is not a block but rather a sequence of
statements ending with a break;
. (The default
must come last, and does not
need a break
.)
You might wonder what the break
is for. The answer
is that the default behavior of switch
, without any break
is quite
unintuitive. Consider the following:
switch(a) {
case 1:
cout << "one";
case 2:
cout << "two";
case 3:
cout << "three";
default:
cout << "other";
}
cout << endl;
What will be printed if a == 1
? You might think just one
, but you’d be
surprised. In fact, what will be printed is “onetwothreeother”. Each case
falls through to the next, without checking it’s condition, unless terminated
by a break. Thus, you should usually end every case (except default
) with a
break
, to make sure this doesn’t happen unintentionally. The one situation
where this “fall through” is useful is when translating something like this:
if(a == 1 || a == 2 || a == 3)
cout << "small";
else if(a == 10)
cout << "medium";
else
cout << "large";
We can translate this into
switch(a) {
case 1:
case 2:
case 3:
cout << "small";
break;
case 10:
cout << "medium";
break;
default:
cout << "large";
}
But note that in this situation, the cases that fall through don’t do anything. In effect, several cases “share” a single body. This is the one really acceptable use for fall through.
There’s a big catch when it comes to using switch
: the switch variable must
be either int
, or something that can be converted to an int
. This means
that you cannot do
float f;
switch(f) { ... }
or
string s;
switch(s) { ... }
In practice, the only things you can really switch on are int
, char
, and
bool
(although the last one is kind of silly).
You can, if you want, use if-else
and loops inside a case
. And likewise,
you can use a switch-case
within a loop, or if-else
. The latter is much
more common than the former. In general, it’s wise to avoid nesting control
structures too deeply. Instead, split things up using functions, which we’ll
look at in a bit.
Examples:
The user can type 1, 2, or 3, or ‘q’ to quit:
char choice; cin >> choice; switch(choice) { case '1': // Do option 1 break; case '2': // Do option 2 break; case '3': // Do option 3 break: case 'q': return 0; // No need for `break`, since return exits default: cout << "That is not a valid option!" << endl; }
It would be good to wrap this in a
while
loop, so that if the user makes a wrong choice, they are re-prompted. That would look something like this:while(true) { char choice; cout << "Enter 1,2, or 3, or q to quit." << endl; cin >> choice; switch(choice) { ... case 'q': return 0; // This ends the loop, too default: cout << "That is not a valid option!" << endl; } }
A not-very-good way of classifying numeric characters as odd or even:
char c = ...; bool is_even = false; switch(c) { case '0': case '2': case '4': case '6': case '8': is_even = true; break; }
After this,
is_even
will containfalse
is the character is odd (or not a number) andtrue
if it is even.