Skip to content

Introduction to Pointers

Pointer Declaration and Initialization

A pointer is a variable that stores the memory address of another variable. Pointers are powerful features of C that provide direct memory access and efficient memory management.

Declaration

Pointers are declared using the asterisk (*) symbol:

int *ptr; // Declaration of an integer pointer
float *fptr;     // Declaration of a float pointer
char *cptr;      // Declaration of a character pointer

Initialization

Pointers must be initialized before use. They can be initialized to contain the address of a variable:

int num = 10;
int *ptr = # // ptr now contains the address of num

Key Points

  • The data type of the pointer must match the data type of the variable it points to.
  • The address-of operator (&) returns the memory address of a variable.
  • The value-of operator (*) returns the value at the memory address stored in the pointer.

Example:

int num = 10;
int *ptr = #

printf("Value of num: %d\n", num);      // Output: Value of num: 10
printf("Address of num: %p\n", &num);  // Output: Address of num: (memory address)
printf("Value of ptr: %p\n", ptr);      // Output: Value of ptr: (same memory address)
printf("Value at ptr: %d\n", *ptr);     // Output: Value at ptr: 10

Pointers and Addresses

Memory Representation

Every variable in C is stored at a specific memory location, which has a unique address.

Example:

int x = 10;
int y = 20;
int *ptr = &x;

printf("Address of x: %p\n", &x);
printf("Address of y: %p\n", &y);
printf("Address of ptr: %p\n", &ptr);
printf("Value stored at ptr: %p\n", ptr);
printf("Value at the address stored in ptr: %d\n", *ptr);

Pointer Arithmetic

You can perform arithmetic operations on pointers:

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

printf("%d\n", *ptr);       // Output: 10
printf("%d\n", *(ptr + 1));  // Output: 20
printf("%d\n", *(ptr + 2));  // Output: 30

[insert image on memory layout of variables and pointers here]

Pointers and Arrays

In C, array names are essentially pointers to the first element of the array.

Relationship Between Arrays and Pointers

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

// These are equivalent
printf("%d\n", arr[0]);    // Output: 10
printf("%d\n", *(arr));    // Output: 10
printf("%d\n", *ptr);      // Output: 10

// These are also equivalent
printf("%d\n", arr[2]);    // Output: 30
printf("%d\n", *(arr + 2)); // Output: 30
printf("%d\n", *(ptr + 2)); // Output: 30

Array Traversal Using Pointers

int arr[] = {10, 20, 30, 40, 50};
int *ptr = arr;
int size = sizeof(arr) / sizeof(arr[0]);

for (int i = 0; i < size; i++) {
    printf("%d ", *ptr);
    ptr++; // Move to the next element
}

// Output: 10 20 30 40 50

Important Notes

  • When you increment a pointer, it moves to the next memory location appropriate for its data type.
  • For integer pointers on most systems, ptr++ increases the address by 4 bytes (size of an int).
  • For character pointers, ptr++ increases the address by 1 byte.

[insert image on array and pointer relationship in memory here]

Basic Concept of Dynamic Memory Allocation

Dynamic memory allocation allows you to allocate memory at runtime rather than compile time. This is useful when you don't know the exact memory requirements until the program is running.

Why Use Dynamic Memory Allocation?

  • Allocate memory as needed during runtime
  • Allocate exactly the amount of memory required
  • Free memory when no longer needed
  • Create data structures like linked lists, trees, etc.

Memory Allocation Functions

The standard library provides several functions for dynamic memory allocation:

  1. malloc() - Allocates a block of memory of a specified size
  2. calloc() - Allocates and initializes a block of memory to zero
  3. realloc() - Resizes a previously allocated block of memory
  4. free() - Deallocates memory previously allocated

malloc() Function

Syntax

void *malloc(size_t size);

Description

  • Allocates a block of memory of size bytes
  • Returns a pointer to the beginning of the allocated memory block
  • If allocation fails, returns NULL
  • The memory contains garbage values (not initialized)

Example

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

int main() {
    int *ptr;
    int n;

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

    ptr = (int *)malloc(n * sizeof(int));

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

    for (int i =  ;0; i < n; i++) {
        ptr[i] = i + 1;
    }

    printf("You entered: ");
    for (int i = 0; i < n; i++) {
        printf("%d ", ptr[i]);
    }

    free(ptr); // Free allocated memory
    return 0;
}

calloc() Function

Syntax

void *calloc(size_t num, size_t size);

Description

  • Allocates memory for an array of num elements, each of size bytes
  • Initializes all bits to zero
  • Returns a pointer to the beginning of the allocated memory block
  • If allocation fails, returns NULL

Example

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

