A dynamic string array that's loopable without requiring a count Here's a dynamic loopable string array that doesn't require a count to be maintained at the caller level.

Definition

char **getList() {
    char **items = (char *[]) {"John", "Jane", NULL}; // dynamic/variable hard coded list

    int size = 0; 
    for (char **ptr = items; *ptr; ptr++) { // count items
        size++;
    }

    char **data = malloc(sizeof(char *) * (size + 1));
    if (data == NULL) goto err;

    for (int i = 0; *items; items++, i++) { // copy
        data[i] = malloc(sizeof(char) * strlen(*items) + 1);
        if (data[i] == NULL) goto err;
        strcpy(data[i], *items);
    }

    data[size] = NULL; // last item
    return data;

    err:
    fprintf(stderr, "Allocation failed.\n");
    return NULL;
}


Caller

char **data = getList();
for (char **ptr = data; *ptr; ptr++) {
    puts(*ptr);
}
free(data);


Please review if this code can be improved upon for any redundancy or optimizations.
review if this code can be improved upon for any redundancy or optimizations.


Micro optimization

String duplication does not take advantage of the pre-calculation of the length.

    data[i] = malloc(sizeof(char) * strlen(*items) + 1);
    if (data[i] == NULL) goto err;
    strcpy(data[i], *items);


For longer strings, code may (or may not) improve with the below.  This tended to improve things.  Less so today (usually a tie) unless in embedded platforms.

    size_t sz = strlen(*items) + 1;
    data[i] = malloc(sz);  // dealing with `char` strings, no need for sizeof(char)
    if (data[i] == NULL) goto err;
    memcpy(data[i], *items, sz);


Note: sizeof(char) * strlen(*items) + 1 is logically wrong.  Should have been sizeof(char) * (strlen(*items) + 1), yet since sizeof(char) is 1, makes no arithmetic difference.

Redundancy

No need to start at 0 and add 1: Start at 1.  Better to use size_t for sizing.  Better to size to the referenced type, than explicitly code the type.

// int size = 0; 
size_t size = 1; 
for (char **ptr = items; *ptr; ptr++) {
    size++;
}
// char **data = malloc(sizeof(char *) * (size + 1));
char **data = malloc(sizeof *data * size);


Even better, just subtract:

for (char **ptr = items; *ptr; ptr++) {
    ;
}
size_t size = ptr - items; 
char **data = malloc(sizeof *data * size);




Design flaw

if (data[i] == NULL) goto err; fails to free prior allocations.

Design

The for (int i = 0; *items; items++, i++) increments one pointer with a ++ and indexes the other.  I'd expect the same for both.

Caller flaw

free(data); leaks the individual strings. Recommend coding a freeList(char **).