Functions

  • Let’s review functions. Remember that functions are like black boxes that take 0 or more inputs (aka arguments or parameters) and return 0 or 1 output. Functions might also have a side effect (e.g. print something to the console).

    black box

Defining Functions

  • To create your own custom functions that don’t come with C++ by default, use the following as a model

    int add(int a, int b) { (1)
        int sum = a + b;
        return sum; (2)
    }
    1 int add(int a, int b) is known as the function’s header or signature. It starts with a return type, which is int in this case. If a function does not return a value, write void. Then comes the name of the function (add), followed by parentheses inside of which you list the function’s parameters (their data types and names) separated by commas: (int a, int b). If a function does not take any input and has zero parameters, write empty parentheses: (). After the function’s header comes the body of the function defined within curly braces {}.
    2 The body of the function includes a return statement where the function return a value whose type matches the return type specified in the function’s header.
  • The above is an example of a function’s definition or implementation.

Declaring Functions

  • The convention is to define functions below main. However, if you use (i.e. call) a custom function in main (or in any other function defined above the definition of the function you are calling), C++ has to know that that function exist, since the compiler reads code top to bottom, left to right. The solution is to write function declarations (aka prototypes) above main.

  • A prototype (declaration) consists of a function’s header followed by a semicolon:

    int add(int a, int b);
  • Be sure to write RMEs above any of your own functions' declarations.

RME

  • Additionally, when we define functions (except for main) in EECS 183, we create a multi-line RME comment, which includes Requires, Modifies and Effects clauses.

    Requires

    Indicates what the function assumes about its inputs (arguments / parameters). The function’s behavior specified in the Effects clause is guarnteed only if the Requires clause is satisfied.

    Modifies

    Indicates what the function modifies outside of its own scope. For example, getline reads from an input stream and stores the value it read into a string variable that is passed into its second argument. For now, your own functions will should not modify anything, except for cin and cout.

    Effects

    Indicates what the function does and specifies what it returns (if it returns a value).

  • Here’s an example of an RME for the sqrt function.

    /**
     * Requires: n >= 0.
     * Modifies: Nothing.
     * Effects:  Returns square root of n.
     */
    double sqrt(double n);

Examples

  • Here’s a small example of a full program declaring, defining and using a function:

    #include <iostream>
    using namespace std;
    
    int add(int a, int b); (1)
    
    int main() {
        int sum = add(1, 2); (2)
        cout << sum << endl;
    }
    
    /**
     * Requires: Nothing.
     * Modifies: Nothing.
     * Effects:  Returns the sum of a and b.
     */
    int add(int a, int b) { (3)
        int sum = a + b;
        return sum;
    }
    1 Prototype / declaration.
    2 Function call.
    3 Implementation / definition.
  • Take a look at functions-0.cpp and at functions-1.cpp in Week 4 source code for more examples of function declarations, definitions and calls.

  • Which is/are valid function prototype(s)?

    1. int getInt(void i);

    2. printInt(int n);

    3. string getString();

    4. int square(int n, 2);

    5. char capitalize(char c);

      Answers: (C) and (E).

      • Remember that we are looking for a valid prototype (declaration).

      • In (A), the type of i is void, which is not a valid data type (but it is a valid return type of a function).

      • The prototype in (B) lacks a return type before printInt.

      • (C) is a prototype of a function that doesn’t take any inputs and returns a string.

      • In (D), 2 should not be inside the parentheses because it is a literal value. Remember that we are looking for a valid function prototype, not a function call. The parameter list in prototypes (declarations) and in implementations (definitions) of functions should have type and names of parameters.

      • (E) is a prototype of a function that takes a char and returns another char that is a capitalized version thereof.

  • Which of the following is a valid function call, given the prototype below?

    void foo(int x, double y);
    1. foo(int x, double y);

    2. cout << foo(1, 42.5);

    3. x = foo(1, 42.5);

    4. foo(1.0, 42);

      Answer: (D).

      • Realize that foo is a function that takes two inputs and does not return a value. The question asks for a valid function call (usage of foo).

      • In (A), we specify the types of x and y, which we should not do when calling a function.

      • In (B), we print the return value of foo. However, foo does not return a value, so the code would not compile.

      • In (C), we assign the return value of foo to x, but foo does not return a value, so the code would again not compile.

      • In (D), we call foo and pass 1.0 and 42 as arguments. Even though 1.0 is a double, we can use it as the first argument, just like we can assign the value of 1.0 to a variable of type int. Similarly, we can use 42 as the first argument, just like we can assign the value of 42 to a variable of type double. We don’t do anything else we foo, since foo does not return a value. (foo presumably has a side effect.)

