Q: What's a good data structure to use for storing lines of text? I started to use fixed-size arrays of arrays of char, but they're just too restrictive.
A: One good way of doing this is with a pointer (simulating an array) to a set of pointers (each simulating an array) of char. This data structure is sometimes called a ``ragged array,'' and looks something like this:
[FIGURE GOES HERE]
You could set up the tiny array in the figure above with these simple declarations:
char *a[4] = {"this", "is", "a", "test"};
char **p = a;
(where p is the pointer-to-pointer-to-char
and a is an intermediate array used to allocate the four 
pointers-to-char).
To really do dynamic allocation, you'd of course have to call malloc:
#include <stdlib.h>
char **p = malloc(4 * sizeof(char *));
if(p != NULL) {
	p[0] = malloc(5);
	p[1] = malloc(3);
	p[2] = malloc(2);
	p[3] = malloc(5);
	if(p[0] && p[1] && p[2] && p[3]) {
		strcpy(p[0], "this");
		strcpy(p[1], "is");
		strcpy(p[2], "a");
		strcpy(p[3], "test");
	}
}
(Some libraries have a strdup function
which would streamline the inner
malloc and strcpy calls.
It's not Standard,
but it's obviously trivial to implement something like it.)
Here is a code fragment which reads an entire file into memory, using the same kind of ragged array. This code is written in terms of the agetline function from question 7.30.
#include <stdio.h>
#include <stdlib.h>
extern char *agetline(FILE *);
FILE *ifp;
/* assume ifp is open on input file */
char **lines = NULL;
size_t nalloc = 0;
size_t nlines = 0;
char *p;
while((p = agetline(ifp)) != NULL) {
	if(nlines >= nalloc) {
		nalloc += 50;
#ifdef SAFEREALLOC
		lines = realloc(lines, nalloc * sizeof(char *));
#else
		if(lines == NULL)		/* in case pre-ANSI realloc */
			lines = malloc(nalloc * sizeof(char *));
		else	lines = realloc(lines, nalloc * sizeof(char *));
#endif
		if(lines == NULL) {
			fprintf(stderr, "out of memory");
			exit(1);
		}
	}
	lines[nlines++] = p;
}
(See the comments on reallocation strategy
in question 7.30.)
See also question 6.16.