Week 7: Pointer (2)

Using the const Qualifier with Pointers

The const qualifier is a tool in C programming that signals to the compiler that the value of a given variable should remain unchanged, embodying the principle of least privilege. This practice aids in reducing debugging efforts, avoiding unintended consequences, and enhancing the overall robustness, modifiability, and maintainability of the program. Attempts to alter a const-declared variable will lead to compiler errors, ensuring the integrity of the variable's value.

Historically, much of the early C codebase lacks the use of const due to its unavailability, and even in contemporary code, its application is less frequent than ideal. This presents a considerable opportunity for refining existing C code through re-engineering to incorporate const more effectively.

When passing data to a function via pointers, there are four principal methods, each offering different levels of access rights:

  1. A non-constant pointer to non-constant data.

  2. A constant pointer to non-constant data.

  3. A non-constant pointer to constant data.

  4. A constant pointer to constant data.

These combinations each grant distinct levels of data access and modification capabilities. The choice among them should be guided by the principle of least privilege, ensuring that a function receives only the necessary access to perform its duties, without excess. This approach helps in minimizing potential errors and enhancing code security and efficiency.

Using a non-constant pointer to non-constant data

Example: Converting a string to uppercase. A data can be modified through the dereferenced pointer, and the pointer can be modified to point to other data items.

#include <stdio.h>
#include <ctype.h> // Required for toupper()

// Function to convert a string to uppercase
void convertToUpper(char *str) {
    if (str == NULL) return; // Check for NULL pointer

    while (*str) {
        *str = toupper(*str); // Convert each character to uppercase
        str++; // Move to the next character
    }
}

int main() {
    char myString[] = "Hello, World!"; // Mutable data
    printf("Original string: %s\n", myString);

    convertToUpper(myString); // Pass mutable pointer to mutable data
    printf("Converted string: %s\n", myString);

    return 0;
}

Return value from toupper()

If an argument passed to toupper() is

  • a lowercase character, the function returns its corresponding uppercase character

  • an uppercase character or a non-alphabetic character, the function the character itself

A constant pointer to non-constant data

An example of a non-constant pointer to constant data in C involves declaring a pointer that can change to point to different addresses, but the data it points to is immutable and cannot be modified through this pointer. This is useful when you want to iterate over an array or a string for reading purposes without accidentally modifying the data.

#include <stdio.h>
#include <string.h> // For strlen()

// Function to print a string in reverse
void printReverse(const char *str) {
    if (str == NULL) return; // Check for NULL pointer

    // Find the length of the string
    int length = strlen(str);

    // Pointer to the last character of the string
    const char *ptr = str + length - 1;

    printf("Reversed string: ");
    while (length--) {
        printf("%c", *ptr); // Print the character pointed to by ptr
        ptr--; // Move the pointer backwards
    }
    printf("\n");
}

int main() {
    const char myString[] = "Hello, World!";
    printReverse(myString);

    return 0;
}

In this code:

  • The printReverse function takes a pointer to constant data (const char *str) as its parameter. This means the function promises not to modify the data pointed to by str.

  • Inside printReverse, a non-constant pointer ptr is initialized to point to the last character of the input string (excluding the null terminator). ptr can change to point to different characters within the string (hence non-constant), but it points to constant data (const char), ensuring the string data is not modified during the reversal process.

  • The function iterates backward over the string, printing each character in reverse order. The pointer ptr is decremented to move backwards through the string.

  • myString is declared as an array of constant characters, which matches the parameter type of printReverse. Even though myString is constant, you can still pass it to functions expecting a pointer to constant data, preserving the immutability contract.

Try!

If you add the following line in the while loop (before printf)

if(length==2) *ptr="M"; 

you may see

main.c: In function ‘printReverse’:
main.c:16:27: error: assignment of read-only location ‘*ptr’
   16 |         if(length==2) *ptr="M";
      |   

How to solve it?

If you try to redefine ptr as char, not const char:

char *ptr = str + length - 1;

you will get the waring

main.c: In function ‘printReverse’:
main.c:12:17: warning: initialization discards ‘const’ qualifier from pointer target type [-Wdiscarded-qualifiers]
   12 |     char *ptr = str + length - 1;
      |                 ^~~
main.c:16:27: warning: assignment to ‘char’ from ‘char *’ makes integer from pointer without a cast [-Wint-conversion]
   16 |         if(length==2) *ptr="M";
      |                           ^
Reversed string: !dlroW ,oleH

How to solve it?

sizeof Operator

The sizeof operator in C is a compile-time unary operator that returns the size, in bytes, of its operand, which can be either a variable, a literal, or a type (specified in parentheses). This size is determined based on the data type of the operand and the compiler being used, as different compilers or different target architectures may have varying sizes for the basic data types.

#include <stdio.h>

