Skip to the content of the web site.

Passing and assigning variables

This tutorial describes the behavior of assignment and pass by value, and introduces the const keyword to prevent assignment, pass by reference and pass by constant reference:

Behavior of assignment (=)

In C++, you can assign to local variables and parameters.

#include <iostream>

int main();

int main() {
    int m{42};
    int n{91};

    m = n;    // 'm' is assigned the value 91
    n = 101;  // 'n' is assigned 101
              //    - the value of 'm' is left unchanged

    std::cout << "m = " << m << std::endl;
    std::cout << "n = " << n << std::endl;

    return 0;
}

Passing by value

The same happens when an argument is passed to a function: the parameter is assigned a copy (or the value) of the parameter:

#include <iostream>

int main();
unsigned int factorial( unsigned int n );

int main() {
    unsigned int k{9};
    unsigned int fact_k{ factorial( k ) };

    std::cout << k << "! = " << fact_k << std::endl;

    return 0;
}

unsigned int factorial( unsigned int n ) {
    unsigned int result{1};

    while ( n > 1 ) {
        result *= n;
        --n;
    }

    return result;
}

This behavior of passing arguments into functions is called pass by value.

This is a rather non-standard means of writing the factorial function, but it works, and technically it uses less memory than the more conventional

unsigned int factorial( unsigned int n ) {
    unsigned int result{1};

    for ( unsigned int k{2}; k <= n; ++k ) {
        result *= k;
    }

    return result;
}

Never-the-less, this demonstrates that parameters can be assigned to, as well as local variables. Once an assignment is made, there is no further link to either side. Thus, one can say that assignment copies the value from the right-hand side to the local variable or parameter.

Preventing assignment

In some cases, you may not want someone to change a variable after it has been initialized or to change the value of a parameter inside the function. To do this, you can use the const keyword:

#include <iostream>

int main();
unsigned int factorial( unsigned int n );

int main() {
    unsigned int const k{9};
    unsigned int const fact_k{ factorial( k ) };

    std::cout << k << "! = " << fact_k << std::endl;

    return 0;
}

unsigned int factorial( unsigned int const n ) {
    unsigned int result{1};

    for ( unsigned int k{2}; k <= n; ++k ) {
        result *= k;
    }

    return result;
}

This deesn't affect the final program, but if you ever assign to a local variable or parameter declared const, the compiler will signal an error.

The const keyword is most useful for declaring local variables that should not be changed:

#include <iostream>

int main() {
    double const PI{3.1415926535897932};
    double const SPEED_OF_LIGHT{299792458.0}; 
    double const GRAVITATIONAL_CONSTANT{6.67430e-11}

    // Use these local variables...
}

In general, local variables declared const are written in all capital letters as a reminder to the programmer that the value is constant. Parameters set to const, however, are usually just written normally, with the understanding that the author of the function does not expect that value to change throughout the lifetime of the function.

Assignment and passing of objects

In a previous lesson, we looked at a number of different classes. We can create an array class, but we can also assign objects to other local variables or pass them as functions. Like all primitive data types like int and double, the assignment and passing of objects is by default done by making a complete copy of the object, after which, the two objects are now separated.

This is shown in the following code where

  • When an array is assigned to another array, the other array is lost and overwritten entirely.
  • When an array is passed by value as an argument, the parameter is assigned a copy of the array, and any change to the parameter no longer affects the original argument.

You can see this here, but this is the same behavior for all the classes described in the topic on the The standard template library (STL). It is also the behavior of the classes you will learn to author later on in this collection of tutorials.

#include <iostream>
#include <array>
#include <string>

// Function declarations
int main();
std::string to_string( std::array<int, 5> const a );
void zero_array( std::array<int, 5> array );

