It is a bit difficult to puzzle out what your "sample" code is supposed to be showing happening. Part of that is because the underlying semantics of parameter passing do not really map well into code that can be shown. This is all behind-the-scenes details and can't really be expressed in the language proper in any way more explicit than it normally is.
I'm also not really sure from your brief explanation exactly what your mental model is of argument passing, so I'm not sure where to start in clarifying it. So let's just start from the beginning.
When a function accepts a parameter "by value", the caller of that function makes a new copy of the passed object, and passes it to the function. The function then consumes that copy, doing whatever it wants to it. When that function ends, that copy is effectively thrown away. This leaves the caller with its original object.
When a function accepts a parameter "by reference", the caller of that function actually passes its own copy of the object to the function. The function then consumes that copy, doing whatever it wants to it. When that function ends, any changes that it has made to the object are permanent, since it's the same object, and those changes are reflected at the caller site. In other words, there is no copy made.
Pass-by-value is actually the way everything works in C. When you do:
void Function(int foo);
the parameter foo
is passed by value. Similarly, when you do:
void Function(int * foo);
the parameter foo
is still passed by value; it's just that the parameter being passed by value is actually a pointer, so this simulates passing "by reference", because you're passing a reference to the original value in memory via a pointer indirection.
In C++, you actually have true pass-by-reference semantics because the language has first-class reference types. So when you do:
void Function(int & foo);
the parameter foo
is actually passed by reference—Function
gets a reference to the original object that the caller has. Now, behind the scenes, C++ is going to implement references via pointers, so there really isn't anything new going on. You just get a guarantee from the language that there will never be a "null" reference created, which saves you from a whole category of bugs.
I believe that one's understanding of these details can be enhanced by looking at how this is actually implemented under the hood by a compiler. The implementation details vary across implementations and architectures, but in general, there are two basic ways that parameters can be passed to functions: either on the stack, or in the processor's internal registers.
If a parameter is being passed on the stack, then the caller "pushes" the value onto the stack. The function is then called, and it reads/uses the data from the stack. After the function is complete, the parameter is "popped" off the stack. In a pseudo-assembly language:
PROCEDURE getx // int getx(int one, int two)
LOAD reg1, [stack_slot1] // load parameter from stack slot #1 into reg1
LOAD reg2, [stack_slot2] // load parameter from stack slot #2 into reg2
ADD reg1, reg2 // add parameters (reg1 += reg2)
RETURN reg1 // return result, in reg1
END
PROCEDURE main // int main()
PUSH 2 // push parameter 1 onto stack
PUSH 55 // push parameter 2 onto stack
CALL getx // call function 'getx'
// The function has returned its result in reg1, so we can use it
// if we want, or ignore it.
POP stack_slot1 // pop parameters from stack to clean up stack
POP stack_slot2
RETURN 0
END
Here, we have "pushed" constant values onto the stack. However, we could just as easily have pushed a copy of a value in a register.
Notice that the "push" makes a copy of the value, so passing via stack is always going to be pass-by-value, but as we have said, a copy of a pointer can be passed in order to give pass-by-reference semantics. Any changes made to the object through the pointer will be reflected in the callee.
If a parameter is being passed in a register, then the caller must ensure that the value is loaded into the appropriate register. The function is then called, and it reads/uses the data from that register. After the function is complete, any changes it made to the value in the register are still visible. For example:
PROCEDURE getx // int getx(int one, int two)
ADD reg1, reg2 // add parameters (reg1 += reg2)
RETURN // result is left in reg1
END
PROCEDURE main // int main()
MOVE reg1, 2 // put '2' in reg1
MOVE reg2, 55 // put '55' in reg2
CALL getx // call function 'getx'
// The function has modified one or both registers, so we can use
// those values here, or ignore them.
RETURN 0
END
If main
is doing something else with the values before or after the function call, then it can do that in the exact same registers that getx
uses for its parameters. This would basically be pass-by-reference semantics. Or, it can get pass-by-value semantics by copying the values into new registers first, calling getx
, and then copying the result(s) back out.