int main() {
    // Size of char
    printf("Size of char: %zu byte(s)\n", sizeof(char));

    // Size of short
    printf("Size of short: %zu byte(s)\n", sizeof(short));

    // Size of int
    printf("Size of int: %zu byte(s)\n", sizeof(int));

    // Size of long
    printf("Size of long: %zu byte(s)\n", sizeof(long));

    // Size of long long
    printf("Size of long long: %zu byte(s)\n", sizeof(long long));

    // Size of float
    printf("Size of float: %zu byte(s)\n", sizeof(float));

    // Size of double
    printf("Size of double: %zu byte(s)\n", sizeof(double));
    
    // Size of long double
    printf("Size of double: %zu byte(s)\n", sizeof(long double));

    return 0;
}

You may get

Size of char: 1 byte(s)
Size of short: 2 byte(s)
Size of int: 4 byte(s)
Size of long: 8 byte(s)
Size of long long: 8 byte(s)
Size of float: 4 byte(s)
Size of double: 8 byte(s)
Size of double: 16 byte(s)

Arrays of Pointers

An array of pointers in C programming is a collection where each element of the array is a pointer. This structure combines the concepts of arrays and pointers, leveraging the ability of pointers to store memory addresses of other variables, including other arrays or strings.

Key characteristics and uses of arrays of pointers include:

  1. Dynamic Data Structures: They are often used to create dynamic and complex data structures like an array of strings, where each pointer in the array points to the first character of a separate string. This is useful for handling a list of text items, such as names or words.

  2. Efficiency: Arrays of pointers can be more efficient in terms of memory and processing power when dealing with an array of large structures or arrays. Instead of storing the actual data items directly in the array, storing pointers to the data can save space and allow for quicker modification and access, as only the addresses are moved or changed, not the data itself.

  3. Functionality: They allow for the creation of non-contiguous data structures and can facilitate the handling of multidimensional arrays in a more flexible manner. For example, each pointer in the array can point to arrays of different sizes, enabling the construction of jagged arrays.

  4. Memory Allocation: Arrays of pointers are crucial for dynamic memory allocation scenarios, where the size of data structures isn't known at compile time and can grow or shrink at runtime. They are used to point to blocks of memory allocated on the heap.

Here's a brief example to illustrate an array of pointers:

#include <stdio.h>

int main() {
    // Array of pointers to char, each pointing to a string literal
    const char *suits[4] = {"Hearts", "Diamonds", "Clubs", "Spades"};

    // Printing the words stored in the array
    for(int i = 0; i < 4; i++) {
        printf("%s\n", suits[i]);
    }

    return 0;
}

Every pointer in the array targets the initial character of its respective string. Therefore, despite the fixed size of a char * array, it has the flexibility to reference character strings of varying lengths.

What if we use two-dimensional array?

A two-dimensional array could be used as an alternative to an array of pointers for storing strings like "Hearts", "Diamonds", "Clubs", and "Spades". In this scenario, each string would be stored in a separate row of the array. However, this approach can lead to inefficient memory usage.

The inefficiency stems from the requirement that the two-dimensional array must be declared with fixed dimensions. Specifically, the column size must be large enough to accommodate the longest string, including the null terminator character. This means that for shorter strings, the remaining space in their respective rows is wasted. For example, if the longest string is "Diamonds" with 8 characters (plus the null terminator, making 9), then each row in the array must have enough space for 9 characters, even though "Hearts", "Clubs", and "Spades" do not require that much space.

To compare the memory usage between using an array of pointers and a two-dimensional array for storing strings like "Hearts", "Diamonds", "Clubs", and "Spades", let's break down the memory requirements for both cases. We'll assume a scenario where the system uses a 4-byte pointer (common in 32-bit systems, though 64-bit systems typically use 8-byte pointers).

Array of Pointers

In the array of pointers case:

  • Each pointer consumes 4 bytes.

  • There are 4 pointers for the 4 strings, totaling 4 pointers * 4 bytes/pointer = 16 bytes.

  • The strings themselves are stored in separate memory locations. Assuming ASCII encoding (1 byte per character) and including the null terminator for each string:

    • "Hearts" = 7 characters = 7 bytes

    • "Diamonds" = 9 characters = 9 bytes

    • "Clubs" = 6 characters = 6 bytes

    • "Spades" = 7 characters = 7 bytes

  • Total string memory = 7 + 9 + 6 + 7 = 29 bytes

  • Overall total memory usage = 16 bytes (for pointers) + 29 bytes (for strings) = 45 bytes

Two-Dimensional Array

In the two-dimensional array case:

  • The array size is determined by the longest string, "Diamonds", which has 9 characters including the null terminator.

  • Each row must accommodate 9 characters, regardless of the actual string length.

  • With 4 strings, the array size is 4 rows * 9 characters/row = 36 characters.

  • Each character is 1 byte, so the total memory usage is 36 characters * 1 byte/character = 36 bytes.

