I find the use of both __declspec(dllexport) and the .def file together to be useful in creating portable DLLs, i.e. DLLs that can be called from code compiled with a different compiler or with different compiler settings.
Just putting __declspec(dllexport) on your function declarations will cause those functions to be "exported" by your DLL (at least on Windows) so that they can be called from outside the DLL.
However, adding to the build a .def file that lists all of your exported functions lets you stop Microsoft compilers (for example) from adding a leading underscore and trailing parameter-width information to the exported function name (at least when combined with the __stdcall directive, also useful for portability). E.g. the function declaration
void foo(int i);
could end up being exported as "_foo@4" if you aren't careful about calling convention and .def file usage.
Keeping the exported function names in the symbol table free of such name-decoration comes in really handy when making GetProcAddress() calls as part of loading and hooking into a DLL explicitly at runtime. i.e. to get a pointer to the above function foo() (assuming it was exported at all) at runtime, you ideally just want to call:
HANDLE dllHandle = LoadLibrary("mydll.dll");
void* fooFcnPtr = GetProcAddress(dllHandle, "foo");
With some appropriate error case checking of course!
Use of a .def file plus __stdcall, __declspec(dllexport) and extern "C" on your function declarations when building your DLL will ensure that the above client-side code will work for a wide range of compilers and compiler settings.