(Updated November 21, 2024)
Table of contents
-
Relational Operators
One-way and two-way selection
Compound Statements
Multi-way selection
The Dangling
else
Logical Operators
The
switch
StatementShort-circuit Evaluation
The ternary operator
?:
Safely Reading Numbers – Part II
Exercises
Relational Operators
Up to this point, our statements have been executed sequentially; they have been processed in the order written. The program will execute statements in the order they are written. Sometimes, however, we would like to execute statements conditionally rather than unconditionally.
Statements can be executed in three ways:
- Sequential (unconditional)
- Conditional (selective or branch)
- Iterative (looping)
Programs may contain all three forms of statement execution but, as we have seen, are not required to do anything more than sequential. Iterative statements will be covered in a successive document. This document will concentrate on the conditional execution of our programs.
When deciding what should be done when a condition is met, we must first determine the condition that needs to be detected. In other words, what condition must be met, and how will we know when that condition or event has occurred? Some examples are:
if 18 is evenly divisible by 2
then print "the number is even"
if 8 is less than 0
then print "the number is negative"
if the Sun is shining
note that it is daytime
otherwise
note that is is NOT daytime
Examples of conditional statements in English.
Of course, none of these are actual C statements. These statements are known as pseudocode. Pseudocode describes the details of a piece of logic yet to be written. The beauty of pseudocode is that we can express anything we like without writing in a specific language. Therefore, pseudocode assists in conceptualizing ideas while remaining language neutral.
Determining if a condition has been met often requires comparing a particular value with some known quantity. To make these comparisons, we will need a set of tools that perform the comparison work and give a yes or no result, or more to the point, a true or false result. The collection of C relational operators gives us a true or false result based on comparisons. There are six relational operators, and they are shown in Table 1.
Relational Operator | Description |
---|---|
== |
Equal to |
!= |
Not equal to |
< |
Less than |
<= |
Less than or equal to |
> |
Greater than |
>= |
Greater than or equal to |
Table 1: Relational operators for comparison.
As we will see, the relational operators are binary, just like our arithmetic operators. The operators can involve integral and floating-point primitive data types. Only equal to (==) and not equal to (!=) may be applied when comparing boolean primitive data type variables.
It would be best if you were cautious when testing floating-point values for equality since there is a possibility of a loss of precision when these numbers are stored in memory. It is more likely to occur when these values are involved in complex arithmetic expressions.
One-way and two-way selection
Let us take the first two examples from above and rewrite them in actual C code. The first two examples demonstrate the concept of one-way selection; that is, there is only one possible outcome based on the test being performed. The rewrite is shown below.
if ( 18 % 2 == 0 )
printf("The number is even.");
if ( 8 < 0 )
printf("The number is negative.");
Examples of conditional statements in C.
These examples show a conditional test that must be satisfied before performing the indented portion of code. The line below the if
test is indented to show that the code will only be executed if the test evaluates to true
. Parentheses always surround the condition test itself.
It is now established that statements will be executed if conditions are true. The third example, however, shows an opportunity where work may need to be done when the condition is true
or alternate work done when the condition is false
– a two-way selection. Example 1 shows a complete program for the third example. The original pseudocode that has been converted is highlighted.
#include <stdio.h>
#include <stdbool.h>
int main(void) {
bool daytime, sunShining;
sunShining = true;
if ( sunShining == true )
daytime = true;
else
daytime = false;
printf("sunShining = %d\n", sunShining);
printf("daytime = %d\n", daytime);
}
Example 1: Program demonstrating the else clause of the if statement.
The program output is as expected:
sunShining = true daytime = true
It was easy to predict the output without really knowing what the else
clause would do for our program. Since sunShining
was true
, we would expect the value of daytime to be set to true. The else
clause of the if
statement allows the programmer to offer an alternative if the condition test fails to evaluate to true
. Therefore, if the condition test failed, the statement indented beneath the else
clause would be executed setting daytime
to false
.
The else
portion of an if is matched with the closest unfinished if
, that is, one that has no else
portion.
Compound Statements
Let's change our dayornight
class and add another boolean
variable, nighttime. The product of which is Example 2. In this example we can see that two assignment statements will be executed for both the true and false portion of our if
/else
construct.
The if
portion and the else
portion, by default, can only have one statement associated to each. When we need to perform more than one statement worth of work, we use a compound statement. A compound statement is formed the same way our main method body was formed – with curly braces. Note the closing of the curly braces before the else
portion begins.
As we will see in upcoming sections and future chapters, the compound statement is used by many constructs.
#include <stdio.h>
#include <stdbool.h>
int main(int argc, char *argv[]) {
bool daytime, nighttime, sunShining;
sunShining = true;
if ( sunShining == true ) {
daytime = true;
nighttime = false;
} else {
daytime = false;
nighttime = true;
}
printf("sunShining = %d\n", sunShining);
printf("daytime = %d\n", daytime);
printf("nighttime = %d\n", nighttime);
}
Example 2: Two-way selection with a compound statement.
Multi-way selection
Multi-way selection is often needed when the data tested falls into more than two response categories. Consider the code in Example 3 where we want to test if a number is positive, negative or zero. Clearly a single else
is not enough, but we also cannot have more than one else
per if
statement. The concept of a nested if
can be applied to allow a test with more than two options.
#include <stdio.h>
int main(int argc, char *argv[]) {
int x;
printf("Enter an integer: ");
scanf("%d", &x);
printf("The value %d is ", x);
if ( x < 0 )
printf("Negative.");
else if ( x > 0 )
printf("Positive.");
else
printf("Zero.");
}
Example 3: Multi-way if statement.
Technically speaking, a nested if actually looks like:
if ( x < 0 )
printf("Negative.");
else
if ( x > 0 )
printf("Positive.");
else
printf("Zero.");
The format of Example 3 is preferred for ease of writing and is accepted conventional form in several programming languages.
The Dangling else
Consider a multi-way selection where there is an else portion for the outer condition, but not for the inner one. Here is a piece of code to demonstrate:
if ( age >= 18 )
if ( age >= 21 )
printf("Legal Drinking Age.");
else
printf("Ineligible To Vote.");
The issue is that although the indentation shows the else belongs to the test of age >= 18
, C (and other languages) associates the else
with the closest unfinished if
. As a result, the else
actually belongs to the test for legal drinking age. Those who are ages 18 to 20 are therefore ineligible to vote! This clearly needs fixing and can be done with a simple compound statement to correctly associate the else
as shown below:
if ( age >= 18 ) {
if ( age >= 21 )
printf("Legal Drinking Age.");
} else
printf("Ineligible to vote.");
Another example is an intentional multi-way test after the initial test. This bit of code is intended to find the three largest numbers in a group of four.
sm = min(min(r1, r2), min(r3, r4));
if ( r4 != sm )
if ( r1 == sm )
r1 = r4;
else if ( r2 == sm )
r2 = r4;
else if ( r3 == sm )
r3 = r4;
The C compiler notices that there is a potential that this may not be what you intended and gives the warning.
D20.c:86:5: warning: add explicit braces to avoid dangling else [-Wdangling-else] else if ( r2 == sm ) ^ 1 warning generated.
This is an excellent warning reminding the programmer that there may be unintended side effects. The fix to remove the warning is easy. Simply adding brackets confirms that we intend the multi-way if inside the initial if
test.
sm = min(min(r1, r2), min(r3, r4));
if ( r4 != sm ) {
if ( r1 == sm )
r1 = r4;
else if ( r2 == sm )
r2 = r4;
else if ( r3 == sm )
r3 = r4;
}
Logical Operators
The set of logical operators allows the joining of logical expressions into larger ones. The logical operators are listed in Table 2.
Logical Operator | Description |
---|---|
&& |
and |
|| |
or |
! |
not (unary operator) |
Table 2: Logical operators.
The ! (not) unary operator simply inverts the boolean value. Therefore !true
is false
and !false
is true
.
The two binary operators &&
(and) and ||
(or) work according to standard boolean algebra where result of &&
is only true if both operands are true. Likewise ||
is only false when both operands are false. A standard truth table is shown in Table 3.
Boolean Expression | Result |
---|---|
true && true | true |
true && false | false |
false && true | false |
false && false | false |
true || true | true |
true || false | true |
false || true | true |
false || false | false |
Table 3: Truth table showing &&
and ||
.
The char
type can be used with relational operators since it is an integer primitive type, so let's add some characters to our examples of selection. Here are a few:
char ch;
//...
if ( ch >= '0' && ch <= '9' )
printf("The character is a digit.\n");
if ( ch >= 'A' && ch <= 'Z' )
printf("The character is an uppercase letter.\n");
if ( (ch >= 'A' && ch <= 'Z') ||
(ch >= 'a' && ch <= 'z') )
printf("The character is a letter.\n");
if ( (ch >= 'A' && ch <= 'Z') ||
(ch >= 'a' && ch <= 'z') ||
(ch >= '0' && ch <= '9') )
printf("The character is a alphanumeric.\n");
The first two selection statements show the simple determination of digit (in the range '0' through '9') or uppercase letter (in the range 'A' through 'Z').
The third one joins the concept of letters being upper or lowercase. Note the parentheses to separate the uppercase test from the lowercase test. Also, see that or was used to join the two tests since a character can't be both uppercase and lowercase.
Our final example takes on the more generalized classification of a character being alphanumeric, that is, the character is uppercase, lowercase or numeric.
The switch
Statement
The multi-way selection of if-else-if-else-if-else is very reliable. There exists another method for accomplishing a similar task – the switch
statement. First, let us review the multi-way if
selection using an integer month
to reflect the month of the year.
#include <stdio.h>
int main(int argc, char *argv[]) {
int month;
char monthStr[20], *s;
printf("Enter the month (1-12): ");
scanf("%d", &month);
if ( month == 1 )
s = "January";
else if ( month == 2 )
s = "February";
else if ( month == 3 )
s = "March";
else if ( month == 4 )
s = "April";
else if ( month == 5 )
s = "May";
else if ( month == 6 )
s = "June";
else if ( month == 7 )
s = "July";
else if ( month == 8 )
s = "August";
else if ( month == 9 )
s = "September";
else if ( month == 10 )
s = "October";
else if ( month == 11 )
s = "November";
else if ( month == 12 )
s = "December";
else
s = "AN UNKNOWN MONTH";
snprintf(monthStr, sizeof monthStr, "%s", s);
printf("The month selected is %s\n", monthStr);
}
Example 4: Recap of the if-else multi-way selector.
As you can see in Example 4, the integer is used to determine the string value for the month. We also used a string pointer called s
to point to the month text. This was done simply to reduce the number of snprintf
calls. Rather than have a snprintf
for every condition in the multi-way test, we point to a string. Now, did we have to copy the text to monthStr
?
This approach can be rewritten using a switch
statement as in Example 5.
#include <stdio.h>
int main(int argc, char *argv[]) {
int month;
char monthStr[20], *s;
printf("Enter the month (1-12): ");
scanf("%d", &month);
switch ( month ) {
case 1: s = "January";
break;
case 2: s = "February";
break;
case 3: s = "March";
break;
case 4: s = "April";
break;
case 5: s = "May";
break;
case 6: s = "June";
break;
case 7: s = "July";
break;
case 8: s = "August";
break;
case 9: s = "September";
break;
case 10: s = "October";
break;
case 11: s = "November";
break;
case 12: s = "December";
break;
default: s = "AN UNKNOWN MONTH";
break;
}
snprintf(monthStr, sizeof monthStr, "%s", s);
printf("The month selected is %s\n", monthStr);
}
Example 5: Multi-way selection using a switch
statement.
The switch
statement uses a parenthesized integer expression or selector. In this example the selector is simply month
. The switch
statement takes the selector value and compares it to the list of possible action cases. The selector value and the following list of cases must be an integer. Each case
represents a selection or path to be taken based on the value of the selector. In addition, all cases have a particular value to be matched, followed by a colon.
The optional default
case is used to catch selector values that have not matched any other cases.
All of the statements that follow the colon immediately after a matched case are executed and are not enclosed in curly braces as you would expect a compound statement would normally be. Instead of encasing the statement block in curly braces, a break
statement is used to signify both the end of the block and to leave the switch
statement. If a break
statement is absent, execution would continue through the next case
.
Leaving a break
statement out could be accidental or intentional. The accidental omission of the break
statement would result in code that is logically incorrect in one or more cases of the switch
statement. However, the intentional omission of the break
statement serves to combine cases where appropriate. The latter can help to simplify the code.
An accidental example would be to leave the break
statement out of case 3
in Example 6. If this were done, the value of monthStr
would be initially set to "March"
, then the program would fall through to case 4
and set monthStr
to "April"
only then encountering the break
and leave the switch
statement. Clearly the break
statement is making sure we only set the value of monthStr
appropriate to the integer stored in month
.
An intentional example would be to construct a situation where multiple values are acceptable or differ only in case. With variables letterGrade
as a char
and qp
as an int
, consider the following:
switch (letterGrade) {
case 'a':
case 'A':
qp = 4;
break;
case 'b':
case 'B':
qp = 3;
break;
case 'c':
case 'C':
qp = 2;
break;
case 'd':
case 'D':
qp = 1;
break;
default:
qp = 0;
break;
}
This code determines the quality points as a step in calculating the cumulative average of a student. If letterGrade
had not been determined to be in a specific alphabetic case prior to arriving in the switch
statement, it can handle either one. This code can also be written as follows:
switch (letterGrade) {
case 'a': case 'A':
qp = 4; break;
case 'b': case 'B':
qp = 3; break;
case 'c': case 'C':
qp = 2; break;
case 'd': case 'D':
qp = 1; break;
default:
qp = 0; break;
}
As you can see, the format is rather loose. After all of this, the only remaining question is when do you choose if
-else
over switch
? For a possible answer to that question, consider the earlier code example where we identified a character as alphanumeric. With 52 letters and 10 digits, it would be unlikely one would choose switch
.
In other words, if there is an extensive set of values or the condition test is not a simple comparison, it is probably better suited to an if
construct. If there are only a few values, it is possible either would work, and one version may require more code. In this case, it is really up to the programmer which is the better choice and which could be easily modified to accommodate future changes.
Short-circuit Evaluation
C offers a fail-early or pass-early model of evaluation called short circuit evaluation with the logical operators &&
and ||
. The details of how this works are shown below.
How does it work?
To use the electrical term, a short circuit is precisely what it implies - a connection with a low-resistance conductor. In other words, there is nothing to resist the circuit, and there will be a delivery of a large amount of energy in a short period - this is a bad thing, electrically speaking.
From a coding perspective, the same is true - because there is low resistance in our ability to determine the outcome of a condition. The difference here is we can use less energy because of the predictive nature of truth tables.
Consider the following truth table.
Boolean Expression | Result |
---|---|
true && true | true |
true && false | false |
false && true | false |
false && false | false |
true || true | true |
true || false | true |
false || true | true |
false || false | false |
Table 4: Truth table showing && and ||.
Table 4 is the same Table 3 with a few rows highlighted. The specific details of interest have to do with the left operand.
With AND (&&)
if either side of the expression is false
, then the answer is false
.
Therefore: With AND (&&
) when the left operand is false
, the entire expression is false
.
With OR (||
) if either side of the expression is true
, then the answer is true
.
Therefore: With OR (||
) when the left operand is true
, the entire expression is true
.
In each of these circumstances, it does not matter what the value of the right operand is. No one cares - including the compiler! After your program compiles, the resultant code is such that when the left side is false
for AND (&&
) and the left side is true
for OR (||
), the right side of the expression is ignored since it is insignificant.
In other words, the result is already well-known, and we have short-circuited our way to the answer.
The ternary operator ?:
A ternary operator is a clever tool that has its origins back in the early C programming language. It offers a means to make an if-else a bit more elegant.
Consider the following classic min/max example.
int x, y;
int min, max;
if ( x < y ) {
min = x;
max = y;
} else {
min = y;
max = x;
}
Although this code does what we expect, it is, well, wordy. In its most powerful form, the ternary operator allows us to make decisions on the right-hand side of an assignment operator. It is most often written in the form:
resultVar = condition ? trueResult : falseResult;
The variable resultVar
will have the trueResult
value assigned if the condition
is true, otherwise it will have the falseResult
assigned. The previous example can be simplified to the following.
min = ( x < y ) ? x : y;
max = ( x > y ) ? x : y;
Another helpful example is the plural solution. Consider this bit of code.
if ( guesses > 1 )
printf("You have %d guesses remaining.", guesses);
else
printf("You have 1 guess remaining.");
While this is not an overly complicated scenario, it does lend itself the opportunity for the ternary operator.
printf("You have %d %s remaining.", guesses, (guesses > 1 ? "guesses" : "guess") );
It could be argued that upon initial inspection, this version seems less clear. The value for guesses
is always the value for the %d
. The value for the %s
is determined by the ternary operator which has been parenthesized. A possible implementation is shown in Example 6.
#include <stdio.h>
int main(int argc, char *argv[]) {
int guesses = 1;
char response[80];
snprintf(response, sizeof response, "You have %d %s remaining.", guesses, guesses > 1 ? "guesses" : "guess");
printf("%s\n", response);
}
Example 6: Formatted plural response with ternary operator.
In this final version, we have opted to produce a formatted string response
. This is useful if you have some other portion of the program creating messages that could be passed around - but it is not essential, and we could have kept the simple printf
.
The decision to use the ternary operator is often a personal one. It is generally chosen to simplify bits of code, as demonstrated above.
Safely Reading Numbers - Part II
In the previous chapter we introduced the idea of reading numbers with fgets()
instead of scanf()
. We also indicated that appropriate error checking was needed. Now we augment the original code for that purpose.
#include <stdio.h>
#include <stdlib.h>
#include <errno.h>
#include <string.h>
int main(void) {
long a;
char buf[300];
char *p;
int success;
printf("enter a number: ");
if (!fgets(buf, sizeof buf, stdin)) {
// reading input failed, give up:
return 1;
}
// have some input, convert it to integer:
errno = 0;
a = strtol(buf, &p, 10);
// *p can be '\0' or '\n', but p cannot be buf.
success = ((!*p || *p == '\n') && p != buf && !errno);
if (errno)
perror("strtol");
else if (!success)
printf("You did not enter a valid number.\n");
else
printf("You entered %ld\n", a);
return 0;
}
Example 7: Adding some error checking to our safe method of reading numbers.
Now we will break down some of the finer details of Example 7.
- Lines 12-16 are about getting input from the user while confirming the read was successful. Remember that
fgets()
returns either the first argument (buf
) or null if there is an error. Since there is nothing more we can do if the error does occur, we simply leave themain()
function with a non-zero result. - Lines 19-20 are focused on the conversion. The global variable,
errno
, is obtained fromerrno.h
. Thestrtol()
function will seterrno
to a specific value,ERANGE
, if the number is out of range. - Line 23 tries to determine if the conversion was successful. The value of
*p
will be the character pointed to byp
. If we consumed the entire string in the the conversion, then we will either be pointing to'\0'
or'\n'
. Remember thatfgets()
will capture the newline. We also want to make sure thatp
andbuf
are not equal which would indicate either the user pressed enter or that the first character is not par of a valid number. Finally, we want to make sure thaterrno
is still zero. - Lines 24-29 use a multi-way
if
test to print a pre-defined error message usingperror()
, indicate that the number entered is invalid, or print the result of the conversion.
errno
, and other subtle and even complex methods of indicating something has gone wrong. We have to decide to use these details and write more effective, safe, and resilient code.Exercises
- (Beginner) Given a spell DC (difficulty class) of 16, generate a random number representing a d20 roll and add 2 to it (the constitution modifier). Write an
if
test to see if the roll meets or exceeds the DC representing success. Tell the user the DC, the roll, and their modifier. If the roll succeeds, tell them they avoided being poisoned; otherwise, indicate they are poisoned. - (Beginner) Write a
switch
statement to convert the String values of the days of the week "Sunday" through "Saturday" to the range 0 through 6. - (Intermediate) Convert the
switch
statement for letter grade to quality points conversion into a multi-wayif
test. - (Advanced) Read two strings from the user. Show them both strings and tell them which one is lexicographically greater than the other.
- (Advanced) Perform the previous exercise using the ternary operator
?:
.