Comparison

  • Array of Pointers: 45 bytes total (16 bytes for pointers + 29 bytes for actual strings).

  • Two-Dimensional Array: 36 bytes total, allocated as a contiguous block.

While the two-dimensional array seems more memory-efficient in this particular case, it's important to remember that the array of pointers approach offers greater flexibility and can be more memory-efficient when dealing with a large number of strings or significantly varying string lengths, as it avoids the over-allocation for shorter strings. The array of pointers approach also allows strings to be dynamically modified or replaced without reallocating the entire array, offering advantages in scenarios where memory efficiency and flexibility are critical.

Function Pointers

Function pointers are powerful tools that allow the dynamic invocation of functions, enabling programming techniques such as callback functions, dynamic dispatch, and event-driven programming. A function pointer holds the address of a function that can be called later in the program. This capability provides flexibility in code structure and logic, allowing functions to be passed as arguments to other functions, returned from functions, stored in arrays, or assigned to variables.

#include <stdio.h>
#include <stdbool.h>

// Function prototypes
void bubbleSort(int arr[], int size, bool (*compare)(int, int));
bool ascending(int a, int b);
bool descending(int a, int b);

// Main function
int main() {
    int arr[] = {64, 34, 25, 12, 22, 11, 90};
    int n = sizeof(arr) / sizeof(arr[0]);

    // Sort array in ascending order
    bubbleSort(arr, n, ascending);
    printf("Sorted array in ascending order:\n");
    for(int i = 0; i < n; i++)
        printf("%d ", arr[i]);
    printf("\n");

    // Sort array in descending order
    bubbleSort(arr, n, descending);
    printf("Sorted array in descending order:\n");
    for(int i = 0; i < n; i++)
        printf("%d ", arr[i]);
    printf("\n");

    return 0;
}

// Function to perform bubble sort
void bubbleSort(int arr[], int size, bool (*compare)(int, int)) {
    int temp;
    for (int i = 0; i < size-1; i++) {
        for (int j = 0; j < size-i-1; j++) {
            if (compare(arr[j], arr[j+1])) {
                temp = arr[j];
                arr[j] = arr[j+1];
                arr[j+1] = temp;
            }
        }
    }
}

// Function to compare for ascending order
bool ascending(int a, int b) {
    return a > b; // True if first element is greater than second
}

// Function to compare for descending order
bool descending(int a, int b) {
    return a < b; // True if first element is less than second
}

Using function pointers to create a menu-driven system

#include <stdio.h>

// Function prototypes
void function1(void);
void function2(void);
void function3(void);

int main() {
    // Array of pointers to functions
    void (*functionArray[])(void) = {function1, function2, function3};

    int choice;

    while (1) {
        printf("\nMenu:\n");
        printf("1. Execute Function 1\n");
        printf("2. Execute Function 2\n");
        printf("3. Execute Function 3\n");
        printf("4. Exit\n");
        printf("Enter your choice (1-4): ");
        scanf("%d", &choice);

        if (choice == 4) {
            printf("Exiting...\n");
            break;
        }

        if (choice >= 1 && choice <= 3) {
            // Call the selected function
            (*functionArray[choice - 1])();
        } else {
            printf("Invalid choice, please choose 1-4.\n");
        }
    }

    return 0;
}

void function1(void) {
    printf("Function 1 executed.\n");
}

void function2(void) {
    printf("Function 2 executed.\n");
}

void function3(void) {
    printf("Function 3 executed.\n");
}

Classwork - Week 7

Exercise - 1

Using the function pointer technique, create a menu-driven program to calculate circle circumference, circle area, or sphere volume. The program should allow the user to choose what to calculate. It should then input a radius from the user, perform the appropriate calculation, and display the result. Each function should display messages indicating which calculation was performed, the value of the radius, and the result of the calculation.

Exercise - 2

Study the maze-solving algorithm called the right-hand rule, summarize, and visualize on the answer sheet. Given that the entrance is at [0][1], what will be the exit?

1 0 1 1 1 1 1 1 1 1 1 1
1 0 0 0 1 0 0 0 0 0 0 1
1 0 1 0 0 0 1 1 1 1 0 1
1 0 1 1 1 1 1 0 0 1 0 1
1 0 0 0 0 0 0 0 1 1 0 1
1 1 1 1 1 1 1 0 1 0 0 1
1 0 0 0 0 0 1 0 1 1 1 1
1 0 1 1 1 0 1 0 0 0 0 1
1 0 1 0 0 0 1 1 1 1 0 1
1 0 0 0 1 1 0 0 0 1 0 0
1 1 1 0 1 0 0 1 0 0 0 1
1 1 1 1 1 1 1 1 1 1 0 0

Last updated