loading...

C++ References

noah11012 profile image Noah11012 ・3 min read

C++ is backward compatible with C so it inherited pointers. A quick recap if you don't know what a pointer is: a pointer is a variable that holds the memory address of an object. Declaring a pointer involves the type of object we are pointing to and the symbol '*'.

Example:

int value = 10;
int *ptr_value = &value;

double vaule2 = 506.305;
double *ptr_value2 = &value;

We have our pointer, but how do we get an object's memory address? With the ampersand symbol (&).

To access data through a pointer, we need to do what is called dereferencing. This again includes the use of the * symbol.

printf("value: %d\n", *ptr_value);
printf("value2: %f\n, *ptr_value2);

Now that the recap is over, we can move onto references.

C++ introduced references as an alternative to pointers. References were designed to be simpler, safer, and easier to read when in use. To create a reference is similar to how we declare a pointer. First, we need the type and the symbol '&'.

int value = 200;
int &ref_value = value;

Notice we don't have anything extra on the right-hand side as we had with pointers. The only extra symbol in this statement is the & symbol on the left.

Another benefit is obvious when it is time to retrieve value from the reference:

std::cout << ref_value << "\n";

There are no extra symbols cluttering the code. Viewing small examples of dereferenced pointers may not show the cognitive burden that may be placed on someone when viewing a larger block of code that has several dereferences going on in a short amount of time.

References, as aforementioned, are safer. Going back to pointers for a moment, a pointer can be within a state of "invalidness". This could mean two things:

  1. The pointer is declared but has no initializer and is subsequently default-initialized

Within local scope, if a variable is not provided with an initial value at creation, a garbage value is left there. Most likely, it was some value left over from some memory previously that we are now occupying. Accessing a default-initialized object is undefined behavior and dereferencing a default-initialized pointer is also undefined behavior.

  1. Null Pointers

A null pointer is a special kind of pointer. Usually, it is a way to say, "Hey, I'm not in use right now, but I am still explicitly initialized!". Null pointers have a place in API design. Many C functions that return a pointer may instead return a null pointer on error. Despite it being "safer" than the previous type of invalid pointer, dereferencing a null pointer is undefined behavior.

At the creation of a reference, it must have an initial value to bind. Failure to do so is a compile time error. Also, a reference is bound to one value for its entire lifetime.

However, references have their weaknesses, too. Like pointers, a reference can "dangle": referring to a space of memory that is freed.

Example:

int &return_five()
{
    int value = 5;
    return &value;
}

int some_value = return_five();
std::cout << some_value << '\n'; // undefined behavior

return_five() is a function that creates a variable and returns it. This may appear innocent to the inexperienced but this program is in error. This value being returned is created on the stack and when the function returns, the stack frame is destroyed and so is the variable inside.

By the time we return the main function, the reference some_value is now referring to freed memory and is a dangling reference.

The Conclusion

References, in theory, are a better alternative to pointers in some regards. Providing a simpler syntax at creation and accessing bounded values. The restraints in place eliminate some of the problems that using pointers may arise.

Posted on by:

Discussion

markdown guide
 

I don't write a lot of C++ but shouldn't your code example declare some_value as a reference to int to match the return type of the return_five function?

int &some_value = return_five();
 

No, because the non-reference variable will contain the value of the bounded value of the reference.

int value = 100;
int &ref_value = value;
int new_value = ref_value;

std::cout << new_value << "\n"

Output:

100
 

Oh yeah okay you're dereferencing when assigning to some_value, gotcha. Just another question: if you use auto, what would the inferred type be?

int value = 100;
int &ref_value = value;
auto new_value = ref_value;

Does new_value remain a reference or now contains the dereferenced value?

Not entirely sure as auto in some instances is tricky. I believe in your example that new_value will the type int.

 

A reference is, of course, a bit of syntactic sugar for a pointer. But there are two differences:

  • You cannot rebind a reference. Assignment to a reference assigns to the referenced lvalue, and not to the reference itself, unlike a pointer:
int one=1, two=2;
int & ref = one;
int * ptr = &two;
std::cout << *ptr << ", " << ref << std::endl; // "2, 1\n"
ptr = one; // Pointer assignment
std::cout << *ptr << ", " << ref << std::endl; // "1, 1\n"
ref = two; // lvalue assignment
std::cout << *ptr << ", " << ref << std::endl; // "2, 2\n"!!

The last line prints two, because the reference is bound to one, and you've just changed the value. The pointer, now also bound to one, therefore prints the same value.

You can make a pointer non-rebindable with a const after the point, though. References, and const-pointers, are similar to the effect of the const keyword in Javascript, and the final keyword in Java.

  • Assignments to and from a reference type are a bit magic. This is best shown with a class type, like a string.
std::string s1 = "Hello world!"; // Legal - s1 is created containing the C-style string's contents.

void f1(std::string); // Always copies.
void f2(std::string &); // Always references existing object.
void f3(std::string const &); // Can reference anything.

Calling any of these functions with s1 is fine - there's always something to reference. f2() might change the string though.

But you cannot call f2 with either an expression, or a literal - because f2 says it might change the string, f2 cannot be called with a temporary.

No such protections exist for pointers.

You can, though, create a nullptr reference quite easily:

int & nullref = *reinterpret_cast<int *>(nullptr);
// Or if you prefer:
int * ptr = nullptr;
int & nullref = *ptr;

Of course, the moment you do anything with ref, you're implicitly referencing it, and you'll experience the joy of undefined behaviour.