int main() {
    int *ptr;
    int n;

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

    ptr = (int *)calloc(n, sizeof(int));

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

    printf("Values after calloc (initialized to zero):\n");
    for (int i = 0; i < 10; i++) {
        printf("%d ", ptr[i]);
    }

    free(ptr); // Free allocated memory
    return 0;
}

Difference Between malloc() and calloc()

Feature malloc() calloc()
Syntax malloc(size) calloc(num_elements, size_of_each)
Initialization Contains garbage values Initialized to zero
Parameters One parameter (size) Two parameters (number of elements, size of each)
Speed Faster Slightly slower (initializes memory)

Introduction to Functions

What is a Function?

A function is a self-contained block of statements that performs a specific task. Functions help in modular programming, code reusability, and better organization.

Advantages of Functions

  • Code reusability
  • Modularity
  • Easier debugging and maintenance
  • Better readability and organization
  • Reduced redundancy

Function Declaration and Definition

Function Declaration (Prototype)

A function declaration tells the compiler about the function name, return type, and parameters without providing the actual implementation.

Syntax:

return_type function_name(parameter_list);

Example:

int add(int a, int b);  // Function declaration without definition
float calculateSum(float arr[], int size);  // Another example

Function Definition

A function definition provides the actual implementation of the function.

Syntax:

return_type function_name(parameter_list) {
    // Function body
    return return_value;
}

Example:

int add(int a, int b) {
    int sum = a + b;
    return sum;
}

// Alternative implementation
int add(int a, int b) {
    return a + b;
}

Return Types of Function

The return type specifies the type of value that the function will return to the calling function.

Basic Return Types

  1. int - Returns an integer value
  2. float - Returns a floating-point value
  3. double - Returns a double-precision floating-point value
  4. char - Returns a character
  5. void - Indicates that the function does not return any value

Examples

// Returning an integer
int getSquare(int num) {
    return num * num;
}

// Returning a float
float getAverage(int arr[], int size) {
    int sum = 0;
    for (int i = 0; i < size; i++) {
        sum += arr[i];
    }
    return (float)sum / size;
}

// Void function (no return value)
void printArray(int arr[], int size) {
    for (int i = 0; i < size; i++) {
        printf("%d ", arr[i]);
    }
    printf("\n");
}

Function Arguments

Types of Function Arguments

  1. Formal Arguments: Variables declared in the function header.
  2. Actual Arguments: Values passed to the function during the function call.

Pass by Value

In pass by value, the actual value of the argument is copied to the formal parameter. Changes made to the formal parameter inside the function do not affect the actual argument.

Example:

#include <stdio.h>

void swapByValue(int a, int b) {
    int temp = a;
    a = b;
    b = temp;
    printf("Inside function: a = %d, b = %d\n", a, b);
}

int main() {
    int x = 5, y = 10;
    printf("Before swap: x = %d, y = %d\n", x, y);
    swapByValue(x, y);
    printf("After swap: x = %d, y = %d\n", x, y);
    return 0;
}

// Output:
// Before swap: x = 5, y = 10
// Inside function: a = 10, b = 5
// After swap: x = 5, y = 10

Pass by Reference Using Pointers

In pass by reference, the address of the actual argument is passed to the formal parameter, which is a pointer. Changes made to the value at that address inside the function affect the actual argument.

Example:

#include <stdio.h>

void swapByReference(int *a, int *b) {
    int temp = *a;
    *a = *b;
    *b = temp;
    printf("Inside function: a = %d, b = %d\n", *a, *b);
}

int main() {
    int x = 5, y = 10;
    printf("Before swap: x = %d, y = %d\n", x, y);
    swapByReference(&x, &y);
    printf("After swap: x = %d, y = %d\n", x, y);
    return 0;
}

// Output:
// Before swap: x = 5, y = 10
// Inside function: a = 10, b = 5
// After swap: x = 10, y = 5

Function Calling – Call by Value vs Call by Reference

Call by Value

  • The actual value is passed to the function
  • Changes made to formal parameters do not affect actual arguments
  • Original values remain safe
  • Implemented in C by default

Example:

#include <stdio.h>

void modifyValue(int num) {
    num = num * 2;
    printf("Inside function: num = %d\n", num);
}

int main() {
    int value = 10;
    printf("Before function call: value = %d\n", value);
    modifyValue(value);
    printf("After function call: value = %d\n", value);
    return 0;
}

// Output:
// Before function call: value = 10
// Inside function: num = 20
// After function call: value = 10

Call by Reference

  • The address of the actual argument is passed
  • Changes made to formal parameters affect actual arguments
  • Used to modify original values or to avoid copying large data
  • Implemented using pointers in C

Example:

#include <stdio.h>

void modifyValueByReference(int *num) {
    *num = *num * 2;
    printf("Inside function: num = %d\n", *num);
}

