Skip to content

Struct and Function

Since type declarations add new types to the program, you may have noticed how this can be connected to a function. As function prototypes specify the types of parameters and return value, we can now use the newly declared types in our functions. What you may wonder is probably how the values are passed and returned from a function.

Returning Structure

Consider a different result structure below. This structure simply returns two values:

  1. The largest value.
  2. The average value.
Result Structure
1
2
3
4
typedef struct {
  int   max; // largest
  float ave; // average
} result_t;

If we have a function func() that returns a structure of type result_t, what would be the result? To be more precise, consider the following:

Function Definition
1
2
3
4
5
result_t func(...) {
  result_t res = {...};
  :
  return res;
}
Function Call
1
2
result_t result;
result = func(...);

What is happening behind the scene when we run the code? Remember that the mechanism behind a function call is called pass-by-value. This consists of 4 operations:

  1. Evaluate the arguments.
  2. Copy the arguments to parameters positionally (i.e., 1st argument to 1st parameter, 2nd argument to 2nd parameter, and so on).
  3. Evaluate the function body.
  4. Copy the return value back to the caller (if any).

Of interest is the 4th step, in particular the highlighted part. We actually copy the result back to the caller. As such, the statement result = func(...); is equivalent to:

Equivalent Statements
1
2
3
4
5
6
/* First  Equivalence */
result = res;

/* Second Equivalence */
result.max = res.max;
result.ave = res.ave;

StructureEg2.c

StructureEg2.c
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
#include <stdio.h>

typedef struct {
  int max;  
  float ave;
} result_t;

result_t max_and_average(int, int, int);

int main(void) {
  int num1, num2, num3;     
  result_t result;

  printf("Enter 3 integers: "); 
  scanf("%d %d %d", &num1, &num2, &num3);   
  result = max_and_average(num1, num2, num3);   // returned structure is copied

  printf("Maximum = %d\n", result.max);   // max is printed
  printf("Average = %.2f\n", result.ave);   // ave is printed
  return 0;
}

// Computes the maximum and average of 3 integers
result_t max_and_average(int n1, int n2, int n3) {
  result_t res; // answers are stored in the structure variable res

  res.max = n1;
  if (n2 > res.max)
    res.max = n2;
  if (n3 > res.max) 
    res.max = n3;

  res.ave = (n1+n2+n3)/3.0;     

  return res;   // res is returned here
}

Passing Structure Value

The copying mechanism is basically the essence of pass-by-value. This is also the mechanism used when we pass the arguments to parameters. If the argument is a structure, assuming the accepting parameter is also the same structure, then we copy the argument to the parameter. This means that there is no aliasing, unlike arrays.

We will illustrate this with the following example below.

PassStructureToFn.c

PassStructureToFn.c
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
// #include statements and definition
// of player_t are omitted here for brevity
void print_player(char [], player_t);

int main(void) {
  player_t player1 = { "Brusco", 23, 'M' }, player2;

  strcpy(player2.name, "July");
  player2.age = 21;
  player2.gender = 'F';

  print_player("player1", player1); // passing a structure to a function
  print_player("player2", player2); // passing a structure to a function

  return 0;
}

// Print player’s information
void print_player(char header[], player_t player) { // receiving a structure from the caller
  printf("%s: name = %s; age = %d; gender = %c\n", header,
         player.name, player.age, player.gender);
}

Arrays in Structure

There is a subtlety involved here. When we say that we copy the structure, we really mean copy the entire memory state belonging to the structure. What if we have an array inside the structure? That will also be copied!

The example below illustrates this subtlety.

PassStructureToFn2.c

PassStructureToFn2.c
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
// #include statements, definition of player_t, 
// and function prototypes are omitted here for brevity
int main(void) {
  player_t player1 = { "Brusco", 23, 'M' };

  change_name_and_age(player1);
  print_player("player1", player1);
  return 0;
}

// To change a player’s name and age
void change_name_and_age(player_t player) {
  strcpy(player.name, "Alexandra"); // Remember, strcpy takes an array (i.e., pointer to first element)
  player.age = 25;
}

// Print player’s information
void print_player(char header[], player_t player) {
  printf("%s: name = %s; age = %d; gender = %c\n", header,
         player.name, player.age, player.gender);
}

Explanation

Notice that the output is:

Output
1
player1: name = Brusco; age = 23; gender = M

The behaviour for age is obvious since it is a primitive data type int. Hence, pass-by-value ensures that the copy inside the function change_name_and_age is a different copy from the one inside main. But the fact that the name is unchanged, is rather curious. This curious behaviour is often expressed as the following deduction:

  1. String is an array of char with null terminator.
  2. An array is a pointer to the first element.
  3. Pass-by-value copies the value of the member.
  4. The member being an array, has a value of an array.
  5. Hence, the value copied should be the address.

