CSE101 Unit 5 Notes

Pointers and Dynamic Memory Allocation

This unit explores the powerful features of pointers in the C programming language and how they facilitate dynamic memory management. Pointers are a fundamental aspect of C, providing efficiency and flexibility in handling memory and data structures. Understanding pointers and dynamic memory allocation is crucial for advanced programming tasks, including implementing complex data structures like linked lists and trees.

Introduction to Pointers

Understanding Pointers

Definition

  • A pointer is a variable that stores the memory address of another variable.
  • Pointers provide a way to indirectly access and manipulate variables.

Purpose

  • Direct Memory Access: Allows manipulation of memory directly.
  • Dynamic Memory Allocation: Enables allocation of memory at runtime.
  • Efficient Array and String Manipulation: Facilitates operations on arrays and strings.
  • Complex Data Structures: Essential for creating linked lists, trees, graphs, and other dynamic data structures.
  • Function Arguments: Enables passing large structures or arrays efficiently to functions.

Analogy

  • Think of a pointer as a bookmark that tells you where to find a page in a book (the memory address).

Memory Model

  • Variables have addresses and store values.
  • Pointer Variables store addresses, which point to the values of other variables.

Pointer Declaration and Initialization

Declaration Syntax

data_type *pointer_name;
  • data_type: The type of data the pointer will point to (e.g., int, float, char).
  • *: Indicates that the variable is a pointer.
  • pointer_name: The name of the pointer variable.

Examples

int *pInt;      // Pointer to an integer
float *pFloat;  // Pointer to a float
char *pChar;    // Pointer to a char
double *pDouble;// Pointer to a double

Initialization

  • Assigning the Address of a Variable

    • Use the address-of operator (&) to get the memory address of a variable.
    • Assign the address to a pointer of the appropriate type.
  • Syntax

    pointer_name = &variable_name;
  • Examples

    int num = 10;
    int *pNum = # // 'pNum' now points to 'num'
    
    char ch = 'A';
    char *pCh = &ch;  // 'pCh' now points to 'ch'

Visual Representation

  • If num is stored at memory address 0x1000 and has a value 10, pNum points to 0x1000.

    +-----------+          +-----------+
    |   num     |          |   pNum    |
    +-----------+          +-----------+
    | Value: 10 |          | Address:  |
    +-----------+          | 0x1000    |
                           +-----------+
    Location: 0x1000       Location: 0x2000

Types of Pointers

Understanding different types of pointers is essential for safe and effective programming.

Null Pointer

Definition

  • A null pointer is a pointer that points to nothing.
  • The value of a null pointer is NULL.

Declaration

int *ptr = NULL;

Purpose

  • Initialization: Initializes pointers that do not point to valid memory addresses.
  • Error Checking: Before dereferencing a pointer, check if it is NULL to prevent undefined behavior.
  • Function Return Values: Functions can return NULL to indicate failure or absence of data.

Example

int *pData = NULL;

if (pData != NULL) {
    // Safe to use pData
} else {
    // Handle the case when pData is NULL
}

Void Pointer (Generic Pointer)

Definition

  • A void pointer (void *) is a pointer that can hold the address of any data type.
  • Also known as a generic pointer.

Syntax

void *ptr;

Usage

  • Dynamic Memory Allocation: Functions like malloc() return void *.
  • Generic Functions: Functions that need to handle different data types.

Limitations

  • Cannot be dereferenced directly.
  • Must be typecast to an appropriate pointer type before dereferencing.

Example

void *vp;
int num = 5;
vp = # // Assign address of int to void pointer

// Typecast before dereferencing
printf("Value: %d\n", *(int *)vp);

Wild Pointer

Definition

  • A wild pointer is a pointer that has not been initialized to a known address.
  • Contains a garbage value (random memory address).

Characteristics

  • Dangerous to dereference.
  • Can cause segmentation faults or unpredictable behavior.

Example

int *pWild; // Uninitialized pointer
// *pWild = 5; // Undefined behavior

