FWIW: in my case I wrap most filesystem stuff in a custom VFS layer.
internally, it is more object-based (and new types of file-like objects can be created), but uses a C-like front-end API (it is actually very similar to stdio, just with a name prefix added). however, the backend is more extensible, where new types of files and filesystems can be added as-needed (the VFS backend is actually a little more like the Linux VFS, using a hierarchy and mounting things at various mount-points, with "FS drivers" generally defining both the behavior at the mount-point, and also internally managing file-types specific to this FS type, such as for mounting an OS directory or a ZIP archive).
like, the FS driver will register itself with the VFS, providing a call to mount a filesystem, and the VMount interface provides things like open/opendir/stat/... open may return a VFile, which contains any methods to read/write/seek/close/... within the file, ...
most of this stuff is kept hidden internally though (where code will just open/read/write/... files, without needing to worry too much about things like VFS mounts).
theoretically, a person could also do it Java-style, but IMHO this adds a lot more code and complexity to the API without making really a huge improvement in usability.
another alternative is, granted, just doing something like having a File class or interface (say, an IFile abstract base class) containing a subset of a stdio-like API. if needed, this could be further wrapped to implement more specialized IO interfaces.
admittedly, I am not really personally a huge fan of the design of iostream.