5
\$\begingroup\$

Below is the code for a str_split() function; this function is not present in standard C library.

Syntax: char **str_split(const char *str, const char *delim, long max_splits);

Full documentation is in comments in the header file.

Code is below:


str_split.c


#include "str_split.h"

#include <stdio.h>
#include <stdlib.h>
#include <string.h>

static void free_all_allocated_memory(char **strings_array, long n);
static char **transform_str_to_string_array(const char *str);

/* The description of funtions is in the header file "str_split.h" */

char **str_split(const char *str, const char *delim, long max_splits)
{

    char **output_strings_array = NULL;
    char *temp = NULL;
    char *prev_temp = NULL;
    long num_tokens = 0;
    size_t delim_len = 0;
    size_t len = 0;
    long i = 0;

    if ((!str) || (!*str))
        return NULL;

    if ((!delim) || (!*delim))
        return transform_str_to_string_array(str);

    if (max_splits == 0)
        return transform_str_to_string_array(str);

    // handle special case where delim does not occur in str
    if (strstr(str, delim) == NULL)
        return transform_str_to_string_array(str);

    delim_len = strlen(delim);

    temp = (char *)(str);
    prev_temp = (char *)(str);

    while (1) {

        temp = strstr(temp, delim);

        num_tokens = num_tokens + 1;

        if (!temp)
            break;

        temp = temp + delim_len;
        prev_temp = temp;

    } // end of while loop

    if ((max_splits > 0) && (max_splits < num_tokens))
        num_tokens = max_splits + 1;

    // allocate 1 extra character pointer to terminate output_strings_array with
    // a NULL pointer.
    output_strings_array = calloc((size_t)(num_tokens) + 1, (sizeof(*output_strings_array)));
    if (!output_strings_array)
        return NULL;

    temp = (char *)(str);
    prev_temp = (char *)(str);
    i = 0;

    while (1) {

        temp = strstr(temp, delim);

        len = (size_t)(temp - prev_temp);

        // allocate 1 extra byte for null terminator
        output_strings_array[i] = malloc(len + 1);
        if (!output_strings_array[i]) {
            free_all_allocated_memory(output_strings_array, i);
            return NULL;
        }

        memmove(output_strings_array[i], prev_temp, len);
        (output_strings_array[i])[len] = 0;
        i = i + 1;

        temp = temp + delim_len;
        prev_temp = temp;

        if ((num_tokens - i) == 1) { // last token

            len = (size_t)(str + strlen(str) - prev_temp);

            // allocate 1 extra byte for null terminator
            output_strings_array[i] = malloc(len + 1);
            if (!output_strings_array[i]) {
                free_all_allocated_memory(output_strings_array, i);
                return NULL;
            }

            memmove(output_strings_array[i], prev_temp, len);
            (output_strings_array[i])[len] = 0;
            i = i + 1;

            break;

        } // end of if ((num_tokens - i) == 1)

    } // end of while loop

    output_strings_array[i] = 0;

    return output_strings_array;

} // end of str_split

/*
 * static char **transform_str_to_string_array(const char *str):
 *
 * Function transform_str_to_string_array() basically allocates a pointer to pointer
 * to character (means a pointer to an array of strings/elements). This array of
 * strings have two elements - the first element is a pointer to a copy of 'str'
 * and the second element is a NULL pointer/string/element.
 *
 * This is a static function and this function should not be called from outside
 * this file.
 */
static char **transform_str_to_string_array(const char *str)
{

    char **output_strings_array = NULL;
    size_t num_tokens = 1;
    size_t len = strlen(str);

    // allocate 1 extra character pointer to terminate output_strings_array with
    // a NULL pointer.
    output_strings_array = calloc(num_tokens + 1, (sizeof(*output_strings_array)));
    if (!output_strings_array)
        return NULL;

    // allocate 1 extra byte for null terminator
    output_strings_array[0] = malloc(len + 1);
    if (!output_strings_array[0]) {
        free(output_strings_array);
        return NULL;
    }

    memmove(output_strings_array[0], str, len);
    (output_strings_array[0])[len] = 0;

    output_strings_array[num_tokens] = 0;

    return output_strings_array;

} // end of transform_str_to_string_array

