Different OO languages differ in terms of when an object is actually instantiated (created). In C#, when we declare a variable to be of a user-defined type, like
we haven't actually created an object in memory yet. Rather, we've simply declared a reference variable of type Student named y. This reference variable has the potential to refer to a Student object, but it doesn't refer to one just yet; rather, it's said to have the value null, which as we saw in Chapter 1 is the C# keyword used to represent a nonexistent object.
We have to take the distinct step of using a special C# operator, the new operator, to actually carve out a brand-new Student object in memory, and to then associate the new object with the reference variable y, as follows:
y = new Student();
Behind the scenes, what we're actually doing is associating the value of the physical memory address at which this object was created—known as a reference—to variable y.
Don't worry about the parentheses at the end of the preceding statement; we'll talk about their significance in Chapter 4, when we discuss the notion of constructors.
Think of the newly created object as a helium balloon, as shown in Figure 3-3, and a reference variable as the hand that holds a string tied to the balloon so that we may access the object whenever we'd like.
Because a reference variable is sometimes informally said to "hold onto" an object, we often use the informal term handle, as in the expression "reference variable y maintains a handle on a Student object."
We can also create a new object without immediately assigning it to a reference variable, as in the following line of code:
but such an object would be like a helium balloon without a string tied to it: it would indeed exist, but we'd never be able to access this object in our program. It would, in essence, "float away" from us in memory.
Note that we can combine the two steps—declaring a reference variable and actually instantiating an object for that variable to refer to—into a single line of code:
Student y = new Student();
Another way to initialize a reference variable is to hand it a preexisting object: that is, an object ("helium balloon") that is already being referenced ("held onto") by a different reference variable ("hand"). Let's look at an example:
// We declare a reference variable, and instantiate our first Student object. Student x = new Student(); // We declare a second reference variable, but do not instantiate a // second object. Student y; // We pass y a "handle" on the same object that x is holding onto // (x continues to hold onto it, too). We now, in essence, // have two "strings" tied to the same "balloon". y = x;
The conceptual outcome of the preceding code is illustrated in Figure 3-4: two "strings", being held by two different "hands", tied to the same "balloon"—that is, two different reference variables referring to the same physical object in memory.
We therefore see that the same object can have many reference variables simultaneously referring to it; but, as it turns out, any one reference variable can only hold onto/refer to one object at a time. To grab onto a new object handle means that a reference variable must let go of the object handle that it was previously holding onto, if any.
If there comes a time when all handles for a particular object have been let go of, then as we discussed earlier the object is no longer accessible to our program, like a helium balloon that has been let loose. Continuing with our previous example (note highlighted code below and Figures 3-5, 3-6, and 3-7):
// We instantiate our first Student object. Student x = new Student(); // We declare a second reference variable, but do not instantiate a // second object. Student y; // We pass y a "handle" on the same object that x is holding onto // (x continues to hold onto it, too). We now, in essence, // have TWO "strings" tied to the same "balloon". y = x; // We now declare a third reference variable and instantiate a second // Student object. Student z = new Student(); // y now lets go of the first Student object and grabs onto the second. y = z; // Finally, x lets go of the first Student object, and grabs onto // the second, as well; the first Student object is now lost to // the program because we no longer have any reference variables // maintaining a "handle" on it! x = z;
As it turns out, if all of an object's handles are lost, it might seem as though the memory that the object occupies is permanently wasted. (In a language like C++, this is indeed the case, and programmers have to explicitly take care to "reclaim" the memory of an object that is no longer needed before all of its handles are dropped. Failure to do so is a chronic source of problems in C++ programs.) In C# (and all other .NET languages) the common language runtime (CLR) periodically performs garbage collection, a process that automatically reclaims the memory of "lost" objects for us. We'll revisit this topic in Chapter 13.