CS 225 Notes 5

Introduction

This will be a discussion of functions in C++. There will also be notes on the use of reference parameters and maybe operator overloading.
 

Functions (Chapter 3)

All programming languages have functions. They can be called different things. Subroutine, routines, procedures and functions all describe the same thing. A function is a piece of code that is run by itself. Control is transferred to it from t he main program and control returns after the function is completed. Functions can take arguments and can return results. The general form of a function in C++ is
     return_type func_name(type1 arg1, type2 arg2,...)
    {
        // code for the function
   } // func_name

A function can have no arguments. In C++, a function is called by using its name followed by a parenthesized list of arguments. So this function, from one of the example programs

// calculate length
int
str_length(char * str)
{
 char *ptr=str;
 int len=0;
 while(*ptr++) len++;
 return(len);
} // str_length

The return type is integer. This means that anywhere an integer can be used, a call to this function can be used. Its name is str_length and it takes one argument, a character pointer. At the end, it returns an integer value. In between the braces is the code the implements the function. Variables can be declared inside the function that are local to the function. This means that the variables, like len, are only used inside the function. This variable len would be different from another variable len used somewhere else in the program. A few other comments. If the function doesn't return anything, the return type is void. It is possible to make a function that takes an unknown number of arguments, but this works in a very different way.

Some details about the return statement. If the function doesn't return a value, the return statement is optional as a return occurs when the end of the function is reached. But a return statement can be used if you want to exit the function early. In this case, the form is just
    return;
If there is a value to return,  there are two forms. Both forms work exactly the same.
   return expr;
   return (expr);
A function can return only one value.

About function calls and how they work.

A typical function call looks like this

     len=str_length(instr);

When this is executed, the control transfers from where the call to str_length is in the code to the beginning of the code in the str_length body. The variables described in the function header are set with the values in the function call. The variables in the function header are called the formal parameters. The values in the call to the function are called the actual parameters. The formal parameters are like local variables in the function code. The code in the function is executes in the usual order. When a return statement is executed, control goes back to the main program just at the point it left. If the function returns a value, then it is as if that value was put in place of the function call.  So if the actual call  was like this
    len = str_length("hello");  // note the use of a string constant
Then control starts at the top of the str_length function. The formal parameter is a character pointer. The type of a string constant is a character pointer so the types match. All the arguments to the function are evaluated before the code is executed. But C++ doesn't guarantee that they are evaluated in any particular order. So don't assume it is left to right. Then the formal parameter,  str is set to the value "hello".  Two local variables are created, len ( set to 0) and ptr (set to str).  The while loop bumps the counter len for each character in the string and stops when it sees the zero (Nul) at the end. So in this case, len = 5. The return statement send the 5 back to the calling function. So the program acts as if the line above was changed to
   len = 5;
Then the program goes on from that point as if it had never called the function.

Function declarations are used in cases where the compiler needs to know about the function before it sees the code. An example of this is in the example code. Here the main function comes before the str_len function. But the compiler needs to know about the str_len function inside main so it can tell if the call is legal or not. So we put the function declaration at the top of the file. The declaration looks just like the definition except for two things. The body code is missing and the formal parameters names are not needed. A common use of declarations is in header files so the compiler can know about functions that may be in other files and which may not have been written yet.

Functions in C++ may have default arguments. For example,
   void str_print(char * str, int length = 10);

This declares a function with one default parameter. If the function is called with only on parameter, like
   str_print("hello");
Then the value of the second parameter is set to 10. If a value is supplied, that is used rather than the default. The default parameter is mentioned only in the declaration, not in the definition. Also, if there is a default parameter, then all parameters after that one nust also have defaults.

All parameters in a C++ function are pass-by-value. There are two major ways information is passed to a function. One is pass-by-value. This means that the expression that is the parameter is evaluated and the result is stored in a local variable. This value is a copy of the original. In this example from the code,
   len=str_length(instr);
A copy of the value of instr (a pointer to an array) is stored in the local variable str. If we were to change the str_length() function to add a line
   str="hello again";
this would have no affect on the value of instr. This is because str is a local variable and is not related to instr.