/*
 * static void free_all_allocated_memory(char **strings_array, long n):
 *
 * Function free_all_allocated_memory() frees all elements of the array of strings
 * that is passed to this function. It also frees the pointer to the array of
 * strings ('strings_array').
 *
 * This is a static function and this function should not be called from outside
 * this file.
 */
static void free_all_allocated_memory(char **strings_array, long n)
{

    long i = 0;

    if (!strings_array)
        return;

    for (i = 0; i < n; i = i + 1) {
        free(strings_array[i]);
    }

    free(strings_array);

} // end of free_all_allocated_memory

void print_strings_array(char **strings_array)
{

    long i = 0;

    printf("Tokens are printed below (within single quotes):\n\n");
    printf("---- Start of Tokens ----\n");

    if (!strings_array) {
        printf("---- End of Tokens ----\n\n");
        return;
    }

    while (strings_array[i]) {
        printf("'%s'\n", strings_array[i]);
        i = i + 1;
    }

    printf("---- End of Tokens ----\n\n");

} // end of print_strings_array

void free_strings_array(char **strings_array)
{

    long i = 0;

    if (!strings_array)
        return;

    while (strings_array[i]) {
        free(strings_array[i]);
        i = i + 1;
    }

    free(strings_array);

} // end of free_strings_array

long get_number_of_strings_in_strings_array(char **strings_array)
{

    long i = 0;

    if (!strings_array)
        return 0;

    while (strings_array[i]) {
        i = i + 1;
    }

    return i;

} // end of get_number_of_strings_in_strings_array


str_split.h


#ifndef STR_SPLIT_H
#define STR_SPLIT_H

/*
 * char **str_split(const char *str, const char *delim, long max_splits):
 *
 * Function str_split() splits a string ('str') into tokens. It uses the 'delim'
 * string to split 'str' into tokens. If a 'delim' is found at position "i", then
 * the token ends at position "i - 1".
 *
 * If there are "n" 'delim' in 'str' then "n + 1" tokens are generated/returned.
 * However, some or all of these tokens may be empty strings. For example, if
 * 'str' contains only a single 'delim' then two empty tokens are generated.
 *
 * The reason that empty tokens are returned is that some users may want empty
 * tokens. One use case is that, if they are splitting records from a file to
 * insert in a database, then when an empty token is found, then they can insert
 * NULL value or 0 or empty string, etc. in that column.
 *
 * Users who don't want empty tokens can skip them by testing which token is empty
 * and which is not.
 *
 * The return value of this function is a pointer to pointer to character (means
 * a pointer to an array of strings/elements). This array of strings is terminated
 * by a NULL pointer/string/element which means that the last element in this
 * strings of array is a NULL pointer/string. So, you can loop through this array
 * of strings until you get a NULL pointer/string.
 *
 * The code of looping through this array of strings is:
 *
 *          long i = 0;
 *          while (strings_array[i]) {
 *              ..do stuff here..
 *              i = i + 1;
 *          }
 * 
 * The above can be achieved using a for loop also:
 *
 *          long i = 0;
 *          for (i = 0; strings_array[i]; i = i + 1) {
 *              ..do stuff here..
 *          }
 *
 * If you want to skip the empty tokens then the following would be the code for
 * looping through this array of strings:
 *
 *          long i = 0;
 *          while (strings_array[i]) {
 *              if (!*(strings_array[i])) {
 *                  i = i + 1;
 *                  continue;
 *              }
 *              ..do stuff here..
 *              i = i + 1;
 *          }
 * 
 * The above can be achieved using a for loop also:
 *
 *          long i = 0;
 *          for (i = 0; strings_array[i]; i = i + 1) {
 *              if (!*(strings_array[i]))
 *                  continue;
 *              ..do stuff here..
 *          }
 *
 *
 * If 'str' is NULL or empty then NULL is returned. NULL is also returned if memory
 * was not available. To find out what exactly happened, the user can check whether
 * 'str' is NULL or empty. In case, 'str' is neither NULL nor empty then it means
 * that memory was not available.
 *
 * 'max_splits' argument is used to control hwo many times 'str' should be split.
 * If 'max_splits' is less than the number of tokens that would be ideally generated
 * then the number of tokens is reduced to "max_splits + 1". If max_splits is
 * negative then it means that all tokens should be returned.
 *
 * If 'max_splits' is 0 or 'delim' is NULL or empty string or 'delim' is not found 
 * in 'str' then an array of strings is returned which will have two elements -
 * the first element will be a pointer to a copy of 'str' and the second element
 * will be a NULL pointer/string/element.
 *
 * The return value of this function is a pointer to pointer to character (means
 * a pointer to an array of strings/elements) and it had been allocated using
 * malloc, so it is user's responsibility to free this memory. The user can
 * use the function free_strings_array() to free the strings_array returned by
 * this function.
 */
