Introduction to C Programming
Decision and Branching Concepts
Outline:
Boolean Type ( or lack thereof in C ) and stdbool.h
- A true boolean data type could be used for storing logical values, and would only have two legal values - "true", and "false".
- C does not have boolean data types, and normally uses integers for boolean testing.
- When a programmer is setting values, set 0 for false and 1 for true
- When the computer interprets them, 0 is false and nonzero is true
- To make life easier, C Programmers typically define the terms "true" and "false" to have values 1 and 0 respectively.
- In the old days, this was done using #define:
- #define true 1
- #define false 0
- Today it is better to use const int instead of #define:
- const int true = 1;
- const int false = 0;
- C99 provides a new header, stdbool.h, that defines some new helpful types and constants:
- The type "bool" is the same as a newly defined type "_Bool"
- _Bool is an unsigned integer, that can only be assigned the values 0 or 1
- Attempting to store anything else in a _Bool stores a 1. ( Recall that C interprets any non-zero as true, i.e. 1 )
- Variables can now be declared of type "bool".
- stdbool.h also defines constants "true" and "false", with value 1 and 0 respectively.
- Use these new features using #include <stdbool.h> at the top of your program
Relational Operators
- Relational operators are binary operators that evaluate the truthhood or falsehood of a relationship between two arguments, and produce a value of true ( 1 ) or false ( 0 ) as a result.
- The following table shows the 6 relational operators and their meaning, by example:
Operator |
Meaning |
A > B |
True ( 1 ) if A is greater than B, false ( 0 ) otherwise |
A < B |
True ( 1 ) if A is less than B, false ( 0 ) otherwise |
A >= B |
True ( 1 ) if A is greater than or equal to B, false ( 0 ) otherwise |
A <= B |
True ( 1 ) if A is less than or equal to B, false ( 0 ) otherwise |
A == B |
True ( 1 ) if A is exactly equal to B, false ( 0 ) otherwise |
A != B |
True ( 1 ) if A is not equal to B, false ( 0 ) otherwise |
- DANGER DANGER: Do not confuse the equality test operator, ==, with the assignment operator, =. It is a VERY common error to use the wrong number of equals signs, ( either too many or too few ), and the resulting code will usually compile and run but produce incorrect ( and often unpredictable ) results.
- Example: "while( i = 5 )" is an infinite loop. i=5 is an assignment operator which returns the value being assigned (e.g. 5), which is an integer. But a while statement expect a boolean. But C doesn't have any real boolean type - booleans are just integers. Therefore, type checking does not save you. 5 is nonzero, so it always evaluates to true. The programmer probably meant to use "while( i = = 5 )", which will test whether or not i is equal to 5, and will only continue to loop as long as this is true.
- <, <=, >, and >= have equal precedence, higher than = = and !=, which have equal precedence with each other. All these operators are evaluated left to right.
- Note: As a general rule, floating point numbers ( e.g. floats or doubles ) should NOT be tested for exact equality, because roundoff makes it virtually impossible for two doubles to ever be exactly equal. Instead one checks to see if the two numbers are acceptable close to each other:
- double x, y, tolerance = 1.0E-6;
- BAD: if( x = = y )
- GOOD: if( fabs( x - y ) < tolerance )
- This is particularly important when testing the stopping condition of an iterative algorithm, because otherwise the roundoff error could cause the program to run forever. WRONG: while( estimatedError != 0.0 )
Logical Operators
- Logical operators are used to combine and evaluate boolean expressions, generally in combination with the relational operators listed above.
- The following table shows the 3 logical operators and their meaning, by example:
Operator |
Meaning |
A && B |
A AND B - True ( 1 ) if A is true AND B is also true, false ( 0 ) otherwise |
A | | B |
A OR B - True ( 1 ) if A is true OR B is true, false ( 0 ) otherwise |
! A |
NOT A - True ( 1 ) if A is false ( 0 ), false ( 0 ) if A is true ( non-zero ). |
- Note: There should not be a space between the | characters in | |, but one is being added here for clarity.
- DANGER DANGER: If you only use a single & or | instead of the double && or | |, then you get valid code that is beyond the scope of this course, but which will not do what you want.
- ! has a very high precedence, higher than the relational operators listed above. && and | | have lower precedence than the relational operators, with && having higher precedence than | |.
- Example: To determine whether x is between 5.0 and 10.0:
- INCORRECT: 5.0 < x < 10.0
- CORRECT: 5.0 < x && x < 10.0
- Question - What is the value of the incorrect expression ( regardless of the value of x ), and why?
- Example: Leap year occurs when the current year is divisible by 4, but NOT when the year is divisible by 100, UNLESS the year is also divisible by 400.
int year;
bool leap_year;
leap_year = ( ( ( ( year % 4 ) == 0 ) && ( ( year % 100 ) != 0 ) ) || ( ( year % 400 ) == 0 ) );
// Or equivalently:
leap_year = (( !( year % 4 ) && (year % 100)) || !( year % 400 ));
- Short Circuiting of Logical Operations:
- In a complex logical expression, the computer will stop evaluating the expression as soon as the result is known.
- "A && B" will not evaluate B if A is already false.
- "A | | B" will not evaluate B if A is already true.
- Example: "if( x != 0.0 && 1.0 / x < 10.0 )" will never divide by zero.
if
There is a broad category of conditional statements - they all hinge on a conditional
that evaluates to true or false.
if( condition )
{
BB;
}
(more code)
|
|
void give_feedback(int studentid)
{
float perc = calculate_grade(studentid);
if (perc >= 90)
{
printf("Great job!\n");
}
printf("Current class percentage: %f\n",perc);
}
|
|
Details:
- A condition is any expression that evaluates to true (non-zero) or false (0)
- BB can be a single instruction or multiple instructions. The curly braces ({ })
are only strictly necessary when BB contains multiple instructions.
- control flow are depicted in control-flow diagrams. Diamonds are decisions and
rectangles are actions. Arrows show the sequence of actions that occur.
- This CFG shows only the one part of the program - it is assumed that there is
code before and after the control structure.
- These can be composed in several ways, as we will see.
How it works:
If the condition is true, the BB will execute and then continue with code after the close curly brace. Otherwise, it will skip over BB and go directly to the code after the curly brace.
When to use this construct:
When you want something to happen in a special case. Otherwise, you do the normal thing.
if-else
The if-else is the complete fundamental building block - based on a single condition, you
choose between two possibilities, whereas for an if alone, you conditionally do one thing.
if( condition )
{
BB1;
}
else
{
BB2;
}
|
|
if (x < 0.0)
{
printf("x is negative.\n");
}
else
{
printf("x is non-negative.\n");
}
|
|
How it works:
If the condition is true, the BB1 will execute and then continue with the code after the
close curly brace. If the condition is false, it will skip over BB1 and go to BB2. Once it
completes BB2, it will continue with the code after the curly brace.
When to use this construct:
When you want to choose between two actions.
nested if-else
One way to choose between three choices is a nested if-else statement. Imagine
instead of negative and non-negative, we want to identify negative, zero, and non-negative.
With nested if statements, we do not view it as choosing between three - we first choose
between two categories (e.g. positive and non-positive) and then separately distinguish between
one of the others (non-positive is split between zero and negative).
if and else basic blocks (BB) can contain any arbitrarily complex code - it could include
loops, sequential code intermixed with code, etc. Once you are inside the BB, the program
can be anything.
if( condition1 )
{
BB1;
}
else
{
if (condition2)
{
BB2;
}
else
{
BB3;
}
}
|
|
if (x < 0.0)
{
printf("x is negative.\n");
}
else
{
if (x > 0.0)
{
printf("x is positive.\n");
}
else
{
printf("x is zero.\n");
}
}
|
|
How it works:
If condition1 is true, the BB1 will execute and then continue with the code after the
close curly brace. If condition1 is false, it will skip over BB1 and go to the else clause.
The else clause contains more code, including another if statement. It will then evaluate
the condition2. If condition2 is true, then it will run BB2. If condition2 is false,
it will run BB3. Once it finishes, it will continue with code after the last curly brace.
The if statement could contain anything - not just a nested if statement. There can
be loops, function calls, etc.
When to use this construct:
When you do not have a simple case. You want to choose between two things, but then you have
further subdivisions of the task.
if-else if-else if
This is a more appropriate choice for when you are choosing between more than two
options. Nesting is a general technique for complex code within if statements.
if-else if-else if is more specifically for choosing between many actions.
When an if-else is used in the false block of another if-else, that is termed a sequential if, ( or cascaded if ) :
if( condition1 )
{
BB1;
}
else if (condition2)
{
BB2;
}
else if (condition3)
{
BB3;
}
else // doesn't always need an else
{
BB4;
}
|
|
if (x < 0.0)
{
printf("x is negative.\n");
}
else if (x > 0.0)
{
printf("x is positive.\n");
}
else
{
printf("x is zero.\n");
}
|
|
How it works:
If condition1 is true, the BB1 will execute and then continue with the code after the
close curly brace. If condition1 is false, it will skip over BB1 and go to the next condition.
It will then evaluate the condition2. If condition2 is true, then it will run BB2.
If condition2 is false, it will run BB3. Once it finishes, it will continue with code
after the last curly brace.
When to use this construct:
When you are choosing between more than 2 actions.
switch
A switch statement is both a syntactic tool used in a specific circumstance and a
structure that leads to a much more efficient implementation. A switch statement should
be used when you are comparing a single variable to possible constant values:
const int left = 1, right = 2, up = 3, down = 4; // For improved readability
int direction;
. . .
printf( "Please enter a direction, 1 for left, 2 for right, ... " );
scanf( "%d", &direction );
if( direction == left )
x--;
else if ( direction == right )
x++;
else if ( direction == up )
y++;
else if ( direction == down )
y--;
else
printf( "Error - Invalid direction entered.\n" );
switch( variable_integer_expression ) {
case constant_integer_expression_1 :
BB1;
case constant_integer_expression_2 :
BB2;
break; // ( Optional - See below )
case constant_integer_expression_3 :
BB3;
default: // Optional
BB4;
} // End of switch on . . .
|
|
const int left = 1, right = 2, up = 3, down = 4; // For improved readability
int direction;
. . .
printf( "Please enter a direction, 1 for left, 2 for right, ... " );
scanf( "%d", &direction );
switch( direction ) {
case left:
x--;
break;
case right:
x++;
break;
case up:
y++;
break;
case down:
y--;
break;
default:
printf( "Error - Invalid direction entered.\n" );
break;
} /* End of switch on direction */
|
|
How it works:
- The variable expression is compared against each of the constant expressions one by one. As soon as a match is found ( equality test is true ), execution jumps to that location and begins executing the statements found there.
- Execution continues until a break statement is encountered.
- If the break statement is omitted from the end of a case, then execution will continue into the next case, and so on until a break is eventually encountered, or until the end of the switch is reached.
- Omitting the break statement is a common mistake.
- Sometimes the break is omitted intentionally, in order to combine cases.
- The default case is a catch-all, and will be executed if none of the above cases match the variable expression. Although the default case is not required, it is good programming practice to always include a default case, in order to catch unexpected or even "impossible" situations.
- Where execution speed is critical, the most likely case should be placed first, followed by the next most likely, and so on. Where execution speed is not critical, or where all cases are equally likely, then cases should be placed in a logical order. There are obvious exceptions to these rules of thumb, such as when one case is designed to fall through to a following case.
- The last case ( or default if present ) does not technically need a "break", but it is good defensive programming to add one anyway, so that it doesn't get overlooked if additional cases get added at a later time.
When to use this construct:
- You have several choices of what to do
- Choices are determined by the value in a single integer variable
- Choices are based on an equality comparison, not less than or greater than.
To illustrate the usefulness of not using the break every time, here is a second
example. This is a very common usage of switch statements - menus. A user enters a menu
choice, and the switch statement determines what to do based on what character the user entered.
bool done = false; // Assumes stdbool.h has been #included
char choice;
while ( ! done ) {
// Code to print out a menu and ask for choice omitted
scanf( "%c", &choice );
switch( choice ) {
case 'E':
case 'e':
EnterData();
break;
case 'H':
case 'h':
case '?':
PrintMenu();
break;
case 'Q':
case 'q':
done = true;
break;
// could have more choices.....
case 'C':
case 'c':
/* Code here to handle Calculations */
break;
// end with the default for invalid input
default:
printf( "Error - %cinvalid\n", choice );
printf( "Choose 'H' for help.\n";
break;
} /* End of switch on menu choices */
} /* End of infinite while loop */
|
|
The Conditional Operator
Note: You are not allowed to use this operator in your code. This is provided
so you understand how to read it, not use.
Earlier we looked at unary and binary operators under C. In addition, the C language contains one ternary operator, known as the conditional operator, ?:
- The conditional operator functions very much like an if-else construct, and the two constructs can often be used interchangeably.
- However, due to its operator status, the results of the conditional operator can also be used as part of a larger expression and/or be assigned to a variable.
- The syntax of the conditional operator is:
condition ? true_clause : false_clause
- The condition is first evaluated, ( along with any side effects. ) Then, either the true clause or the false clause will be executed, ( along with any side effects ), depending on whether the condition evaluates to true ( non-zero ) or false ( zero ) respectively. The value of the conditional operator as used in a larger expression is the value of whichever clause gets executed.
- Examples:
max = i > j ? i : j;
abs = x > 0 ? x : -x;
printf( "X is %s.\n", ( x < 0 ? "negative" : "non-negative" ) );
// Parentheses needed in the last example because + and * higher than > and ?:
pay = base + ( sales > quota ? 1.5 : 1.0 ) * bonus;