Register

C for Beginners

0 of 24 notes read 0%

Reading progress is saved to your INFIRALS account. Sign in to keep this note marked as read.

Arrays of structs and passing them to functions

Records, files and recursion12 min readLesson 3 of 6

3.1 A table is an array of structs

Records of the same shape belong in an array of structs. Each element holds a complete record, the array is contiguous, and a loop reaches every field.

The count is a separate variable, or a one-past-the-end pointer. Inside a function, sizeof on the parameter gives the pointer size, so pass the count.

Indexing reads in two steps: students[i] selects a record, and .score selects a field. Initialising with brace groups keeps the rows aligned with the declaration.

c
#include <stdio.h>

struct Student {
    char name[16];
    int score;
};

int main(void)
{
    struct Student students[3] = {
        {"Ada", 91},
        {"Grace", 88},
        {"Linus", 74}
    };
    int count = (int)(sizeof students / sizeof students[0]);
    for (int i = 0; i < count; i++) {
        printf("%s %d\n", students[i].name, students[i].score);
    }
    return 0;
}

Output

Ada 91
Grace 88
Linus 74

3.2 Passing by value copies the record

A struct argument is copied. Changes to the parameter stay in the function, and the caller's record is untouched.

That is fine for small structs and for functions that treat the record as a value. It becomes expensive for records with embedded arrays, because the copy includes the whole array.

Passing a pointer passes the address instead. The function can modify the original, and a const pointer states that it will not. The arrow operator reaches members through the pointer.

c
#include <stdio.h>

struct Account { char owner[16]; int balance; };
static void add_value(struct Account account, int amount)
{
    account.balance += amount;
}
static void add_pointer(struct Account *account, int amount)
{
    account->balance += amount;
}
int main(void)
{
    struct Account account = {"Ada", 100};
    add_value(account, 50);
    printf("after value: %d\n", account.balance);
    add_pointer(&account, 50);
    printf("after pointer: %d\n", account.balance);
    return 0;
}

Output

after value: 100
after pointer: 150

3.3 Returning a struct is fine

A function can return a struct by value. The compiler copies it to the caller, and modern compilers often build it directly in the caller's storage.

Returning a struct avoids output parameters for small results: struct Point make_point(int x, int y) reads better than three pointer arguments.

Returning a pointer to a local is still wrong. If the function allocates the struct, the caller owns it and the ownership rule belongs in the name or a comment.

c
#include <stdio.h>

struct Stats { int min; int max; int total; };
static struct Stats summarise(const int *values, int count)
{
    struct Stats result = {values[0], values[0], 0};
    for (int i = 0; i < count; i++) {
        if (values[i] < result.min) result.min = values[i];
        if (values[i] > result.max) result.max = values[i];
        result.total += values[i];
    }
    return result;
}
int main(void)
{
    int values[5] = {4, 9, 2, 7, 5};
    struct Stats stats = summarise(values, 5);
    printf("%d %d %d\n", stats.min, stats.max, stats.total);
    return 0;
}

Output

2 9 27

3.4 A const pointer to an array of records

A printing function only reads, so its parameter is const struct Student *students. The caller passes the array, which becomes a pointer to the first element.

Inside the function, students[i] works because subscripting is defined on pointers. The const applies to the elements, so an accidental write fails to compile.

For an array of structs, the pointer type is struct Student *, not struct Student **. The second form is an array of pointers, which is a different layout.

c
#include <stdio.h>

struct Student { char name[16]; int score; };

static void print_all(const struct Student *students, int count)
{
    for (int i = 0; i < count; i++) {
        printf("%s: %d\n", students[i].name, students[i].score);
    }
}

int main(void)
{
    struct Student students[2] = {{"Ada", 91}, {"Linus", 74}};
    print_all(students, 2);
    return 0;
}

Output

Ada: 91
Linus: 74

3.5 Sorting records with qsort

qsort reorders elements of any type given the element size and a comparison function. For an array of structs, the comparator receives pointers to elements, so cast to const struct Student *.

The comparator returns a negative value, zero, or a positive value. Subtracting two int scores is safe only when the difference cannot overflow; comparing and subtracting booleans works for any values.

qsort is not stable. Records that compare equal can come out in any order, which matters when a second key should break ties.

c
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
struct Student { char name[16]; int score; };
static int by_score(const void *left, const void *right)
{
    const struct Student *a = (const struct Student *)left;
    const struct Student *b = (const struct Student *)right;
    if (a->score != b->score) return (a->score > b->score) - (a->score < b->score);
    return strcmp(a->name, b->name);
}
int main(void)
{
    struct Student students[3] = {{"Linus", 74}, {"Ada", 91}, {"Grace", 91}};
    qsort(students, 3, sizeof students[0], by_score);
    for (int i = 0; i < 3; i++) printf("%s %d\n", students[i].name, students[i].score);
    return 0;
}

Output

Linus 74
Ada 91
Grace 91

3.6 Passing arrays of records through layers

A parser fills an array of records and returns how many it wrote. The caller passes the array and its capacity, and the function reports a count that is at most the capacity.

The bound has to be checked before each write. A function that trusts the capacity from a header, without checking it against the array it was given, has the same overflow problem as strcpy.

Returning the count and taking the capacity as a parameter is a common C signature: int load_records(struct Record *out, int capacity).

c
#include <stdio.h>

struct Record { int id; int value; };
static int fill(struct Record *out, int capacity)
{
    int written = 0;
    for (int id = 1; id <= 4 && written < capacity; id++, written++) {
        out[written].id = id;
        out[written].value = id * id;
    }
    return written;
}
int main(void)
{
    struct Record records[3];
    int count = fill(records, 3);
    for (int i = 0; i < count; i++) printf("%d:%d\n", records[i].id, records[i].value);
    printf("written: %d\n", count);
    return 0;
}

Output

1:1
2:4
3:9
written: 3

3.7 Struct of arrays or array of structs

An array of structs keeps each record together, which suits code that reads or writes one record at a time. A struct of arrays groups one field from every record, which suits loops over a single field.

The second layout, often written SoA, reads well for numeric code and vectorises better. It trades locality per record for locality per field.

For a beginner program the array of structs is the simpler default. Changing layout later means changing the accessors, not the algorithm, if the code goes through functions.

c
#include <stdio.h>
struct Scores {
    int scores[4];
    int count;
};
static int total(const struct Scores *all)
{
    int sum = 0;
    for (int i = 0; i < all->count; i++) {
        sum += all->scores[i];
    }
    return sum;
}
int main(void)
{
    struct Scores all = {{71, 84, 60, 93}, 4};
    printf("%d\n", total(&all));
    return 0;
}

Output

308

Interview questions

3 questions from this note. Open one to see the answer; no account needed.

  • 01When should a struct be passed by pointer?

    When the function must modify it, when the struct is large, or when copying its embedded arrays would cost too much. A `const` pointer covers the read-only case.

  • 02Can a function return a struct by value?

    Yes. The compiler copies the struct to the caller, and there is no dangling reference because the caller receives a completed value.

  • 03What does the `qsort` comparator receive?

    Two `const void *` pointers to elements of the array. Cast them to the element type and return a negative, zero, or positive value.