char **str_split(const char *str, const char *delim, long max_splits);

/*
 * void print_strings_array(char **strings_array):
 *
 * Function print_strings_array() prints all the string elements of 'strings_array'.
 */
void print_strings_array(char **strings_array);

/*
 * void free_strings_array(char **strings_array):
 *
 * Function free_strings_array() frees all the string elements of 'strings_array'.
 * It also frees 'strings_array'.
 */
void free_strings_array(char **strings_array);

/*
 * long get_number_of_strings_in_strings_array(char **strings_array):
 *
 * Function get_number_of_strings_in_strings_array() returns the count of number
 * of elements in 'strings_array'. It is assumed that this array of strings is
 * terminated by a NULL pointer/string/element.
 *
 */
long get_number_of_strings_in_strings_array(char **strings_array);

#endif


test_str_split.c


#include "str_split.h"

#include <stdio.h>
#include <stdlib.h>
#include <string.h>

char *get_input_from_stdin_and_discard_extra_characters(char *str, long size);

#define ARRAY_SIZE 256

int main(void)
{

    char str[ARRAY_SIZE] = {0};
    char delim[ARRAY_SIZE] = {0};
    long max_splits = 0;
    char **strings_array = NULL;
    char *arg_str = NULL;
    char *arg_delim = NULL;

    while (1) {

        arg_str = str;
        arg_delim = delim;

        system("clear");
 
        printf("\nPlease input a string to split (max 256 characters) (To enter NULL"
               " string, type NULL and press ENTER): ");
        get_input_from_stdin_and_discard_extra_characters(str, ARRAY_SIZE);
        if (strcmp(str, "NULL") == 0)
            arg_str = NULL;

        printf("\nPlease input a delimiter for splitting the string (max 256 characters)"
               " (To enter NULL delimiter, type NULL and press ENTER): ");
        get_input_from_stdin_and_discard_extra_characters(delim, ARRAY_SIZE);
        if (strcmp(delim, "NULL") == 0)
            arg_delim = NULL;

        printf("\nPlease input maximum number of splits (a negative value means to"
               " split the string as many times as possible): ");
        scanf("%ld", &max_splits);
        // now clear the stdin input buffer
        get_input_from_stdin_and_discard_extra_characters(NULL, 0);

        printf("\n\n------\n");
        printf("Output\n");
        printf("------\n");
        printf("\nInput parameters: str=\"%s\", delim=\"%s\", max_splits=%ld\n\n",
               arg_str?arg_str:"(null string)", arg_delim?arg_delim:"(null delimiter)", max_splits);

        strings_array = str_split(arg_str, arg_delim, max_splits);
        if (strings_array) {
            printf("Number of tokens = %ld\n\n", get_number_of_strings_in_strings_array(strings_array));
            print_strings_array(strings_array);
            free_strings_array(strings_array);
        } else {
            if (!arg_str) {
                printf("str_split() returned NULL because 'str' passed in to function str_split() was NULL.\n\n");
            } else if (!*arg_str) {
                printf("str_split() returned NULL because 'str' passed in to function str_split() was empty.\n\n");
            } else {
                printf("str_split() returned NULL because memory was not available.\n\n");
            }
        }

        printf("\n\nPlease press ENTER to continue..");
        // now clear the stdin input buffer
        get_input_from_stdin_and_discard_extra_characters(NULL, 0);

    } // end of while(1) loop

} // end of main