Best Practice

  • Always initialize pointers either to a valid address or to NULL.

Dangling Pointer

Definition

  • A dangling pointer points to a memory location that has been freed or deallocated.
  • The memory address is no longer valid for use.

Causes

  1. Deallocation of Memory

    int *p = (int *)malloc(sizeof(int));
    *p = 10;
    free(p); // 'p' becomes a dangling pointer
  2. Returning Addresses of Local Variables

    int *getNumber() {
        int num = 5;
        return # // 'num' is local; returning its address leads to dangling pointer
    }

Risks

  • Dereferencing a dangling pointer leads to undefined behavior.

Solution

  • After freeing memory, set the pointer to NULL.

    free(p);
    p = NULL;

Far and Near Pointers (Legacy Concept)

Note

  • Relevant in DOS and 16-bit programming environments.
  • Not commonly used in modern 32-bit or 64-bit systems.

Near Pointer

  • 16-bit pointer.
  • Can access memory within the current segment (64 KB).

Far Pointer

  • 32-bit pointer (16-bit segment and 16-bit offset).
  • Can access memory outside the current segment.

Huge Pointer

  • Similar to far pointer but normalized.
  • Adjusts segment and offset to point to the entire memory space.

Pointer Expressions and Arithmetic

Pointers support arithmetic operations that allow for traversal and manipulation of array elements and memory addresses.

Pointer Operators

Address-of Operator (&)

  • Returns the memory address of a variable.

  • Syntax:

    int var = 10;
    int *ptr = &var; // '&' gets the address of 'var'

Indirection or Dereference Operator (*)

  • Accesses or modifies the value at the address pointed to by the pointer.

  • Syntax:

    int val = *ptr;   // Reads the value at 'ptr'
    *ptr = 20;        // Sets the value at 'ptr' to 20

Pointer Arithmetic

Valid Operations

  • Increment (++) and Decrement (--):

    • Moves the pointer to the next or previous element.
    • Increments by the size of the data type.
    ptr++; // Moves to the next element
    ptr--; // Moves to the previous element
  • Addition and Subtraction of Integers:

    ptr = ptr + n; // Moves forward by 'n' elements
    ptr = ptr - n; // Moves backward by 'n' elements
  • Subtraction of Pointers:

    • Calculates the number of elements between two pointers.
    int diff = ptr2 - ptr1;

Invalid Operations

  • Cannot add two pointers.
  • Cannot multiply or divide pointers.

Example

int arr[] = {10, 20, 30, 40, 50};
int *ptr = arr;      // Points to arr[0]

printf("%d\n", *ptr); // Output: 10

ptr++;                // Now points to arr[1]
printf("%d\n", *ptr); // Output: 20

ptr += 2;             // Now points to arr[3]
printf("%d\n", *ptr); // Output: 40

int distance = ptr - arr; // distance = 3

Pointer Comparison

  • Pointers can be compared using relational operators (==, !=, <, >, <=, >=).
  • Only valid if both pointers point to elements of the same array or one past the last element.
if (ptr1 == ptr2) {
    // Pointers point to the same location
}

Operations on Pointers

Pointer Assignment

  • Assigning Addresses

    int num = 10;
    int *pNum = &num;
  • Assigning Pointers

    • One pointer can be assigned to another if they are of the same type.
    int *ptr1, *ptr2;
    ptr1 = &num;
    ptr2 = ptr1;

Accessing Variables via Pointers

  • Reading a Value

    int value = *pNum; // Reads the value pointed to by 'pNum'
  • Modifying a Value

    *pNum = 20; // Changes the value at the address pointed to by 'pNum'

Array of Pointers

  • Definition

    • An array where each element is a pointer.
  • Syntax

    data_type *array_name[array_size];
  • Example

    int *ptrArray[3];
    int a = 5, b = 10, c = 15;
    
    ptrArray[0] = &a;
    ptrArray[1] = &b;
    ptrArray[2] = &c;
    
    // Accessing values
    for (int i = 0; i < 3; i++) {
        printf("%d ", *ptrArray[i]);
    }
    // Output: 5 10 15

