Let’s say I have:
class Animal { }
class Dog : Animal { }
And in the main method:
Dog dog = new Dog();
Animal animal = dog;
Now there is one instance of Dog on the heap, and two variables on the stack, with the same reference stored in them. The next step is
//Try to invoke dog.Bark() programatically
//Try to invoke animal.Bark() programatically
The latter fails at run time, because animal hasn’t a Bark method. So there must be something different on stack, for the two variables. What’s the difference resulting in the run time error?
You are correctly deducing a number of implementation details.
Before going into those details, some terms. There are three kinds of storage:
Unmanaged storage is outside the scope of your question. Managed storage is what we’ll call “the heap”. Temporary storage is “the stack” or registers if the jitter is clever and registers are available, but as a simplifying assumption lets ignore registers and just call the temporary storage pool “the stack”.
There are four kinds of types:
Unmanaged pointers are outside the scope of your question. C# allows you to manipulate references (“managed pointers”) and value types directly; contents of reference type instances are only manipulated via references and are always on the heap in the CLR implementation. (Nothing stops the jitter from creating an instance of reference type on the stack if it has enough smarts to know that the object never needs garbage collection and does not survive the method, but in practice our jitters do not do so to my knowledge.)
The other three kinds of things can be on the stack or the heap as necessary, depending on their lifetimes.
You are assuming (correctly) that managed references are implemented as addresses in a flat 32 bit address space. (That would not actually be a legal heap address, but we’ll let it slide.) Of course, an implementation of the CLR need not use raw addresses as references. They could be opaque handles into a table controlled by the GC, for example.
By its “declaring” type I think you mean the actual runtime type of the instance, Dog. This is a confusing use of “declaring”. Instances are not “declared”; instances are ” allocated”. Variables are “declared” and the variable has the declared type “Animal”.
Yes, somewhere in the actual instance data allocated for “new Dog()” there is a “token” that describes the runtime type of the object. If the exact details of how the token works are important to you, ask another question.
Your question is ambiguous because there are two variables on the stack and neither of them are named “dog”.
At runtime the stack space just contains the managed reference to the heap space allocated for the instance. The C# compiler knows that the stack space for dog1 was of compile-time type managed-reference-to-Animal and the stack space for dog2 was of compile time type managed-reference-to-Dog. The jitter therefore knows that information as well. The jitter keeps information about that in its own data structures; it does not pollute the stack. The jitter can keep this information around for optimizations, or for debugging purposes. Or it can throw it away if it doesn’t need that information anymore.
This again is an implementation detail; the jitter is perfectly free to put any additional information on the stack it pleases.