/*
 * get_input_from_stdin_and_discard_extra_characters(char *str, long size):
 *
 * Function get_input_from_stdin_and_discard_extra_characters() reads at most
 * 'size - 1' characters into 'str' from stdin and then appends the null
 * character ('\0'). If 'size' is 0 then this function will discard all input
 * and return NULL. So, to discard all input, this function can be called with
 * 'str' having value NULL and 'size' having value 0.
 * In all cases, reading input stops after encountering a newline ('\n') or EOF
 * even if 'size - 1' characters have not been read. If a newline ('\n') or EOF
 * is read then it is replaced by null character ('\0'). If there are extra
 * characters in input, they are read and discarded.
 * In all cases, 'str' or NULL is returned.
 */
char *get_input_from_stdin_and_discard_extra_characters(char *str, long size)
{

    int c = 0;
    long i = 0;

    // If size is 0 then this function will discard all input and return NULL.
    // No need to check str if size is 0.
    if (size == 0) {
        // discard all input
        while ((c = getchar()) && (c != '\n') && (c != EOF));
        return NULL;
    }

    if (!str)
        return str;

    if (size < 0)
        return NULL;

    for (i = 0; i < (size - 1); i = i + 1) {

        c = getchar();

        if ((c == '\n') || (c == EOF)) {
            str[i] = 0;
            return str;
        }

        str[i] = (char)(c);

    } // end of for loop

    str[i] = 0;

    // discard rest of input
    while ((c = getchar()) && (c != '\n') && (c != EOF));

    return str;

} // end of get_input_from_stdin_and_discard_extra_characters

\$\endgroup\$
3
  • 1
    \$\begingroup\$ Does this do what strtok does? \$\endgroup\$
    – Andy Alt
    Commented Jan 20, 2022 at 23:22
  • 2
    \$\begingroup\$ @ArchStanton, Yes, it does what strtok does. IMHO, I think that str_split is better than strtok. This is because strtok modifies the string but str_split doesn't modify the string. strtok has to be called multiple times to get all the tokens but str_split has to be called only once to get all the tokens. strtok takes only single character delimiters but str_split can take delimiters that are composed of multiple characters. \$\endgroup\$
    – user245050
    Commented Jan 21, 2022 at 7:10
  • 1
    \$\begingroup\$ @Arch, it appears to act differently: strtok() uses a set of characters to divide the input string, and str_split() uses a sequence. \$\endgroup\$ Commented Jan 22, 2022 at 8:37

2 Answers 2

4
\$\begingroup\$

Make the tests self-checking

It's good that we have a test program that allows us to exercise the code (although I would drop the non-portable and unnecessary system("clear")).

I think it's better if we have an automated test that doesn't require user input. We could do this by having a script drive the existing test program, but I think it's clearer if we use the same language as the implementation (this also allows us to test long strings and ones that contain newline characters, which the test program doesn't).

The benefits of automated tests include:

  • Repeatability - while we are implementing code, we can repeatedly run one test until it gives the correct results.
  • Analysability - it's easy to run the test suite under Valgrind to ensure that we're not accessing memory we shouldn't, or leaking or double-freeing.
  • Reviewability - everyone can see exactly what testing has been done - and, crucially, whether something was missed.
  • Mantainability - we run all the tests every time we make a change, alerting us immediately if our new code broke something that used to work. Remember that the person modifying the code in future won't have the same insight into its operation as you do right now.

I would start as soon as the interface is defined, with a function that can perform a given test, and a macro that can tell us where we call it from:

