Structs
On this page: Accessors, Structs and Functions, Good Practices, Typedef
So far we have been able to create individual variables and arrays that contain multiples of the same type of variable. Sometimes it is handy to group different variables together because we use them to accomplish a certain task or they are related in some way. Structs, or structures, are the way we create groups of variables in C.
When we define a struct we are essentially creating a blueprint for how we want the variables to be grouped together. The variables that we add to our structs are often called fields or members. Let's look at an example:
struct data {
int a;
char b;
char string[20];
};
We first start with the struct keyword followed by the name our of struct. In this case I've called it 'data' but we could call it anything we'd like. This is followed by the variables that we would like have grouped together. Notice how the variables do not need to be the same type. This makes structures a little more flexible for moving data around than arrays provided we know in advance what we're using the structure for. Here's another example that may be familiar to people who have used the Unity or Godot game engines:
struct Vector3 // I intentionally changed the curly brace position here to demonstrate that both work.
{
double x;
double y;
double z;
}; // Notice that a semicolon is required at the end of a struct definition.
Once we've created our blueprint we still need to create a variable which will contain our struct.
struct Vector3 {
double x;
double y;
double z;
}
int main() {
struct Vector3 var;
/* ... */
return 0;
}
The struct Vector3 var is where we created a variable called var (creative, I know) with a type of struct Vector3. Whenever we use structs in C we have to use the struct
keyword to let the compiler know what's going on. That is, unless we use the typedef keyword, which I will cover in a moment.
Accessors
Once we've created our struct we need to be able to get access to those variables inside of it.
To do this we use a special symbol called an accessor. In most languages the accessor is represented by the . character and this true of C.
Let's see how it looks:
int main() {
struct Vector3 var; // At this point all of the variables in var are
// garbage values so we need to set them to something
var.x = 1.0;
var.y = 2.0;
var.z = 3.0;
/* ... */
return 0;
}
After doing this each of the struct's fields are set to their new values. When we access the variables using . we can interact with them as if they were ordinary variables.
It's also possible to have multiple variables as the same type of struct:
struct Vector3 var; // struct 1
struct Vector3 var_2; // struct 2
var1.x = 150.0;
var2.x = 10.0;
We could even create an array of structs like we can with any other type: struct Vector3 struct_arr[10];. Here I created an array of 10 Vector3 structs. We can access them like so:
struct_arr[2].x = 10.0; // Accessing the x field in the third Vector3 in the array.
The -> Accessor
This only matters when we get to pointers but if we have a pointer to a struct, the . accessor will not work on the pointer. Instead we have to use ->, like so:
struct Vector3 *p_var;
p_var->x = 10.0;
There is no particularly good reason for this apart from making it obvious that we're working with a pointer. Otherwise it's just a layer of confusion on top of structs.
Structs and Functions
Here is what it might look like to pass the Vector3 struct into a function:
void printVector(struct Vector3 v) {
printf("%lf, %lf, %lf", v.x, v.y, v.z);
}
We can also return structs from functions:
struct Vector3 normalized(struct Vector3 v) {
/* ... */
return new_vector3;
}
A handy feature of structs is that, unlike arrays, structs can be passed by value or by reference into a function. It's worth noting that a struct containing an array, when passed by value, will copy the array rather than the reference. This is different to what typically happens with arrays.
Good Practices
- Structs are really great for grouping data together that is used for a common process. In my examples above, a Vector3 is common in game engines to keep track of an object's x, y and z position in space. Grouping them with a struct makes it much easier to keep track of them, especially when we're dealing with a lot of data.
- It's often a good idea to define your structs in a header file so that each file that has that header is aware of what the struct should look like. (This will make more sense when we get to header files.)
Typdef
You may be wondering why I haven't used the typedef keyword yet, and why I've used the struct keyword more frequently that what we see in our lecture and lab material.
That is because I wanted to demonstrate what the typedef keyword is actually doing for us.
Simply typedef creates a new type (similar to int, char, double, etc.) that the compiler is aware of. Typedefs can be used to redefine already existing types such as:
typedef unsigned int u_int; // u_int now represents the same type as an unsigned int
// saving us from typing unsigned int all the time.
typedef long int DWORD; // DWORD is a common type used in the Win32 api but it's actually
// just another name for a long int (32-bit integer)
This is fine, but at a glance it can be tricky to read that typedef line. So instead there is a more idiomatic way that is easier and shorter to read and write:
typedef struct Vector3 {
double x;
double y;
double z;
} Vector3_t;
Here we can see the typedef keyword being used before the struct and the name of the new type is put just after the closing curly brace. Pretty handy!
Once we've done this we can omit all the uses of the struct keyword and instead use our newly defined type Vector3_t:
int main() {
Vector3_t var;
var.x = 10.0;
return 0;
}
Vector3_t normalized(Vector3_t v) {
/* ... */
return new_vector3;
}
A Note About The _t Suffix
The lecture material has suggested that we append _t to the end of our struct types. This is pretty old fashioned and tends to be frowned upon for various reason,
but perhaps the most significant reason why people avoid this is because the POSIX implementation of C has reserved many types with the _t suffix and it's
possible that our own types may clash with one of their names. If this happens it can be a tricky bug to track down, so in general it's better to not use _t.
That said, it's required in our assignments so I recommend using them during the assignment.
It is very common to find structs declared with the same struct and type name:
typedef struct Vector3 {
double x;
double y;
double z;
} Vector3;
