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:
- The largest value.
- The average value.
Result Structure | |
---|---|
1 2 3 4 |
|
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 |
|
Function Call | |
---|---|
1 2 |
|
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:
- Evaluate the arguments.
- Copy the arguments to parameters positionally (i.e., 1st argument to 1st parameter, 2nd argument to 2nd parameter, and so on).
- Evaluate the function body.
- 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 |
|
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 |
|
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 |
|
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 |
|
Explanation
Notice that the output is:
Output | |
---|---|
1 |
|
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:
- String is an array of
char
with null terminator. - An array is a pointer to the first element.
- Pass-by-value copies the value of the member.
- The member being an array, has a value of an array.
- 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 |
|
Now the output shows that the function can change the content of the structure.
Output | |
---|---|
1 |
|
This behaviour can be explained by looking at the visualisation of the memory using box-and-arrow diagram.
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 |
|
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 |
|
Equivalent Dot Operator | |
---|---|
1 |
|
<pointer variable>
is a pointer to a structure containing the<member name>
.- 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 |
|
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 |
|