static unsigned test_str_split(const char *file, int line,
                               const char *string, const char *delim, long max_splits,
                               const char **expected)
{
    char **const actual = str_split(string, delim, max_splits);
    if (!actual && !expected) {
        /* fine: expected and got NULL */
        return 0;
    }
    if (!actual) {
        goto error;
    }
    if (!expected) {
        goto error;
    }
    /* now check the elements */
    char **a = actual;
    const char **e = expected;
    while (*a && *e) {
        if (!*a || !*e || strcmp(*a++, *e++)) {
            goto error;
        }
    }

    free_strings_array(actual);
    return 0;

 error:
    printf("%s:%d: TEST FAILED: str_split(\"%s\", \"%s\", %ld)\n",
           file, line,
           string, delim, max_splits);
    printf("%s:%d: EXPECTED:\n", file, line);
    if (expected) {
        print_strings_array((char **)expected);
    } else {
        puts("(null)");
    }
    printf("%s:%d: ACTUAL:\n", file, line);
    if (actual) {
        print_strings_array(actual);
    } else {
        puts("(null)");
    }
    free_strings_array(actual);
    return 1;
}

#define TEST_STR_SPLIT(string, delim, max_splits, expected) \
    test_str_split(__FILE__, __LINE__, string, delim, max_splits, expected)

We can start using it straight away:

int main(void)
{
    unsigned failures = 0;
    failures += TEST_STR_SPLIT(NULL, NULL, 0, NULL);
    failures += TEST_STR_SPLIT(NULL, "abc", 1, NULL);
    printf("There were %u test failures\n", failures);
    return failures > 0;
}

Once we have that test case passing, it's easy to add another test, and make it pass:

int main(void)
{
    unsigned failures = 0;
    failures += TEST_STR_SPLIT(NULL, NULL, 0, NULL);
    failures += TEST_STR_SPLIT(NULL, "abc", 1, NULL);
    {
        const char *expected[] = {"abc", NULL};
        failures += TEST_STR_SPLIT("abc", NULL, 0, expected);
    }
    printf("There were %u test failures\n", failures);
    return failures > 0;
}

So you can see that after the hurdle of creating the first test, adding dozens more is easy.

Now we need to focus our testing on the edge cases - does it work when the delimiter appears at start and/or end of the string? What about two consecutive delimiters? Does the max_splits argument work as described? What about negative max_splits?

int main(void)
{
    unsigned failures = 0;
    failures += TEST_STR_SPLIT(NULL, NULL, 0, NULL);
    failures += TEST_STR_SPLIT(NULL, "abc", 1, NULL);
    failures += TEST_STR_SPLIT(NULL, "abc", -1, NULL);
    {
        const char *expected[] = {"abc", NULL};
        failures += TEST_STR_SPLIT("abc", NULL, 0, expected);
    }
    {
        const char *expected[] = {"abc", NULL};
        /* I would prefer this to return {"a", "b", "c", NULL }. */
        /* But the documentation is clear that's not what we get. */
        failures += TEST_STR_SPLIT("abc", "", 0, expected);
    }
    {
        /* I would prefer this to return {"", NULL }. */
        /* But the documentation is clear that's not what we get. */
        failures += TEST_STR_SPLIT("", ".", 0, NULL);
    }
    /* single character separator */
    {
        const char *expected[] = {"", NULL };
        failures += TEST_STR_SPLIT("", ".", -1, expected);
    }
    {
        const char *expected[] = {"=abc", NULL};
        failures += TEST_STR_SPLIT("=abc", "=", 0, expected);
    }
    {
        const char *expected[] = {"", "abc", NULL};
        failures += TEST_STR_SPLIT("=abc", "=", -1, expected);
    }
    {
        const char *expected[] = {"abc", "", NULL};
        failures += TEST_STR_SPLIT("abc=", "=", -1, expected);
    }
    {
        const char *expected[] = {"", "abc", "", NULL};
        failures += TEST_STR_SPLIT("=abc=", "=", -1, expected);
    }
    {
        const char *expected[] = {"ab", "cd", NULL};
        failures += TEST_STR_SPLIT("ab=cd", "=", -1, expected);
    }
    {
        const char *expected[] = {"ab", "cd", "ef", NULL};
        failures += TEST_STR_SPLIT("ab=cd=ef", "=", -1, expected);
    }
    {
        const char *expected[] = {"ab", "", "cd", NULL};
        failures += TEST_STR_SPLIT("ab==cd", "=", -1, expected);
    }
    {
        const char *expected[] = {"ab", "cd=ef", NULL};
        failures += TEST_STR_SPLIT("ab=cd=ef", "=", 1, expected);
    }
    {
        const char *expected[] = {"ab", "cd", "ef", NULL};
        failures += TEST_STR_SPLIT("ab=cd=ef", "=", 2, expected);
    }
    /* multi-char separator */
    {
        const char *expected[] = {"ab", "cd", NULL};
        failures += TEST_STR_SPLIT("ab==cd", "==", -1, expected);
    }
    {
        const char *expected[] = {"", "ab", "=cd", NULL};
        failures += TEST_STR_SPLIT("==ab===cd", "==", -1, expected);
    }
    {
        const char *expected[] = {"ab", "cd", NULL};
        failures += TEST_STR_SPLIT("ab==>cd", "==>", -1, expected);
    }
    {
        const char *expected[] = {"ab", "-=cd", NULL};
        failures += TEST_STR_SPLIT("ab=-=-=cd", "=-=", -1, expected);
    }
    printf("There were %u test failures\n", failures);
    return failures > 0;
}

