Taking the recommendation to use fgets
is one thing, putting it into use the first time is quite another. You use fgets
(or POSIX getline
) because they provide a mechanism for reading an entire line of text into a buffer at once. This eliminates the pitfalls inherent in trying to use scanf
for that purpose.
While POSIX getline
will handle a line of any length for you, it dynamically allocates storage for the resulting buffer. fgets
on the other hand will read only as many characters as can be stored in the size you specify in the fgets
call (reserving space for the nul-character, as fgets
always provides a nul-terminated buffer)
This means it is up to you to check that a complete line fit into the buffer you provided for fgets
use. Essentially you want to check whether the buffer is full and the last character is not the '\n'
character. Note, you are not concerned with trimming the trailing newline here, just in checking for its presence to validate whether a complete line was read. So here you can check whether the length of buffer is your max size (minus 1
for the nul-character) and the last character is not '\n'
. If those two conditions exist, you have no way of knowing whether the entire line was read (but see the note after this example). A simple approach to the validation whether a full line was read into buf
is, e.g.
while (fgets (buf, MAXC, fp)) {
...
size_t len = strlen (buf); /* length for line validation */
/* validate whole line read into buf - exit on error */
if (len == MAXC - 1 && buf[len - 1] != '\n') {
fprintf (stderr, "error: line %d too long.\n", row + 1);
return 1;
}
(note: for the corner-case of a file without a POSIX eof
(end-of-file), e.g. without a '\n'
following the last line of text, there is a chance you could actually read an exact buffer full of characters and have no trailing '\n'
, but still have a complete read -- you can check for EOF
with a call to getchar()
and return the character to the buffer with putchar
if it is other than EOF
)
Now on to handling your arrays. Rather than declaring separate arrays of 20
int each, instead declare a 2D array of n
row of 20
int each. This makes handling the read and indexing much easier.
You also have the problem of having to capture the number of values you store in each row. While you can do a little indexing magic and store the number of values in each row as the first-column value, it is probably a bit easier just to have a separate array of n
values where each index corresponds to the number of values store for each row in your 2D array. For example,
int row = 0, /* row count during read */
idx[ROWS] = {0}, /* array holding col count per row */
arr[ROWS][COLS] = {{0}}; /* 2D array holding each line array */
That way, each time you add a value to one of your rows, you simply increment the corresponding value in idx
, e.g.
/* fill a value in row, then */
idx[row]++; /* update col-index for array */
With that background, you are finally ready to start filling your array. The approach is straight-forward. You will:
- use an outer loop reading a complete line using
fgets (buf, MAXC, fp)
;
- initialize inner loop variable (for
offset
, etc.);
- check that a complete line was read (as shown above);
- use an inner loop over
buf
using sscanf
to repeatedly parse a single-integer from buf
until all integers are read;
- (really 4(a.)) (you call
sscanf
on buf + offset
from the beginning), saving the number characters consumed (saved with the %n
format specifier to update offset
);
- update
offset
with the number of characters consumed, and repeat.
(note: it is up to you to protect your array bounds to make sure you do not attempt to store more integer values in each array than you have storage for, and that you do not try and store more rows than you have storage for. So on each the outer and inner loop you will add a check to limit the number of rows and columns you read to the available storage)
Your read loops implementing the steps above could look like the following:
/* constants for max rows, cols, and chars for read buf */
enum { ROWS = 4, COLS = 20, MAXC = 512 };
...
while (row < ROWS && fgets (buf, MAXC, fp)) { /* read each line */
int col = 0, /* col being filled */
nchr = 0, /* no. chars consumed by sscanf */
offset = 0, /* offset in buf for next sscaf call */
tmp = 0; /* temp var to hold sscanf conversion */
size_t len = strlen (buf); /* length for line validation */
/* validate whole line read into buf - exit on error */
if (len == MAXC - 1 && buf[len - 1] != '\n') {
fprintf (stderr, "error: line %d too long.\n", row + 1);
return 1;
}
while (col < COLS && /* read each value in line into arr */
sscanf (buf + offset, "%d%n", &tmp, &nchr) == 1) {
arr[row][col++] = tmp; /* assign tmp to array */
offset += nchr; /* update offset in buffer */
idx[row]++; /* update col-index for array */
}
row++; /* increment row for next read */
}
Putting it altogether, you could do something like the following:
#include <stdio.h>
#include <string.h>
/* constants for max rows, cols, and chars for read buf */
enum { ROWS = 4, COLS = 20, MAXC = 512 };
int main (int argc, char **argv) {
int row = 0, /* row count during read */
idx[ROWS] = {0}, /* array holding col count per row */
arr[ROWS][COLS] = {{0}}; /* 2D array holding each line array */
char buf[MAXC] = ""; /* buffer for fgets */
FILE *fp = argc > 1 ? fopen (argv[1], "r") : stdin;
if (!fp) { /* validate file open for reading */
fprintf (stderr, "error: file open failed '%s'.\n", argv[1]);
return 1;
}
while (row < ROWS && fgets (buf, MAXC, fp)) { /* read each line */
int col = 0, /* col being filled */
nchr = 0, /* no. chars consumed by sscanf */
offset = 0, /* offset in buf for next sscaf call */
tmp = 0; /* temp var to hold sscanf conversion */
size_t len = strlen (buf); /* length for line validation */
/* validate whole line read into buf - exit on error */
if (len == MAXC - 1 && buf[len - 1] != '\n') {
fprintf (stderr, "error: line %d too long.\n", row + 1);
return 1;
}
while (col < COLS && /* read each value in line into arr */
sscanf (buf + offset, "%d%n", &tmp, &nchr) == 1) {
arr[row][col++] = tmp; /* assign tmp to array */
offset += nchr; /* update offset in buffer */
idx[row]++; /* update col-index for array */
}
row++; /* increment row for next read */
}
if (fp != stdin) fclose (fp); /* close file if not stdin */
for (int i = 0; i < row; i++) { /* output the arrays read */
for (int j = 0; j < idx[i]; j++)
printf (" %3d", arr[i][j]);
putchar ('\n');
}
return 0;
}
Note: rather than using a fixed size 2D array, you can take things a step further and instead use a pointer-to-pointer-to-int (e.g. a double-pointer, int **arr;
) and dynamically allocate and reallocate pointers for rows, as required, and dynamically allocate and reallocate the storage assigned to each pointer to handle any number of integer values per-row. While it is not that much additional work, that is left as an exercise to you when you get to dynamic allocation in your studies. What you are doing with an differing number of column values per-row is creating a jagged array.
Example Input File
Using your input file for testing, e.g.:
$ cat dat/3arr.txt
3 2 1 1 1
4 3 2
1 1 4 1
Example Use/Output
Produces the following output:
$ ./bin/arr_jagged dat/3arr.txt
3 2 1 1 1
4 3 2
1 1 4 1
Look things over and let me know if you have further questions.