My advice to all of these problems is the same. Your students do not know how to draw out memory. This is a basic technique that most people who have been programming for a while do mentally, but that nearly all students need to be explicitly taught and shown on paper.
How to Draw Memory
There is a good chance that you have seen something like this before. Maybe when you were learning to program, or maybe when watching another instructor teach a beginner class. Drawing memory is about physically (with pencil or chalk) drawing a diagram of the state of the program's memory. For beginner programmers, some things are typically abstracted away, such as the idea of a register and the specifics about how memory is loaded and stored.
I am going to create a short video tutorial, showing how I personally draw memory, including how I draw loops and functions, but here are some basics in text form:
- Variable is declared => I write the name of the variable and draw a box with a slash through it to signal that it is Null.
- The Variable is defined => I erase the slash and write the value in the box. If the variable is ever reassigned, the value is erased, and the new value is written in its place.
- The Variable is a pointer => the box contains an arrow to another, larger box that holds the data.
- Loop => I draw loops (and other smaller scope constructs) as circles or "bubbles." At the top are the condition and an iterator value if it's a for loop (the iterator value is drawn like any other variable). An arrow points from the bottom of the loop to the condition. All locally scoped values are drawn inside the circle, and the whole thing is erased once the loop finishes.
- Function => I draw functions as large blocks. At the very top, any parameter values are placed in their own box that is linked with an arrow. All locally scoped values, loops, etc. are drawn inside the function block, and the output is linked to the bottom of the box. I typically do not erase function boxes since they are likely to be used again, but you can if you wish.
- Print Statements => I find it helpful to have a three-board view when drawing memory. One display contains the code, which we will go through line-by-line. The next display contains my drawing, an abstract representation of what the program "knows." The third contains the text output. This helps students visualize the distinction between what is happening and what is displayed to the console.
How to Teach Drawing Memory
In lecture, you should draw memory constantly. Any time you review what a piece of code does, you should draw it out with and for your students.
Since your students already sort-of know some basics, you will likely need to go back and explicitly teach them how to draw. Talk to them about why it's important to understand what the program knows at any given time and how it flows through lines of code, and ask them to brainstorm some ways to keep track of that.
Then, show them the basics for a very simple program that they understand. Something that declares and defines two variables and uses an if statement. Have them practice the drawing structure in small groups or pairs on a slightly different problem. Switch gears back to your normal lecture topic. The next lecture, have your students tell you how to draw yet another variant on the same few lines of code. If they can do that, introduce the next drawing construct, maybe a while loop. Repeat this slow and methodical process over the course of several lectures until your students are able to draw anything they can code. Then, when you introduce a new concept, you should show them how to draw and visualize it as part of the initial learning.
The goal is to make drawing second nature so that they do it when they hit a bug or look at code that is not their own. Tell them that eventually, they will be good enough at drawing that they can do it in their heads, but that for now, they should always do it on paper or a board so that they can practice. Until they are at least intermediate programmers, you should encourage or even mandate that your students draw the state of memory every time they approach a problem.
As you teach them this important skill, you should have them practice on code that works and on code that does not work. Give them something with a bug- a one-off error, or a logical issue. Tell them to draw it out or do it with them on the board. Once they are done, ask them to tell you what the issue is and how to fix it. Have them draw it again once it's fixed.
Once they know how to draw, you can ask them to do so in all sorts of situations. Put it on a quiz or test. Require a memory drawing before helping with a bug in their projects. Make sure your TAs do the same. Have them continue to draw memory during class, and encourage them to draw out tricky parts of assignments as they work on them.
How Drawing Solves Your Students' Confusion
You gave three specific examples of holes in understanding. Drawing solves all three.
They are convinced that for (i=0; i<5; i++) return i is going to return 5 results. They are obviously so much confused in thinking that they're doing a minor variation of for (i=0; i<5; i++) printf("%d\n", i).
A drawing of a function only has one return value, and it is at the bottom. They will see clearly that once a value is returned, the function is over. Since they won't reach the return statement until the end, they will return once, the final value of i. Due to erasing past values, they will only see the most recent assignment.
Given a function that needs X as input they constantly point out to me that somewhere in the first lines we need to read X from somewhere, they just ignore that we can add another argument as needed.
A drawing of a function has input values included. In practicing drawing, you will demonstrate that those inputs can be used as defined variables.
They cannot trace the execution of a for loop, they don't check the loop condition: to them it's just "I do the body N times", I don't yet know what do they think of the index used in the loop besides that it magically happens to take the values 0..N-1.
Drawing the loop involves updating the iterator each time through. They will never forget again after drawing out a loop that updates 10+ times.
I hope this helps, I will try to upload a rough video tutorial in the next few days and will update my answer with a link.