What you said about malloc on Windows is true, but I wouldn’t blame it on symbols not being exported by default – but rather on Windows’ choice to ship N different C runtime libraries, one for each MSVC version, as separate DLLs. Most applications and libraries link to one of those DLLs, and if two images link to the same DLL, they do share C runtime state, including the malloc heap [1]. But that’s only possible if they were compiled with the same MSVC version. (There is also an option, -MT, to fully statically link the C runtime, but it’s less commonly used.)
In contrast, both Linux and Darwin have only one C runtime library for the system, in libc.so.6 (typically) or libSystem.dylib respectively, which maintains backwards compatibility over a longer time period. Sometimes there’s a need to make changes to libc that would normally be ABI-breaking, e.g. when off_t was changed to be 64 bits to support >4GB file sizes on 32-bit platforms. But to avoid breakage, special mechanisms are used to provide two different versions of the same symbol within the same library, for each affected symbol; existing binaries will use the old version, while programs compiled and linked on a newer system will automatically use the new version. (On Linux, a complicated mechanism called symbol versioning is used for this, while Darwin just renames symbols with the asm() directive in header files.)
On Linux, it is possible to statically link any of various libcs, but programs that do that typically can’t use shared libraries * at all*, so the issue of malloc across library boundaries doesn’t come up.
Oh, and for the record, apparently Windows 10 has a new “universal” CRT DLL for new code that will be maintained in place going forward, but it’s still separate from all the pre-existing versioned DLLs, and there’s a debug variant that’s a separate DLL from the normal one and probably doesn’t share state with it.
[1] https://msdn.microsoft.com/en-us/library/ms235460.aspx