I found a blog posting with the most thorough, even elaborate, function I have ever seen to solve this problem.  It handles anything, even horrible corner cases like V:foo.txt where you used the subst command to map V: to Z: but you already used subst to map Z: to some other drive; it loops until all subst commands are unwound.  URL:
http://pdh11.blogspot.com/2009/05/pathcanonicalize-versus-what-it-says-on.html
My project is pure C code, and that function is C++.  I started to translate it, but then I figured out that I could get the normalized path that I wanted with one function call: GetLongPathName().  This won't handle the horrible corner cases, but it handled my immediate needs.
I discovered that GetLongPathName("foo.txt") just returns foo.txt, but just by prepending ./ to the filename I got the expansion to normalized form:
GetLongPathName("./foo.txt"), if executed in directory C:\Users\steveha, returns C:\Users\steveha\foo.txt.
So, in pseudocode:
if the second char of the pathname is ':' or the first char is '/' or '\', just call GetLongPathName()
else, copy "./" to a temp buffer, then copy the filename to temp buffer + 2, to get a copy of the filename prepended with "./" and then call GetLongPathName().