Week 6: Pointer (1)

A pointer in C is a type of variable that stores the memory address of another variable. Unlike regular variables that hold a specific value like an integer or a character, pointers hold addresses where these values are stored.

Why Do We Use Pointers?

  1. Efficiency in Handling Arrays and Strings:

    • Pointers provide a more efficient way to handle arrays and strings. Since they directly access memory locations, operations like traversing an array or string can be done more quickly.

  2. Dynamic Memory Allocation:

    • Pointers are essential for dynamic memory allocation in C. This allows programs to allocate memory while running, which is crucial for applications where the size of memory needed isn't known in advance.

  3. Building Complex Data Structures:

    • They are key in constructing complex data structures such as linked lists, trees, and graphs. These structures require dynamic memory allocation and pointers provide the flexibility to efficiently manage memory.

  4. Functionality and Flexibility in Functions:

    • Pointers enable passing large structures or arrays to functions with efficiency and ease. They also allow returning multiple values from a function through pointer arguments.

  5. Direct Memory Access:

    • They offer a way to directly read and write in memory, which can lead to highly efficient code in system-level programming.

  6. Compatibility with Low-level System Hardware:

    • Pointers are crucial in systems programming for interacting directly with memory, which is essential for hardware-level operations.

Defining a pointer in C involves specifying the type of data the pointer will reference and then using a specific syntax to declare the pointer variable. Here's a step-by-step guide on how to define a pointer in C:

Pointer Variable Definitions and Initialization

The general syntax for declaring a pointer in C is as follows:

type *pointerName;
  • type: The data type of the variable that the pointer will point to. This could be any of C's fundamental data types (e.g., int, float, char) or derived types like structures.

  • *: The asterisk (*) is the dereference operator and is used in the declaration to indicate that this is a pointer variable.

  • pointerName: The name of the pointer variable. Like any variable name, it should be descriptive and follow C naming conventions.

Here are a few examples of pointer declarations in C:

int *ptr;      // Pointer to an integer
char *cptr;    // Pointer to a character
float *fptr;   // Pointer to a float
void *vptr;    // Pointer to an object of any type (generic pointer)

A pointer is usually initialized to the address of a variable using the address-of operator (&). Here's an example:

int y = 5;
int *yPtr = &y;  // The pointer ptr now holds the address of var
  • &y gives the address of the variable y, and yPtr is initialized with this address. Now, yPtr points to y.

To use the pointer:

  • Dereferencing: To access the value at the address a pointer is holding, you use the dereference operator (*) again, but this time in an expression:

int data = *yPtr;  // Dereferencing yPtr gives the value of y, which is 5
  • Assignment: You can change the value of the variable pointed to by a pointer by dereferencing the pointer on the left-hand side of an assignment:

*yPtr = 20;  // This changes the value of y to 20

Example

The example demonstrates the pointer operators & and *. The printf conversion spec- ification %p outputs a memory location as a hexadecimal integer on most platforms. The output shows that the address of a and the value of aPtr are identical, confirming that a’s address was indeed assigned to the pointer variable aPtr. The & and * operators are complements of one another. Applying both consecutively to aPtr in either order produces the same result. The addresses in the output will vary across systems that use different processor architectures, different compilers and even different compiler settings.

#include <stdio.h>

int main()
{
    int a = 7;
    int *aPtr = &a;

    printf("Address of a is %p\nValue of aPtr is %p\n\n", &a, aPtr);
    printf("Value of a is %d\nValue of *aPtr is %d\n\n", a, *aPtr);
    printf("Showing that * and & are complements of each other\n");
    printf("&*aPtr = %p\n*&aPtr = %p\n", &*aPtr, *&aPtr);

    return 0;
}

Output will look like

Address of a is 0x7ffcf31c6d1c
Value of aPtr is 0x7ffcf31c6d1c

Value of a is 7
Value of *aPtr is 7

Showing that * and & are complements of each other
&*aPtr = 0x7ffcf31c6d1c
*&aPtr = 0x7ffcf31c6d1c

Passing Arguments to Functions by Reference