This kind of testing is hard to add after writing the code, but I encourage you to use it in a test-first fashion for your next project.

\$\endgroup\$
12
  • \$\begingroup\$ Toby, automated test suite is definitely a good thing but it requires lot of effort which I don't want to put in. \$\endgroup\$
    – user245050
    Commented Jan 21, 2022 at 11:16
  • 1
    \$\begingroup\$ Well, if you don't want good code, then that's your choice. But I promise you, it's a lot less effort than manual testing, as I've attempted to demonstrate. (And I'm using it to give a robust review of your production code - it's saving me immense amounts of effort already.) \$\endgroup\$ Commented Jan 21, 2022 at 11:54
  • 2
    \$\begingroup\$ Good code can be written without automated tests. It just takes a lot longer and carries more risk, particularly when modifications are made in future - perhaps by other authors. \$\endgroup\$ Commented Jan 21, 2022 at 13:23
  • 1
    \$\begingroup\$ It's pretty easy to set up tests using the meson build system, and even adding valgrind checks. Although the program might not have any bugs now, later if you want to edit it, you can simply run any tests to make sure it still operates as intended. For me anyway, it's pretty common that I make what I thought would be a simple change but it breaks things. Not that you'd wanna have a meson.build file for every one of your functions, but having one for all your functions might be pretty convenient. \$\endgroup\$
    – Andy Alt
    Commented Jan 22, 2022 at 19:47
  • 1
    \$\begingroup\$ Right that good code can be written without automated tests, but from what I've seen,, the best and most trusted code (or projects) has (have) automated tests. \$\endgroup\$
    – Andy Alt
    Commented Jan 22, 2022 at 19:49
0
\$\begingroup\$

Interface usability

Naming: although C doesn't have namespaces like C++, it's helpful to users if we have a short common prefix to a set of functions. For example, we could use starr for our string array functions (we can't use strarr, as that's a reserved identfier):

starr_split()
starr_free()
starr_print()
starr_count()

I find it surprising that an empty string as delimiter doesn't split the string into 1-character chunks. That's what experience with other libraries leads me to expect, so consider changing that behaviour.

The max_split argument works like Python's str.split - that could be good for users, giving a familiar model to follow. But we should ensure that we've given full consideration to specifying as max_results, as that would allow us to use an unsigned type (i.e. size_t) for that argument, reserving the value 0 to mean "unlimited" rather than wasting half the range of a signed type.

The utility functions shouldn't be changing any of the pointed-to strings, so make them accept const char** arguments to make that clear.

The long function comments in the header file are great, but the one for str_split() is missing a key piece of information - how to release the returned memory.

The return type (char**) could cause surprise, as a user may expect to be able to overwrite any of the pointers. For example, consider this code:

char *replace_2nd_field(char *s)
{
    char **const fields = str_split(s, ",", 2);
    if (get_number_of_strings_in_strings_array(fields) < 2) {
        free_strings_array(fields);
        return NULL;
    }

    char replacement[] = "new value";
    fields[1] = replacement;
    char *result = str_join(fields);
    free_strings_array(fields);
    return result;
}

There are two memory errors here, because we leak the original value of fields[1] and we erroneously free(replacement) when we call free_strings_array().

