When it comes to allocating memory, most of the time, the developer doesn't really care where the memory is, just as long as it's "somewhere".
So, the "* = * + N" technique of grabbing memory leaves the minutiae of managing the actual addresses to the assembler, rather than the developer.
This is clearly most common for simple variables (1 byte, 2 bytes) rather than larger spaces, but the concept is sound. You want to have larger areas for things like terminal buffers, or disk buffers, etc.
Now, instead of statically allocating these areas within the assembly program directly, you can "dynamically" allocate them at runtime. Here, you need to simply know where your "free memory" begins. And from there, you program can gobble blocks as necessary.
Early UCSD Pascal has the simple concept of "mark" and "release". Here, it maintained a pointed to the start of the free area. When you wanted more memory, you grabbed a copy, and then bumped it up. When you were done, you simply reset the pointer back.
So:
Code:
ptr = mark(1024);
... work with ptr ...
release(prr);
This is distinct from more common memory managers, that work with individual blocks, and manage free space, and block chains, etc. (like C's malloc). It's obviously vastly more simpler. But it can still be effective for many use cases.
But in all of these cases, after the code is built, and at runtime, is when these addresses are decided. They're not "assigned" by the developer (i.e. I/O buffer is going to be page $0200). Rather, they're just organically scattered through the code and free memory space and referenced symbolically.
Sometime you do care exactly where a block of memory should be, but many times, you don't. You just need to know you have 64 bytes open someplace safe.