Timilearning

Learning C++ from Java - Pointers and References

· 6 min read

This is a continuation of the series on C++ topics I've found interesting. You can read the earlier parts here and here.

Before I started learning C++, I had read about Java being pass-by-value rather than pass-by-reference, but I found it easier to think I was passing objects around by reference.

This post begins by summarizing pointers and references in C++, before describing the different semantics for passing arguments to a function.

Table of Contents

Pointers

A pointer in C++ is a variable that holds the address of another variable. We use the address-of(&) operator to get the address of a variable, and can store that address in a pointer.

For example, we can declare an integer and print its memory address, as shown below.

#include <iostream>

int main()
{
int width = 40;
std::cout << "The address of the width variable is " << &width << "\n";

int *widthPtr = &width; // We use the asterisk to declare a pointer.
std::cout << "The address of the width variable is "
<< widthPtr << "\n";
}

In the example, we print the address of the width variable using the address-of operator directly and the widthPtr pointer. This prints:

The address of the width variable is 0x7ffcaf841c3c
The address of the width variable is 0x7ffcaf841c3c

Now that we have declared a pointer, we may want to access the value stored at the address it holds. To do that, we use the indirection or dereference (*) operator:

#include <iostream>

int main()
{
int width = 40;
int *widthPtr = &width;

std::cout << "The value of the width variable is " << width << "\n";
std::cout << "The address of the width variable is: " << widthPtr << "\n";

*widthPtr = 93;

std::cout << "The value of the width variable is " << width << "\n";
std::cout << "The value of the width variable accessed via the pointer is "
<< *widthPtr << "\n";
}

Which gives the output below:

The value of the width variable is 40
The address of the width variable is: 0x7ffc882a0f0c
The value of the width variable is 93
The value of the width variable accessed via the pointer is 93

It might confuse you to see the same asterisk * operator used to both declare and dereference a pointer. To clarify this, a rule of thumb is you are declaring a pointer when you use the operator after specifying a type—as it is with declaring a regular variable—and dereferencing one when there's no type before it.

References

A reference variable is an alias to an existing variable. You use the ampersand & symbol to declare a reference. For example, widthRef is an alias to width in the example below:

int main()
{
int width = 40;
int &widthRef = width;
}

Here, the ampersand & symbol means "reference to" and not "address of". A rule of thumb here is the operator always means "reference to" when on the left-hand side of the equals sign and means "address of" when on the right.

After creating a reference, you can use it to access the variable it is aliasing:

#include <iostream>

int main()
{
int width = 40;
int &widthRef = width;

std::cout << "The value of the width variable is " << width << "\n";

widthRef = 93; // Notice the missing ampersand

std::cout << "The value of the width variable is " << width << "\n";
std::cout << "The value of the width variable accessed via the reference is "
<< widthRef << "\n";
}

Which prints:

The value of the width variable is 40
The value of the width variable is 93
The value of the width variable accessed via the reference is 93

There are some rules for using references which make them safer to use than pointers, such as that unlike pointers, they cannot hold a null value. Also, you must always initialize a reference when you declare it and, once initialized, you cannot reassign it.

Using an example from Learn C++:

int main()
{
int value1{5};
int value2{6};

int &ref{value1}; // okay, ref is now an alias for value1
ref = value2; // assigns 6 (the value of value2) to value1 -- does NOT change the reference!
}

Passing arguments to a function

There are three ways of passing arguments to C++ functions:

  1. Pass-by-Value
  2. Pass-by-Reference
  3. Pass-by-Address

To show these methods, I'll use a simple Book class, which takes a title in its constructor and has functions to access the book title:

#include <string>

class Book
{
public:
Book(std::string title)
{
d_title = title;
}

std::string getTitle()
{
return d_title;
}

void setTitle(std::string newTitle)
{
d_title = newTitle;
}

private:
std::string d_title;
};

Pass-by-Value

When you pass an argument by value, the function simply takes a copy of the argument. This means if you modify the parameter in the function, it will leave the original argument unchanged. For example:

#include <string>
#include <iostream>

void passValue(Book b)
{
b.setTitle("The Thing Around Your Neck");
}

