Conditional statements

Please note!
This article is under heavy development, has parts of it missing and may include mistakes!

This is the comprehensive and overly long page about writing a basic conditional statement.

Introduction

Conditional statements are the most basic way of managing program control flow. In more literal sense, it allows us to alter what actions the program takes depending on the conditions that we check against.

E.g. an event ticketing system, which will check for the person’s age and conditionally choose an action. Minors will not be allowed to purchase a ticket, elderly will have a reduced price for the ticket and everyone else will pay the full price.

In any sort of conditional statement, we do an evaluation that can only have two possible results – either true or false. In different programming languages they take different forms. In the C89/C90 standard, they use integers for this purpose, where true is 1 and false is 0. In the C99 extension. The keywords true and false can be used when they are either manually defined, or a library stdbool.h  is included, which will define them.

Note that in many of the following paragraphs there are also negative examples – what not to do. They use high background color.

The if statement

The if statement has a basic form of
if ( expression ) statement

Where the expression is the condition to be evaluated and statement means all statements that will be executed, if the expression is evaluated to true.

The if statement is evaluated to true when the expression evaluates to a non-zero value! Note that it was said that true considered to be integer value 1, but the conditional statement will be also true when it is any non-zero value (including -5 or 100). More information under tips and tricks.

Let’s look at an example of this. In this case, we have initialized the variable age to 10. The conditional statement checks if the age is less than 18. Since  it is, everything in the code block marked by the braces will be executed.

Try it out

  • compile and run the code, see what happens
  • Edit the age to be 20, will the printf() statements be executed now?
  • Edit the age to be 18 and try it again. This is what is known an edge case. Edge cases must always be tested, as they are common places for mistakes to creep in!

Relational operators

Assume the following declarations and values for the examples:

Note: The result of relational operators in C is always either integer value 1 (true) or 0 (false).

Operator Description Example
== Evaluates to true when both operands are equal (a == c) –> false
(b == c) –> true
!= Evaluates to true when operands are not equal. ! (exclamation symbol is used for negation) (a != c) –> true
(b != c) –> false
< Evaluates to true if left operand is smaller than the right operand (a < c) –> true
(b < c) –> false
(c < a) –> false
<= Evaluates to true if left operand is smaller than or equal to the right operand (a <= c) –> true
(b <= c) –> true
(c <= a) –> false
> Evaluates to true if left operand is greater than the right operand (a > c) –> false
(b > c) –> false
(c > a) –> true
>= Evaluates to true if left operand is greater  than or equal to the right operand (a >= c) –> false
(b >= c) –> true
(c >= a) –> true

The if-else statement

To add an alternative  actions for the program to execute when the condition is not true, we can add the else keyword. The statements in the else code block will be executed when the expression is evaluated as false.

if ( expression ) statement else statement

Let’s take the previously used example and add an else statement to it. Now depending on the value of the age variable, either the statements in the if code block run or the ones in the else code block.

Try it out!

The initialized value for the age variable is now set to 30.  The code will:
1. Evaluate the condition if (age < 18). This will be false, as age is set to 30 in this example.
2. The code block between the lines 3 – 6 will be jumped over. It will find the else keyword on line 7 and execute the contents in the braces.

Never leave an empty code block! This is bad style! If you don’t have anything to put in the code block where the if statement evaluates to true, invert the condition. 

Handling empty if statement code blocks

You should never have an empty code block. If it happens, there are are two possible options how to solve this.

  1. Find the opposite condition and use that
  2. Invert the  original condition using the inversion operator (!)

Let’s take the bad example from the previous paragraph and fix it. First let’s try to find an opposite condition. To find people who are not younger than 18, we have to look for people who are over 18 years old or exactly 18  years old.

Common mistake: The opposite of (age < 18)  is not age > 18 , but rather (age >= 18) . Watch out for edge cases!

An alternative way would be to invert the condition. This is done by the inversion operator (! – exclamation mark). In this case we take the whole original condition, put it in parenthesis and invert it.

Nested conditionals

Multiple if statements can be nested inside of each other. Be careful with this however, as it can mess up the program structure to the extent where it will take extremely long to untangle and debug it when something goes wrong. Always consider using else if statements, combining multiple conditions using logical operators and other refactoring techniques before writing multiple levels of conditionals. See headings below for this.

Using if-else if-else

To test against multiple subsets of conditions, we can introduce else if statements.  The way it works is that it starts from the first condition and checks them one by one. If one of the conditions is evaluated to being true, the statement(s) within the code block following it are executed. No following expressions are checked once one of them is evaluated to true! If no conditional statements evaluate to true, an else statement will be executed, if it exists.

Logical operators

There are a total of three logical operators defined in the C language.  Note, that logical operators are different from bitwise operators.

Assume the following declarations and values for the examples:

Operator Operation Result will be true, when … Example
&& Logical AND

Conjunction

… both sides are true (a && b) –> false
(b && c) –> true
((a < b) && (b < c)) –> true
|| Logical OR

Disjunction