Function arguments in C can be passed in two ways: pass-by-value and pass-by-reference. Generally, arguments are passed by value, with the exception of arrays, which are passed by reference. This method allows functions to alter variables in the calling function and handle pointers to large data objects without incurring the overhead associated with copying these objects, as is the case with pass-by-value. While a return statement in a function is limited to returning a single value to the caller, passing arguments by reference provides a means for a function to effectively 'return' multiple values by modifying the variables of the caller.

Pass-by-Value

Passing by value means that the function receives a copy of the argument, not the original variable itself. Changes made to the parameter inside the function do not affect the original argument.

void modify(int val) {
    val = 10;  // Modifying the copy of the variable
}

int main() {
    int x = 5;
    
    modify(x);  // Passing the value of x
    // x is still 5, as only the copy was modified in the function
    
    x = modify(x);
    // Value of x is modified to 10
    return 0;
}

Pass-by-Reference

In C, passing by reference means passing the address of a variable (usually using pointers) to a function so that the function can directly modify the variable's value.

void modify(int *ptr) {
    *ptr = 10;  // Modifying the value at the address pointed to by ptr
}

int main() {
    int x = 5;
    modify(&x);  // Passing the address of x
    // x is now 10
    return 0;
}

Comparison between Pass-by-Value and Pass-by-Reference

  • Pass by Value: The original variable is not affected by what happens inside the function. It's a safe way to ensure that the original data cannot be accidentally modified. However, it can be less efficient for large data structures since it involves creating copies.

  • Pass by Reference: The function has direct access to and can modify the original variable. This approach is more efficient for large data structures but requires careful management to avoid unintended side-effects.

Something Important to Consider for Pass-by-Reference

  • Null Pointers: Always check if pointers are NULL before dereferencing to avoid segmentation faults.

  • Scope: Variables should have a scope beyond the function call (e.g., not local to another function) to ensure they exist throughout the function execution.

  • Const Correctness: If a function should not modify the data pointed to by the pointer, use const keyword to enforce this.

NULL Pointers

The concept of a NULL pointer is an important aspect of pointer usage in C programming. A NULL pointer is a special pointer that points to nothing or no valid memory location. It is used as a sentinel value to indicate that the pointer is not intended to point to an accessible memory location.

Why Use NULL Pointers?

  1. Safety: Prevents accidental use of uninitialized pointers, which can lead to undefined behavior or program crashes.

  2. Error Handling: Indicates that a function that returns a pointer failed to perform its intended task.

  3. End of Structures: Marks the end of a data structure, like linked lists or trees.

Initializing a NULL Pointer

In C, you can define a NULL pointer by initializing a pointer variable with the NULL macro, which is defined in several standard headers like <stdio.h>, <stdlib.h>, and <stddef.h>.

#include <stdio.h>

int main() {
    int *ptr = NULL;  // A NULL pointer

    if (ptr == NULL) {
        printf("The pointer is NULL and does not point to any valid memory location.\n");
    } else {
        printf("The pointer is pointing to the value: %d\n", *ptr);
    }

    return 0;
}

Caution

  • Dereferencing a NULL Pointer: Attempting to dereference (access the value pointed by) a NULL pointer will lead to undefined behavior, often resulting in a segmentation fault or program crash.

  • Checking for NULL: Always check if a pointer is NULL before dereferencing it, especially if there is a possibility that it might not point to a valid memory location.

Example

Suppose we have a function that dynamically allocates memory and returns a pointer to it. Using a NULL pointer is a good practice to handle situations where memory allocation might fail.

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

int* allocateArray(int size) {
    int* arr = (int*) malloc(size * sizeof(int));

    if (arr == NULL) {
        printf("Memory allocation failed.\n");
        return NULL;
    }

    // Initialize array elements for the sake of this example
    for (int i = 0; i < size; ++i) {
        arr[i] = i;
    }

    return arr;
}

int main() {
    int size = 10;
    int *myArray = allocateArray(size);

    if (myArray != NULL) {
        printf("Array elements: ");
        for (int i = 0; i < size; ++i) {
            printf("%d ", myArray[i]);
        }
        printf("\n");
    } else {
        printf("Failed to allocate memory for the array.\n");
    }

    // Free the allocated memory
    free(myArray);

    return 0;
}