We need to either be more explicit in the function description (perhaps recommend realloc() + strcpy - but then the user has more errors to handle) or return char *const * instead.


Memory handling

Currently, we allocate lots of small chunks of memory. We can help the allocator if we allocate a single block of memory that holds all of the results - the pointers and the string contents.

Conceptually, we'd like the memory to look like this if we str_split("Code Review", " ", 5):

{
    char *results[] = {.code, .review, NULL};
    char code[5] = "Code\0";
    char review[7] = "Review\0";
}

We can do this, if we count the total length of all the result strings as we're pre-processing:

const size_t delim_len = strlen(delim);

const char *temp = str;     /* TODO: improve the name */
size_t total_strings_len = 0;

do {
    const char *prev_temp = temp;
    temp = strstr(temp, delim);
    ++num_tokens;

    if (temp) {
        total_strings_len += (size_t)(temp - prev_temp) + 1;
        temp += delim_len;
    } else {
        total_strings_len += strlen(prev_temp) + 1;
    }
} while (temp && num_tokens + 1 <= max_splits);

if (temp) {
    /* remaining unsplit string */
    total_strings_len += strlen(temp) + 1;
    ++num_tokens;
}

Allocate the memory:

// allocate 1 extra character pointer to terminate output_strings_array with
// a NULL pointer.
size_t pointers_len = (size_t)(num_tokens + 1) * sizeof(char *);
void *memory = malloc(pointers_len + total_strings_len);
if (!memory) {
    return memory;
}
char **const output_strings_array = memory;
char *output_strings_data = memory;
output_strings_data += pointers_len;

Then use the same control flow to populate it:

temp = str;
i = 0;

num_tokens = 0;
do {
    const char *prev_temp = temp;
    temp = strstr(temp, delim);
    ++num_tokens;

    if (temp) {
        size_t len = (size_t)(temp - prev_temp);
        output_strings_array[i++] = output_strings_data;
        memcpy(output_strings_data, prev_temp, len);
        output_strings_data += len;
        *output_strings_data++ = '\0';

        temp += delim_len;
    } else {
        output_strings_array[i++] = output_strings_data;
        strcpy(output_strings_data, prev_temp);
    }
} while (temp && num_tokens + 1 <= max_splits);

if (temp) {
    output_strings_array[i++] = output_strings_data;
    strcpy(output_strings_data, temp);
}

output_strings_array[i] = NULL;