Scope

  • Local variables (defined inside functions) are known within functions wherein they were defined.

    If a variable called numberOfCookies is defined in main, then no other function can access or use it. If another function also defines a variable called numberOfCookies, then the main and that other function will only know about their own numberOfCookies, since they don’t have access to each other’s local variables.

  • Local variables are passed by value to other functions.

    This means that other functions receive a copy of the value, not the variable itself. This means that the variable in the original function is unchanged unless overwritten.

  • In this example, doubleScore has no effect on playerScore declared in main:

    int doubleScore(int score);
    
    int main() {
        int playerScore = 100;
        doubleScore(playerScore);
    }
    
    int doubleScore(int score) {
        score = score * 2;
        return score;
    }
  • And here, the value of playerScore is overwritten by the return value of doubleScore:

    int doubleScore(int score);
    
    int main() {
        int playerScore = 100;
        playerScore = doubleScore(playerScore); (1)
    }
    
    int doubleScore(int score) {
        score = score * 2;
        return score;
    }
    1 Notice how the value of playerScore is set to the return value of doubleScore.
  • Here’s another example. Try to figure out what this program prints:

    scope.cpp
    1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28
    #include <iostream> using namespace std; // prototypes void foo(); void bar(); // global variable int x = 5; int main() { cout << x << endl; int x = 7; cout << x << endl; foo(); cout << x << endl; bar(); } void foo() { x = 10; int x = 42; cout << x << endl; } void bar() { cout << x << endl; }

Swap

  • We often want to swap two variables' values in programs. Suppose we have two integers, a, that has a value of 2, and b, that has a value of 5. After swapping a and b, we want a to have a value of 5 and b to have a value of 2.

  • To illustrate how to swap to variables, recall that we can think of variables as containers that can hold something. Then suppose we have two cups: one has some orange juice and the other one has milk. If we want to swap the contents of the two cups, we must use a third cup (let’s call it temp).[1] We can pour orange juice into the temp cup, then we can pour milk into the cup that used to hold orange juice, and finally, we’ll pour orange juice from the temp cup into the cup that used to hold milk.

  • Similarly, to swap two variables' values, we need a third, temporary variable. Here’s an example:

    int temp = a;
    a = b;
    b = temp;

    It’s also possible to swap two variables' values without a temporary variable (in most cases). Here’s how. Suppose a has a value of 2 and b has a value of 5.

    a = a + b; (1)
    b = a - b; (2)
    a = a - b; (3)
    1 a is now 7.
    2 b is now 2.
    3 a is now 5.

    However, this method does not work for all cases. Can you think of when this method would fail?

    This method fails when a and b's values are very large, since addition may cause integer overflow.

  • Now, let’s write a program that swaps two variables' values using a function:

    noswap.cpp
    1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32
    #include <iostream> using namespace std; /** * Requires: Nothing. * Modifies: Nothing. * Effects: Fails to swap arguments' values. */ void swap(int a, int b); int main() { int x = 2; int y = 5; cout << "x is " << x << endl; cout << "y is " << y << endl; cout << "Swapping..." << endl; swap(x, y); cout << "Swapped!" << endl; cout << "x is " << x << endl; cout << "y is " << y << endl; return 0; } void swap(int a, int b) { int temp = a; a = b; b = temp; }

    If you try to compile noswap.cpp and run the program. Unfortunately, it will print:

    x is 2
    y is 5
    Swapping...
    Swapped!
    x is 2
    y is 5

    As you can see, x and y are not swapped.

    x and y are not swapped because they are passed by value to swap from main. That means that a and b in swap get copies of the values of x and y. That also means that whatever happens to a and b in swap will have no effect on x and y in main. So even though swap does swap the values of a in b, it fails to swap the values of x and y.

Comparison Operators

  • Comparison operators are binary operators, which means that they are used to compare two values.

    • x < y checks if x is less than y.

    • x <= y checks if x is less than or equal to y.

    • x > y checks if x is greater than y.

    • x >= y checks if x is greater than or equal to y.

    • x == y checks if x is equal to y.

    • x != y checks if x is not equal to y.

  • Notice how two equal signs (==) are used for comparison. By contrast, a single equal sign (=) is used for assignment.

  • The result of comparing two values is always a bool, i.e. either true or false.

Conditions

  • To execute some code only if some condition is true, write something like this:

    if (condition) {
        // do this
    }
  • If you want to execute some code if a condition is true, and some other code if that condition is false, write something like this:

    if (condition) {
        // do this
    } else {
        // do that
    }
  • Here’s an example that prints different things depending on whether the value of course is 183:

    if (course == 183) {
        cout << "EECS 183 is Elementary Programming Concepts"
             << endl;
    } else {
        cout << "I'm not familiar with that class!" << endl;
    }
  • And here’s another familiar example from Project 1:

    string pluralize(string singular, string plural,
                     int number) {
        if (number == 1) {
            return singular;
        }
        return plural;
    }
  • And if you want to have three (or more) forks in the road, you can do this:

    if (condition) {
        // do this
    } else if (condition) {
        // do that
    } else {
        // do this other thing
    }
  • Let’s expand on the previous example:

    if (course == 183) {
        cout << "EECS 183 is Elementary Programming Concepts"
             << endl;
    } else if (course == 203) {
        cout << "EECS 203 is Discrete Mathematics" << endl;
    } else {
        cout << "I'm not familiar with that class!" << endl;
    }
  • And another example that prints different things depending on how x compares to y:

    if (x < y) {
        cout << "x is less than y" << endl;
    } else if (x > y) {
        cout << "x is greater than y" << endl;
    } else {
        cout << "x is equal to y" << endl;
    }
  • For each statement below, determine if it is true or false.

    1. (T/F) if is a function.

    2. (T/F) Every if statement needs an else statement.

    3. (T/F) Every else statement must go with an
 if statement.

    4. (T/F) Order of if / else if / else statements matters.

    Answers:

    1. False.

    2. False.

    3. True.

    4. True.