// Function definitions
int main() {
    std::array<int, 5> first{};
    std::array<int, 5> second{};

    for ( std::size_t k{0}; k < 5; ++k ) {
        first.at(k) = 100 + k;
        second.at(k) = 200 + k;
    }

    std::cout << "First array:  " << to_string( first ) << std::endl;
    std::cout << "Second array: " << to_string( second ) << std::endl;

    second = first;

    for ( std::size_t k{0}; k < 5; ++k ) {
        first[k] = 1000 + k;
    }

    std::cout << "First array:  " << to_string( first ) << std::endl;
    std::cout << "Second array: " << to_string( second ) << std::endl;

    zero_array( first );
    std::cout << "First array:  " << to_string( first ) << std::endl;

    return 0;
}

// This array should not be changed in this function,
// so it is declared 'const'
std::string to_string( std::array<int, 5> const array ) {
    std::string result{ std::to_string( array[0] ) };

    for ( std::size_t k{1}; k < 5; ++k ) {
        result += ", " + std::to_string( array[k] );
    }

    return result;
}

void zero_array( std::array<int, 5> array ) {
    std::cout << "Array passed by value: "
              << to_string( array ) << std::endl;

    for ( std::size_t k{0}; k < 5; ++k ) {
        array[k] = 0.0;
    }

    std::cout << "Array passed by value: "
              << to_string( array ) << std::endl;
}

You can execute this code on repl.it.

Note: This is the default behavior of all objects in the Standard Template Library (STL). Unfortunately, not all programmers of classes ensure this happens, so you may have to be careful about this.

Passing by reference

Suppose you want to change a value. For example, if you tried to write your own swap function, it would fail:

void swap( int a, int b ) {
    int c{a};
    a = b;
    b = c;
}

If you called this function as follows:

int main() {
    int m{1};
    int n{42};

    std::cout << "m = " << m << std::endl;
    std::cout << "n = " << n << std::endl;

    std::cout << "Calling swap(...)..." << std::endl;
    swap( m, n );

    std::cout << "m = " << m << std::endl;
    std::cout << "n = " << n << std::endl;

    return 0;
}

As you may correctly deduce, yes, the parameters in swap(...) are swapped, but these were only assigned the values of the arguments. The arguments themselves were left unchanged.

What is more confusing is you could call this function:

int main() {
    swap( 42, 91 );

    return 0;
}

What does it even mean to swap two literal integers? But again, all that is happening is that the values of the arguments are assigned to the parameters, and then the parameters are swapped.

To be able to make changes to the actual arguments, the parameters must indicate that they are being passed by reference. This is done by prefixing the parameter name with an ampersand (&):

void swap( int &a, int &b ) {
    int c{a};
    a = b;
    b = c;
}

Now when you call swap, any changes to the parameters are actually changes to the arguments. That is, the parameters are now aliases for the arguments.

Note: Recall that an alias is just another name for something else. For example, Mark Twain is an alias for Samuel Langhorne Clemens, and Joseph Stalin is an alias for Ioseb Vissarionovich Dzhugashvili. In the above code, a and b are aliases for the arguments the function was called with.

Now, for this to work, the only arguments that can be passed by reference are objects that can be assigned a value. With this new swap that uses pass-by-reference, you cannot call swap( 3, m + 5 ) because you cannot assign to a literal integer, and you cannot assign to a sum. Only local variables, parameters, and other objects that can be assigned may be passed by reference.

Here is a working example of the swap function we authored:

#include <iostream>

// Function declarations
int main();
void swap( int &a, int &b );

int main() {
    int m{1};
    int n{42};

    std::cout << "m = " << m << std::endl;
    std::cout << "n = " << n << std::endl;

    std::cout << "Calling swap(...) with pass-by-reference..." << std::endl;
    swap( m, n );

    std::cout << "m = " << m << std::endl;
    std::cout << "n = " << n << std::endl;

    return 0;
}

void swap( int &a, int &b ) {
    int c{a};
    a = b;
    b = c;
}

You can try this out at repl.it.

Passing objects by reference

Similar to passing primitive data types by reference, objects can be passed by reference, as well. For example, previously we had a function that zeroed an array; however, we saw that changes to the parameter did not affect the original argument. We can now pass that array by reference, so that changes to the array in the function affect the array that was passed as an argument:

void zero_array( std::array<int, 5> &array ) {
    for ( std::size_t k{0}; k < 5; ++k ) {
        array[k] = 0.0;
    }
}

You can try this out here:

#include <iostream>
#include <array>
#include <string>

// Function declarations
int main();
void zero_array( std::array<int, 5> &array );
std::string to_string( std::array<int, 5> const a );

// Function definitions
int main() {
    std::array<int, 5> first{};

    for ( std::size_t k{0}; k < 5; ++k ) {
        first.at(k) = 100 + k;
    }

    std::cout << "First array:  " << to_string( first ) << std::endl;

    zero_array( first );

    std::cout << "First array:  " << to_string( first ) << std::endl;

    return 0;
}

void zero_array( std::array<int, 5> &array ) {
    for ( std::size_t k{0}; k < 5; ++k ) {
        array[k] = 0.0;
    }
}

std::string to_string( std::array<int, 5> const array ) {
    std::string result{ std::to_string( array[0] ) };

    for ( std::size_t k{1}; k < 5; ++k ) {
        result += ", " + std::to_string( array[k] );
    }

    return result;
}

You can try this out at repl.it.

You will see that the function zero_array(...) changes all the entries of the array that was passed as an argument. If the argument is not passed by reference, then the only changes inside the function zero_array would affect the parameter, which only has a copy of the argument.

Passing objects by constant reference

Suppose we had a larger array. For example, we may write one function to calculate the average value and standard deviation of the entries of an array, or even simply consider the to_string(...) function written above. To copy an array is expensive, as all the values stored in that array must be copied over.

For example, suppose we want to calculate the average or standard deviation of an array, or even just converting the array into a string. In all these cases: we do not want to modify the array, but we don't want to make a copy, either.

To solve this, we can pass objects by constant reference: thus, no copy is made, but the function cannot accidentally modify the argument, either. For example,

#include <iostream>
#include <array>
#include <string>
#include <cmath>

// Function declarations
int main();
std::string to_string( std::array<double, 20> const &a );
double average( std::array<double, 20> const &a );
double std_dev( std::array<double, 20> const &a );

// Function definitions
int main() {
    std::array<double, 20> data{};

    for ( std::size_t k{0}; k < 20; ++k ) {
        data[k] = 1.0e-10*std::rand();
    }

    std::cout << "Data array: " << to_string( data )
              << std::endl << std::endl;
    std::cout << "           Average: " << average( data )
              << std::endl;
    std::cout << "Standard deviation: " << std_dev( data )
              << std::endl;

    return 0;
}

std::string to_string( std::array<double, 20> const &array ) {
    std::string result{ std::to_string( array[0] ) };

    for ( std::size_t k{1}; k < 20; ++k ) {
        result += ", " + std::to_string( array[k] );
    }

    return result;
}

double average( std::array<double, 20> const &array ) {
    double result{0.0};

    for ( std::size_t k{1}; k < 20; ++k ) {
        result += array[k];
    }

    return result/20.0;
}

double std_dev( std::array<double, 20> const &array ) {
    double result{0.0};
    double array_average{ average( array )};

    for ( std::size_t k{1}; k < 20; ++k ) {
        double difference{ array[k] - array_average };
        result += difference*difference;
    }

    return std::sqrt( result/20.0 );
}

You can try this out at repl.it.

You may wonder why should something be passed by constant reference if it is obviously not going to be changed anyway? This helps protect the author of the code from making mistakes. For example, by inspecting the following code, can you explain what happens?

// Find if the argument 'target' is in the vector
//  - return the index it is located at if is in the vector,
//    otherwise, return the vector size
std::size_t find( std::vector<int> &data, int target ) {
    for ( std::size_t k{0}; k < data.size(); ++k ) {
        if ( data[k] = target ) {
            return k;
        }
    }

    return data.size();
}

How could using constant reference helped the programmer while writing this code?

Summary

To summarize, in general:

Instances of primitive data types should be passed to a function by value if you don't want to change them, and passed by reference if you do.

Objects should be passed to a function by constant reference if you don't want to change them, and passed by reference if you do.