Pointer to Pointer

  • Definition

    • A pointer that stores the address of another pointer.
  • Declaration

    data_type **ptr_to_ptr;
  • Example

    int num = 10;
    int *ptr = &num;
    int **ptr2 = &ptr;
    
    printf("Value of num: %d\n", num);          // Output: 10
    printf("Value via ptr: %d\n", *ptr);        // Output: 10
    printf("Value via ptr2: %d\n", **ptr2);     // Output: 10
  • Visualization

    • ptr points to num.
    • ptr2 points to ptr.

Passing Pointers to Functions

Passing pointers to functions allows the function to modify the original data and is essential for efficient memory usage.

Functions with Pointer Parameters

Syntax

return_type function_name(data_type *parameter_name);

Example: Swapping Two Numbers

void swap(int *a, int *b) {
    int temp = *a;
    *a = *b;
    *b = temp;
}

int main() {
    int x = 5, y = 10;
    swap(&x, &y);
    printf("x = %d, y = %d\n", x, y); // Output: x = 10, y = 5
    return 0;
}
  • By passing the addresses (pointers), the swap() function can modify the original variables x and y.

Advantages

  • Modify Original Data: Functions can change the values of variables passed by reference.
  • Efficient Memory Use: Avoids copying large amounts of data.
  • Return Multiple Values: Functions can return values via pointers.

Relationship Between Pointers and Arrays

Pointers and arrays are closely related in C, and understanding their relationship is vital for effective programming.

Pointer and One-Dimensional Array

Array Name as a Pointer

  • The name of an array acts as a constant pointer to the first element.

    int arr[5] = {1, 2, 3, 4, 5};
    int *ptr = arr; // Equivalent to int *ptr = &arr[0];

Accessing Elements via Pointers

printf("%d\n", *ptr);       // Output: 1
printf("%d\n", *(ptr + 2)); // Output: 3
  • ptr + n moves the pointer to the n-th element ahead.

Iterating Through an Array Using Pointers

for (int *p = arr; p < arr + 5; p++) {
    printf("%d ", *p);
}
// Output: 1 2 3 4 5

Pointer Arithmetic with Arrays

  • Pointer arithmetic considers the size of the data type.

    ptr++; // Increments by sizeof(data_type)
  • Example

    int *p = arr; // If arr starts at 0x1000
    p++;          // p now points to 0x1004 (assuming int is 4 bytes)

Arrays of Pointers

  • Useful for handling arrays of strings (array of character pointers).

Example: Array of Strings

char *names[] = {"Alice", "Bob", "Charlie"};

for (int i = 0; i < 3; i++) {
    printf("%s\n", names[i]);
}
  • Each element names[i] is a pointer to a string (array of characters).

Dynamic Memory Allocation

Dynamic memory allocation allows programs to obtain memory space during runtime, providing flexibility in memory management.

Concept of Dynamic Memory

  • Static Memory Allocation: Memory size is fixed at compile time (e.g., arrays with fixed sizes).
  • Dynamic Memory Allocation: Memory is allocated during program execution (runtime) from the heap.

Dynamic Memory Management Functions

Located in the <stdlib.h> header file.

  • malloc()
  • calloc()
  • realloc()
  • free()

malloc() Function

Purpose

  • Allocates a block of memory of specified size.
  • Does not initialize the memory (contains garbage values).

Syntax

void *malloc(size_t size);
  • size: Number of bytes to allocate.
  • Returns: Pointer to the allocated memory or NULL if allocation fails.

Example

int *arr = (int *)malloc(5 * sizeof(int)); // Allocates memory for 5 integers

if (arr == NULL) {
    printf("Memory allocation failed!\n");
    exit(1);
}

// Use the array
for (int i = 0; i < 5; i++) {
    arr[i] = i + 1;
}

// Free the memory
free(arr);

calloc() Function

Purpose

  • Allocates memory for an array of elements.
  • Initializes all bits to zero.