Boolean Expressions

  • Boolean expressions can be used as conditions in if/else statements to make them more interesting. They always evaluate to true or false.

  • There are three logical operators that you can use to build boolean expressions. The first two are binary operators (they operate on two operands) and the last one is a unary operator.

    • && (and)

    • || (or)

    • ! (not)

  • To illustrate how these logical operators work, we can use truth tables.

    A B A && B

    true

    true

    true

    true

    false

    false

    false

    true

    false

    false

    false

    false

    A B A || B

    true

    true

    true

    true

    false

    true

    false

    true

    true

    false

    false

    false

    A !A

    true

    false

    false

    true

  • We can use these logical operators to build more interesting conditions in if/else statements:

    if (condition && condition) { (1)
        // do this
    }
    
    if (condition || condition) { (2)
        // do this
    }
    
    if (!condition) { (3)
        // do this
    }
    1 Both conditions have to be true.
    2 Either condition (or both) has to be true.
    3 Condition has to be false.
  • If you ever find yourself wanting to write this code, where you don’t do anything if some condition is true, but to something if that condition is false, as in

    if (condition) {
        // do nothing
    } else {
        // do something
    }

    Know that you can simplify your code and improve its style and readability if you instead write

    if (!condition) {
        // do something
    }

Practice

  • Write a boolean expression in C++ that means “\(a < b < c\)”. Hint: a == b is a boolean expression that checks if \(a\) is equal to \(b\).

    Answer:

    a < b && b < c

    Note that the expression a < b < c is not equivalent to the one above in C++.

  • Write a boolean expression in C++ that means “\(0 ≤ x < 10\) or \(x\) is less than \(y\)”.

    Answer:

    (0 <= x && x < 10) || x < y

    Though not always necessary, putting parenthesis around parts of the expression clarifies precedence to both the programmer and the reader of the code.

  • Write a program that asks the user for an integer between 1 and 9. Then say if that number is small (1, 2, 3), medium (4, 5, 6), large (7 8, 9) or invalid.

    The program should behave per sample outputs shown below, wherein red underlined text represents some user’s input:

    Give me an integer between 1 and 9: 7
    You typed a large number.
    Give me an integer between 1 and 9: 183
    You typed an invalid number.

    Solution:

    conditions-2.cpp
    1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
    #include <iostream> using namespace std; int main() { cout << "Give me an integer between 1 and 9: "; int n; cin >> n; if (n >= 1 && n < 4) { cout << "You typed a small number." << endl; } else if (n >= 4 && n < 7) { cout << "You typed a medium number." << endl; } else if (n >= 7 && n < 10) { cout << "You typed a large number." << endl; } else { cout << "You typed an invalid number." << endl; } }

Operator Precedence

  • As in math, some operations evaluate before others, and there is a specific, defined order.

  • From greatest to smallest priority, C++ operators are evaluated in the following order. This table does not include all of the operators in C++, but it does cover all of the ones you’ll see in EECS 183.

    Level Precedence Group Operators Description Associativity

    2

    Postfix (unary)

    ++ --

    Postfix increment / decrement

    Left-to-right

    ()

    Function call

    []

    Subscript

    .

    Member access

    3

    Prefix (unary)

    ++ --

    Prefix increment / decrement

    Right-to-left

    !

    Logical NOT

    + -

    Unary prefix

    &

    Reference

    (type)

    Type-casting

    5

    Arithmetic: scaling

    * / %

    Multiply, divide, modulo

    Left-to-right

    6

    Arithmetic: addition

    + -

    Addition, subtraction

    Left-to-right

    8

    Relational

    < > <= >=

    Comparison operators

    Left-to-right

    9

    Equality

    == !=

    Equality / inequality

    Left-to-right

    13

    Conjunction

    &&

    Logical AND

    Left-to-right

    14

    Disjunction

    ||

    Logical OR

    Left-to-right

    15

    Assignment-level expressions

    =

    Assignment

    Right-to-left

    *= /= %= += -=

    Compound assignment

    16

    Sequencing

    ,

    Comma separator

    Left-to-right

    Operator precedence table is a good thing to put on a notecard for the exam!
  • For a complete table of precedence and associativity of all C++ operators, take a look at http://en.cppreference.com/w/cpp/language/operator_precedence.


1. It’s possible to swap oil and water with just two cups.