Things I Wish I Had Known: Learning C++ After Learning Java

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.