Unfortunately, this is not the case. To refure the deduction, we have to note that when a structure is created, we allocate all the memory required to create this structure. This include the array. Since we have to declare the maximum size of the array, we practically know the maximum size of memory we need to allocate. Hence, we can copy the entire structure including the array.

To show this in action, we can use the sizeof operator.

If you run the ReplIt above, you will see that the size in both cases will be 40.

Passing Structure Address

Similar to an ordinary variable (e.g., of type int, float, double, char, etc), when a structure variable is passed to a function, a separate copy of it is made in the callee (i.e., the function being called). This removes aliasing and the original structure variable will not be modified by the function1.

Recap the similar problem in swapping two variables without using pointer. To allow the function to modify the content of the original structure variable, we will need to pass the address (pointer) of the structure variable to the function.

Passing Array of Structure?

Since the value of an array is the address of the first element (due to array decay), passing an array of structure is the same as passing the address of the first element. Therefore, the name of the array decays into a pointer. This means that the function is able to modify the array element.

In comparison to PassStructureToFn2.c above, to actually modify the content of the structure, we will need to pass the address instead. This is illustrated in the example below.

PassAddrStructToFn.c

PassAddrStructToFn.c
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
// #include statements, definition of player_t, 
// and function prototypes are omitted here for brevity
int main(void) {
  player_t player1 = { "Brusco", 23, 'M' };

  change_name_and_age(&player1);
  print_player("player1", player1);
  return 0;
}

// To change a player’s name and age
void change_name_and_age(player_t *player_ptr) {
  strcpy((*player_ptr).name, "Alexandra");
  (*player_ptr).age = 25;
}

// Print player’s information
void print_player(char header[], player_t player) {
  printf("%s: name = %s; age = %d; gender = %c\n", header,
         player.name, player.age, player.gender);
}  

Now the output shows that the function can change the content of the structure.

Output
1
player1: name = Alexandra; age = 25; gender = M

This behaviour can be explained by looking at the visualisation of the memory using box-and-arrow diagram.

Box-and-Arrow Structure

Arrow Operator

Note that the expression (*player_ptr).name used in PassAddrStructToFn.c is actually rather common. The parentheses here is actually very important because without the parentheses, the expressios *player_ptr.name is actually equivalent to

*(player_ptr.name)

This is because the dot (.) operator has higher precedence than dereferencing (*) operator. This is clearly wrong! The variable player_ptr is a pointer and not a structure variable. Therefore, we cannot access the member using player_ptr.name.

To make matter worse, if we have a nested structure with pointer, then we will need to nest the parentheses! For instance, consider the example below.

NestStructPtr.c

NestStructPtr.c
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
typedef struct {
  int val;
} base_t;
typedef struct {
  base_t *base1;
  base_t *base2;
} nested_t;
:
base_t base1  = {1};
base_t base2  = {2};
nested_t nest = {&base1, &base2};
nested_t *ptr = &nest;
printf("%d %d\n", (*(*ptr).base1).val, (*(*ptr).base2).val);

The expressions (*(*ptr).base1).val and (*(*ptr).base2).val are hard to write and very prone to error. Due to how common this is1, C provides an alternative shortcut syntax.

Arrow Operator

Arrow Operator
1
<pointer variable> -> <member name>;
Equivalent Dot Operator
1
(*<pointer variable>) . <member name>;

  1. <pointer variable> is a pointer to a structure containing the <member name>.
  2. The symbol -> must be written without a space between them.

Using this new syntax, we can then rewrite the NestStructPtr.c above to a simpler version NestStructArr.c below.

NestStructArr.c

NestStructArr.c
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
typedef struct {
  int val;
} base_t;
typedef struct {
  base_t *base1;
  base_t *base2;
} nested_t;
:
base_t base1  = {1};
base_t base2  = {2};
nested_t nest = {&base1, &base2};
nested_t *ptr = &nest;
printf("%d %d\n", ptr->base1->val, ptr->base2->val);

This is much better for readability as well. At the very least, you will be saving 2 characters writing it using arrow operator instead of dereferencing + dot operators (i.e., _t->member compared to (*_t).member).

Quick Quiz

Rewrite the function change_name_and_age to use the arrow operator.

"PassAddrStructToFn2.c
1
2
3
4
void change_name_and_age(player_t *player_ptr) {
  strcpy(player_ptr->name, "Alexandra");
  player_ptr->age = 25;
}


  1. This is common because we do NOT want to copy the entire structure especially if the structure is very big or containing an array.