Syntax

void *calloc(size_t num_elements, size_t element_size);
  • num_elements: Number of elements to allocate.
  • element_size: Size of each element in bytes.

Example

int *arr = (int *)calloc(5, sizeof(int)); // Allocates and zeros memory for 5 integers

if (arr == NULL) {
    printf("Memory allocation failed!\n");
    exit(1);
}

// arr is initialized to zeros

realloc() Function

Purpose

  • Resizes the memory block pointed to by a pointer.
  • Can expand or shrink the allocated memory.

Syntax

void *realloc(void *ptr, size_t new_size);
  • ptr: Pointer to previously allocated memory (from malloc(), calloc(), or realloc()).
  • new_size: New size in bytes of the memory block.

Example

int *arr = (int *)malloc(5 * sizeof(int));

// Resize to hold 10 integers
int *new_arr = (int *)realloc(arr, 10 * sizeof(int));

if (new_arr == NULL) {
    printf("Memory reallocation failed!\n");
    free(arr); // Original memory should be freed if realloc fails
    exit(1);
}

arr = new_arr; // Update pointer if realloc succeeds

Note

  • If ptr is NULL, realloc() behaves like malloc().
  • If new_size is 0, realloc() behaves like free(ptr).

free() Function

Purpose

  • Deallocates memory previously allocated by malloc(), calloc(), or realloc().

Syntax

void free(void *ptr);
  • ptr: Pointer to memory to free.

Example

free(arr);
arr = NULL; // Prevents dangling pointer

Best Practices in Dynamic Memory Allocation

  • Always Check for Allocation Failure

    if (ptr == NULL) {
        // Handle error
    }
  • Avoid Memory Leaks

    • Ensure that every allocated memory block is freed.
    • Use tools like Valgrind to detect memory leaks.
  • Set Pointers to NULL After Freeing

    • Prevents dangling pointers.
    free(ptr);
    ptr = NULL;
  • Be Careful with realloc()

    • If realloc() fails, the original memory block is left untouched.
    • Assign the result of realloc() to a temporary pointer.

Types of Memory Allocation

Static Memory Allocation

  • Compile-Time Allocation

    • Memory size is determined before the program runs.

    • Examples:

      int arr[100]; // Fixed-size array
      static int count = 0; // Static variable
  • Characteristics

    • Faster access.
    • Limited flexibility.

Dynamic Memory Allocation

  • Run-Time Allocation

    • Memory size can be adjusted during program execution.
    • Uses heap memory.
  • Examples

    • Allocating memory for user-defined size:

      int n;
      printf("Enter the size of the array: ");
      scanf("%d", &n);
      
      int *arr = (int *)malloc(n * sizeof(int));
  • Characteristics

    • Greater flexibility.
    • Requires manual memory management.

Applications of Pointers and Dynamic Memory Allocation

Implementing Data Structures

Linked Lists

  • Nodes contain data and a pointer to the next node.

    struct Node {
        int data;
        struct Node *next;
    };

Trees

  • Nodes contain data and pointers to child nodes.

    struct TreeNode {
        int data;
        struct TreeNode *left;
        struct TreeNode *right;
    };

Function Arguments

  • Passing large structures or arrays efficiently.

    void processData(struct LargeStruct *data);

Memory Management

  • Allocating memory for variable-sized data structures.
  • Managing buffers for file I/O or networking.

Dynamic Arrays

  • Arrays that can grow or shrink during execution.

Common Errors and Debugging

Dereferencing Null or Uninitialized Pointers

  • Error: Leads to segmentation faults (crashes).

  • Solution: Always initialize pointers; check for NULL before dereferencing.

    if (ptr != NULL) {
        // Safe to dereference
        *ptr = value;
    }

Memory Leaks

  • Cause: Not freeing dynamically allocated memory.
  • Effect: Program consumes more memory over time, possibly exhausting system memory.
  • Solution: Ensure that all allocated memory is freed.