The other main method is pass-by-reference. Instead of a  copy of the value being passed, a reference to the actual parameter is passed. This means that if the formal parameter is changed, so is the actual parameter. There are times when this is what you want and the details are in the next section.

Recursion is supported by C++. This allows a function to call itself. There are many problems, for example, linked lists, where recursion makes the program easier to understand. Key to the ability to do recursion is the fact that the formal parameters are local variables. So each time a function calls itself, there is a new copy of the variables. It is important to remember that the computer views all function calls the same, even if it is calling itself.

Reference Parameters

If a function is set to be call-by-value, which is the normal case, then changing the formal parameters has no effect on the actual parameters. For example,
    int outside_a=8;
    cout << "before: " << outside_a << endl;
    cbv(outside_a);
    cout << "after: " << outside_a << endl;
where the function cbv() is defined as
   void
   cbv(int inside_a) {
       inside_a=7;
   } // cbv

We would see printed
    before: 8
    after: 8
And this is what we often want. But sometimes, we want to change the actual parameter when we change the formal parameter. If we use pass_by_reference, then when we change the formal parameter, the actual parameter is changed. So in this example,
 int outside_a=8;
    cout << "before: " << outside_a << endl;
    pbr(outside_a);
    cout << "after: " << outside_a << endl;
where the function pbr() is defined as
   void
   cbv(int& inside_a) {
       inside_a=7;
   } // cbv

We would see printed
    before: 8
    after: 7

the corresponding code if we didn't have pass-by reference  is

    int outside_a=8;
    cout << "before: " << outside_a << endl;
    cbv(&outside_a);
    cout << "after: " << outside_a << endl;
where the function cnv is defined as
   void
   cbv(int *inside_a) {
       *inside_a=7;
   } // cbv

We would see printed
    before: 8
    after: 7

This works because we passed the address (a pointer) of outside_a and when we changed *inside_a, this changed the thing
it pointed at, which is outside_a.

Common uses for pass-by-reference are to update something when we want the return value to to tell us if it worked or not. Or when we need to change or return several value. It can also be be used even when we don't want to change the argument. If the thing we are passing is large, some kind of structure for example, pass-by-reference is better because pass-by-value makes a copy of the who structure, which can take time and uses memory. Also, you may have functions where you want to change the parameters sometimes and not others.

The pass-by-reference method is C++ is much easier to use and get right than the pointer method. If you forget the ampersand on a call, the function will still work but not do what you expect. This can be hard to debug.

References can be used outside of function calls. In this case they are a kind of alias of another variable. Look at the following declarations:
    int normal_a=42;
    int *pointer_a = &normal_a;
    int & ref_a = normal_a;
ref_a has become another name for normal_a. To use pointer_a to change normal_a, you have to do
    *pointer_a = 56;
But you can use
   ref_a = 56;
pointer_a can be changed to point to another integer, but ref_a is always attached to normal_a.

Operator Overloading

Overloaded operators are simply another name for a function call. the expression 'a+b' is another way of saying '+(a,b)'. You can overload a
          variety of operators in C++, both unary and binary. The overloaded operators must operate on a user type. You cannot redefine what it means
          to add integers by overloading the '+' operator. A unary operator function takes no arguments and a binary operator takes one. This is because
          the object on the left side is the other argument. So 'a+b' is more like 'a.+(b)'. Here is an example involving string concatenation.

          class string {
          public:
          char * operator+(const string str)
          {
             char *result= new[this->length() + str.length() + 1];
              strcpy(result,this->value);
              strcat(result,str.value);
               return(result);
          }
          private:
             char value[32];
          }

          iostream overloading
          You can create your own insertors by overloading the << operator. To print a name with a label for a class you created:

              ostream &
              operator<<(ostream& os, const myclass &t)
              {
                 os << "myclass:name is " << t.name << endl;
                 return os;
              } // << operator

          increment and decrement overloading
          It is possible to overload the ++ and -- operators. You can even differentiate between pre and post increment. The compiler sees the prefix call,

           ++a;

          as a call to

              operator++(a)

          However, a postfix call is seen differently:
 
              a++;
              operator++(a,int)

          The compiler provides the dummy second argument.