A FILE stream is a C library object with buffering, error/EOF indicators and a logical position. It is not interchangeable with an integer Unix descriptor. Use
Own streams, borrow operations
fopen and tmpfile return an owned FILE pointer or nullptr. A unique_ptr with fclose as its deleter is useful for cleanup; an operation accepting FILE* usually borrows it. Never fclose a borrowed stream or duplicate ownership of the same pointer. A destructor can ensure release, but an explicit finalization step is needed if fclose failure must be reported to the caller.
Open binary files with an appropriate b mode when bytes must be preserved across platforms. Update streams have direction-change rules: output followed by input needs a flush or positioning operation; input followed by output needs positioning unless the input encountered EOF. The examples use rewind/fseek to make that boundary explicit. Do not mix raw descriptor operations and buffered stream I/O casually.
Follow one complete example
Run this program, then change one boundary while keeping the API preconditions valid. Predict its output before pressing Run.
#include <cstdio>
#include <memory>
int main() {
using file=std::unique_ptr<std::FILE,decltype(&std::fclose)>;
file f(std::tmpfile(),std::fclose); if(!f)return 1;
const unsigned char input[]={65,0,66};
if(std::fwrite(input,1,3,f.get())!=3)return 2;
if(std::fseek(f.get(),0,SEEK_SET)!=0)return 3;
unsigned char output[8]{};auto n=std::fread(output,1,8,f.get());
std::printf("%zu %u %d %d\n",n,unsigned(output[1]),std::feof(f.get())!=0,std::ferror(f.get())!=0);
}Control reads by their returned count
fread reports complete elements, so fread(buffer,1,capacity,file) gives a byte count. A short result can still contain useful data. Process those bytes first, then distinguish feof from ferror. A loop controlled only by !feof predicts something the indicator does not promise and often processes stale data once more.
fgetc returns int so every byte stays distinct from EOF. fgets retains a newline when it fits and can return one long logical line in pieces; define a maximum-line policy instead of silently treating each chunk as a complete record. rewind resets position and indicators, while clearerr resets indicators without performing a transfer or repairing the underlying cause.
Buffered acceptance is not completed output
fwrite can accept bytes into a user-space buffer. A later fflush or fclose can report the actual output error. Check positive partial counts, avoid a zero-progress write loop and check the final flush when success promises flushed output. The helper does not claim rollback: bytes already transferred stay transferred on a later failure.
fflush sends the library buffer to the host environment; it is not a power-loss persistence guarantee. Portable ISO C does not define using fflush as a general input-discard operation. The companion Unix/Linux course explains descriptors and host-specific durability APIs. Here the implementation uses standard stdio, while a controlled POSIX fixture closes a backing descriptor to prove that a buffered flush failure is detected.
Read the return value
Predict this smaller case. The check item later changes the values, so explain the rule as well as the output.
#include <cstdio>
int main(){auto f=std::tmpfile();if(!f)return 1;std::fputs("abc",f);std::rewind(f);char b[8]{};auto n=std::fread(b,1,8,f);std::printf("%zu %d\n",n,std::feof(f)!=0);std::fclose(f);}Complete the faded example
State the missing precondition or result check before filling the code. The fill exercise supplies the rest of the implementation.
auto n=std::fread(buffer.data(), 1, buffer.size(), source);
// Fill: write the n returned bytes completely.
// Fill: distinguish ferror from feof.
// Fill: check the destination flush before success.Check your model
If the final fread returns three bytes and sets EOF, which happens first: writing those bytes or leaving the loop?
Practice the boundary
Implement copy_stream(FILE* source, FILE* destination) -> std::expected<std::size_t,stream_error>.
Borrow two distinct binary streams at their current positions. The source is open for reading and the destination for writing; both refer to finite files, and any update-stream direction changes have already been synchronized by the caller. Their lifetime remains the caller's responsibility. Neither is closed, rewound or cleared by this utility.
Reject null or identical pointers as invalid. A pre-existing source error returns read; a pre-existing destination error returns write. Read and write byte-sized elements until EOF, preserving embedded NULs and the final partial block. Return the total byte count on success. Check ferror separately from feof, handle positive partial writes, and report zero write progress or output error as write. Flush output at the end and report flush failure. Read errors return read; output failures encountered while writing a returned block take precedence. No rollback is promised on failure.
Total source bytes fit size_t. A successful flush transfers stdio buffers to the host environment; it does not promise power-loss durability. Implement with the C stdio interface.
Read → six checks → two bug hunts → fill → build. A successful graded submission is evidence; reading or watching alone does not grant mastery. Review cards reinforce API choices separately.
Watch and verify
The Watch & read panel links a free lecture for the relevant background and primary API references. The lecture notes say which parts are background; the tested contract here supplies the exact wrapper policy.
C standard-library course · C library review cards · Unix/Linux systems