(Note: use memcpy() rather than memmove() when we know the source and destination can't overlap)

Do the same for transform_str_to_string_array():

static char **transform_str_to_string_array(const char *str)
{
    static const size_t pointers_size = 2 * sizeof (char*);
    void *memory = malloc(pointers_size + strlen(str) + 1);
    if (!memory) {
        return NULL;
    }

    char **output_strings_array = memory;
    char *out_str = memory;
    out_str += pointers_size;

    strcpy(out_str, str);
    output_strings_array[0] = out_str;
    output_strings_array[1] = NULL;

    return output_strings_array;
}

Now, we no longer have any need for free_all_allocated_memory() or transform_str_to_string_array, and our free_strings_array becomes simply:

void free_strings_array(char **strings_array)
{
    free(strings_array);
}

We could even remove this function from the interface, and simply document that users should free() the result to deallocate all the strings. And it no longer matters if users overwrite any of the pointers, thereby enabling the replace_2nd_field() implementation posited earlier.


Improved code

With behaviour slightly changed: null or empty delimiter means "split between each pair of characters".

#include <assert.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

/* The description of funtions is in the header file "str_split.h" */

char **str_split(const char *const str, const char *const delim, long max_splits)
{
    if (!str) {
        return NULL;
    }
    const size_t delim_len = delim ? strlen(delim) : 0;

    size_t total_strings_len = 0;
    size_t num_tokens = 0;
    if (delim_len == 0) {
        /* split into 1-character strings */
        num_tokens = strlen(str);
        total_strings_len = 2 * num_tokens;
    } else {
        const char *end = str;
        while (max_splits < 0 || (long)num_tokens < max_splits) {
            const char *start = end;
            end = strstr(end, delim);
            ++num_tokens;

            if (!end) {
                total_strings_len += strlen(start) + 1;
                break;
            }
            total_strings_len += (size_t)(end - start) + 1;
            end += delim_len;
        }

        if (end) {
            /* remaining unsplit string */
            total_strings_len += strlen(end) + 1;
            ++num_tokens;
        }
    }
    /* output_strings_array is terminated with a null pointer. */
    const size_t pointers_len = (num_tokens + 1) * sizeof(char *);
    void *memory = malloc(pointers_len + total_strings_len);
    if (!memory) {
        return memory;
    }
    char **const output_strings_array = memory;

    char *output_strings_data = memory;
    output_strings_data += pointers_len;
    char **out_str = output_strings_array;

    if (delim_len == 0) {
        /* split into 1-character strings */
        for (const char *p = str;  *p;  ++p) {
            *out_str++ = output_strings_data;
            *output_strings_data++ = *p;
            *output_strings_data++ = '\0';
        }
        *out_str++ = NULL;
    } else {
        long num_tokens = 0;
        const char *end = str;
        while (max_splits < 0 || (long)num_tokens < max_splits) {
            const char *start = end;
            end = strstr(end, delim);
            ++num_tokens;

            if (!end) {
                size_t len = strlen(start) + 1;
                memcpy(output_strings_data, start, len);
                output_strings_data += len;
                break;
            }
            size_t len = (size_t)(end - start);
            *out_str++ = output_strings_data;
            memcpy(output_strings_data, start, len);
            output_strings_data += len;
            *output_strings_data++ = '\0';

            end += delim_len;
        }

        if (end) {
            size_t len = strlen(end) + 1;
            memcpy(output_strings_data, end, len);
            output_strings_data += len;
        }

        *out_str = NULL;
    }

    /* We've used exactly all our allocated storage */
    assert(output_strings_data == (char*)memory + pointers_len + total_strings_len);

    return output_strings_array;

} // end of str_split

void print_strings_array(char **strings_array)
{
    printf("Tokens are printed below (within single quotes):\n\n");
    printf("---- Start of Tokens ----\n");

    if (strings_array) {
        while (*strings_array) {
            printf("'%s'\n", *strings_array++);
        }
    }

    printf("---- End of Tokens ----\n\n");

}

void free_strings_array(char **strings_array)
{
    free(strings_array);
}

size_t get_number_of_strings_in_strings_array(char **strings_array)
{
    if (!strings_array)
        return 0;

    size_t i;
    while (*strings_array++)
        ++i;

    return i;
}
\$\endgroup\$
12
  • 2
    \$\begingroup\$ I find it surprising that an empty string as delimiter doesn't split the string into 1-character chunks. That's what experience with other libraries leads me to expect, so consider changing that behaviour.: I think I am doing what strtok() does with the delimiter being NULL or empty string. But I will check again and post the results. \$\endgroup\$
    – user245050
    Commented Jan 21, 2022 at 14:12
  • 1
    \$\begingroup\$ I missed the sentence saying that negative max_splits can be used to mean "unlimited" - sorry for my error! At the risk of repeating a different point, a test suite helps reviewers see what is confirmed to work! \$\endgroup\$ Commented Jan 21, 2022 at 14:29
  • \$\begingroup\$ Using strtok() as comparison isn't helpful, since that function takes a set of characters, any of which act as delimiter. Passing an empty set makes no sense. \$\endgroup\$ Commented Jan 22, 2022 at 8:49
  • \$\begingroup\$ I actually thought about memcpy() and memmove() when I was writing the code. I decided to use memmove() all the time so that I don't have to figure out whether source and destination are overlapping or not. \$\endgroup\$
    – user245050
    Commented Jan 23, 2022 at 9:00
  • \$\begingroup\$ I checked Python's string split function and if you pass in an empty delimiter, it gives an error: Traceback (most recent call last): File "./prog.py", line 3, in <module> ValueError: empty separator. So, can you tell me in which language did you find this: null or empty delimiter means "split between each pair of characters".? \$\endgroup\$
    – user245050
    Commented Jan 23, 2022 at 9:05

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.