Learning C++ from Java - Pointers and References
· 6 min readThis 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:
- Pass-by-Value
- Pass-by-Reference
- 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 #
- Introduction to pointers - Learn C++
- Reference variables - Learn C++
- Passing arguments by value - Learn C++
- Passing arguments by reference - Learn C++
- Passing arguments by address - Learn C++
- Java is Pass-by-Value, Dammit! - Javadude.com
- Is Java "pass-by-reference" or "pass-by-value" - Stack Overflow answer