02 - member functions
Classes, just like member variables (fields) can also contain member functions (methods).
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 |
#include <iostream> // (greatest common divisor) // if you have C++17, you can remove this function and use std::gcd from <numeric> int gcd(int a, int b) { if (b == 0) return a; else return gcd(b, a % b); } class fraction { public: int numerator; int denominator; void simplify() { const int n = gcd(numerator, denominator); numerator /= n; denominator /= n; } void print() { std::cout << numerator << "/" << denominator << "\n"; } }; int main() { fraction fr1{2, 6}; fraction fr2{5, 10}; fr1.print(); fr2.print(); fr1.simplify(); fr2.simplify(); fr1.print(); fr2.print(); } |
Output:
Notice few things:
Member functions, just like member variables are defined inside the class.
Member functions have innate ability to access member variables - as if all member variables were always passed as arguments.
Because member functions are tied to a specific class, they can not be called like global functions. They need to be called on a specific object, and this is done using
.
operator.
If you would like to split the code above into files:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 |
// header class fraction { public: int numerator; int denominator; void simplify(); void print(); }; // source // notice that member function definitions use class_name:: // this informs the compiler about access to members void fraction::simplify() { const int n = gcd(numerator, denominator); numerator /= n; denominator /= n; } void fraction::print() { std::cout << numerator << "/" << denominator << "\n"; } |
For simplicity, since majority of examples in the tutorial are single-file they will contain function definitions inside the class definition.
Invariants
Thanks to member functions, the code is now more object-oriented but it still contains some flaws. What happens if we do something like this?
1 2 3 4 5 6 |
fr1.numerator = 2; fr1.denominator = 0; fr2.numerator = 0; fr2.denominator = 0; fr1.simplify(); // (1) fr2.simplify(); // (2) |
Answer
For (1), the fraction will be simplified to 1/0 because
gcd(2, 0) == 1
.For (2), the code will invoke undefined behavior because
gcd(0, 0) == 0
and integer division by 0 is undefined.
In both cases, the intial problem is that fraction invariant was broken - the denominator can not be zero. No matter how well member functions treat the data, we can always use the .
operator to access and modify class members, potentially breaking some conditions on which the methods operate.
Can we protect the code from breaking invariants? Yes. For this, we will need to restrict the access to some class members.