Search code examples
arrayscpointers

How to pass a char** to a function that expects a const array of const char pointers


As shown below, I want to pass a variable defined by char **x to a function as read only.

A reference book shows linear search source code that executes linear search to a variable defined by int *x by passing it to search function as a temporary argument defined by const int a[].

So I hit upon an idea that what if in the case of a string? Then I wrote the code below.

gcc, clang and MSVC Analysis can be found here.

// Linear Search

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

int scanf_s(const char *format ,...); // for gcc and clang environment
// int search(const char **a, int n, char *key) {       //※[1]not allowed in c lang specification⇒NG
// int search(char * const *a, int n, char *key) {      //※[2]⇒NG
// int search(const char * const *a, int n, char *key) {    //※[3]warning occured in gcc and clang!!
// int search(char const * const *a, int n, char *key) {//※[4]same as above
// int search(const char * const a[], int n, char *key) {//※[5]same as above
// int search(const char *a[], int n, char *key) {      //※[6]I thought this was ok, but warning occured in gcc and clang!!
int search(char **a, int n, char *key) {            //in conclusion, gcc,clang and MSVC only allowing this!!
    int i = 0;

    for (i = 0; i < n; i++) {
        if (strcmp(a[i], key) == 0)
            return i;
    }
    return -1;

    /* or while style!! the reference book shows this style!!
    while (1) {
        if (i == n)
            return -1;
        if (strcmp(a[i], key) == 0)
            return i;
        i++;
    }
    */
}

int main(void) {
    char **x;
    char *ky;
    int nx;
    int idx;
    int i;

    puts("Linear Search");
    printf("How many Elements??:");
    scanf_s("%d", &nx);
    
    x = malloc(sizeof(char*) * nx);
    if (x == NULL) {
        printf("Pointer to Pointer x malloc failed!!\n");
        exit(EXIT_FAILURE);
    }

    for (i = 0; i < nx; i++) {
        printf("x[%d]:", i);
        x[i] = malloc(sizeof(char) * 35);
        if (x[i] == NULL) {
            printf("Pointer Array x[] malloc failed!!\n", i);
            exit(EXIT_FAILURE);
        }
        scanf_s("%s", x[i], 35);
    }

    printf("Target Value:");
    ky = malloc(sizeof(char) * 35);
    if (x == NULL) {
        printf("target value malloc failed!!\n");
        exit(EXIT_FAILURE);
    }
    // Or
    // ky = calloc(35, sizeof(char));
    scanf_s("%s", ky, 35);

    idx = search(x, nx, ky);

    if (idx == -1)
        puts("no target value.");
    else
        printf("%s is in x[%d]!!\n", ky, idx);

    free(ky);
    for (i = 0; i < nx; i++) {
        free(x[i]);
    }
    free(x);
    system("pause");
    return 0;
}

Code with const makes gcc and clang show warnings though MSVC can compile without any warning as comments(※[1]~[6]) show.

So, how to write code under the idea that search function has to accept a variable defined by char **x as read-only argument?


