Using struct: A Mixed Fraction Case Study
Using struct in C++, we revisit the mixed fraction case study, resulting in a more modular and compact implementation.
In this lesson, we’ll improve the structured programming approach for a mixed fraction calculator by utilizing struct
to group related data elements. By doing so, we aim to enhance code modularity and readability. This will result in a more effective and concise implementation, showcasing the benefits of structured programming with data type grouping in C++.
Case study: A mixed fraction arithmetic calculator
We’ll follow the same pattern that we did in our last case study lesson. We’ll build our code step by step, creating a deeper understanding of how the functionality of structures enables us to reduce code complexity. In this case study, we’ll go over the addition function of the mixed fraction case study, and the rest of the operations will be given to you as exercises. So, let’s start.
Finding the required memory
In the procedural programming implementation, we used three variables for storing the data for a single mixed fraction (whole number, numerator, and denominator), so there was a separate variable for each fraction element. Considering we needed three mixed fractions (left, right, and final), that made a total of nine variables that were being passed to the add()
function. This would make the variable management and tracking hectic, and in larger codes where we might need, say, the sum of four mixed fractions, it could lead to potential logical errors, and the code itself would become difficult to understand.
Luckily, structures in C++ provide immunity to this problem. Instead of passing three separate variables to the add()
function, we can now easily pass a struct
containing these three variables as its members, simplifying our problem. The comparison is drawn below.
With primitive data types
int w; //whole number
int n; //numerator
int d; //denominator
With struct
struct MixedFraction
{
int sign; // either will be +1 or -1
int w; //whole number
int n; //numerator
int d; //denominator
};
By employing struct
, we can create grouped data (commonly referred to as an object) for MixedFraction
. This data type will encompass all three elements of a mixed fraction, along with an additional variable to represent its sign
, resulting in a simplified and more efficient code implementation.
MixedFraction result = {}; //Final fraction that will store the resultMixedFraction L = {}; // Left fractionMixedFraction R = {}; // Right fraction
In the example above, we made three MixedFraction
variables for each mixed fraction for our arithmetic operations. Throughout the case study, L
and R
will represent the left and right fractions, while the result
fraction will be used to store the arithmetic operation output.
Implementing the add()
function
Technically, instead of nine int
variables that we passed to the add()
function in procedural programming (left and right fraction variables by value and the final fraction’s variables that stored the result of the arithmetic operation by reference method), we now just have to pass three MixedFraction
variables from the previous widget, like this:
void add(MixedFraction L, MixedFraction R, MixedFraction &result)
It is a void
type function that will update the result
fraction as it is passed by reference. It's important to note that we chose to pass variables by reference because we had the limitation of returning more than one variable from a function. So, it only made sense that updating three variables must require each one of them to be passed by reference. However, we don’t have this limitation now, and we can go a step further to make our code more modular and easier to manage.
By using MixedFraction
as the function data type, we enable it to return a structure variable, thus excluding the need to pass the third result
fraction by reference.
With primitive data types
void add(int l_w, int l_n, int l_d, //left
int r_w, int r_n, int r_d, //right
int &f_w, int &f_n, int &f_d); //final
With struct
MixedFraction add(MixedFraction L, //left
MixedFraction R) //right
The flow of the add()
function
Following the previous convention, we’ll give you an overview of the add()
function. The purpose here is to help you understand how and what helper functions are used, in what order they are used, and the general changes we’re supposed to make as compared to procedural programming.
MixedFraction add(MixedFraction L, MixedFraction R){MixedFraction result{};//Step 1: Convert to Improper FractionimproperFraction(L);improperFraction(R); // L.w and R.w are 0's now//Step 2: Make d same, and attach the sign to numeratorsameDenominator(L,R);//Step 3: Compute resulting fractionresult.n = L.n + R.n;result.d = L.d;computeSign(result);//Step 4: Reduce the fraction (GCD)int gcdRes = gcd(abs(result.n), result.d); // numerator could be -veresult.n /= gcdRes;result.d /= gcdRes;//Step 5: Convert to Mixed Fraction & PrintMixedFraction(result);return result;}
Note: Notice how we defined the
result
structure variable within the scope of the function. This helps us eliminate the need to initialize it in themain()
function and allows us to pass it to functions by reference, improving overall variable management.
Much cleaner than before, right? Let’s look at the coding details.
Step 1: Convert the mixed fraction into an improper fraction
With the use of structures, converting mixed fractions into improper fractions is much easier now.
void improperFraction(MixedFraction &frac){frac.n = (frac.d) * (frac.w) + (frac.n);frac.w = 0; // frac.sign/frac.d are unchanged}
Line 1: A fraction of the
MixedFraction
data type is passed by reference to theimproperFraction()
function, meaning the members of this fraction will be updated using thevoid
function.Line 3: The
frac.n
numerator of thefrac
fraction is updated by adding the product of thefrac.d
denominator and the whole numberfrac.w
to thefrac.n
numerator. Since we have associated a separate structure member to the sign of the fraction (frac.sign
in this case), we don’t need to compute a sign or takeabs()
of each member.Line 4: The whole number of the fraction is set to
0
because there is no whole number in an improper fraction. Overall, fraction sign and denominator values remain the same.
Step 2: Making denominators the same
The next step involves making denominators the same for both fractions so they can be added together.
void sameDenominator(MixedFraction &L, MixedFraction &R){// 1: left n = left n * right dL.n = L.n * R.d * L.sign; // attaching sign to numerator// 2: left n = right n * left dR.n = R.n * L.d * R.sign; // attaching sign to numerator// 3: Denominator calulationL.d = R.d = L.d * R.d;L.sign = R.sign = 0; // removing sign, as it is not needed anymore}
Line 1: The left and right fractions are passed by reference to the
sameDenominator()
function, which updates the numerators and denominators of both improper fractions, making their denominators the same.Line 4: This updates the numerator of the left fraction
L.n
by multiplyingL.n
with the right fraction denominatorR.d
and the overall sign of the left fractionL.sign
.Line 6: This updates the numerator of the right fraction
R.n
by multiplyingR.n
with the left fraction denominatorL.d
and the overall sign of the right fractionR.sign
.Line 8: The left and right fraction denominators are both equal to the product with each other.
Note: Sign is now associated with the numerators of both fractions as we needed to add both of these components. We'll need to disassociate it and assign it back to the resulting fraction's
result.sign
sign member later on.
Step 3: Computing the resulting fraction
The next step involves adding both fractions. This is done by accessing the members of each structure’s variables and adding them together.
result.n = L.n + R.n; // Resultant numeratorresult.d = L.d; //Resultant denominatorcomputeSign(result); // Resultant sign
The output of the addition is stored in the result
fraction.
Line 1: The final fraction numerator
result.n
is updated by adding the left and right fractions’ numerators.Line 2: The final fraction denominator
result.d
equals either the left or right fraction’s denominator. Since both are equal, we can use either one. In this case, we have usedL.d
.Line 3: A
compute_sign()
helper function is used, which computes and assigns the overall sign for the resulting fraction.
The code for the compute_sign()
helper function is given below.
void computeSign(MixedFraction & frac){if (frac.n < 0)frac.sign = -1;else frac.sign = +1;frac.n = abs(frac.n);}
Unlike the computeSign()
function in procedural programming, this function disassociates the sign from the numerator of the fraction and assigns it to the fraction member frac.sign
(which, in this case, is result.sign
).
Line 1: A fraction is passed by reference to the
computeSign()
function.Lines 3–5: The fraction sign is set to
-1
(negative) if the numerator value is less than0
. Otherwise, it’s set to+1
(positive).Line 6: The fraction numerator value is set to absolute, removing any sign.
Step 4: Reduce the fraction (GCD)
The next step involves reducing the result
fraction before it is converted back to a mixed fraction.
int gcdRes = gcd(result.n, result.d); // numerator could be -veresult.n /= gcdRes;result.d /= gcdRes;
Line 1: The
gcd()
helper function is called, and the greater common devisor of the numerator and the denominator of theresult
fraction is computed.Line 2: The numerator of the
result.n
fraction is reduced.Line 3: The denominator of the
result.d
fraction is reduced.
Step 5: Convert the resulting improper fraction back to a mixed fraction
The last step involves converting the improper fraction back to mixed form. To do that, we will repeat the same steps that we did for procedural programming. This time, instead of variables, we will access and update the structure variable’s members.
void mixedFraction(MixedFraction &frac)// Convert to Mixed Fractionfrac.w = frac.n / frac.d;frac.n = frac.n % frac.d;}
Line 1: A fraction is passed by reference to the
mixedFraction()
function.Line 3: The fraction’s whole number is updated by dividing its numerator by its denominator.
Line 4: The fraction’s numerator is updated by taking the remainder of its numerator with its denominator.
Printing the resulting mixed fraction
To print the resulting mixed fraction, we create a new helper function called print()
.
void print(string msg, MixedFraction frac){cout<< msg <<" = " ;if(frac.sign==-1)cout << "-";cout<< frac.w << "<" << frac.n << "/" << frac.d << ">\n";}
How exactly the code works is not important in the context of this lesson. So, we can just copy and paste this into our use case.
Complete code
This is what the complete working code looks like:
#include <iostream>using namespace std;struct MixedFraction{int sign; // either will be +1 or -1int w;int n;int d;};//GCD helper functionint gcd(int a, int b){while (a % b != 0){int r = a % b;a = b;b = r;}return b;}//Print helper functionvoid print(string msg, MixedFraction frac){cout<< msg <<" = " ;if(frac.sign==-1)cout << "-";cout<< frac.w << "<" << frac.n << "/" << frac.d << ">\n";}//Step 1: Convert to Improper Fractionsvoid improperFraction(MixedFraction &frac){frac.n = (frac.d) * (frac.w) + (frac.n);frac.w = 0; // frac.sign / frac.d are unchanged}//Step 2: Make the Improper Fractions' denominator samevoid sameDenominator(MixedFraction &L, MixedFraction &R){// 1: left n = left n * right dL.n = L.n * R.d * L.sign;// 2: left n = right n * left dR.n = R.n * L.d * R.sign;// 3: Denominator calulationL.d = R.d = L.d * R.d;}void computeSign(MixedFraction & frac){if (frac.n < 0)frac.sign = -1;elsefrac.sign = +1;frac.n = abs(frac.n);frac.n = abs(frac.n);}//Step 5: Convert back to Mixed fractionvoid mixedFraction(MixedFraction &frac) // assume frac is an improper fraction{// Convert to Mixed Fractionfrac.w = frac.n / frac.d;frac.n = frac.n % frac.d;}//ADDITION OPERATIONMixedFraction add(MixedFraction L, MixedFraction R){MixedFraction result{};//Step 1: Convert to Improper FractionimproperFraction(L);improperFraction(R); // L.w and R.w are 0's now//Step 2: Making d samesameDenominator(L,R);//Step 3: Compute resulting fractionresult.n = L.n + R.n;result.d = L.d;computeSign(result);//Step 4: Reduce the fraction (GCD)int res = gcd(result.n, result.d); // numerator could be -veresult.n /= res;result.d /= res;//Step 5: Convert to Mixed Fraction & PrintmixedFraction(result);return result;}int main(){MixedFraction result= {}; //resulting fractionMixedFraction left = {-1, 2, 1, 2}; //left fractionMixedFraction right = {+1, 0, 1, 3}; //right fractionresult = add(left,right);print ("Left", left);print ("Right", right);print ("Left + Right", result);return 0;}
The code is much cleaner, easier to understand, and overall more efficient. Play with the above code and see how the addition function works.
Note: Someone might raise an objection, attributing the simplicity in the code to the introduction of an extra variable,
sign
, within the struct. The question that might arise is: why wasn't the same efficiency observed in the case of other primitive data types? However, it's essential to bear in mind that programmers typically aim to minimize the number of variables for ease of management. In contrast, withstruct
, data is already grouped, negating the need to handle multiple parameters. This advantage grants programmers the leverage to make more straightforward decisions, capitalizing on the power ofstruct
's organized data, which we have successfully utilized in our implementation as well.