… at least one side is true (a || b) –> true
((a > b) || (b > c) –> false
((a < b) || (b > c) –> true
! Logical negation

Inversion

… initial condition is false (!a) –> true
(!b) –> false
(!a || !b) –> true
(!(a || b)) –> false

Truth tables

Logical and

A B A && B
0 0 0
0 1 0
1 0 0
1 1 1

Logical or

A B A || B
0 0 0
0 1 1
1 0 1
1 1 1

Logical inversion

A !A
0 1
1 0

 

The switch statement

TODO

 

Some tips and tricks

The following section is a combination of various ways to use conditional statements efficiently.

Using only variable name as expression

This technique relies on the notion, that integer 0 is considered to be false  in C language. Every other (non-zero) value is considered not false ( true).

In this kind of condition, we often use !  (inversion) to get the condition we need quickly.

Try the following code with the initialized value to 0, 1, -1 etc.

Single-line conditionals without braces

In the previous examples, we have shown the the statements to be executed are always within braces.  This is not a requirement. In C, both loops and conditional statements can be written without any braces, however in that case, only the following statement will be executed conditionally as a part of it.

The following example is a perfectly valid if-else statement in C and acts as expected.

These are often preferred to save space and are frequently used by experienced programmers. Sometimes you will also see that the statement will be on the same line as the conditional statement starts to save even more space.

Problems with single-line conditionals

Less experienced should however be careful when using such compact notions. There are multiple reasons to avoid it when starting to program.

1. Less consistent style

You will have to differentiate and notice various styles of writing the statements. This might be confusing in the beginning. Sometimes you will miss a statement here or there and it will cause bugs that are annoying to locate.

2. More prone to accidental mistakes when statements are added or removed.

You will often make alterations to your code during writing it or later on. Without enough experience you might not remember to add the braces when you add a line.

Let’s take the previous age check where it only notified about being underage and add and add another printf()  statement that reminds about requiring parental supervision. We’ll also indent it nicely so it looks like it might be a part of the conditional statement.

In this case when the age is less than 18, all 3 lines will be printed out, which is to be expected. When you forget to test the other case – where the age is 18 or greater, you will not even know that there is a mistake.  For those 18 and over, it will still now warn about requiring parental supervision, even though that was not the intent of the developer.

3. Avoid ambiguity when nested conditionals are used.  

This is also called the dangling else problem, which is important enough to deserve a separate heading. See below.

The dangling else problem

This one brings us back to the nested if statements. Let’s take an example

Syntactically the following code is correct and compiles. It contains a nested if statement that will print out the full ticket price for everyone who is from 18 to 65. Functionally however it behaves incorrectly. This is what is known a dangling else issue. In more layman’s terms – to which of the if  statements does the else belong to? This is made worse by the deliberately indented code, that will make you think it’s correct. However just a reminder – C does not care about whitespace. It could all be written on the same line and it would still compile.

If you have relevant warning messages enabled for the compiler, you will see a warning about this:
warning: suggest explicit braces to avoid ambiguous 'else' [-Wdangling-else]|

Try the code out! Try it with the age set to 20. Then try with the age value of 80. As you can see, the else actually belongs to the last if statement, not the previous one as the indentation might suggest.

Refactoring bad nested conditional statements

Even though it is possible to nest if-else statements together to form constructs where multiple conditions need to be true, in many cases you will end up with badly readable code. This code can become hard to trace, debug and may even have branches that will never or always be true.

This statement can be refactored in many ways. One way would be to reorder the conditions and introduce an else if  statement.

There are both benefits and disadvantages to this. The benefit would be, that it’s really easy to read. All specific cases are tested first and then what will be left is the generic case.

The downside of this can be if the code needs to be well optimized for speed and the event is mainly for adults. This would make the code for the adult branch most likely to be executed, but all the other conditions will be tested first. This will cause the code to run slower. Refactoring often includes code execution speed or memory footprint optimizations, which tend to lie with theory of probability (according to the nature of our data, which condition is the most likely to occur).

Pre-calculating long conditionals

TODO

Function calls in conditionals

To optimize code readability, you often will want to separate code into lots of small single-purpose functions. If these functions return a value, it can be checked within the if statement’s condition.

We can use the comparison operators to compare the returned value from the function if it matches our criteria. Let’s take a function that will count the number of commas in a string.

Now let’s call this function in an if statement.

You can of course use the previously introduced concepts and make it cleaner

You will see a lot of similarly written code in data validation, where you will have only true/false return values for the functions.

We could have functions such as SentenceBeginsWithChar() , SentenceMoreThanOneWord() , SentenceEndWithPunctuation() , ContainsAVerb()  etc – if all are considered true, then it will count it as a possibly a correctly formed sentence.

As usual, this can be combined with other conditional statements. You can also have multiple calls to functions in the conditional statement, as long as you formulate it according to the rules of the language.

Assignments in conditionals

This concept is more used within loops, but also appears in tasks such as data validation. It can be considered a 2-in-1 operation, so instead of doing an assignment before and checking the value later, you combine the two.

 

 

 

Ternary operator

TODO

Ternary operator is essentially a short way of writing an if-else statement. It has the benefit that it can be embedded into code where an if-else statement cannot, hence it’s also sometimes called an inline if. By language syntax, the following is the form for the statement, with symbols such as ? and : separating expressions.

expression ? expression : expression;

However this is not readable unless you know what you are looking at. Let’s write it out a bit more clearly

(condition_to_check) ? expression(s)_if_true : expression(s)_if_false;

 

De Morgan’s law

TODO