In the above example,

  • The malloc function is used for dynamic memory allocation. If malloc fails (e.g., due to insufficient memory), it returns NULL. The function checks if arr is NULL to determine if memory allocation was successful. If not, it prints an error message and returns NULL.

  • Checking for a NULL pointer after calling malloc (or similar functions like calloc and realloc) is crucial for robust error handling in dynamic memory allocation.

  • Always free dynamically allocated memory to prevent memory leaks.

  • The check for NULL before using the returned pointer in main ensures that the program does not attempt to dereference a NULL pointer, which would lead to undefined behavior.

Pointer Arithmetic

Pointer arithmetic in C involves operations on pointer values. Unlike regular arithmetic, pointer arithmetic is performed with respect to the data type to which the pointer points. Here's a brief summary:

Basic Concepts

  1. Type-Specific Scaling:

    • When you increment or decrement a pointer, it moves by the number of bytes of its data type. For example, an int * moves by sizeof(int) bytes with each increment.

  2. Pointer Increment and Decrement:

    • ptr++ or ++ptr: Moves the pointer to the next memory location for its type.

    • ptr-- or --ptr: Moves the pointer to the previous memory location for its type.

  3. Pointer Addition and Subtraction:

    • ptr + n: Moves the pointer forward by n elements.

    • ptr - n: Moves the pointer backward by n elements.

  4. Difference Between Two Pointers:

    • ptr2 - ptr1: Computes the number of elements between two pointers (of the same type).

Example

An example demonstrating pointer arithmetic in C. This example will use an array of integers and a pointer to traverse (ravel across or through) the array.

#include <stdio.h>

int main() {
    int arr[] = {10, 20, 30, 40, 50}; // An array of integers
    int *ptr; // Pointer to an integer

    ptr = arr; // Point to the first element of the array

    printf("Using pointer arithmetic:\n");

    // Print each element of the array using pointer arithmetic
    for(int i = 0; i < 5; i++) {
        printf("Element %d: %d\n", i, *(ptr + i));
    }

    // Example of pointer increment
    ptr++; // Now ptr points to arr[1]
    printf("\nAfter ptr++: *ptr = %d\n", *ptr);

    // Example of pointer addition
    int *ptr2 = ptr + 3; // Pointing to ptr[4], which is arr[1+3]
    printf("After ptr + 3: *ptr2 = %d\n", *ptr2);

    // Example of pointer difference
    printf("Difference between ptr2 and ptr: %ld\n", ptr2 - ptr);
    
    printf("%p, %p\n",ptr,ptr+2);

    return 0;
}

Example of output

Using pointer arithmetic:
Element 0: 10
Element 1: 20
Element 2: 30
Element 3: 40
Element 4: 50

After ptr++: *ptr = 20
After ptr + 3: *ptr2 = 50
Difference between ptr2 and ptr: 3
0x7ffc405d9474, 0x7ffc405d947c

Reminder: Hexadecimal number system is a type of number system, that has a base value equal to 16. Hexadecimal numbers are represented by only 16 symbols. These symbols or values are 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, A, B, C, D, E and F.

Classwork - Week 6

Part 1: Pointers and Pass-by-Reference

Swapping Two Numbers

  • Write a function void swap(int *x, int *y) that swaps the values of two integers.

  • In your main function, declare two integers, initialize them, and call swap using their addresses.

  • Print the values before and after the swap to verify the function works.


Part 2: Arrays and Pointer Arithmetic

Array Sum Using Pointers

  • Create an int array and initialize it with some values.

  • Write a function int arraySum(int *arr, int size) that returns the sum of the array elements. Use pointer arithmetic to navigate the array.

  • In main, call this function and print the result.


Part 3: Null Pointers

Null Pointer Check

  • Write a function void printValue(int *ptr) that prints the value pointed to by ptr. The function should first check if ptr is not a null pointer before attempting to print.

  • In main, create an int pointer, initialize it to NULL, and then call printValue with it.

  • Afterward, assign a valid address to the pointer and call printValue again.

Last updated