Are the related objects in an SOQL query shared?

Can anyone point me to some documentation that states this as expected behaviour?

I've never seen such documentation.

Empirically, it appears that the common B__c instances are actually shared (I can update the B__c instances obtained via A__c.B__r in different ways, based on other A__c values, and get the expected results).

This is definitely true; I was able to verify this in my developer edition using ===, which compares memory addresses rather than contents:

Contact[] records = [select account.name from contact where account.name = 'demo'];
system.assert(records[0].account === records[1].account);

However, without documentation, we cannot rely on this behavior. At this point, someone should probably open a case with support. Either the documentation should be updated (thus ensuring we have a guarantee), or the behavior should actually be changed, because this could have unintended consequences for developers that try to do "clever" things with those records.

While I'm glad that Apex is saving us some memory (heap is really easy to fill up), undocumented behavior could be dangerous. I've never seen this behavior before, but I can envision some sort of algorithm that uses the account records for several different fields and ends up having invalid data because of this shared object behavior.