Let's talk about structs.
Structs provide us with a way to group a bunch of variables together
into a nice package.
It's probably easiest to see an example right away,
so we say struct,
then opening curly brace,
and in this struct, we'll have an int age,
a char * name,
and that's it.
It may seem weird with a semicolon after a curly brace,
but it's in fact necessary with structs.
Any valid type can go within the struct definition.
Here, we've used an int and a char *,
but you could also use an array, of say, 100 elements
or even another struct.
When you're using structs in C,
you're creating new types
out of a collection of other types.
Here, we're making a new type
out of an integer and a char *.
As we'll see later, a struct type
is in a lot of ways equivalent to any other type you're used to.
Usually, I'll be comparing how a struct type
is similar to an integer type.
While the code we wrote is valid C,
it's not very useful,
and clang will give us a warning.
Remember how structs and its are similar?
Well, we basically just said
int,
which isn't a very helpful line.
So let's actually declare a variable of that type
by giving it a name before the semicolon.
We'll call the variable student.
Now we've declared a variable called student
with the type given by the struct.
How do we get to the variables inside the struct?
Technically, the names for these variables
are members.
To access any particular member in a student struct,
you append a dot to the variable name,
followed by the name of the member you want.
So here, the only 2 valid possibilities
are student.age
and student.name.
And we can do something like
student.age = 12
and student.name = student.
Now what if we wanted to make a second student?
You might think to copy and paste these lines
and change student to student 2 or something,
and that will work,
but technically, student and student 2
don't have the same type.
See, you won't be able to assign them to one another.
This is because, so far,
your struct has been anonymous.
We need to give it a name.
To do that, we insert the name of the struct
after the word struct.
student,
followed by the definition.
We can still immediately declare a variable of type
struct student, like we did before.
We'll call it S1
By giving the struct a name,
we can now use struct student
in almost the exact same way we would use int.
So we can declare a variable of type struct student,
like
struct student S2.
Like arrays, structs provide a shortcut initialization syntax,
so we can say, struct student S2
equals
left curly brace 3, S2.
Here, S2.age will be 3,
and S2.name will point to S2.
Think of all the things you can do with an int type
and most of them you can do with a struct student type.
We can use a struct student as a type of a function parameter.
We can use struct student inside of a new struct.
We can have a pointer to a struct student.
We can do size of struct student.
Struct student is a type
just like int is a type.
We can also assign S1 to S2
since both are of the same type, so we can do
S1 = S2.
What happens if we do
S1.age = 10?
Does S2 change at all?
Again, think of the structs just as regular integers.
If we assign some int X to some int Y,
like X = Y
and then change X,
as in X++,
does Y change at all?
Y does not change here, and so neither does S2 above.
S2.age is still 3.
But note that when assigning one struct to another,
all of the pointers still point to the same thing,
since they were just copied.
If you don't want the pointers to be shared,
you'll need to manually handle that,
perhaps by malicking one block of memory for one of the pointers to point to
and copying the data over.
It might be annoying to have to write struct student everywhere.
Using a type def, we can do
type def
struct
and we'll call it student.
Now, we can use student everywhere
that we used to use struct student.
This type def's an anonymous struct
and calls it student.
But if we also keep the student identifier
next to the word struct, as in typedef struct student,
we could use both struct student and student interchangeably now.
They don't even have to have the same name.
We could type def struct student to Bob
and then struct student and Bob
would be interchangeable types.
Regardless of the type def,
we need the identifier next to struct
if the definition of the struct
is recursive.
For example,
type def struct node
and it will be defined as an int val
and it will have a pointer that points to another struct node.,
as in struct node * next.
And then we'll call it node.
This struct is recursive,
since the definition of struct node contains within it
a pointer to a struct node.
Notice that we have to say struct node * next
inside of the definition of the struct node,
since the type def hasn't finished yet to allow us to simplify this
to just node * next.
You'll learn more about structs similar to this
when dealing with linked lists and trees.
What about structs in a function?
This is also perfectly valid.
We could have
void func
which takes as an argument,
student s
and does something with that student.
And then we can pass it as student struct like so.
Func of S1 from before.
The struct behaves
exactly as an integer would when passed to a function.
Func receives a copy of S1
and so can't modify S1;
rather, only the copy of it that's stored in S.
If you want the function to be able to modify S1,
func will need to take a student * S,
and you'll have to pass S1 by address, like so.
Student * S, func & S1.
There's another reason to pass by address here.
What if our struct contained 100 fields?
Every single time we pass a student to func,
our program needs to copy all of those 100 fields into func's argument S,
even if it never uses the vast majority of them.
So even if func doesn't plan on modifying the student,
if can still be valuable to pass by address.
Okay, what if we want to create a pointer to a struct?
We could do something like
student* S
equals malloc
size of student.
Notice that size of still works here.
So how do we now access the age member
of the block that S points to?
You might first think to do
* S.age = 4,
but this won't quite work.
Since this will really be interpreted as
* S.age in parentheses = 4,
which won't even compile,
since S isn't a struct or rather a pointer to a struct,
and so the dot won't work here.
We could do
(*S).age=4
but the parentheses can get annoying and confusing.
Thankfully, we have a special arrow operator
that looks something like
S->age=4.
These 2 ways of referencing age
are equivalent
and we don't really ever need the arrow operator,
but it makes things look nicer.
Since S is a pointer to some block of memory that contains the struct,
you can think of S>age as follow the pointer arrow
and grab the age member.
So why should we ever use structs?
It's definitely possible to get away with just the primitive integers,
chars, pointers and the like
that we're used to;
instead of S1 and S2 before,
we could have had age1, age2, name1, and name2
all at separate variables.
This is fine with only 2 students,
but what if we had 10 of them?
And what if instead of only 2 fields,
the student struct had 100 fields?
GPA, courses, hair color, gender, and so on.
Instead of just 10 structs, we need 1,000 separate variables.
Also, consider a function
that takes that struct with 100 fields with its only argument
and prints out all fields.
If we didn't use a struct,
every single time we call that function,
we need to pass at all 100 variables,
and if we have 100 variables for student 1,
and 100 variables for student 2,
we need to be sure we don't accidentally pass some variables from student 1
and some variables from student 2.
It's impossible to make that mistake with a struct,
since all 100 variables are contained in a single package.
Just a couple of final notes:
If you've understood everything up to this point, great.
The rest of the video is just for completeness' sake.
Because structs can hold any type of pointer,
they can also hold function pointers.
If you're familiar with object oriented programming,
this provides a way to use structs to program in an object oriented style.
More on function pointers at another time.
Also, sometimes you may have 2 structs
whose definitions depend on one another.
For example,
we could have struct A,
which is defined as
a pointer to a struct B,
struct B * X,
and now we can have a struct B
which is defined as a pointer
to a struct A,
struct A * Y.
But this won't compile,
since struct B doesn't exist at the time that struct A is being compiled.
And if we swap struct A and struct B,
then we'd just be left with the same problem;
this time, with struct A not existing.
To solve this, we can write
struct B;
before the definition of struct A.
This is called a forward declaration.
This just lets the compiler know that
struct B is a valid type that will be fully defined later or elsewhere.
My name is Rob Bowden, and this is CS50.
[CS50.TV]