Introduction
When I first learned to program at the University of Alberta, I learned in Java. It was at the time, and still remains, a popular language to teach in a first-year programming class. When I started studying the second language in my degree program, C++, I found myself struggling to understand the subtle differences in syntax, the significant differences in how memory management works, and even the differences in the meaning of some terms between Java and C++.
Over the past while, I’ve tutored several students in introductory C++ programming, including some who first learned to program in Java. Because their experience mirrored my own, I was unsurprised to find these students struggling with many of the same points that confused me when I was first learning C++.
That gave me the idea for this blog post — concisely laying out a few things I wish had realized right off the bat when I started with C++. Today, we’ll explore three of them.
Declaration vs. Instantiation
One of the first differences I ran up against is that code that looks like it would just be a variable declaration in Java can actually instantiate an object in C++. To explore this difference, let’s consider a small Square
class. First, here’s the class in Java:
public class Square
{
private int length;
public Square()
{
this.length = 1;
}
public Square(int length)
{
this.length = length;
}
public int getLength()
{
return this.length;
}
}
And, the equivalent class in C++:
class Square {
public:
Square();
Square(int length);
int getLength();
private:
int length_;
};
Square::Square()
{
length_ = 1;
}
Square::Square(int length)
{
length_ = length;
}
int Square::getLength()
{
return length_;
}
In Java, I can declare a variable of type Square
. But, if I want to work with an actual Square
, I have to explicitly create one or assign an existing Square
to the variable. Consider this code, which compiles and runs:
public static void main(String[] args)
{
Square sq = new Square();
System.out.println(sq.getLength()); // 1
}
The following Java code, on the other hand, produces a compiler error, because the variable sq
hasn’t been initialized:
public static void main(String[] args)
{
Square sq;
System.out.println(sq.getLength());
}
Let’s contrast that with the behaviour of the Square
class in C++. The following code compiles and prints out 1
:
int main()
{
Square sq;
std::cout << sq.getLength() << std::endl; // 1
return 0;
}
The first line of the main()
function in this C++ code does more than just declare a variable. It actually calls the default constructor of the Square
class (that is, the zero-argument constructor) and instantiates a new Square
on the stack.
Objects Can Live on the Stack
The Square
in the previous C++ example was instantiated on the stack. That’s a bit of a surprise, coming from a Java background. In Java, all objects are instantiated on the heap. In C++, on the other hand, objects can live on the stack or on the heap.
That has some significant implications for returning objects from functions. Consider the following Java code:
public Square makeSquare()
{
Square ret = new Square(5);
return ret;
}
Because this Java code instantiates the new Square
on the heap, it’s safe to return a reference to that Square
back to the calling function.
If we choose to instantiate a Square
on the stack in C++ (instead of choosing to instantiate it on the heap), we can’t return a reference to it back to the calling function:
Square &makeSquare()
{
Square sq(5);
return sq;
}
This code will compile, but my C++ compiler rightly warns us:
warning: reference to stack memory associated with local variable 'sq' returned
The reason for this warning is that once the makeSquare()
function completes and its stack frame vanishes, the memory associated with the variable sq
becomes garbage memory. At any time, it might be overwritten by another function call. If we want to return a new Square
object from makeSquare()
, we have to allocate it on the heap with the new
keyword, then return a pointer to it instead of a reference:
Square *makeSquare()
{
Square *sq = new Square(5);
// Returned Sqaure must be deleted by calling function
return sq;
}
The Word “Reference” Means Something Different
That brings us to another thing I wish I had realized sooner when I was learning C++. The concept of a reference to an object in Java is far more similar to a pointer in C++ than it is to a C++ reference.
In Java, if we have a variable that holds a reference to a Square
, we can change that variable to refer to a different Square
. Here, we set the variable ref
to refer to the same Square
as sq1
, then to refer to the same Square
as sq2
:
public static void main(String[] args)
{
Square sq1 = new Square(1);
Square sq2 = new Square(2);
Square ref = sq1;
ref = sq2;
System.out.println(sq1.getLength()); // 1
System.out.println(ref.getLength()); // 2
}
When we output the length of sq1
, we get the value 1
. Next, we output 2
, because ref
now refers to the same Square
as sq2
.
Something very different happens in C++ when we create a reference to sq1
. We do that on the first highlighted line of code:
int main()
{
Square sq1(1);
Square sq2(2);
Square &ref = sq1;
ref = sq2;
std::cout << sq1.getLength() << std::endl; // 2
std::cout << ref.getLength() << std::endl; // 2
return 0;
}
Here, ref
is not a variable in its own right. Instead, it’s an alias for sq1
. So, when we run the second highlighted line of code that assigns sq2
to ref
, we’re not modifying a variable ref
to make it refer to sq2
. Because ref
is just an alias, these two lines of code are equivalent:
ref = sq2;
sq1 = sq2;
These lines of code assign the state of sq2
to the state of sq1
. That is, sq1
‘s length is set to the length of sq2
. So when our C++ program outputs the length of sq1
, and the length of the alias for sq1
, it outputs 2
both times.
If we wanted a way to change which Square
is being examined when we call getLength()
on ref
, we actually want to use a pointer in C++:
int main()
{
Square sq1(1);
Square sq2(2);
Square *ref = &sq1;
ref = &sq2;
std::cout << sq1.getLength() << std::endl; // 1
std::cout << ref->getLength() << std::endl; // 2
return 0;
}
This time, ref
is a variable in its own right. It’s a pointer variable, which means that it holds the memory address of a Square
object. First, we assign the address of sq1
to ref
. Then, we assign the memory address of sq2
to ref
. Like in the Java code, the Square
object sq1
remains unchanged, and we output the values 1
and 2
.
Conclusion
Learning a second programming language can be tricky — subtle differences between our first language and our second one can catch us. Hopefully, this blog post helps students who’re facing some of the same challenges I did, when I was learning C++ after first learning Java.
Another tip that could be useful to people learning C or C++ can be found in my post about constant pointers. While the article is written in C, the concept applies equally to code written in C++.
For more tips, and to arrange for personalized tutoring for yourself or your study group, check out Vancouver Computer Science Tutoring.