CS211: Struct
Table of Contents
Introduction
C provides only a select few predefined types: char, int, float, double. Some of these types can be further modified to represent long and short versions, as well as be made signed or unsigned. Every element type can be represented as a pointer using the * delcarator. There's even a type that represents the "absence" of a type with void. For many programmers, this simply isn't enough. C also furnishes space for those wish to go a bit further with the addition of structs.
What is a struct?
A struct is an aggregate data type defined by the programmer which combines other predefined data types into one larger grouping. From the C specification:
A structure type describes a sequentially allocated nonempty set of member objects (and, in certain circumstances, an incomplete array), each of which has an optionally specified name and possibly distinct type.
The above technical definition sounds a bit scarier than it is. Here is an example of a struct declaration:
1: struct frac{ 2: int n; // Numerator 3: int d; // Denominator 4: };
Here, I declare a new data type called struct frac which contains two member variables, n and d. I can declare a variable of type struct frac like this (assuming the struct has already been declared above):
1: int main(){ 2: struct frac f; // Declaring a variable "f" of type "struct frac" 3: f.n = 1; // Using "." to reference member "n" 4: f.d = 2; // Using "." to reference member "d" 5: } 6:
The code above shows the declaration of a variable f of type "struct frac" and the setting of both of its member variables.
Much like arrays, initialization lists can be used to set the members:
1: int main(){ 2: struct frac f = {1, 2}; 3: }
Above, n and d are set using the initilization list. Initilization lists will set the values in the same order in which they were defined ({1,2} will match {n,d}).
Using typedef with structs
Using the definition shown in Listing 3, I would be forced to always write struct frac when referencing my bespoke fraction type. This leads to visual clutter in the code that can often be tiresome to the eye. A way to avoid that is through the use of the typedef keyword which will map a given symbol to a type definition. For example:
1: typedef struct frac{ 2: int n; 3: int d; 4: }frac; // Matching a symbol "frac" to the entire type definition 5: 6: int main(){ 7: frac f; // Declaring a variable "f" of type "frac" 8: f.n = 1; // Using "." to reference member "n" 9: f.d = 2; // Using "." to reference member "d" 10: }
Although it only eliminated a little bit of extra keyword usage in the above example, this can be rather useful when applied to longer stretches of code.
Dereferencing struct pointers
It is entirely possible to create pointers to structs. For example, I might want to place one of my fracs on the heap. We know that using malloc to do so will return a pointer. We can access members of struct references in one of two ways, both shown below:
1: #include<stdio.h> 2: 3: typedef struct frac{ 4: int n; 5: int d; 6: } frac; 7: 8: int main(){ 9: frac* fp = malloc(sizeof(frac)); 10: (*fp).n = 1; // Using "*()." to reference member "n" 11: fp->d = 2; // Using "->" to reference member "d" 12: }
Line 10 shows that we can dereference fp like any other pointer type. Once that's done we should get the frac returned by the dereference and then use the . notation to grab the n member. Line 11 condenses both with the -> notation—f is dereferenced and then member d is selected.
Why use structs?
Structures are incredibly useful when programming in C. They allow for programmers to define type-specifc logic to functions that would not be aware of it otherwise. For example, we could utilize an array as if it were a derived type like frac as follows:
1: void print_frac(void * arr){ 2: int * f = (int *) arr; 3: printf("%d/%d\n", f[0], f[1]); 4: } 5: 6: int main(){ 7: int frac1[2] = {1,2}; 8: char* frac2[2] = {"hello", "world"}; 9: 10: print_frac(frac1); 11: print_frac(frac2); 12: }
The above will not throw warnings or provide any information to the user that there is any attempt to break the underlying logic of the print_frac function. The C compiler lacks additional type information to see that:
1: josephraskind@stargazer:/tmp/struct$ gcc arr_struct.c -o arr_struct; ./arr_struct 2: 1/2 3: 2032603147/21931
If we alter the code in Listing 6 to now be:
1: #include <stdio.h> 2: 3: typedef struct frac{ 4: int n; 5: int d; 6: } frac; 7: 8: void print_frac(frac f){ 9: printf("%d/%d\n", f.n, f.d); 10: } 11: 12: int main(){ 13: frac frac1 = {1,2}; 14: frac frac2 = {"hello", "world"}; 15: 16: print_frac(frac1); 17: print_frac(frac2); 18: }
We can compile and run it:
1: josephraskind@stargazer:/tmp/struct$ gcc strict_struct.c -o strict_struct; ./strict_struct 2: strict_struct.c: In function ‘main’: 3: strict_struct.c:14:17: warning: initialization of ‘int’ from ‘char *’ makes integer from pointer without a cast [-Wint-conversion] 4: 14 | frac frac2 = {"hello", "world"}; 5: | ^~~~~~~ 6: strict_struct.c:14:17: note: (near initialization for ‘frac2.n’) 7: strict_struct.c:14:26: warning: initialization of ‘int’ from ‘char *’ makes integer from pointer without a cast [-Wint-conversion] 8: 14 | frac frac2 = {"hello", "world"}; 9: | ^~~~~~~ 10: strict_struct.c:14:26: note: (near initialization for ‘frac2.d’) 11: 1/2 12: 1715884043/1715884049
Although C allows the logic to be broke, the compiler does provide useful type mismatch information in the form of warnings which could be used to debug any apparent problems.
How are structs Stored?
In order for structs to function as data types in C there size must be known at compile time. This can be seen in Line 9 of 5 which shows that the built-in gcc macro sizeof is capable of being applied to the user-defined struct symbol. The size of structs are not always obvious a priori. For example, take the following struct definitions and the results of sizeof on each of them:
1: #include <stdio.h> 2: struct padded { 3: char a; // 1 byte -- offset 0 4: // 3 bytes padding 5: int b; // 4 bytes -- offset 4 6: char c; // 1 byte -- offset 8 7: // 7 bytes padding 8: double d; // 8 bytes -- offset 16 9: }; // total: 24 bytes 10: 11: struct packed { 12: double d; // 8 bytes -- offset 0 13: int b; // 4 bytes -- offset 8 14: char a; // 1 byte -- offset 12 15: char c; // 1 byte -- offset 13 16: // 2 bytes padding 17: }; // total: 16 bytes 18: 19: int main(){ 20: printf("%lu\n", sizeof(struct padded)); // 24 21: printf("%lu\n", sizeof(struct packed)); // 16 22: }
1: josephraskind@stargazer:/tmp/struct$ gcc struct_size.c -o size; ./size 2: 24 3: 16
These results are for my x86-64 machine—it could be different depending on the architecture. The padding is a result of the natural word size of x86-64 machines being 8 bytes long. The architecture expects each variable to be aligned 4/8 byte boundaries, so the C compiler which targets x86-64 will comply by stuffing "padding" bits in the slots in between.
There are ways of overriding bit padding, but they are well outside the scope of this class.
Exercises
Implement the following functions for the
fracstruct defined in Listing 3:print_frac— prints the fraction in the formn/dadd_frac— adds two fractions and returns the resultmul_frac— multiplies two fractions and returns the resultsimplify— divides numerator and denominator by their GCD
Test each with at least two examples and verify the results by hand.
- Using
typedefas shown in Listing 4, rewrite your solutions from exercise 1 so thatstruct fracis replaced with the aliasfracthroughout. How does this change the readability of your code? Extend the
fracstruct with two more functions:sub_frac— subtracts one fraction from anotherdiv_frac— divides one fraction by another by multiplying by the reciprocal
Verify that
div_frac(add_frac(f1, f2), f2)returns the same value asf1for a few test cases.- Using
mallocand the->operator as shown in Listing 5, allocate afracon the heap, set its members, pass it toprint_frac, and free it. Run the program undervalgrindto confirm there are no memory leaks. - Write a program that declares an array of five
fracstructs, initializes each using an initialization list, and prints them all usingprint_frac. Then write a functionsum_allthat takes an array offracstructs and its length and returns their sum as a singlefrac. - Compile Listing 10 on your machine and verify the
sizeofoutputs match those shown in Listing 11. Then useoffsetoffrom<stddef.h>to print the byte offset of each member in bothstruct paddedandstruct packed. Do the offsets match the comments in the listing? - Design a new struct
struct packed_fracthat reorders the members ofstruct fracto minimize padding. Is it possible to reduce the size below what the default layout produces? Usesizeofandoffsetofto verify your answer. - The lecture shows in Listing 6 that using a plain array instead of a struct loses type information. Reproduce this example and compile it. Then alter it to use the
fracstruct as in Listing 8 and compare the compiler output. In your own words, explain what the struct provides that the array does not. Consider the following attempted self-referencing struct:
struct frac { int n; int d; struct frac next; };
Try to compile it and record the error. Explain why C cannot allow a struct to contain a member of its own type directly. Then fix it by replacing
struct frac nextwithstruct frac *nextand explain why the pointer version works where the direct member does not.Implement a simple family tree using a self-referencing struct:
struct person { char name[50]; int age; struct person *parent; };
Create at least three generations, link them together using the
parentpointer, and write a functionprint_ancestorsthat walks up the chain printing each person's name and age until it reaches aNULLparent pointer.