Laboratory 7

Laboratory 7 โ€“ Arrays, Pointers and Dynamic Memory

This laboratory introduces arrays as function parameters, the relationship between arrays and pointers, Variable Length Arrays, dynamic memory allocation with malloc and calloc, and reusable array functions that will be extended in the next sorting laboratories.

arrays pointers VLA malloc calloc free struct array.h

Laboratory Overview

What is new in this laboratory.

In the previous laboratory, you used structures, header files, source files, and separate compilation to solve a calendar problem. In this laboratory, you will build another important foundation: working with arrays whose size may be known either at compile time or only at runtime.

Main idea of this lab:
array โ†’ pointer โ†’ function parameter โ†’ runtime size โ†’ dynamic allocation โ†’ struct result โ†’ reusable library function

The goal is not only to allocate memory, but also to understand where memory lives, how long it exists, who is responsible for releasing it, and how to write functions that safely process arrays.

Learning Outcomes

  • declare and use fixed-size arrays,
  • explain why arrays passed to functions behave like pointers,
  • use array size as a separate function parameter,
  • understand what a Variable Length Array is and when it should be avoided,
  • allocate and release memory using malloc, calloc, and free,
  • distinguish stack lifetime from heap lifetime,
  • avoid common memory errors such as invalid free, memory leaks, and returning local arrays,
  • write simple reusable functions for arrays of long,
  • return a structure containing statistics calculated for an array,
  • create array.h, separate implementation files, object files, and a static library libarray.a.

How to work with this laboratory

Some code is shown, some code must be completed, and some code must be written independently.

Label Meaning
Shown The code is provided because it introduces a new concept. Read it, run it, and understand it.
Complete A partial solution is provided. You must fill in the missing parts.
Implement yourself You should write the solution independently using patterns introduced earlier.
Self-check Answer the question first. Then open the hidden answer and compare.
File naming convention:
  • exXX.c โ€” final solution for an exercise; these files are evaluated.
  • demo_*.c โ€” demonstration programs provided in the laboratory; run them to understand a concept.
  • test_*.c โ€” test programs for checking your own code or the library; they are not evaluated.

Task 0 โ€” Reminder and preparation

Shown. Log in to the AGH server and prepare the working directory.

Step 1 โ€” Log in to the AGH UNIX server

ssh your_login@student.agh.edu.pl

Step 2 โ€” Go to the course directory

cd ~/I2PL

Step 3 โ€” Create the directory for Laboratory 7

mkdir lab7
cd lab7

Step 4 โ€” General rules

  • Work only in ~/I2PL/lab7.
  • Compile with warnings enabled.
  • Always check the result of malloc and calloc.
  • Every successful allocation must have a corresponding free.
  • Do not use global variables.
gcc -Wall program.c -o program

Task 1 โ€” Fixed-size arrays

Implement yourself. Start with a normal local array whose size is known at compile time.

Step 1 โ€” Create the file

nano ex20.c

Step 2 โ€” Write a program using a fixed-size array

Declare an array of five long values, print all elements, and print the total size of the array in bytes. You may use the following structure, but write the program yourself.

#include <stdio.h>

#define SIZE 5

int main(void) {
    long tab[SIZE] = { 12, -4, 7, 0, 25 };

    /* TODO: print all elements */
    /* TODO: print sizeof(tab) */
    /* TODO: print number of elements */

    return 0;
}
Inside the same block where the array is declared, sizeof(tab) gives the size of the whole array in bytes.

Task 2 โ€” Arrays as function parameters

Shown + variation. Learn why array functions need the size as a separate parameter.

Step 1 โ€” Create the file

nano ex21.c

Step 2 โ€” Study the shown function

#include <stdio.h>

void print_l(const long tab[], int size) {
    for (int i = 0; i < size; ++i) {
        printf("%ld ", tab[i]);
    }
    printf("\n");
}

int main(void) {
    long tab[] = { 5, 10, 15, 20 };
    int size = sizeof(tab) / sizeof(tab[0]);

    print_l(tab, size);

    return 0;
}

Step 3 โ€” Implement a variation yourself

Add one more function:

void print_reverse_l(const long tab[], int size);

The function should print the same array in reverse order.

In a function parameter, long tab[] is adjusted to long *. The function does not know how many elements the array has unless you pass the size separately.