int main() {
    int value = 10;
    printf("Before function call: value = %d\n", value);
    modifyValueByReference(&value);
    printf("After function call: value = %d\n", value);
    return 0;
}

// Output:
// Before function call: value = 10
// Inside function: num = 20
// After function call: value = 20

Passing an Array as Argument to a Function

Arrays are always passed to functions by reference in C. When we pass an array, we're actually passing a pointer to the first element of the array.

Methods to Pass Arrays to Functions

  1. Pointer notation
  2. Array notation
  3. Size of array as a separate parameter

Example: Pointer Notation

#include <stdio.h>

void printArray(int *arr, int size) {
    for (int i = 0; i < size; i++) {
        printf("%d ", arr[i]);
    }
    printf("\n");
}

int main() {
    int arr[] = {1, 2, 3, 4, 5};
    int size = sizeof(arr) / sizeof(arr[0]);

    printArray(arr, size);
    return 0;
}

Example: Array Notation

#include <stdio.h>

void printArrayArr(int arr[], int size) {
    for (int i = 0; i < size; i++) {
        printf("%d ", arr[i]);
    }
    printf("\n");
}

int main() {
    int arr[] = {1, 2, 3, 4, 5};
    int size = sizeof(arr) / sizeof(arr[0]);

    printArrayArr(arr, size);
    return 0;
}

Modifying Array Elements in a Function

#include <stdio.h>

void modifyArray(int *arr, int size) {
    for (int i = 0; i < size; i++) {
        arr[i] = arr[i] * 2;
    }
}

int main() {
    int arr[] = {1, 2, 3, 4, 5};
    int size = sizeof(arr) / sizeof(arr[0]);

    printf("Before modification: ");
    for (int i = 0; i < size; i++) {
        printf("%d ", arr[i]);
    }
    printf("\n");

    modifyArray(arr, size);

    printf("After modification: ");
    for (int i = 0; i < size; i++) {
        printf("%d ", arr[i]);
    }
    printf("\n");

    return 0;
}

// Output:
// Before modification: 1 2 3 4 5
// After modification: 2 4 6 8 10

Basic Concept of Recursion

What is Recursion?

Recursion is a technique in which a function calls itself directly or indirectly in order to solve a problem. The function that calls itself is called a recursive function.

Two Main Components of Recursion

  1. Base Case: The condition that stops the recursion. Without a base case, recursion would continue indefinitely, leading to a stack overflow.
  2. Recursive Case: The part where the function calls itself with a modified argument, moving toward the base case.

Simple Example: Factorial Calculation

#include <stdio.h>

// Recursive function to calculate factorial
int factorial(int n) {
    // Base case
    if (n == 0 || n == 1) {
        return 1;
    }
    // Recursive case
    else {
        return n * factorial(n - 1);
    }
}

int main() {
    int num = 5;
    printf("Factorial of %d is %d\n", num, factorial(num));
    return 0;
}

// Output: Factorial of 5 is 120

Recursion vs Iteration

Factor Recursion Iteration
Implementation Uses function calls Uses loops (for, while)
Time Complexity Can be less efficient due to function call overhead Generally more efficient
Space Complexity Higher (uses call stack) Lower
Code Readability Often simpler for complex problems Can become complex for some problems
Risk of Errors Stack overflow if not properly handled Limited by loop bounds

Another Example: Fibonacci Sequence

#include <stdio.h>

// Recursive function to calculate Fibonacci
int fibonacci(int n) {
    // Base cases
    if (n == 0) {
        return 0;
    }
    else if (n == 1) {
        return 1;
    }
    // Recursive case
    else {
        return fibonacci(n - 1) + fibonacci(n - 2);
    }
}

int main() {
    int terms = 10;
    printf("Fibonacci sequence up to %d terms:\n", terms);

    for (int i = 0; i < terms; i++) {
        printf("%d ", fibonacci(i));
    }
    printf("\n");

    return 0;
}

// Output: Fibonacci sequence up to 10 terms:
// 0 1 1 2 3 5 8 13 21 34

Tail Recursion

A special form of recursion where the recursive call is the last operation in the function. Some compilers can optimize tail-recursive functions to avoid stack growth.

Example:

#include <stdio.h>

// Tail-recursive function for factorial
int factorialTail(int n, int accumulator) {
    if (n == 0 || n == 1) {
        return accumulator;
    }
    else {
        return factorialTail(n - 1, n * accumulator);
    }
}

// Wrapper function
int factorial(int n) {
    return factorialTail(n, 1);
}

int main() {
    int num = 5;
    printf("Factorial of %d is %d\n", num, factorial(num));
    return 0;
}

[insert image on recursion call stack visualization here]