"how this works exactly when all have the same destination"?
The answer is simply: They do not have the same destination.
With an assignment like
c0->imag = c1->imag + c2->imag;
No two of the expressions "have the same destiantion",
at least not with a meaningful, or lets say usual, call.
Let's discuss this example:
complex_t complexA = complex_1;
complex_t complexB = complex_1;
complex_t complexSum = complex_0; /* init, later overwritten */
Note that two of those have the same value, but are different variables of type complex_t.
Then usual calling would look like
complex_add( &complexSum, &complexA, &complexB );
Now what happens with ... ?
c0->imag = c1->imag + c2->imag;
c0->real = c1->real + c2->real;
c0->imag
access the imaginary part of what c0
is pointing to, i.e. complexSum.imag
.
c1->imag
access the imaginary part of what c1
is pointing to, i.e. complexA.imag
.
c2->imag
access the imaginary part of what c2
is pointing to, i.e. complexB.imag
.
c0->real
access the real part of what c0
is pointing to, i.e. complexSum.real
.
c1->real
access the real part of what c1
is pointing to, i.e. complexA.real
.
c2->real
access the real part of what c2
is pointing to, i.e. complexB.real
.
See? No two are the same destination.
By the way, I think you can aviod upcoming trouble if you change to
void complex_add( complex_t * const cSum,
const complex_t * const c1,
const complex_t * const c2 );
I trust that the example explained in detail (involving more than one complex variable) also answer the questions in the title.
That will allow changing the content of the output parameter, but nothing else, especially not any of the pointers and not any of the operand values. And that in turn will allow to call with the constants you have shown as parameters for the operands.