Task 3 โ€” Variable Length Arrays

Shown. Use a runtime array size and understand the limits of this technique.

Step 1 โ€” Create the file

nano demo_vla.c

Step 2 โ€” Run the VLA experiment

#include <stdio.h>

int main(void) {
    int n;

    printf("Number of elements: ");
    if (scanf("%d", &n) != 1 || n <= 0 || n > 1000) {
        printf("Invalid size\n");
        return 1;
    }

    long tab[n];   /* VLA: size known at runtime */

    for (int i = 0; i < n; ++i) {
        tab[i] = i * 10L;
    }

    printf("sizeof(tab) = %zu bytes\n", sizeof(tab));

    for (int i = 0; i < n; ++i) {
        printf("%ld ", tab[i]);
    }
    printf("\n");

    return 0;
}
A Variable Length Array is automatic storage with a runtime size. It is useful to know, but in this course dynamic allocation with malloc will be the main technique for runtime-sized arrays.

Task 4 โ€” Memory lifetime: stack vs heap

Shown + self-check. Compare automatic storage with dynamic allocation.

Step 1 โ€” Create the demonstration file

nano demo_stack_heap.c

Step 2 โ€” Automatic arrays

#include <stdio.h>

void demo_stack(int n) {
    int a[10];   /* fixed-size local array */
    int b[n];    /* VLA */

    printf("sizeof(a) = %zu\n", sizeof(a));
    printf("sizeof(b) = %zu\n", sizeof(b));
}

int main(void) {
    demo_stack(5);
    return 0;
}
Both arrays are allocated automatically. They are destroyed when the function block is left.

Step 3 โ€” Dynamic memory

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

void demo_heap(int n) {
    int *p = malloc(n * sizeof(*p));

    if (p == NULL) {
        printf("Allocation failed\n");
        return;
    }

    printf("sizeof(p) = %zu\n", sizeof(p));

    free(p);
    p = NULL;
}
For dynamically allocated memory, sizeof(p) gives the size of the pointer, not the size of the allocated block.

Step 4 โ€” Find the errors

For each example, identify the problem. Then open the answer.

Example 1 โ€” Invalid free

void f(int n) {
    int a[n];
    free(a);
}
Answer

a is a VLA with automatic storage. It was not allocated with malloc, calloc, or realloc, so calling free(a) is invalid.

Example 2 โ€” Returning stack memory

int *f(int n) {
    int a[n];
    return a;
}
Answer

The function returns a pointer to a local array. After the function returns, the array no longer exists, so the returned pointer becomes invalid.

Example 3 โ€” Memory leak

void f(int n) {
    int *p = malloc(n * sizeof(*p));
}
Answer

If malloc succeeds, the allocated memory is never released. The function should call free(p) before it finishes, unless the pointer is returned to the caller and freed there.

Example 4 โ€” Wrong sizeof

int *p = malloc(10 * sizeof(p));
Answer

sizeof(p) is the size of the pointer, not the size of one int object.
Prefer one of the following forms:

malloc(10 * sizeof(*p));
malloc(10 * sizeof(int));

Task 5 โ€” Understanding sizeof

Shown + self-check. Learn how sizeof works with arrays and pointers.

Step 1 โ€” Create the demonstration file

nano demo_sizeof.c

Step 2 โ€” Compare a pointer and the pointed object

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

int main(void) {
    int x = 10;
    int *p = &x;

    printf("sizeof(p)  = %zu\n", sizeof(p));
    printf("sizeof(*p) = %zu\n", sizeof(*p));

    return 0;
}
sizeof(p) gives the size of the pointer itself.
sizeof(*p) gives the size of the object pointed to by p.

Step 3 โ€” Parentheses and readability

sizeof *p      /* correct */
sizeof(*p)     /* also correct, recommended in this course */
Both forms are equivalent, but sizeof(*p) is easier to read and less confusing for beginners.

Step 4 โ€” Correct allocation pattern

long *tab = malloc(n * sizeof(*tab));
This form automatically uses the correct object size even if the type of tab changes later.

Step 5 โ€” Self-check

What is wrong with the following allocation?

long *tab = malloc(n * sizeof(tab));
Answer

sizeof(tab) is the size of the pointer, not the size of one long object. The allocation should use sizeof(*tab) or sizeof(long).

