formatting of IPAddress via printf(...) family similar to Serial Object
Whereas KIIV's answer solves your problem, it does not address some very
interesting parts of your original question:


  I am not sure why Serial.print() is able to convert IPAddress to a
  reasonable human readable representation


This works thanks to a couple of abstract classes in the Arduino library
called Print and Printable. A Print object is anything that can
output text, like a serial port, a network connection or an LCD. A
Printable object is anything that can be converted to a text
representation and sent through a Print object.

From Print.h:



class Print
{
    // ...
    virtual size_t write(uint8_t) = 0;
    // ...
    size_t print(const String &amp;);
    size_t print(const char[]);
    size_t print(unsigned int, int = DEC);
    size_t print(const Printable&amp;);
    // ...
    size_t println(const String &amp;s);
    // ...
};


In other words, in order to create a Print, you only have to implement
write(uint8_t), which should output a single character. Then you get
all the print() and println() methods.

From Printable.h:

class Printable
{
  public:
    virtual size_t printTo(Print&amp; p) const = 0;
};


This means you can make any object Printable by implementing
printTo(Print&amp;).

From IPAddress.h:

class IPAddress : public Printable {
    // ...
};


This means IPAddress objects are Printable: they know how to print
themselves into any Print. You can find the implementation of
IPAddress::printTo(Print&amp;) at the end of
IPAddress.cpp.

Thus, to answer your question, Serial.print() does not really know how
to create a textual representation of the IPAddress, but IPAddress knows
how to send itself through Serial, or any other Print object.


  I want to somehow leverage that functionality [...] there must be some
  relatively simple way to utilize the functionality that Serial.print()
  provides but somehow channel it to an sprintf() call.


The functionality is not provided by Serial.print(): it's the fact
that the IPAddress is Printable. In order to leverage that, you have to
create a Print object. You cannot use sprintf() for that, but you can
create a Print object which behaves like sprintf() and writes into a C
string:

// Write into a C string (a char array), a la sprintf().
// Warning: there is no check for buffer overflow.
class StringPrinter : public Print
{
public:
    StringPrinter(char *buffer) : buf(buffer), pos(0) {}
    virtual size_t write(uint8_t c)
    {
        buf[pos++] = c;  // add the character to the string
        buf[pos] = 0;    // null perminator
        return 1;        // one character written
    }
private:
    char *buf;
    size_t pos;
};


which could be used like this:

char buf[16];
StringPrinter(buf).print(ip);


For the sake of simplicity, I would generally prefer the straight
sprintf() solution. However, if you are short on flash, the code above
is likely to be smaller, as sprintf() is large and you are very likely
to be already using Print in your program.