Solution

  • Compilers want the strings in the array to be const char* type. Simply cast your data to const char* const* while calling the function.

    #include <stdio.h>
    #include <stdlib.h>
    #include <string.h>
    #include <limits.h>
    
    #define STRING_LENGTH 36
    
    int scanf_s(const char *format ,...);
    
    unsigned int search(const char* const stringArray[], unsigned int stringCount, const char* stringToSearch)
    {
        for(unsigned int i = 0; i < stringCount; i++)
        {
            if(strcmp(stringArray[i], stringToSearch) == 0)
            {
                return i;
            }
        }
    
        return UINT_MAX;
    }
    
    int main(void)
    {
        printf("Enter the count of strings: ");
    
        unsigned int strCount;
        scanf_s("%u", &strCount);
    
        char* *strArray = malloc(sizeof(char*) * strCount);
        if(strArray == NULL)
        {
            printf("Could not get memory for the string array!\n");
            return EXIT_FAILURE;
        }
    
        for(unsigned int i = 0; i < strCount; i++)
        {
            strArray[i] = calloc(sizeof(char), STRING_LENGTH);
            if(strArray[i] == NULL)
            {
                printf("Could not get memory for the next string!\n");
                return EXIT_FAILURE;
            }
    
            printf("Enter %uth string (%i characters at most): ", (i + 1), (STRING_LENGTH - 1));
            scanf_s("%s", strArray[i], STRING_LENGTH);
        }
    
        char* strToSearch = calloc(sizeof(char), STRING_LENGTH);
        if(strToSearch == NULL)
        {
            printf("Could not get memory for the string to be searched!\n");
            return EXIT_FAILURE;
        }
    
        printf("Enter string to be searched (%i characters at most) : ", (STRING_LENGTH - 1));
        scanf_s("%s", strToSearch, STRING_LENGTH);
    
        unsigned int result = search((const char* const*)strArray, strCount, strToSearch);
        if(result != UINT_MAX) {
            printf("String found at index: %u\n", result);
        } else {
            printf("String was not found!\n");
        }
    
        free(strToSearch);
        for(unsigned int i = 0; i < strCount; i++)
        {
            free(strArray[i]);
        }
        free(strArray);
    
        return EXIT_SUCCESS;
    }
    

    I tried this code in the link you provided. Check it here. The output is as follows:

    Enter the count of strings: 3
    Enter 1th string (35 characters at most): Goodbye
    Enter 2th string (35 characters at most): Cruel
    Enter 3th string (35 characters at most): World
    Enter string to be searched (35 characters at most) : Cruel
    String found at index: 1
    

    EDIT: The original question was about passing a char** to a function expecting const char* const a[] but in the answers and after questions, it has somehow evolved to the ultimate way of protecting the data in a function. So, I decided to add a short tutorial on character pointers.

    A Short Tutorial on Strings in C (With Pointers)

    Usage of char* to Represent A String

    We all know that pointers store just memory addresses and not data. If you declare a char* cptr;, it does not yet point to a valid memory address. So, *cptr = 'a'; is invalid and the compiler produces a warning for that.

    int main(void)
    {
        char* cptr;
        *cptr = 'a'; // gcc -> warning: ‘cptr’ is used uninitialized
        printf("*cptr: %c\n", *cptr);
        return 0;
    }
    

    If you try to run the above program, it causes to a Segmentation Fault.

    To be able to use a character pointer, you must first point it to a valid memory location with something like this: char* cptr = malloc(sizeof(char) * 6); Or you can assign the address of a valid memory location from another char* containing a char or a string.

    int main(void)
    {
        char* cptr = malloc(sizeof(char));
        *cptr = 'a';
        printf("*cptr: %c\n", *cptr);
        free(cptr);
    
        cptr = malloc(sizeof(char) * 6);
        strcpy(cptr, "Hello\0");
        printf("*cptr: %s\n", cptr);
        free(cptr);
        
        return 0;
    }
    

    The output is:

    *cptr: a
    *cptr: Hello
    

    The length of the character array is not important for char* since it always just points to the address of the first character and assumes that the string is terminated with a null character (\0) at the end of a contiguous space.

    Usage of const with char*

    After explaining the declaration and usage of char*, it is now time to explain the usage of const keyword with character pointers.

    Recall that pointers can provide us 3 different values: the addresses of themselves, the addresses they point to and the data at the addresses they point to.

    We can't change the addresses of pointers but we can change the other properties of pointers.

    In the last program, you saw that *cptr first points to a memory location containing only one character. And after that, it was set to point to another location which can store up to 6 characters (including \0 character). That usage was an example of how the address pointed by a pointer can be changed.

    If you don't want a pointer to point to another location you need to use a const keyword before the name of the pointer like this: char* const cptr

    int main(void)
    {
        char* const cptr = malloc(sizeof(char));
        *cptr = 'a';
        printf("*cptr: %c\n", *cptr);
        free(cptr);
    
        cptr = malloc(sizeof(char) * 6); // gcc -> error: assignment of read-only variable ‘cptr’
        strcpy(cptr, "Hello\0");
        printf("*cptr: %s\n", cptr);
        free(cptr);
        
        return 0;
    }
    

    In the program above, compiler does not allow the assignment of a new address to *cptr in the line cptr = malloc(sizeof(char) * 6); *cptr points to the same location throughout the program execution.

    Other than changing the addresses pointed to by pointers, you can change the data at the pointed addresses. This can be done with something like this: strcpy(cptr, "Hello\0");

    int main(void)
    {
        char* const cptr = malloc(sizeof(char));
        *cptr = 'a';
        printf("*cptr: %c\n", *cptr);
        *cptr = 'b';
        printf("*cptr: %c\n", *cptr);
    //  cptr = malloc(sizeof(char)); // gcc -> error: assignment of read-only variable ‘cptr’
        free(cptr);
    
    
        char* const sptr = malloc(sizeof(char) * 6);
        strcpy(sptr, "Hello\0");
        printf("*sptr: %s\n", sptr);
        strcpy(sptr, "World\0");
        printf("*sptr: %s\n", sptr);
    //  sptr = malloc(sizeof(char) * 6); // gcc -> error: assignment of read-only variable ‘sptr’
        free(sptr);
        
        return 0;
    }
    

    The output you will get is:

    *cptr: a
    *cptr: b
    *sptr: Hello
    *sptr: World
    

    In the program above, although you can't change the addresses pointed to by the pointers *cptr and *sptr, you can change the data at the pointed addresses.

    What if you want to protect the data at the address pointed by *cptr and not the address pointed to? In the program below, const char* cptr can be set to point to both *newCharPtr1 and *newCharPtr2 one after the other. However, *cptr cannot change the data at the addresses pointed by them.

    int main(void)
    {
        const char* cptr;
        char *newCharPtr1 = malloc(sizeof(char));
        *newCharPtr1 = 'a';
        printf("*newCharPtr1: %c\n", *newCharPtr1);
        cptr = newCharPtr1;
        printf("*cptr: %c\n", *cptr);
    
        char *newCharPtr2 = malloc(sizeof(char));
        *newCharPtr2 = 'b';
        printf("*newCharPtr2: %c\n", *newCharPtr2);
        cptr = newCharPtr2;
        printf("*cptr: %c\n", *cptr);
    
        *cptr = 'c'; // gcc -> error: assignment of read-only location ‘*cptr’
    
        free(newCharPtr1);
        free(newCharPtr2);
        
        return 0;
    }
    

    Also, in code written by others you may see const char* and char const* are used interchangeably. Those are exactly the same.

    The strange thing is that sometimes C Compilers can be fooled as in the program below.

    int main(void)
    {
        const char* sptr;
        char* newStringPtr1 = malloc(sizeof(char) * 6);
        strcpy(newStringPtr1, "Hello\0");
        printf("*newStringPtr1: %s\n", newStringPtr1);
        sptr = newStringPtr1;
        printf("*sptr: %s\n", sptr);
    
        char* newStringPtr2 = malloc(sizeof(char) * 6);
        strcpy(newStringPtr2, "World\0");
        printf("*newStringPtr2: %s\n", newStringPtr2);
        sptr = newStringPtr2;
        printf("*sptr: %s\n", sptr);
    
        // gcc -> warning: passing argument 1 of ‘strcpy’ discards ‘const’ qualifier from pointer target type
        strcpy(sptr, "Cruel\0");
        // gcc -> /usr/include/string.h:141:39: note: expected ‘char * restrict’ but argument is of type ‘const char *’
        printf("*newStringPtr2: %s\n", newStringPtr2);
    
        putchar('\n');
        const char* const sptr2 = newStringPtr2;
        printf("*newStringPtr2: %s\n", newStringPtr2);
        printf("*sptr2: %s\n", sptr2);
        // gcc -> warning: passing argument 1 of ‘strcpy’ discards ‘const’ qualifier from pointer target type
        strcpy(sptr2, "What?\0");
        // gcc -> /usr/include/string.h:141:39: note: expected ‘char * restrict’ but argument is of type ‘const char *’
        printf("*newStringPtr2: %s\n", newStringPtr2);
        printf("*sptr2: %s\n", sptr2);
    
        free(newStringPtr1);
        free(newStringPtr2);
        
        return 0;
    }
    

    The output of the above program is:

    *newStringPtr1: Hello
    *sptr: Hello
    *newStringPtr2: World
    *sptr: World
    *newStringPtr2: Cruel
    
    *newStringPtr2: Cruel
    *sptr2: Cruel
    *newStringPtr2: What?
    *sptr2: What?
    

    Although I declared *sptr to be const char* sptr and *sptr2 to be const char* const sptr2, somehow strcpy() managed to modify data at the addresses pointed by them. This is because strcpy() is using pointer arithmetic and it adds another level of indirection. There is no end to adding another level of indirection. This is a clear example of why you should TREAT WARNINGS AS ERRORS.

    To see another example of how you can fool a C Compiler, please check @dbush's answer below.

    Representation of String Arrays in C (char** arr / char* arr[])

    After reviewing the representation of strings in C, it is now time to review the representation of String Arrays in C.

    You declared a string with something like char* str. A string array is declared just appending two square brackets to it: char* str[]. If you want to use pointers, you do it as char* *str which can be read as a char* (str) to another char* (str[0]) which is the first element in a list of char* elements.

    The program below shows how you can construct a string array using pointers. Note that you can't use array notation to get memory from the heap. However, after getting memory from the heap with pointer notation, you can use it as an array. This is because arrays do not have their own addresses and they can't point to another address in the heap. You can try and see it for yourself by printing the address of an array and the address of the first element in the array.

    #define STRING_LENGTH 6
    #define STRING_COUNT 2
    
    int main(void)
    {
        char* str1 = calloc(sizeof(char), STRING_LENGTH);
        strcpy(str1, "Hello\0");
    
        char* str2 = calloc(sizeof(char), STRING_LENGTH);
        strcpy(str2, "World\0");
        
        char** strArr = calloc(sizeof(char*), STRING_COUNT);
        strArr[0] = str1;
        strArr[1] = str2;
    
        for(unsigned int i = 0; i < STRING_COUNT; i++)
        {
            printf("strArr[%u]: %s\n", i, strArr[i]);
        }
    
        return 0;
    }
    

    The output is:

    string[0]: Hello
    string[1]: World
    
    

    Usage of const with char**

    Do you remember how you can try to protect the content of a char*? By putting a const before it: const char*.

    And what does char** mean? An array of char* elements. Can you make the strings constant in that array? Yes, you can do it by putting a const before the elements: const char* *arr.

    #include <stdio.h>
    #include <stdlib.h>
    #include <string.h>
    
    #define STRING_COUNT 2
    #define STRING_LENGTH 6
    
    int main(void)
    {
        char* str1 = calloc(sizeof(char), STRING_LENGTH);
        strcpy(str1, "Hello\0");
    
        char* str2 = calloc(sizeof(char), STRING_LENGTH);
        strcpy(str2, "World\0");
    
        const char* *strArr = calloc(sizeof(char*), STRING_COUNT);
        strArr[0] = str1;
        strArr[1] = str2;
    
        for(unsigned int i = 0; i < STRING_COUNT; i++)
        {
            printf("string[%u]: %s\n", i, strArr[i]);
        }
    
        strArr[0][0] = 'W'; // gcc -> error: assignment of read-only location ‘**strArr’
    
        for(unsigned int i = 0; i < STRING_COUNT; i++)
        {
           free(strArr[i]);
        }
        free(strArr);
    
        return 0;
    }
    

    What if you want the elements in the array not to change with other char* elements? You put another const keyword before the array name and you get const char* const *arr or const char* const* arr or const char* const arr[]. Those 3 are the same things (while accessing the elements but not while getting memory from the heap, recall what I said earlier about array notation).

    #include <stdio.h>
    #include <stdlib.h>
    #include <string.h>
    
    #define STRING_COUNT 2
    #define STRING_LENGTH 6
    
    int main(void)
    {
        char* str1 = calloc(sizeof(char), STRING_LENGTH);
        strcpy(str1, "Hello\0");
    
        char* str2 = calloc(sizeof(char), STRING_LENGTH);
        strcpy(str2, "World\0");
    
        const char* const* strArr = calloc(sizeof(char*), STRING_COUNT);
        strArr[0] = str1; // gcc -> error: assignment of read-only location ‘*strArr’
        strArr[1] = str2; // gcc -> error: assignment of read-only location ‘*(strArr + 8)’
    
        for(unsigned int i = 0; i < STRING_COUNT; i++)
        {
            printf("string[%u]: %s\n", i, strArr[i]);
        }
    
        for(unsigned int i = 0; i < STRING_COUNT; i++)
        {
           free(strArr[i]);
        }
        free(strArr);
    
        return 0;
    }
    

    The above example shows that you can't use a const char* const* arr after getting the memory because the assignment of a char* to its elements is immediately forbidden.

    To be able to initialize a const char* const* arr, you must do something like the following.

    #include <stdio.h>
    #include <stdlib.h>
    #include <string.h>
    
    #define STRING_COUNT 2
    #define STRING_LENGTH 6
    
    int main(void)
    {
        char* str1 = calloc(sizeof(char), STRING_LENGTH);
        strcpy(str1, "Hello\0");
    
        char* str2 = calloc(sizeof(char), STRING_LENGTH);
        strcpy(str2, "World\0");
    
        const char* *strArrTemp = calloc(sizeof(char*), STRING_COUNT);
        strArrTemp[0] = str1;
        strArrTemp[1] = str2;
    
        const char* const* strArr = strArrTemp;
    
        for(unsigned int i = 0; i < STRING_COUNT; i++)
        {
            printf("string[%u]: %s\n", i, strArr[i]);
        }
    
        // strArr[1] = str1; // gcc -> error: assignment of read-only location ‘*(strArr + 8)’
    
        for(unsigned int i = 0; i < STRING_COUNT; i++)
        {
           free(strArr[i]);
        }
        free(strArr);
    
        return 0;
    }
    

    The output is:

    string[0]: Hello
    string[1]: World
    

    How to (Try to) Guarantee The Safety of A Pass-by-Reference Argument (char**) In A Function

    There are three different places a data can be saved in the memory: read-only memory, stack memory and heap memory. All the previous examples were using heap memory. The next and final example will show all three of them. It's a long program for an example but will make you understand the protection of an argument in a function.

    Don't focus on the usability or repetition of same things.

    #include <stdio.h>
    #include <stdlib.h>
    #include <string.h>
    #include <limits.h>
    
    #define STRING_COUNT 3
    #define STRING_LENGTH 12
    #define SEARCH_STRING "Heap\0"
    
    unsigned int search0(char* stringArray[], unsigned int stringCount, const char* const StringToSearch)
    {
    //  stringArray[0][0] = 'W'; // no warning or error
        strcpy(stringArray[0], "search0\0"); // no warning or error
    //  stringArray[0] = calloc(sizeof(char), 10); // no warning or error
    //  stringArray = malloc(sizeof(char*) * 10); // no warning or error
        printf("Inside search0(char* stringArray[], ...)\n");
        for(unsigned int i = 0; i < stringCount; i++)
        {
            if(strcmp(stringArray[i], StringToSearch) == 0)
            {
                return i;
            }
        }
    
        return UINT_MAX;
    }
    
    unsigned int search1(char* const stringArray[], unsigned int stringCount, const char* const StringToSearch)
    {
    //  stringArray[0][0] = 'W'; // no warning or error
        strcpy(stringArray[0], "search1\0"); // no warning or error
    //  stringArray[0] = calloc(sizeof(char), 10); // error
    //  stringArray = malloc(sizeof(char*) * 10); // no warning or error
    
        printf("Inside search1(char* const stringArray[], ...)\n");
        for(unsigned int i = 0; i < stringCount; i++)
        {
            if(strcmp(stringArray[i], StringToSearch) == 0)
            {
                return i;
            }
        }
    
        return UINT_MAX;
    }
    
    unsigned int search2(const char* stringArray[], unsigned int stringCount, const char* const StringToSearch)
    {
    //  stringArray[0][0] = 'W'; // error
        strcpy(stringArray[0], "search2\0"); // warning
    //  stringArray[0] = calloc(sizeof(char), 10); // no warning or error
    //  stringArray = malloc(sizeof(char*) * 10); // no warning or error
    
        printf("Inside search2(const char* stringArray[], ...)\n");
        for(unsigned int i = 0; i < stringCount; i++)
        {
            if(strcmp(stringArray[i], StringToSearch) == 0)
            {
                return i;
            }
        }
    
        return UINT_MAX;
    }
    
    unsigned int search3(const char* const stringArray[], unsigned int stringCount, const char* const StringToSearch)
    {
    //  stringArray[0][0] = 'W'; // error
        strcpy(stringArray[0], "search3\0"); // warning
    //  stringArray[0] = calloc(sizeof(char), 10); // error
    //  stringArray = malloc(sizeof(char*) * 10); // no warning or error
    
        printf("Inside search3(const char* const stringArray[], ...)\n");
        for(unsigned int i = 0; i < stringCount; i++)
        {
            if(strcmp(stringArray[i], StringToSearch) == 0)
            {
                return i;
            }
        }
    
        return UINT_MAX;
    }
    
    unsigned int search4(const char* const* stringArray, unsigned int stringCount, const char* const StringToSearch)
    {
    //  stringArray[0][0] = 'W'; // error
        strcpy(stringArray[0], "search4\0"); // warning
    //  stringArray[0] = calloc(sizeof(char), 10); // error
    //  stringArray = malloc(sizeof(char*) * 10); // no warning or error
    
        printf("Inside search4(const char* const* stringArray, ...)\n");
        for(unsigned int i = 0; i < stringCount; i++)
        {
            if(strcmp(stringArray[i], StringToSearch) == 0)
            {
                return i;
            }
        }
    
        return UINT_MAX;
    }
    
    unsigned int search5(const char* const* const stringArray[], unsigned int stringCount, const char* const StringToSearch)
    {
    //  stringArray[0][0] = 'W'; // error
        strcpy(stringArray[0], "search5\0"); // warning
    //  stringArray[0] = calloc(sizeof(char), 10); // error
    //  stringArray = malloc(sizeof(char*) * 10); // no warning or error
    
        printf("Inside search5(const char* const* const stringArray[], ...)\n");
        for(unsigned int i = 0; i < stringCount; i++)
        {
            if(strcmp(stringArray[i], StringToSearch) == 0)
            {
                return i;
            }
        }
    
        return UINT_MAX;
    }
    
    unsigned int search6(const char* const* const stringArray, unsigned int stringCount, const char* const StringToSearch)
    {
    //  stringArray[0][0] = 'W'; // error
        strcpy(stringArray[0], "search6\0"); // warning
    //  stringArray[0] = calloc(sizeof(char), 10); // error
    //  stringArray = malloc(sizeof(char*) * 10); // error
    
        printf("Inside search6(const char* const* const stringArray, ...)\n");
        for(unsigned int i = 0; i < stringCount; i++)
        {
            if(strcmp(stringArray[i], StringToSearch) == 0)
            {
                return i;
            }
        }
    
        return UINT_MAX;
    }
    
    void printStringArray(char* stringArray[], unsigned int stringCount, unsigned int stringPosition)
    {
        printf("Inside printStringArray(char* stringArray[], ...)\n");
        for(unsigned int i = 0; i < stringCount; i++)
        {
            printf("stringArray[%u]: %s\n", i, stringArray[i]);
        }
    
        if(stringPosition != UINT_MAX) {
            printf("%s found at: %u\n\n", SEARCH_STRING, stringPosition);
        } else {
            printf("%s was not found!\n", SEARCH_STRING);
        }
    }
    
    // Don't focus on the usability of the program.
    // Or repetition of similar things.
    int main(int argc, char* argv[])
    {
        char** strArray = calloc(sizeof(char*), STRING_COUNT);
        for(unsigned int i = 0; i < STRING_COUNT; i++)
        {
            strArray[i] = calloc(sizeof(char), STRING_LENGTH);
        }
    
        // They are saved in the heap
        strcpy(strArray[0], "Hello\0");
        strcpy(strArray[1], "Heap\0");
        strcpy(strArray[2], "World\0");
    
        // They are saved in main function stack
        const char strStack1[15] = "Hello\0";
        const char strStack2[15] = "Stack\0";
        const char strStack3[15] = "World\0";
        const char* const strStackArray[] = { strStack1, strStack2, strStack3 };
    
        printf("Before search function calls, the strings are like the following!\n");
        putchar('\n');
    
        printf("strArray[0]: %s\n", strArray[0]);
        printf("strArray[1]: %s\n", strArray[1]);
        printf("strArray[2]: %s\n", strArray[2]);
        putchar('\n');
    
        printf("strStack[0]: %s\n", strStackArray[0]);
        printf("strStack[1]: %s\n", strStackArray[1]);
        printf("strStack[2]: %s\n", strStackArray[2]);
        putchar('\n');
    
        // Below is the only way of protecting a string from modification.
        // However, you have to initialize the string in the declaration at compile time.
        // And it gives a Segmentation Fault at runtime if you try to modify it.
        // They are saved in the read-only memory section while loading the program.
        const char* strConstant1 = "Hello\0";
        const char* strConstant2 = "Constant\0";
        const char* strConstant3 = "World\0";
        const char* const strConstantArray[] = { strConstant1, strConstant2, strConstant3 };
    
        printf("strConstantArray[0]: %s\n", strConstantArray[0]);
        printf("strConstantArray[1]: %s\n", strConstantArray[1]);
        printf("strConstantArray[2]: %s\n", strConstantArray[2]);
        putchar('\n');
    
        printf("After search function calls, the strings are like the following!\n");
        putchar('\n');
    
        printf("Heap strings array example!\n");
        putchar('\n');
    
        unsigned int strPosition;
    
        // no casting needed
        strPosition = search0(strArray, STRING_COUNT, SEARCH_STRING);
        printStringArray(strArray, STRING_COUNT, strPosition);
    
        // warning without a cast
        strPosition = search1((char* const*)strArray, STRING_COUNT, SEARCH_STRING);
        printStringArray(strArray, STRING_COUNT, strPosition);
    
        // warning without a cast
        strPosition = search2((const char**)strArray, STRING_COUNT, SEARCH_STRING);
        printStringArray(strArray, STRING_COUNT, strPosition);
    
        // warning without a cast
        strPosition = search3((const char* const*)strArray, STRING_COUNT, SEARCH_STRING);
        printStringArray(strArray, STRING_COUNT, strPosition);
    
        // warning without a cast
        strPosition = search4((const char* const*)strArray, STRING_COUNT, SEARCH_STRING);
        printStringArray(strArray, STRING_COUNT, strPosition);
    
        // warning without a cast.
        strPosition = search5((const char* const* const*)strArray, STRING_COUNT, SEARCH_STRING);
        printStringArray(strArray, STRING_COUNT, strPosition);
    
        // warning without a cast. Also, accepts (const char* const*)
        strPosition = search6((const char* const* const)strArray, STRING_COUNT, SEARCH_STRING);
        printStringArray(strArray, STRING_COUNT, strPosition);
    
        for(unsigned int i = 0; i < STRING_COUNT; i++)
        {
            free(strArray[i]);
        }
        free(strArray);
    
        printf("Stack strings array example!\n");
        putchar('\n');
    
        // no casting needed
        strPosition = search6(strStackArray, STRING_COUNT, SEARCH_STRING);
    
        // warning at compile time without casting but it is not important for our aim here
        // However, notice that "const char* const arr[]" was casted to "char**" without a warning or error
        // This shows that if there is an explicit casting to requested type by the programmer,
        // C Compiler does not take any responsibility.
        printStringArray((char**)strStackArray, STRING_COUNT, strPosition);
        putchar('\n');
    
        printf("Constant strings array example!\n");
        putchar('\n');
    
        // No warning or error at compile time
        // But Segmentation Fault at runtime in all cases due to strcpy() call in the function bodies.
    
        // warning without casting
    //  search1((char* const*)strConstantArray, STRING_COUNT, SEARCH_STRING);
    
        // warning without casting
    //  search2((const char**)strConstantArray, STRING_COUNT, SEARCH_STRING);
    
        // no warning or error
    //  search3(strConstantArray, STRING_COUNT, SEARCH_STRING);
    
        // no warning or error
    //  search4(strConstantArray, STRING_COUNT, SEARCH_STRING);
    
        // warning without casting
    //  search5((const char* const* const*)strConstantArray, STRING_COUNT, SEARCH_STRING);
    
        // no warning or error
        search6(strConstantArray, STRING_COUNT, SEARCH_STRING);
    
        return EXIT_SUCCESS;
    }
    

    The output is:

    Before search function calls, the strings are like the following!
    
    strArray[0]: Hello
    strArray[1]: Heap
    strArray[2]: World
    
    strStack[0]: Hello
    strStack[1]: Stack
    strStack[2]: World
    
    strConstantArray[0]: Hello
    strConstantArray[1]: Constant
    strConstantArray[2]: World
    
    After search function calls, the strings are like the following!
    
    Heap strings array example!
    
    Inside search0(char* stringArray[], ...)
    Inside printStringArray(char* stringArray[], ...)
    stringArray[0]: search0
    stringArray[1]: Heap
    stringArray[2]: World
    Heap found at: 1
    
    Inside search1(char* const stringArray[], ...)
    Inside printStringArray(char* stringArray[], ...)
    stringArray[0]: search1
    stringArray[1]: Heap
    stringArray[2]: World
    Heap found at: 1
    
    Inside search2(const char* stringArray[], ...)
    Inside printStringArray(char* stringArray[], ...)
    stringArray[0]: search2
    stringArray[1]: Heap
    stringArray[2]: World
    Heap found at: 1
    
    Inside search3(const char* const stringArray[], ...)
    Inside printStringArray(char* stringArray[], ...)
    stringArray[0]: search3
    stringArray[1]: Heap
    stringArray[2]: World
    Heap found at: 1
    
    Inside search4(const char* const* stringArray, ...)
    Inside printStringArray(char* stringArray[], ...)
    stringArray[0]: search4
    stringArray[1]: Heap
    stringArray[2]: World
    Heap found at: 1
    
    Inside search5(const char* const* const stringArray[], ...)
    Inside printStringArray(char* stringArray[], ...)
    stringArray[0]: search5
    stringArray[1]: Heap
    stringArray[2]: World
    Heap found at: 1
    
    Inside search6(const char* const* const stringArray, ...)
    Inside printStringArray(char* stringArray[], ...)
    stringArray[0]: search6
    stringArray[1]: Heap
    stringArray[2]: World
    Heap found at: 1
    
    Stack strings array example!
    
    Inside search6(const char* const* const stringArray, ...)
    Inside printStringArray(char* stringArray[], ...)
    stringArray[0]: search6
    stringArray[1]: Stack
    stringArray[2]: World
    Heap was not found!
    
    Constant strings array example!
    
    Segmentation fault
    

    The above program clearly shows that no matter how you declare the function, there is still a way to modify a char** argument in the function unless its elements are in read-only memory. In this example it is done by calling strcpy() on a string in the string array.

    Then How Should You Declare Your Function That Expects And Will Not Modify A String Array?

    Let's look at search6() again.

    unsigned int search6(const char* const* const stringArray, unsigned int stringCount, const char* const StringToSearch)
    {
    //  stringArray[0][0] = 'W'; // error
        strcpy(stringArray[0], "search6\0"); // warning
    //  stringArray[0] = calloc(sizeof(char), 10); // error
    //  stringArray = malloc(sizeof(char*) * 10); // error
    
        printf("Inside search6(const char* const* const stringArray, ...)\n");
        for(unsigned int i = 0; i < stringCount; i++)
        {
            if(strcmp(stringArray[i], StringToSearch) == 0)
            {
                return i;
            }
        }
    
        return UINT_MAX;
    }
    

    Note that you can change 3 things in a char**. The contents of its elements, the addresses of its elements and the address of the array it points to. If you want to guarantee the safety of a char** argument in a function, you have to protect all of them together. And, you can do it by only accepting the array as const char* const* const arr.

    You may say: but it can't protect the contents of the elements in the array, why should I use it? If you're developing the function for your own application, it actually doesn't matter whether you use char** arr or const char* const* const arr because you know the aim of the function while developing it and can modify it anytime if there is a problem. However, if you are developing a library for others, you should use it to express the intention of the function to the user clearly for any level.

    Note: I did write something like this earlier in the answer: const char* const arr[] is the same as const char* const* const arr.

    I already knew it wasn't correct but I wanted to promote the usage of const char* const* const arr over const char* const arr[]. I shall now explain my intention clearly.

    If you are using something like const char* const arr[] in the prototype of your function, you are trying to tell the user that you won't modify the content of the array or the contents of its elements. But what about the address it points to? You probably won't modify it neither but you're not telling it.

    There may be some cases you may want to modify it. And how will you differentiate those cases? You should tell the user everything clearly. So, although THE UNDERSTANDING OF THE COMPILER of const char* const arr[] and const char* const* const arr are different, YOUR AIM is probably the same.

    And, this concludes my answer. :)