int main()
{
Book b1 = Book("Americanah");

std::cout << "b1 has the title: " << b1.getTitle() << "\n";

passValue(b1);

std::cout << "After calling passValue(), b1 has the title: " << b1.getTitle() << "\n";
}

Here, passValue() takes a copy of b1 and modifies the copy, resulting in the output:

b1 has the title: Americanah
After calling passValue(), b1 has the title: Americanah

Use pass-by-value for simple data types like int and float, where the cost of copying the argument is low.

Pass-by-Reference

With pass-by-reference, you are using the parameter as an alias to the argument variable. This means you can change the value of the variable being referenced directly. For example:

#include <string>
#include <iostream>

void passRef(Book &bk)
{

std::string newTitle = "Half of a Yellow Sun";
bk = Book(newTitle);
}

int main()
{
Book b1 = Book("Americanah");

std::cout << "b1 has the title: " << b1.getTitle() << "\n";

passRef(b1); // Notice how we pass b1 as normal

std::cout << "After calling passRef(), b1 has the title: " << b1.getTitle() << "\n";
}

In passRef(), we are not reassigning bk to reference a new variable—recall that we cannot reassign references. Instead, we are changing the value of the variable it is currently referencing, b1 in our example.

This gives the output:

b1 has the title: Americanah
After calling passRef(), b1 has the title: Half of a Yellow Sun

Pass-by-Address

With this approach, you are passing the address of the argument rather than the argument value itself. You use a pointer as the function parameter to store the address, and can dereference the pointer to access the value at the address.

When you pass-by-address, the compiler actually passes the address by value, i.e., the function only gets a copy of the address. This means if you reassign the parameter in the function, you are only telling it to point to a new memory address.

#include <string>
#include <iostream>

void passAddress(Book *bk)
{
bk = new Book("Half of a Yellow Sun"); // bk now points to a new address
}

int main()
{
Book b1 = Book("Americanah");

std::cout << "b1 has the title: " << b1.getTitle() << "\n";

passAddress(&b1);

std::cout << "After calling passAddress(), b1 has the title: " << b1.getTitle() << "\n";
}

Which produces:

b1 has the title: Americanah
After calling passAddress(), b1 has the title: Americanah

In the example, we are assigning a new address to bk in passAddress and leaving the value of b1 untouched.

If we want to access the members of b1 in passAddress(), we can use the dereference(*) operator or the shorthand arrow (->) operator:

void passAddressAlt(Book *bk)
{
std::cout << "bk has the title: " << (*bk).getTitle() << "\n";
std::cout << "bk has the title: " << bk->getTitle() << "\n";
}

An aside

When you create an object in C++ using the new keyword, you must explicitly deallocate it from the heap using the delete keyword to prevent a memory leak.

This means the passAddress() function above will cause a memory leak, since there is no way to access the new object created in the function after it returns, but I'm keeping things simple for demonstration.

Modern C++ introduced smart pointers, which can auto-delete objects not being referenced, but I'm leaving that out in this post.


Pass-by-Reference or Pass-by-Address

For complex data types where copying may be too expensive, the choice between pass-by-reference and pass-by-address is not straightforward.

Learn C++, for example, recommends sticking with pass-by-reference, but a more common recommendation I have found is to use pass-by-address when you are modifying the parameter in the function and pass by const reference otherwise.

By using the latter approach, you are making it more explicit that an argument may be modified.

Java is Pass-by-Value

In Java, you only pass arguments by value, but this has the same semantics as pass-by-address in C++.

I mentioned earlier that with pass-by-address, you are actually passing the argument's address by value, i.e. passing a copy of the address. This is the same in Java: you are passing a copy of a variable's address.

If you have the following method signature in your Java program:

void passAddress(Book b); 

It is equivalent to the below in C++:

void passAddress(Book* b);  

Which is why if you implement the Java method like below, it will leave the original argument unchanged.

void passAddress(Book b)
{
b = new Book("Purple Hibiscus");
}

Further Reading

c++17

A small favour

Did you find anything I wrote confusing, outdated, or incorrect? Please let me know by writing a few words below.

Follow along

To get notified when I write something new, you can subscribe to the RSS feed or enter your email below.

← Home