Safely handling memory allocation across DLL boundaries in Windows
The standard (and recommended) way to solve this is for each module to provide its own exported AllocMem and FreeMem functions (or whatever you want to call them—the names are not important).

This is what Windows DLLs do (for example, the Network API functions provide NetApiBufferAllocate and NetApiBufferFree to handle memory management), and you should strongly consider following the same model in your own project.

Regardless of their names, the implementation of these functions is downright trivial. AllocMem just needs to call new (or malloc if you're using C), and FreeMem just needs to call delete (or free if you're using C). They are just wrappers.

Header:

EXPORTED void* AllocMem(size_t cb);

EXPORTED void FreeMem(void* ptr);


Implementation:

EXPORTED void* AllocMem(size_t cb)
{
    return new char[cb];
}

EXPORTED void FreeMem(void* ptr)
{
    delete[] ptr;
}


You then follow the simple rule that every call to AllocMem is matched with a corresponding call to FreeMem from the same module. This ensures that the block of memory is always deallocated by the same module that allocated it.

For additional robustness, you might want to add error checking and even modify the function signatures to make extra information available to the callee for this purpose and/or return a status code to the caller.

Along similar lines lines, if you don't actually need general-purpose memory allocation, you might modify the function signatures to allocate only a certain type of memory and thus make them harder to use incorrectly. For example, AllocIntArray could always allocate an array of integers, taking as its only parameter the length of the array, and FreeIntArray would free a block of memory allocated by AllocIntArray.

These wrappers actually give you a lot of power. If diagnostics indicate that you have memory-fragmentation issues, you can add fixed-size allocation logic or switch over to the low fragmentation heap without introducing breaking behavior that would affect your clients.

As far as your goal of making it easy for the client to use, no competent programmer is going to have a hard time understanding how to use AllocMem and FreeMem functions. Personally, I make it a rule that all memory a caller needs to be freed must be explicitly allocated by the caller. This makes memory-management responsibilities significantly easier to reason about. But if you don't have such a rule (and I can't encourage you to adopt one, because you care more about simplicity than correctness), you could just have the DLL allocate the memory internally by calling the AllocMem function and return that block. Then, the client will just follow the rule that any memory it gets from the DLL, whether implicitly or explicitly, must be freed by calling FreeMem. There are Win32 functions that follow this model, too.

Alternatively, if you don't want to add additional exported functions to your libraries, you can just ensure that all of your code is standardized to use a single external memory allocator. For example, if you are targeting the Windows API, you can use either LocalAlloc and LocalFree, or CoTaskMemAlloc and CoTaskMemFree. This is safe because these allocators are universal and don't depend on the calling module.

I should also point out that if you are writing strictly C++ code and don't have any need for a lowest-common-denominator "C" API, then you can just return a shared smart pointer object from any function that needs to allocate memory. In addition to the obvious benefit of preventing the possibility of memory leaks and vastly simplifying the client code, smart pointer objects remember their associated deleter, thus guaranteeing that the memory will be correctly deallocated, even across module boundaries. If you're C++11, use std::shared_ptr; otherwise, you can use Boost's shared_ptr.