long *tab = malloc(n * sizeof(*tab));
long *tab = malloc(n * sizeof(long));

Task 6 โ€” Dynamic allocation with malloc and calloc

Complete. Use heap memory for arrays whose size is known at runtime.

Step 1 โ€” Create the file

nano ex22.c

Step 2 โ€” Complete the program

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

int main(void) {
    int n;

    printf("Number of elements: ");
    if (scanf("%d", &n) != 1 || n <= 0) {
        printf("Invalid size\n");
        return 1;
    }

    long *tab = malloc(n * sizeof(*tab));

    if (tab == NULL) {
        printf("Allocation failed\n");
        return 1;
    }

    /* TODO: fill the array with values */
    /* TODO: print all values */

    free(tab);
    tab = NULL;

    return 0;
}

Step 3 โ€” Repeat the experiment with calloc

Replace malloc with calloc and check the initial values of the array.

long *tab = calloc(n, sizeof(*tab));

Task 7 โ€” Utility functions for arrays

Implement yourself. Write reusable functions for arrays of long.

Step 1 โ€” Create the file

nano ex23.c

Step 2 โ€” Implement printing

Write:

void print_l(const long tab[], int size);

Step 3 โ€” Implement randomization

Write:

void randomize_l(long tab[], int size, long min, long max);

The function should fill the array with random values from the interval [min, max].

Step 4 โ€” Hint

tab[i] = min + rand() % (max - min + 1);
This method is sufficient for small ranges in this course, but rand() % n may introduce slight bias.
Remember to include <stdlib.h> for rand and <time.h> for time if you initialize the random number generator with srand(time(NULL)).

Step 5 โ€” Random number initialization: two design variants

A library function should be easy to use. There are two reasonable ways to handle initialization of the random number generator.

Variant 1 โ€” explicit initialization by the caller

#include <stdlib.h>
#include <time.h>

int main(void) {
    long tab[10];

    srand((unsigned int) time(NULL));
    randomize_l(tab, 10, -20, 20);

    return 0;
}
This is explicit and traditional in C. The caller is responsible for calling srand once.

Variant 2 โ€” automatic initialization inside randomize_l

#include <stdlib.h>
#include <time.h>

void randomize_l(long tab[], int size, long min, long max) {
    static int initialized = 0;

    if (!initialized) {
        srand((unsigned int) time(NULL));
        initialized = 1;
    }

    for (int i = 0; i < size; ++i) {
        tab[i] = min + rand() % (max - min + 1);
    }
}
This variant is more convenient for the user of the function, but it hides internal state. The local static variable remembers whether initialization has already been done.
Self-check: which variant should we use?

Both variants are valid. Variant 1 is more explicit and closer to standard C style. Variant 2 is more convenient and demonstrates a useful role of a local static variable.

In this laboratory, you may choose one variant, but be consistent and document your decision in array.h or in comments near randomize_l.

Task 8 โ€” Array statistics with a structure

Implement yourself. Pass an array to a function and return a structure with calculated statistics.

Step 1 โ€” Create the file

nano ex24.c

Step 2 โ€” Study the structure

The structure stores several values calculated from one array.

typedef struct {
    int size;
    long min;
    long max;
    long range;
    long sum;
    double mean;
} ARRAY_STATS;

Step 3 โ€” Implement the function

Write the function below. Calculate all values in a single loop.

ARRAY_STATS statistics_l(const long tab[], int size);

Assume that size > 0.

Step 4 โ€” Starting hint

ARRAY_STATS stats;

stats.size = size;
stats.min = tab[0];
stats.max = tab[0];
stats.sum = tab[0];

Step 5 โ€” Test case

For this array:

long tab[] = { 4, -2, 10, 8 };

The expected values are:

size  = 4
min   = -2
max   = 10
range = 12
sum   = 20
mean  = 5.00
Self-check: possible implementation
ARRAY_STATS statistics_l(const long tab[], int size) {
    ARRAY_STATS stats;

    stats.size = size;
    stats.min = tab[0];
    stats.max = tab[0];
    stats.sum = tab[0];   /* first element already included */

    for (int i = 1; i < size; ++i) {
        if (tab[i] < stats.min) {
            stats.min = tab[i];
        }

        if (tab[i] > stats.max) {
            stats.max = tab[i];
        }

        stats.sum += tab[i];
    }

    stats.range = stats.max - stats.min;
    stats.mean = (double) stats.sum / stats.size;

    return stats;
}
Initialize min, max, and sum from the first element. Then start the loop from index 1 to avoid processing the first element twice.
This task combines arrays, loops, structures, and return values. It also introduces a useful idea: collect several results in one structure instead of using many separate output variables.

