Student Records - V2
In our previous example in the struct chapter, we introduced the idea of creating a struct that can be used to create custom data types.
We created a struct which we initialized on creation then we printed out the data members of that struct. What if we wanted to isolate this printing process to a function? This would align with how we have been working throughout the course. We want to create a function to print our struct so that it is much much easier to update and modify.
We can always pass in each component of the struct as separate arguments:
struct Student
{
char firstName[101];
char lastName[101];
int studentNumber;
char program[5];
float gpa;
};
void printStudent(const char* firstName, const char* lastName,
int studentNumber, const char* program, float gpa);
int main(void)
{
struct Student student1 ={ "Amy", "Lee", 123456789, "CPA", 3.7};
printStudent(studen1.firstName, student1.lastName,student1.studentNumber, student1.program, student1.gpa);
return 0;
}
void printStudent(const char* firstName, const char* lastName,
int studentNumber, const char* program, float gpa){
printf("%s %s\n", firstName, lastName);
printf("Student #: %d\n", studentNumber);
printf("Program: %s\n", program);
printf("GPA: %f\n", gpa);
}
But.... this isn't good! What if our struct had a lot more information in it? our parameter list would be very long. Also what if we had to alter the struct... what if we had to add or remove a data member? Every parameter list and function call to this function would be affected. The point of the struct is to package everything we want into one single entity. Separating out the data members into separate parameters really defeats the purpose of a struct.
Thus, the better solution is to pass the entire struct into the function!
However, this also has some drawbacks. In C parameters are generally pass by value. This means that every value that is passed to a function is duplicated. This is why when we do walkthroughs, we have a separate table for each function call because the variables that are declared in the parameter list or locally in the function exists separately.
If we were to pass a struct to a function by value, we would be duplicating all of its data members. This will make things very inefficient. Instead, when we pass structs to functions we use a mechanism called pass by address. Pass by address means we are passing the address of the variable into the function.
We have seen this previously when we called scanf(). The reason why we use pass by address with scanf() is to allow the original variable to be modified by the scanf() function.
When we pass something to a function by address, we are providing access to the original variable. We need to take care not to accidentally modify the original variable unless this is what we want to do. This is where the use of the const modifier is really useful as any attempted changes to the original variable will be flagged as an error
Declaring Variables for Addresses
A variable that can hold an address is called a pointer. All pointers hold addresses. The amount of memory needed to hold an address is the same regardless of what the pointer is pointing at. However, we still need to know how to interpret the data at the end of pointer, thus all pointers have a specific type related to what it is pointing at. A pointer to an integer expects that at the end of the pointer is an integer. To declare a pointer to point at a specific type of data we put a * before the name of the variable.
Examples:
int* ptrToInt; //can hold address of an int
double* ptrToDouble; //can hold address of a double
struct Student* ptrToAStudent; //can hold address of a Student struct
Applying this to our problem, we can rewrite our prototype and function call as follows:
struct Student
{
char firstName[101];
char lastName[101];
int studentNumber;
char program[5];
float gpa;
};
//this function accepts the address of a Student struct.
void printStudent(const struct Student* student);
int main(void)
{
struct Student student1 ={ "Amy", "Lee", 123456789, "CPA", 3.7};
//here we pass the adddress of student1 to the printStudent function
//the reason we do this is because this only passes in a single address
//to the function which is much more efficient than passing in the entire struct
printStudent(&student1);
return 0;
}
void printStudent(const struct Student* student)
{
...
}
This is what the above would do right after the printStudent() function gets called.
Accessing data members through pointers
Previously we said that we use . notation to access the data member of a struct (for example, student1.firstName).
Inside the printStudent() function, the variable student is not a Student struct. It is a pointer to a Student struct.
Thus if we wrote:
void printStudent(const struct Student* student)
{
printf("%s %s\n", student.firstName, student.lastName);
printf("Student #: %d\n", student.studentNumber);
printf("Program: %s\n", student.program);
printf("GPA: %f\n", student.gpa);
}
we would end up with a syntax error because . expects the operand on the left to be an instance of a struct, not a pointer to a struct.
To access a data member through a pointer we need to use the -> operator instead. This operator works just like . except it expects the left operand to be a pointer to a struct as opposed to a struct.
Thus our function is:
void printStudent(const struct Student* student)
{
printf("%s %s\n", student->firstName, student->lastName);
printf("Student #: %d\n", student->studentNumber);
printf("Program: %s\n", student->program);
printf("GPA: %f\n", student->gpa);
}