Dangling Pointers

  • Cause: Using pointers after the memory they point to has been freed.
  • Solution: Set pointers to NULL after freeing memory.

Incorrect Pointer Arithmetic

  • Cause: Miscalculations in pointer movements.

  • Effect: Accessing invalid memory locations.

  • Solution: Be mindful of data type sizes; use proper pointer arithmetic.

    ptr = ptr + n; // Moves n elements ahead, not n bytes

Buffer Overflows

  • Cause: Writing data beyond the bounds of allocated memory.
  • Effect: Overwrites adjacent memory, leading to undefined behavior or security vulnerabilities.
  • Solution: Ensure that writes stay within allocated bounds.

Examples and Practice

Swapping Two Variables Using Pointers

Code Example

#include <stdio.h>

void swap(int *a, int *b) {
    int temp = *a;
    *a = *b;
    *b = temp;
}

int main() {
    int x = 100, y = 200;
    printf("Before swap: x = %d, y = %d\n", x, y);

    swap(&x, &y);

    printf("After swap: x = %d, y = %d\n", x, y);
    return 0;
}

Output

Before swap: x = 100, y = 200
After swap: x = 200, y = 100

Dynamic Memory Allocation for Arrays

Code Example

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

int main() {
    int n;
    printf("Enter number of elements: ");
    scanf("%d", &n);

    // Allocate memory
    int *arr = (int *)malloc(n * sizeof(int));
    if (arr == NULL) {
        printf("Memory allocation failed!\n");
        return 1;
    }

    // Input elements
    for (int i = 0; i < n; i++) {
        printf("Enter element %d: ", i);
        scanf("%d", &arr[i]);
    }

    // Display elements
    printf("You entered: ");
    for (int i = 0; i < n; i++) {
        printf("%d ", arr[i]);
    }
    printf("\n");

    // Free memory
    free(arr);
    arr = NULL;

    return 0;
}

Sample Output

Enter number of elements: 3
Enter element 0: 10
Enter element 1: 20
Enter element 2: 30
You entered: 10 20 30

Using realloc() to Resize an Array

Code Example

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

int main() {
    int n = 2;

    // Allocate initial memory
    int *arr = (int *)malloc(n * sizeof(int));
    if (arr == NULL) {
        printf("Initial memory allocation failed!\n");
        return 1;
    }

    // Initialize elements
    arr[0] = 1;
    arr[1] = 2;

    // Resize array
    int new_size = 4;
    int *temp = (int *)realloc(arr, new_size * sizeof(int));
    if (temp == NULL) {
        printf("Memory reallocation failed!\n");
        free(arr);
        return 1;
    }
    arr = temp;

    // Initialize new elements
    arr[2] = 3;
    arr[3] = 4;

    // Display elements
    for (int i = 0; i < new_size; i++) {
        printf("%d ", arr[i]);
    }
    printf("\n");

    // Free memory
    free(arr);
    arr = NULL;

    return 0;
}

Output

1 2 3 4

Best Practices with Pointers

  • Initialize Pointers

    • Always initialize pointers to NULL or to a valid memory address.

      int *ptr = NULL;
  • Check for NULL Before Dereferencing

    • Prevents segmentation faults.

      if (ptr != NULL) {
          // Safe to dereference
      }
  • Avoid Memory Leaks

    • Ensure every malloc() or calloc() has a corresponding free().

      int *data = (int *)malloc(size);
      // Use data
      free(data);
      data = NULL; // Prevent dangling pointer
  • Set Pointers to NULL After Freeing

    • Helps to detect usage of freed memory.
  • Be Cautious with Pointer Arithmetic

    • Understand the size of data types.
  • Use Type Casting Carefully

    • When using void *, cast to the appropriate type before dereferencing.
  • Validate Function Parameters

    • When writing functions that accept pointers, validate them before use.
  • Avoid Returning Addresses of Local Variables

    • Local variables are destroyed when the function exits.
  • Use Tools for Debugging

    • Utilize tools like Valgrind to detect memory leaks and invalid memory accesses.

Post a Comment