For Windows specifically, using DLL's properly can be a notable challenge. The problems tend to fall in two categories: memory management and content discovery. Memory management specifically causes the most problems because it is silent but deadly when you mess up. Basically a DLL in Windows is just another executable which is mapped into the same memory space as the caller and even has it's own main function (DLLMain) as an optional item. The impact of this is that Windows has several different ways of integration with the runtime memory manager: static versus shared, single threaded versus multithreaded, debug versus release, unicode variations and probably more I'm forgetting. Mixing these different memory manager types works for allocations but when it comes time to free the memory if you call from the wrong library, bad things happen (tm)... Simply stating that everything will use a single variation of memory manager at all times is not going to fix things in many ways. For instance if you use static runtimes you still end up with two separate memory heaps. Additionally you can't always guarantee that 3rd party libraries you may use can be built in the same manner, so sometimes you are stuck.
So, a first goal of utilizing DLL's in Windows is making sure you can fix this problem or absolutely guarantee that no memory ownership is ever going to cross from one side of the boundary to the other. There are a number of potential solutions to this, a common one is to write your DLL content to use an installable override for alloc/free, another would be making all return types opaque handles and a third would be using a COM like reference counted solution. Depending on goals, the way you approach this is up to you though in general the reference counting solution has advantages for the second part of your question.
Discoverability can be either very easy or very hard depending on goals. If you wish to use classes and C++ over the DLL boundary there are a number of challenges involved. First, linkage is compiler specific meaning that in Windows you have to tag things up with declspec(dllexport) everywhere, guarantee the memory management is handled properly (generally speaking you must use the DLL variations of the runtime) and you must still be aware of side issues such as throwing exceptions being illegal over the boundary. (Pretty sure the exception issue is still there, haven't checked in a long time though.) I really don't suggest trying to use C++ classes over the DLL boundary in general, it is just problematic in too many ways, this also means passing things like std::vector in the interfaces is highly dubious.
The most common solution to discoverability involves writing factory classes for anything you need to be shared over DLL boundaries and then exporting one or two functions from every DLL. This is the plugin pattern such that your exe loads the DLL, finds the single function and then calls it so it can register it's content into the factory(s). In order to make this useful there are several potential solutions, but as mentioned I favor the ref count solutions and a COM like pattern:
// Public API stuff...
struct MyStuffInterface
{
virtual int32_t AddRef() = 0;
virtual int32_t Release() = 0;
virtual int32_t Add(int32_t rhs) = 0;
};
MyStuffInterface* (*Creator)(void);
struct Factory
{
virtual int32_t Install(Creator* creator) = 0;
virtual MyStuffInterface* Create() = 0;
};
// Private DLL implementation.
class MyStuff : public MyStuffInterface
{
... trivial implementation details ...
};
declspec(dllexport) int Install(Factory* factory)
{
factory->Install([]() { return new MyStuffImpl(); });
return 1;
}
If you don't like the C++ in this, you can write a compatible variation in C fairly trivially and in fact if you stick to pure C style arguments in the interfaces, you sidestep entire swatches of potential DLL hassles. The benefit is that you can easily expose entire API's from the DLL without the calling side knowing the internals of the DLL except the single "Install" function.
Having said all this, there are many more ways to go, this is just my favorite approach because it solves a lot of different issues in a clean and concise manner. Well, as clean as you get with Windows and the horrible way DLL's work in the totally mixed up environment which is Windows....