Task 9 โ€” Create array.h and separate implementation files

Shown. Organize reusable array functions into a small multi-file module.

Step 1 โ€” Create the header file

nano array.h
#ifndef ARRAY_H
#define ARRAY_H

typedef struct {
    int size;
    long min;
    long max;
    long range;
    long sum;
    double mean;
} ARRAY_STATS;

void print_l(const long tab[], int size);

/*
 * randomize_l fills the array with random values from [min, max].
 * Choose one design:
 * 1. call srand((unsigned int) time(NULL)) once in main before using it, or
 * 2. initialize the generator inside randomize_l using a local static variable.
 */
void randomize_l(long tab[], int size, long min, long max);

ARRAY_STATS statistics_l(const long tab[], int size);

#endif

Step 2 โ€” Create print_l.c

nano print_l.c

Move only the implementation of print_l to this file.

#include <stdio.h>
#include "array.h"

void print_l(const long tab[], int size) {
    /* TODO: paste your implementation here */
}

Step 3 โ€” Create randomize_l.c

nano randomize_l.c

Move only the implementation of randomize_l to this file.

#include <stdlib.h>
#include <time.h>
#include "array.h"

void randomize_l(long tab[], int size, long min, long max) {
    /* TODO: paste your implementation here */
}
The header <time.h> is needed only if your implementation initializes the generator with time(NULL) inside randomize_l.

Step 4 โ€” Create statistics_l.c

nano statistics_l.c

Move only the implementation of statistics_l to this file.

#include "array.h"

ARRAY_STATS statistics_l(const long tab[], int size) {
    /* TODO: paste your implementation here */
}
Each function is stored in a separate source file. This makes the library easier to extend in later laboratories, where new files such as bsort_l.c and isort_l.c will be added.

Step 5 โ€” Create a minimal program that uses the library

nano ex25.c
#include <stdio.h>
#include <stdlib.h>
#include <time.h>
#include "array.h"

int main(void) {
    long tab[10];

    /* Needed only if you chose Variant 1 for randomize_l. */
    srand((unsigned int) time(NULL));
    randomize_l(tab, 10, -20, 20);
    print_l(tab, 10);

    ARRAY_STATS stats = statistics_l(tab, 10);

    printf("size  = %d\n", stats.size);
    printf("min   = %ld\n", stats.min);
    printf("max   = %ld\n", stats.max);
    printf("range = %ld\n", stats.range);
    printf("sum   = %ld\n", stats.sum);
    printf("mean  = %.2f\n", stats.mean);

    return 0;
}

Task 10 โ€” Build a static library

Shown. Compile each implementation file separately and package all object files as libarray.a.

Step 1 โ€” Compile each implementation file

gcc -Wall -c print_l.c
gcc -Wall -c randomize_l.c
gcc -Wall -c statistics_l.c

Step 2 โ€” Create the static library

ar rcs libarray.a print_l.o randomize_l.o statistics_l.o

Step 3 โ€” Compile the program with the library

gcc -Wall ex25.c -L. -larray -o ex25
./ex25
In the next laboratory, you will add sorting functions to the same header file and library by creating additional source files and adding their object files to libarray.a.

Checklist before submission

  • you can explain the difference between a fixed-size array, a VLA, and a dynamically allocated array,
  • you know when memory is released automatically and when free is required,
  • you do not call free on local arrays or VLAs,
  • you do not return pointers to local arrays,
  • you implemented print_reverse_l,
  • you implemented print_l and randomize_l,
  • you implemented statistics_l and tested it on known values,
  • you understand why the statistics loop starts from index 1,
  • array.h contains include guards, the ARRAY_STATS type, and function declarations,
  • print_l.c, randomize_l.c, and statistics_l.c contain separate implementations,
  • ex25.c uses functions from libarray.a,
  • libarray.a is created and used during compilation.