Showing posts with label I/O. Show all posts
Showing posts with label I/O. Show all posts

2008/03/18

Files in C

A file is a flat representation of data as an ordered sequence of bytes.

Files are written to and read from as a stream, but a file handle can have the concept of a file cursor. The file cursor represents the position in the file of the "next" byte to be read or written.

In Java, the use of the file cursor for random access to a file is discouraged (seeking is fairly expensive, and often leads to errors). Most file I/O happens strictly through the stream abstraction. In C, it's a lot easier to seek backwards and forwards. There are methods to move the file cursor to different locations in the file, but reading and writing still happens through the stream abstraction. When you read or write a byte, the file cursor is automatically advanced one byte.

C's I/O libraries are set up to be type agnostic so that you can read or write any type to/from the stream without excessive overloading (overloading is actually not allowed in C). They work by giving the I/O function a pointer to a buffer of unknown type (a void*) and telling the function how large that buffer is and how many elements it contains.

Reading and writing a single integer value looks like this:

int val;
fread(&val, sizeof(val), 1, f_in);
fwrite(&val, sizeof(val), 1, f_out);

Reading and writing a struct looks like this:

struct AStructType val;
fread(&val, sizeof(val), 1, f_in);
fwrite(&val, sizeof(val), 1, f_out);

Reading and writing an array looks like this:

int[10] val;
fread(val, sizeof(int), 10, f_in);
fwrite(val, sizeof(int), 10, f_out);


Because the read and write values take pointers to buffers, you cannot write any temporary values to a stream.

frwite(atoi("7"), sizeof(int), 1, f);

The above example will generate a compile warning and segfault because the value returned from atoi() is the value 7 and not a pointer to the value 7. The compiler warns us that we are trying to convert an integer to a pointer without a cast, and it segfaults because fwrite tries to read what is stored at memory location 7 (which is not a valid pointer address).

frwite(&atoi("7"), sizeof(int), 1, f);

The above example does not compile because we are trying to take the address of a temporary value. This value may not have an address at all, it may only exist in a register, so we cannot take its address.

int val = atoi("7");
frwite(&val, sizeof(int), 1, f);

Corrects the problem and works as intended.

See: stdio.h (cppreference.com, cplusplus.com)

Stream Abstraction

A common abstraction used for I/O is a "stream." A stream is a reliable connection between two components through which one device can send data to the other. Streams usually operate on bytes (and are called byte streams).

A stream gets it's name because it is like a river of data. The data length is unknown, and you cannot randomly access elements in the stream. When data is added to the stream it flows down the river. You are only allowed to insert data at the start of the stream -- you cannot change it, remove it, or insert something ahead of it -- once it has gone into the stream, it is out of your control. When you remove data from a stream, you can only remove it from the end. You cannot "go back" in the stream (doing so would be the same as pushing a river backwards) and you cannot read a value later in the stream (without reading the data earlier in the stream). The data pours out of the end, and once it is removed, it is no longer part of the stream.

Imagine a stream as a river, and your program is using it to send/receive messages. Putting data in the stream is the same as putting a message into a bottle and floating it down the river. Taking data from the stream is the same as removing a bottle from the river. However, in this case the river is a perfect delivery mechanism. Bottles always arrive in the same order that they were sent, they are never lost (or missed), and there are no gaps between the bottles.

Output streams are occasionally called a "sink," whereas input streams are sometimes called a "source."

Streams are a useful abstraction because they hide the underlying transport mechanism used between components, and they hide the details of the component at the other end. The stream could be a network socket, a pipe to another program, a terminal, a keyboard, a disk, a printer, a modem, a billboard, or a buffer in memory. Programs written to use streams as their I/O can be easily adapted to substitute one implementation of a stream for another.

See: Java's InputStream, OutputStream, C++'s iostream, and C's I/O with streams (stdio.h).